CNAEXT Engine Layer

CNA snapshot 009d40f5

⚠

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.

PropertyCNAEXTCNA_STRICT_XNA_APICNA_CNAEXTCNA_DEVICES
What it isMarker macro (plus an EXT naming convention)Preprocessor defineCMake optionCMake option
DefaultExpands to nothingNever definedOFFOFF
EffectTags a declaration as “not XNA 4.0”; documentation and lint onlyTurns the marker into [[deprecated]] in the translation unit that defines itCompiles the CNA::Graphics engine layer (modules/graphics-ext, 98 public headers) and a few ShaderEffect overloadsCompiles CNA::Devices (modules/devices-ext, 17 public headers)
GranularityPer declarationPer translation unitPer buildPer 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

NamespaceCompiled whenContents
CNA::Graphics (module graphics-ext)CNA_CNAEXT=ONThe 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=ONClipboard, file dialogs, message boxes, power, system info, locale, display info, tray, camera, URL launcher — see Tutorial 88 and Tutorial 117.
Microsoft::Xna::Framework::GraphicsalwaysCNAEXT-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)alwaysRendererCapabilityProfile, 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 compiledNotes
PbrEffect, SkinnedPbrEffectMetallic-roughness PBR; Tutorial 114. Implement IShadowReceiverEXT; take an ImageBasedLightEXT.
IShadowReceiverEXT, ShadowCascadeStateEXT, PunctualLightEXT, ImageBasedLightEXT, AreaLightEXTThe 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 settersGetCompileErrorEXT(), GetShaderDiagnosticsEXT(), GetSelectedShaderLanguageEXT(), SetUniformVec3Array/SetUniformMat4Array. The portable-payload constructors (ShaderCodeEXT, ShaderPackageEXT) and the texture-array / storage-texture bindings are #ifdef CNA_CNAEXT.
ColorMatrixEffectA colour transform that only the CPU SpriteBatch path executes (GDI, SOFTWARE); Tutorial 116.
GraphicsDevice EXT queries and RendererCapabilityProfileSupportsRendererFeatureEXT, 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 importUnchanged since alpha.1 at the header level; Tutorial 112, Tutorial 113.
GamerServices::AvatarRenderer extension membersReal 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

OptionDefaultGates
CNA_CNAEXTOFFThe CNA::Graphics engine layer, a few ShaderEffect overloads, and the implementation behind the C API's engine-layer routes.
CNA_DEVICESOFFCNA::Devices (devices-ext).
CNA_STRICT_XNA_APInot a CMake optionMakes 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 libcna names both extension modules when they are present. The C API keeps one exported ABI regardless of the option: engine_layer.h declares its routes in every build and they return CNA_RESULT_NOT_SUPPORTED when 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.1This snapshot
graphics-ext public headers1298 (none removed)
graphics-ext implementation files (src/*.cpp)783
graphics-ext test source files392 (967 statically counted TEST-family macros)
graphics-ext example programs732
Packaged shader directories011 (156 GLSL source files; 208 manifest payload entries)
Engine-layer revision (CNA_CNAEXT_ENGINE_VERSION)none18
devices-ext public headers1717 (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:

  1. Does the renderer accept custom effects? GraphicsCapability::CustomEffects (and ThreeD for shadows and the prepass).
  2. 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 which GraphicsDevice::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, and 310 es for compute), desktop GLSL (330 core, and 430 core for compute), SPIR-V and WGSL. The default answer for SupportsShaderLanguageEXT is 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
OPENGL33Yesdesktop GLSLyes, yeswhen the context is GL 4.3 or newer
OPENGLES3, WEBGL2YesGLSL ESyes, yesES 3.1 or newer on native contexts; never on WebGL
OPENGLES2, WEBGL1Reported, unverifiedGLSL ES (the profile reports it)yes, yesno on WebGL; ES 3.1+ only
OPENGL4Yesdesktop GLSLyes, yesGL 4.3+ with native compute
VULKANYes (built-in passes)SPIR-Vyes, yeswhen the device supports it
WEBGPUYesWGSLyes, yesyes
SDL_GPUOnly in libshaderc builds — CustomEffects is true only where the SPIR-V/shaderc path is built (not Windows, Apple or Emscripten)SPIR-Vyes, yes (with a device)yes (with a device)
DIRECTX9, DIRECTX11, DIRECTX12No — they compile HLSL ShaderEffect source, but the engine ships no HLSL variant, so selectFor finds nothing usablenone from the engine's packagesno, nono
METALNo (CustomEffects is false)noneno, nono
FNA3DNo (CustomEffects is false; it runs compiled XNA effects instead)noneno, nono
SOFTWARENo (source is accepted for compatibility but never executed)noneno, nono
PORTABLEGLNononeno, nono
HEADLESS, STUBNo (no pixels)noneno, nono
SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SVG_DOM, GDI, FREEDIRECTNo (2D-only: no ThreeD)noneno, nono

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. OPENGLES2 and WEBGL1 report 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() == false because it consumes SPIR-V rather than the GLSL text. The built-in passes override the base-class check with their own CustomEffects-plus-package test, so Vulkan runs them; a user-written PostProcessPass that keeps the default isSupported() 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. Ask SupportsShaderLanguageEXT(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().

FeatureNeedsWhere it is missing
RenderPipeline with nothing enablednothingRenders 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 profileFalls 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 filteringisSupported() is false; the pass copies its input through. The chain still runs the pass.
SSAO, SSR, depth of field, contact shadowsas 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.
DepthNormalPrepassThreeD and a usable package; uses multiple render targets when the renderer keeps that promise (probed at construction), else two passesisSupported() false.
ShadowMap, CascadedShadowMap, CubeShadowMap, SpotShadowMapThreeD, CustomEffects, a usable caster packageThe constructor logs once and isSupported() is false; the frame renders unshadowed rather than failing.
Shadow receiving (IShadowReceiverEXT)SupportsShadowSamplingEXT() on the rendererThe 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.
EnvironmentProcessornothing (CPU only; 8-bit Color products)Works on any renderer that can create the textures.
Skybox, AtmosphericSkyCustomEffects and a usable packageDraws nothing, logs once.
ClusteredForwardEffectCustomEffects and a usable packageisSupported() false. CPU light assignment (ClusteredLightAssignment) needs no capability.
ClusteredLightComputeComputeShadersUse the CPU assignment.
ParticleSystemComputeShaders, Instancing, CustomEffects and packagesisSupported() false.
GpuInstanceCullerComputeShaders, IndirectDraw, CustomEffects and a packageUse the CPU culler.
InstancedRendererEXTInstancing and MultiStreamVertexInputisSupported() false. FrustumCullerEXT and LodGroupEXT are pure CPU.
WeightedBlendedTransparency (TransparencyMode::OrderIndependent)MultipleRenderTargets, a half-float render target, CustomEffects and a packageThe pipeline draws the Sorted phase instead and records the reason.
ComputeShader, StorageBuffer, StorageTexture2DComputeShaders (and IndirectDraw for indirect-argument buffers)Construction is refused.
GpuTimer, per-pass timinga renderer timer queryAccepted and does nothing; isGpuTimingEnabledEXT() then reports false. On EasyGL desktop GL has it from 3.3; ES needs EXT_disjoint_timer_query.
AsciiPostProcessEffectthe ability to read pixels back (Texture2D::GetData)Rejected with System::NotSupportedException on HEADLESS.
CRTEffect, DepthEffectCustomEffects; package variants in four languagesThe 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) and getEngineLayerVersion() (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 lowerCamelCase plus a behaviour change (shadow and skybox isSupported() 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”.

EvidenceWhat it isRuns where
Unit tests92 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 tests32 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 checksSeven 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 harnessStrictXnaApiSurfaceCheck_Compile_Run and a WILL_FAIL leak check.Covers Microsoft::Devices and Sensors only.
CNA::Devices tests56 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. EnvironmentProcessor writes SurfaceFormat::Color cubes and a Color BRDF 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 HiDef profile. The default profile is Reach, which refuses float render targets, multiple render targets, occlusion queries and 32-bit indices on every renderer; the engine layer's own examples request HiDef when 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; RenderPipeline deliberately does not.
  • Cascades are app-driven. RenderPipeline::setShadowScene drives a single ShadowMap; CascadedShadowMap, SpotShadowMap and CubeShadowMap are used by calling begin/end yourself, 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.md renderer matrix (it says OPENGL4 has no shadow sampling or IBL, which the code contradicts) and docs/cnaext-engine-changelog.md (C ABI 0.26.0) lag the snapshot; and several header comments (PbrEffect.hpp saying there is no image-based lighting, FileDialog/MessageBox saying 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