Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

  1. 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.
  2. The instance enumerates available GPUs and picks one. The chosen adapter exposes the GPU's supported texture formats, limits, and features.
  3. 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.
  4. The window surface is configured with the GPU's preferred format (usually Bgra8UnormSrgb) and a present mode (Fifo for vsync, Mailbox for low latency).
  5. 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.
  6. A RenderGraph<World> is constructed with the transient textures and passes registered.
  7. The game's State::configure_render_graph() runs. The game can add custom passes, declare custom textures, or replace the default post-process chain.
  8. 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.

TextureFormatDescription
depthDepth32FloatMain depth buffer (reversed-Z, 0.0 = far)
scene_colorRgba16FloatHDR color accumulation buffer
compute_outputSurface formatPost-processed output before swapchain blit
shadow_depthDepth32FloatCascaded shadow map (8192x8192 native, 4096 WASM)
spotlight_shadow_atlasDepth32FloatSpotlight shadow atlas (4096 native, 1024 WASM)
entity_idR32FloatEntity ID buffer for GPU picking
view_normalsRgba16FloatView-space normals for SSAO/SSGI
selection_maskR8UnormSelection mask for editor outlines
ssao_rawR8UnormRaw SSAO before blur
ssaoR8UnormBlurred SSAO
ssgi_rawRgba16FloatRaw SSGI (half resolution)
ssgiRgba16FloatBlurred SSGI (half resolution)
ssr_rawRgba16FloatRaw screen-space reflections
ssrRgba16FloatBlurred SSR
ui_depthDepth32FloatSeparate 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.

  1. Sync data. Transform matrices, material uniforms, light data, and animation bone matrices are uploaded to GPU buffers.
  2. Process commands. Queued WorldCommand values run (texture loads, screenshots, anything else queued during the frame).
  3. Set the swapchain texture. The next swapchain image is acquired and bound as the external swapchain resource.
  4. Call State::update_render_graph() so the game can flip per-frame graph state (enabling or disabling passes, updating parameters).
  5. Execute the graph. Every enabled, non-culled pass runs in topological order. Each pass writes one command buffer.
  6. Submit. The command buffers go to the GPU queue.
  7. 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.