Relates to: render-graph-redesign.md, pso-cache-architecture.md, gpu-allocator-rearchitecture.md
Scope: The current frame path from editor input to presentation. This document describes the implemented ZUI and render-graph path; it is not the retired ImGui design.
The engine has a main thread and a render thread. During runtime, Vulkan command recording, queue submission, and presentation run on the render thread; startup resource creation completes before that split. The main thread owns events, simulation, editor logic, and ZUI tree construction.
MAIN THREAD RENDER THREAD
----------- -------------
Poll window and input take newest ready frame state
tick world, imports, schedulers apply a newer viewport resize, if any
update application and camera BeginFrame: acquire + upload/retirement work
build ZUI tree when its token/slot is available take newest ready overlay or retain the last one
publish state and, independently, overlay ---> RenderScene + RenderGraph::Execute
EndFrame: submit + present
AppRenderPipeline has two independent three-slot handoffs. RenderFrameState
is a latest-state channel: its producer claims a free slot or replaces the oldest
unread ready slot; the consumer claims the newest ready slot and frees older ready
slots. If every slot is being written or read, publication is skipped for that
iteration. This deliberately favours current camera/resize state over a FIFO queue.
OverlayPayload uses a separate three-slot channel and a presentation-paced build
token. The render thread retains its last readable overlay until a newer one is
available, then releases the old slot. Its per-slot arena is cleared only when a
later writer claims that slot, so draw data remains valid while it is recorded.
RenderFrameState copies camera data, sky configuration, celestial-light data,
sky revision, render-target extent, resize sequence, and the overlay-enabled flag.
It currently carries a borrowed RenderScene*; only the sky configuration has a
separate immutable copy. RenderScene::GetInstancesSnapshot() protects the
instance list at draw preparation time, but an engine-owned immutable whole-scene
render snapshot is still required before the render thread can be said to consume
only immutable scene input.
For every non-minimized window frame, Engine::MainThreadRun() performs the following work.
- Poll window events, tick the VFS watcher, run fixed world steps, progress imports, and drain main-thread callbacks.
- Call
GameApplication::Update(dt). It polls input, calls the application update, then updates the camera controller. - The production
EditorSession, asynchronous object-ID picking, and transaction mutation point described in the editor design documents are not implemented yet. - When overlay rendering is enabled, call
AppRenderPipeline::BeginOverlayFrame(dt),GameApplication::OnRenderUI(), andEndOverlayFrame(). BeginOverlayFrame()updates the ZUI context with the logical GLFW window size and the physical/logical framebuffer scale. That keeps GLFW cursor coordinates, ZUI layout, and high-DPI scissor conversion in the same coordinate system.- Tetragrama builds the editor through
ZUILayer:ZUIPanelManagerComponent, the dockspace shell, status bar, and panels such asViewportPanel. This replaces the old ImGui component hierarchy. FillOverlayPayload()walks the completed ZUI box tree and writes its draw list into the arena assigned to the overlay slot.SyncHierarchy,SyncECSToRenderScene, andSyncECSToLightsupdate the live render scene.GameApplication::PrepareScene()drains pending viewport-resize requests, keeps the final extent, and captures camera data.- Publish the frame state; publish an overlay separately when one was built.
ViewportPanel reads GraphicRenderer::GetFrameOutput() and emits an image box using its bindless texture index. The handle is published by the render thread after a real graph execution, using an index/generation pair guarded by a sequence counter. There is intentionally no output handle during renderer initialization: before ZUI declares its read of FrameColor, graph culling may legitimately leave that transient resource unallocated.
The render thread waits until it has received a frame state. Its frame lifecycle is:
if frame state contains a newer resize:
AppRenderPipeline::ResizeRenderTarget()
BeginFrame()
flush shader reload and asynchronous pipeline creation
acquire a swapchain image
RenderResourceManager::BeginFrame(frame index)
collect swapchain async operations and texture releases
reset command pools and retire completed texture-upload slots
complete texture deferrals
begin the application-owned primary graphics command buffer
RenderScene(camera, borrowed scene, copied sky state, retained ZUI payload)
rebuild per-frame scene input and upload it
set ZUIPass payload
GraphicRenderer::DrawScene()
RenderGraph::Execute()
clear ZUIPass payload
EndFrame()
finish deferred resource-manager batch work
transition an otherwise-unused acquired image back to present, if necessary
enqueue/end command buffers
present the swapchain image
submit deferred asynchronous uploads
An invalid acquired frame skips RenderScene() but still runs EndFrame() so the swapchain lifecycle remains balanced.
AppRenderPipeline::RenderScene() snapshots render-scene instances, resolves resident mesh allocations, calculates world-space bounds, extracts the camera frustum, and fills per-frame arrays for transforms, sub-mesh draw data, and frustum-culling input. Non-resident mesh handles are requested for streaming and are omitted until available.
The pipeline uploads those arrays along with the light array. GraphicRenderer::DrawScene() uploads materials and pushes UBOCameraLayout into the active frame heap, retaining the resulting dynamic-uniform offset in SceneData. The compute culling pass writes the indirect-draw buffer consumed by the depth pre-pass and G-buffer pass.
GraphicRenderer installs the persistent callback passes below. Each RenderGraph::Execute() registers the frame's virtual resources, validates declarations, culls dead work, builds a topological and queue schedule, allocates/reuses transient resources, derives synchronization2 barriers, compiles/binds passes, and records/submits the graph batches. Recordable dependency levels can use worker secondary command buffers; the final graphics batch remains available to the application primary buffer.
| Pass | Main inputs | Main result |
|---|---|---|
| Frustum Culling | culling input and indirect buffer | culled indirect commands |
| Depth Pre-Pass | global geometry, transforms, draw data, culled indirect commands | FrameDepth |
| G-Buffer | global geometry, transforms, draw data, materials, bindless textures, depth | albedo/AO, normal/roughness, metallic/emissive |
| Lighting | G-buffer textures, depth, lights, camera | sampled-capable FrameColor |
| Environment background | optional HDRI environment map, depth, FrameColor |
FrameColor loaded and extended when HDRI or fallback presentation is active |
| Editor overlays | depth and post-tone-map scene color | selected/hovered outlines, grid, gizmo, and optional editor IDs |
| ZUI Draw | final scene/editor color, ZUI geometry, bindless texture array | acquired swapchain image |
The environment-background pass registers conditionally from the selected SkyEnvironment snapshot. Editor overlays register from immutable editor/render snapshots: grid settings, selected/hovered draw proxies, gizmo state, and optional object-ID work. The ZUI pass declares its final scene/editor-color read and its swapchain write, making it the graph's presentation side effect and retaining the scene-color producer. When no ZUI draw geometry exists, its execution is empty; EndFrame() still ensures the acquired image is ready for presentation.
The graph owns resource state transitions and inter-pass synchronization. Individual callback passes own their draw body and use the resolved framebuffer/resource bindings supplied by the graph. FrameColor is recreated at the editor viewport extent, not the window/swapchain extent, and is sampled by ZUI through the global bindless texture array.
ViewportPanel layout changes
-> push requested viewport extent into ApplicationState queue
-> PrepareScene drains queue and retains final extent
-> `RenderFrameState` crosses the latest-state channel
-> render thread calls RenderGraph::Resize(width, height)
-> next graph execution allocates/binds resized transient targets
-> GraphicRenderer publishes the allocated FrameColor handle
-> following ZUI build consumes that handle as its image texture
Requests are coalesced before they cross the mailbox, so intermediate panel extents from a dock/window drag do not produce a separate resize call. RenderGraph::Resize() is intentionally a slow path: it waits for the Vulkan device to be idle before replacing image/framebuffer backing and schedules old Vulkan objects for deferred destruction in valid lifetime order. A window/swapchain recreation does not force the viewport targets to the window extent; the panel controls the viewport extent.
GraphicRenderer publishes only valid physical FrameColor handles after RenderGraph::Execute(). It then queues the bindless descriptor update. This ordering matters: a UI image must never use the old setup-time handle for a graph resource that was culled or recreated.
RenderResourceManager owns buffer, texture, geometry-streaming, and descriptor-update work.
| Resource class | Current path |
|---|---|
| Host-visible per-frame buffers (transforms, draw data, lights, materials, ZUI vertices/indices) | vmaCopyMemoryToAllocation, plus a flush when memory is not coherent |
| Device-local buffer with available ring space | copy through the mapped staging ring, record/submit a graphics copy, then retire the ring chunk against the render timeline |
| Device-local buffer when the ring cannot serve it | create a staging buffer and join the resource manager's deferred batch |
| Texture uploads | deferral/streaming work completed at frame boundaries and submitted through the asynchronous upload queues |
| Texture descriptors | queued by producers and flushed after graph registration, before commands consume bindless textures |
BeginFrame() advances streaming, compaction, pending mesh swaps, and texture reload/release work. CompleteDeferrals() processes texture data that could not claim an upload slot earlier. EndFrame() finishes a deferred batch, and SubmitAsyncUploads() submits the resulting asynchronous upload operations after presentation has prepared the frame's waits.
- A render-target resize calls
vkDeviceWaitIdle; it is correct and coalesced, but intentionally not a hitch-free resize path. - The mapped-ring device-local
UpdateBuffer()path performs a fence-synchronized graphics copy. It is appropriate for the current loading/update cadence but should not be mistaken for a fully asynchronous high-frequency streaming path. - ZUI has fixed per-frame GPU capacities (65,536 vertices and 131,072 indices). Excess draw data is clipped for that frame rather than growing GPU buffers while rendering.
- The viewport texture reaches the UI through the independent state/overlay handoffs, so a newly allocated or resized viewport becomes visible after a subsequent UI build. This is normal frame pipelining, not a stale-handle path.
main-thread render state + ZUI build --separate slots--> ZUI Draw Pass --swapchain write--> present
^
| final editor-overlay color
Frustum Culling --> Depth --> G-Buffer --> Lighting --> [Environment Background] --> [Editor overlays]
The graph supplies the actual scheduling, resource lifetimes, aliases, barriers, and queue waits; the sketch only expresses the default data flow.