Repository navigation
Description
Summary
A RendererAttachedTest fixture that switches RendererSettings::Path to
RenderingPath::Deferred and drives a frame, when run immediately after
another test in the same process that also switched to Deferred, sees its
entire RenderGraph compile with zero declared reads/writes across all 37
passes — every pass (ScenePass, ShadowPass, DeferredLightingPass,
UICompositePass, ...) is marked culled, and ReadbackComposite() fails
because UICompositePass never produces its output framebuffer.
No GL errors, no warnings, no crash — the frame silently renders nothing.
Reproduction
Run the existing, unmodified OcclusionCullDeferredVisualEvidenceTest
(OloEngine/tests/Rendering/PropertyTests/OcclusionCullDeferredVisualEvidenceTest.cpp)
twice in the same process:
build\OloEngine\tests\Debug\OloEngine-Tests.exe --gtest_filter=*DeferredOccludedInstanceFieldScene* --gtest_repeat=2
- Iteration 1: passes. Log shows
RenderGraph BuildFrameGraph stats: passes=37, reads=69, writes=49, derivedEdges=35, orderSensitiveResults=10. - Iteration 2: fails. Log shows
RenderGraph BuildFrameGraph stats: passes=37, reads=0, writes=0, derivedEdges=3, orderSensitiveResults=0, every pass culled,ReadbackCompositereturns false.
No test-body changes are needed to reproduce — this is a pure re-entry issue with the render graph / Renderer3D process-wide state, not anything content-specific (same scene, same entities, same everything — just run twice).
Impact
This is a CI-flakiness risk for any two RendererAttachedTest-based visual-evidence tests that both switch to RenderingPath::Deferred and happen to run back-to-back in the same test binary — several already exist (OcclusionCullDeferredVisualEvidenceTest, and the SSGI/SSR/EASU evidence tests per RendererAttachedTest.h's own teardown comment referencing them). Today it just hasn't been triggered because gtest's registration/link order happens to keep them apart, but that's fragile — a new test, a reordered CMakeLists.txt, or --gtest_shuffle could trip it at any time.
Discovered while working on #515 (routing instanced statics to the deferred G-Buffer shader) — a new visual-evidence test (DeferredInstancedShadingEvidenceTest.cpp) landed right after OcclusionCullDeferredVisualEvidenceTest in one run and hit this, which is how it surfaced. It is unrelated to #515's shader-routing fix — the fix itself is verified correct (both tests pass reliably in isolation / as the first Deferred-switch in a run).
Suspected area
RendererAttachedTest::TearDown() already documents that a naive settings-struct comparison in ApplyRendererSettings can miss a stale graph left by a previous test ("the render graph's internal 'configured-for' state... A struct-level comparison can therefore read 'unchanged' while the graph was left reconfigured") and works around it by unconditionally reasserting ApplyRendererSettings() in teardown. That mitigation is evidently not sufficient — something in Renderer3D's process-wide state (Renderer3D::s_Data) survives a full render-graph rebuild (a fresh ConfigureRenderGraph + BuildFrameGraph call both show Adding graph node: ... for every pass) and still yields zero declared resources on the second Deferred entry. Likely candidates: the retained Hi-Z occlusion pyramid (OcclusionHZB/OcclusionHZBValid), FrameDataBufferManager/FrameResourceManager per-frame ring state, or some other per-process cache not reset between Scene lifetimes.
Suggested next steps
- Reproduce with the command above and get a debugger/instrumented build on the second iteration's
BeginScene/EndSceneto see which pass'sSetup()is the first to see an invalid blackboard resource (or whetherSetup()is being skipped for all passes entirely). - Compare
Renderer3D::s_Datastate after iteration 1's teardown vs. a truly fresh process to find what didn't reset. - Consider whether
RendererAttachedTest::TearDown()needs to force a harder reset (e.g. via a dedicatedRenderer3D::ResetPerSceneState()call) rather than relying solely onApplyRendererSettings().
Score
capability: 3
craft: 1
stability: 8
decay: 5
effort: 3
confidence: 0.8
learning: 5
fun: 5
kano: table-stakes
blocked_by: []
blocks: []
Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.
Activity
coderabbitai commentedon Jul 3, 2026
🔗 Related PRs
#263 - fix(tests): untangle Renderer3D test coupling for full-pipeline render tests (
#258) [merged]#465 - Fix/flaky fog visual evidence test [merged]
#514 - feat: Implement deferred two-phase occlusion culling [merged]
#529 - fix(renderer): lazily upload deferred font textures so headless-first-touch fonts render (
#520) [merged]#532 - fix(renderer): resize GTAO scratch textures with the FSR1 scene band [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 4, 2026
Root cause confirmed and fixed in #548.
What's happening:
ConfigureRenderGraph(path / AO-technique switch) rebuildsthe graph through
RenderGraph::ResetTopology(), which wipes the blackboard andimported-resource maps. But the
RenderPipelineblackboard-populate cache(
m_HasValidBlackboardCache/m_BlackboardFingerprint, process-wide ins_Data.Pipeline) is keyed only on a fingerprint of scene/settings inputs. Twoconsecutive Deferred entries of the same scene compute an identical
fingerprint, so
PopulateBlackboardshort-circuits past the just-wipedblackboard. Every pass's
Setup()then resolves empty handles,RGBuildersilently drops every
Read/Write, and all 37 passes cull →reads=0/writes=0,a blank frame with no GL error.
The fixture
TearDownreassert doesn't rescue it: it reconfigures to Forward butrenders no frame, so the cache retains the prior Deferred fingerprint for the
next test to collide with. That's why it's order-dependent flakiness.
Fix: couple the cache key to the event that invalidates it.
RenderGraphgets a monotonic
m_TopologyGeneration(+GetTopologyGeneration()) bumped byevery
ResetTopology()/Shutdown(), andComputeBlackboardFingerprinthashesit. Since that fingerprint feeds both the populate cache and the
BuildFrameGraphcache key, any reconfigure self-invalidates without enumerating call sites —
robust against a future reconfigure path that forgets an explicit hook.
Verification: the repro (
*DeferredOccludedInstanceFieldScene* --gtest_repeat=2)now passes both iterations (reads stay at 69). Added a CPU contract test
(
ResetTopologyAdvancesTopologyGenerationForCacheInvalidation) and a GPUregression that reproduces the reconfigure→reconfigure→rerender sequence in one
test body. Also documented the class of bug in
docs/agent-rules/render-pipeline-caches.md.Merge pull request #548 from drsnuggles8/feature/rendergraph-deferred…
fix(renderer): address #628 review — volumetric toggle in fingerprint…
fix(renderer): address CodeRabbit review on the shader debug draws (#725
feat(renderer): GPU-pushable shader debug draws (#725) (#749)
feat(rhi): Phase 7 — port the full render-pass suite to Vulkan (#691) (…
fix(renderer): three GL render bugs surfaced by the Vulkan port — dec…