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 009d40f5; 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 seven implementations of its platform contract, selected at configure time with CNA_PLATFORM: SDL3 (the default), SDL2, X11, WAYLAND, WIN32, HEADLESS and TERMINAL. Which of them CMake offers depends on the target and on the development packages it finds, and 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 32-flag capability column and its CI status are in the user guides Platform Support and Native Platforms; 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 (PRIVATE) | Primary maintainer concerns |
|---|---|---|---|---|---|
SDL3 (default) | Always; refused when CNA_ENABLE_SDL=OFF | When selected | SDL3 | SDL3::SDL3 (PUBLIC, plus SDL3main, on Android and iOS) | Subsystem refcounts in a host-shared library, window creation and adoption, event mapping, logical vs pixel size, GL/Vulkan/presenter services |
SDL2 | Always; refused when CNA_ENABLE_SDL=OFF; must not share a process with SDL3 | When selected | SDL2 | SDL2::SDL2, SDL 2.30.11 fetched at a pinned revision (override with CNA_SDL2_ROOT) | A deliberately narrower service set: events without matching snapshot services, OpenGL as the only graphics seam |
X11 | When libX11, libXext and X11/XKBlib.h are found and the target is not Windows, Emscripten, Android or iOS | When selected | X11 | libX11, libXext and whichever of Xi, Xrandr, Xcursor, Xfixes, Xau, Xss exist; GLX, Vulkan and libdbus resolved at run time | Connection lifetime and a frozen capability set, visual choice before window creation, an ordered protocol multiplexer, GLX/Vulkan/XPutImage seams |
WAYLAND | When wayland-client ≥ 1.18, xkbcommon ≥ 0.5, wayland-scanner and wayland-protocols with stable xdg-shell are found; not on Windows, Emscripten, Android or Apple | When selected, with protocol bindings generated into the build tree | Wayland | wayland-client, xkbcommon; EGL, wayland-egl, wayland-cursor, Vulkan and libdbus opened at run time | Registry and capability snapshot, asynchronous xdg configure, callback-driven events, EGL/Vulkan/wl_shm seams, proxy destruction order |
WIN32 | Only when the target is Windows (native or a mingw-w64 cross-build) | When selected | Win32 | user32 gdi32 opengl32 ole32 shell32 uuid; vulkan-1.dll loaded at run time | HWND ownership and adoption, message pump and window procedure, per-monitor DPI, WGL/Vulkan/GDI seams |
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, PORTABLEGL, 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 the selected SDL or native backend first (when one of those five 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. For example, nothing in the selection files excludes HEADLESS, TERMINAL or SDL2 on macOS or Emscripten, and the X11 detection does not exclude macOS, so an X11 development environment there would make X11 selectable. Whether any of those combinations builds or runs is not established by these pages; 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 four 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) | The reserved list _cna_platforms_reserved | “is a reserved identifier that is NOT implemented”, lists the available set, and points at CNA's platform plan, where SDL 1.2 and Emscripten are recorded as possible future implementations that are not in scope. It says falling back to SDL3 would build something other than what was asked for. |
WIN32 on a non-Windows target; TERMINAL on a Windows target | The same reserved list: a host-conditional implementation is appended to it on the hosts it does not support | The same message, plus the reason (TERMINAL is built on termios; WIN32 on user32/gdi32 and an HWND) and how to select WIN32 with a Windows toolchain, including -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake. Reusing the reserved path is deliberate, so the request reads as unavailable here rather than as a typo. |
X11 or WAYLAND where detection failed | Dedicated checks right after cna_detect_x11() / cna_detect_wayland() | “was requested but this machine cannot build it”, the detector's reason (naming the missing package, for example libx11-dev or libwayland-dev) and a pointer to CNA's backend document; never a fallback to SDL3, or to X11 through Xwayland. |
| 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, which is the only platform CNA's Emscripten workflows configure. No CMake test at this snapshot exercises the reserved-name path; the selection test that exists, CnaWaylandPlatformSelection (WaylandPlatformSelection.cmake), covers only Wayland's offered/refused/default behaviour through a fixture that reproduces the message.
Three refusals involving the platform axis live outside that file and run after it:
Sdl2OnlyConfiguration.cmakerefusesSDL2platform withSDL3audio and the reverse (both would put two SDL generations with identically named entry points in one process), and withSDL2platform plusSDL2audio refuses the renderers that link SDL3 directly (SDL_RENDERER,SDL_GPU,FNA3D,FREEDIRECT).CnaSdl2OnlyRendererGateruns that file as a script test.SdlAvailability.cmakerefuses platformSDL3orSDL2whenCNA_ENABLE_SDL=OFF, with a message that begins “CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL”.RendererSelection.cmakerefusesTERMINALwith any renderer other than the four CPU ones, 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, refused
| add_compile_definitions(CNA_PLATFORM_<NAME>)
ThirdPartySDL2.cmake only when CNA_PLATFORM=SDL2
AudioPlatformSelection.cmake, Sdl2OnlyConfiguration.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/<Selected>/*.cpp + src/Common/*.cpp
+ src/Headless/*.cpp (always) + src/Terminal/*.cpp (not WIN32)
+ src/Xkb, src/Freedesktop, src/Posix (+ src/Linux with linux/input.h) X11 / WAYLAND only
|
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 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 (X11 and Wayland connect there) 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 | SDL pixel density; drawable from SDL_GetWindowSizeInPixels | Sdl3GlContext, Sdl3VulkanSurface, Sdl3SurfacePresenter | SDL windows by id or handle |
| SDL2 | SDL_PollEvent loop | Ratio of drawable to logical width | Nested Sdl2Platform::GlContext only | SDL2 windows by id or handle |
| X11 | XPending/XNextEvent, then evdev, portal and tray pumps | One coordinate space: scale is always 1 | X11GlContext (GLX), X11VulkanSurface, X11SurfacePresenter | Registered windows or an XID on this display |
| Wayland | Protocol callbacks queue events; PollEvents pumps and moves them | Compositor-provided integer or fractional scale | WaylandGlContext (EGL), WaylandVulkanSurface, WaylandSurfacePresenter (wl_shm) | Only this platform's own windows |
| Win32 | PeekMessageW(nullptr) drain, dispatch to the window procedure | Client rectangle is the drawable; scale from the window's DPI | Win32GlContext (WGL), Win32VulkanSurface, Win32SurfacePresenter | By id or HWND |
| 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 and fitting helpers that the SDL3, X11, Wayland, Win32 and Terminal presenters share |
src/Xkb, src/Freedesktop, src/Posix | X11 and Wayland only | XKB key tables, libdbus loaded at run time with the desktop portal and screen-saver bus, the monotonic clock and the Vulkan loader lookup |
src/Linux | X11 and Wayland, when linux/input.h exists | Kernel evdev controllers and haptics, sysfs power and host facts; another Unix simply reports no gamepad |
src/Sdl3, src/Sdl2, src/X11, src/Wayland, src/Win32, src/Headless, src/Terminal | Their own backend | Native handles, event-loop mechanics, window and surface creation, native input translation |
No source is shared between src/Sdl3 and src/Sdl2; the SDL2 backend includes only the Common helpers. SDL is confined to the platform module and a short allowlist (the SDL audio implementations and the sdl-renderer, sdl-gpu, fna3d and freedirect renderer families), 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 own implementation table lists five backends and omits X11 and Wayland; the selection files above are authoritative. The maintainer-facing recipe is I need to modify a platform backend.
Debugging a native 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:
native message / callback / 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, PORTABLEGL, 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 when their backend is selected (UnitTests.cmake filters Sdl3*, Sdl2*, X11*, Wayland*, Win32* and the shared Xkb/Freedesktop/Posix/Linux suites by selection, and Terminal* only 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) | Matrix cells with OPENGLES3, VULKAN and SOFTWARE |
| SDL2 | CnaSdl2PlatformTests (its own executable), CnaSdl2OnlyRendererGate | One cell: SDL2 platform, SDL2 audio, OPENGLES3 |
| X11 | CnaX11MappingTests, CnaX11EvdevTests, CnaX11IntegrationTests, CnaX11WindowManagerTests, CnaX11InputMethodTests, CnaX11TouchscreenTests, CnaX11ExclusiveFullscreenTests | Two SDL-free jobs on Xvfb |
| Wayland | CnaWaylandMappingTests, …ProtocolTests, …PortalTests, …WestonTests, …WestonGpuTests, …WestonScaledTests, …MutterTests, …MutterCzechTests, …IbusTests, CnaWaylandLinkClosure; CnaWaylandPlatformSelection in every build | None: no workflow selects CNA_PLATFORM=WAYLAND |
| Win32 | Discovered CnaTests cases; the standalone harness in tools/platform/standalone_tests registers its own CnaPlatformTests | mingw-w64 cross-build run under Wine on every push; native MSVC job on manual dispatch only |
| Headless | CnaPlatformTests, CnaPlatformWindowTests (conformance instances) | The display-free matrix cell, which also runs the platform source gates |
| 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,PlatformX11.cmakeandPlatformWayland.cmake: what is offered, reserved 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, SDL2, X11, Wayland, Win32, 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-236: docs/platform-abstraction.md's Implementations table lists five backends and omits X11 and Wayland — CNA's platform-abstraction document lists SDL3, SDL2, WIN32, HEADLESS and TERMINAL in its Implementations table, but PlatformSelection.cmake offers seven implementations, adding the native X11 and WAYLAND backends.
- 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 and unknown-name refusal paths — PlatformSelection.cmake fails configure for SDL12, EMSCRIPTEN, WIN32 off Windows, TERMINAL on Windows and unknown names, but the only platform-selection configure test, CnaWaylandPlatformSelection, covers Wayland alone.
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) · Native Platforms (X11, Wayland, Win32)
- Architecture
- Platform architecture · Audio and input architecture
- 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