CNA-BUG-098: A throwing shadow-caster or transparent-phase callback leaves RenderPipeline and its ShadowMap in an unrecoverable state

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

RenderPipeline::begin() raises frameOpen_ before running the shadow-caster callback between ShadowMap::begin and end, and end() lowers it before the transparent callback; an exception from either skips the cleanup that pass exceptions get.

Identifier
CNA-BUG-098
Category
Bug
Subsystem
Graphics & renderers
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
CNA::Graphics::RenderPipeline::begin()/end() with callbacks registered through setShadowScene and setTransparentScene (CNAEXT)

Expected behaviour

MOD-713 made the pipeline recover from an exception inside a post-process pass: PipelineExceptionSafetyTest.AThrowingPassLeavesNoTargetBoundAndTheNextFrameRendersNormally requires that no target stays bound and that "a throw is not a one-way door into a broken object", and AFailedFrameDoesNotLeaveTheFrameOpen notes that CubeShadowMap and ShadowMap once had the same trap at their own begin(). The two caller callbacks are the same kind of caller code running inside a frame.

Actual behaviour at TARGET

In RenderPipeline::begin, frameOpen_ is raised first; with shadows enabled, shadowMap_->begin(...), the caster callback and shadowMap_->end() then run unguarded. If the callback throws, the shadow map's target stays bound, the ShadowMap stays open (its next begin throws "a shadow pass is already open") and the pipeline's frame stays open (its next begin throws "a frame is already open"); calling end() to recover runs the transparent phase and the chain for a frame whose scene target was never bound. In RenderPipeline::end, frameOpen_ is lowered first and drawTransparentPhase() runs before SetRenderTarget(nullptr): a throwing transparent callback leaves the scene target bound and the DepthRead/NonPremultiplied state (or the order-independent accumulation targets) in place, and the next Present refuses while a render target is bound.

Source locations

Evidence

Checked by reading at 009d40f5; not executed. Both existing exception-safety tests throw from a user post-process pass, which runs after the unbind in end(); no test throws from either callback.

Focused reproduction

// Illustrative; not compiled or run for this entry.
pipeline.getSettings().setShadowsEnabled(true);
pipeline.setShadowScene(&shadowMap, light, bounds, [] { throw std::runtime_error("caster"); });
try { pipeline.begin(Color::Black); } catch (const std::runtime_error&) {}
pipeline.begin(Color::Black);   // throws std::logic_error: a frame is already open

Current tests

PipelineDiagnosticsTests.cpp (PipelineExceptionSafetyTest.*, pass exceptions), RenderPipelineTests.cpp (TheShadowPassRunsBeforeTheSceneTargetIsBound, EachMissingIngredientLeavesTheShadowPassUnrun) and TransparentPhaseTests.cpp; none throws from a callback.

Regression test

Two cases beside PipelineExceptionSafetyTest: a caster callback that throws once, followed by a normal begin()/end() that must succeed with no target bound and a reusable ShadowMap; and a transparent callback that throws once, followed by assertions that no target is bound, default depth and blend state are back, and the next frame renders. The fix is a scope guard in each routine.

Blast radius

CNAEXT RenderPipeline users whose shadow-caster or transparent callbacks can throw (content loading, validation exceptions from draw calls). The shadow path leaves the pipeline and ShadowMap unusable until they are recreated; the transparent path leaves the frame bound to the scene target. Pass exceptions, and games without these callbacks, are unaffected.

Workaround

Catch exceptions inside both callbacks; after a callback failure, recreate the RenderPipeline and the ShadowMap.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index