OHAO · Implementation Monograph

Monograph · 05 · Deferred

05

Pipelines · deep dive

Deferred raster

Chapter contract

Walk one deferred frame as DeferredRenderer::render actually builds it: jitter, render graph, hybrid inject, lighting, post. Plus what the GBuffer stores and why.

Plates

GBuffer layout strip
Fig. GB-1

MRT packing layout (didactic — not a live channel dump).

Conceptual GBuffer
Plate GB-C1

Conceptual illustration — not engine output

Host wiring (before the graph)

VulkanRenderer::renderDeferred prepares the child every frame:

renderDeferred prelude
  1. Wait ring fence; copy staging → CPU pixel buffer
  2. setScene, camera view/proj/pos on DeferredRenderer
  3. If RT AS live → enable hybrid shadow path; pass TLAS pointer
  4. Pass env map view/sampler for IBL
  5. Pass VB/IB + meshBufferMap
  6. updateLightBuffer; optional sky/sun direction blend into fallback directional
  7. Begin command buffer → deferredRenderer->render(cmd, frameIndex)
  8. Copy final HDR/LDR output to staging; submit; advance frame

Pass graph (inside DeferredRenderer::render)

Graph build + execute
  1. TAA: fetch jitter; build jittered projection for GBuffer only
  2. Update pass uniforms (scene, light dir, camera, invViewProj)
  3. m_renderGraph.reset() then add passes with resource declarations
  4. CSM — cascade depth from unjittered camera (stable shadows)
  5. GBuffer — MRT + depth; finalLayout shader-read for colors
  6. SSAO — then bind result into lighting
  7. If TLAS instances > 0: RT shadow compute/trace pass (pos/normal/depth)
  8. If TLAS: RT GI (+ albedo)
  9. Deferred lighting — PBR + shadows + IBL + RT inputs
  10. Sky, optional SSS/SSR, bloom/TAA/tonemap post stack
  11. Optional GPU timestamp queries around passes
Why jitter GBuffer but not CSM

TAA needs sub-pixel phase in the color history. Cascaded shadows that jitter every frame shimmer. Split the projections.

Jargon · render graph

OHAO’s graph declares image reads/writes so barriers can be inserted between passes. Each pass still owns its VkRenderPass/framebuffer — hybrid of modern graph + classic Vulkan objects.

GBuffer write path

Vertex: skinned or static; push constants carry model, viewProj, prevMVP, material params, albedo, emissive indices. Fragment (gbuffer.frag):

gbuffer.frag essentials
  1. Sample albedo bindless if index valid
  2. Optional normal map via TBN
  3. ORM: ao*=R, rough*=G, metal*=B; rough = max(rough, 0.04)
  4. out0 = (worldPos, metallic); out1 = (octNormal, rough, emissiveY); out2 = (albedo, ao)
  5. velocity = 0.5 * (ndc_curr − ndc_prev)

Lighting consume path

deferred_lighting.frag reconstructs surface from GBuffer, loops lights (UBO, max 8), evaluates BRDF (Ch. 04), applies CSM and optional RT visibility, multiplies SSAO, adds IBL (equirect LOD ≈ 9·roughness) with analytical BRDF-LUT fallback when the real LUT is zeroed — so chrome is not pure black when only setEnvMap was used.

\[ L_o=\sum_k f(v,l_k)\,L_k\,(n\!\cdot\!l_k)\,V_k + L_{\mathrm{IBL}} \]

Limits

Not bugs

No multi-bounce room GI in pure deferred. Hybrid RT is a sparse inject, not full PT. SSS/SSR experimental. See Status.

deferred_renderer.cppGraph + inject
gbuffer_pass.* / gbuffer.fragMRT
csm_pass.*Cascades
deferred_lighting_pass.*PBR + IBL
post_processing_pipeline.*SSAO bloom TAA
render/graph/*Barriers
render_dispatch.cppHost frame

Design units in this module

Each card is a focused design page (what / how / why + sources). Full tree: Sitemap.