Designing the Rendering Architecture
RenderModule, Dear ImGui and the First Editor Layer
In the previous STAGE VK development update, the Vulkan backend reached the point where it could acquire a swapchain image, record GPU commands, submit them to the graphics queue and present the result to the window.
The rendered result was still intentionally simple: a solid colour.
However, reaching that point exposed the next architectural problem.
VulkanModule was responsible for both managing the Vulkan frame lifecycle and deciding what should be rendered. That was acceptable while the only rendering operation was a clear command, but it would not scale well once the engine started introducing rendering passes, UI, geometry, lighting or post-processing.
The goal of this stage was therefore to separate the low-level Vulkan backend from the higher-level renderer and create a cleaner foundation for future rendering systems.

Separating the Vulkan Backend from the Renderer
The first important change was introducing RenderModule.
Before this refactor, VulkanModule controlled the complete frame and also contained the code that cleared the swapchain image.
After the refactor, its responsibility became more specific. VulkanModule is responsible for how a Vulkan frame is executed.
It handles:
swapchain image acquisition,
synchronization,
command-buffer preparation,
GPU submission,
and presentation.
RenderModule, on the other hand, is responsible for what rendering work is recorded into that frame.
This distinction is important because the renderer will eventually contain many independent stages. Geometry rendering, lighting, shadows, post-processing and UI should not become responsibilities of the low-level Vulkan backend. The dependency is now clearer: VulkanModule provides the Vulkan environment required to execute GPU work, while RenderModule coordinates the renderer that uses that environment.
The New Vulkan Frame Lifecycle
The frame lifecycle is now divided between the backend and the renderer.
At the beginning of the frame, VulkanModule::PreRender() waits for previous GPU work when necessary, acquires the next swapchain image, resets the command buffer and begins recording.
Importantly, the command buffer is left open. That allows RenderModule and its rendering passes to record their own Vulkan commands. Once all rendering work has been recorded, VulkanModule::PostRender() ends the command buffer, submits it to the graphics queue and presents the finished swapchain image.
This means VulkanModule controls the lifetime of the Vulkan frame without needing to know what was actually rendered inside it.

Moving the Render Target into RenderModule
The next step was moving the main render target logic out of VulkanModule.
Previously, the swapchain image was cleared manually using vkCmdClearColorImage.
That worked, but future graphics rendering would require a proper graphics render pass and framebuffers.
RenderModule now owns:
the main VkRenderPass,
one VkFramebuffer per swapchain image,
and the logic used to begin and end the main rendering scope.
The visible result is still the same colour clear, but internally the frame now follows the structure expected by future graphics passes.
This was also a useful point to clarify an important naming difference.
A Vulkan VkRenderPass is a Vulkan API object describing how attachments are used during rendering.
An engine rendering pass, such as ImGuiPass or a future GeometryPass, is a higher-level engine component responsible for a particular rendering stage.
They are related concepts, but they are not the same abstraction.
Handling Swapchain-Dependent Resources
Moving framebuffers into RenderModule introduced another problem.
A framebuffer is created using swapchain image views. If the swapchain is recreated, those image views change, which means the old framebuffers are no longer valid. RenderModule therefore needs to know when the swapchain is about to be destroyed and when the new one has been created.
A simple solution would have been to make VulkanModule call RenderModule directly.
However, that would create an undesirable dependency from the low-level Vulkan backend to the higher-level renderer.
To avoid that dependency, STAGE VK now uses a small listener interface called IVulkanSwapchainListener.
Interface and Observer / Listener Pattern
An interface in C++ acts as a contract.
IVulkanSwapchainListener defines two operations:
BeforeSwapchainRecreate()
AfterSwapchainRecreate()
Any class that implements this interface promises that it knows how to react to those events.
RenderModule currently implements this interface.
VulkanModule does not need to know that the registered object is specifically a RenderModule. It only knows that the object implements the IVulkanSwapchainListener contract.
This is an example of the Observer / Listener design pattern.
VulkanModule acts as the event source. It owns the swapchain lifecycle and keeps a list of registered listeners.
When the swapchain must be recreated, it notifies those listeners before destroying the old swapchain and again after creating the new one.
The main benefit is decoupling.
The Vulkan backend can announce that something important has changed without depending directly on renderer-specific classes.

The current flow is simple.
Before recreation, RenderModule releases resources that depend on the old swapchain.
VulkanModule then recreates the swapchain.
Afterwards, RenderModule rebuilds its render pass and framebuffers against the new swapchain.
This keeps ownership clear: Vulkan resources are recreated by the systems that actually own them.
RenderModule as the Renderer Orchestrator
RenderModule now acts as the orchestrator of the renderer.
Its job is not to implement every rendering technique itself.
Instead, it coordinates rendering passes, decides when they execute and provides the resources they need.
A useful way to think about it is that RenderModule is the conductor, while the rendering passes are the musicians.
The conductor determines the order and coordinates the frame, while each pass performs its own specific rendering work.
For example, future passes may include geometry, lighting, shadows or post-processing.
Not every pass needs to react to swapchain recreation.
Only passes that depend on swapchain-related resources, such as the window size, image format, image views or a render pass recreated with the swapchain, need to be updated.
A shadow-map pass using its own fixed-size depth texture, for example, may not need to react to a window resize at all.
Introducing ImGuiPass
The first real rendering pass introduced into this architecture is ImGuiPass.
Dear ImGui requires both platform integration and renderer integration.
In STAGE VK, ImGuiPass handles the technical side of that integration.
It is responsible for:
creating the ImGui context,
initializing the GLFW backend,
initializing the Vulkan backend,
managing the descriptor pool required by ImGui,
starting a new ImGui frame,
and recording ImGui draw data into the active Vulkan command buffer.
The important architectural point is that ImGuiPass does not define the editor itself.
Its responsibility is only to answer:
How does ImGui become Vulkan rendering commands?
The actual editor windows are created elsewhere.
[Suggested image: sequence diagram showing RenderModule starting the ImGui frame, EditorModule building widgets, ImGui::Render producing draw data and ImGuiPass recording that draw data into the Vulkan command buffer.]
ImGui and Swapchain Recreation
ImGui also introduced another swapchain-related dependency.
The Vulkan backend used by ImGui creates a graphics pipeline that is compatible with the current VkRenderPass.
Because the main render pass is recreated together with the swapchain-dependent render targets, ImGui must also update its Vulkan resources.
ImGuiPass does not register directly as a swapchain listener.
Instead, RenderModule receives the swapchain recreation event and then coordinates ImGuiPass.
This is another example of the orchestration role of RenderModule.
If the swapchain image count remains unchanged, the ImGui graphics pipeline can simply be rebuilt against the new render pass.
If the image count changes, the ImGui Vulkan backend must be initialized again because it maintains resources associated with those swapchain images.
This keeps the low-level Vulkan backend unaware of ImGui while still allowing the renderer to update it correctly.
A More Structured Module Lifecycle
Introducing RenderModule and EditorModule also required defining a clearer execution order between modules.
The application currently processes modules through three rendering stages:
PreRender
Render
PostRender
PreRender and Render execute in module order.
PostRender executes in reverse order.
This creates a scope-like lifecycle.
VulkanModule begins command-buffer recording first.
RenderModule starts the ImGui frame and opens the main render pass.
EditorModule then builds its interface.
During the reverse PostRender stage, RenderModule records the final ImGui draw data and closes the render pass.
Finally, VulkanModule ends the command buffer, submits it to the GPU and presents the image.
This ordering is important because higher-level systems must finish using Vulkan resources before lower-level systems close or submit them.
Introducing EditorModule
Once ImGui rendering was working, the first test was the standard ImGui Demo Window.
Initially, that call lived inside RenderModule.
It worked technically, but it mixed two different responsibilities.
RenderModule should know how UI rendering fits into the frame.
It should not know what editor windows exist.
That responsibility now belongs to EditorModule.
EditorModule defines the content of the editor, such as menus and future tools.
ImGuiPass handles how that UI is rendered through Vulkan.
This separation will become increasingly useful as the editor grows to include systems such as:
Console
Performance tools
Hardware information
Window settings
Hierarchy
Inspector
Scene View

Current Editor State
The editor is still intentionally small.
It currently contains a main menu and can display the Dear ImGui Demo Window.
Docking support is enabled, but the fullscreen dockspace is temporarily disabled.
The reason is architectural.
The scene is still rendered directly into the swapchain image. A fullscreen ImGui dockspace would therefore cover the rendering behind it.
A proper editor layout will make more sense once the scene renderer produces an off-screen texture that can be displayed inside an ImGui Scene View.
At that point, the editor can use a full docking layout while keeping the rendered scene visible inside its own panel.
Current Architecture
The engine now has three clearly separated layers.
VulkanModule is the low-level Vulkan backend. It controls the Vulkan frame lifecycle and submits work to the GPU.
RenderModule is the renderer orchestrator. It owns renderer-level resources and coordinates rendering passes.
EditorModule defines the editor interface and tools.
Inside the renderer, ImGuiPass provides the technical bridge between Dear ImGui and Vulkan.
This separation gives each system a much clearer responsibility and creates a foundation that can grow without pushing every new feature into VulkanModule.
Result
Visually, this stage is still modest.
The engine continues to clear the main render target with a solid colour, with Dear ImGui rendered on top.

Architecturally, however, STAGE VK has changed significantly. The Vulkan backend no longer decides what the frame contains. It prepares the frame, provides the command buffer, submits the recorded work and presents the result.
RenderModule coordinates the rendering work recorded inside that frame.
Rendering passes implement individual rendering stages. EditorModule defines the tools and interface that make up the editor.
The most important lesson from this stage is that building a renderer is not only about adding new visual effects.
A large part of engine architecture is deciding which system owns each responsibility, how those systems communicate and how dependencies are kept under control.
With that foundation in place, future rendering passes and editor tools can now be added without turning the Vulkan backend into a monolithic system.



Comments