Platform backends
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit c1c316b9; unless a sentence says otherwise, nothing here was built or executed. The selector matrix and refusal paths were read from the CMake files at this snapshot, not from a configure run. CTest entries and CI cells are listed as registered and configured; none was executed, and the offered set is not a claim that every offered combination builds or runs.
CNA has three implementations of its platform contract, selected at configure time with CNA_PLATFORM: SDL3 (the default and the one windowing implementation), HEADLESS and TERMINAL. Windows, X11, Wayland, macOS, iOS, Android and the browser are all reached through SDL3’s own video drivers, and a renderer that needs a native handle (an HWND, an X11 Display* and window, or a wl_display* and wl_surface*) receives it through IPlatformWindow::GetNativeHandle(). Two further names, SDL12 and EMSCRIPTEN, are reserved and refused. This page is the maintainer’s map of those backends: how a selection becomes a live IPlatform, how the refusal paths work, what is shared and what is backend-specific, and where each backend’s evidence lives. What each backend offers a game, its capability column and its CI status are in the user guides Platform Support and Windows, X11 and Wayland; the contract’s rules are on Platform architecture.
Current selector matrix
Every row below was read from PlatformSelection.cmake, modules/platform/CMakeLists.txt and PlatformFactory.cpp at this snapshot. “Compiled” means the backend’s translation units are in cna_platform; “factory name” is the string PlatformFactory::Create(name) and GetAvailable() use, which is not always the CMake spelling.
CNA_PLATFORM | Offered by CMake | Compiled | Factory name | Native link inputs | Primary maintainer concerns |
|---|---|---|---|---|---|
SDL3 (default) | Always; refused when CNA_ENABLE_SDL=OFF | When selected | SDL3 | SDL3::SDL3 (PRIVATE on desktop; PUBLIC, plus SDL3main, on Android and iOS; UIKit on iOS) | Subsystem refcounts in a host-shared library, window creation and adoption, event mapping, logical vs pixel size, GL/Vulkan/presenter services, the native handle of whichever video driver SDL chose |
HEADLESS | Always | Always, whatever the selection | Headless | None | Truthful refusals, one in-memory window, deterministic services for display-free runs and conformance |
TERMINAL | On every non-Windows target; renderer restricted to SOFTWARE, HEADLESS, STUB | Always on non-Windows targets | Terminal | None (termios, poll and signals are libc) | Terminal session leases and restoration, signal handling, byte-stream input decoding, ANSI presentation of CPU frames |
Because Headless is always compiled and Terminal is compiled on every POSIX target, “one selected platform” does not mean only one platform class exists in a binary: GetAvailable() lists SDL3 first when it is selected, then Headless, then Terminal off Windows. The selection decides which one PlatformFactory::Create() returns and therefore which one Game runs on; the others exist so the conformance suite has more than one implementation in the same process.
The offered set is a CMake fact, not a validation claim. Nothing in the selection files excludes HEADLESS or TERMINAL on macOS or Emscripten; whether such a combination builds or runs is not established by these pages, and the status CNA’s CI actually configures is in Status by operating system.
Reserved and refused identities
A value of CNA_PLATFORM that does not name an offered implementation never falls back to another backend. PlatformSelection.cmake has three refusal paths, all FATAL_ERROR at configure time; values are compared with STREQUAL and IN_LIST, so they are case-sensitive.
| Request | Where it is refused | What the message says |
|---|---|---|
SDL12, EMSCRIPTEN (every target); TERMINAL on a Windows target | The reserved list _cna_platforms_reserved; a host-conditional implementation is appended to it on the hosts it does not support | “is a reserved identifier that is NOT implemented”, the available set, and a pointer to CNA’s platform plan; for TERMINAL the reason (it is built on termios and has no Windows console path). It says falling back to SDL3 would build something other than what was asked for. |
WIN32, X11, WAYLAND | The list _cna_platforms_retired | The value names a window system CNA reaches through SDL3: the message says to configure -DCNA_PLATFORM=SDL3, that SDL picks its windows, x11 or wayland video driver at run time (SDL_VIDEODRIVER selects one explicitly), and that renderers still receive the native handles through IPlatformWindow::GetNativeHandle(). It is deliberately not an alias. |
| Any other name | The final IN_LIST check | “is not a known platform” with both the available and the reserved lists. |
The CMake GUI choice list (set_property(CACHE CNA_PLATFORM PROPERTY STRINGS …)) shows only the offered set. The reserved names reach no source file: there is no CNA_PLATFORM_SDL12 or CNA_PLATFORM_EMSCRIPTEN branch in PlatformFactory.cpp; a web build uses the default SDL3 backend. The script-mode test CnaPlatformSelection_Retired (registered in UnitTests.cmake, running RetiredPlatformSelection.cmake against the production selection file on a Windows and a non-Windows host model) keeps WIN32, X11 and WAYLAND refused by name and an unknown name refused as unknown.
Two refusals involving the platform axis live outside that file and run after it:
SdlAvailability.cmakerefuses platformSDL3whenCNA_ENABLE_SDL=OFF(as it refusesSDL3audio and theSDL_RENDERER,SDL_GPUandFNA3Drenderers), with a message that begins “CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL”.RendererSelection.cmakerefusesTERMINALwith any renderer other than the three CPU ones (SOFTWARE,HEADLESS,STUB), before any renderer dependency is probed. There is no equivalent gate forHEADLESS; a GPU renderer on Headless is refused at run time by the renderer factory (see Headless platform internals).
From the CMake option to a live IPlatform
CMakeLists.txt (top level, in this order)
RendererIdentities.cmake renderer name validated first
ApplePlatform.cmake Apple sysroot / deployment target
PlatformSelection.cmake CNA_PLATFORM: offered, reserved, retired, refused
| add_compile_definitions(CNA_PLATFORM_<NAME>)
AudioPlatformSelection.cmake
SdlAvailability.cmake CNA_ENABLE_SDL=OFF refusals
ThirdPartySDL.cmake vendored SDL3 only if CNA_ENABLE_SDL is on
|
modules/platform/CMakeLists.txt
src/*.cpp (contract types) + src/Sdl3/*.cpp (when SDL3 is selected) + src/Common/*.cpp
+ src/Headless/*.cpp (always) + src/Terminal/*.cpp (not WIN32)
|
PlatformFactory.cpp kDefaultName chosen by #if CNA_PLATFORM_<NAME>
|
Game::Game() -> PlatformFactory::Create() -> Game(std::unique_ptr<IPlatform>)
platform_ is the first member; InstallPlatform() -> SetCurrentPlatform()
platformCapabilities_ = GetCapabilities() read once, cached
The platform decision is settled before SDL is configured at all, because whether SDL is needed is a consequence of the platform, audio and renderer choices together. The compile definition CNA_PLATFORM_<NAME> is added project-wide (and exported to consumers through CNA::BuildConfig, so a game's own main() sees it too) and is what PlatformFactory.cpp and the entry-point header key off.
At run time, Game.cpp owns the platform: platform_ is initialised first, installed as the ambient platform from that initialiser (so a GraphicsDevice or ContentManager constructed later finds it instead of creating a second one), guarded by a PlatformInstallation scope object that undoes the installation if a later member throws, and uninstalled in ~Game. Outside a Game, CurrentPlatform.cpp creates a default platform lazily on first use of GetCurrentPlatform(). Its holders are deliberately immortal (allocated once, never destroyed at process exit) so that teardown code running from late static destructors still finds them; ResetCurrentPlatform() destroys the lazy default explicitly. One consequence is recorded in lsan_x11_mesa.supp: a process that draws through a standalone GraphicsDevice on the lazy default exits without eglTerminate, and the Mesa allocation that results is suppressed for LeakSanitizer. The startup and shutdown order around this is traced in Startup source trace and Ownership and shutdown.
Reading one backend
Every backend page in this section follows the same route. The table after the list shows where each step lands in each implementation. Backend sources live under modules/platform/ (one src/<Backend>/ directory each) and their tests under modules/platform/tests/.
- Factory registration and CMake target. Find the
#if defined(CNA_PLATFORM_<NAME>)arms inPlatformFactory::Create(name)andGetAvailable(), and the source glob and PRIVATE links inmodules/platform/CMakeLists.txt. - Construction without eager initialisation. The contract has no
Initialize/Shutdownpair; subsystems are acquired and released with refcounts, an unpaired release is a no-op, and a backend may do real work in its constructor as long as its capability set stays stable for the platform's lifetime. - Window creation, adoption and destruction, and the event pump. Owning windows come from
CreateWindow;AdoptWindow/AdoptWindowHandlereturn non-owning wrappers where a backend supports them (Headless and Terminal inherit the contract's refusing default). Then findPollEvents. - Native event to
PlatformEventto public input. Follow one event through the backend's mapper,Game::PollEvents,PlatformInputBridge::ProcessEventand, separately, the snapshot service'sUpdate()(see Debugging). - Timing.
Game::TickreadsGetPerformanceCounter/GetPerformanceFrequency, and in fixed-step mode sleeps withDelay(1)while it can, then spins onstd::this_thread::yield(); changing a backend's clock or sleep changes frame pacing for every game on it. - The narrow service a renderer receives. A renderer descriptor declares
needsWindow,needsVideoSubsystem,windowKind,needsGlContext,needsVulkanSurfaceandneedsSurfacePresenter;GraphicsDevicehands it only those services plus aNativeWindowHandlesnapshot (see Renderer selection internals). - Tests and CI host. Find the backend's own suites, their CTest registration and host prerequisites, and what the workflow files configure (see Where the evidence lives).
| Backend | Event pump | Logical vs pixel size | Graphics services | Adoption |
|---|---|---|---|---|
| SDL3 | SDL_PollEvent loop into a mapper, the same code over every SDL video driver | SDL pixel density; drawable from SDL_GetWindowSizeInPixels | Sdl3GlContext, Sdl3VulkanSurface, Sdl3SurfacePresenter | SDL windows by id or handle |
| Headless | Drains a queue filled by InjectEvent | Identical; scale 1.0 | None | Refused (contract default) |
| Terminal | SIGWINCH flag, then one byte-stream decoder | Nominal 8×16 pixels per cell; scale 1.0 | TerminalSurfacePresenter only, and only on a TTY | Refused (contract default) |
Shared vs backend-specific
Shared by every backend: the public contract in include/CNA/Platform (IPlatform, IPlatformWindow, the service interfaces, PlatformEvent, PlatformCapabilities, NativeWindowHandle, WindowDescription), the contract’s own value types in src/*.cpp, and the runtime and input consumers downstream of it. The rest of the source tree is organised by who may share what:
| Directory | Used by | Contents |
|---|---|---|
src/Common | Any backend that needs it | StandardFileSystem, StandardSystemInfo, and the SurfaceFrame validation the SDL3 and Terminal presenters share |
src/Sdl3, src/Headless, src/Terminal | Their own backend | Native handles, event-loop mechanics, window and surface creation, native input translation; in src/Sdl3 also the gamepad, joystick, haptics, sensor, camera, tray, dialog and clipboard services every windowed build gets from SDL |
SDL is confined to the platform module, where src/Sdl3 is the only backend that includes it, and to a short allowlist (the SDL3 audio implementation and its mixer, and the sdl-renderer, sdl-gpu and fna3d renderer families); it is linked PRIVATE on desktop targets, and must not migrate into the general contract or into input: ContractIsSdlFreeTests (ContractIsSdlFreeTests.cpp) includes every contract header and fails to compile if an SDL header comes along, and the source gates run by the platform CI cell (listed on Platform architecture) count and classify every remaining SDL reference. The input side is downstream of translation: PlatformInputBridge::ProcessEvent is implemented in SdlInputBridge.cpp, a historical filename; the code there consumes only neutral PlatformEvent values.
CNA's own contract document, docs/platform-abstraction.md, carries the twelve-step procedure for adding an implementation (directory, selection, CMake, factory, capabilities that start false, event mapping at the edge, explicit ownership, conformance, native tests, a default-selected build, the source gates, one CI cell). Its implementation table lists the same three backends and says a new implementation is for a host SDL3 cannot reach, not a second route to a window system SDL3 already drives; the selection files above remain authoritative. The maintainer-facing recipe is I need to modify a platform backend.
Debugging a backend
Log capability acquisition, window state transitions and the translated CNA events, not only raw native messages. After a resize, call Sync() before reading bounds back (the contract allows SetSize to be asynchronous) and compare GetClientBounds() (logical units) with GetPixelSize() (drawable pixels) and GetDisplayScale(); a swapchain sized from the wrong one is the classic resize bug.
For an input bug, prove each link separately. There are two paths out of the same batch, and they fail independently:
SDL event / injected event / terminal byte
-> backend mapper -> PlatformEvent (value) in the caller's batch
Game::PollEvents, for each event, in order:
1. PlatformInputBridge::ProcessEvent InputManager accumulators, TextInputEXT, touch, device events
2. Game handling Quit -> Exit(); CloseRequested -> Exit();
FocusLost/FocusGained -> IsActive;
Resized / PixelSizeChanged / DisplayScaleChanged
-> GameWindow update, UpdateViewportFromWindow,
renderer OnSurfaceInvalidated
after the whole batch:
3. GetKeyboard()->Update(), GetMouse()->Update()
4. GetGamepad()/GetJoystick()->Update() only once the Gamepad subsystem is initialised
Keyboard::GetState / Mouse::GetState read the snapshots from step 3,
and return empty states when the platform has no such service
A key that fires a text or bridge callback but never shows in Keyboard::GetState is a snapshot-service problem; a key that shows in GetState but not in a callback is a mapper or bridge problem. Exit() does not stop the batch: later events still reach input. Game does not filter window events by id and re-queries the window rather than trusting a resize payload. Those last behaviours are pinned for every implementation by EveryImplementation/GameEventSemanticsGoldenTest.* against the checked-in transcript platform-event-semantics.txt. The detailed trace is in Input internals and One frame source trace.
For a window that opens but shows nothing, separate the platform's answer (capability flag, service pointer, native handle tag) from the renderer's own context, surface or presenter acquisition. A window-free renderer (SOFTWARE, HEADLESS, STUB) opening no window on a windowing platform is by design, not a platform bug.
Where the evidence lives
Backend-specific test sources in modules/platform/tests/CNA/Platform are compiled into CnaTests only where their backend can be: UnitTests.cmake drops the Sdl3* suites when SDL3 is not the selected platform and the Terminal* suites on Windows. The implementation-neutral suites always build and are parameterised over GetAvailable().
| Backend | CTest entries (cmake/UnitTests.cmake unless noted) | Automatic CI (as configured in platform-ci.yml) |
|---|---|---|
| SDL3 | CnaPlatformTests, CnaPlatformWindowTests (SDL dummy video driver), CnaPlatformXErrorHandlerTests (real X display), CnaPlatformSdl3X11Tests (private Xvfb) and CnaPlatformSdl3WaylandTests (private headless Weston), which force SDL_VIDEODRIVER and skip (77) where their server is not installed | Matrix cells with OPENGLES3, VULKAN and SOFTWARE, including SDL3 on X11 and on Wayland; a native MSVC Windows job on manual dispatch only |
| Headless | CnaPlatformTests, CnaPlatformWindowTests (conformance instances) | The display-free matrix cell, which also runs the platform source gates; an SDL-free job (CNA_ENABLE_SDL=OFF, Headless, ALSA audio) |
| Terminal | CnaPlatformTests (the Terminal* suites its filter names), TerminalSoftwareDemoIntegration (registered in modules/graphics/examples) | Matrix cell: TERMINAL, SOFTWARE, NULL audio, with the pseudo-TTY demo |
Tests self-skip with exit code 77 when their environment is missing, and CTest reports those as skipped, so a green run on a host without the prerequisite proves nothing about that backend. The CI column describes what platform-ci.yml is configured to do at this snapshot; run results were not inspected. None of the tests on these pages was executed for them. The generated index of every registered entry is Test target index, and the change-driven view is What to test after changing X.
PlatformSelection.cmake: what is offered, reserved, retired and refused, and why.modules/platform/CMakeLists.txtandPlatformFactory.cpp: what is compiled and how each backend is named.IPlatform.hpp,IPlatformWindow.hppandPlatformCapabilities.hpp: the promises every backend makes.CurrentPlatform.cppandGame.cpp: ownership, installation and the per-frame consumption of events and snapshots.PlatformConformanceTests.cppanddocs/platform-abstraction.md: the rules a new or changed backend must keep.- Then one backend page: SDL3, Headless or Terminal.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- The cross-platform contract: axes, composition and evidence per route — How target OS, platform implementation, renderer set, audio implementation and the XNA surface compose in CNA, what IPlatform owns, and why each platform claim is an evidence vector.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-242: Source comments still say StorageDevice reaches the platform or the SDL preference path — IPlatformFileSystem::GetPreferencesPath is documented as backing StorageDevice and several comments cite storage as a reason for the ambient platform, but storage has no platform dependency and GetPreferencesPath has no
- CNA-VGAP-011: No test exercises PlatformSelection.cmake's reserved-identifier refusal paths — PlatformSelection.cmake fails configure for SDL12, EMSCRIPTEN and TERMINAL on Windows, but no test asserts those refusals; the one platform-selection test, CnaPlatformSelection_Retired, covers only the retired and unknow
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Platform Support: platform and audio implementations · Combination rules and hard exclusions · Capability matrix (32 flags) · Windows, X11 and Wayland
- Architecture
- Platform architecture · Audio and input architecture
- Internals
- SDL3 · Headless · Terminal · Input internals
- Maintainer workflow
- I need to modify a platform backend · I need to change build configuration
- Tests and validation
- Test architecture and change recipes · What to test after changing X
- Reference
- CMake option index · Test target index