Graphics Renderers

CNA snapshot c1c316b9  ·  14 public identities  ·  12 implementation families

How renderers work

CNA separates the game-facing rendering API from the underlying graphics implementation with a pure virtual interface, IGraphicsRenderer. Game code never calls a graphics API directly, and never names a renderer.

The compact default is a single-renderer build. Choose its renderer at configure time with either form below; do not mix the forms:

# Long form: name the renderer
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=VULKAN

# Option form: exactly one CNA_RENDERER_<NAME> may be ON
cmake -S . -B build -DCNA_RENDERER_VULKAN=ON

CNA exposes 14 public renderer identities, implemented by 12 families. The three GL identities below (OPENGLES3, OPENGL33, WEBGL2) share one family, EasyGL; none of the public identities is an alias. The defaults are WEBGL2 under Emscripten, OPENGLES3 on Linux, and SDL_RENDERER everywhere else (Windows, macOS, iOS, Android and other Unix hosts).

Every identity has a stable, sparse C ABI value (SDL_RENDERER is 1, FNA3D is 43) that is never renumbered. The CMake list, the C++ enum, the CMake registry map, the C header and the C API table are held together by scripts/check_renderer_identities.py, which CTest runs as RendererIdentityRegistry. C++ enum ordinals, by contrast, are dense and are not a stable contract.

A name outside the 14 given to CNA_GRAPHICS_RENDERER or as a member of CNA_GRAPHICS_RENDERERS — a misspelling, a lower-case spelling, an identity that an older document or an alpha.1-era build script mentions but this snapshot does not provide — stops the configure step with a FATAL_ERROR that lists the supported names, and a retired identity is refused by name on the CNA_RENDERER_<NAME>=ON route as well. The check runs first, before SDL or any dependency is configured. There are no aliases, and CMake matches the public names case-sensitively (vulkan is an unknown name; VULKAN is not). One route is not covered: a CNA_RENDERER_<X>=ON switch whose name is neither a public nor a retired identity (-DCNA_RENDERER_D3D9=ON, -DCNA_RENDERER_EASYGL=ON, a typo) is a cache entry nothing reads, so the configure does not fail and builds the platform default renderer (at most CMake's generic warning about a manually specified variable that was not used). In old scripts name the renderer with CNA_GRAPHICS_RENDERER, and read the CNA: Using <X> graphics renderer line of the configure log.

💡

Which CNA does this page describe? CNA snapshot c1c316b9 (9 October 2026), the tip of branch apple/m4-stabilization, which still reports the product version 0.1.0-alpha.1. A plain git clone https://github.com/libcna/cna.git gives the older alpha.1 tag; use git clone -b apple/m4-stabilization and check out c1c316b9 for what is documented here, with sharp-runtime and meta-gl on their branches of the same name and easy-gl on develop (the branch rule CNA’s own CI applies).

💡

Windows and macOS default to a 2D-only renderer. Because the default there is SDL_RENDERER, a game that draws 3D must name a 3D renderer explicitly (OPENGL33, VULKAN, SDL_GPU, DIRECTX11, METAL …). iOS and Android take the same default; whether an Android build works end to end is not verified here.

⚠

14 identities are not 14 equally complete renderers. They differ in scope, dependencies, platform gates and verification. Treat an identity as a named implementation contract, not as a promise of feature parity with every other row — the capability matrix and the declared maturity table show the differences.

Multi-renderer builds and runtime selection

This snapshot supports an opt-in build mode that links several compatible renderer families into one binary. CNA_GRAPHICS_RENDERERS is a semicolon-separated set; CNA_GRAPHICS_RENDERER remains the default and must be a member of that set (otherwise the configure step fails; nothing is silently substituted). Omitting the plural option keeps the ordinary single-renderer build. Duplicates are removed, the default is moved first, and the configure log prints CNA: renderer set -- <list> (default: <X>).

cmake -S . -B build-multi -G Ninja \
  -DCNA_GRAPHICS_RENDERER=HEADLESS \
  -DCNA_GRAPHICS_RENDERERS="HEADLESS;SOFTWARE;STUB"

Before the first GraphicsDevice is created, CNA::GraphicsRendererSelection resolves the renderer in this order: an explicit SetPreferred() call, the CNA_GRAPHICS_RENDERER environment variable, then the compile-time default. In an Emscripten build the page property Module.cnaPreferredRenderer is consulted at the environment variable's precedence (a real environment variable wins over it), and the exported Module._cna_set_preferred_renderer(name) behaves like an explicit SetPreferred(). A name that is unknown or not compiled in throws; CNA never substitutes silently.

#include <array>
#include "CNA/GraphicsRendererSelection.hpp"

using CNA::GraphicsRendererSelection;
using CNA::GraphicsRendererType;

GraphicsRendererSelection::SetPreferred(GraphicsRendererType::Vulkan);   // or SetPreferred("VULKAN")

constexpr std::array chain{GraphicsRendererType::OpenGLES3,
                           GraphicsRendererType::Software};
GraphicsRendererSelection::SetFallbackChain(chain);                      // opt-in

MyGame game;
game.Run();

On a native build the environment route is CNA_GRAPHICS_RENDERER=VULKAN ./mygame; in a page it is var Module = { cnaPreferredRenderer: "WEBGPU" };. Names are case-insensitive at run time (unlike CMake).

Fallback is off by default. SetFallbackChain() (which takes a std::span, so pass an array rather than a bare brace list) or EnableAutomaticFallback(true) opts in; every skipped renderer is recorded in GetFallbackHistory() with a reason (NotCompiledIn, ProbeUnavailable, InitializationFailed, WindowKindConflict). GetSelected() is the renderer CNA will try, GetActive() is what actually started, and GraphicsDevice::GetGraphicsRendererType() reports the device's real renderer. The compile-time getCurrentGraphicsRendererType() still reports only the build default.

⚠

Selection latches at the first successful device. The latch closes when the first GraphicsDevice has successfully created a renderer. After that, SetPreferred, SetFallbackChain and EnableAutomaticFallback throw InvalidOperationException. A failed creation does not latch, so a game may catch the error and try another configuration; GetSelected() never latches; recreating the same renderer on a live device (Reset, an MSAA change) and creating a second GraphicsDevice keep the selection. Fallback across OpenGL- and Vulkan-kind windows can recreate a CNA-owned window, but a caller-owned DeviceWindowHandle cannot be recreated; that candidate is skipped as WindowKindConflict.

Two device flags ask for a specific renderer: GraphicsAdapter::UseNullDevice requires HEADLESS and UseReferenceDevice requires SOFTWARE (C++ accessors setUseNullDeviceProperty and setUseReferenceDeviceProperty). If that renderer is not compiled in, or the selection already latched to another one, device creation fails with NoSuitableGraphicsDeviceException; a fallback chain cannot satisfy them with a different renderer. For testing, the environment lists CNA_DEBUG_UNAVAILABLE_RENDERERS and CNA_DEBUG_FAIL_RENDERER_INIT simulate an unavailable or failing renderer, and CNA_FORCE_HEADLESS_DEVICE_EXT creates the named renderers' device without a window. The full API, fallback ordering and C ABI surface are on Runtime Renderer Selection.

Combination rules

Multi-renderer builds add code size and every selected family's dependencies. This snapshot rejects three demonstrated conflicts at configure time, each with a reason in the error:

  • Windows-only, Emscripten-only and Apple-only identities cannot cross platform partitions: one toolchain cannot target both.
Platform partitionIdentitiesEnforced by
Windows only (2)DIRECTX9, DIRECTX11FATAL_ERROR unless CMAKE_SYSTEM_NAME is Windows (native MSVC/MinGW or the mingw-w64 cross toolchain)
Emscripten only (1)WEBGL2Refused on any other target; OPENGLES3 and OPENGL33 are refused under Emscripten
Apple only (1)METALRefused on every system except macOS and iOS (on iOS it is one of the two renderers the Apple allow-list admits)
Everything else (10)All other identitiesNo operating-system partition; each family's own dependencies still apply

Several EasyGL identities may coexist in one binary — the GL profile is a runtime value — provided they do not cross the native/Emscripten partition. In a multi-renderer build only the default renderer's CNA_RENDERER_<IDENTITY> macro is defined project-wide; CNA_MULTI_RENDERER is defined when more than one is compiled in. The rules are checked by scripts/check_renderer_combinations.py.

Names that changed (and one trap)

The renderer vocabulary was normalised. If you are following an older document, translate first — the old spellings are not aliases, they are unknown values that stop the configure step with a FATAL_ERROR when given to CNA_GRAPHICS_RENDERER or CNA_GRAPHICS_RENDERERS (an old-style -DCNA_RENDERER_D3D9=ON switch is simply not read; see above). The old option name behaves the same way: no build file at this snapshot reads CNA_GRAPHICS_BACKEND, so -DCNA_GRAPHICS_BACKEND=VULKAN is accepted without an error and the platform default renderer is configured (read from the CMake files; not executed). In old scripts, rename the option itself.

Old nameToday
CNA_GRAPHICS_BACKENDCNA_GRAPHICS_RENDERER
IGraphicsBackendIGraphicsRenderer
EASYGLOne of OPENGLES3 / OPENGL33 / WEBGL2
D3D9 / D3D11DIRECTX9 / DIRECTX11
ASCIINot a renderer. It is now AsciiPostProcessEffect, a CNAEXT effect (and CNAEXT is off by default: -DCNA_CNAEXT=ON)
⚠

Case matters in CMake, not at run time. -DCNA_GRAPHICS_RENDERER=vulkan stops the configure with unknown graphics renderer 'vulkan', while GraphicsRendererSelection::SetPreferred("vulkan") and the CNA_GRAPHICS_RENDERER environment variable accept it.

Test labels moved with the names too: ctest -L D3D9 and ctest -R D3D11 match nothing. The labels are DIRECTX9 and DIRECTX11.

The 14 renderers at a glance

One row per public identity, in C ABI order. Scope comes from the code-declared ThreeD capability; maturity is CNA's own declaration (see Declared maturity), not a measurement. Detail, dependencies and caveats follow in the per-family sections.

RendererC ABIFamilyScopeDeclared maturityPlatform gate
SDL_RENDERER1SdlRenderer2D onlyProductionAny SDL3 platform (default off Linux/web)
OPENGLES33EasyGL2D + 3DProductionNative hosts (Linux default)
OPENGL334EasyGL2D + 3DProductionNative hosts
WEBGL26EasyGL2D + 3DSupportedEmscripten only (default)
VULKAN8Vulkan2D + 3DProductionVulkan loader + a platform with a Vulkan surface
WEBGPU9WebGPU2D + 3DExperimentalNative (wgpu-native) and browser (emdawnwebgpu)
HEADLESS11HeadlessNo pixelsSupportedAny host
SOFTWARE12Software2D + 3DExperimentalAny host (off-screen; displayed on Terminal)
STUB13StubNo pixelsSupportedAny host
DIRECTX1114DirectX112D + 3DProductionWindows only
DIRECTX922DirectX92D + 3DProductionWindows only
SDL_GPU31SdlGpu2D + 3DSupportedAny host SDL3's GPU API supports
METAL42Metal2D + 3DSupportedmacOS and iOS
FNA3D43Fna3d2D + 3DExperimentalAny host FNA3D + SDL3 support

The gaps in the C ABI numbering are deliberate: the values that are not listed are permanently reserved and refused by every C route, and the next new identity would take 52. CNA's stated policy is a curated set — a renderer earns a place with meaningful platform coverage, compatibility value, architectural value or a capability the set does not cover, and "renderer count is not a goal"; no new identity is planned.

The GL family — three profiles, one EasyGL implementation

Three renderer identities share one internal implementation, EasyGL, which lives in the ../easy-gl sibling repository (itself needing ../meta-gl). The profile is a runtime value inside the family, so the two native profiles can be linked into one binary; WEBGL2 sits on the other side of the native/Emscripten partition.

RendererWhat it isScopePlatformDependency
OPENGLES3OpenGL ES 3.0 context (GLSL ES 3.00) via EasyGL — the Linux default2D + 3D; MSAA, anisotropy and wireframe by runtime probe; MRT, occlusion, Texture3D, multi-stream and instancing yes; shadow sampling and IBL claimedNative (non-Emscripten) hosts; CMake warns "primarily tested on Linux" elsewhere../easy-gl + ../meta-gl; a GL context from the SDL3 platform
OPENGL33Desktop OpenGL 3.3 core profile (GLSL 3.30) via EasyGL2D + 3D, as OPENGLES3 (native wireframe); on macOS the core-profile context Apple provides, where the campaign ran EasyGL’s custom-shader, compiled-effect and oracle testsNative hostsSame as above
WEBGL2Browser WebGL 2 (GLSL ES 3.00) via EasyGL — the Emscripten default2D + 3D, as OPENGLES3 (no compute or indirect draw in a browser)Emscripten onlySame; the link contract pins WebGL 2

Hand-written ShaderEffect shaders execute on all three: GLSL ES on OPENGLES3 and WEBGL2, desktop GLSL on OPENGL33. Compute shaders need ES 3.1 / GL 4.3 and indirect draw needs ES 3.1 / GL 4.0, so on this family they are reported only where the context provides them, and never under WebGL. Compiled XNA effects are opt-in with CNA_EASYGL_COMPILED_EFFECTS. Against the 39-scene XNA oracle corpus, EasyGL is gated on two line scenes at tolerance 0 (EasyGL_XnaLineCoverage); a whole-corpus measurement recorded 10 of 39 on 2026-08-11, before later pixel-centre fixes — see How renderers are verified. Step-by-step use is in Tutorial 102.

"EasyGL" remains the correct name for the implementation and for the sibling library. It is no longer a value you can pass to CNA_GRAPHICS_RENDERER.

Modern GPU APIs

Four renderers drive a modern explicit GPU API: two of them directly (VULKAN, METAL), two through a portable layer (WEBGPU, SDL_GPU).

RendererWhat it isScopePlatformDependency
VULKANDirect Vulkan renderer (instance API 1.1 requested); declared Production, and the renderer with the most registered example and test executables2D + 3D; the most complete modern surface: compute, indirect draw, base-instance, GPU timers, float and half render targets, shadow sampling, IBL (device-conditional)Any host with a Vulkan loader and a Vulkan surface from the SDL3 platform (Windows, X11 and Wayland windows through SDL’s video drivers); not Terminal or HeadlessVulkan SDK/loader (find_package(Vulkan REQUIRED))
WEBGPUWebGPU renderer; declared Experimental2D + 3D; MRT, occlusion queries (counts reported as imprecise on a Metal adapter), wireframe, multi-stream, instancing, MSAA (probed), float render targets (probed), half-float linear filtering, computeNative (Linux, macOS, Windows through wgpu-native) and the browser (Emscripten emdawnwebgpu port)Pinned wgpu-native v29.0.1.1 (auto-downloaded, SHA-256 pinned) or the Emscripten port
SDL_GPURenderer on SDL3's GPU API (SPIR-V; SDL selects the driver); declared Supported2D + 3D; MSAA and stencil device-probed, MRT, instancing, compute, indirect draw, float targets, shadow sampling, IBL; no occlusion queries; custom ShaderEffect only where libshaderc existsAny host SDL3's GPU API supports; requires the SDL3 platformSDL3; SDL_shadercross + SPIRV-Cross (default ON on Windows and Apple); optional libshaderc
METALApple Metal directly; SDL3 supplies only the window and the view that holds the CAMetalLayer; declared Supported2D + 3D: MSAA (from the device’s sample counts), MRT, occlusion queries, instancing and multi-stream input, float, half-float and packed surface formats, DXT, cube and volume textures; custom effects as SpriteBatch-scoped MSL; compiled XNA effects opt-in; no compute or indirect drawmacOS and iOS (AppKit view on macOS, UIKit view on iOS); tvOS refusedObjective-C++; AppKit or UIKit, Metal, QuartzCore and Foundation frameworks; SDL3

The "modern graphics" work landed on VULKAN, SDL_GPU, WEBGPU and the Direct3D 11 renderer: float and half-float render targets, shadow and image-based-lighting sampling, and renderer-internal compute, indirect-draw and GPU-timer support that the capability queries report (see Modern features by renderer). VULKAN is covered by Tutorial 85, SDL_GPU by Tutorial 131, WEBGPU by Tutorial 132 and METAL by Tutorial 109.

METAL is declared Supported. CNA’s Apple campaign qualified it on a physical Mac mini M4: the full CTest tree passed (11,015 tests, 114 skipped by name, none failed), the Metal-labelled set of 261 tests — 101 shared pixel fixtures, 146 renderer-neutral oracle scenes and 14 native tests — passed under the Metal validation layers in Debug and in a Release build, and all 91 ported XNA samples passed the automated renderer matrix. CNA’s verdict is still not primary-production: compiled XNA effects are off unless CNA_METAL_COMPILED_EFFECTS=ON, custom effects are SpriteBatch-scoped MSL, on-screen presentation was never observed (the console was locked), PresentInterval.Two presents as One, the backbuffer is always BGRA8, the sampler LOD bias needs the macOS or iOS 26 SDK and OS, and no soak run has bounded its caches. No Intel Mac was run. On iOS, METAL ran in the iOS Simulator (iPhone 17 profile) with an exact pixel probe and its device build final-links; no physical iPhone or iPad ran it. In CI, metal-macos-ci.yml builds it and runs the ^Metal tests on GitHub’s macos-26 runners (a paravirtual GPU without shader validation). tvOS remains unsupported.

2D-only renderers

One identity is 2D by design (ThreeD is false in code): SDL_RENDERER. Its 3D entry points do not silently draw nothing — a failure mode you can test for. (STUB also reports ThreeD false, but it is a no-op renderer, not a 2D renderer.)

RendererWhat it isScopePlatformDependency
SDL_RENDERERSDL3's built-in accelerated 2D renderer — the default on Windows, macOS, iOS, Android and other non-Linux hosts, and one of the two renderers the iOS allow-list admits (with METAL); declared Production2D only; additive blending is the only capability answering true; 3D calls throw or warn-and-stubAny SDL3 platformSDL3 (vendored); requires the SDL3 platform

What a 3D call does on a 2D-only renderer

The behaviour is a setting, Unsupported3DGraphicsCallBehavior, changed with GraphicsDevice::SetUnsupported3DGraphicsCallBehavior (or, from C, cna_graphics_device_set_unsupported_3d_call_behavior). Throw is the default and raises std::runtime_error("<renderer> does not support 3D: <method>"); WarnAndStub logs one warning per operation and returns a safe no-op or null object. SDL_RENDERER honours WarnAndStub. WarnAndStub does not change any SupportsCapability() answer.

Windows renderers: Direct3D 9 and 11

Two renderers are hard-gated to CMAKE_SYSTEM_NAME=Windows and stop the configure step with a FATAL_ERROR anywhere else. They are the Direct3D generations that matter to an XNA-shaped framework — Direct3D 9 (the API real XNA 4.0 itself ran on) and 11 — using the genuine COM interfaces rather than emulating them over another API. (SDL_GPU can also reach Direct3D 12 on Windows, as one of the drivers SDL’s GPU API chooses from.)

⚠

The repository provides manual Wine plus DXVK validation paths, not an automatic Wine lane. A native-Windows MSVC workflow exists — d3d-windows-ci.yml for DIRECTX11 — but it is manual-dispatch only, so nothing on real Windows runs automatically. Its recorded dispatch on GitHub’s windows-latest image (2026-10-06) passed all 300 CTest entries, with the hardware smoke skipped for want of a GPU after its SDL3 window, swap-chain and resize checks passed on Microsoft’s Basic Render Driver; Direct3D 11 was earlier validated by hand on one physical Windows 11 machine with an Intel GPU. The reference images of the oracle corpus were captured under Wine + DXVK on Linux, not on Windows. Treat any Wine result as evidence for that exact manually run configuration.

RendererWhat it isScopePlatformDependency
DIRECTX9Real Direct3D 9; the only renderer with a full exact-match CTest against the real-XNA-4.0 reference corpus (tolerance 0, run under Wine + DXVK); declared Production2D + 3D; inherits the permissive capability default wholesale (MSAA, instancing, Texture3D, custom effects true; multi-stream false; MRT bounded by profile: Reach 1, HiDef 4); no float render targets; no shadow or IBL claimWindows only (native or mingw-w64 cross-build)d3d9, d3dcompiler (Windows SDK)
DIRECTX11Real Direct3D 11 (feature level 11_0+, negotiating 11_1 down); declared Production2D + 3D; MRT, occlusion, wireframe, instancing, Texture3D, custom effects, float and half-float render targets (device format queries); renderer-internal compute and indirect draw at feature level 11_0; no shadow or IBL claimWindows onlyd3d11, dxgi, d3dcompiler; shared common/d3d helpers

Two things are worth calling out in this group:

  • DIRECTX11 reports compute, indirect draw and GPU timers from renderer-internal implementations; renderer_capability_truth_test.cpp checks that the capability answers match those implementations. There is no public compute class for game code.
  • Only DIRECTX9 has no SupportsCapability() override. It relies on the permissive base defaults wholesale, so its true answers are the least informative; DIRECTX11 uses a switch with no default arm (an unknown capability answers false).

Custom ShaderEffect shaders on this group are HLSL, compiled at run time with D3DCompile (DIRECTX9: vs_3_0/ps_3_0 under HiDef, vs_2_0/ps_2_0 under Reach). Compiled XNA effects are opt-in with CNA_DIRECTX9_COMPILED_EFFECTS and CNA_DIRECTX11_COMPILED_EFFECTS. DIRECTX9 is the only renderer recorded as pixel-exact against the XNA oracle corpus — see How renderers are verified. A guided tour is Tutorial 103.

Browser renderers

Two renderers draw in a browser. WEBGL2, from the GL family, exists only there and is the Emscripten default. WEBGPU also has a browser route (the browser’s own WebGPU through the emdawnwebgpu port) but is not Emscripten-only, because it also builds natively through wgpu-native. A page uses WebGPU where the browser offers it and WebGL 2 elsewhere; both can be linked into one bundle.

The Emscripten multi-renderer workflow builds one bundle containing WEBGL2 and WEBGPU and asserts that both renderer archives, both registry entries and the JavaScript selection surface are present — it builds and inspects symbols; it does not run them. Under Emscripten a WEBGPU build needs a larger Asyncify stack, which the build now sizes automatically. The strongest browser evidence is outside CI: the 90 sample builds at samples.libcna.com run on WEBGL2 and were each run in Chrome when they were published.

To pick a renderer from the page, set the property before the module starts: var Module = { cnaPreferredRenderer: "WEBGPU" }; (or call Module._cna_set_preferred_renderer); the bundle must have been built with that renderer in CNA_GRAPHICS_RENDERERS. Tutorial 105 and Tutorial 132 walk through both routes.

⚠

Web caveats apply to both renderers in this group (to WEBGPU when it is built for the browser). CNA itself provides no save persistence on the web: this snapshot's sources mount no IDBFS and never call FS.syncfs, so the storage root (resolved from XDG_DATA_HOME, LOCALAPPDATA or HOME, not from SDL_GetPrefPath) lands in the default volatile in-memory file system unless your page adds persistence. There is no video: FFmpeg is never built for Emscripten, so the Video types exist and link but throw NotSupportedException at run time. Unlike alpha.1, your Game object no longer has to be heap-allocated: Game::Run() now blocks through Asyncify, so MyGame game; game.Run(); is fine. See Platforms.

Translation and adapter layers

Three renderers do not name one native graphics API. Each targets a portable layer that itself picks a native API — sometimes at build time, sometimes at run time. CNA classifies SDL_RENDERER, WEBGPU, SDL_GPU and FNA3D as its TranslationLayer category; the three below are the ones where the indirection changes what your code does. That indirection is the point, and it is also where the surprises live.

RendererWhat it isScopePlatformDependency
SDL_GPUSDL3's GPU API; SDL picks the native driver and CNA feeds it SPIR-V2D + 3DAny host SDL3's GPU API supportsSDL3; SDL_shadercross + SPIRV-Cross; optional libshaderc
WEBGPUWebGPU: wgpu-native natively, the browser's own WebGPU through emdawnwebgpu on the web2D + 3DNative desktop hosts and EmscriptenPinned wgpu-native v29.0.1.1 or the Emscripten port
FNA3DFNA-XNA's XNA-shaped C graphics library; it chooses SDL_GPU, Direct3D 11 or OpenGL at run time (override with FNA3D_FORCE_DRIVER); declared Experimental2D + 3D; compiled effects always on; instancing and MSAA follow FNA3D; no float render targets, no computeAny host FNA3D + SDL3 supportFetched FNA3D 3240147 + MojoShader; SDL3

Group-specific caveats, all of which change what your code does rather than merely how fast it runs:

  • SDL_GPU is an SDL3 API, so it requires the SDL3 platform. A configure with CNA_ENABLE_SDL=OFF refuses it by name. On Windows and Apple SDL_shadercross is on by default (CNA_SDL_GPU_SHADERCROSS); Linux and Android use the committed SPIR-V natively. A custom ShaderEffect additionally needs a target-native libshaderc, so it is unavailable on Windows, Apple and Emscripten builds.
  • WEBGPU is Experimental. Its native route downloads a pinned wgpu-native release unless you give CNA_WEBGPU_ROOT; its browser route needs no native library. No CI workflow names it.
  • FNA3D enables XNA/FNA compiled Effect Framework bytecode automatically. It executes those binaries through MojoShader and cannot compile ShaderEffect source at all. Its Fna3d_XNA_Oracle test renders the whole corpus but gates only that every scene renders, not that it matches. See Tutorial 108.

CPU and no-GPU renderers

Four renderers need no GPU, no driver and no display server. Two of them produce real pixels on the CPU; two produce none at all.

RendererWhat it isScopePlatformDependency
SOFTWARECNA's own CPU rasterizer that owns its framebuffer; no window, no GPU library; declared Experimental2D + 3D; MSAA, MRT (up to 4), occlusion, instancing, wireframe, Texture3D storage, anisotropy, float and half-float targets; no custom ShaderEffect execution (fixed stock path); compiled effects opt-in; no shadow or IBLAny host; off-screen (readback) except on CNA_PLATFORM=TERMINAL, where it is displayedNone (compiled effects: MojoShader via CNA_SOFTWARE_COMPILED_EFFECTS)
HEADLESSA no-GPU, no-window harness that validates arguments and counts or traces calls (CNA_HEADLESS_MODE = Fast, Validation (default) or Trace); renders nothingNo pixel output; reports 3D, depth/stencil, MSAA and instancing true (nothing is rasterized); Texture3D, additive and multi-stream falseAny host; selected by UseNullDeviceNone
STUBThe smallest possible complete renderer: renders nothing, keeps no bookkeepingNo pixel output; all 19 capabilities falseAny host; the default of the dev and unit presetsNone
💡

SOFTWARE never presents to a window. It rasterizes for real and stays off-screen in an SDL3 window (on Windows, X11 or Wayland alike): you read the result back with GetBackBufferData() rather than watching it appear in a window. That makes it useful for deterministic, GPU-free pixel checks. The one deliberate exception is SOFTWARE on CNA_PLATFORM=TERMINAL attached to a TTY, where each finished frame is handed to the terminal platform's surface presenter; Terminal accepts only SOFTWARE, HEADLESS and STUB, and of those only SOFTWARE actually displays.

SOFTWARE reports CustomEffects false: its fixed CPU shading path produces the pixels, and your shader source will not run. HEADLESS validates and records the call without executing it, which is exactly what a harness should do. UseNullDevice maps to HEADLESS and UseReferenceDevice to SOFTWARE. See Tutorial 107.

What each renderer needs from the platform

The renderer and the platform implementation (CNA_PLATFORM: SDL3 by default, HEADLESS, TERMINAL) are independent build axes, but the platform decides which renderers can find a window, a GL context or a Vulkan surface. SDL3 is CNA’s one windowing implementation: Windows, X11, Wayland, macOS, iOS, Android and the browser are reached through SDL’s own video drivers, and a renderer that needs a native handle (an HWND, an X11 Display* and window, a Wayland wl_display* and wl_surface*, a Cocoa or UIKit view) receives it from the SDL3 window through NativeWindowHandle.

  • Three renderers are SDL3 APIs or depend on SDL directly: SDL_RENDERER, SDL_GPU and FNA3D. The other families are SDL-free at source level and take their window, GL context and Vulkan surface from the platform.
  • A GL context and a Vulkan surface are provided by the SDL3 platform (not Terminal, not Headless).
  • An SDL-free build has no window. CNA_ENABLE_SDL=OFF refuses CNA_PLATFORM=SDL3, SDL3 audio and the three SDL renderers by name; what remains is a windowless configuration (HEADLESS or TERMINAL with HEADLESS, SOFTWARE or STUB, and Null or ALSA audio) for servers, test runners and terminal games.
  • Terminal accepts only the CPU/no-pixel renderers and is POSIX-only. iOS admits SDL_RENDERER and METAL; anything else is a FATAL_ERROR unless the unsupported override -DCNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ON is given.

The platform matrix, the CNA_AUDIO_PLATFORM axis and the per-OS status are on Platforms.

Sibling repositories and fetched dependencies

Sibling repositories are not submodules; the configure step checks that they sit next to the CNA checkout. Third-party sources that CNA fetches are pinned.

DependencyNeeded byHow it arrives
../sharp-runtime (branch apple/m4-stabilization)Every buildSibling checkout
../easy-gl + ../meta-glThe three GL identitiesSibling checkouts (default branch develop)
FNA3D 3240147 + MojoShaderFNA3D; every CNA_*_COMPILED_EFFECTS optionFetched by CMake
wgpu-native v29.0.1.1 (SHA-256 pinned)WEBGPU (native route)Auto-downloaded unless CNA_WEBGPU_ROOT is given
SDL_shadercross 1ff05be + SPIRV-Cross vulkan-sdk-1.4.350.0SDL_GPU (default ON on Windows and Apple)Fetched by CMake; new since alpha.1
Vulkan SDK / loaderVULKANfind_package(Vulkan REQUIRED)
d3d9/d3d11/dxgi/d3dcompilerDIRECTX9, DIRECTX11Windows SDK / MinGW import libraries
AppKit (macOS) or UIKit (iOS), Metal, QuartzCore, FoundationMETAL (Objective-C++)macOS or iOS SDK

Shaders: what actually executes where

Two separate things get called "shaders" in an XNA-shaped framework, and CNA treats them very differently.

Compiled Effect Framework bytecode is the XNA-compatible path described below. It accepts XNA/FNA Direct3D 9 Effect binaries on qualified renderer builds; it is not a compiler for HLSL .fx source and does not accept DXBC or MGFX.

Renderer-native custom shaders use ShaderEffect, and three separate questions apply: does the renderer accept a ShaderEffect (the CustomEffects capability), does the supplied source text determine the pixels (the detailed ShaderEffectSourceExecution feature), and which payload does it consume (its declared shader dialect).

Renderer(s)Accepts ShaderEffectPayload consumedSource execution declared
OPENGLES3, WEBGL2YesGLSL ES sourceYes
OPENGL33YesDesktop GLSL sourceYes
DIRECTX11YesHLSL source, compiled with D3DCompileYes
DIRECTX9Yes (inherited default)HLSL source, compiled with D3DCompileNot declared
WEBGPUYesWGSL sourceYes
VULKANYesCompiled SPIR-V bytecodeNot declared (the payload is bytecode, not source)
SDL_GPUOnly where libshaderc exists (Linux and Android builds)Compiled SPIR-V bytecodeYes while a device exists
METALYes, for SpriteBatch only (one MSL vertex and one fragment function)Metal Shading Language source; a source that does not compile gives an invalid effect carrying the Metal compiler’s messageNot declared
HEADLESSReports yes; nothing is rasterized—No
SOFTWARE, FNA3D, STUB, and the 2D-only SDL_RENDERERNo (CustomEffects false)——

The last column comes from the renderers' own ExecutesShaderEffectSourceEXT() overrides; the profile states an accepted-but-not-executed ShaderEffect in its report text ("the supplied source text does not determine rendered pixels on this renderer"). FNA3D cannot compile custom shaders at all; its only shader entry point is XNA's own compiled stock-effect bytecode, run through MojoShader. See Shader Effects for the API itself.

Compiled XNA effects

GraphicsCapability::CompiledEffects is independent of CustomEffects. It means the renderer can execute XNA/FNA Direct3D 9 Effect Framework bytecode (.fxb), which needs MojoShader (or CNA's translation of it) from the pinned FNA3D checkout. FNA3D supports it always; eight other families support it only when their build option is on, and every option defaults to OFF. A default configure therefore reports CompiledEffects false on 13 of the 14 identities. See Effects System.

Family (identities)Gate
FNA3D (FNA3D)Always on
EasyGL (OPENGLES3/OPENGL33/WEBGL2)-DCNA_EASYGL_COMPILED_EFFECTS=ON; on macOS this includes the core-profile OpenGL context
Vulkan (VULKAN)-DCNA_VULKAN_COMPILED_EFFECTS=ON
WebGPU (WEBGPU)-DCNA_WEBGPU_COMPILED_EFFECTS=ON; the browser build translates SPIR-V to WGSL
SDL_GPU (SDL_GPU)-DCNA_SDL_GPU_COMPILED_EFFECTS=ON
Software (SOFTWARE)-DCNA_SOFTWARE_COMPILED_EFFECTS=ON
Metal (METAL)-DCNA_METAL_COMPILED_EFFECTS=ON; MojoShader emits SPIR-V and SPIRV-Cross translates it to MSL, which Metal compiles at run time
Direct3D 9/11 (DIRECTX9/DIRECTX11)-DCNA_DIRECTX9_COMPILED_EFFECTS=ON, -DCNA_DIRECTX11_COMPILED_EFFECTS=ON

There is no compiled-effect implementation on HEADLESS, STUB or SDL_RENDERER. Each opt-in family has its own compiled-effect test file; FNA-executed reflection, state and pixel fixtures (tests/fixtures/compiled-effects/) are replayed by several of them with a ±3 channel tolerance on 8×8 flat-input pixels — compiled effects only, not sprites or 3D scenes.

Capability reporting

⚠

Do not branch game logic on a bare positive SupportsCapability() answer and expect execution proof. The base implementation of IGraphicsRenderer::SupportsCapability still returns true for the original members, except MultiStreamVertexInput (false), StencilBuffer (delegated to the renderer), CompiledEffects (routed through its false-by-default opt-in) and, new in this snapshot, FloatRenderTargets and HalfFloatRenderTargets (false). Only DIRECTX9 has no override at all; VULKAN, DIRECTX11, SDL_GPU and METAL use switches with no default arm, and WEBGPU, HEADLESS, SOFTWARE, EasyGL and FNA3D use partial default: true arms.

GraphicsCapability now has 19 members: the 14 that existed in alpha.1 (ThreeD, DepthStencilBuffer, MultiSampleAntiAliasing, MultipleRenderTargets, AnisotropicFiltering, WireFrame, OcclusionQuery, CustomEffects, Texture3D, MultiStreamVertexInput, Instancing, StencilBuffer, AdditiveBlending, CompiledEffects) and five added since: FloatRenderTargets, HalfFloatRenderTargets, HalfFloatTextureLinearFiltering, ComputeShaders and IndirectDraw. At the device level, GraphicsDevice::SupportsCapability derives six answers from separate false-by-default virtuals rather than the renderer's own switch (CompiledEffects, the two float render-target members, half-float filtering, ComputeShaders, IndirectDraw), and ANDs MultipleRenderTargets with the profile's MRT limit. The device-level answer is the public one: a caller who holds the renderer interface directly can see different values (for example SDL_GPU).

The practical consequence is unchanged: a capability query is a hint, not a contract. The reliable checks are the ones this page states directly — which renderers are 2D only, which execute custom shaders, and which report a feature in the matrix below — plus the detailed profile that follows. Tutorial 101 shows how to query and interpret them.

One gap no capability member reports: a cube face inside a multiple-render-target set. DIRECTX9 and SDL_GPU refuse it at SetRenderTargets with "cube faces in a multi-target set are not implemented by this CNA renderer"; WEBGPU and HEADLESS refuse it too, with their own messages (a face "cannot be bound in a multiple-render-target set" on WEBGPU, "cannot bind a cube face alongside other render targets" on HEADLESS); a single cube face bound on its own is a different, ordinary path. The GL family, VULKAN, DIRECTX11 and SOFTWARE contain code for the multi-target case. Check Render Targets before relying on it.

Capability matrix

Derived from each renderer's own capability code: T true, F false, c conditional (a device, driver or build-option probe decides). The first table is the 13 original members; the second the newer six. Unless noted, a c means the answer comes from a run-time probe of the driver or device.

Original members: 3D ThreeD, DS DepthStencilBuffer, AA MultiSampleAntiAliasing, RT MultipleRenderTargets, An AnisotropicFiltering, Wf WireFrame, Oq OcclusionQuery, Cx CustomEffects, T3 Texture3D, MS MultiStreamVertexInput, In Instancing, St StencilBuffer, Ad AdditiveBlending
Renderer3DDSAARTAnWfOqCxT3MSInStAd
SDL_RENDERERFFFFFFFFFFFFT
OPENGLES3TTcTccTTTTTTT
OPENGL33TTcTcTTTTTTTT
WEBGL2TTcTccTTTTTTT
VULKANTTccccTTTTTcT
WEBGPUTTcTTTTTTTTTT
HEADLESSTTTTTTTTFFTTF
SOFTWARETTTTTTTFTTTTT
STUBFFFFFFFFFFFFF
DIRECTX11TTcTTTTTTTTTT
DIRECTX9TTTT*TTTTTFTTT
SDL_GPUTccTTTFcTTTcT
METALTTcTTTTTTTTTT
FNA3DTTcTTTcFTTccT
Newer members: CE CompiledEffects, F32 FloatRenderTargets, F16 HalfFloatRenderTargets, HF HalfFloatTextureLinearFiltering, Cs ComputeShaders, Id IndirectDraw
RendererCEF32F16HFCsId
SDL_RENDERERFFFFFF
OPENGLES3cccTcc
OPENGL33cccTcc
WEBGL2cccTFF
VULKANcccccc
WEBGPUcccTTc
HEADLESSFFFFFF
SOFTWAREcTTTFF
STUBFFFFFF
DIRECTX11ccccTT
DIRECTX9cFFFFF
SDL_GPUccccTT
METALcTTTFF
FNA3DTFFFFF

Reading the conditions. T*: DIRECTX9 carries its own profile table for MRT (Reach 1, HiDef 4), but the gating is not specific to it. At the device level GraphicsDevice::SupportsCapability(MultipleRenderTargets) is the renderer's answer ANDed with that same profile ceiling (the interface default is also Reach 1, HiDef 4) for every identity, and FloatRenderTargets and HalfFloatRenderTargets are refused outright under Reach (Texture::IsRenderTargetFormatAllowedByProfileEXT). CNA defaults to Reach, so the RT, F32 and F16 columns describe each renderer under HiDef: on the default profile GraphicsDevice::SupportsCapability reports them false on every renderer. The Oq column is likewise the renderer's own answer: under Reach the OcclusionQuery constructor throws NotSupportedException whatever the column says. EasyGL: AA is GL_MAX_SAMPLES>1; An needs GL_EXT_texture_filter_anisotropic; Wf is native glPolygonMode, GL_NV_polygon_mode on ES, or a WebGL extension; CE is the CNA_EASYGL_COMPILED_EFFECTS build; F32/F16 come from a run-time framebuffer probe; Cs needs ES 3.1 / GL 4.3 (never WebGL) and Id ES 3.1 / GL 4.0 (never WebGL). VULKAN: An, Wf, AA, RT, St, Cs and Id come from device features and limits. WEBGPU: AA from a 4x MSAA probe, F32/F16 from device probes, Id needs the IndirectFirstInstance feature. DIRECTX11: AA is the clamped multisample count > 1, F32/F16/HF come from DXGI format-support queries, Cs and Id need feature level 11_0 (which device creation already requires). METAL: AA is true when the device reports any supported sample count; Cx covers SpriteBatch-scoped MSL effects only; F32/F16 come from its render-target storage table (RGBA32F and RGBA16F). SDL_GPU: DS/St from the formats created, Cx only with libshaderc; the table shows the device-level answers (see the note above). FNA3D: Cx is false because FNA3D accepts only compiled D3D9 Effect binaries; Oq is whatever FNA3D’s driver reports (its SDL_GPU driver has no occlusion queries). CE c means "true only in a build with that renderer's compiled-effects option".

RendererCapabilityProfile: the detailed answer

The 19-member GraphicsCapability enum is a summary. This snapshot adds a second, finer API, CNA::RendererCapabilityProfile: 32 RendererFeature ids (the legacy summaries split further, plus ShaderEffectSourceExecution, Texture3DSampling, ComputeImageBinding, ShadowSampling, ImageBasedLighting, GpuTimers, six shader-dialect flags and BaseInstanceDrawing), a four-state answer (Unknown, Unsupported, Supported, Restricted; at this snapshot the device fills in only Supported and Unsupported), 22 RendererLimit ids (texture size, vertex streams, compute work-group limits, buffer sizes, attachments, alignments, timestamp period, each with an explicit known/unknown flag), per-format usage masks for the 27 SurfaceFormat values (each usage carries separate known and supported bits, so an unclassified use is not misread as unsupported), and a generated English report. The profile is built lazily by the GraphicsDevice and cached until renderer reconstruction or device destruction.

#include "Microsoft/Xna/Framework/Game.hpp"
#include "CNA/RendererCapabilityProfile.hpp"

// inside a Game subclass, after the device exists:
auto& device = getGraphicsDeviceProperty();

bool compute = device.SupportsRendererFeatureEXT(CNA::RendererFeature::ComputeShaders);
CNA::RendererLimitValue maxTex =
    device.GetRendererLimitEXT(CNA::RendererLimit::MaxTextureDimension);   // maxTex.known / maxTex.value
CNA::RendererFormatSupport color =
    device.GetRendererSurfaceFormatSupportEXT(Graphics::SurfaceFormat::Color);
bool colorRt = color.Supports(CNA::RendererFormatUsage::RenderTarget);

std::string_view report = device.GetRendererCapabilityReportEXT();       // English text

The accessors are GetRendererCapabilityProfileEXT(), GetRendererFeatureSupportEXT(), SupportsRendererFeatureEXT(), GetRendererLimitEXT(), GetRendererSurfaceFormatSupportEXT() and GetRendererCapabilityReportEXT(); five C functions expose the same surface in the experimental C API (ABI 0.46.0). Tutorial 133 is a full walkthrough.

Modern features by renderer

Only where the code claims the feature and a test exists. "Verified" means read in the renderer's implementation and its test registration; nothing here says a test was run.

FeatureRenderers claiming itTest evidence
Shadow sampling (single map, cascades, punctual)VULKAN, SDL_GPU, WEBGPU, and all three EasyGL identities (claimed unconditionally; only the built profile is tested); no otherPer-family shadow-receiver and shadow-caster tests
Image-based lighting (PBR IBL)The same five familiesCNAEXT_ImageBasedLighting; per-family PBR golden or hand-derived tests
PBR / skinned-PBR drawing (PbrEffect, SkinnedPbrEffect)GL family, VULKAN, SDL_GPU, WEBGPU, DIRECTX9/11 (native HLSL), METALGolden and hand-derived tests per family (easygl_*pbreffect_golden, vulkan_pbreffect_*, sdlgpu_*pbreffect_test, webgpu_*pbr3d_test)
Skinned mesh drawingEasyGL, VULKAN, SDL_GPU, WEBGPU, SOFTWARE, DIRECTX9, METALMany per-family skinned-effect tests; parity fixture skinned_terms
Hardware instancing and base-instance drawingInstancing: EasyGL, VULKAN, WEBGPU, SDL_GPU, DIRECTX9/11, METAL, SOFTWARE, HEADLESS; FNA3D conditional. Base-instance: VULKAN, SDL_GPU (device level)Parity fixture instanced_draw
MSAASee the matrixPer-family MSAA tests; parity fixture backbuffer_msaa
Float / half-float render targets (HDR)VULKAN, SDL_GPU, WEBGPU, DIRECTX11, METAL, EasyGL, SOFTWAREParity fixture hdr_render_target; float-render-target tests
Compute and storage buffers (capability answer only)VULKAN, SDL_GPU, WEBGPU, DIRECTX11, EasyGL (ES 3.1+/GL 4.3+, not WebGL); false on DIRECTX9, METAL, SOFTWARE, FNA3Drenderer_capability_truth_test checks the answers; there is no public compute class, so no game-level test exists
Indirect draw (capability answer only)VULKAN (conditional: needs the drawIndirectFirstInstance device feature), SDL_GPU, WEBGPU (conditional), DIRECTX11, EasyGL (ES 3.1+/GL 4.0+)renderer_capability_truth_test; GraphicsDevice::DrawPrimitivesIndirectEXT needs a renderer-internal argument buffer that no public class creates
GPU timers (renderer-internal)VULKAN, DIRECTX11, EasyGL (desktop ≥ 3.3, or ES with EXT_disjoint_timer_query), WEBGPUUsed by Diagnostics; no public timer class

The opt-in CNAEXT extensions (CNA::Graphics: CRT, colour-depth and ASCII effects and debug drawing) need custom ShaderEffect support for the two shader-based effects; see CNAEXT extensions. Shadow maps and environment maps are rendered by the application and passed to the receivers in the table above.

Texture and surface formats

SurfaceFormat has 27 members: the 20 classic XNA formats and 7 CNA extensions (ColorBgraEXT, ColorSrgbEXT, Dxt5SrgbEXT, Bc7EXT, Bc7SrgbEXT, ByteEXT, UShortEXT). Each renderer classifies formats itself; a renderer with no classifier gets the framework rule: SurfaceFormat::Color only, anything else throws ("... is not implemented by the selected graphics renderer"). Summaries below come from one implementation file each, so treat them as guidance and use the profile for the truth on your device.

  • Color-only (no classifier): HEADLESS, STUB, SDL_RENDERER; DIRECTX9 as well, for textures and render targets alike (see the last bullet).
  • METAL: all 20 classic formats in native storage — the packed 16-bit ones where the GPU has packed 16-bit formats, DXT1/3/5 as BC blocks or decoded — in Texture2D, TextureCube and (without DXT) Texture3D; render targets Color, Rgba1010102, Rg32, Rgba64 and the float and half-float formats; the 7 extension formats defer to the framework rule.
  • VULKAN: the 20 classic formats, each mapped to a VkFormat and checked against the device's format properties (BC1-3 native only with textureCompressionBC, Bgra4444 only with VK_EXT_4444_formats); the 7 extension formats have VkFormat mappings for the capability-profile queries but defer at Texture2D creation, so they are refused; render targets Color, Rgba64, Single, Vector2, Vector4 and the half-float formats.
  • EasyGL: Color, Alpha8 and DXT1/3/5 on every profile (CPU-decoded without S3TC), the packed 16-bit formats as sized GL storage, NormalizedByte2/4 and the remaining classic formats through the ES 3 sized-format set; the extension formats defer.
  • SDL_GPU: Color, packed 16-bit, DXT (native BC or CPU decode), NormalizedByte2/4 and Alpha8; Rgba1010102, Rg32, Rgba64 and the float and half-float formats in their exact native storage (widened to four channels with Direct3D 9’s fill) where the device can sample that storage, refused otherwise — never substituted.
  • DIRECTX11: the 20 classic formats asked of the device, BC1-3 native; the 7 extension formats classified unsupported. WEBGPU: BC only when the device enabled it; HalfVector4 and HdrBlendable stored natively as rgba16float in Texture2D; Rgba64 has no render-target mapping. SOFTWARE: the 20 classic formats, DXT stored as blocks. FNA3D: Color and the signed-normalized NormalizedByte2/NormalizedByte4 (authentic XNA normal maps) are classified supported; DXT and BC7 follow the driver’s own format answer; every other format defers to the framework rule, so a public Texture2D in one of them is refused although the renderer layer itself can carry it. DIRECTX9: an internal D3DFMT mapping exists (Bc7EXT/Bc7SrgbEXT unmapped), but the renderer does not override ClassifySurfaceFormatEXT, so a public Texture2D falls back to the framework rule and is Color-only.

Declared maturity

Every identity carries a code-declared maturity and category, readable from C++ and from the C API. These are CNA's own classification, not a measurement. Historical and Deprecated exist in the enum, but no renderer uses them at this snapshot.

Declared maturityIdentities
ProductionSDL_RENDERER, OPENGLES3, OPENGL33, VULKAN, DIRECTX9, DIRECTX11
SupportedWEBGL2, HEADLESS, STUB, SDL_GPU, METAL
ExperimentalWEBGPU, SOFTWARE, FNA3D

Categories: Native (OPENGLES3, OPENGL33, VULKAN, DIRECTX9/11, METAL), TranslationLayer (SDL_RENDERER, WEBGPU, SDL_GPU, FNA3D), Software (SOFTWARE), Web (WEBGL2) and Diagnostic (HEADLESS, STUB). Automatic fallback uses these two declarations to order the candidates (mature GPU renderers first, CPU renderers ten places later, STUB last). Read "Production" with the verification section beside it: it is a declaration, and the Windows and Metal lanes in particular are exercised mostly under Wine or a build-only lane.

How renderers are verified

Renderer verification is configuration-scoped. The selected renderer set, platform implementation, audio implementation, host and feature options decide which pixel, smoke and integration tests are built and registered. A multi-renderer build may contain several renderer test sets. Report ctest -N and executed results for the exact configuration rather than quoting a tree-wide registration total as though it were universal.

The XNA oracle corpus

CNA keeps 39 renderer-neutral reference scenes (256×256, HiDef) under tools/xna-oracle/scenes/, with 39 reference PNGs captured from the genuine Microsoft XNA 4.0 runtime running under Wine + DXVK on Linux — not on Windows. A further 7 unbound-texture scenes and a 17-format channel-expansion table were measured the same way; they sit outside every diff denominator. Renderers are compared against the 39 references at tolerance 0:

RendererScenes matched at tolerance 0What is gated
DIRECTX939 of 39 recorded (Wine + DXVK; the last consolidated dated report covers 31 scenes; later per-scene notes record the rest)CTest D3D9_XNA_Diff runs all 39; not in CI
SOFTWARE18 of 39 byte-exact, 30 of 39 within one channel value (2026-09-11 measurement)Two line scenes (Software_XnaLineCoverage), diffed at tolerance 0; only a failure to render fails the test
EasyGL (the three GL profiles)10 of 39 in a 2026-08-11 measurement that predates later fixes; the only later count is 9 of 39 for the OPENGLES3 leg of a 2026-08-15 multi-renderer run, which predates them tooTwo line scenes (EasyGL_XnaLineCoverage); any difference fails the test
FNA3D10 of 39 in the same 2026-08-11 measurement (OpenGL driver, Mesa llvmpipe)Only that every scene renders (Fna3d_XNA_Oracle); differences are reported, not failed
⚠

"Pixel-exact" is a DIRECTX9-only statement. It is also a statement about DIRECTX9 over DXVK. It is the one renderer recorded as matching the oracle corpus exactly, which makes sense — real XNA 4.0 ran on Direct3D 9. It has not been repeated on native Windows, and none of this runs in CI. No other renderer is held to that bar, and CNA does not claim otherwise.

Cross-renderer parity fixtures and glTF conformance

Thirty-two renderer-neutral parity fixtures (modules/graphics/examples/parity/) each state their expected result themselves; they are registered as <Renderer>_Parity_<fixture> for four renderer families — EasyGL, WEBGPU, SDL_GPU and DIRECTX11. The oracle is the fixture's own assertions, not real XNA. Separately, a glTF conformance corpus of 148 assets (140 captured plus 8 safely rejected) has renderer-owned goldens for OPENGLES3/EasyGL, VULKAN, SOFTWARE and DIRECTX11; that proves each renderer against its own past output, not against a reference renderer, and only the EasyGL job runs in CI.

What CI does and does not reach

CI exists across Linux, macOS and Emscripten jobs: 18 workflow files, 16 of which run automatically. It builds and smoke-tests; it is not a full GPU-oracle gate:

Renderer coverageStatus
OPENGLES3 (the full default suite under Xvfb); VULKAN, SOFTWARE, HEADLESS and STUB (glTF conformance and platform jobs); SDL_RENDERER on macOS, and an iOS device build and simulator launch for both SDL_RENDERER and METAL; METAL build plus ctest -R '^Metal' on GitHub’s macos-26 runners (a paravirtual GPU), and multi-renderer builds (HEADLESS;SOFTWARE;STUB and the Emscripten bundle WEBGL2;WEBGPU, built and inspected, not run)Automatic on push and pull request
DIRECTX11 on native MSVC; the SDL3 platform suite on WindowsManual dispatch only
SDL_GPU, FNA3D, DIRECTX9, OPENGL33Named by no workflow
Android, and a build of the experimental C APINo workflow
All 14 identities across every valid platform/driver combinationNot exhaustively covered

The workflow inventory is evidence of build and smoke coverage, not of universal renderer fidelity. In particular, it does not establish every graphics driver path or the full GPU pixel/oracle matrix. Platforms has the workflow grouping, and Verification & Known Issues the wider testing story.

Which renderer should I choose?

💡

Default answer: the platform default. OPENGLES3 on Linux, WEBGL2 on the web, SDL_RENDERER everywhere else. These are the configurations CNA itself builds by default, so they are the ones most likely to behave. Anything else should be a deliberate choice — and on Windows or macOS a 3D game must make that choice.

💡

For 2D-only games wanting maximum portability: SDL_RENDERER. It needs nothing beyond the vendored SDL3, it is one of the two renderers the iOS build admits, and its 3D entry points throw deterministically (or warn-and-stub, if you ask) instead of quietly drawing nothing.

💡

For a modern 3D desktop game: VULKAN (declared Production, the most complete surface; it takes its surface from the SDL3 window on Windows, X11 or Wayland) or OPENGL33; SDL_GPU if you want SDL to choose the native driver; DIRECTX11 on Windows; METAL on macOS. Check the profile at start-up rather than assuming compute or indirect draw.

💡

For measured XNA pixel fidelity: DIRECTX9, the only renderer recorded as matching the checked-in 39-scene oracle corpus exactly. It is Windows-gated; the repository provides a manual MinGW/Wine + DXVK route, not an automatic workflow.

💡

For CI with no GPU: HEADLESS or STUB when you only need game logic exercised, or SOFTWARE when you need real pixels — read back with GetBackBufferData(), since it does not present to a window (UseNullDevice and UseReferenceDevice select HEADLESS and SOFTWARE for you).

💡

For the web: WEBGL2 is the default and the widest path, and the one the 90 published sample builds use; WEBGPU is Experimental. Whichever you pick, the web caveats above apply (no video; threaded builds proxy WebGL to the main thread).

⚠

Know what you are opting into. Custom ShaderEffect shaders are not accepted by SOFTWARE, FNA3D or the 2D-only SDL_RENDERER (FNA3D supports the separate compiled Effect Framework path instead). METAL takes custom effects only for SpriteBatch, in MSL, and its compiled-effect support is an opt-in build. CI does not exhaustively exercise every identity and driver combination.