Graphics Renderers

CNA 0.1.0-alpha.1  ·  50 public identities  ·  46 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 50 public renderer identities, implemented by 46 families. The five EasyGL identities below share one family; none of the public identities is an alias. The defaults are WEBGL2 under Emscripten, OPENGLES3 on Linux, and SDL_RENDERER everywhere else.

50 identities are not 50 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.

Multi-renderer builds and runtime selection

v0.1.0-alpha.1 adds 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. Omitting the plural option keeps the ordinary single-renderer build.

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. A name that is unknown or not compiled in throws; CNA never substitutes silently.

#include "CNA/GraphicsRendererSelection.hpp"

CNA::GraphicsRendererSelection::SetPreferred(
    CNA::GraphicsRendererType::Vulkan);
CNA::GraphicsRendererSelection::SetFallbackChain({
    CNA::GraphicsRendererType::OpenGLES3,
    CNA::GraphicsRendererType::Software
});
MyGame game;
game.Run();

Fallback is off by default. SetFallbackChain() or EnableAutomaticFallback(true) opts in; failures are recorded in GetFallbackHistory(). 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 the build default.

Selection latches at the first device. Changing preference or fallback policy afterwards throws. 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.

Combination rules

Multi-renderer builds add code size and every selected family's dependencies. The tag rejects demonstrated conflicts at configure time:

  • PORTABLEGL cannot share a binary with a renderer that calls real OpenGL because PortableGL defines the same global gl* symbols.
  • GDI cannot be combined with SOFTWARE; GDI recompiles the Software sources with a different mode.
  • Windows-only, Emscripten-only and macOS-only identities cannot cross platform partitions in one toolchain.
  • GLIDE pins the binary to the 32-bit Glide ABI and cannot be combined with another renderer.

Several EasyGL identities may coexist after alpha.1's runtime-profile work, provided they do not cross the native/Emscripten platform partition.

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.

Old nameToday
CNA_GRAPHICS_BACKENDCNA_GRAPHICS_RENDERER
IGraphicsBackendIGraphicsRenderer
EASYGLOne of OPENGLES2 / OPENGLES3 / OPENGL33 / WEBGL1 / WEBGL2
D3D9 / D3D11 / D3D12DIRECTX9 / DIRECTX11 / DIRECTX12
DX3FREEDIRECT
ASCIIRemoved as a renderer. It is now AsciiPostProcessEffect, a CNAEXT effect (and CNAEXT is off by default)

DIRECTX3 is not the old DX3. A renderer named DIRECTX3 exists today, but it is real DirectX 3 — DirectDraw v2 plus Direct3D v2 — and it is Windows-only. The old DX3, which was backed by the free-direct sibling library, is now called FREEDIRECT. Picking the wrong one gets you a completely different renderer on a completely different platform gate.

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

The GL family — five profiles, one EasyGL implementation

Five renderer identities share one internal implementation, EasyGL, which lives in the ../easy-gl sibling repository (itself needing ../meta-gl). The profiles are distinguished by a CNA_GL_PROFILE_* define.

The profiles are not cosmetic. OPENGLES2 and WEBGL1 genuinely lose multiple render targets, occlusion queries, Texture3D, instancing and multi-stream vertex input. Choosing the older profile is a real capability cut, not a version label.

RendererWhat it isScopePlatformDependency
OPENGLES2OpenGL ES 2.0 profile of EasyGL2D + 3DNot Emscripten../easy-gl + ../meta-gl
OPENGLES3OpenGL ES 3.0 profile of EasyGL — the Linux default2D + 3DNot Emscripten../easy-gl + ../meta-gl
OPENGL33Desktop OpenGL 3.3 core profile of EasyGL2D + 3DNot Emscripten../easy-gl + ../meta-gl
WEBGL1WebGL 1 context in the browser2D + 3DEmscripten only../easy-gl + ../meta-gl
WEBGL2WebGL 2 context in the browser — the Emscripten default2D + 3DEmscripten only../easy-gl + ../meta-gl

Hand-written ShaderEffect shaders execute on all five. Cube faces used inside a multiple-render-target set are unimplemented on this family. Against the 39-scene XNA oracle corpus, EasyGL matches 10 of 39 scenes at tolerance 0 — see How renderers are verified.

"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.

Standalone GL renderers

Four further GL renderers are deliberately independent of EasyGL: each creates its own context because EasyGL cannot express the profile they target.

RendererWhat it isScopePlatformDependency
OPENGLES1OpenGL ES 1.1 fixed-function ("Common" profile)2D + 3DAny (no CMake gate)System GLESv1_CM library and headers
OPENGL1Legacy desktop OpenGL 1.x fixed-function2D + 3DLinux / WindowsSystem desktop OpenGL
OPENGL2Desktop OpenGL 2.1 compatibility profile (GLSL 1.10)2D + 3DAny (no CMake gate)System desktop OpenGL
OPENGL4Real desktop OpenGL 4.x core profile2D + 3DAny (no CMake gate)System desktop OpenGL

OPENGL2 and OPENGL4 execute custom ShaderEffect shaders; both leave cube faces inside an MRT set unimplemented. OPENGLES1 is the strongest non-Direct3D result against the XNA oracle corpus, matching 11 of 39 scenes at tolerance 0.

Modern GPU APIs

Four renderers drive a modern explicit GPU API directly, with no middleware in between.

RendererWhat it isScopePlatformDependency
VULKANVulkan, driven directly by CNA2D + 3DAny (no CMake gate)Vulkan headers, loader and a driver ICD
SDL_GPUSDL3's own GPU API2D + 3DAny (no CMake gate)SDL3 (vendored submodule)
WEBGPUWebGPU through the native wgpu-native implementation2D + 3DAny (no CMake gate)wgpu-native package (downloaded by CMake)
METALApple Metal directly; SDL3 supplies only the window and CAMetalLayer2D + 3DmacOS onlymacOS SDK (Metal)

VULKAN, SDL_GPU and METAL are the three here with a stated shader position: custom ShaderEffect shaders execute on VULKAN and SDL_GPU, and throw on METAL. SDL_GPU is one of the four renderers that never override SupportsCapability() (see below), and it leaves cube faces inside an MRT set unimplemented.

METAL has automatically triggered macOS CI on a macos-14 runner. Alpha.1's separate iOS path is restricted to SDL_RENDERER and proves final linking plus a one-frame simulator launch; it does not validate METAL on iOS, physical devices or pixels. tvOS remains unsupported.

Portable 2D renderers and vector rasterizers

These five are 2D by design. Their 3D entry points throw deterministically rather than silently drawing nothing — a failure mode you can test for.

RendererWhat it isScopePlatformDependency
SDL_RENDERERSDL3's built-in accelerated 2D renderer — the default everywhere except Linux and the web2D onlyAny (no CMake gate)SDL3 (vendored submodule)
SKIAGoogle Skia raster surface, presented through SDL32D onlyAny (no CMake gate)An externally built Skia (CNA_SKIA_ROOT + CNA_SKIA_BUILD_DIR)
BLEND2DBlend2D CPU vector rasterizer, presented through SDL32D onlyAny (no CMake gate)Blend2D + AsmJit (fetched by CMake)
OPENVGOpenVG 1.1 vector API (ShivaVG) on a desktop OpenGL context it creates itself2D onlyLinux / Windows / macOSShivaVG (fetched by CMake) + desktop OpenGL
NANOVGNanoVG using its compiled GLSL 1.10 GL2 pipeline2D onlyDesktop OpenGL 2.1Pinned NanoVG source + desktop OpenGL

SKIA is the one renderer here that cannot be built from a plain git clone: Skia has to be checked out and built separately, then pointed at with two cache variables.

Windows renderers: the Direct3D ladder

Fourteen renderers are hard-gated to CMAKE_SYSTEM_NAME=Windows and stop the configure step with a FATAL_ERROR anywhere else. Eleven of them walk the historical Direct3D ladder from DirectX 1 to DirectX 12, using the genuine COM interfaces of each era rather than emulating them over a modern API.

The tag provides manual Wine plus DXVK / vkd3d-proton validation paths, not an automatic Wine lane. Two native-Windows MSVC workflows exist — one for DIRECTX11, DIRECTX12 and DIRECT2D, one for GDI — but both are manual-dispatch only, so nothing on real Windows runs automatically. Treat any Wine result as evidence for that exact manually run configuration.

RendererWhat it isScopePlatformDependency
DIRECTX1Real DirectDraw v1 COM interfaces2D onlyWindows onlyWindows ddraw headers / import libraries
DIRECTX2DirectDraw v1 plus Direct3D v2 DrawPrimitive2D + 3DWindows onlyWindows ddraw / d3d
DIRECTX3Real DirectX 3: DirectDraw v2 plus Direct3D v2 — not the old DX32D + 3DWindows onlyWindows ddraw / d3d
DIRECTX5DirectDraw v4 plus Direct3D v3, FVF DrawPrimitive2D + 3DWindows onlyWindows ddraw / d3d
DIRECTX6The DirectX 5 interfaces plus a real stencil buffer2D + 3DWindows onlyWindows ddraw / d3d
DIRECTX7DirectDraw v7 plus Direct3D v7; the viewport object is gone2D + 3DWindows onlyWindows ddraw / d3d
DIRECTX8Real Direct3D 8, fixed-function; DirectDraw no longer exists at this era2D + 3DWindows onlyWindows d3d8 (DXVK-delivered under Wine)
DIRECTX9Real Direct3D 9 — the API real XNA 4.0 itself ran on2D + 3DWindows onlyWindows d3d9
DIRECTX10Real Direct3D 10: no fixed-function pipeline, so every draw needs real HLSL2D + 3DWindows onlyWindows d3d10 (DXVK-delivered under Wine)
DIRECTX11Real Direct3D 112D + 3DWindows onlyWindows d3d11 / dxgi
DIRECTX12Real Direct3D 122D + 3DWindows onlyWindows d3d12 / dxgi
DIRECT2DDirect2D 1.12D onlyWindows onlyWindows d2d1
GDIClassic Win32 GDI2D onlyWindows onlyWindows GDI
GLIDE3dfx Glide 3.x, called dynamically2D + 3DWindows only, 32-bit (i686)A caller-supplied glide3x.dll at runtime (deliberately not vendored)
FREEDIRECTThe DirectDraw-shaped free-direct reimplementation on SDL3 — formerly DX32D onlyAny (no CMake gate)../free-direct (which uses ../free-api)

Three things are worth calling out in this group:

  • FREEDIRECT is the odd one out. It is listed here because it is DirectDraw-shaped and inherited the old DX3 name, but it is not Windows-gated — it is built on SDL3 through the free-direct sibling repository and configures anywhere.
  • DIRECTX1 is a declared stub for 3D. Its entire 3D pipeline throws; it is a 2D renderer in practice.
  • GLIDE needs a 32-bit toolchain. Glide's native application ABI is 32-bit, so an x86_64 configure is refused rather than silently truncating pointers.

DIRECTX9, DIRECTX11 and DIRECTX12 execute custom ShaderEffect shaders and leave cube faces inside an MRT set unimplemented; all three also never override SupportsCapability(). GDI throws on custom shaders. DIRECTX9 is the only renderer that is pixel-exact against the XNA oracle corpus — see How renderers are verified.

Browser renderers (Emscripten-only)

Four renderers exist only inside a browser, on top of browser-facing 2D APIs. They join WEBGL1 and WEBGL2 from the GL family, which are Emscripten-only for the same reason.

RendererWhat it isScopePlatformDependency
CANVASThe HTML5 2D canvas context, driven directly — no WebGL involved2D onlyEmscripten onlyEmscripten SDK
HTML_DOMSpriteBatch output as pooled, CSS-transformed <div> elements2D onlyEmscripten onlyEmscripten SDK
SVG_DOMSpriteBatch output as real <svg> / <image> elements, tinted with feColorMatrix2D onlyEmscripten onlyEmscripten SDK
PIXIJSPixiJS v7 sprites through PixiJS's WebGL batch renderer2D onlyEmscripten onlyPinned PixiJS 7.4.2 UMD bundle

HTML_DOM has its own automatic browser workflow, while the Emscripten multi-renderer workflow also exercises browser configurations. HTML_DOM and SVG_DOM throw on custom ShaderEffect shaders.

Web caveats apply to every renderer in this group, and to WEBGL1/WEBGL2. There is no save persistence at all on the web — SDL_GetPrefPath resolves to volatile MEMFS, CNA mounts no IDBFS and never calls FS.syncfs, so every save is silently discarded on reload. There is no video: the video translation units are excluded from the build, so Video/VideoPlayer compile and then fail to link. And your Game object must be heap-allocated — a stack-allocated Game is silently corrupted. See Platforms.

Portable middleware layers

Seven renderers do not name a native graphics API at all. Each targets a portable middleware layer that itself picks a native API — sometimes at build time, sometimes at runtime. That indirection is the point, and it is also where the surprises live.

RendererWhat it isScopePlatformDependency
BGFXbgfx's cross-platform rendering library2D + 3DAny (no CMake gate)bgfx.cmake (fetched by CMake at a pinned revision)
LLGLLLGL, which picks OpenGL or Vulkan itself2D + 3DLinux / X11 in practiceLLGL (fetched by CMake)
DILIGENTDiligent Engine; the device type is picked at runtime2D + 3DAny (no CMake gate)DiligentCore (fetched by CMake)
SOKOLsokol_gfx, a single-header GPU abstraction2D + 3DAny (no CMake gate)sokol (fetched by CMake)
WICKEDWicked Engine's render hardware interface (Vulkan or D3D12)2D + 3DNot EmscriptenWickedEngine (fetched by CMake)
MAGNUMDesktop OpenGL expressed entirely through Magnum's typed GL wrappers2D + 3DNot EmscriptenMagnum + Corrade (system packages or fetched)
FNA3DFNA's own XNA-shaped C graphics library, with MojoShader2D + 3DAny (no CMake gate)FNA3D + the pinned FNA stock-effect blobs (fetched by CMake)
IGLMeta IGL with a runtime OpenGL or Vulkan device choice2D + 3DLinux/X11IGL v1.1.1 and selected native dependencies

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

  • BGFX accepts custom ShaderEffect shaders and silently ignores them. Nothing throws, nothing warns; the effect simply does not happen. It also leaves cube faces inside an MRT set unimplemented.
  • LLGL is experimental, Linux/X11-only and OpenGL-only. LLGL's own Vulkan module cannot be selected from CNA. Custom shaders do execute.
  • SOKOL's native API is a build-time choice. CNA_SOKOL_API defaults to GLCORE; the other values are wired but unverified. Custom shaders execute.
  • MAGNUM executes custom shaders and, like WICKED, cannot be selected under Emscripten.
  • FNA3D enables XNA/FNA compiled Effect Framework bytecode automatically. It executes those binaries through MojoShader and CNA feeds it the stock-effect blobs from the pinned FNA checkout. EasyGL, SDL_GPU and Vulkan can enable the same compiled-effect layer explicitly; it is not universal. FNA3D still cannot compile ShaderEffect source. Against the XNA oracle corpus it matches 10 of 39 scenes at tolerance 0.

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 rasterizer2D + 3DAny (no CMake gate)None
PORTABLEGLPortableGL's CPU implementation of an OpenGL 3.x-style pipeline2D + 3DAny (no CMake gate)PortableGL single header (fetched by CMake)
TINYGLTinyGL's fixed-function OpenGL 1.x subset, rasterized on the CPU2D + constrained 3DNo window or display server requiredPinned TinyGL source
HEADLESSA no-GPU harness with validation modes and countersNo pixel outputAny (no CMake gate)None
STUBA deliberate no-op: no window, no video subsystem, no GPU library, no bookkeepingNo pixel outputAny (no CMake gate)None
💡

SOFTWARE and PORTABLEGL never present to a window. They rasterize for real, but you read the result back with GetBackBufferData() rather than watching it appear on screen. That makes them useful for deterministic, GPU-free pixel checks — and unsuitable as a way to actually show a game to a player.

SOFTWARE accepts a custom ShaderEffect but does not execute it: its effect renderer takes any GLSL/HLSL/WGSL source, reports success without compiling it, and its own fixed CPU shading path produces the pixels. Your shader will not run. HEADLESS records the call without executing it, which is exactly what a harness should do.

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 D3D9 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 they genuinely execute on: the five GL-profile identities, OPENGL2, OPENGL4, VULKAN, SDL_GPU, SOKOL, LLGL, MAGNUM, DIRECTX9, DIRECTX11 and DIRECTX12. HEADLESS records them only. Elsewhere:

  • BGFX accepts them and silently ignores them — the one case with no diagnostic at all.
  • GDI, METAL, HTML_DOM and SVG_DOM throw.
  • 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. FNA3D supports XNA/FNA Direct3D 9 Effect Framework bytecode by default. EasyGL, SDL_GPU and Vulkan support it only when their respective CNA_EASYGL_COMPILED_EFFECTS, CNA_SDL_GPU_COMPILED_EFFECTS or CNA_VULKAN_COMPILED_EFFECTS option is enabled; all three default to OFF. Other renderer families report the capability unavailable. See Effects System.

Capability reporting

Do not branch game logic on a bare positive SupportsCapability() answer and expect execution proof. The base implementation explicitly returns false for MultiStreamVertexInput, delegates StencilBuffer, and routes CompiledEffects through its false-by-default opt-in; its other entries return true. DIRECTX9, DIRECTX11, DIRECTX12 and SDL_GPU have no override at all.

The practical consequence is that a capability query is a hint, not a contract. If your game needs a feature, the reliable checks are the ones this page states directly: which renderers are 2D only, which execute custom shaders, and which leave cube faces inside an MRT set unimplemented (DIRECTX9, DIRECTX11, DIRECTX12, BGFX, the EasyGL family, SDL_GPU, OPENGL2 and OPENGL4).

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 reference scenes under tools/xna-oracle/scenes/, with 39 reference PNGs captured from real XNA 4.0 running under Wine + DXVK. Renderers are diffed against those images:

RendererScenes matched at tolerance 0
DIRECTX939 of 39
OPENGLES111 of 39
EasyGL (the five GL profiles)10 of 39
FNA3D10 of 39

"Pixel-exact" is a DIRECTX9-only statement. It is the one renderer that matches the oracle corpus exactly, which makes sense — real XNA 4.0 ran on Direct3D 9. No other renderer is pixel-perfect against real XNA, and CNA does not claim otherwise.

What CI does and does not reach

CI exists across Linux, macOS and headless-browser jobs, but alpha.1 has no effective full-suite or GPU-oracle gate. The intended unfiltered job fails at configure time because it still selects the removed EASYGL identity:

Renderer coverageStatus
Linux, Apple, Emscripten, platform-abstraction, multi-renderer and declared C API gatesPresent among 21 files; some jobs are broken or manual
Unfiltered general suite and the two EasyGL Input rowsBroken configuration: EASYGL is not selectable
DIRECTX11, DIRECTX12, DIRECT2D, GDI on native MSVCManual dispatch only
All 50 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.

💡

For 2D-only games wanting maximum portability: SDL_RENDERER. It needs nothing beyond the vendored SDL3, and its 3D entry points throw deterministically instead of quietly drawing nothing.

💡

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

💡

For CI with no GPU: HEADLESS or STUB when you only need game logic exercised, or SOFTWARE/PORTABLEGL when you need real pixels — read back with GetBackBufferData(), since neither presents to a window.

💡

For the web: WEBGL2 is the default and the widest path; HTML_DOM is the one browser renderer with automatic CI. Whichever you pick, the web caveats above — no save persistence, no video, heap-allocated Game — apply.

Know what you are opting into. Custom ShaderEffect shaders are silently ignored on BGFX and throw on GDI, METAL, HTML_DOM and SVG_DOM; FNA3D cannot compile them but does support the separate compiled Effect Framework path. DIRECTX1's 3D pipeline is a declared stub. LLGL is experimental and Linux/X11-only. OPENGLES2 and WEBGL1 drop MRT, occlusion queries, Texture3D, instancing and multi-stream vertex input. CI does not exhaustively exercise every identity and driver combination.