Tutorial 72: Choosing a Renderer

CNA Tutorials  ·  Architecture

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

ValueWhat it isPlatform
OPENGLES3OpenGL ES 3.0 via EasyGL. The Linux default.Not Emscripten
OPENGL33Desktop OpenGL 3.3 core profile via EasyGL.Not Emscripten
OPENGLES2OpenGL ES 2.0 via EasyGL. No MRT, no occlusion queries, no Texture3D, no instancing.Not Emscripten
WEBGL2WebGL 2 via EasyGL. The Emscripten default.Emscripten only
WEBGL1WebGL 1 via EasyGL. Same feature losses as OPENGLES2.Emscripten only
OPENGL4Real desktop OpenGL 4.x core profile. Independent of EasyGL.Desktop
OPENGL2Native OpenGL 2.1, written without EasyGL.Desktop
OPENGL1Native legacy OpenGL 1.x fixed-function.Linux and Windows only
OPENGLES1OpenGL 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)

ValueWhat it isPlatform
VULKANNative Vulkan. Executes custom ShaderEffect shaders as SPIR-V.Where Vulkan is available
SDL_GPUSDL's own GPU abstraction. Also SPIR-V for custom shaders.Where SDL_GPU has a device
WEBGPUNative WebGPU through wgpu-native, WGSL shaders.Desktop
METALNative 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.

ValueWhat it isNotes
BGFXThe bgfx library.Accepts a custom ShaderEffect and silently ignores it. See Tutorial 86.
SOKOLsokol_gfx. Executes custom shaders.CNA_SOKOL_API defaults to GLCORE; other values are wired but unverified and warn at configure time.
DILIGENTDiligent Engine.Needs an external Diligent checkout.
LLGLLLGL. Executes custom shaders.Experimental: Linux/X11-only and OpenGL-only — LLGL's Vulkan module cannot be selected.
IGLMeta's Intermediate Graphics Library.Linux/X11; chooses an OpenGL or Vulkan device inside the family.
MAGNUMmosra/magnum. Executes custom shaders.Not available on Emscripten.
WICKEDWicked Engine.Not available on Emscripten; needs CNA_WICKED_ROOT.
FNA3DFNA-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.

ValueWhat it targets
DIRECTX1Real DirectDraw v1. A declared stub — its entire 3D pipeline throws, and it is 2D-only in practice.
DIRECTX2Real DirectDraw v1 + Direct3D v2 DrawPrimitive.
DIRECTX3Real DirectDraw v2 + Direct3D v2 DrawPrimitive. Not the old site's DX3.
DIRECTX5Real DirectDraw v4 + Direct3D v3 FVF DrawPrimitive.
DIRECTX6Real DirectDraw v4 + Direct3D v3, with real stencil.
DIRECTX7Real DirectDraw v7 + Direct3D v7, flattened device model.
DIRECTX8Real Direct3D 8, fixed-function, delivered through DXVK.
DIRECTX9Direct3D 9. Compiles HLSL at runtime. The pixel-exactness renderer — see below.
DIRECTX10Real ID3D10Device with real HLSL shaders, delivered through DXVK's d3d10core.
DIRECTX11Native Direct3D 11. Compiles HLSL at runtime.
DIRECTX12Native Direct3D 12. Compiles HLSL at runtime.
DIRECT2DDirect2D 1.1. 2D-only.
GDIClassic Win32 GDI. 2D-only; throws on a custom ShaderEffect.
GLIDEGlide 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:

ValueWhat it is
CANVASHTML Canvas 2D. 2D-only.
HTML_DOMCSS-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_DOMSVG DOM nodes. 2D-only; throws on a custom ShaderEffect.
PIXIJSPixiJS-backed browser rendering through Emscripten integration. 2D-only.

Three web caveats apply to every renderer in this group. There is no save persistenceSDL_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.

ValueWhat it isPlatform
SDL_RENDERERSDL3's built-in accelerated 2D renderer. The default outside Linux and Emscripten.Wherever SDL3 runs
SKIASkia 2D raster.Desktop
BLEND2DBlend2D 2D vector raster.Desktop
OPENVGOpenVG through ShivaVG, which needs a real fixed-function GL context.Linux, Windows, macOS only
NANOVGNanoVG's GLSL 1.10 / GL2 vector pipeline. 2D-only.Linux, Windows, macOS only
FREEDIRECTDirectDraw-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)

ValueWhat it is
SOFTWAREA 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.
PORTABLEGLrswinkle/PortableGL, a CPU software OpenGL 3.x. Also read-back only, never presented.
TINYGLTinyGL software OpenGL implementation for CPU-side compatibility experiments.
HEADLESSNo GPU and no window. It records custom shader submissions rather than executing them. Built for fast CI of game logic.
STUBA 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:

  • BGFX and SOFTWARE accept the effect and silently ignore the source. No error, no shader, no clue — this is the one to watch for.
  • GDI, METAL, HTML_DOM and SVG_DOM throw. You find out immediately.
  • FNA3D cannot 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

SituationChoose
Not sure / just startingOPENGLES3 — CNA's day-to-day development renderer and the Linux default
Pure 2D game, maximum portabilitySDL_RENDERER
3D game on Linux or WindowsOPENGLES3, or VULKAN for explicit GPU control
Bit-identical XNA 4.0 outputDIRECTX9 (Windows-only) — the only renderer that matches 39/39 oracle scenes
macOSMETAL — the only macOS renderer, and the only one with automatic macOS CI
Browser, 3D or 2DWEBGL2 (the Emscripten default). Read the three web caveats above first.
Browser, no WebGL context availableCANVAS, HTML_DOM or SVG_DOM — all 2D-only
CI without a GPU, game logic onlyHEADLESS
Deterministic, GPU-free pixel comparisonSOFTWARE (read back with GetBackBufferData())
Targeting genuinely old Windows hardwarePick the matching rung of the DirectX ladder — but verify on real hardware yourself
Anything that needs a custom shaderNot 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.