CNAEXT Engine Layer
Experimental, opt-in and moving. The engine layer (CNA::Graphics) is compiled only when you configure CNA with -DCNA_CNAEXT=ON; the option is OFF by default. The layer's own header says its revision number (CNA_CNAEXT_ENGINE_VERSION, currently 18) is “not an ABI guarantee”, and CNA's design note describes the layer as “still moving”. No CNA workflow builds it with CNA_CNAEXT=ON, so nothing here is continuously integrated: the evidence on this page is source, headers, CMake registrations and CNA's own tests as written — we built and ran none of it. Everything below describes CNA snapshot 009d40f5 (branch next), not the alpha.1 tag.
CNA reimplements the XNA 4.0 API. Everything it adds on top — physically based materials, shadow generation, image-based lighting, a post-processing pipeline, compute passes, clipboard and file dialogs — is labelled as an extension, and the biggest body of it lives in one opt-in module. This page is the reference for that module: what “CNAEXT” means, what the engine layer contains, which renderers can run which part of it, how it is built and tested, and where its limits are. For a guided tour with code, start with Tutorial 115, then Tutorial 153 (image-based lighting) and Tutorial 154 (cascaded shadows).
What CNAEXT is
Four unrelated things share the name, and confusing them is the most common mistake. Only two of them are build switches.
| Property | CNAEXT | CNA_STRICT_XNA_API | CNA_CNAEXT | CNA_DEVICES |
|---|---|---|---|---|
| What it is | Marker macro (plus an EXT naming convention) | Preprocessor define | CMake option | CMake option |
| Default | Expands to nothing | Never defined | OFF | OFF |
| Effect | Tags a declaration as “not XNA 4.0”; documentation and lint only | Turns the marker into [[deprecated]] in the translation unit that defines it | Compiles the CNA::Graphics engine layer (modules/graphics-ext, 98 public headers) and a few ShaderEffect overloads | Compiles CNA::Devices (modules/devices-ext, 17 public headers) |
| Granularity | Per declaration | Per translation unit | Per build | Per build |
The marker is exactly this (modules/core/include/CNA/CNAHelper.hpp):
#ifdef CNA_STRICT_XNA_API
#define CNAEXT [[deprecated("CNAEXT: not part of the XNA 4.0 API surface")]]
#else
#define CNAEXT
#endif
It is never a compile guard: everything it marks is compiled in every build. CNA_STRICT_XNA_API is not a CMake option — nothing in CMake declares it — so you set it on the one target you want checked, together with -Werror=deprecated-declarations. Its verification harness (cna_strict_xna_api_check, and a WILL_FAIL leak check) covers only the Microsoft::Devices and Sensors surface, not the graphics, content or input API, so a clean strict build is a hint about your own code, not a proof that you stayed within XNA 4.0. Tutorial 115 walks through the mechanism.
The EXT suffix is a convention, not an enforced rule. New members on XNA types usually carry it (SetDisplayColorSpaceEXT, GetRendererCapabilityProfileEXT, SupportsShaderLanguageEXT), but marked members without it exist too (BasicEffect::SetOwnedTexture, Effect::Apply(), the class names ShaderEffect and ColorMatrixEffect). Use the marker, not the suffix, to decide what is an extension. There is no repository-wide validator that every non-XNA symbol is marked.
Where extensions live
| Namespace | Compiled when | Contents |
|---|---|---|
CNA::Graphics (module graphics-ext) | CNA_CNAEXT=ON | The engine layer described on this page. Verbs are lowerCamelCase and accessors are getX()/setX()/isX(), not the XNA-style getXProperty() spelling. |
CNA::Devices (module devices-ext) | CNA_DEVICES=ON | Clipboard, file dialogs, message boxes, power, system info, locale, display info, tray, camera, URL launcher — see Tutorial 88 and Tutorial 117. |
Microsoft::Xna::Framework::Graphics | always | CNAEXT-marked members and types that extend an XNA type: PbrEffect, SkinnedPbrEffect, ShaderEffect, ColorMatrixEffect, the shadow and IBL state types, morph targets, skinned-model data. |
CNA:: (core value types) | always | RendererCapabilityProfile, ShaderLanguageEXT, ShaderDiagnosticEXT, DisplayColorSpace, GraphicsImageAccess, GraphicsMemoryBarrier, IndirectDrawArguments. |
CNA::Graphics is not a synonym for “gated”: CNA::Graphics::VertexTypeRegistryEXT lives in the always-compiled graphics module. And CNAEXT itself can never be a namespace, because it is a macro.
The always-compiled extension surface
A large part of what people call “CNAEXT” needs no build option. The headers say so deliberately: an effect's public surface must not change with a build flag.
| Always compiled | Notes |
|---|---|
PbrEffect, SkinnedPbrEffect | Metallic-roughness PBR; Tutorial 114. Implement IShadowReceiverEXT; take an ImageBasedLightEXT. |
IShadowReceiverEXT, ShadowCascadeStateEXT, PunctualLightEXT, ImageBasedLightEXT, AreaLightEXT | The receiving half of shadows and IBL. BasicEffect, SkinnedEffect, PbrEffect and SkinnedPbrEffect are receivers. Generating shadow maps and IBL products needs the engine layer. |
ShaderEffect(device, vertexSource, fragmentSource), its diagnostics and array setters | GetCompileErrorEXT(), GetShaderDiagnosticsEXT(), GetSelectedShaderLanguageEXT(), SetUniformVec3Array/SetUniformMat4Array. The portable-payload constructors (ShaderCodeEXT, ShaderPackageEXT) and the texture-array / storage-texture bindings are #ifdef CNA_CNAEXT. |
ColorMatrixEffect | A colour transform that only the CPU SpriteBatch path executes (GDI, SOFTWARE); Tutorial 116. |
GraphicsDevice EXT queries and RendererCapabilityProfile | SupportsRendererFeatureEXT, GetRendererCapabilityProfileEXT (32 features, 22 limits, per-format usage), GetRendererCapabilityReportEXT, SupportsShaderLanguageEXT, ExecutesShaderEffectSourceEXT, SupportsShadowSamplingEXT, SupportsImageBasedLightingEXT, SupportsSurfaceFormatAsRenderTargetEXT, indirect and base-instance draws, compute work-group limits. |
MorphTargetEXT, SkinnedModelEXT, AnimationPlayer, ModelAnimationsEXT, glTF import | Unchanged since alpha.1 at the header level; Tutorial 112, Tutorial 113. |
GamerServices::AvatarRenderer extension members | Real avatar rendering (DrawRealEXT and friends); XNA's own Draw() overloads stay no-ops. Independent of the engine layer. |
Building it, and how the gate works
| Option | Default | Gates |
|---|---|---|
CNA_CNAEXT | OFF | The CNA::Graphics engine layer, a few ShaderEffect overloads, and the implementation behind the C API's engine-layer routes. |
CNA_DEVICES | OFF | CNA::Devices (devices-ext). |
CNA_STRICT_XNA_API | not a CMake option | Makes CNAEXT deprecated in the consuming translation unit only. |
Both options are attached as public compile definitions to the shared CNA::BuildConfig interface target, so every consumer of the CNA target sees the same macro the headers test. Both extension modules are always added and always compiled; with the option off, every source file and every header of graphics-ext is wrapped in #ifdef CNA_CNAEXT ... #endif (a text-level check enforces this) and compiles to nothing. The consequence is the classic trap: #include "CNA/Graphics/RenderPipeline.hpp" succeeds in a default build and declares nothing, so the error you get is “no type named RenderPipeline”, not “missing header”. The fix is the CMake flag, not an include path.
# The repository's own preset: OPENGLES3, Debug, CNA_CNAEXT=ON, tests on, examples off.
cmake --preset cnaext
# Or by hand, in your own build directory:
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCNA_CNAEXT=ON
# In a game project that adds CNA as a subdirectory (Tutorial 03), set the option first:
set(CNA_GRAPHICS_RENDERER "OPENGLES3" CACHE STRING "CNA graphics renderer")
set(CNA_CNAEXT ON CACHE BOOL "Build the CNA::Graphics engine layer")
add_subdirectory(${CNA_DIR} ${CMAKE_BINARY_DIR}/cna)
target_link_libraries(MyGame PRIVATE CNA) # CNA already includes graphics-ext; CNA_CNAEXT reaches your code
Then include the master header and use CNA::Graphics:
#include "CNA/Graphics/CNAEXT.hpp" // every engine-layer type; empty without CNA_CNAEXT
Where the tests and examples come from: engine-layer tests need CNA_BUILD_TESTS and CNA_CNAEXT; the CRT and colour-depth demos additionally need CNA_BUILD_EXAMPLES and a renderer of OPENGLES3, OPENGL33 or VULKAN. The cnaext preset builds tests but not examples. Two more facts worth knowing before you plan a project around it:
- Shared library and C API. A shared
libcnanames both extension modules when they are present. The C API keeps one exported ABI regardless of the option:engine_layer.hdeclares its routes in every build and they returnCNA_RESULT_NOT_SUPPORTEDwhen the layer is absent; the C ABI number (0.29.0) is independent of the engine-layer revision. See the C API page. Whether the C API library builds at this snapshot was not verified by us. - No extra dependencies. The layer's shaders are embedded (11 packaged shader directories holding 156 GLSL source files, plus 11 package manifests and 11 generated headers; the manifests declare 208 shader payload entries. Regenerated by
tools/shader_package/generate_shader_package.py); at run time nothing links a shader compiler.
From 12 to 98 headers
Alpha.1's graphics-ext was twelve files: AsciiPostProcessEffect, AsciiQuantizeMode, CRTEffect, CRTMaskType, DepthEffect, DepthEffectMode, DitherMode, PbrMaterial, RenderPipelineSettings, RenderQuality, ShadowQuality and TonemappingMode. RenderPipelineSettings was a configuration bag with no orchestrator to consume it. At this snapshot the layer is a real engine:
Metric (public include/ and module trees) | Alpha.1 | This snapshot |
|---|---|---|
graphics-ext public headers | 12 | 98 (none removed) |
graphics-ext implementation files (src/*.cpp) | 7 | 83 |
graphics-ext test source files | 3 | 92 (967 statically counted TEST-family macros) |
graphics-ext example programs | 7 | 32 |
| Packaged shader directories | 0 | 11 (156 GLSL source files; 208 manifest payload entries) |
Engine-layer revision (CNA_CNAEXT_ENGINE_VERSION) | none | 18 |
devices-ext public headers | 17 | 17 (unchanged; only DisplayInfo changed) |
The counts are source counts made with a stated method (headers under modules/graphics-ext/include/CNA/Graphics/; TEST, TEST_F, TEST_P and TYPED_TEST macros under modules/graphics-ext/tests); they say nothing about how many tests pass. Note that 233 commits touch modules/graphics-ext between the alpha.1 tag and this snapshot, and much of the work was merged after the tag from side branches, so the tag contains none of it.
What the engine layer contains
Every header below is wrapped in #ifdef CNA_CNAEXT. Names are the C++ types under CNA::Graphics unless stated.
Frame pipeline
RenderPipeline wraps whatever a game already draws in begin()/end(): an off-screen scene target (best of HdrBlendable, Vector4, Color that the renderer can create), an ordered chain of passes, optional skybox, shadow pass and transparency phase, frame statistics and per-pass GPU timing. With nothing enabled it renders straight to the back buffer — CNA's own test asserts the output is bit-identical to no pipeline and that nothing is allocated. Supporting types: RenderPipelineSettings, RenderQuality (Low/Medium/High/Ultra), PostProcessChain, PostProcessPass, PostProcessContext, FullscreenPass, RenderTargetPool, ScopedRenderTarget, EffectPass (runs any Effect as a full-screen pass).
Post-processing passes
Nineteen PostProcessPass subclasses: BlitPass, BloomPass, TonemapPass (TonemappingMode: None, Reinhard, Filmic, Aces, Uncharted2), FxaaPass, SsaoPass, SsrPass, DepthOfFieldPass, MotionBlurPass, ColorGradePass (with CubeLut and LutInterpolation), ChromaticAberrationPass, FilmGrainPass, LensFlarePass, HeightFogPass, VolumetricFogPass, LightShaftPass, AerialPerspectivePass, ContactShadowPass, AsciiPass and EffectPass. Related classes that are not pass subclasses: DecalPass, SpatialUpscalePass, HdrDisplayOutput (with CNA::DisplayColorSpace), AutoExposureEXT (compute), ThinFilmIridescence.
Prepass and transparency
DepthNormalPrepass (with DepthEncoding) produces the depth and view-space normal images that SSAO, SSR, depth of field and contact shadows read; it uses one multiple-render-target pass where the renderer really keeps that promise and two passes otherwise, and can add a per-object velocity image. TransparencyMode (None, Sorted, OrderIndependent), TransparentDrawList and WeightedBlendedTransparency handle the transparent half of a scene; when order-independent transparency is unavailable the pipeline falls back to sorted and records why (getTransparencyFallbackReasonEXT()).
Shadows
ShadowMap (directional), CascadedShadowMap (2 to 4 cascades in one atlas, practical split scheme, cross-fade band, debug tint), SpotShadowMap, CubeShadowMap (point lights, six faces), ContactShadowPass (screen space) and ClusteredShadowPolicyEXT (which clustered lights get maps). ShadowQuality (Disabled, Low, Medium, High, Ultra) sets the map edge to 512, 512, 1024, 2048 and 4096 texels. The map is a float target where the renderer can create one, otherwise 8-bit. Receiving is the always-compiled IShadowReceiverEXT. Tutorial 154.
Lights and image-based lighting
DirectionalLightEXT, PointLightEXT, SpotLightEXT; clustered forward lighting (ClusteredForwardEffect with up to 128 lights per fragment, ClusteredLightSetEXT up to 256 lights, CPU assignment in ClusteredLightAssignment, GPU assignment in ClusteredLightCompute, plus ClusteredLightGrid, ClusteredLightBuffer, ClusteredLightEXT, ClusteredLightType); area lights (AreaLightShading, AreaLightBrdfTable); light probes (LightProbeEXT, LightProbeVolumeEXT, LightProbeBaker); and EnvironmentProcessor, which converts an equirectangular panorama to a cube and generates the irradiance cube, prefiltered specular cube and BRDF lookup table on the CPU. Tutorial 153.
Sky, atmosphere and fog
Skybox (cube-map environment, yaw, intensity, tint), AtmosphericSky (procedural sun sky), AerialPerspectivePass, HeightFogPass, VolumetricFogPass and LightShaftPass. A skybox on a renderer that cannot compile its shader draws nothing and says so once in the log.
Materials
PbrMaterial (seven texture slots), PbrMaterialExtensions (the glTF clearcoat, sheen, transmission, volume and iridescence extensions, consumed by ClusteredForwardEffect rather than by PbrEffect), MaterialBinding (material to PbrEffect) and GltfMaterialBridge (imported glTF material to PbrMaterial). The stock PbrEffect/SkinnedPbrEffect are always compiled.
Geometry throughput
InstancedRendererEXT, LodGroupEXT, FrustumCullerEXT (both CPU-only), GpuInstanceCuller (compute plus indirect draw), ParticleSystem (compute simulation, instanced draw), DebugDraw and DebugGizmos. The always-compiled GraphicsDevice gains DrawInstancedPrimitivesBaseInstanceEXT, DrawPrimitivesIndirectEXT and DrawIndexedPrimitivesIndirectEXT.
Compute and GPU resources
ComputeShader, StorageBuffer (and StorageBufferT<T>), ConstantBuffer (and ConstantBufferT<T>), StorageTexture2D, Texture2DArray and GpuTimer. The capability side is always compiled: GraphicsCapability::ComputeShaders and IndirectDraw, CNA::GraphicsImageAccess, CNA::GraphicsMemoryBarrier.
Shader authoring
ShaderCodeEXT (one payload in one language and stage), ShaderPackageEXT (several payloads; selectFor(device) picks the variant the live renderer accepts and explains a refusal), ShaderEffectFactory (compile-once-by-name cache) and ShaderDiagnostics. Languages the engine's own packages ship: GLSL ES, desktop GLSL, SPIR-V and WGSL — there are no HLSL or MSL packages in this snapshot. CNA::ShaderLanguageEXT also names Hlsl, Msl, Dxil and GlslVulkan for callers who bring their own.
Post-process effects that predate the layer
AsciiPostProcessEffect (a CPU glyph quantiser, also wrapped as AsciiPass), CRTEffect and DepthEffect (both ShaderEffect subclasses). They are the whole of alpha.1's engine layer and are still here; see Tutorial 116.
Foundation
CNAEXT.hpp (master include), EngineException, EngineLayerVersion (getEngineLayerVersion(), getEngineLayerVersionString()) and RequireCapability (an internal detail::requireCapability helper that throws EngineException::notSupported; see how a renderer's “no” is handled).
Which renderers can run it
The engine layer never assumes a renderer. Almost every shader-based subsystem asks the same two questions, and both must be yes:
- Does the renderer accept custom effects?
GraphicsCapability::CustomEffects(andThreeDfor shadows and the prepass). - Does a shader variant exist that this renderer takes?
ShaderPackageEXT::selectFor(device)walks the package's languages in a fixed preference order and keeps the first for whichGraphicsDevice::SupportsShaderLanguageEXT(language, stage)accepts every required stage (it also checks the package's binding requirements, such as 3D-texture sampling, and returns a diagnostic that says why a package was refused). The engine's packages contain GLSL ES (#version 300 es, and310 esfor compute), desktop GLSL (330 core, and430 corefor compute), SPIR-V and WGSL. The default answer forSupportsShaderLanguageEXTis false for every pair, and exactly five renderer families override it: EasyGL, OPENGL4, VULKAN, SDL_GPU and WEBGPU.
Shadow generation and shadow/IBL sampling are separate questions: generation is engine-layer work that needs the two answers above; sampling is a property of the receiving renderer (SupportsShadowSamplingEXT(), SupportsImageBasedLightingEXT()), and it is false by default. The table lists all 25 public renderer identities, grouped where the answer is identical.
| Renderer identity | Engine shader passes and shadow generation | Package language it takes | Shadow and IBL sampling | Compute |
|---|---|---|---|---|
OPENGL33 | Yes | desktop GLSL | yes, yes | when the context is GL 4.3 or newer |
OPENGLES3, WEBGL2 | Yes | GLSL ES | yes, yes | ES 3.1 or newer on native contexts; never on WebGL |
OPENGLES2, WEBGL1 | Reported, unverified | GLSL ES (the profile reports it) | yes, yes | no on WebGL; ES 3.1+ only |
OPENGL4 | Yes | desktop GLSL | yes, yes | GL 4.3+ with native compute |
VULKAN | Yes (built-in passes) | SPIR-V | yes, yes | when the device supports it |
WEBGPU | Yes | WGSL | yes, yes | yes |
SDL_GPU | Only in libshaderc builds — CustomEffects is true only where the SPIR-V/shaderc path is built (not Windows, Apple or Emscripten) | SPIR-V | yes, yes (with a device) | yes (with a device) |
DIRECTX9, DIRECTX11, DIRECTX12 | No — they compile HLSL ShaderEffect source, but the engine ships no HLSL variant, so selectFor finds nothing usable | none from the engine's packages | no, no | no |
METAL | No (CustomEffects is false) | none | no, no | no |
FNA3D | No (CustomEffects is false; it runs compiled XNA effects instead) | none | no, no | no |
SOFTWARE | No (source is accepted for compatibility but never executed) | none | no, no | no |
PORTABLEGL | No | none | no, no | no |
HEADLESS, STUB | No (no pixels) | none | no, no | no |
SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SVG_DOM, GDI, FREEDIRECT | No (2D-only: no ThreeD) | none | no, no | no |
Reading the table: engine shader subsystems can run on up to nine of the 25 identities — EasyGL's five profiles, OPENGL4, VULKAN, WEBGPU, and SDL_GPU in builds that have libshaderc. They cannot run on the DirectX renderers, METAL, FNA3D, SOFTWARE, PORTABLEGL, HEADLESS, STUB or any of the seven 2D-only identities, and that is a property of the shipped packages (no HLSL or MSL variant), not of those renderers' quality. Three cautions:
- The two ES-2-generation profiles are the least certain.
OPENGLES2andWEBGL1report GLSL ES, but the engine's ES payloads are written for GLSL ES 3.00 and those profiles lack multiple render targets, instancing and 3D textures; whether individual passes compile on a live ES 2.0 or WebGL 1 context was not verified. - Vulkan reports
ExecutesShaderEffectSourceEXT() == falsebecause it consumes SPIR-V rather than the GLSL text. The built-in passes override the base-class check with their ownCustomEffects-plus-package test, so Vulkan runs them; a user-writtenPostProcessPassthat keeps the defaultisSupported()would report unsupported there. - The detailed capability profile is misleading for shader dialects on VULKAN and SDL_GPU: they report all six
ShaderDialect*features unsupported even though they take SPIR-V. AskSupportsShaderLanguageEXT(ShaderLanguageEXT::SpirV, ShaderStageEXT::Fragment)instead.
Requirements per feature
Each row is read from the subsystem's own isSupported() / construction code in modules/graphics-ext/src. “Package” means selectFor(device).isUsable().
| Feature | Needs | Where it is missing |
|---|---|---|
RenderPipeline with nothing enabled | nothing | Renders directly to the back buffer on every renderer; CNA's test asserts bit-identical output. |
HDR scene target (setHDREnabled(true)) | a float render-target format (HdrBlendable, else Vector4); the HiDef profile | Falls back to an 8-bit Color target: the pipeline still runs, it is just not HDR. |
| Shader passes (bloom, tonemap, FXAA, colour grade, fog, lens, motion blur, decals, upscale, HDR output, …) | CustomEffects and a usable package; bloom additionally probes HalfFloatTextureLinearFiltering to choose between hardware and manual filtering | isSupported() is false; the pass copies its input through. The chain still runs the pass. |
| SSAO, SSR, depth of field, contact shadows | as above, plus depth/normal inputs (DepthNormalPrepass, setDepthNormalInputs) and, for reconstruction, a camera (setCamera) | Without inputs the pass reports it once and renders an unmodified frame. |
DepthNormalPrepass | ThreeD and a usable package; uses multiple render targets when the renderer keeps that promise (probed at construction), else two passes | isSupported() false. |
ShadowMap, CascadedShadowMap, CubeShadowMap, SpotShadowMap | ThreeD, CustomEffects, a usable caster package | The constructor logs once and isSupported() is false; the frame renders unshadowed rather than failing. |
Shadow receiving (IShadowReceiverEXT) | SupportsShadowSamplingEXT() on the renderer | The state is accepted and ignored: an unshadowed image, no error. |
IBL receiving (ImageBasedLightEXT on the PBR effects) | SupportsImageBasedLightingEXT() | Accepted and ignored, and the flat ambient colour is zeroed because an environment replaces it: an unlit-ambient PBR surface. Do not set the environment where the query is false. |
EnvironmentProcessor | nothing (CPU only; 8-bit Color products) | Works on any renderer that can create the textures. |
Skybox, AtmosphericSky | CustomEffects and a usable package | Draws nothing, logs once. |
ClusteredForwardEffect | CustomEffects and a usable package | isSupported() false. CPU light assignment (ClusteredLightAssignment) needs no capability. |
ClusteredLightCompute | ComputeShaders | Use the CPU assignment. |
ParticleSystem | ComputeShaders, Instancing, CustomEffects and packages | isSupported() false. |
GpuInstanceCuller | ComputeShaders, IndirectDraw, CustomEffects and a package | Use the CPU culler. |
InstancedRendererEXT | Instancing and MultiStreamVertexInput | isSupported() false. FrustumCullerEXT and LodGroupEXT are pure CPU. |
WeightedBlendedTransparency (TransparencyMode::OrderIndependent) | MultipleRenderTargets, a half-float render target, CustomEffects and a package | The pipeline draws the Sorted phase instead and records the reason. |
ComputeShader, StorageBuffer, StorageTexture2D | ComputeShaders (and IndirectDraw for indirect-argument buffers) | Construction is refused. |
GpuTimer, per-pass timing | a renderer timer query | Accepted and does nothing; isGpuTimingEnabledEXT() then reports false. On EasyGL desktop GL has it from 3.3; ES needs EXT_disjoint_timer_query. |
AsciiPostProcessEffect | the ability to read pixels back (Texture2D::GetData) | Rejected with System::NotSupportedException on HEADLESS. |
CRTEffect, DepthEffect | CustomEffects; package variants in four languages | The effect is built but its shader does not compile on renderers that take none of those languages; the error is logged and available from GetCompileErrorEXT(). |
How a renderer's “no” is handled
The layer's stated preference is to ask rather than throw. Every subsystem exposes isSupported() and degrades where a sensible fallback exists: a pass copies its input to its output, a shadow map leaves the scene unshadowed, a skybox draws nothing, order-independent transparency becomes sorted. Shadow maps, skyboxes and shader-compile failures log once in the renderer log category, naming the subsystem and what was missing; order-independent transparency records its reason in getTransparencyFallbackReasonEXT().
There is also a throwing route: CNA::Graphics::EngineException (with notSupported(subsystem, what, rendererName) and accessors getSubsystemProperty(), getRequirementProperty(), getRendererNameProperty()), and an internal detail::requireCapability(device, capability, subsystem) that throws it. In this snapshot no built-in subsystem calls requireCapability (we searched every module; only its own unit test does), and in library code EngineException is thrown by the CubeLut parser (malformed .cube files) and by requireCapability. Do not design around exceptions for unsupported renderers: design around the query methods.
A minimal example
Wrapping an existing draw in a pipeline is the layer's central promise: switch things on and the frame improves without the draw changing. This complete program uses the calls that CNA's own cnaext_render_pipeline_test uses (resize, getSettings, begin, end). It was syntax-checked against the snapshot headers with -DCNA_CNAEXT; it was not built or run.
#include "CNA/Graphics/CNAEXT.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsAdapter.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsProfile.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include <memory>
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using CNA::Graphics::RenderPipeline;
using CNA::Graphics::TonemappingMode;
class GlowGame final : public Game {
public:
GlowGame() : graphics_(this) {
// The HDR path wants float targets, which the default Reach profile refuses.
if (GraphicsAdapter::getDefaultAdapterProperty().IsProfileSupported(GraphicsProfile::HiDef))
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
graphics_.setPreferredBackBufferWidthProperty(960);
graphics_.setPreferredBackBufferHeightProperty(540);
}
protected:
void LoadContent() override {
auto& device = getGraphicsDeviceProperty();
spriteBatch_ = std::make_unique<SpriteBatch>(device);
white_ = std::make_unique<Texture2D>(device, 1, 1);
const Color pixel = Color::White;
white_->SetData(&pixel, 1);
pipeline_ = std::make_unique<RenderPipeline>(device);
pipeline_->resize(960, 540);
auto& s = pipeline_->getSettings();
s.setHDREnabled(true);
s.setBloomEnabled(true);
s.setBloomThreshold(0.6f);
s.setBloomIntensity(1.2f);
s.setTonemappingMode(TonemappingMode::Aces);
s.setFXAAEnabled(true);
}
void Draw(const GameTime&) override {
pipeline_->begin(Color::CornflowerBlue); // draws below go to the scene target
spriteBatch_->Begin();
spriteBatch_->Draw(*white_, Rectangle(400, 200, 160, 120), Color::White);
spriteBatch_->End();
pipeline_->end(); // runs the enabled passes, resolves to the back buffer
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> white_;
std::unique_ptr<RenderPipeline> pipeline_;
};
There is no Present() call: Game presents in EndDraw. On a renderer that cannot run the shader passes the same program still runs; each enabled pass copies its input, so the picture is the unprocessed sprite. To find out which it is, ask the passes rather than the renderer name:
const bool bloomRuns = CNA::Graphics::BloomPass(device).isSupported(device);
const bool shadowsRun = CNA::Graphics::ShadowMap(device, CNA::Graphics::ShadowQuality::Low).isSupported();
// Renderer-wide answers, when you need them:
const bool spirv = device.SupportsShaderLanguageEXT(CNA::ShaderLanguageEXT::SpirV, CNA::ShaderStageEXT::Fragment);
const bool sample = device.SupportsShadowSamplingEXT();
const bool ibl = device.SupportsImageBasedLightingEXT();
CNA::Logger::Info(CNA::Graphics::getEngineLayerVersionString(), CNA::LogCategory::RENDER); // "CNA engine layer 18"
// A human-readable feature/limit/format dump for bug reports (returns std::string_view; do not parse it):
const std::string report{device.GetRendererCapabilityReportEXT()};
API style and ownership
The engine layer has its own style, and a lint check enforces it: verbs are lowerCamelCase (begin, end, addUserPass), accessors are getBloomThreshold()/setBloomThreshold()/isSupported(), and every get/is/has is [[nodiscard]] (and const when it takes no argument). The XNA-style getXProperty() spelling appears only on EngineException (getSubsystemProperty() and friends). UpperCamelCase functions exist only as named exceptions: XNA base-class overrides, System.Object mirrors, and AsciiPostProcessEffect::Draw/GetLastGridDimensions for the C ABI.
Ownership follows three public shapes and no public shared_ptr: objects that return resources hand you a unique_ptr (EnvironmentProcessor's cubes); passes and skyboxes you attach are borrowed pointers that must outlive the pipeline (addUserPass(PostProcessPass*), setSkybox(Skybox*)); some helpers are attached by reference. Nothing may outlive its GraphicsDevice, and the layer is not thread-safe. RenderPipeline subscribes to the device's reset event and drops its targets on a device reset (it can also be released explicitly with releaseDeviceResourcesEXT(), but not inside a frame).
Stability
- Pre-1.0, and the whole project says so. This is a development snapshot after the alpha.1 tag; CNA's own version string is still
0.1.0-alpha.1. Nothing here is a release. - Revision 18.
CNA_CNAEXT_ENGINE_VERSION(a macro, so it records what your headers said) andgetEngineLayerVersion()(what the linked library was built with) start at 1 and rise whenever a consumer could notice a change — including additions. They exist so that a header/library mismatch is visible. They do not promise compatibility between two revisions. - It has already broken callers. CNA's changelog records revision 2 as an incompatible rename of six members to
lowerCamelCaseplus a behaviour change (shadow and skyboxisSupported()began asking for capability and source execution), then a burst of additions in revisions 3–18 (texture arrays, storage textures and buffers, base-instance and indirect draws, GPU timing, explicit shader languages, packages, selection and diagnostics, typed constant buffers). - What is settled and what is not. The design note calls the shape of the layer, the ownership rules, the two-part support question and the naming conventions settled; per-renderer behaviour, capability queries and compute/storage are still moving. Its advice is to pin a CNA revision.
- The C ABI is separate. The C API's ABI version (0.29.0 at this snapshot) does not move with the engine-layer revision.
Test evidence and what is CI'd
No CI workflow configures CNA with CNA_CNAEXT=ON. Of the 20 workflow files at this snapshot, the only one that mentions CNAEXT at all is the Devices workflow, and that concerns CNA_DEVICES. The engine layer's tests therefore run only on a developer's own cnaext build. We read the sources and CMake; we ran nothing, and every “registered” below means “a test exists”, not “it passes”.
| Evidence | What it is | Runs where |
|---|---|---|
| Unit tests | 92 test source files under modules/graphics-ext/tests, 967 TEST-family definitions (a source count, not a pass count). Registered only with CNA_BUILD_TESTS and CNA_CNAEXT. | A developer's cnaext preset build only. The preset's documented filter is CnaExt*:DepthEffect*:CRTEffect*:AsciiPostProcess*. |
| Example programs as tests | 32 example sources; 24 are registered as CNAEXT_* ctest programs (shadow map, cascades, point shadows, skybox, IBL, shadow receiver, instancing and LOD, compute and particles, clustered lights, bloom, render pipeline, FXAA, showcase, glTF PBR, GPU-driven, transparency, contact shadow, grading, GPU timing, SSAO, tonemap, post-process chain, HDR target, a leak loop). Each self-SKIPs (exit code 77) where the renderer lacks a capability, so a skip is not a failure. | Registered for every renderer except Emscripten and Windows; only OPENGLES3 is the preset's renderer. |
| Lint checks | Seven ctest entries: CNAEXT_GuardDiscipline (every engine-layer file wrapped in #ifdef CNA_CNAEXT), CNAEXT_NamingRule, CNAEXT_AccessorConventions, CNAEXT_DoxygenGroup (skips without doxygen), CNAEXT_MatrixCompleteness, CNAEXT_RendererDescriptorsParse, CNAEXT_NoPosixSetenv. | Registered whenever CNA_BUILD_TESTS is on, independent of CNA_CNAEXT. The general workflow runs an unfiltered ctest on OPENGLES3 with tests on and CNA_CNAEXT left at its default, so these fall inside that run. We did not see a run. |
| Strict-API harness | StrictXnaApiSurfaceCheck_Compile_Run and a WILL_FAIL leak check. | Covers Microsoft::Devices and Sensors only. |
CNA::Devices tests | 56 test definitions in 10 files under modules/devices-ext/tests. | The Devices and Sensors Tests workflow builds the devices-ubsan preset (CNA_DEVICES=ON) and runs them; it triggers on pushes and pull requests to next, develop and main that touch the module paths, and on manual dispatch. Not the engine layer. |
CNA's own unit tests generally use small synthetic scenes and A/B comparisons (“the same frame with exactly one subsystem switched off”) rather than golden images, and its example programs say plainly which capability they need. That is honest evidence that a feature was exercised on a developer's renderer; it is not evidence about the other renderers in the table above, and it is not a comparison with any reference renderer.
Limits and open questions
- IBL products are 8-bit.
EnvironmentProcessorwritesSurfaceFormat::Colorcubes and aColorBRDF table on the CPU, so a “high dynamic range” panorama is quantised before it is convolved. It is a load-time cost, and it works on any renderer that can create the textures. - HDR needs the
HiDefprofile. The default profile isReach, which refuses float render targets, multiple render targets, occlusion queries and 32-bit indices on every renderer; the engine layer's own examples requestHiDefwhen the adapter offers it, and so should you (Tutorial 152). - SSAO and friends need your help. Producing depth and normals means drawing your geometry a second time with a different effect, which only the game can do;
RenderPipelinedeliberately does not. - Cascades are app-driven.
RenderPipeline::setShadowScenedrives a singleShadowMap;CascadedShadowMap,SpotShadowMapandCubeShadowMapare used by callingbegin/endyourself, and the casters are drawn once per cascade. - No Windows or Apple evidence. DirectX 9/11/12 and Metal are outside the engine layer's packages anyway; the SDL_GPU Direct3D 12 and Metal routes and the cross-build claims were not verified.
- Some CNA documents are stale against the code. CNA's own
docs/cnaext-engine-layer.mdrenderer matrix (it says OPENGL4 has no shadow sampling or IBL, which the code contradicts) anddocs/cnaext-engine-changelog.md(C ABI 0.26.0) lag the snapshot; and several header comments (PbrEffect.hppsaying there is no image-based lighting,FileDialog/MessageBoxsaying support is unconditional) predate the code. This page follows the code. - The 12-to-98 growth is quantity, not maturity. The engine layer has no CI, one preset renderer, and a revision counter that CNA's changelog dates from revision 1 on 2026-08-18 to revision 18 on 2026-09-10.
Where to go next
- Tutorial 115: CNAEXT — extending beyond XNA — the marker, strict mode and the build gate, with code.
- Tutorial 153: Image-based lighting with PbrEffect —
EnvironmentProcessorandImageBasedLightEXTend to end. - Tutorial 154: Cascaded shadow maps —
CascadedShadowMapend to end. - Tutorial 114: PBR materials, Tutorial 116: post-process effects, Tutorial 117: the Devices layer.
- Effects System, Shader Effects, Renderers, Experimental C API, Verification & Known Issues.
- In the CNA repository at this snapshot:
modules/graphics-ext/include/CNA/Graphics/CNAEXT.hpp(the master include) andcnaext_showcase_test.cpp(every subsystem in one frame).
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- CNAEXT catalogue: extension surfaces by namespace — A catalogue of CNA's non-XNA surface at snapshot 009d40f5, namespace by namespace, with member names, reasons and boundaries, how much the CNAEXT marker covers, and what the strict check proves.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-094: SDL_GPU claims compute and SPIR-V shader intake on every device, including Direct3D 12 and Metal drivers that cannot take raw SPIR-V — SupportsComputeShadersEXT() and SupportsShaderLanguageEXT(SpirV) answer true whenever a device exists, but compute pipelines and ShaderEffect SPIR-V payloads reach SDL_gpu as raw SPIR-V, which its Direct3D 12 and Metal d
- CNA-BUG-097: RenderPipeline::releaseDeviceResourcesEXT() keeps the Bloom, SSAO and volumetric-fog target pools, and the memory estimate never counts them — 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 getGpuMemoryEstimat
- CNA-BUG-098: A throwing shadow-caster or transparent-phase callback leaves RenderPipeline and its ShadowMap in an unrecoverable state — 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 p
- CNA-BUG-181: Sdl3Platform reports the nativeFileDialog capability on iOS and web builds, where SDL has no usable file-dialog backend — The SDL3 platform sets nativeFileDialog unconditionally, so on iOS and the web the platform says a dialog can be shown while FileDialog::getIsSupportedProperty says it cannot, and FileDialog::Show* reach SDL, which fails
- CNA-BUG-274: README.md calls the CNAEXT purity check "a dedicated CMake build option", but CNA_STRICT_XNA_API is a compile definition on two harness targets and no CMake option exists — The README describes the compile-time CNAEXT purity check as a dedicated CMake build option; cmake/Harnesses.cmake only sets the CNA_STRICT_XNA_API compile definition on two check targets, and passing -DCNA_STRICT_XNA_AP
- CNA-GAP-031: ClusteredForwardEffect binds no material textures: many punctual lights cannot be combined with a texture set — The CNAEXT ClusteredForwardEffect consumes scalar material inputs only (base colour, metallic, roughness, IOR and the scalar clearcoat, sheen, iridescence and subsurface lobes) and binds no material textures, so a game t
- CNA-GAP-032: Engine-layer PBR limits: one punctual light per PbrEffect draw, 8-bit environment cubes, no back-face normal flip for double-sided materials, no alpha-tested shadow casters — PbrEffect takes one punctual light, EnvironmentProcessor builds its cubes as SurfaceFormat::Color, no stock or engine-layer shader reads a front-facing input to flip normals, and the shadow-caster shaders write depth wit
- CNA-VGAP-017: graphics-ext shader-package drift guards have holes: no reproducibility check for shadow_caster, none run in CI, desktop clustered-forward fragment unpinned — Two checked-in packages (shadow_caster and the test package modern_resource_interop) have no ShaderPackageReproducibility CTest, CI installs no shaderc so every such CTest skips, and only the ES clustered-forward fragmen
- CNA-VGAP-018: No test asserts RenderPipeline's fixed post-process order; the order-named tests only count passes — RenderPipeline::end() adds up to fourteen built-in passes and the user passes in a deliberate order, but the three tests named after that order assert only getLastFramePassCount(), so any permutation passes.
- CNA-VGAP-056: The checked-in shader bytecode headers of VULKAN, SDL_GPU and the Direct3D renderers come from generators that no CTest or workflow runs and that have no --check mode — spirv_shaders.hpp (VULKAN, SDL_GPU), hlsl_shaders.hpp (DIRECTX11/12) and the d3d9_*shaders.hpp headers come from hand-run generators with no --check mode and no CTest or workflow, so a shader source edit without regenera