Platform architecture
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 contract rules were read from IPlatform.hpp, PlatformFactory.cpp, IPlatformWindow.hpp, the CMake selection files, docs/platform-abstraction.md and the tools/platform scripts; the gates and conformance suites are named as they are registered and were not executed for this page.
IPlatform (IPlatform.hpp) owns host integration: subsystem acquisition, windows, event pumping, timing, input services, host services and the seams through which a renderer gets a native surface. Renderers receive narrow window and surface capabilities instead of a platform object, so that the same renderer works whichever of CNA's seven platform implementations created the window. This page states the boundary, the rules an implementation is held to, and the mechanical gates that protect them; the per-backend source tours are the platform backends pages.
Contract shape
PlatformFactory::Create() default implementation chosen by CNA_PLATFORM
│ (Create(name) and GetAvailable() serve the conformance suite)
▼
IPlatform
├─ subsystems AcquireSubsystem / ReleaseSubsystem / IsSubsystemInitialized
├─ events PollEvents(caller-owned batch) → input bridge, then Game
├─ timing performance counter, frequency, Delay
├─ windows CreateWindow / AdoptWindow → IPlatformWindow
├─ services keyboard, mouse, gamepad, joystick, clipboard, dialogs, filesystem, system info ...
└─ graphics seams GetGlContext · GetVulkanSurface · CreateSurfacePresenter
│
└── RendererSurfaceInfo snapshot + one narrow service ──► renderer
The factory is the single place where an implementation is chosen. Create() builds the one selected by CNA_PLATFORM; Create(name) builds any compiled implementation by name, and GetAvailable() lists them. HEADLESS is compiled into every binary and TERMINAL into every POSIX one, whatever the selection says, because a conformance suite is only worth something with more than one implementation live in one process.
- Subsystems are acquired, not initialized. There is deliberately no platform-wide
Initialize()orShutdown().PlatformSubsystem(Video,Audio,Gamepad,Haptic,Sensor) is acquired lazily through reference-countedAcquireSubsystem/ReleaseSubsystem; an unpaired release is a no-op, and global native-library lifetime belongs to the embedding application. Nothing is controlled by a process-global switch. - Capability reports are promises.
PlatformCapabilitieshas 32 flags, every one defaulting to false; it is read once and cached, and querying it per frame is defined as a defect. A service accessor is null exactly when its presence capability is false (quality flags such asexactKeyboardStatedo not gate presence), and unsupported behavior refuses deterministically withPlatformNotSupportedExceptionnaming the missing capability. Capabilities do not change during an instance's lifetime. - Events are batches.
PollEvents(std::vector<PlatformEvent>&)clears and refills a caller-owned vector that the runtime reuses every frame.PlatformEventis astd::variantof 16 event types, and per-event virtual dispatch is intentionally impossible. - Native pointers stay at the edge. Native input and event data is mapped into CNA enums and owning values inside the implementation. A renderer receives a
RendererSurfaceInfovalue (window id, a tagged non-owningNativeWindowHandlewhosesystemfield says which of Win32, X11, Wayland, Cocoa, Android, Web, Headless or Terminal its pointers belong to, drawable pixel size and display scale) plus one narrow service if its descriptor asks for it: the GL context service, the Vulkan surface service or a surface presenter. It never receives anIPlatformWindowor a windowing-library type, and it never calls back through the platform to draw. - The ambient accessor is for the static XNA surface only.
Keyboard::GetState(),Mouse::GetState(),StorageDeviceandTitleContainertake no context argument, so they reach the platform throughGetCurrentPlatform();Gameinstalls its owned platform there, and code that has aGameshould use it directly.
IPlatformSurfacePresenter is a one-finished-frame API, not a drawing API, for a CPU-raster family that owns no swap chain. GraphicsDevice builds a presenter only for a family whose descriptor sets needsSurfacePresenter (only the SOFTWARE descriptor does) and only when the platform reports surfacePresentation without a native window handle. At this snapshot that means SOFTWARE on TERMINAL attached to a TTY; every windowing platform offers a native handle and keeps a CPU renderer off-screen.
The seven implementations
CNA_PLATFORM | Offered | Character |
|---|---|---|
SDL3 (default) | everywhere | The default platform. CNA’s contract document calls it the reference implementation, but the conformance suite does not diff backends against it: the parameterised cases assert the same rules on each implementation independently. |
SDL2 | everywhere | An independent backend written against SDL 2.30's own API (never a compatibility layer over SDL3) with a deliberately narrow capability profile. It cannot share a process with SDL3. |
X11 | where the X development environment exists | Native Xlib backend with no SDL in it; refused with a list of packages to install rather than replaced by SDL3. |
WAYLAND | where the Wayland development environment exists | Native Wayland client with neither SDL nor an X11 library; never falls back to SDL3 or Xwayland. |
WIN32 | Windows targets, including a mingw-w64 cross-build | Native user32 and gdi32 backend with no SDL; compiled only when selected, because every translation unit includes <windows.h>. |
HEADLESS | always | No windowing system, display or input devices: one in-memory window object, every capability false, events injected by tests. The conformance and CI role; compiled into every binary. |
TERMINAL | POSIX only | termios session that can present a finished CPU frame; compiled into every POSIX binary. |
SDL12 and EMSCRIPTEN are reserved and refused, and a host-conditional name is reserved on the hosts that cannot build it (TERMINAL on Windows, WIN32 elsewhere), so the refusal reads as “not here” rather than as a typo. Each backend's source tour, tests and CI cell is on the platform backends page; what each offers a game, with the full capability matrix, is in the Platform Support guide.
Window units and synchronization
Window size and placement APIs distinguish logical units from physical framebuffer pixels. IPlatformWindow::GetClientBounds() and SetSize() are logical; GetPixelSize() is the drawable size in physical pixels and may exceed the logical size under high DPI; GetDisplayScale() relates the two and is 1.0 where the platform reports no HighDpi capability. A renderer sizes its swapchain from the pixel size, never from the logical bounds. Four concerns stay separate: logical client coordinates for input, physical drawable dimensions for back buffers and swapchains, renderer-owned viewport, letterbox and overscan transforms, and the stable WindowId used by events and the renderer registry. Borderless and exclusive fullscreen are distinct modes, and a backend that cannot provide the requested distinction fails the operation instead of reporting the other mode as success.
Window state changes are asynchronous on most systems: a SetSize followed immediately by GetClientBounds may report the old size. Sync() blocks until pending changes have been applied, and it is part of the contract that code whose next step depends on the change calls it; consuming stale dimensions is a common route to an incorrectly sized swapchain. The Headless backend models the asynchrony deliberately so code cannot accidentally depend on immediate application. GraphicsDevice::UpdateViewportFromWindow tolerates a platform window that refuses a size query by logging a warning and keeping the renderer's previous surface, so a resize delivered from inside the event pump cannot end the game by throwing.
Event ordering
The runtime sends each PlatformEvent through the input bridge before applying game-level quit, window and lifecycle handling; that order is what stops an Exit() in the middle of a batch from hiding the remaining events from input. The current implementation of PlatformInputBridge::ProcessEvent is in modules/input/src/Internal/SdlInputBridge.cpp; older prose that names a standalone PlatformInputBridge.cpp is stale. The bridge maps mouse clicks, text input and editing, touch, device connect and disconnect events into the input module. The keyboard, mouse and gamepad state that games read is different: it lives in the platform's own services, whose Update() runs once per frame at the end of Game::PollEvents, after the batch (see the input flow).
Window ids are stable non-zero identifiers within one platform instance. The current runtime deliberately does not filter events by its own window id; that observable behavior, and the whole event-to-state mapping, is pinned by the golden transcript platform-event-semantics.txt and GameEventSemanticsGoldenTest. A new event variant must update the event taxonomy helpers, every implementation's mapper, the input bridge where applicable and that golden transcript.
Required platform gates
python3 tools/platform/sdl_inventory.py --check
python3 tools/platform/sdl_classify.py --check
python3 tools/platform/renderer_sdl_audit.py --check
python3 tools/platform/sdl_ratchet.py --check --strict
python3 tools/platform/hot_path_lint.py
python3 tools/platform/nonproduction_sdl_audit.py --check
python3 tools/platform/check_contract.py
These seven commands live in tools/platform, are listed in the implementer's checklist in docs/platform-abstraction.md, and run in the HEADLESS cell of the platform-contract job of the platform CI workflow (the SDL-containment subset also runs in the x11-sdl-free job). Two of them also run at configure time when a Python 3 interpreter is found: the ratchet (options CNA_PLATFORM_RATCHET and CNA_PLATFORM_RATCHET_STRICT, both ON, which also audits the non-production manifest) and the hot-path lint (CNA_PLATFORM_HOT_PATH_LINT, ON).
sdl_inventory.pycounts every SDL identifier per module and checks that the generated section ofplans/plan_platform.mdis current;sdl_classify.pyassigns each identifier to a contract area and fails on an unclassified one.renderer_sdl_audit.pyholds the allowlist of renderer families that may use SDL:sdl-rendererandsdl-gpuby identity,fna3dandfreedirectby upstream dependency.sdl_ratchet.pyrecords a checked-in budget that may go down and never up; without--strictit only warns.hot_path_lint.pyrejects platform calls reached through a platform receiver inside per-pixel, per-vertex, per-sample or per-event loops (a justified exception carriesCNA_PLATFORM_HOT_PATH_OK: reason);nonproduction_sdl_audit.pykeeps SDL use in tests and examples classified and under per-file ceilings;check_contract.pykeeps every contract header in the SDL-free probe and documented.
Run focused unit and conformance tests too. The audits protect architecture and source classification; they do not prove event behavior on every host, and the two heuristic checks under-report by design, so a pass is not proof and a failure is always real. The conformance suites are parameterized over PlatformFactory::GetAvailable() and run as EveryImplementation/PlatformConformance.*, EveryImplementation/PlatformWindowConformance.* and EveryImplementation/GameEventSemanticsGoldenTest.*; ContractIsSdlFreeTests.EveryContractHeaderCompilesWithoutSdl proves the contract headers pull in no SDL, Vulkan or GL header. None of them was executed for this page. The last changes under tools/platform before this snapshot were routine upkeep: a Mesa gallium leak-suppression entry for the OpenGL4 tests and two new SDL_GPU example entries in the non-production budget.
Adding host behavior safely
- Find the narrow platform contract that owns the capability; if none does, add one under
the platform contract headers. - Define semantics in platform-neutral types. Do not expose an SDL, Win32, X11 or Wayland pointer or an event-lifetime string across the contract.
- Update every implementation that claims the capability, or narrow its advertised capability: start each implementation with every flag false and flip a flag only when the accessor and its refusal behavior are complete.
- Trace how
Gameconsumes the event or service and how input state is published (the end ofGame::PollEvents). - Add contract tests to the parameterized suites, add implementation-specific mapper tests beside them, and run the source gates above.
Adding a whole implementation
The implementer's guide adds these steps: create modules/platform/src/<Name>/ and implement the entire IPlatform surface without subclassing the SDL3 one; add the uppercase name to PlatformSelection.cmake and fail loudly on unsupported hosts instead of falling back; add conditional sources and private native-library links in modules/platform/CMakeLists.txt; register the display name in all three PlatformFactory operations (default selection, named construction, GetAvailable()); give every acquired native resource an explicit owner and test constructor failure as well as teardown order; and add one compatible renderer to the CI matrix rather than multiplying every platform by every renderer. The SDL2 and SDL3 implementations cannot share a process because their entry points collide, which is why CNA_PLATFORM=SDL2 with CNA_AUDIO_PLATFORM=SDL3 (and the reverse) is a configure error.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Android and Apple targets: structure, lifecycle, assets and evidence — How CNA's Android application and NDK code are structured, how Game handles mobile lifecycle events, how assets and saves are found on a device, and what the macOS and iOS workflows check.
- GameWindow, GraphicsDeviceManager and the supporting types — CNA's single GameWindow facade and its failure policy, GraphicsDeviceManager construction, CreateDevice, ApplyChanges and presentation preferences, and LaunchParameters, TitleContainer, TitleLocation and FrameworkDispatcher.
- Host devices: the optional CNA::Devices layer — The CNA_DEVICES build contract, what each CNA::Devices class really does, its platform reach, the overlap with CNA::Input, the camera polling model and the callback boundary callers must respect.
- Sensors and vibration: delivery, math and lifetime — Which thread delivers a CNA sensor reading and in which units, the Android compass and motion mathematics, the landscape remap, VibrateController semantics, the open Dispose(bool) defect and the evidence limits.
- 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.
- Windows from Linux: MinGW cross-builds, runtime staging and Wine evidence — The MinGW-w64 route from Linux to a runnable Windows test: toolchain, target-built SDL, DLL staging, CTest emulators, which Wine runtime owns each renderer, prefix hygiene and evidence tiers.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-176: Setting Game::IsMouseVisible throws PlatformException on the terminal platform when the terminal reports mouse input — Game::setIsMouseVisibleProperty forwards to IPlatformMouse::SetCursorVisible whenever a window and a mouse service exist; the terminal platform supplies a TerminalMouse whenever stdin and stdout are both terminals (Termi
- CNA-BUG-177: The Headless and Terminal window constructors accept BorderlessFullscreen although their own SetFullscreenMode refuses it — Both capability-minimal windows store WindowDescription::fullscreenMode directly at construction instead of routing it through SetFullscreenMode, so a window created in BorderlessFullscreen reports that mode though the p
- 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.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Platform Support: implementation table · Capability matrix (32 flags) · Native Platforms: SDL-free builds · What each renderer needs from the platform
- Architecture
- Architecture overview · Runtime lifecycle · Graphics architecture · Audio and input architecture
- Internals
- Platform backends · SDL3 (reference backend) · X11 · Wayland · Win32 · Headless · Terminal · Input internals
- Maintainer workflow
- I need to modify a platform backend · Thread and callback map
- Tests and validation
- Test architecture · What to test after changing X