Tutorial 72: Choosing a Renderer
What you’ll learn
- How CNA's 50 renderer identities group, and what each group is for.
- Which renderers are platform-gated, which are 2D-only, and which never produce a pixel.
- Selecting one renderer or compiling several for pre-device runtime selection.
- Why
SupportsCapability()is not yet a trustworthy feature test.
Before you start — Tutorial 20: Building and Running Your Game. The default remains a single renderer; Tutorial 126 covers the opt-in multi-renderer workflow in full.
CNA exposes 50 renderer identities, implemented by 46 renderer families. The default mode compiles one. An opt-in CNA_GRAPHICS_RENDERERS list can compile several compatible families; the application chooses one before the first graphics device is created, after which the choice latches. This is startup selection and optional fallback, not hot device switching.
The count is not a coverage claim. The 50 identities are not equally complete: some exist to prove a historical API can still be driven, several are deliberately 2D-only or no-output, and others are narrow or experimental. For reference-level capability and platform detail, see the Renderers guide.
Selecting a renderer
For the recommended single-renderer build, use either the named value or exactly one legacy boolean. Do not mix the two forms.
# Form 1 — name the renderer directly (case-sensitive)
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3
# Form 2 — switch exactly one CNA_RENDERER_<NAME> option ON
cmake -S . -B build -DCNA_RENDERER_OPENGLES3=ON
With form 2, turning on zero or more than one option is a hard FATAL_ERROR: "Exactly one renderer option must be ON when using CNA_RENDERER_* options."
For a multi-renderer build, provide a semicolon-separated list and keep the singular value as the default member:
cmake -S . -B build-multi \
-DCNA_GRAPHICS_RENDERERS="OPENGLES3;VULKAN;SOFTWARE" \
-DCNA_GRAPHICS_RENDERER=OPENGLES3
Defaults when you pass nothing: Emscripten gets WEBGL2, Linux gets OPENGLES3, and every other platform gets SDL_RENDERER. Because the Linux default is OPENGLES3, an ordinary Linux build needs the ../sharp-runtime, ../easy-gl and ../meta-gl sibling checkouts present next to cna/ — they are separate repositories, not submodules.
Names that no longer exist. EASYGL, D3D9, D3D11, D3D12, DX3 and ASCII are not accepted values and fail with "Unknown graphics renderer". Use one of the 50 identities in the renderer reference. The former DX3 free-direct-backed path is now FREEDIRECT; today's DIRECTX3 is the actual DirectDraw v2 plus Direct3D v2 renderer.
Group 1 — the OpenGL family (9 identities)
Five of these share one internal implementation, called EasyGL, which lives in the ../easy-gl sibling library (itself needing ../meta-gl). "EasyGL" is the implementation's name; it is not a selectable renderer value. Selecting any of the five compiles the same tree and adds a CNA_GL_PROFILE_* define.
The GL profiles are not cosmetic. OPENGLES2 and WEBGL1 genuinely lose multiple render targets, occlusion queries, Texture3D, instancing and multi-stream vertex input. Choosing the ES 2 / WebGL 1 profile removes real features, it does not merely relabel a context.
| Value | What it is | Platform |
|---|---|---|
| OPENGLES3 | OpenGL ES 3.0 via EasyGL. The Linux default. | Not Emscripten |
| OPENGL33 | Desktop OpenGL 3.3 core profile via EasyGL. | Not Emscripten |
| OPENGLES2 | OpenGL ES 2.0 via EasyGL. No MRT, no occlusion queries, no Texture3D, no instancing. | Not Emscripten |
| WEBGL2 | WebGL 2 via EasyGL. The Emscripten default. | Emscripten only |
| WEBGL1 | WebGL 1 via EasyGL. Same feature losses as OPENGLES2. | Emscripten only |
| OPENGL4 | Real desktop OpenGL 4.x core profile. Independent of EasyGL. | Desktop |
| OPENGL2 | Native OpenGL 2.1, written without EasyGL. | Desktop |
| OPENGL1 | Native legacy OpenGL 1.x fixed-function. | Linux and Windows only |
| OPENGLES1 | OpenGL ES 1.1 fixed-function. | Desktop |
OPENGLES3 is the renderer CNA develops against day to day, and the one its Linux CI configures. It is the safe answer if you have no reason to pick another.
Group 2 — modern GPU APIs (4 identities)
| Value | What it is | Platform |
|---|---|---|
| VULKAN | Native Vulkan. Executes custom ShaderEffect shaders as SPIR-V. | Where Vulkan is available |
| SDL_GPU | SDL's own GPU abstraction. Also SPIR-V for custom shaders. | Where SDL_GPU has a device |
| WEBGPU | Native WebGPU through wgpu-native, WGSL shaders. | Desktop |
| METAL | Native Apple Metal. The only renderer with automatic macOS CI (macos-14). | macOS only |
METAL is macOS-only in this tag. Alpha.1 has a separate, narrow iOS path restricted to SDL_RENDERER, with final-link and one-frame simulator smoke coverage; tvOS remains unsupported.
Group 3 — portable middleware and engine wrappers (8 identities)
These delegate to a third-party rendering library, which then picks its own native API. That extra layer is the point — and also the risk, because CNA's coverage of each wrapper is narrower than the wrapped library itself.
| Value | What it is | Notes |
|---|---|---|
| BGFX | The bgfx library. | Accepts a custom ShaderEffect and silently ignores it. See Tutorial 86. |
| SOKOL | sokol_gfx. Executes custom shaders. | CNA_SOKOL_API defaults to GLCORE; other values are wired but unverified and warn at configure time. |
| DILIGENT | Diligent Engine. | Needs an external Diligent checkout. |
| LLGL | LLGL. Executes custom shaders. | Experimental: Linux/X11-only and OpenGL-only — LLGL's Vulkan module cannot be selected. |
| IGL | Meta's Intermediate Graphics Library. | Linux/X11; chooses an OpenGL or Vulkan device inside the family. |
| MAGNUM | mosra/magnum. Executes custom shaders. | Not available on Emscripten. |
| WICKED | Wicked Engine. | Not available on Emscripten; needs CNA_WICKED_ROOT. |
| FNA3D | FNA-XNA's FNA3D plus MojoShader. | Cannot compile custom shaders at all. Matches 10 of the 39 XNA oracle scenes at tolerance 0. |
Group 4 — the Windows ladder (14 identities)
Fourteen renderers are hard-gated to Windows: configuring them anywhere else is a FATAL_ERROR, not a warning. Eleven of them walk the Direct3D version ladder from 1995 forward, and three are the other classic Windows drawing APIs.
| Value | What it targets |
|---|---|
| DIRECTX1 | Real DirectDraw v1. A declared stub — its entire 3D pipeline throws, and it is 2D-only in practice. |
| DIRECTX2 | Real DirectDraw v1 + Direct3D v2 DrawPrimitive. |
| DIRECTX3 | Real DirectDraw v2 + Direct3D v2 DrawPrimitive. Not the old site's DX3. |
| DIRECTX5 | Real DirectDraw v4 + Direct3D v3 FVF DrawPrimitive. |
| DIRECTX6 | Real DirectDraw v4 + Direct3D v3, with real stencil. |
| DIRECTX7 | Real DirectDraw v7 + Direct3D v7, flattened device model. |
| DIRECTX8 | Real Direct3D 8, fixed-function, delivered through DXVK. |
| DIRECTX9 | Direct3D 9. Compiles HLSL at runtime. The pixel-exactness renderer — see below. |
| DIRECTX10 | Real ID3D10Device with real HLSL shaders, delivered through DXVK's d3d10core. |
| DIRECTX11 | Native Direct3D 11. Compiles HLSL at runtime. |
| DIRECTX12 | Native Direct3D 12. Compiles HLSL at runtime. |
| DIRECT2D | Direct2D 1.1. 2D-only. |
| GDI | Classic Win32 GDI. 2D-only; throws on a custom ShaderEffect. |
| GLIDE | Glide 3.x against an external glide3x.dll. Additionally requires a 32-bit (i686) toolchain. |
These have manual validation paths. CNA ships cross-build and Wine scripts for DXVK/vkd3d-proton, but alpha.1 has no automatic Wine workflow. Windows MSVC workflows also exist as manual-dispatch only. Treat native-hardware behaviour as outside CNA's continuous gates and verify it yourself before shipping.
Pixel-exactness is a DIRECTX9 statement, and only that. CNA's XNA oracle corpus is 39 scenes captured from the genuine Microsoft XNA 4.0 runtime. DIRECTX9 matches all 39 at tolerance 0. The EasyGL family matches 10, FNA3D 10 and OPENGLES1 11. If bit-identical XNA 4.0 output is the goal, DIRECTX9 is the answer and nothing else is.
Group 5 — browser renderers (4 identities, plus the two WebGL profiles)
Six identities are available to Emscripten. Two — WEBGL1 and WEBGL2 — are EasyGL profiles. The DOM/canvas-facing identities are:
| Value | What it is |
|---|---|
| CANVAS | HTML Canvas 2D. 2D-only. |
| HTML_DOM | CSS-composited HTML DOM elements. 2D-only; throws on a custom ShaderEffect. It has a dedicated automatic browser workflow; a separate Emscripten multi-renderer workflow covers additional browser configurations. |
| SVG_DOM | SVG DOM nodes. 2D-only; throws on a custom ShaderEffect. |
| PIXIJS | PixiJS-backed browser rendering through Emscripten integration. 2D-only. |
Three web caveats apply to every renderer in this group. There is no save persistence — SDL_GetPrefPath resolves to volatile MEMFS under Emscripten, CNA mounts no IDBFS and never calls FS.syncfs, so every save is discarded on reload. There is no video — the video translation units are excluded from web builds, so Video/VideoPlayer compile and then fail to link. And your Game subclass must be heap-allocated. Tutorial 81 covers all three.
Group 6 — 2D raster and vector (6 identities)
These have no 3D pipeline at all. That is a design decision, not a gap: a 3D call raises a deterministic exception rather than drawing nothing.
| Value | What it is | Platform |
|---|---|---|
| SDL_RENDERER | SDL3's built-in accelerated 2D renderer. The default outside Linux and Emscripten. | Wherever SDL3 runs |
| SKIA | Skia 2D raster. | Desktop |
| BLEND2D | Blend2D 2D vector raster. | Desktop |
| OPENVG | OpenVG through ShivaVG, which needs a real fixed-function GL context. | Linux, Windows, macOS only |
| NANOVG | NanoVG's GLSL 1.10 / GL2 vector pipeline. 2D-only. | Linux, Windows, macOS only |
| FREEDIRECT | DirectDraw-shaped 2D, fronting the ../free-direct sibling (a narrow, game-driven DirectX 3 subset reimplemented on SDL3). Formerly named DX3. | Cross-platform |
Group 7 — CPU-only and no-output (5 identities)
| Value | What it is |
|---|---|
| SOFTWARE | A genuine CPU rasteriser. It produces real pixels but never presents to a window — you read them back with GetBackBufferData(). It accepts a custom ShaderEffect but does not execute it — its own fixed CPU shading path renders instead. |
| PORTABLEGL | rswinkle/PortableGL, a CPU software OpenGL 3.x. Also read-back only, never presented. |
| TINYGL | TinyGL software OpenGL implementation for CPU-side compatibility experiments. |
| HEADLESS | No GPU and no window. It records custom shader submissions rather than executing them. Built for fast CI of game logic. |
| STUB | A no-op renderer. No pixel output of any kind. |
Which renderers refuse 3D
Thirteen renderers are 2D-only and throw deterministically the moment you reach for the 3D pipeline: SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SKIA, BLEND2D, FREEDIRECT, DIRECTX1, GDI, SVG_DOM, OPENVG, NANOVG and PIXIJS. Two more — HEADLESS and STUB — produce no pixel output at all, and SOFTWARE and PORTABLEGL render real pixels but never present them to a window.
Which renderers execute custom shaders
A custom ShaderEffect (see Tutorial 52) genuinely executes on the five GL profile identities, OPENGL2, OPENGL4, VULKAN, SDL_GPU, SOKOL, LLGL, MAGNUM, DIRECTX9, DIRECTX11 and DIRECTX12. HEADLESS records the submission without executing it.
Everywhere else you get one of three different outcomes, and the difference matters:
BGFXandSOFTWAREaccept the effect and silently ignore the source. No error, no shader, no clue — this is the one to watch for.GDI,METAL,HTML_DOMandSVG_DOMthrow. You find out immediately.FNA3Dcannot compile custom shaders at all.
The shading language also varies: GLSL on the GL family, SPIR-V on VULKAN and SDL_GPU, HLSL compiled at runtime on DIRECTX9/DIRECTX11/DIRECTX12, and WGSL on WEBGPU. One source string is not portable across all of them.
SupportsCapability() fails open
GraphicsCapability has 14 members. The base explicitly opts out of multi-stream input and compiled effects and delegates stencil, but returns true for the remaining entries — and DIRECTX9, DIRECTX11, DIRECTX12 and SDL_GPU supply no override at all. Treat a true as "not known to be unsupported", not as a guarantee, and test the renderer you ship.
One gap worth knowing before you design around it
Cube faces inside a multiple-render-target set are unimplemented on eight otherwise-capable 3D renderers: DIRECTX9, DIRECTX11, DIRECTX12, BGFX, the EasyGL family, SDL_GPU, OPENGL2 and OPENGL4. Plain 2D MRT works on these; binding a RenderTargetCube face as one slot of an MRT set does not. See Tutorial 62.
Decision table
| Situation | Choose |
|---|---|
| Not sure / just starting | OPENGLES3 — CNA's day-to-day development renderer and the Linux default |
| Pure 2D game, maximum portability | SDL_RENDERER |
| 3D game on Linux or Windows | OPENGLES3, or VULKAN for explicit GPU control |
| Bit-identical XNA 4.0 output | DIRECTX9 (Windows-only) — the only renderer that matches 39/39 oracle scenes |
| macOS | METAL — the only macOS renderer, and the only one with automatic macOS CI |
| Browser, 3D or 2D | WEBGL2 (the Emscripten default). Read the three web caveats above first. |
| Browser, no WebGL context available | CANVAS, HTML_DOM or SVG_DOM — all 2D-only |
| CI without a GPU, game logic only | HEADLESS |
| Deterministic, GPU-free pixel comparison | SOFTWARE (read back with GetBackBufferData()) |
| Targeting genuinely old Windows hardware | Pick the matching rung of the DirectX ladder — but verify on real hardware yourself |
| Anything that needs a custom shader | Not BGFX (silently ignored) and not FNA3D (cannot compile one) |
Setting the renderer in CMake
# Configure and build with the Vulkan renderer
cmake -S . -B build-vulkan -DCNA_GRAPHICS_RENDERER=VULKAN
cmake --build build-vulkan --target CnaTests
# Equivalent, using the per-renderer option form
cmake -S . -B build-vulkan -DCNA_RENDERER_VULKAN=ON
# Windows-only renderers need a Windows toolchain (MSVC, or MinGW-w64 from Linux)
cmake -S . -B build-d3d11 \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
-DCNA_GRAPHICS_RENDERER=DIRECTX11
# Emscripten-only renderers refuse to configure for a native target
emcmake cmake -S . -B build-canvas -DCNA_GRAPHICS_RENDERER=CANVAS
cmake --build ... --target CNA no longer works. CNA is an add_library(CNA INTERFACE) umbrella with no sources, so it is not a buildable target. Build CnaTests, a demo target, or simply cmake --build build.
CTest labels follow the renderer names too: ctest -L DIRECTX9 works, ctest -L D3D9 matches nothing.
Compile-time defines in game code
The selected renderer emits a CNA_RENDERER_<NAME> preprocessor define. The five GL profiles are the one exception: they all emit CNA_RENDERER_EASYGL (the shared implementation identity) plus a CNA_GL_PROFILE_<NAME> define naming the profile.
#if defined(CNA_RENDERER_VULKAN)
// Vulkan-specific startup work
#elif defined(CNA_RENDERER_EASYGL)
// Any of OPENGLES2 / OPENGLES3 / OPENGL33 / WEBGL1 / WEBGL2.
// Narrow it further when the profile actually matters:
#if defined(CNA_GL_PROFILE_OPENGLES2) || defined(CNA_GL_PROFILE_WEBGL1)
// No MRT, no occlusion queries, no Texture3D, no instancing here.
useDeferredShading_ = false;
#endif
#elif defined(CNA_RENDERER_DIRECTX11) || defined(CNA_RENDERER_DIRECTX12)
// Windows-only native Direct3D path
#endif
Runtime selection in a multi-renderer build
GraphicsRendererSelection::SetPreferred() has highest precedence, followed by the CNA_GRAPHICS_RENDERER environment variable and finally the compiled default. Selection must happen before the first graphics device and then latches. Unknown or uncompiled identities throw. Fallback is opt-in; a failed preferred renderer is otherwise a hard failure.
Not every list is legal: CMake rejects known ABI/windowing conflicts such as PortableGL with a real GL family, GDI with Software, Glide with another renderer, and incompatible OS partitions. Multi-renderer builds also carry every selected dependency and increase build and binary size. See Tutorial 126.
What CI actually covers
Alpha.1 contains 21 workflow files. They include Linux, Apple, Emscripten, platform-abstraction, multi-renderer and five declared C API gates; the C API final target is compile-blocked, and Windows D3D/GDI lanes remain manual. The remaining matrix is meaningful scoped evidence, not coverage of every one of the 50 identities, every valid combination, physical iOS hardware, or the full GPU pixel/oracle matrix.