Graphics Renderers
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 partition | Identities | Enforced by |
|---|---|---|
| Windows only (2) | DIRECTX9, DIRECTX11 | FATAL_ERROR unless CMAKE_SYSTEM_NAME is Windows (native MSVC/MinGW or the mingw-w64 cross toolchain) |
| Emscripten only (1) | WEBGL2 | Refused on any other target; OPENGLES3 and OPENGL33 are refused under Emscripten |
| Apple only (1) | METAL | Refused 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 identities | No 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 name | Today |
|---|---|
CNA_GRAPHICS_BACKEND | CNA_GRAPHICS_RENDERER |
IGraphicsBackend | IGraphicsRenderer |
EASYGL | One of OPENGLES3 / OPENGL33 / WEBGL2 |
D3D9 / D3D11 | DIRECTX9 / DIRECTX11 |
ASCII | Not 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.
| Renderer | C ABI | Family | Scope | Declared maturity | Platform gate |
|---|---|---|---|---|---|
SDL_RENDERER | 1 | SdlRenderer | 2D only | Production | Any SDL3 platform (default off Linux/web) |
OPENGLES3 | 3 | EasyGL | 2D + 3D | Production | Native hosts (Linux default) |
OPENGL33 | 4 | EasyGL | 2D + 3D | Production | Native hosts |
WEBGL2 | 6 | EasyGL | 2D + 3D | Supported | Emscripten only (default) |
VULKAN | 8 | Vulkan | 2D + 3D | Production | Vulkan loader + a platform with a Vulkan surface |
WEBGPU | 9 | WebGPU | 2D + 3D | Experimental | Native (wgpu-native) and browser (emdawnwebgpu) |
HEADLESS | 11 | Headless | No pixels | Supported | Any host |
SOFTWARE | 12 | Software | 2D + 3D | Experimental | Any host (off-screen; displayed on Terminal) |
STUB | 13 | Stub | No pixels | Supported | Any host |
DIRECTX11 | 14 | DirectX11 | 2D + 3D | Production | Windows only |
DIRECTX9 | 22 | DirectX9 | 2D + 3D | Production | Windows only |
SDL_GPU | 31 | SdlGpu | 2D + 3D | Supported | Any host SDL3's GPU API supports |
METAL | 42 | Metal | 2D + 3D | Supported | macOS and iOS |
FNA3D | 43 | Fna3d | 2D + 3D | Experimental | Any 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.
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
OPENGLES3 | OpenGL ES 3.0 context (GLSL ES 3.00) via EasyGL — the Linux default | 2D + 3D; MSAA, anisotropy and wireframe by runtime probe; MRT, occlusion, Texture3D, multi-stream and instancing yes; shadow sampling and IBL claimed | Native (non-Emscripten) hosts; CMake warns "primarily tested on Linux" elsewhere | ../easy-gl + ../meta-gl; a GL context from the SDL3 platform |
OPENGL33 | Desktop OpenGL 3.3 core profile (GLSL 3.30) via EasyGL | 2D + 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 tests | Native hosts | Same as above |
WEBGL2 | Browser WebGL 2 (GLSL ES 3.00) via EasyGL — the Emscripten default | 2D + 3D, as OPENGLES3 (no compute or indirect draw in a browser) | Emscripten only | Same; 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).
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
VULKAN | Direct Vulkan renderer (instance API 1.1 requested); declared Production, and the renderer with the most registered example and test executables | 2D + 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 Headless | Vulkan SDK/loader (find_package(Vulkan REQUIRED)) |
WEBGPU | WebGPU renderer; declared Experimental | 2D + 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, compute | Native (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_GPU | Renderer on SDL3's GPU API (SPIR-V; SDL selects the driver); declared Supported | 2D + 3D; MSAA and stencil device-probed, MRT, instancing, compute, indirect draw, float targets, shadow sampling, IBL; no occlusion queries; custom ShaderEffect only where libshaderc exists | Any host SDL3's GPU API supports; requires the SDL3 platform | SDL3; SDL_shadercross + SPIRV-Cross (default ON on Windows and Apple); optional libshaderc |
METAL | Apple Metal directly; SDL3 supplies only the window and the view that holds the CAMetalLayer; declared Supported | 2D + 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 draw | macOS and iOS (AppKit view on macOS, UIKit view on iOS); tvOS refused | Objective-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.)
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
SDL_RENDERER | SDL3'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 Production | 2D only; additive blending is the only capability answering true; 3D calls throw or warn-and-stub | Any SDL3 platform | SDL3 (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.
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
DIRECTX9 | Real 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 Production | 2D + 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 claim | Windows only (native or mingw-w64 cross-build) | d3d9, d3dcompiler (Windows SDK) |
DIRECTX11 | Real Direct3D 11 (feature level 11_0+, negotiating 11_1 down); declared Production | 2D + 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 claim | Windows only | d3d11, dxgi, d3dcompiler; shared common/d3d helpers |
Two things are worth calling out in this group:
DIRECTX11reports compute, indirect draw and GPU timers from renderer-internal implementations;renderer_capability_truth_test.cppchecks that the capability answers match those implementations. There is no public compute class for game code.- Only
DIRECTX9has noSupportsCapability()override. It relies on the permissive base defaults wholesale, so itstrueanswers are the least informative;DIRECTX11uses a switch with nodefaultarm (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.
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
SDL_GPU | SDL3's GPU API; SDL picks the native driver and CNA feeds it SPIR-V | 2D + 3D | Any host SDL3's GPU API supports | SDL3; SDL_shadercross + SPIRV-Cross; optional libshaderc |
WEBGPU | WebGPU: wgpu-native natively, the browser's own WebGPU through emdawnwebgpu on the web | 2D + 3D | Native desktop hosts and Emscripten | Pinned wgpu-native v29.0.1.1 or the Emscripten port |
FNA3D | FNA-XNA's XNA-shaped C graphics library; it chooses SDL_GPU, Direct3D 11 or OpenGL at run time (override with FNA3D_FORCE_DRIVER); declared Experimental | 2D + 3D; compiled effects always on; instancing and MSAA follow FNA3D; no float render targets, no compute | Any host FNA3D + SDL3 support | Fetched FNA3D 3240147 + MojoShader; SDL3 |
Group-specific caveats, all of which change what your code does rather than merely how fast it runs:
SDL_GPUis an SDL3 API, so it requires the SDL3 platform. A configure withCNA_ENABLE_SDL=OFFrefuses 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 customShaderEffectadditionally needs a target-nativelibshaderc, so it is unavailable on Windows, Apple and Emscripten builds.WEBGPUis Experimental. Its native route downloads a pinnedwgpu-nativerelease unless you giveCNA_WEBGPU_ROOT; its browser route needs no native library. No CI workflow names it.FNA3Denables XNA/FNA compiled Effect Framework bytecode automatically. It executes those binaries through MojoShader and cannot compileShaderEffectsource at all. ItsFna3d_XNA_Oracletest 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.
| Renderer | What it is | Scope | Platform | Dependency |
|---|---|---|---|---|
SOFTWARE | CNA's own CPU rasterizer that owns its framebuffer; no window, no GPU library; declared Experimental | 2D + 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 IBL | Any host; off-screen (readback) except on CNA_PLATFORM=TERMINAL, where it is displayed | None (compiled effects: MojoShader via CNA_SOFTWARE_COMPILED_EFFECTS) |
HEADLESS | A no-GPU, no-window harness that validates arguments and counts or traces calls (CNA_HEADLESS_MODE = Fast, Validation (default) or Trace); renders nothing | No pixel output; reports 3D, depth/stencil, MSAA and instancing true (nothing is rasterized); Texture3D, additive and multi-stream false | Any host; selected by UseNullDevice | None |
STUB | The smallest possible complete renderer: renders nothing, keeps no bookkeeping | No pixel output; all 19 capabilities false | Any host; the default of the dev and unit presets | None |
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_GPUandFNA3D. 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=OFFrefusesCNA_PLATFORM=SDL3, SDL3 audio and the three SDL renderers by name; what remains is a windowless configuration (HEADLESSorTERMINALwithHEADLESS,SOFTWAREorSTUB, 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_RENDERERandMETAL; anything else is aFATAL_ERRORunless the unsupported override-DCNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ONis 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.
| Dependency | Needed by | How it arrives |
|---|---|---|
../sharp-runtime (branch apple/m4-stabilization) | Every build | Sibling checkout |
../easy-gl + ../meta-gl | The three GL identities | Sibling checkouts (default branch develop) |
FNA3D 3240147 + MojoShader | FNA3D; every CNA_*_COMPILED_EFFECTS option | Fetched 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.0 | SDL_GPU (default ON on Windows and Apple) | Fetched by CMake; new since alpha.1 |
| Vulkan SDK / loader | VULKAN | find_package(Vulkan REQUIRED) |
d3d9/d3d11/dxgi/d3dcompiler | DIRECTX9, DIRECTX11 | Windows SDK / MinGW import libraries |
| AppKit (macOS) or UIKit (iOS), Metal, QuartzCore, Foundation | METAL (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 ShaderEffect | Payload consumed | Source execution declared |
|---|---|---|---|
OPENGLES3, WEBGL2 | Yes | GLSL ES source | Yes |
OPENGL33 | Yes | Desktop GLSL source | Yes |
DIRECTX11 | Yes | HLSL source, compiled with D3DCompile | Yes |
DIRECTX9 | Yes (inherited default) | HLSL source, compiled with D3DCompile | Not declared |
WEBGPU | Yes | WGSL source | Yes |
VULKAN | Yes | Compiled SPIR-V bytecode | Not declared (the payload is bytecode, not source) |
SDL_GPU | Only where libshaderc exists (Linux and Android builds) | Compiled SPIR-V bytecode | Yes while a device exists |
METAL | Yes, 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 message | Not declared |
HEADLESS | Reports yes; nothing is rasterized | — | No |
SOFTWARE, FNA3D, STUB, and the 2D-only SDL_RENDERER | No (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.
| Renderer | 3D | DS | AA | RT | An | Wf | Oq | Cx | T3 | MS | In | St | Ad |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SDL_RENDERER | F | F | F | F | F | F | F | F | F | F | F | F | T |
OPENGLES3 | T | T | c | T | c | c | T | T | T | T | T | T | T |
OPENGL33 | T | T | c | T | c | T | T | T | T | T | T | T | T |
WEBGL2 | T | T | c | T | c | c | T | T | T | T | T | T | T |
VULKAN | T | T | c | c | c | c | T | T | T | T | T | c | T |
WEBGPU | T | T | c | T | T | T | T | T | T | T | T | T | T |
HEADLESS | T | T | T | T | T | T | T | T | F | F | T | T | F |
SOFTWARE | T | T | T | T | T | T | T | F | T | T | T | T | T |
STUB | F | F | F | F | F | F | F | F | F | F | F | F | F |
DIRECTX11 | T | T | c | T | T | T | T | T | T | T | T | T | T |
DIRECTX9 | T | T | T | T* | T | T | T | T | T | F | T | T | T |
SDL_GPU | T | c | c | T | T | T | F | c | T | T | T | c | T |
METAL | T | T | c | T | T | T | T | T | T | T | T | T | T |
FNA3D | T | T | c | T | T | T | c | F | T | T | c | c | T |
| Renderer | CE | F32 | F16 | HF | Cs | Id |
|---|---|---|---|---|---|---|
SDL_RENDERER | F | F | F | F | F | F |
OPENGLES3 | c | c | c | T | c | c |
OPENGL33 | c | c | c | T | c | c |
WEBGL2 | c | c | c | T | F | F |
VULKAN | c | c | c | c | c | c |
WEBGPU | c | c | c | T | T | c |
HEADLESS | F | F | F | F | F | F |
SOFTWARE | c | T | T | T | F | F |
STUB | F | F | F | F | F | F |
DIRECTX11 | c | c | c | c | T | T |
DIRECTX9 | c | F | F | F | F | F |
SDL_GPU | c | c | c | c | T | T |
METAL | c | T | T | T | F | F |
FNA3D | T | F | F | F | F | F |
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.
| Feature | Renderers claiming it | Test 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 other | Per-family shadow-receiver and shadow-caster tests |
| Image-based lighting (PBR IBL) | The same five families | CNAEXT_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), METAL | Golden and hand-derived tests per family (easygl_*pbreffect_golden, vulkan_pbreffect_*, sdlgpu_*pbreffect_test, webgpu_*pbr3d_test) |
| Skinned mesh drawing | EasyGL, VULKAN, SDL_GPU, WEBGPU, SOFTWARE, DIRECTX9, METAL | Many per-family skinned-effect tests; parity fixture skinned_terms |
| Hardware instancing and base-instance drawing | Instancing: EasyGL, VULKAN, WEBGPU, SDL_GPU, DIRECTX9/11, METAL, SOFTWARE, HEADLESS; FNA3D conditional. Base-instance: VULKAN, SDL_GPU (device level) | Parity fixture instanced_draw |
| MSAA | See the matrix | Per-family MSAA tests; parity fixture backbuffer_msaa |
| Float / half-float render targets (HDR) | VULKAN, SDL_GPU, WEBGPU, DIRECTX11, METAL, EasyGL, SOFTWARE | Parity 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, FNA3D | renderer_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), WEBGPU | Used 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;DIRECTX9as 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 — inTexture2D,TextureCubeand (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 aVkFormatand checked against the device's format properties (BC1-3 native only withtextureCompressionBC,Bgra4444only withVK_EXT_4444_formats); the 7 extension formats haveVkFormatmappings for the capability-profile queries but defer atTexture2Dcreation, 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;HalfVector4andHdrBlendablestored natively asrgba16floatinTexture2D; Rgba64 has no render-target mapping.SOFTWARE: the 20 classic formats, DXT stored as blocks.FNA3D: Color and the signed-normalizedNormalizedByte2/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 publicTexture2Din one of them is refused although the renderer layer itself can carry it.DIRECTX9: an internal D3DFMT mapping exists (Bc7EXT/Bc7SrgbEXTunmapped), but the renderer does not overrideClassifySurfaceFormatEXT, so a publicTexture2Dfalls back to the framework rule and isColor-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 maturity | Identities |
|---|---|
| Production | SDL_RENDERER, OPENGLES3, OPENGL33, VULKAN, DIRECTX9, DIRECTX11 |
| Supported | WEBGL2, HEADLESS, STUB, SDL_GPU, METAL |
| Experimental | WEBGPU, 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:
| Renderer | Scenes matched at tolerance 0 | What is gated |
|---|---|---|
DIRECTX9 | 39 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 |
SOFTWARE | 18 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 too | Two line scenes (EasyGL_XnaLineCoverage); any difference fails the test |
FNA3D | 10 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 coverage | Status |
|---|---|
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 Windows | Manual dispatch only |
SDL_GPU, FNA3D, DIRECTX9, OPENGL33 | Named by no workflow |
| Android, and a build of the experimental C API | No workflow |
| All 14 identities across every valid platform/driver combination | Not 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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Capability answers, interface defaults and draw evidence — What a GraphicsCapability answer asks and guarantees in CNA: the 19 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.
- Compatibility levels and the evidence vector — The distinct levels of XNA compatibility CNA keeps apart, the evidence vocabulary used across the site, what each renderer class can prove, and how to state a verification claim.
- Custom HLSL ShaderEffect on the Direct3D renderers — How DIRECTX11 and DIRECTX9 compile custom HLSL, resolve uniform names by reflection, feed SpriteBatch and 3D draws and bind textures, and where DIRECTX9 stops.
- Direct3D evidence: MinGW cross-builds, Wine translators and native Windows — How DIRECTX9 and DIRECTX11 are built and tested: MinGW cross-builds, DXVK gates and separate Wine prefixes, the shared parity inventory, the manual Windows job and what each evidence tier proves.
- Direct3D presentation, swap interval, clears, viewport and scissor — How DIRECTX9 and DIRECTX11 treat presentation modes, PresentInterval, back-buffer and depth formats, full screen, flip-model rebinding, viewport, scissor, clears and depth bias.
- DIRECTX11 internals: lifetimes, recovery, readback and target finalisation — DIRECTX11 device creation, the three lifetime groups and device recovery, compute and indirect draws, the HeadlessEXT refusal, back-buffer readback, MRT and cube finalisation and render-target usage.
- DIRECTX9: stock-effect bytecode, device lifecycle and oracle findings — How the XNA-fidelity renderer compiles Microsoft's stock effects, enforces GraphicsProfile from D3DCAPS9, recovers lost devices, handles targets and why its sprite projection is what it is.
- easy-gl and meta-gl: the two-library GL stack beneath the EasyGL family — How meta-gl and easy-gl split loading, typed calls, ownership and failure beneath CNA's three GL identities: revisions CNA needs, feature gating, per-thread state, context loss, tests and build inheritance.
- EasyGL state, clears, targets, queries and buffers: current semantics — What the EasyGL GL-profile renderer does at this snapshot for wireframe, occlusion counts, colour masks, clears, two-sided stencil, fog, base vertex, render targets, context-loss policy and viewports.
- EasyGL: three GL profiles, one implementation, and how far evidence carries — What OPENGLES3, OPENGL33 and WEBGL2 share in EasyGL, where they differ, how far evidence carries between them, and how to choose among them.
- Evidence tiers of the native modern GPU renderers — What VULKAN, SDL_GPU, WEBGPU and METAL implement at this snapshot, what evidence backs each, why a capability bit is not evidence, and the defect shapes these renderers exposed.
- Glossary of XNA and CNA terms — Checked definitions of the XNA and CNA terms used across libcna.com, grouped by subject, each correcting common older readings and linking to the page with the detail.
- Presentation modes, swap interval, native handles and back-buffer readback across renderers — What each renderer family does with the presentation mode, swap interval, formats and full-screen request, how window handles are borrowed, and exactly what GetBackBufferData returns.
- SDL_GPU shader intake, pipeline keys and draw order — Why CNA's SDL_GPU renderer uses precompiled SPIR-V in SDL_gpu's set convention, which GLSL ShaderEffect accepts, how pipelines are keyed, what state is dynamic, and how draw order and vsync are kept.
- SDL_GPU uploads, render-target lifetime and swapchain recovery — What SDL_gpu validation exposed in CNA's SDL_GPU renderer, when uploads may cycle, how render targets outlive their wrappers, what MRT writes, and how a failed swapchain acquisition keeps the frame.
- SDL_RENDERER: the 2D contract, its refusals and its evidence — Where SDL_RENDERER's 2D boundary sits: execution-time 3D refusal, its single capability, emulated and unhonoured XNA features, address modes, clears, readback coordinates and how its tests run.
- Surface formats: profile gates, renderer verdicts and format usage — How CNA decides whether a texture, cube, volume or render target may use a SurfaceFormat: per-resource profile tables, the renderer verdict, draw-time rules and usage masks.
- The renderer contract: IGraphicsRenderer defaults, factories and failure shapes — Which IGraphicsRenderer bodies a renderer family must write, what each inherited default does to a public call, how null factories fail, and the evidence ladder behind a feature.
- Vulkan presentation, frame pacing and back-buffer readback — How CNA's VULKAN renderer picks its swapchain format and present mode, synchronises two frames in flight, and reads the back buffer without racing the presentation engine.
- WebGPU renderer semantics: surfaces, targets, mips and pipeline state — Exact behaviour of CNA's WEBGPU renderer: non-sRGB surface policy, target-relative SpriteBatch coordinates, cube render targets, blit-free mip generation, dynamic and baked state, ordered clears and BC textures.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-012: GraphicsDeviceCapabilityTest's catch-all expectations are still wrong for STUB and for Linux SDL_GPU builds without libshaderc — ExpectedCapabilities() now has an SDL_GPU arm, but on Linux it keeps the catch-all custom-effects expectation true although an SDL_GPU build without libshaderc answers false, and STUB, which answers false to every capabi
- CNA-BUG-054: HEADLESS reports occlusion-query support and a precise pixel count while its query always answers 1 — HEADLESS inherits OcclusionQuery = true and the default isPixelCountPreciseEXT() = true, but HeadlessOcclusionQueryRenderer completes at once and returns PixelCount() == 1 whatever was drawn.
- CNA-BUG-094: SDL_GPU claims compute shaders and SPIR-V compute intake on every device, including Direct3D 12 and Metal drivers that cannot take its raw SPIR-V compute pipelines — SupportsComputeShadersEXT() and the SPIR-V compute-stage answer are true whenever a device exists, but compute pipelines still reach SDL_gpu as raw SPIR-V, which its Direct3D 12 and Metal drivers reject; ShaderEffect SPI
- CNA-BUG-224: GraphicsDevice's DeviceLost documentation and device-status comments still say only Direct3D 9 reports device loss — The DeviceLost Doxygen says the event is never raised on desktop, and three maintainer comments say only Direct3D 9 calls the device-event callback, while three renderer families raise DeviceLost from their real error pa
- CNA-BUG-225: Two EasyGL comments still describe runtime GL profiles (phase P11) as future work — EasyGLRendererDescriptor.cpp's file header and the CNA_RENDERER_SHARED_EASYGL comment in RendererCombinations.cmake say a runtime GL profile is still to come, although both files implement or record it.
- CNA-BUG-234: CLAUDE.md's 'WebGPU Is Active' section still gives the current WEBGPU baseline as clear/present, Texture2D, buffer uploads and SpriteBatch, contradicting its own renderer paragraph, which describes a real 3D route with render targets — The renderer paragraph and the 'WebGPU Is Active' section of CNA's CLAUDE.md disagree about what WEBGPU implements. The section's 'current baseline' list and its 'remaining shader, state, effect, render-target ... tasks'
- CNA-BUG-254: docs/directx9-renderer.md says the DIRECTX9 custom ShaderEffect phase (D9-11) has not been started; plans/plan_dx9.md records it fully closed on 2026-07-15 — The DIRECTX9 renderer document lists custom ShaderEffect (D9-11) as not started, while the plan it points to records D9-110, D9-111 and D9-112 all closed on 2026-07-15.
- CNA-BUG-271: known_bugs.md still lists the FNA3D dangling-device use-after-free as OPEN and a lost repeated SpriteBatch Begin/End as a live symptom, although the source has fixed both — CNA's own known-bug list keeps two entries whose fixes are in the source and pinned by registered tests: FNA3D resource renderers now hold a shared device state that the renderer clears (Fna3d_Device_Lifetime), and repea
- CNA-GAP-064: WEBGPU does not propagate a real device loss: OnDeviceLost only logs, so DeviceLost, the device status and the draw gate react only to the debug hooks — WebGPURenderer sets its lost flag, closes CanBeginDrawEXT() and raises DeviceLost only inside DebugSimulateContextLoss; the callback registered for a real loss only writes to stderr. CNA's own plan (WEBGPU-182) records t
- CNA-PLAT-016: On Apple GPUs SamplerState.MipMapLevelOfDetailBias is refused by METAL below macOS/iOS 26, in builds against an older SDK and on devices that ignore it, and does not reach the GPU on FNA3D's Metal-backed driver — METAL throws NotSupportedException for a non-zero sampler LOD bias unless the OS and the build SDK are 26 or later and the device honours the bias; FNA3D on Apple hands the bias to SDL's Metal backend, which ignores it.
- CNA-PLAT-017: FNA3D has no occlusion queries on its SDL_GPU driver, the driver FNA3D tries first and the one it runs on Apple — Fna3dRenderer probes queries only on FNA3D's OpenGL and Direct3D 11 drivers; on its SDL_GPU driver (FNA3D's first choice, and its Metal route on Apple) OcclusionQuery is reported unsupported and its constructor refuses,