Runtime Renderer Selection
Single and multi-renderer modes
Single-renderer mode remains CNA's default and recommended smallest build. Set CNA_GRAPHICS_RENDERER (or, equivalently, turn exactly one CNA_RENDERER_<NAME> option ON) and only that implementation family is linked. Omit both to get the per-host default: WEBGL2 under Emscripten, OPENGLES3 on Linux, SDL_RENDERER everywhere else. The name must be one of the 18 public identities, spelled exactly — CMake matches the names case-sensitively, and a name outside the 25 stops the configure with a FATAL_ERROR that lists the supported names.
cmake -S . -B build -G Ninja \
-DCNA_GRAPHICS_RENDERER=VULKAN
To link several compatible identities, add the plural list. The singular identity is still the compile-time default and must be a member; duplicates are removed and the default is moved to the front of the registry. The configure log confirms the result with CNA: renderer set -- <list> (default: <X>).
cmake -S . -B build-multi -G Ninja \
-DCNA_GRAPHICS_RENDERER=HEADLESS \
-DCNA_GRAPHICS_RENDERERS="HEADLESS;SOFTWARE;STUB"
An empty CNA_GRAPHICS_RENDERERS value means single-renderer mode. A default outside the plural list is a configure error; CNA does not silently rewrite the request.
Selection API
Selection precedence is an explicit API preference, then the CNA_GRAPHICS_RENDERER environment variable, then the compile-time default. In an Emscripten build the Module.cnaPreferredRenderer page property sits at the environment variable's level (see below). The environment variable affects runtime selection; it does not add a family that was not linked.
#include "CNA/GraphicsRendererSelection.hpp"
using CNA::GraphicsRendererSelection;
using CNA::GraphicsRendererType;
for (auto available : GraphicsRendererSelection::GetAvailable()) {
// populate a settings UI before Game/GraphicsDevice exists
}
GraphicsRendererSelection::SetPreferred(GraphicsRendererType::Vulkan); // or SetPreferred("VULKAN")
MyGame game;
game.Run();
GetAvailable(), IsAvailable(), GetSelected() and IsLatched() are safe before a device exists. The failures are specific:
SetPreferred(GraphicsRendererType)for a renderer that is not compiled in throwsSystem::InvalidOperationException("the X renderer is not compiled into this build. Available: … Rebuild with -DCNA_GRAPHICS_RENDERER=X, or configure a fallback chain") — unless a fallback chain or automatic fallback is enabled, in which case the rejection is recorded instead.SetPreferred(std::string_view)accepts exactly the 25 canonical spellings, case-insensitively; anything else, including a name that older documentation used, throwsSystem::ArgumentException.- A
CNA_GRAPHICS_RENDERERenvironment variable that names no renderer throwsSystem::InvalidOperationException(CNA_GRAPHICS_RENDERER="<x>" does not name any CNA graphics renderer.). The variable is read once, on the first query, and in our reading of the code that first query is made beforemain(): the generated renderer registry publishes the compiled-in set from a namespace-scope initialiser, which computes the selection. A bad value (or one naming a renderer that is not compiled in), like an invalidModule.cnaPreferredRendererpage property, therefore cannot be caught by game code and ends the process throughstd::terminate; the build default does not apply afterwards. This is derived from reading the source at this snapshot, not from running it.
Choosing a renderer from a web page
A browser has no environment variables, so an Emscripten build reads the choice from the page's Module object. The property is consulted at the same point, and with the same precedence, as the environment variable — below an explicit SetPreferred() call in the program, above the compile-time default; a real environment variable, if one exists, wins over the property.
<script>
var Module = { cnaPreferredRenderer: "CANVAS" }; // any renderer linked into this bundle
</script>
The value must be plain ASCII and a public identity spelled in the CNA_GRAPHICS_RENDERER form (case-insensitive); the property is read once. A page that drives the module directly can instead call the exported Module._cna_set_preferred_renderer, which behaves like SetPreferred(), returns 1 on success and 0 for an unknown name, a renderer that is not compiled into the bundle (with no fallback chain), or a call that arrives after the latch. The bundle must have been built with the renderer in CNA_GRAPHICS_RENDERERS; the five browser-only identities are WEBGL1, WEBGL2 and CANVAS, and WEBGPU can be built for the browser through the emdawnwebgpu port. CNA's own Emscripten CI builds one bundle of WEBGL2;WEBGL1;CANVAS and asserts that all four renderers and this JavaScript surface are present; it does not run the other three.
Failure and fallback
Hard failure is the default. If the selected renderer is unavailable or initialization throws, CNA reports the failure. This prevents a game that requested one capability set from quietly running on another.
#include <array>
constexpr std::array fallbackChain{
GraphicsRendererType::OpenGLES3,
GraphicsRendererType::Software,
GraphicsRendererType::Headless
};
GraphicsRendererSelection::SetFallbackChain(fallbackChain); // takes a std::span
// Alternatively opt into CNA's automatic fallback ordering:
GraphicsRendererSelection::EnableAutomaticFallback(true);
Fallback history records availability-probe refusals, initialization failures and skipped candidates, each with a reason (NotCompiledIn, ProbeUnavailable, InitializationFailed, WindowKindConflict), and every skip is logged at warning level. Chain entries that are not compiled in are skipped and recorded, so one chain can serve several build configurations. If every candidate fails, CNA throws "CNA: no graphics renderer could be created." with the first failure as the primary cause. Every family's availability probe is unconditional, so "available" means "compiled in"; ProbeUnavailable can only come from the debug variable or a device-flag mismatch below, and real failures arrive as InitializationFailed.
EnableAutomaticFallback(true) orders the compiled-in renderers from CNA's declared maturity and category: more mature first, renderers in the Software category ten places later, STUB always last. From that ranking code, HEADLESS (a Supported diagnostic renderer) sorts ahead of SOFTWARE; no test we found exercises this, so if you want a CPU rasterizer as the last resort, spell the chain out with SetFallbackChain().
OpenGL and Vulkan require incompatible window kinds (as do the windowless CPU renderers and METAL). CNA may destroy and recreate a window it owns while falling back across that boundary. A caller-supplied DeviceWindowHandle cannot be recreated, so that candidate is skipped with WindowKindConflict.
The policy latches on the first successful renderer creation, not when the first GraphicsDevice begins construction (a header comment still says the latter; the code does the former). After that, SetPreferred, SetFallbackChain and EnableAutomaticFallback throw InvalidOperationException ("must be selected before the first GraphicsDevice is constructed"), and GetActive() throws until a renderer exists. A failed resolution does not latch, so a game may catch the error and retry with another configuration. GetSelected() never latches, and recreating the same renderer on a live device (Reset, an MSAA change) or creating a second GraphicsDevice keeps the selection.
Default, selected and active are different answers
| Question | API |
|---|---|
| What is the build default? | CNA::getCurrentGraphicsRendererType() (compile time) |
| What will CNA attempt? | GraphicsRendererSelection::GetSelected() |
| What actually started? | GraphicsRendererSelection::GetActive() |
| What does this device use? | GraphicsDevice::GetGraphicsRendererType() / GetGraphicsRendererName() |
| What was skipped, and why? | GraphicsRendererSelection::GetFallbackHistory() |
Only the default renderer's CNA_RENDERER_<IDENTITY> macro is defined project-wide (each family's own macro is private to its target). Multi-renderer builds define CNA_MULTI_RENDERER; renderer-aware test targets also receive CNA_RENDERER_PRESENT_<IDENTITY> for each linked identity, and a generated CnaRendererRegistry.generated.cpp lists the linked families, default first.
Device flags and debug variables
Two XNA device flags now pick a renderer for you: GraphicsAdapter.UseNullDevice (C++: setUseNullDeviceProperty) requires the HEADLESS renderer, and UseReferenceDevice (setUseReferenceDeviceProperty) requires SOFTWARE; the null device wins if both are set. Device creation is refused with NoSuitableGraphicsDeviceException when the required renderer is not compiled in or the selection has already latched to another renderer, and a fallback chain may not satisfy the flag with a different one.
| Environment variable | Effect |
|---|---|
CNA_GRAPHICS_RENDERER | Preferred renderer, below SetPreferred(), above the build default |
CNA_DEBUG_UNAVAILABLE_RENDERERS | Comma-separated, case-insensitive list; each named renderer is treated as unavailable (records ProbeUnavailable) — for testing fallback |
CNA_DEBUG_FAIL_RENDERER_INIT | Comma-separated list; each named renderer fails initialization (records InitializationFailed) |
CNA_FORCE_HEADLESS_DEVICE_EXT | Comma-separated list; creates the named renderers' device as PresentationParameters.HeadlessEXT (no window, no video subsystem) |
CNA_HEADLESS_MODE | Behaviour of the HEADLESS renderer: Fast, Validation (default) or Trace |
The C ABI surface
The experimental C API (ABI 0.44.0) exposes the same selection surface: cna_graphics_renderer_set_preferred_ext and ..._by_name_ext, get_selected, get_active, get_is_latched, the available count/copy/query functions, set_fallback_chain, set/get_automatic_fallback, the fallback history, message and reason accessors, try_parse_name_ext and cna_graphics_renderer_get_current_type/name. Renderer identities are the stable, sparse numbers listed on the renderer table (CNA_GRAPHICS_RENDERER_MAXIMUM is 43); a numeric identity outside the public table returns CNA_RESULT_INVALID_ARGUMENT, and a name outside the 18 is "not a CNA graphics renderer". See Experimental C API.
Combination rules
Multi-renderer builds add code size and every selected family's dependencies. One conflict is rejected at configure time, with a reason:
- Windows-only, Emscripten-only and macOS-only identities cannot cross target-toolchain partitions.
Several EasyGL identities can coexist because the GL profile is a runtime value, subject to the native-versus-Emscripten partition. Configure-time coverage in CNA's own CI is HEADLESS;SOFTWARE;STUB, OPENGL33;VULKAN;SOFTWARE;HEADLESS and, for Emscripten, WEBGL2;WEBGL1;CANVAS.
Cost and dependencies
Every selected family contributes code, build time and dependencies. Adding a second identity served by an already-linked EasyGL family is cheaper than adding an unrelated renderer, but it is not free of platform constraints; the three SDL3-direct renderers (SDL_RENDERER, SDL_GPU, FNA3D) also need the SDL3 platform. Ship only the set your application can meaningfully select and test. The dependency table lists what each family needs.
Continue with Tutorial 126: Build and Select Several Renderers or return to the full renderer inventory.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Native C API contract: admission, buffers, retention and route families — What CNA's C ABI 0.44.0 version checks admit, the exact count-then-copy protocol, callback, registration and thread rules, resources that retain others, caller-created devices and all 60 headers by family.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-036: run-all-renderer-smoke-tests.sh ignores the build's exit status, and its '-- -k' build flag aborts every Ninja build, so smoke verdicts can come from stale binaries — The orchestrator runs cmake --build <dir> -j4 -- -k and discards the result; -k without a count is a make option that Ninja rejects, so in a Ninja tree nothing is rebuilt and ctest runs whatever executables the tree alre
- CNA-BUG-095: A GraphicsDevice constructor that fails after renderer resolution leaves the selection latched to the renderer it destroyed — The GraphicsDevice constructor latches the selection inside resolveRenderer(), then updates the viewport and pushes default states; if one of those throws, the catch block destroys the renderer without unlatching, so Set
- CNA-BUG-096: VulkanRenderer's constructor leaks the Vulkan handles it created when a later construction step throws — VulkanRenderer::VulkanRenderer creates the instance, debug messenger, device, swapchain and other raw handles in sequence with no cleanup path, so a throw from a later step such as PickPhysicalDevice leaves them undestro
- CNA-BUG-184: Core's build files still carry dependencies its sources dropped: -lembind on Emscripten and a link probe that tolerates SDL3 — modules/core/CMakeLists.txt still adds -lembind for every Emscripten consumer although the browser preference reader was rewritten with EM_JS to avoid embind, and probe_core still permits SDL3 although core no longer use
- CNA-BUG-190: modules/renderers/CMakeLists.txt enters common/d3d and the Metal renderer target only when the default identity matches, so a multi-renderer set that lists DIRECTX11, DIRECTX12 or METAL without the matching default passes the combination rules and then cannot build — Two helper directories are entered by testing the default identity instead of set membership, yet the combination rules accept sets such as HEADLESS;DIRECTX11 (or DIRECTX9;DIRECTX11), whose DIRECTX11 family then links an
- CNA-BUG-202: In a multi-renderer build, non-default renderers' example CTests are registered but run under the default renderer — The SDL_GPU, SOFTWARE, STUB and VULKAN example blocks are entered for a non-default member of CNA_GRAPHICS_RENDERERS, yet their registrations select no renderer, so each executable runs under the build default.
- CNA-BUG-215: GraphicsRendererSelection::IsLatched() is documented to latch when the first GraphicsDevice begins construction; it latches only on successful resolution — The header comment on IsLatched() says the selection latches when the first GraphicsDevice begins construction, but GraphicsDevice::resolveRenderer latches only after a renderer has been created, so a resolution that fai