CNA-BUG-097: RenderPipeline::releaseDeviceResourcesEXT() keeps the Bloom, SSAO and volumetric-fog target pools, and the memory estimate never counts them
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.
The DeviceReset handler documented to drop every target the pipeline owns resets only the scene target and the chain's pool; the private pools of BloomPass, SsaoPass and VolumetricFogPass survive, and getGpuMemoryEstimateBytes() excludes them.
- Identifier
CNA-BUG-097- 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::releaseDeviceResourcesEXT() and getGpuMemoryEstimateBytes() / FrameStatistics::gpuMemoryEstimateBytes (CNAEXT)
Expected behaviour
RenderPipeline.hpp: releaseDeviceResourcesEXT() "Drops every target the pipeline owns, so the next frame allocates fresh ones" and is called automatically on DeviceReset, because "after a context loss every GPU object the pipeline held names storage the driver has already destroyed"; getGpuMemoryEstimateBytes() "Returns an estimate of the GPU memory the pipeline's own targets occupy".
Actual behaviour at TARGET
releaseDeviceResourcesEXT() resets sceneTarget_ and chain_.getTargetPool() and zeroes the statistics. BloomPass, SsaoPass and VolumetricFogPass, all owned by the pipeline, allocate from private RenderTargetPool pool_ members that the reset path does not touch (resize() resets the Bloom and SSAO pools only; the fog volume has a fixed size). getGpuMemoryEstimateBytes() sums the chain pool and the scene target only, so a frame with bloom, SSAO or volumetric fog under-reports, and after a reset the estimate reads 0 while those targets are still allocated.
Source locations
modules/graphics-ext/src/RenderPipeline.cpp— RenderPipeline::releaseDeviceResourcesEXT, getGpuMemoryEstimateBytes, resize and the DeviceReset subscriptionmodules/graphics-ext/include/CNA/Graphics/RenderPipeline.hpp— releaseDeviceResourcesEXT and getGpuMemoryEstimateBytes documentationmodules/graphics-ext/src/BloomPass.cpp— BloomPass::resetTargets and pool_ allocationsmodules/graphics-ext/src/SsaoPass.cpp— SsaoPass::resetTargets and pool_ allocationsmodules/graphics-ext/include/CNA/Graphics/VolumetricFogPass.hpp— private RenderTargetPool pool_ with no reset entry pointmodules/graphics-ext/tests/CNA/Graphics/DeviceLifecycleTests.cpp— DeviceLossTest and MultiDeviceTest assert an estimate of 0 after a reset
Evidence
Checked by reading at 009d40f5; not executed. DeviceLossTest.EverySubsystemSurvivesAResetAndRendersAgain and MultiDeviceTest.AResetOnOneDeviceDoesNotDisturbTheOther assert getGpuMemoryEstimateBytes() == 0 after a reset; they pass because the estimate omits the pass pools, not because those pools were released. What a kept pooled target does after a real loss depends on the renderer and was not established: WebGPU's DebugRestoreContext rebuilds every registered live resource (RecreateAfterDeviceLossEXT), which would keep such targets usable there, whereas the pipeline's documentation assumes they are dead.
Focused reproduction
// Illustrative; not compiled or run for this entry. HiDef device on a renderer that supports the passes.
CNA::Graphics::RenderPipeline pipeline(device);
pipeline.resize(256, 256);
pipeline.getSettings().setBloomEnabled(true);
pipeline.begin(Color::Black);
pipeline.end();
std::size_t before = pipeline.getGpuMemoryEstimateBytes(); // excludes BloomPass's own targets
pipeline.releaseDeviceResourcesEXT();
std::size_t after = pipeline.getGpuMemoryEstimateBytes(); // 0, while BloomPass still holds them
Current tests
DeviceLifecycleTests.cpp (DeviceLossTest.*, MultiDeviceTest.*), PipelineDiagnosticsTests.cpp (PipelineDeviceResetTest.*) and the passes' own suites (BloomPassTests.cpp, SsaoPassTests.cpp, VolumetricFogPassTests.cpp). None observes the pass pools through the pipeline.
Regression test
Have releaseDeviceResourcesEXT() reset the Bloom, SSAO and volumetric-fog pools (adding a reset to VolumetricFogPass), include those pools in getGpuMemoryEstimateBytes(), and extend DeviceLossTest to enable bloom and fog and assert that the estimate before a reset exceeds the chain's alone and is 0 afterwards.
Blast radius
CNAEXT RenderPipeline users with bloom, SSAO or volumetric fog enabled: memory statistics read low, and after DeviceReset those three passes keep their previous targets. The reset half is reachable only on renderers that raise DeviceReset and can run these passes. XNA-only builds are unaffected.
Workaround
After a device reset, dispose and recreate the RenderPipeline rather than relying on its reset handler.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- CNAEXT engine layer: limits
- Architecture
- Graphics architecture: device loss
- Known issues
- Bug index