Platform backends

CNA snapshot 009d40f5  ·  Development › Platform internals  ·  source links pinned to 009d40f5

✓

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_PLATFORMOffered by CMakeCompiledFactory nameNative link inputs (PRIVATE)Primary maintainer concerns
SDL3 (default)Always; refused when CNA_ENABLE_SDL=OFFWhen selectedSDL3SDL3::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
SDL2Always; refused when CNA_ENABLE_SDL=OFF; must not share a process with SDL3When selectedSDL2SDL2::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
X11When libX11, libXext and X11/XKBlib.h are found and the target is not Windows, Emscripten, Android or iOSWhen selectedX11libX11, libXext and whichever of Xi, Xrandr, Xcursor, Xfixes, Xau, Xss exist; GLX, Vulkan and libdbus resolved at run timeConnection lifetime and a frozen capability set, visual choice before window creation, an ordered protocol multiplexer, GLX/Vulkan/XPutImage seams
WAYLANDWhen wayland-client ≥ 1.18, xkbcommon ≥ 0.5, wayland-scanner and wayland-protocols with stable xdg-shell are found; not on Windows, Emscripten, Android or AppleWhen selected, with protocol bindings generated into the build treeWaylandwayland-client, xkbcommon; EGL, wayland-egl, wayland-cursor, Vulkan and libdbus opened at run timeRegistry and capability snapshot, asynchronous xdg configure, callback-driven events, EGL/Vulkan/wl_shm seams, proxy destruction order
WIN32Only when the target is Windows (native or a mingw-w64 cross-build)When selectedWin32user32 gdi32 opengl32 ole32 shell32 uuid; vulkan-1.dll loaded at run timeHWND ownership and adoption, message pump and window procedure, per-monitor DPI, WGL/Vulkan/GDI seams
HEADLESSAlwaysAlways, whatever the selectionHeadlessNoneTruthful refusals, one in-memory window, deterministic services for display-free runs and conformance
TERMINALOn every non-Windows target; renderer restricted to SOFTWARE, PORTABLEGL, HEADLESS, STUBAlways on non-Windows targetsTerminalNone (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.

RequestWhere it is refusedWhat 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 targetThe same reserved list: a host-conditional implementation is appended to it on the hosts it does not supportThe 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 failedDedicated 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 nameThe 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.cmake refuses SDL2 platform with SDL3 audio and the reverse (both would put two SDL generations with identically named entry points in one process), and with SDL2 platform plus SDL2 audio refuses the renderers that link SDL3 directly (SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT). CnaSdl2OnlyRendererGate runs that file as a script test.
  • SdlAvailability.cmake refuses platform SDL3 or SDL2 when CNA_ENABLE_SDL=OFF, with a message that begins “CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL”.
  • RendererSelection.cmake refuses TERMINAL with any renderer other than the four CPU ones, before any renderer dependency is probed. There is no equivalent gate for HEADLESS; 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/.

  1. Factory registration and CMake target. Find the #if defined(CNA_PLATFORM_<NAME>) arms in PlatformFactory::Create(name) and GetAvailable(), and the source glob and PRIVATE links in modules/platform/CMakeLists.txt.
  2. Construction without eager initialisation. The contract has no Initialize/Shutdown pair; 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.
  3. Window creation, adoption and destruction, and the event pump. Owning windows come from CreateWindow; AdoptWindow/AdoptWindowHandle return non-owning wrappers where a backend supports them (Headless and Terminal inherit the contract's refusing default). Then find PollEvents.
  4. Native event to PlatformEvent to public input. Follow one event through the backend's mapper, Game::PollEvents, PlatformInputBridge::ProcessEvent and, separately, the snapshot service's Update() (see Debugging).
  5. Timing. Game::Tick reads GetPerformanceCounter/GetPerformanceFrequency, and in fixed-step mode sleeps with Delay(1) while it can, then spins on std::this_thread::yield(); changing a backend's clock or sleep changes frame pacing for every game on it.
  6. The narrow service a renderer receives. A renderer descriptor declares needsWindow, needsVideoSubsystem, windowKind, needsGlContext, needsVulkanSurface and needsSurfacePresenter; GraphicsDevice hands it only those services plus a NativeWindowHandle snapshot (see Renderer selection internals).
  7. 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).
BackendEvent pumpLogical vs pixel sizeGraphics servicesAdoption
SDL3SDL_PollEvent loop into a mapperSDL pixel density; drawable from SDL_GetWindowSizeInPixelsSdl3GlContext, Sdl3VulkanSurface, Sdl3SurfacePresenterSDL windows by id or handle
SDL2SDL_PollEvent loopRatio of drawable to logical widthNested Sdl2Platform::GlContext onlySDL2 windows by id or handle
X11XPending/XNextEvent, then evdev, portal and tray pumpsOne coordinate space: scale is always 1X11GlContext (GLX), X11VulkanSurface, X11SurfacePresenterRegistered windows or an XID on this display
WaylandProtocol callbacks queue events; PollEvents pumps and moves themCompositor-provided integer or fractional scaleWaylandGlContext (EGL), WaylandVulkanSurface, WaylandSurfacePresenter (wl_shm)Only this platform's own windows
Win32PeekMessageW(nullptr) drain, dispatch to the window procedureClient rectangle is the drawable; scale from the window's DPIWin32GlContext (WGL), Win32VulkanSurface, Win32SurfacePresenterBy id or HWND
HeadlessDrains a queue filled by InjectEventIdentical; scale 1.0NoneRefused (contract default)
TerminalSIGWINCH flag, then one byte-stream decoderNominal 8×16 pixels per cell; scale 1.0TerminalSurfacePresenter only, and only on a TTYRefused (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:

DirectoryUsed byContents
src/CommonAny backend that needs itStandardFileSystem, StandardSystemInfo, and the SurfaceFrame validation and fitting helpers that the SDL3, X11, Wayland, Win32 and Terminal presenters share
src/Xkb, src/Freedesktop, src/PosixX11 and Wayland onlyXKB key tables, libdbus loaded at run time with the desktop portal and screen-saver bus, the monotonic clock and the Vulkan loader lookup
src/LinuxX11 and Wayland, when linux/input.h existsKernel 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/TerminalTheir own backendNative 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().

BackendCTest entries (cmake/UnitTests.cmake unless noted)Automatic CI (as configured in platform-ci.yml)
SDL3CnaPlatformTests, CnaPlatformWindowTests (SDL dummy video driver), CnaPlatformXErrorHandlerTests (real X display)Matrix cells with OPENGLES3, VULKAN and SOFTWARE
SDL2CnaSdl2PlatformTests (its own executable), CnaSdl2OnlyRendererGateOne cell: SDL2 platform, SDL2 audio, OPENGLES3
X11CnaX11MappingTests, CnaX11EvdevTests, CnaX11IntegrationTests, CnaX11WindowManagerTests, CnaX11InputMethodTests, CnaX11TouchscreenTests, CnaX11ExclusiveFullscreenTestsTwo SDL-free jobs on Xvfb
WaylandCnaWaylandMappingTests, …ProtocolTests, …PortalTests, …WestonTests, …WestonGpuTests, …WestonScaledTests, …MutterTests, …MutterCzechTests, …IbusTests, CnaWaylandLinkClosure; CnaWaylandPlatformSelection in every buildNone: no workflow selects CNA_PLATFORM=WAYLAND
Win32Discovered CnaTests cases; the standalone harness in tools/platform/standalone_tests registers its own CnaPlatformTestsmingw-w64 cross-build run under Wine on every push; native MSVC job on manual dispatch only
HeadlessCnaPlatformTests, CnaPlatformWindowTests (conformance instances)The display-free matrix cell, which also runs the platform source gates
TerminalCnaPlatformTests (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.

  1. PlatformSelection.cmake, PlatformX11.cmake and PlatformWayland.cmake: what is offered, reserved and refused, and why.
  2. modules/platform/CMakeLists.txt and PlatformFactory.cpp: what is compiled and how each backend is named.
  3. IPlatform.hpp, IPlatformWindow.hpp and PlatformCapabilities.hpp: the promises every backend makes.
  4. CurrentPlatform.cpp and Game.cpp: ownership, installation and the per-frame consumption of events and snapshots.
  5. PlatformConformanceTests.cpp and docs/platform-abstraction.md: the rules a new or changed backend must keep.
  6. Then one backend page: SDL3, SDL2, X11, Wayland, Win32, Headless or Terminal.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Internals
SDL3 · SDL2 · X11 · Wayland · Win32 · Headless · Terminal · Input internals