Tutorial 115: CNAEXT: Extending Beyond XNA
What you’ll learn
- The three separate things in CNA that share the name CNAEXT, and how to keep them apart.
- What the
CNAEXTmarker macro does — and, more importantly, what it does not do. - How
CNA_STRICT_XNA_APIturns "please stay XNA-pure" into a compile error. - How to keep a port XNA-pure on purpose, and how to opt into extensions deliberately.
Before you start — Tutorial 20: Building and Running Your Game, since half of this tutorial is about compile definitions and CMake options.
CNA reimplements XNA 4.0. It also does things XNA never did: physically-based materials, runtime glTF loading, custom GLSL shaders, morph targets, clipboard access, native file dialogs. Those two goals pull in opposite directions, and CNA’s answer is not to pick a side but to label every extension, everywhere, and give you a build mode that rejects them.
This page is the authoritative explanation of that mechanism.
Three things called CNAEXT
Conflating these is the single most common source of confusion in the codebase, and CNA’s own design document opens by nailing down the boundary. There are three, they are unrelated, and only one of them is a build switch:
CNAEXT | CNA_STRICT_XNA_API | CNA_CNAEXT | |
|---|---|---|---|
| What it is | A marker macro | A compile definition | A CMake option |
| Where it comes from | CNA/CNAHelper.hpp | You define it on your target | option(... OFF) in the top-level CMakeLists.txt |
| Default | Expands to nothing | Never defined in a normal build | OFF |
| Effect | Tags one declaration as non-XNA. Purely documentation. | Flips the marker to [[deprecated]] | Compiles the CNA::Graphics retro effects, DebugDraw and shader-package types |
| Granularity | Per declaration | Per translation unit | Per build |
Said in one sentence each: CNAEXT labels, CNA_STRICT_XNA_API enforces the label, and CNA_CNAEXT gates an entirely different body of code that merely shares the name. A second build option, CNA_DEVICES, gates the separate CNA::Devices module (also off by default; Tutorial 117). The CNAEXT extensions reference covers all four in one place.
The marker macro
Here is the whole thing. It is not a simplification:
#ifdef CNA_STRICT_XNA_API
#define CNAEXT [[deprecated("CNAEXT: not part of the XNA 4.0 API surface")]]
#else
#define CNAEXT
#endif
In a normal build the macro expands to nothing at all. It generates no code, changes no layout, costs no cycles and affects no behaviour. It is a label a human and a compiler can both read.
It is applied at the point of declaration, and it can tag a whole type or a single member:
// A whole type that XNA never had:
CNAEXT struct MorphTargetDataEXT : public System::Object { /* ... */ };
// A single extension member on a type XNA did have:
CNAEXT void SetOwnedNormalMap(std::shared_ptr<Texture2D> texture);
A type can be XNA on one line and CNAEXT on the next. PbrEffect is instructive: its setWorldProperty(), setViewProperty() and setProjectionProperty() are unmarked because they come from the real XNA IEffectMatrices interface, while setMetallicFactorProperty() and the map setters carry the marker. The class declaration itself carries no marker — only its members do (whereas ShaderEffect and ColorMatrixEffect are marked as whole classes). So constructing a PbrEffect gives no warning under strict mode, and calling setMetallicFactorProperty() does. The granularity is genuinely per declaration.
CNAEXT is not a compile guard. Everything it marks is compiled into every build, unconditionally. Turning "CNAEXT off" is not a thing you can do — the closest equivalent is refusing to call the marked members, which is exactly what the next section is for.
The EXT suffix
The macro travels with a naming convention. When a new member is added to a type real XNA also has, its name usually gets an EXT suffix so that the extension is obvious at the call site, not just at the declaration:
GamePad::setKeyboardEmulationEnabledEXT(true); // extension
joysticks.GetJoysticksEXT(); // extension
graphicsDevice.DrawInstancedPrimitives(...); // real XNA 4.0 -- no suffix, no marker
That last line is worth pausing on. Instancing is real XNA 4.0 API, so it carries neither the marker nor the suffix, even though it feels modern. The rule tracks the XNA specification, not intuition — which is precisely why a mechanical marker beats guessing.
And the suffix is a convention, not a guarantee: some marked members have no EXT in their name — BasicEffect::SetOwnedTexture, Effect::Apply() (real FNA has no public Effect::Apply), and the class names ShaderEffect and ColorMatrixEffect. The marker is the authority; the suffix is a hint.
Where extensions live
Namespace placement is the second half of the convention, and it is a hard rule:
| Namespace | Contents |
|---|---|
Microsoft::Xna::Framework... | The XNA 4.0 surface, plus CNAEXT-marked members that naturally extend an XNA type — an Effect, a vertex format, a GraphicsDevice member. |
CNA:: | Everything that has no XNA counterpart to extend — CNA::Graphics (the opt-in extensions), CNA::Devices, CNA::Input, CNA::Logger, and cross-cutting value types such as CNA::RendererCapabilityProfile and CNA::ShaderLanguageEXT. |
Nothing new is ever invented inside Microsoft::Xna::Framework without the marker. The split follows the shape of the thing being added: per-object, per-draw shading extensions ship as marked members alongside the XNA types they extend, because that keeps porting simple and costs nothing when unused. Self-contained helpers that do not map onto any single XNA type — the retro post-process effects, DebugDraw — go into CNA::Graphics instead.
Two small traps. The extension namespace is CNA::Graphics, never CNA::CNAEXT: CNAEXT is a preprocessor macro, so it cannot be a namespace name. And CNA::Graphics does not imply the build gate — CNA::Graphics::VertexTypeRegistryEXT lives in the always-compiled graphics module.
The CNA::Graphics types also have their own style, different from the XNA layer’s: verbs are lowerCamelCase and accessors are getX()/setX()/isX(), not getXProperty(). Compare effect.setMetallicFactorProperty(0.5f) (XNA-style, on PbrEffect) with crt.setScanlineIntensity(0.35f) (on CNA::Graphics::CRTEffect).
CNA_STRICT_XNA_API: making purity a compile error
If you are porting an XNA or FNA title and want to guarantee you have not drifted onto CNA-only API, define CNA_STRICT_XNA_API and turn the resulting deprecation warning into an error:
add_executable(my_game_purity_check ${MY_GAME_SOURCES})
target_link_libraries(my_game_purity_check PRIVATE CNA)
target_compile_definitions(my_game_purity_check PRIVATE CNA_STRICT_XNA_API)
target_compile_options(my_game_purity_check PRIVATE -Werror=deprecated-declarations)
Every CNAEXT marker in every header you include is now a [[deprecated]] attribute. Touch one, and the build stops with the marker’s own message:
error: 'setMetallicFactorProperty' is deprecated:
CNAEXT: not part of the XNA 4.0 API surface [-Werror=deprecated-declarations]
Three things about this are worth stating plainly:
- It is not a CMake option. There is no
-DCNA_STRICT_XNA_API=ONto pass at configure time. It is a preprocessor definition you set on the target you want checked — which is the right granularity, since a project may well want one strict target and one ordinary one. - It is per translation unit. The library itself is not rebuilt, and CNA’s own internals are unaffected. Only the code compiled with the definition is checked.
- Without
-Werrorit is only a warning. Useful as a first inventory pass on an existing codebase: compile once, read the list, then decide what to do about each hit.
How CNA proves the mechanism still works
A purity check that quietly stopped catching anything would be worse than none, so CNA verifies the mechanism from both directions with a pair of targets and a pair of registered tests.
| Target | Compiled with | Must |
|---|---|---|
cna_strict_xna_api_check | CNA_STRICT_XNA_API + -Werror=deprecated-declarations | Build cleanly. It calls only members audited as genuine XNA/WP7 API. |
cna_strict_xna_api_leak_check | The same flags | Fail to build. It deliberately calls one CNAEXT-tagged member. |
The negative one is the interesting half. It is marked EXCLUDE_FROM_ALL so its expected failure never breaks an ordinary build, and its registered test is the build command, with CTest’s WILL_FAIL property set. The test therefore passes only when that build fails. If a future change to the macro ever stopped catching an extension call, the target would start building successfully and the test would flip to failing — naming the regression instead of hiding it.
The positive target’s source is worth a look if you ever need to settle an argument about whether some member is real XNA: it carries an explicit "deliberately NOT calling" list, member by member, with the audit that classified each one.
Know the harness’s scope. Both targets include only the Microsoft::Devices and Microsoft::Devices::Sensors headers (plus a few math types). The positive check therefore proves the mechanism and covers the Devices/Sensors surface; it is not an audit of the graphics, content or input API, and there is no strict-mode harness over those. They are registered only with CNA_BUILD_TESTS on GCC or Clang, and the Devices workflow builds and runs the positive check. As always, whether they pass in your environment is something only a run can tell you.
CNA_CNAEXT: the extension module
The third thing sharing the name is a real CMake option, and it gates a body of code rather than a label:
# Default -- extensions off
cmake -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3
# Extensions on
cmake -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCNA_CNAEXT=ON
# The repository's own preset: OPENGLES3, Debug, extensions and tests on
cmake --preset cnaext
With it off, every header of the graphics-ext module is an empty file — each one wraps its whole contents in #ifdef CNA_CNAEXT, and so does every implementation file. The headers still exist and still include cleanly; the types inside simply are not declared.
This is the trap. #include "CNA/Graphics/CRTEffect.hpp" succeeds without the option, and then CNA::Graphics::CRTEffect does not resolve. The error you get is "no type named CRTEffect", not "missing header" — so the fix is a CMake flag, not an include path.
Small, opt-in and outside CI. At this snapshot the gated module is 11 public headers: three standalone retro post-process effects — AsciiPostProcessEffect, CRTEffect and DepthEffect (colour-depth and palette reduction, not depth visualisation) — with their enums, the DebugDraw wireframe helper, the portable shader-package types ShaderCodeEXT and ShaderPackageEXT that those effects and ShaderEffect’s package constructors use, and the master include CNA/Graphics/CNAEXT.hpp. It is off by default and no CI workflow builds it; pin the CNA revision you build against.
What the option covers, next to what stays available in a default build:
Gated by CNA_CNAEXT (CNA::Graphics) | Not gated — always compiled |
|---|---|
Retro effects: AsciiPostProcessEffect (AsciiQuantizeMode), CRTEffect (CRTMaskType), DepthEffect (DepthEffectMode, DitherMode) | PbrEffect, SkinnedPbrEffect |
DebugDraw: lines, boxes, spheres, frusta and crosses drawn over a view and projection | ShaderEffect(device, vertexSource, fragmentSource) and its diagnostics; ColorMatrixEffect |
Shader authoring: ShaderCodeEXT, ShaderPackageEXT, and ShaderEffect’s portable-payload constructors | Shadow receiving: IShadowReceiverEXT, ShadowCascadeStateEXT, PunctualLightEXT; ImageBasedLightEXT |
The master include CNA/Graphics/CNAEXT.hpp | RendererCapabilityProfile and the GraphicsDevice EXT queries, including the GraphicsCapability::ComputeShaders answer |
| — | Morph targets, SkinnedModelEXT, AnimationPlayer, glTF import, the tangent vertex formats |
Read that right-hand column carefully, because it is the point people most often get backwards: PBR, image-based lighting state, shadow receiving, morph targets and glTF import are not behind CNA_CNAEXT. They are marker-convention extensions and they work in a completely default build. What CNA does not do in any configuration is generate shadow maps or image-based-lighting textures: an application renders or bakes those itself and hands them to the receivers (Tutorial 59 and Tutorial 114). The gated effects are the subject of Tutorial 116.
Which of the 14 renderer identities can run the gated effects is a per-effect question. CRTEffect and DepthEffect are ShaderEffects built from one shader package (GLSL ES, desktop GLSL, SPIR-V, WGSL and HLSL variants), so they need a renderer that executes custom shader source in one of those languages; AsciiPostProcessEffect works through texture readback and SpriteBatch; DebugDraw draws BasicEffect line lists on any 3D-capable renderer. The reference page has the table.
Staying XNA-pure on purpose
If your goal is a port that could in principle be built against real XNA, here is the whole recipe:
- Leave
CNA_CNAEXTandCNA_DEVICESoff. Both default to off, so this means doing nothing. - Stay out of the
CNA::namespace. If a fully-qualified name starts withCNA::, it is an extension by construction. - Never write an identifier ending in
EXT. - Add a strict target as shown above and keep it building. That is what catches the marked members inside
Microsoft::Xna::Framework, which the first three rules cannot see.
Rule 4 is the one that does the real work. It is easy to stay out of CNA:: by discipline; it is not easy to notice that SetOwnedTexture() on a BasicEffect is an extension while setTextureProperty() is not.
And the opposite case is just as legitimate. If you are writing a new game rather than porting one, extensions are the reason to choose CNA over a strict XNA reimplementation. Use them deliberately — the marker is there so that "deliberately" is a thing you can verify, not a thing you have to remember.
Purity is about the API surface, not about behaviour. A strict build still runs within CNA's renderer and platform boundaries: compiled XNA/FNA Effect Framework bytecode is renderer-qualified, and a multi-renderer choice latches before device creation. The check tells you your code does not depend on CNA-only API; it does not tell you the code would run identically on Microsoft's runtime.
A note on the old name
The marker macro used to be called NOXNA, and the build option CNA_NOXNA. Both were renamed to CNAEXT and CNA_CNAEXT in CNA’s 2026-08 naming normalisation — the same pass that renamed the graphics “backends” to renderers (CNA_GRAPHICS_BACKEND became CNA_GRAPHICS_RENDERER).
The new names are the only correct ones today. If you meet NOXNA in an old branch, an old article or a historical task ledger, read it as CNAEXT; the mechanism never changed, only the spelling. Historical documents and task identifiers keep their original names on purpose, so that the record stays traceable.
Where to go next
- CNAEXT extensions reference — what the module contains, which renderers run it, its stability and its evidence.
- Tutorial 116: CNAEXT Post-Process Effects — the retro effects and the colour-matrix effect beside them.
- Tutorial 59: Shadow Mapping — rendering your own shadow map and feeding it to the always-compiled receivers.
- Tutorial 117: The CNA::Devices Layer — the other opt-in extension module.
- Tutorial 114: PBR Materials — a marker-convention extension that needs no flag.
- Tutorial 52: Custom Shaders —
ShaderEffect, also always compiled. - Shader Effects reference