Rendering Architecture
This chapter is about how ECS state becomes pixels. The renderer is built on wgpu and the GPU work is orchestrated by a dependency-driven render graph. Game code writes to components, the renderer reads those components every frame, packs the data into GPU buffers, and submits a graph of passes that ends with a swapchain present.
High-Level Flow
ECS World State
|
v
Renderer (WgpuRenderer)
|-- Sync data: upload transforms, materials, lights to GPU buffers
|-- Prepare passes: each pass updates its bind groups and uniforms
|-- Execute render graph: run passes in dependency order
|-- Submit command buffers to GPU queue
|-- Present swapchain surface
|
v
Pixels on Screen
The Render Trait
The Render trait is the abstraction over the GPU backend. Game code never touches wgpu directly. It calls trait methods on whatever renderer the platform layer constructed.
#![allow(unused)] fn main() { pub trait Render { fn render_frame(&mut self, world: &mut World, state: &mut dyn State); fn resize(&mut self, width: u32, height: u32, world: &mut World); // ... additional methods for texture upload, screenshot, etc. } }
WgpuRenderer is the concrete implementation. It owns the wgpu device, queue, surface, and the render graph.
WgpuRenderer
WgpuRenderer is the bag of GPU state the engine carries across frames. The fields are:
- Instance, Adapter, Device, Queue are the wgpu initialization chain.
- Surface is the window's swapchain.
- RenderGraph is the dependency-driven frame graph with every pass.
- Resource IDs are handles to transient and external textures.
- Texture Cache is the set of uploaded GPU textures keyed by handle.
- Font Atlas is the glyph texture for text rendering.
- Camera Viewports are render-to-texture targets for editor viewports.
None of this is exposed to game code. Game code mutates the ECS, the renderer reads it.
Initialization
The startup chain is the standard wgpu bring-up plus some graph construction at the end.
- The instance is created with
wgpu::Instance. The backend is Vulkan on Linux and Windows, Metal on macOS, DX12 on Windows, WebGPU on WASM. The instance is the entry point to the GPU API. - The instance enumerates available GPUs and picks one. The chosen adapter exposes the GPU's supported texture formats, limits, and features.
- The adapter opens a logical device and a command queue. The device creates GPU resources. The queue is where command buffers go to execute. All GPU work flows through the queue.
- The window surface is configured with the GPU's preferred format (usually
Bgra8UnormSrgb) and a present mode (Fifofor vsync,Mailboxfor low latency). - Every built-in pass is constructed. Each pass owns its shader modules, pipeline layouts, render pipelines, bind group layouts, and any persistent GPU buffers. None of this is rebuilt per frame.
- A
RenderGraph<World>is constructed with the transient textures and passes registered. - The game's
State::configure_render_graph()runs. The game can add custom passes, declare custom textures, or replace the default post-process chain. - The graph compiles. Compilation builds dependency edges, topologically sorts the passes, computes resource lifetimes, decides which transients can alias, and works out per-attachment load and store operations.
Transient Textures
The renderer declares every intermediate texture at initialization time. The list is fixed, the sizes track the window, and the graph decides which ones can share memory.
| Texture | Format | Description |
|---|---|---|
depth | Depth32Float | Main depth buffer (reversed-Z, 0.0 = far) |
scene_color | Rgba16Float | HDR color accumulation buffer |
compute_output | Surface format | Post-processed output before swapchain blit |
shadow_depth | Depth32Float | Cascaded shadow map (8192x8192 native, 4096 WASM) |
spotlight_shadow_atlas | Depth32Float | Spotlight shadow atlas (4096 native, 1024 WASM) |
entity_id | R32Float | Entity ID buffer for GPU picking |
view_normals | Rgba16Float | View-space normals for SSAO/SSGI |
selection_mask | R8Unorm | Selection mask for editor outlines |
ssao_raw | R8Unorm | Raw SSAO before blur |
ssao | R8Unorm | Blurred SSAO |
ssgi_raw | Rgba16Float | Raw SSGI (half resolution) |
ssgi | Rgba16Float | Blurred SSGI (half resolution) |
ssr_raw | Rgba16Float | Raw screen-space reflections |
ssr | Rgba16Float | Blurred SSR |
ui_depth | Depth32Float | Separate depth for UI rendering |
External textures are provided fresh each frame. swapchain is the window surface texture. viewport_output is the editor viewport render target.
Per-Frame Rendering
render_frame() does seven things, in order.
- Sync data. Transform matrices, material uniforms, light data, and animation bone matrices are uploaded to GPU buffers.
- Process commands. Queued
WorldCommandvalues run (texture loads, screenshots, anything else queued during the frame). - Set the swapchain texture. The next swapchain image is acquired and bound as the external
swapchainresource. - Call
State::update_render_graph()so the game can flip per-frame graph state (enabling or disabling passes, updating parameters). - Execute the graph. Every enabled, non-culled pass runs in topological order. Each pass writes one command buffer.
- Submit. The command buffers go to the GPU queue.
- Present. The frame appears on the window.
Resize Handling
When the window resizes, the surface is reconfigured with the new dimensions, every transient texture is resized to match, the graph recomputes aliasing, and any pass that cached bind groups invalidates them. SSGI textures resize to half the new dimensions because they run at half resolution.
Custom Rendering
Two State methods let the game change the pipeline. configure_render_graph() runs once at startup. The game adds custom passes, declares custom textures, and changes the pipeline shape. update_render_graph() runs each frame. The game enables or disables passes and updates per-pass parameters.
For the details on the graph itself, see The Render Graph. For end-to-end custom-pass examples, see Custom Passes.