Tutorial 115: CNAEXT: Extending Beyond XNA

CNA Tutorials  ·  CNA Extensions

What you’ll learn

  • The three separate things in CNA that share the name CNAEXT, and how to keep them apart.
  • What the CNAEXT marker macro does — and, more importantly, what it does not do.
  • How CNA_STRICT_XNA_API turns "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 startTutorial 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:

CNAEXTCNA_STRICT_XNA_APICNA_CNAEXT
What it isA marker macroA compile definitionA CMake option
Where it comes fromCNA/CNAHelper.hppYou define it on your targetoption(... OFF) in the top-level CMakeLists.txt
DefaultExpands to nothingNever defined in a normal buildOFF
EffectTags one declaration as non-XNA. Purely documentation.Flips the marker to [[deprecated]]Compiles in the whole CNA::Graphics engine layer
GranularityPer declarationPer translation unitPer 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.

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: the class itself is an extension, but 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 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 gets an EXT suffix so that the extension is obvious at the call site, not just at the declaration:

avatarRenderer.DrawRealEXT(...);        // 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.

Where extensions live

Namespace placement is the second half of the convention, and it is a hard rule:

NamespaceContents
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, CNA::Devices, CNA::Input, CNA::Logger.

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. Scene- and frame-level orchestration — a render pipeline, a shadow subsystem, an IBL precompute — goes into CNA::Graphics instead, because it does not map onto any single XNA type.

One small trap: the engine namespace is CNA::Graphics, never CNA::CNAEXT. CNAEXT is a preprocessor macro, so it cannot be a namespace name.

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=ON to 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 -Werror it 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 it from both directions with a pair of targets and a pair of registered tests.

TargetCompiled withMust
cna_strict_xna_api_checkCNA_STRICT_XNA_API + -Werror=deprecated-declarationsBuild cleanly. It calls only members audited as genuine XNA/WP7 API.
cna_strict_xna_api_leak_checkThe same flagsFail 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.

These tests exist in the tree and are registered with CTest. As always, whether they pass in your environment is something only a run can tell you.

CNA_CNAEXT: the engine layer

The third thing sharing the name is a real CMake option, and it gates a body of code rather than a label:

# Default -- engine layer off
cmake -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3

# Engine layer on
cmake -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCNA_CNAEXT=ON

With it off, every CNA::Graphics header is an empty file — each one wraps its whole contents in #ifdef CNA_CNAEXT. 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.

What the option covers today:

Gated by CNA_CNAEXTNot gated — always compiled
AsciiPostProcessEffect, CRTEffect, DepthEffectPbrEffect, SkinnedPbrEffect
PbrMaterial, RenderPipelineSettingsShaderEffect, ColorMatrixEffect
The TonemappingMode, RenderQuality, ShadowQuality enumsMorph targets, SkinnedModelEXT, the tangent vertex formats
Everything else in CNA::GraphicsRuntime glTF loading, instancing

Read that right-hand column carefully, because it is the point people most often get backwards: PBR, morph targets and glTF import are not behind CNA_CNAEXT. They are marker-convention extensions and they work in a completely default build. The option gates the orchestration layer — the render pipeline, post-process passes, shadows, IBL, skybox and compute — of which the three post-process effects in Tutorial 116 are what ships today.

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:

  1. Leave CNA_CNAEXT and CNA_DEVICES off. Both default to off, so this means doing nothing.
  2. Stay out of the CNA:: namespace. If a fully-qualified name starts with CNA::, it is an extension by construction.
  3. Never write an identifier ending in EXT.
  4. 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 on CNA’s renderers with CNA’s limitations — compiled .fx shader bytecode is still unsupported, one renderer is still chosen at compile time. 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 engine-layer option CNA_NOXNA. Both were renamed to CNAEXT and CNA_CNAEXT in CNA’s 2026-08 naming normalisation — the same pass that renamed rendering "renderers" to renderers.

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