Verification & Known Issues

How CNA's XNA compatibility is actually checked — and what is verifiably not checked at all

Every count on this page is a count of test definitions, not a pass rate. The numbers come from counting GoogleTest macros in the source tree and test registrations in the CMake files. A static count cannot tell you how many of those tests pass — that needs a run. If this page ever disagrees with what ctest prints on your machine, ctest is right.

How CNA is verified

An XNA reimplementation can pass its own test suite perfectly and still be wrong, because the tests encode the author's belief about what XNA does. CNA works around that with five layers, three of which compare against something outside the project.

Layer What it checks Scale
Unit tests (GoogleTest) Math, geometry, curves, packed vectors, game-loop semantics, state objects, input, audio, networking, content readers. Runs headless. 568 CNA-owned C++ files under test paths, excluding vendored third_party; 539 contain 8,263 static GoogleTest definition macros. The instantiated run depends on configuration.
GPU pixel-readback tests (ctest) Renders a scene on a real device context, reads the framebuffer back to the CPU, and asserts on actual pixel values. This is what catches "the shader compiles but draws the wrong thing." CTest registrations are generated from the selected renderer, platform, audio, feature flags and available dependencies. Inspect each configured tree with ctest -N.
XNA 4.0 oracle corpus Scenes rendered by real XNA 4.0 and diffed against CNA's DIRECTX9 output at tolerance 0. Ground truth is the actual Microsoft runtime, not anybody's reading of it. 39 scenes under tools/xna-oracle/, with reference PNGs captured from real XNA
Differential testing against FNA A harness links a real, running FNA build and dumps reference values to JSON; a matching C++ tool dumps CNA's; a script diffs them key by key. Ground truth comes from executing FNA, not from reading its source. tools/fna-reference/, tools/cna-reference/, scripts/compare-fna-reference.py
API purity, checked by the compiler Every declaration that is not part of XNA 4.0 is tagged CNAEXT. The CNA_STRICT_XNA_API purity mode turns any use of a CNA extension into a compile error, and compile-time signature-freeze tests pin the public API so a signature cannot drift silently. CNA_STRICT_XNA_API, tools/devices/StrictXnaApiSurfaceCheck.cpp, StrictXnaApiSurfaceLeakCheck.cpp

What the test counts actually count

Alpha.1 deliberately needs scoped test facts. Static source inventory, instantiated GoogleTest cases, CTest registrations and executed results answer different questions.

NumberWhat it counts
568 files / 8,263 macros CNA-owned C++ files under test paths (vendored third_party excluded) and static GoogleTest-family macro definitions. Of the 568 sources, 539 contain at least one counted macro. Parameterization, conditional compilation and renderer-specific targets mean this is not an executable case count or pass result.
Build-local CTest inventory ctest --test-dir <build> -N is authoritative for a configured tree. Single vs multi renderer, test/example options, platform, audio and detected dependencies all change the result.

Neither number is a claim that the tests pass. They are static counts, reproducible with grep and a look at the CMake files — see Reproducing these numbers yourself. Deliberately absent from this page: per-renderer test counts, per-renderer line counts and per-renderer maturity scores. CNA keeps no machine-readable maturity registry, so any such table would be somebody's impression dressed up as data.

Renderer coverage is configuration-scoped

CNA exposes 50 renderer identities across 46 implementation families. Single-renderer builds remain the default, while CNA_GRAPHICS_RENDERERS can link a compatible set for runtime selection. Both shapes affect verification:

  • A green single-renderer run describes the selected family, not the other 45 families.
  • A multi-renderer run verifies the compiled set and runtime selection/fallback paths; it does not make every renderer share one capability contract.
  • Only the default gets the project-wide CNA_RENDERER_* definition. Renderer-present test definitions identify every family compiled into the test target.
  • 50 identities are not 50 equally complete renderers. Some are intentionally 2D-only or produce no pixels.

Read evidence at its real scope. Windows-only renderers have Linux/Wine evidence plus manual native-MSVC workflows, not one universal native-hardware claim. Capability reporting has improved per family, but the common default still returns true for legacy capabilities unless a renderer narrows them; MultiStreamVertexInput and CompiledEffects are explicit false-by-default gates.

The XNA 4.0 oracle corpus

This is the strongest verification claim CNA can make, so it is worth being exact about what it is — and about which renderer it applies to.

Under tools/xna-oracle/ sit 39 scenes together with 39 reference PNGs captured from real XNA 4.0 running under Wine + DXVK. CNA renders the same scenes and the images are diffed byte for byte.

RendererScenes matched at tolerance 0
DIRECTX939 / 39 Pixel-exact
OPENGLES111 / 39
EasyGL (the shared GL-profile implementation)10 / 39
FNA3D10 / 39

"Pixel-exact" is a statement about DIRECTX9 and nothing else. DIRECTX9 is deliberately held to a byte-identical authenticity bar because it is the renderer that targets XNA's own Direct3D 9 behaviour. Every other renderer is held to a "looks right" bar, and the oracle numbers above show the distance. CNA does not claim that all renderers are pixel-perfect, and neither should anyone quoting this page.

The corpus covers 39 scenes, not the API. It is ground truth for what it covers and silent about everything else.

Eight of the thirty-nine

Every image below was produced by the real Microsoft XNA 4.0 runtime, not by CNA and not by hand. CNA's DIRECTX9 renderer draws the same eight scenes and the diff must come back empty at --tolerance 0. They are reproduced throughout the tutorials where they illustrate the technique being taught.

A triangle on cornflower blue with red, green and blue vertex colours blended smoothly across its face.

Vertex-coloured triangle — see Tutorial 31.

A filled square built from a two-triangle strip, with red, blue, green and yellow corners interpolated across it.

Triangle strip — see Tutorial 40.

A quad showing a two-by-two checker texture at full brightness: red, white, white and blue.

Unlit textured quad — see Tutorial 32.

The same two-by-two checker quad darkened by lighting: dark red, grey, grey and navy.

The same quad with lighting on — see Tutorial 32.

Only the left half of a quad is drawn — a red block above a white block — the right half discarded by the alpha test.

Alpha test — all eight comparisons are in Tutorial 54.

A horizontal band of four blocks in the repeating order red, green, red, green, on cornflower blue.

Wrap texture addressing — see Graphics State.

A square shading from bright orange-brown at the top to near-black at the bottom, on cornflower blue.

Fresnel-weighted reflection — see Tutorial 56.

A skinned quad shading smoothly from white on the left to black on the right, on cornflower blue.

Four-bone skinning, per-pixel lit — see Tutorial 57.

These are deliberately austere scenes. A corpus like this is useful precisely because each frame isolates one piece of behaviour, so a diff that fails points at something specific. It is not a demonstration of what CNA can render — for that, see the demos.

What CI covers (and what it doesn't)

The alpha.1 tag contains 21 workflow files. They are not interchangeable: some run focused suites, some are source/coverage gates, some only build or link, and the native Windows graphics workflows require manual dispatch. File count is not a pass count: the intended unfiltered general job and two Input matrix rows still select the removed EASYGL identity and fail at configure time.

AreaEvidence at the tagImportant boundary
General LinuxFocused input, devices, arithmetic, glTF and renderer workflows; an intended unfiltered CTest jobgeneral-tests-ci.yml is broken at the tag by CNA_GRAPHICS_RENDERER=EASYGL. The same stale value breaks two of five Input rows; the other three use valid identities.
Runtime renderer selectionmulti-renderer-ci.yml exercises native sets and selection/fallback contractsOnly compatible families can share a binary.
EmscriptenBrowser renderer lanes plus emscripten-multi-renderer-ci.ymlThe multi-renderer lane establishes configure/build/link; browser lanes establish selected runtime behavior.
Platform abstractionplatform-ci.yml covers SDL3, real SDL2, Headless, Terminal and audio combinationsHardware-specific behavior is not implied by a conformance harness.
ApplemacOS/Metal plus iOS device final-link and simulator smoke lanesNo physical-iOS-device or pixel-correctness claim.
Native C APIFive workflows are declared for ABI baseline, compatibility, coverage, limitations and release readinessAn isolated tag build reproduced the final implementation failure: its C renderer map has 49 entries against 50 canonical identities. With networking off it fails earlier on an unconditional GamerServices include. Checked-in gate reports do not override either result.
Windows D3D/GDINative MSVC workflows existBoth are manual dispatch, not automatic merge gates.

Three distinctions prevent overclaiming:

  • A declared workflow is not evidence when its configuration is rejected; EASYGL is an implementation family, not one of the 50 selectable identities.
  • A configure or final-link lane does not prove runtime behavior.
  • A simulator or headless-browser result does not prove physical-device or every-GPU behavior.
  • Android has source and NDK support but no workflow in this tag.

Where coverage is thin or absent

These are not "lightly tested", and it is better that you hear it here.

  • Storage. The whole storage module carries five test macros, and StorageContainer — the type your save code actually touches — has none. The implementation is real std::filesystem IO; nothing asserts on it.
  • Renderer evidence remains per family. The 21 workflows include focused renderer gates, but they do not execute every capability of all 46 implementation families on every supported target.
  • Optional layers need their own configurations. CNA_CNAEXT, CNA_DEVICES and CNA_BUILD_C_API default to OFF; a default build says nothing about them. The alpha.1 C API cannot reach runtime verification because its final implementation fails its renderer-count assertion.
  • The XNA oracle diff. Covered above, and worth repeating: the project's most rigorous check is not automated.

Current limitations

What follows is the current list of user-facing limitations. Every entry is a declared boundary — a capability CNA does not claim, or a documented deviation — rather than an open defect report. Where an item once appeared here as a bug and has since been fixed or narrowed into a boundary, it has been removed rather than left standing as a warning.

Graphics and renderers

  • Compiled effects are renderer-qualified. XNA/FNA D3D9 Effect Framework bytecode works on FNA3D and on opt-in EasyGL, SDL_GPU and Vulkan builds. HLSL .fx source and MGFX remain unsupported; other renderer families report CompiledEffects false.
  • Runtime renderer selection is opt-in. A normal build still contains one renderer. Multi-renderer builds must use compatible sets, fallback is disabled by default, and selection latches after the first device.
  • Cube faces inside a multiple-render-target set are unimplemented on eight otherwise-capable 3D renderers: DIRECTX9, DIRECTX11, DIRECTX12, BGFX, the EasyGL family, SDL_GPU, OPENGL2 and OPENGL4.
  • DIRECTX1 is a declared stub — its entire 3D pipeline throws.
  • LLGL is experimental, Linux/X11-only and OpenGL-only; its Vulkan module cannot be selected. OPENGLES2 has no instancing.
  • SupportsCapability() still has a permissive legacy default. Renderers must narrow unsupported entries. Multi-stream input and compiled effects are false-by-default because both require explicit implementation.

Platforms

  • Web has no save persistence at all. Under Emscripten, SDL_GetPrefPath lands in volatile MEMFS; CNA mounts no IDBFS and never calls FS.syncfs. Every save is silently discarded on page reload.
  • On Web, Game must be heap-allocated. A stack-allocated Game is silently corrupted — there is no diagnostic, so this one is worth reading twice.
  • Video is absent entirely on Windows, Emscripten and Android. The video translation units are excluded from those builds, so the headers still compile and the link then fails on missing Video/VideoPlayer symbols.
  • FFmpeg is a hard requirement on Linux and macOS. No option disables it; configure fails without libavcodec, libavformat, libavutil and libswresample development packages.
  • Network discovery is SystemLink only, and nothing at all on Emscripten.

API members that always throw or no-op

  • 15 Guide::Show* overlay entry points are documented no-ops. (The Begin/End message-box and keyboard-input pairs are not among them — those are real implementations.)
  • A few methods always throw: Achievement::GetPicture(), PropertyDictionary::CopyTo(), NetworkMachine::RemoveFromSession(), ResourceContentManager::OpenStream, and BoundingFrustum::Intersects(Ray) in part.
  • 3D audio supports exactly one AudioListener; adding a second throws.

Content and decoding

  • .xnb files using Intel E8 preprocessing are not decoded.
  • DDS cube maps are limited to DXT1, DXT3 and DXT5.
  • .m4a and .aac are unplayable — SDL3_mixer ships no AAC decoder.
  • An XACT wave bank containing XMA or WMA data does not throw: it logs to stderr and returns nullptr, so the sound is simply missing. The XNB SoundEffectReader path does throw.
  • glTF retains unskinned rigid clips and factor-only PBR, validates required extensions and exposes structured import diagnostics. A mixed skinned model cannot also use Tag for extra rigid clips, so those tracks are omitted with a named diagnostic.

Build-time switches

  • CNA_CNAEXT (the CNAEXT engine layer, including AsciiPostProcessEffect, CRTEffect, DepthEffect, PbrMaterial and the CNA::Graphics surface) defaults to OFF.
  • CNA_DEVICES (the CNAEXT device layer) also defaults to OFF.

Deliberate deviations from XNA

These are not bugs and are not on a roadmap to be "fixed" — they are design decisions with real consequences for porting, so plan around them.

XNA does CNA does Why it matters
Ships custom shaders as compiled .fx bytecode Loads compatible Effect Framework binaries on qualified builds; also offers CNAEXT ShaderEffect Already-compiled XNA/FNA effects can be retained when the active renderer exposes CompiledEffects. CNA still does not compile HLSL source or accept MGFX.
Builds .xnb files with a design-time content pipeline Reads .xnb and never writes one CNA is read-side only: it consumes .xnb produced by XNA, MonoGame or FNA. There is no ContentImporter, ContentProcessor or ContentCompiler, and there will not be — that is explicitly out of scope. Call it an XNB loader, not a content pipeline.
Discovers XNB type readers by reflection Requires one explicit RegisterAllBuiltInXnbReaders() call at startup C++ has no reflection. There are 50 built-in readers (49 in a build without FFmpeg, which drops VideoReader), and none of them are registered until you make that call.
Selects a graphics device at runtime Defaults to a compile-time renderer, with opt-in compatible sets through CNA_GRAPHICS_RENDERERS A multi-renderer binary chooses before device creation and may use explicit fallback. The build default and the active renderer are deliberately separate answers.
Exposes Begin*/End* pairs on Storage and GamerServices that look asynchronous Runs the callback inline, before Begin* returns Faithful to XNA's own fake-async, but do not expect it to keep a frame moving. The work happens on the calling thread.
Runs GamerServices against Xbox LIVE Persists achievements and leaderboard entries as JSON on the local disk, under SDL_GetPrefPath Your achievements and leaderboards genuinely survive a restart on that machine. What does not exist is the online half: no sign-in against Microsoft's servers, no matchmaking, no cloud sync.
Supports Xbox LIVE matchmaking (PlayerMatch, Ranked) Implements SystemLink for real — genuine UDP with LAN broadcast discovery and measured RTT for QoS LAN multiplayer works across real machines. Internet matchmaking does not exist, and the other session types no-op rather than pretending.

Reproducing these numbers yourself

Nothing here requires special access. Run these from the exact tag checkout:

# CNA-owned C++ source files under test paths (vendored code excluded)
rg --files -g '*.cpp' -g '!third_party/**' |
  awk '$0 ~ /(^|\/)(test|tests)(\/|$)/' | wc -l

# Static GoogleTest-family definition inventory (not a pass count)
rg -n '^\s*(TEST|TEST_F|TEST_P|TYPED_TEST|TYPED_TEST_P)\s*\(' \
  -g '*.cpp' -g '!third_party/**' | wc -l

# What this configured build actually registers
ctest --test-dir build -N

Building and running the suite needs the sibling repositories checked out next to cna/. They are separate clones, not submodules:

git clone https://github.com/openeggbert/cna.git
git clone https://github.com/openeggbert/sharp-runtime.git   # required by every build
git clone https://github.com/openeggbert/easy-gl.git         # the GL-profile renderers
git clone https://github.com/openeggbert/meta-gl.git         # needed by easy-gl

# Linux also needs the FFmpeg development packages, or configure fails:
#   libavcodec-dev libavformat-dev libavutil-dev libswresample-dev

cd cna
git submodule update --init          # non-recursive is correct, and much faster

cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3
cmake --build build --target CnaTests

# everything registered with CTest (needs a GL context; use xvfb-run on a headless box)
ctest --test-dir build --output-on-failure

Swap OPENGLES3 for another identity to reproduce a different single-renderer slice, or configure a compatible CNA_GRAPHICS_RENDERERS set to exercise runtime selection. The resulting CTest inventory is the only count that applies to that configured build.

💡

Command traps worth knowing before you file a bug against your own shell history.

  • cmake --build build --target CNA no longer works. CNA is an INTERFACE umbrella target with no sources; build CnaTests, a demo target, or just cmake --build build.
  • D3D9, D3D11, D3D12, EASYGL, DX3 and ASCII are not valid renderer values today and stop configure with a FATAL_ERROR. The DirectX names are DIRECTX9, DIRECTX11, DIRECTX12; the old DX3 is now FREEDIRECT; the GL profiles are selected individually as OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 or WEBGL2; and the ASCII renderer became a CNAEXT post-process effect.
  • ctest -L D3D9 and ctest -R D3D11 match zero tests. The labels are DIRECTX9 and DIRECTX11.
  • Two equivalent selection forms exist and must not be mixed: -DCNA_GRAPHICS_RENDERER=<NAME> or -DCNA_RENDERER_<NAME>=ON with exactly one switched on.
  • The C++ framework has no general install/export/CPack rules, so C++ consumers add_subdirectory CNA. The experimental C layer declares its own CNACApi package, but alpha.1 cannot build the final implementation and therefore provides no verified consumable C package.
  • Configure presets available: web, tests, devices-asan, devices-tsan, devices-ubsan.

Found a discrepancy between this page and what your build reports? That is worth an issue — the whole point of publishing these numbers is that they are checkable.