Chapter 20 Renderer Selection and Identity
CNA exposes 50 public renderer identities mapped to 46 implementation families. The normal build still contains one family, but v0.1.0-alpha.1 also has an opt-in multi-renderer mode: configuration chooses the compiled set and its default, and the process chooses one available identity before constructing its first GraphicsDevice. Keeping compiled set, default, selected and active renderer separate prevents the most consequential portability and lifetime mistakes around CNA’s registry.
Authority order. The current identities are jointly pinned by modules/core/include/CNA/GraphicsRendererType.hpp and cmake/RendererSelection.cmake. The family mapping is stated by docs/renderer-registry.md and the physical module list. Older counts in plans, generated scripts, and migration documents are historical snapshots. This edition always prints 50 identities / 46 implementation families, with both nouns present.
20.1 Identity, family, and native API are different axes
A public identity is the spelling supplied to CNA_GRAPHICS_RENDERER, such as VULKAN, WEBGPU, or OPENGLES3. It becomes a GraphicsRendererType enumerator and a CNA_RENDERER_* compile definition.
An implementation family is the source module that provides a generated-registry factory. Five public identities share the easygl family: OPENGLES2, OPENGLES3, OPENGL33, WEBGL1, and WEBGL2. That one five-to-one mapping accounts for the arithmetic: families.
A native API is what the implementation ultimately calls. Usually it follows the family name, but not always. BGFX is itself an abstraction layer that can submit through OpenGL, GLES, Vulkan, Metal, or Direct3D. FNA3D similarly selects a driver below CNA. HEADLESS and STUB call no GPU at all. SOFTWARE and PORTABLEGL rasterize on the CPU. Asking “which API is selected?” is therefore ambiguous until the axis is named.
20.2 The complete registry, grouped by implementation
| Family group | Public identities |
|---|---|
| EasyGL, five profiles | OPENGLES2, OPENGLES3, OPENGL33, WEBGL1, WEBGL2 |
| Independent OpenGL-shaped | OPENGL1, OPENGL2, OPENGL4, OPENGLES1, PORTABLEGL |
| Native modern | VULKAN, SDL_GPU, WEBGPU, METAL, IGL |
| Cross-API abstractions | BGFX, MAGNUM, LLGL, DILIGENT, SOKOL, WICKED, FNA3D |
| Direct3D and Direct2D | DIRECTX9, DIRECTX10, DIRECTX11, DIRECTX12, DIRECT2D |
| DirectX / retro ladder | DIRECTX1, DIRECTX2, DIRECTX3, DIRECTX5, DIRECTX6, DIRECTX7, DIRECTX8, FREEDIRECT, GLIDE |
| 2D raster and vector | SDL_RENDERER, GDI, SKIA, BLEND2D, OPENVG, NANOVG |
| Browser DOM | CANVAS, HTML_DOM, SVG_DOM, PIXIJS |
| Diagnostic / CPU | HEADLESS, SOFTWARE, STUB, TINYGL |
These groups are explanatory, not another registry. Appendix B remains the quick reference; the chapters that follow group families by the API they speak and how their shaders arrive.
20.3 One renderer remains the default build mode
The normal interface is one cache variable:
CMake validates the spelling, applies platform and dependency gates, sets the matching CNA_RENDERER_* default definition, and builds the selected family. In this mode GraphicsRendererSelection::GetAvailable() has one entry; requesting a different one fails rather than inventing a renderer that is not linked.
The default is platform-sensitive:
-
•
Emscripten selects WEBGL2;
-
•
Linux selects OPENGLES3;
-
•
other hosts select SDL_RENDERER.
Treat those defaults as convenience, not as a portability promise. A game depending on MRT, hardware instancing, or a particular shader source language should select and test an explicit identity.
20.4 Opt-in multi-renderer builds
CNA_GRAPHICS_RENDERERS is a semicolon-separated list of identities to compile into one binary. CNA_GRAPHICS_RENDERER remains singular: it names the default and must be a member of the list. For example:
CMake removes duplicate identities, puts the default first, emits CNA_MULTI_RENDERER, builds the required family targets, and generates the descriptor and factory registry. A list is not automatically valid. PORTABLEGL cannot coexist with a family that imports the real global gl* symbols; GDI and SOFTWARE would compile the same translation units with different bodies; GLIDE pins the binary to its 32-bit ABI; and a single toolchain cannot combine Windows-only, Emscripten-only, and macOS-only partitions. The validation fails during configuration with the conflicting pair and reason.
All five EasyGL identities may now coexist when their platform partition permits it: the GL profile became runtime family state. They remain five public identities backed by one source family, so multi-renderer support does not change the 50/46 taxonomy.
20.5 Runtime choice, precedence, and latch
CNA::GraphicsRendererSelection is a process-wide CNAEXT policy surface. Before the first graphics device, a program may call SetPreferred(type) or SetPreferred(name). The exact precedence is:
-
1.
an explicit SetPreferred() call;
-
2.
the CNA_GRAPHICS_RENDERER environment variable (or the equivalent Emscripten module property when no environment value exists);
-
3.
the configured build default.
GetSelected() reports the identity CNA will attempt without freezing it; GetAvailable() and IsAvailable() report the compiled set. Construction of the first GraphicsDevice latches selection. GetActive() becomes meaningful only after a renderer was created and may differ from GetSelected() only when explicit fallback succeeded. Changing preference after the latch throws. Reset and multisample recreation may rebuild the same renderer; no supported operation migrates live textures, buffers, effects, render targets, query objects, or presentation state to a different family.
Unavailable or failed initialization is an error by default. A caller may opt in to an ordered fallback chain or automatic fallback; CNA records skipped/not-compiled and failed attempts in GetFallbackHistory(). This makes substitution observable. Automatic ordering derives from the registry’s maturity and category metadata, with CPU renderers after mature GPU routes and STUB last; it is a recovery policy, not a claim of equivalent output.
20.5.1 Legacy Boolean options are a compatibility surface
The CNA_RENDERER_* Booleans still exist for scripts that predate the single selector. If any are enabled, exactly one must be true. Mixing them does not create a multi-renderer build; the configure step terminates instead. New build instructions should prefer CNA_GRAPHICS_RENDERER, because one string is easier to cache, display, and validate than fifty mutually exclusive switches. Use CNA_GRAPHICS_RENDERERS for an actual set.
20.6 Names that no longer select anything
Several spellings remain common in old documentation but are not aliases:
-
•
EASYGL is a family name, not a public selector. Choose one of its five profiles.
-
•
ASCII was removed as a renderer. Its reusable behavior is now the AsciiPostProcessEffect class in the CNA::Graphics namespace and modules/graphics-ext module.
-
•
DX3 does not select anything. DIRECTX3 is the real DirectX 3 family; FREEDIRECT is the separate renderer built over the sibling free-direct library.
-
•
D3D9, D3D11, and D3D12 are not public selector aliases; use DIRECTX9, DIRECTX11, and DIRECTX12.
-
•
The former GLES identity was made explicit as OPENGLES3; GLES 2 is the separate OPENGLES2 profile.
Unknown names fail at configure time. That strictness is useful: silently redirecting EASYGL to whichever profile happens to be convenient would erase the capability differences Chapter 22 must preserve.
20.7 Platform gates are part of identity
An entry in the registry means CNA has an implementation family, not that the current host can build it. The most important gates are:
-
•
WEBGL1, WEBGL2, CANVAS, HTML_DOM, SVG_DOM, and PIXIJS are Emscripten identities; native builds reject them.
-
•
The native EasyGL profiles are rejected under Emscripten.
-
•
DirectX, Direct2D, GDI, and Glide are Windows-shaped families. Some can be exercised under Wine or Proton; that is a verification route, not a change in their API ownership.
-
•
METAL requires Apple platforms.
-
•
FREEDIRECT requires the sibling ../free-direct checkout; EasyGL profiles require ../easy-gl. Other families have their own system, vendored, pinned-artifact, or FetchContent dependencies.
The configure error is usually the most faithful evidence available on an unsupported host. Forcing past it would not prove the renderer portable; it would only bypass the source tree’s declared boundary.
20.8 How the selected identity reaches C++
GraphicsRendererType.hpp owns the public enum and getCurrentGraphicsRendererName(). For EasyGL it decodes the profile compile definition back into the correct public identity, rather than returning a generic family name. GraphicsDevice::GetGraphicsRendererType() exposes the result to game code.
The per-family factory boundary is deliberately smaller:
The generated registry associates each compiled descriptor with the appropriate function. GraphicsRendererCreateArgs carries platform window/native handles, virtual dimensions, presentation mode, swap interval, MSAA request, recovery policy, graphics profile, and CNAEXT presentation fields. A field’s presence does not mean every family honors it: Chapter 19 shows why the contract includes truthful no-op, refusal, and renderer-specific interpretations.
20.9 Family selection is not BGFX driver selection
Selecting BGFX chooses CNA’s BGFX family. The environment variable CNA_BGFX_RENDERER can then ask bgfx to use OpenGL, GLES, Vulkan, Metal, D3D11, D3D12, or its own no-op driver. That second choice happens below CNA’s identity layer: GetGraphicsRendererType() still reports BGFX, and all CNA capability and lifetime behavior still belongs to BgfxRenderer.
The same reasoning applies when FNA3D selects a native driver. A lower layer changing from OpenGL to D3D11 does not turn the public identity into OPENGL4 or DIRECTX11; it changes how one implementation family fulfills its contract.
20.10 Mechanical checks and the remaining CI trap
Registry validators compare the enum, CMake selector list, descriptors, factories, categories, maturity values and generated metadata. At this pin those sources settle fifty identities and forty-six physical families. The release changelog’s claim of 49 identities and the runtime selection document’s phrase “45 factories” are both one low; neither overrides the registries.
One live build-system defect demonstrates why registry checks and workflow checks are different. Two CI workflow areas still pass EASYGL, even though the selector rejects it. The identity checker can remain green while those jobs fail before compilation, because it proves the two registries agree; it does not parse every workflow command line. The defect is recorded for upstream repair rather than patched from this book.
20.11 A selection checklist
Before choosing a renderer for a port, answer in this order:
-
1.
Is the public identity buildable on every target platform?
-
2.
Does its implementation behavior provide the required 2D/3D, target, effect, and query paths? Do not stop at a default-true capability flag.
-
3.
What verification tier exists on each platform: real-XNA oracle, pixel tests, smoke test, compile-only gate, or no execution?
-
4.
Does a lower abstraction layer add a second runtime driver choice?
-
5.
Are the required sibling checkouts or fetched dependencies acceptable to the product?
The registry answers what can be selected. The rest of Part IV answers the harder question: what that selection is actually proven to do.