Graphics extension pipeline internals

CNA snapshot 009d40f5  ·  Development › Module internals  ·  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. Checked by reading modules/graphics-ext, the shader-package generator and its CMake registrations, and the module's git history between the earlier read and the snapshot; the 92 test files (967 test-macro definitions) were located, none executed. The test figures quoted from CNA's commit message for the clustered forward desktop variant are CNA's record, not ours. The shader-package, compute, shadow, clustered-lighting and PBR families and each individual effect remain unverified beyond their source layout.

modules/graphics-ext/ holds CNA-only rendering systems that sit above XNA's GraphicsDevice: a frame pipeline with post-processing, shader packages, compute resources, shadows, lighting, materials, transparency and a large set of effect passes. It is the implementation of the CNAEXT engine layer, and it is not a renderer: it selects no native graphics API and owns no operating-system surface. This page traces the central frame and pipeline path function by function, explains how the module's shader packages are authored, selected and checked (including the desktop GLSL variant of the clustered forward effect that this snapshot added), and maps the remaining families to their sources and tests without claiming to have traced them. The user-level reference is the CNAEXT engine layer guide; this page is for maintainers changing a pass, the pipeline or a shader.

The physical and compile-time boundary

modules/graphics-ext/CMakeLists.txt globs src/*.cpp (83 translation units at this snapshot) into cna_add_module(cna_graphics_ext GraphicsExt ...), the helper in modules/CMakeLists.txt that makes a STATIC library with the alias CNA::GraphicsExt and a public link to cna_build_config. The module links cna_graphics_core and the Sharp Runtime component Core.Base publicly and owns its own examples/ directory. Its public surface is 98 headers under include/CNA/Graphics/ (plus two internal ASCII helper headers under include/CNA/Internal/), which is the count the engine-layer guide uses.

CNA_CNAEXT is an OFF-by-default option. modules/CMakeLists.txt turns it into a compile definition on the shared cna_build_config interface target, which every module and every consumer inherits, so a header and the code it guards always see the same macro. The guard is total by rule: scripts/check_cnaext_guards.sh, registered as the CNAEXT_GuardDiscipline ctest (a text check that runs whenever tests are on, even with the option off), requires every production header and source of the module to carry #ifdef CNA_CNAEXT and a closing #endif // CNA_CNAEXT as the outermost construct, and the module's tests carry the same guard. The practical result is the classic trap: #include "CNA/Graphics/RenderPipeline.hpp" succeeds in a default build and declares nothing.

CNA::CnaExt is a separate INTERFACE umbrella over cna_graphics_ext and cna_devices_ext; the comment in modules/CMakeLists.txt records that it was named CNA::NoXna (and the static library cna_noxna) before the 2026-08 naming normalisation, and that the old implementation is now this module. The CNA aggregate lists cna_graphics_ext and cna_cnaext among its runtime parts, so the targets are always present; only the macro decides whether the guarded functionality exists. To change one extension, check four things independently: target presence, the compile define, the capability gate the subsystem asks the device, and which renderer is selected underneath. The module's ASCII post-process effect (AsciiPostProcessEffect, wrapped as AsciiPass) is a CPU glyph quantiser that migrated here from an earlier renderer-level implementation; it is an engine-layer post-process, not a selectable renderer identity.

Game owns GraphicsDevice [neutral; the selected renderer sits underneath]
  -> caller owns RenderPipeline(device)
     -> owns PostProcessChain (its own RenderTargetPool + a BlitPass copy pass) and 14 built-in pass objects
     -> owns the optional scene RenderTarget2D (Depth24Stencil8) and, created lazily, a WeightedBlendedTransparency
     -> BloomPass, SsaoPass and VolumetricFogPass each own a private RenderTargetPool
     -> borrows user passes, ShadowMap, Skybox, depth / normal / velocity textures and the two callbacks

begin:  optional shadow callback -> bind scene target or back buffer -> clear -> optional skybox
game:   draws its opaque scene
end:    transparent phase -> unbind scene target -> ordered passes (last writes the back buffer)

Why the pipeline settings are not reachable from GraphicsDevice

RenderPipelineSettings holds HDR, exposure and gamma, tonemapping, bloom, SSAO, SSR, depth of field, fog, FXAA, lens and motion-blur parameters, transparency mode, and shadow and quality settings. It has two routes into a frame. It is owned by a RenderPipeline and returned by RenderPipeline::getSettings(), where a change takes effect on the next frame. A pass run outside a pipeline can instead receive a standalone instance through the PostProcessContext::settings pointer, and null there means defaults (RenderPipeline.hpp, PostProcessContext.hpp). No accessor on GraphicsDevice returns it, and that is a deliberate application of the compile-time boundary described above. GraphicsDevice is an XNA type compiled in every build, while RenderPipelineSettings exists only when CNA_CNAEXT is on. A device member of that type would give an XNA type a member whose type exists only under a compile option.

An earlier header comment told readers to construct the settings "via GraphicsDevice::GetRenderPipelineSettings()". That method never existed. At this snapshot the comment in RenderPipelineSettings.hpp records the correction and the reason. Checked by reading at 009d40f5.

A frame through RenderPipeline, function by function

RenderPipeline.cpp is the file to read first, with its header RenderPipeline.hpp. RenderPipeline(GraphicsDevice&) stores a borrowed device reference, constructs its PostProcessChain and the 14 built-in passes, and subscribes a DeviceReset handler that captures this. The destructor removes that token, so the pipeline must die while the device still exists (test PipelineDeviceResetTest.APipelineDestroyedBeforeItsDeviceUnsubscribes). Nothing is allocated at construction; the scene target's size and format are not known yet.

resize and begin

resize(width,height) throws std::invalid_argument for a non-positive dimension, does nothing for a size it already holds, and otherwise records the size and drops the scene target and the chain, Bloom and SSAO targets; it allocates nothing itself. begin(clearColor) throws std::logic_error for a nested frame or a missing size. It raises frameOpen_, then decides whether the frame needs an off-screen scene target. That decision (wantsSceneTarget) is the short circuit that makes an inert pipeline free: it is true when HDR is enabled, a tonemapping mode other than None is set, a transparent callback is registered with a transparency mode other than None, any of bloom, SSAO, FXAA, SSR, depth of field or colour grade is enabled, any of chromatic aberration, film grain, lens flare, motion blur, height fog, light shafts or volumetric fog has a non-zero amount, or a user pass is attached. A shadow pass alone or a sky alone does not force one (AShadowPassAloneDoesNotForceASceneTarget, ASkyAloneDoesNotForceASceneTarget).

If shadows are enabled and a shadow map and caster callback were supplied through setShadowScene, that callback runs first: the shadow pass binds a target of its own and restores the back buffer, so running it after the scene target was bound would silently unbind the scene (TheShadowPassRunsBeforeTheSceneTargetIsBound). With no scene target the frame goes straight to the back buffer (bound, cleared, sky drawn) and end() does nothing more. Otherwise the scene format is Color unless HDR is enabled; with HDR it is the first of HdrBlendable, Vector4 or Color that SupportsSurfaceFormatAsRenderTargetEXT accepts (HdrPicksTheBestSceneFormatTheRendererActuallyHas), and getSceneTargetFormat reports the truth. The scene target is an owned RenderTarget2D with a Depth24Stencil8 depth buffer, because a 3D scene without depth renders in submission order. Its usage is PreserveContents only when order-independent transparency is selected, since that path unbinds the target during accumulation and binds it again to resolve, and DiscardContents otherwise (preserving is not free on a tiling GPU). The target is re-created when the size, format or usage changes. It then binds and clears the target and draws the optional skybox, after the clear and before the game draws anything.

end and the fixed pass order

The game draws its own geometry between begin and end; the pipeline owns no scene graph. end() throws std::logic_error if no frame is open, clears frameOpen_, and returns immediately if the frame did not use a scene target. Otherwise it runs the transparent phase first, then unbinds the scene target (SetRenderTarget(nullptr)) before any pass samples it. This is an invariant, not tidiness: sampling a bound render target is undefined in GL, and because ScopedRenderTarget restores whatever it found bound, a chain entered with the scene target still bound would put it back after the last pass and the next Present would refuse with “Cannot present while render targets are bound”. It then clears the chain and adds the enabled passes in the following fixed order, whose rationale is recorded in the source comments:

#Added whenPassReferred to
1SSAO enabledSsaoPassScene: values may exceed 1.0
2SSR enabled (after SSAO, so reflections show the shaded scene)SsrPass
3volumetric fog density > 0VolumetricFogPass
4light-shaft intensity > 0LightShaftPass
5height-fog density, light shafts or volumetric fog non-zeroHeightFogPass
6motion-blur strength > 0MotionBlurPass
7depth of field enabled (the lens, so before bloom)DepthOfFieldPass
8lens-flare intensity > 0LensFlarePass
9bloom enabled (its threshold needs scene-referred values)BloomPass
10tonemapping mode not None, or HDR enabledTonemapPassDisplay: after tonemapping
11colour grade enabled (before FXAA, so the edge filter sees the final contrast)ColorGradePass
12chromatic-aberration strength > 0ChromaticAberrationPass
13FXAA enabledFxaaPass
14film-grain intensity > 0 (last built-in, so FXAA does not smooth the grain)FilmGrainPass
15alwaysuser passes, in insertion orderWhatever the game expects

Note that TonemapPass is added for an HDR pipeline even when the mode is None. The tests TheFixedPassOrderIsSsaoThenBloomThenTonemapThenFxaa, SsrSitsBetweenSsaoAndBloom and DepthOfFieldSitsBeforeBloomBecauseItBelongsToTheLens are named after parts of this order but assert only the frame's pass count (getLastFramePassCount()), not the sequence; no test that asserts the positions was found, so the order rests on the source comments and each pass's own suite.

The chain is applied with a PostProcessContext carrying the source (the scene target), a null destination meaning the back buffer, the size, the settings, the optional depth, normal and velocity textures, and the camera data (projection, inverse projection, inverse view, near and far planes, previous view-projection and a flag saying whether a previous frame exists). The frame statistics are derived rather than hooked: passesRun is the chain's pass count and targetSwitches is 2 + passesRun (one bind for the scene target, one unbind, one per pass). The camera history advances here and nowhere else, after the chain ran, so calling setCamera several times in a frame, or on alternate frames, does not manufacture a previous view that was never rendered from (TheCameraHistoryAdvancesOncePerFrameAndNotPerSetCamera); it also means a frame that took the direct-to-back-buffer route does not advance it. setCamera throws std::invalid_argument unless the near plane is positive and the far plane lies beyond it, because the prepass normalises depth by the far plane and a bad range reconstructs NaN positions.

PostProcessChain::apply

PostProcessChain::apply throws std::invalid_argument for a null source or non-positive size. It first collects GPU timer results from earlier frames, before this frame's ranges open (a query object holds one result, so polling after the loop would read a range that has not finished), then either copies the source through a BlitPass when no pass is enabled, or runs the passes: every pass but the last writes a pooled intermediate acquired with slot index % 2, so a pass never reads the target it writes, and the last writes the caller's destination. Intermediates use the source's format, which keeps an HDR chain HDR instead of clamping between two float passes (AnHdrChainKeepsItsIntermediatesInFloat). The chain stores borrowed raw pass pointers (passes_) and separately owns any pass given to addOwnedPass; clear() drops both, and the pipeline rebuilds the list every frame with addPass only. Built-ins are unique_ptr members of the pipeline and user passes are borrowed, so a user pass deleted before clearUserPasses leaves a dangling call target. GPU timing is optional: one lazily created GpuTimer per chain slot, an empty list when the renderer has no timer query (so “not measured” differs from “took no time”), and polling never waits for the GPU. A test that sees no timing value may be seeing an unsupported query, not a zero-duration pass.

Resource lifetime, reset and exception edges

RenderTargetPool::acquire keys owned RenderTarget2Ds by width, height, colour format, depth format and slot and returns borrowed pointers; a non-positive size throws std::invalid_argument. The two alternating slots are how a pass avoids reading and writing the same target. reset destroys every entry, so callers must not keep a pooled target across a resize, reset or destruction. ScopedRenderTarget records the previous bindings where the renderer can report them, binds its destination, and restores in a non-throwing destructor; if the previous state could not be queried, or the previous binding was the back buffer, it binds the back buffer. It swallows restoration errors during unwinding, so a failure after a pass should be debugged at the original exception and the final binding state, not assumed fully restored (TheTargetIsRestoredWhenTheScopeIsLeftByAThrow, NestedScopesUnwindInOrder).

The DeviceReset handler calls releaseDeviceResourcesEXT only when no frame is open; a reset raised between begin and end is deliberately ignored (AResetBetweenBeginAndEndIsIgnoredRatherThanObeyed), on the reasoning that neither begin nor end pumps platform events, which is the basis for the single-threaded assumption in the comment. releaseDeviceResourcesEXT throws if a frame is open, resets the scene target and the chain's pool, zeroes the statistics and clears the scene-target flag. The pipeline is the only object in this module that subscribes to DeviceReset. Three points follow. First, BloomPass, SsaoPass and VolumetricFogPass each hold a private RenderTargetPool; resize resets the Bloom and SSAO pools, the device-reset path resets none of the three, and nothing resets the volumetric-fog pool at all; getGpuMemoryEstimateBytes counts only the chain pool and the scene target, so DeviceLossTest.EverySubsystemSurvivesAResetAndRendersAgain cannot observe them (it also checks standalone subsystems only for not throwing on isSupported-style calls). Whether those pooled targets remain usable after a context loss depends on the renderer and was not established here. Second, none of this is a thread-safety guarantee: the frame flags, pass lists, borrowed texture pointers and device calls are unsynchronised, and CNA's own design note states the engine layer is owner-thread only (thread and callback map). Third, a callback that throws is not recovered.

The exception edges are visible in the source. end() lowers frameOpen_ before it calls drawTransparentPhase. That routine either uses the owned WeightedBlendedTransparency helper where its capabilities allow (accumulate between begin and end, re-bind the scene target, resolve), records why it fell back in getTransparencyFallbackReasonEXT, or applies the sorted phase: DepthRead plus NonPremultiplied blend state around the caller's draw, then Default and Opaque again. If the caller's transparent callback throws, neither the state restoration, the helper's end, the unbind, nor the chain runs, and the pipeline already considers the frame closed. Likewise begin raises frameOpen_ before the shadow callback, so a throwing caster callback leaves the flag set and the shadow map's begin/end unpaired. These are source-observed risks to validate before changing that code, not reported bugs; a change needs an exception and recovery test as well as a successful-frame image. A maintainer debugging missing geometry should separate an unsupported order-independent fallback from bad depth or blend state, target preservation, or a draw callback that never ran.

Shader packages: from manifest to the selected variant

Most of the module's shader-based subsystems carry their shaders as packages. Eleven package directories live under src/shaders (atmospheric sky, auto exposure, clustered forward, clustered light compute, depth-normal prepass, GPU instance culler, particle system, post process, shadow caster, skybox and volumetric fog). Each holds a package.json manifest, the shader sources and one checked-in *.generated.hpp header; every one of the eleven declares payloads in all four groups (glsl-es, desktop glsl, spirv and wgsl).

The authoring chain

generate_shader_package.py (described in its README) reads a manifest with schema: 1, a C++ namespace, a preprocessor guard (CNA_CNAEXT here) and a list of payloads, each naming a symbol, a source file, a language, a stage, a format (text, spirv or wgsl) and an entry point. Text variants are embedded verbatim. Vulkan GLSL sources are compiled to SPIR-V through the system libshaderc.so.1 (--shaderc-library or CNA_SHADERC_LIBRARY), and WGSL payloads are produced from that SPIR-V with naga-cli (--naga or CNA_NAGA) after a mechanical source transform. The generated header records the SHA-256 of the manifest, every source, shaderc and naga, the SPIR-V version and target and the fixed options, so the same inputs and toolchain reproduce it byte for byte; a different toolchain hash is an explicit toolchain change to regenerate, inspect and commit together with the payload diff. The tools are offline authoring tools: no CNA runtime target loads or links them, and an ordinary build needs neither.

The clustered forward header shows the shape: namespace CNA::Graphics::detail::ClusteredForwardGenerated, provenance constants (kManifestSha256, kCompilerSha256, kWgslTranslatorVersion and so on), one constexpr per payload (kForwardEsFragmentSource, kForwardVulkanFragmentSpirV, ...) and a kPayloads table of PayloadProvenance records. Never edit the header by hand. To change a shader, edit the source (and the sibling variants), then regenerate with generate_shader_package.py <package.json> --output <header>. The write-free form, --check, exits 77 when the toolchain is absent. cmake/Tests/ModuleProbes.cmake registers a <Name>ShaderPackageReproducibility ctest per package that runs --check with SKIP_RETURN_CODE 77; they exist only with CNA_BUILD_TESTS on a native (non-Emscripten, non-Android) build with Python found. Ten of the eleven source packages have one (ClusteredForward, ClusteredLightCompute, GpuInstanceCuller, ParticleSystem, AutoExposure, Skybox, AtmosphericSky, VolumetricFog, PostProcess and DepthNormalPrepass); shadow_caster has none in the CMake files, and no CI workflow was found that runs the generator check, so reproducibility is a developer-machine gate.

How a package is selected

ShaderPackageEXT::selectFor walks languages in a fixed preference order — SpirV, Dxil, GlslDesktop, GlslEs, GlslVulkan, Hlsl, Msl, Wgsl — and takes the first language for which every required stage has exactly one payload that the live renderer accepts through GraphicsDevice::SupportsShaderLanguageEXT(language, stage). Declaration order never matters. Graphics stages also require the CustomEffects capability, a compute stage requires ComputeShaders, and the package's declared binding requirements are checked (texture arrays, 3D sampling, constant buffers and vertex or compute storage buffers have conditions; fragment-stage sampled textures and storage buffers add none). A rejected language is listed in the diagnostic with its reasons, so selectFor(device).isUsable() is the question a subsystem's isSupported() asks. Renderer answers live in each renderer: EasyGL accepts exactly one of desktop GLSL (its desktop core profile) or GLSL ES (the ES and WebGL profiles), and OpenGL4 accepts desktop GLSL only; the Vulkan, SDL_GPU and WebGPU renderers override it too (the guide's renderer table says which language each takes). The renderer table is in the guide (which renderers can run it), and the renderer pages describe the implementations (EasyGL, OpenGL4, Vulkan, SDL_gpu).

A second predicate matters to host code: CNA::UsesDescriptorBindingContractEXT(language) in ShaderLanguageEXT.hpp is true for SpirV and Wgsl, whose payloads share one binding layout generated from the same Vulkan GLSL (storage buffers in a set of their own, uniform arrays inside a scalar block, textures in numbered slots). Engine code choosing between that route and the GLSL route must ask this predicate, not name one language.

The clustered forward package at this snapshot

ClusteredForwardEffect.cpp builds one ShaderPackageEXT of eight payloads, required stages vertex and fragment, with eight declared bindings (five sampled 2D textures at 0 to 4, three fragment storage buffers at 6 to 8). The constructor creates a ShaderEffect from the package only when the device supports CustomEffects and selectFor finds a usable variant, reports a compile failure once, and sets supported_. begin then branches on the selected language: for SpirV and Wgsl it packs uniform arrays (uClusterMatrices, uClusterVectors, uClusterScalars) and binds the cluster storage through the light buffer; for a GLSL variant it sets named uniforms one at a time and binds the light data as textures. Transmissive materials without a supplied opaque-frame texture are refused with std::runtime_error on either route, and an un-uploaded light buffer is refused too.

The change relative to the snapshot the earlier deep read used: the package gained two payloads, forward.desktop.vert.glsl and forward.desktop.frag.glsl (language GlslDesktop, #version 330 core), declared in package.json as kForwardDesktopVertexSource and kForwardDesktopFragmentSource, with the generated header regenerated (SPIR-V and WGSL payloads unchanged) and eight lines added to ClusteredForwardEffect.cpp. Reading the files, the desktop fragment source differs from the ES source only in its first two lines (the version line and the precision statement), and the vertex source likewise, so the host-side begin needed no change: desktop GLSL takes the same named-uniform route as ES. Before the change the effect had no usable variant on any renderer whose language answer is desktop GLSL only (the OPENGL33 identity's desktop core profile and OPENGL4), so it reported unsupported there. CNA's commit message for the change (06ee0224, task GL4-0029) records that the clustered forward package was the only engine-layer package without a desktop variant, that 15 cases and the clustered-lights oracle skipped on those renderers and that ATransmissiveMaterialWithoutAnOpaqueFrameIsRefused failed on OPENGL4 and OPENGL33, and reports CnaGraphicsExtTests on OPENGL4 moving from 938/1/23 to 954/0/8 (the message does not label the three figures; they read as passed, failed and skipped) and equal to the EasyGL OPENGLES3 reference, with every Clustered* case passing on OPENGL4, OPENGL33 and OPENGLES3. Those figures are CNA's record; nothing was run for this page.

What the tests do and do not pin. ClusteredForwardEffectTests.cpp holds 34 test macros (light range and falloff, spot cones, 256 lights, agreement with a CPU model, area lights, clearcoat, sheen, transmission and volume absorption, subsurface wrap, probes) and was not modified by the change; none names the desktop variant, so which variant a run exercises depends on the renderer the test binary is built for. Its PackagedEsSourceRetainsEveryPublicGlslFragment pins only kForwardEsFragmentSource against five public GLSL helper texts (the light lookup, probe evaluation, thin-film, area-BRDF lookup and area shading fragments), so a future edit that changes the helper in the ES source and not in the desktop source would not be caught by that test. The general position: a passing focused suite on one renderer says nothing about the other variants. Individual effect behaviour on each renderer, the PBR effects' rendering correctness and the compute and shadow families remain to be audited separately.

Other test-tree changes since the earlier read

The module's test tree also changed. CNA's history attributes the additions to two tasks: GL4-0025 (compute programs, storage buffers and constant buffers on OpenGL4) and GL4-0037 (Texture2DArray on OpenGL4, after a renderer-neutral live suite). The tree gained desktop-GLSL compute variants for the constant_buffer, modern_conformance and modern_resource_interop test packages, a new texture_array package (GLSL ES, desktop GLSL, Vulkan GLSL for SPIR-V and WGSL) with a new Texture2DArrayConformanceTests.cpp (five macros), and two helpers in EngineTestSupport.hpp: RunsLegacyComputeSource and LegacyComputeSource, which derive a #version 430 core program from a #version 310 es source by dropping the precision statements and highp qualifiers. ComputeShaderTests, ComputeCullingTests and ModernGpuConformanceTests were updated to use them. The shared test device, HiDefDevice, is the module's standalone device at the HiDef profile, created for the renderer the build was configured with, because the default Reach profile is enforced and refuses volume textures and multiple render targets; capability gates such as CNA_SKIP_WITHOUT_RENDER_TARGETS, RunsShaderSource, RunsGlslShaderSource and RunsLegacyComputeSource turn an unsupported renderer into a skip, which is not a failure.

The other families: a source and test map

The frame path above is the only part of this module this page traces. For the rest it identifies where the code and its tests live. The grouping below is by file name and is this page's own; counts are line-anchored TEST, TEST_F, TEST_P and TYPED_TEST macros over the module's 92 test files (967 in all), located and not executed.

FamilyMain sources under src/Tests (files, macros)
Frame, chain, settings, timing, resetRenderPipeline, RenderPipelineSettings, PostProcessChain, PostProcessPass, FullscreenPass, EffectPass, RenderTargetPool, ScopedRenderTarget, GpuTimer10 files, 109 (RenderPipeline 26, PipelineDiagnostics 18, PostProcessChain 15, RenderPipelineSettings 10, ScopedRenderTarget 8, PassTiming 7, DeviceLifecycle 6)
Post-process passesBloomPass, TonemapPass, FxaaPass, SsaoPass, SsrPass, DepthOfFieldPass, MotionBlurPass, ColorGradePass with CubeLut, the lens passes, the fog and light-shaft passes, AerialPerspectivePass, ContactShadowPass, DecalPass, SpatialUpscalePass, HdrDisplayOutput, AutoExposureEXT25 files, 247
Earlier post-process effectsAsciiPostProcessEffect, CRTEffect, DepthEffect3 files, 36
Prepass and velocityDepthNormalPrepass5 files, 31
TransparencyTransparentDrawList, WeightedBlendedTransparency3 files, 22
ShadowsShadowMap, CascadedShadowMap, SpotShadowMap, CubeShadowMap, ClusteredShadowPolicyEXT, the caster packages7 files, 92
Lights, clustered lighting, probes, IBLClusteredForwardEffect, ClusteredLightAssignment, ClusteredLightCompute, ClusteredLightBuffer, ClusteredLightGrid, ClusteredLightSetEXT, AreaLightShading, AreaLightBrdfTable, LightProbe*, EnvironmentProcessor, ThinFilmIridescence13 files, 166 (ClusteredForwardEffect 34)
MaterialsPbrMaterial, PbrMaterialExtensions, MaterialBinding, glTF bridge3 files, 28
SkySkybox, AtmosphericSky2 files, 25
Compute and GPU resourcesComputeShader, StorageBuffer, StorageTexture2D, Texture2DArray, indirect draw, GpuInstanceCuller, ParticleSystem, InstancedRendererEXT, LodGroupEXT, FrustumCullerEXT13 files, 126
Debug drawingDebugDraw, DebugGizmos2 files, 25
Shader authoring and foundationShaderCodeEXT, ShaderPackageEXT, ShaderEffectFactory, ShaderDiagnostics, RequireCapability, EngineException, EngineLayerVersion (revision 18)6 files, 60 (ShaderInfrastructure 37)

Their own construction, ownership, failure and validation routes are not reconstructed on this page; do not read the frame trace as coverage of them. The guide's contents, test evidence and limits sections give the user-level view, and Effects: beyond XNA and Shader effects: typed shader code and packages cover the effect and package APIs.

Human route for one extension change

To change a post-process pass, start with RenderPipeline::end to establish order and space (scene-referred or display-referred), then follow PostProcessChain::apply, the pass's own apply, FullscreenPass and the selected renderer's shader and capability route. Check whether the pass owns render targets (its own pool, as Bloom, SSAO and volumetric fog do) or borrows depth and camera inputs. Add a focused test for enable and disable, two consecutive frames, resize and reset, and compare a neutral or software route with a real GPU renderer if shader output matters. RenderPipelineTests, PostProcessChainTests, ScopedRenderTargetTests, DeviceLifecycleTests, PassTimingTests and the owning pass test are starting oracles, not proof of every backend. Ordering claims need a test on the fifteen-slot order (no test asserts the positions today: the three order-named tests only count passes, see CNA-VGAP-018).

For a shader change, the regeneration step is known: edit the source for every variant the package carries (ES, desktop, Vulkan GLSL for SPIR-V and WGSL), run the generator with --output, inspect the payload diff, and commit source, manifest and header together; the package's ShaderPackageReproducibility ctest (where one exists) is the check that the header still derives from its sources. Then run the effect's own suite on each renderer family that takes a different variant, because selection means one renderer exercises one language. For a change that adds a variant, follow the clustered forward precedent: add the payloads to package.json, add the ShaderCodeEXT entries where the package is built, and decide whether the host-side binding route is chosen by UsesDescriptorBindingContractEXT or by an explicit language. For a build-flag change, remember the guard discipline: an unguarded engine-layer file builds in both configurations and only surfaces in an XNA-only consumer.

⚠

Source-observed gaps (not measured). Pass-owned render-target pools are not released by the pipeline's device-reset path; a throwing shadow or transparency callback leaves frame state inconsistent; the shadow_caster package has no reproducibility test registered; and the ES-only source pin in the clustered forward tests does not cover the desktop variant. None is a reported defect; each is a place to add a test before changing the code around it.

Curated source route and open coverage

  1. graphics-ext/CMakeLists.txt and modules/CMakeLists.txt: establish the macro, the physical target, the CNA::CnaExt umbrella and the renderer-selection boundary.
  2. RenderPipeline.hpp then RenderPipeline.cpp: map borrowed inputs, owned passes, begin/end order, target usage, the reset subscription and the diagnostics.
  3. PostProcessChain.cpp, RenderTargetPool.cpp and ScopedRenderTarget.cpp: the intermediate ping-pong, borrowed target lifetime and binding restoration.
  4. ShaderPackageEXT.cpp, ClusteredForwardEffect.cpp, the clustered forward package.json and its desktop fragment source, then the generator: the package mechanism end to end.
  5. RenderPipelineTests.cpp and PostProcessChainTests.cpp: identify the exact ordering, target and fallback oracles before extending a pass; then DeviceLifecycleTests.cpp for what reset coverage really asserts.
  6. docs/cnaext-engine-layer.md: CNA's own design note, useful for orientation and the recorded task history. It lags the code in places (its renderer matrix and its description of RenderPipelineSettings as having no consumer), so verify every statement in source.

Open coverage: the shader packages other than clustered forward, the compute resources, the shadow family, the individual clustered-lighting and PBR effects, transparency and each individual pass have their own construction, ownership, failure and validation routes that this page does not reconstruct. Do not infer that those families are covered by the pipeline trace. Related pages: GraphicsDevice internals, textures and render targets, renderer selection, graphics backends; the change routes are in the Maintainer Handbook and I need to fix a renderer bug, ownership across the tree is in the ownership map, and the module list is in the module index. Evidence level: everything above was checked by reading the TARGET sources at 009d40f5 and the git history of the module between the earlier read and the snapshot; nothing was built or executed, and the CNA-reported test figures are attributed as such.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.