Repository navigation
Description
Problem
The two-phase GPU-driven HZB occlusion cull shipped in #431 (GPUDrivenOcclusionPass)
is Forward / Forward+ only. In the Deferred path, occlusion-culled instanced
static batches stay on the legacy single-phase cull: they are frustum + HZB tested
against the previous frame's pyramid at submission and drawn through the ScenePass
G-Buffer bucket. That is correct but not disocclusion-corrected — on fast camera
moves / disocclusions, an instance that was occluded last frame but is visible this
frame can pop for one frame, exactly the artifact the two-phase scheme removes for the
forward path.
The hardened occlusion shader (InstanceOcclusionCull.comp) is already shared, so the
deferred single-phase path benefits from the UE5.8-conformance work (point-block gather,
prev-transform reprojection); what's missing is the second pass (rebuild HZB from
this frame's depth → re-test the reject list) in the deferred context.
See docs/analysis/ue5.8-hzb-occlusion-analysis.md
§7 row 17 — the one remaining deliberate scope cut from #431.
Why it matters (Tier 2 — performance ceiling)
Deferred is the path the dense-instancing genre content (foliage / debris / props on
buildings + terrain) will actually run in. Bringing it to parity with the forward
two-phase removes the one-frame pop there too, and makes "HZB occlusion culling" a
path-agnostic claim rather than a Forward-only one.
Proposed approach
Extend GPUDrivenOcclusionPass (or a deferred sibling) to draw the phase-1 survivors
and phase-2 recoveries into the G-Buffer, mirroring the established
DeferredOpaqueDecalPass integration (the precedent for a pass that mutates the
G-Buffer between ScenePass and DeferredLightingPass):
- Hold the
GBuffer(per the pipeline wiring), targetGetFramebuffer()/
GetSamplingFramebuffer()correctly for the MSAA vs resolved cases. - Declare the G-Buffer attachment writes (albedo / normal / emissive + their MS
variants + velocity-MS + depth-MS, likeDeferredOpaqueDecalPass::Setup) so the
graph ordersDeferredLightingPassafter this pass and the hazard validator passes. - Build the mid-frame HZB from the G-Buffer depth; run
DispatchPhase2against it. - Re-export SceneDepth (depth-only is acceptable to start; the G-Buffer normal RT is
packed, so a normal re-export is a follow-up nicety). - Drop the
Path != Deferredgate on the submission reroute
(Renderer3D::SubmitGPUCulledInstanced); confirm deferred instanced batches resolve
to the G-Buffer shader (DrawMeshInstancedcurrently does not swap to
PBRGBufferShaderfor instanced — verify / fix as part of this). - Register the pass before
DeferredOpaqueDecalPass/DeferredLightingPassin the
deferred topology.
Spike first (confidence 0.5): prototype the G-Buffer draw + a deferred visual test
before committing — the MSAA / per-sample-resolve interaction and the deferred instanced
shader selection are the unknowns that can make this "tests green, screen wrong."
Acceptance criteria
- Two-phase occlusion runs in Deferred: phase-1 vs previous HZB → reject list → mid-frame
HZB → phase-2 recovery, drawn into the G-Buffer before lighting. - A deferred full-pipeline visual-evidence test (RendererAttachedTest,
Path = Deferred, dense instanced field behind an occluder) proves no corruption / no holes
and a measurable survivor reduction — mirroringOccludedInstanceFieldScene. - No regression to the forward two-phase path or to non-instanced deferred rendering;
render-graph hazard validation stays clean. - AO/SSR see the instanced depth in Deferred (depth re-export), matching the forward
Stage-3 behaviour.
Score
capability: 3
craft: 3
stability: 2
decay: 1
effort: 5
confidence: 0.5
learning: 3
fun: 3
kano: performance
blocked_by: []
blocks: []
Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.
Activity
coderabbitai commentedon Jun 30, 2026
🔗 Related PRs
#155 - Implement Depth Prepass and Occlusion Culling in Renderer [closed]
#203 - feat: Implement Jump Flood Algorithm for selection outlines in OloEditor [merged]
#416 - feat(renderer): consume per-object velocity buffer in motion blur [merged]
#465 - Fix/flaky fog visual evidence test [merged]
#479 - feat(renderer): CAS contrast-adaptive sharpening pass (
#432) [merged]🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
drsnuggles8 commentedon Jul 2, 2026
Implemented in PR #514 (commit 3ac6540, branch
feature/renderer-hzb-deferred).What shipped: a new
DeferredGPUOcclusionPassthat runs afterScenePassand beforeDeferredOpaqueDecalPass/ AO /DeferredLightingPass. Phase-1 draws stay on the normal ScenePass G-Buffer bucket (the phase-1 cull already ran vs the previous frame's HZB); the new pass rebuilds the HZB from this frame's resolved G-Buffer depth (occluders + phase-1 survivors), re-tests the reject list, draws the disoccluded instances into the G-Buffer, resolves MSAA (per-sample mode), and re-exports the G-Buffer attachments (SceneDepth / SceneNormals / GBuffer{Albedo,Normal,Emissive} / Velocity + MSAA companions) — mirroringDeferredOpaqueDecalPass. Registration order derives the correctSceneDepth/SceneNormals/GBufferAlbedoordering edges; verified hazard-free.Verification:
DeferredTwoPhaseOcclusionTest(CPU, pins the disocclusion decision math),OcclusionCullDeferredVisualEvidenceTest(real deferred pipeline on GPU, no false hole / no corruption vs the occlusion-off baseline). Full hazard + path-switch + forward-occlusion suite still green.docs/analysis/ue5.8-hzb-occlusion-analysis.md§7 row 17 updatedFollow-up filed separately: instanced statics in Deferred still select
DefaultForwardShader(pre-existing, unrelated to occlusion routing).