Platform Support
What changed since alpha.1. This page describes the current development snapshot (commit c1c316b9, branch apple/m4-stabilization). This snapshot offers three platform implementations — SDL3, the one windowing implementation, plus the windowless Headless and POSIX Terminal hosts — three audio implementations (one native and SDL-free), a build switch that removes SDL for windowless builds, a 32-flag capability contract (29 in alpha.1), and a curated set of 14 renderer identities. Everything below is stated at the level the code and CI actually prove. Statements about CI describe what the workflow files at this snapshot configure, build, run and assert; we read those files and did not inspect run results (see the caveat under What CI actually covers).
Four separate axes
In this snapshot, “backend” is still too ambiguous to be useful. CNA makes four different decisions, plus one build switch that decides whether SDL is present at all:
| Axis | Question it answers | Selection |
|---|---|---|
| Target operating system | Which toolchain and OS ABI is this binary built for? | CMake toolchain / target; reported by CNA::TargetPlatform |
| Platform implementation | Who owns windows, events, input, timing and host services? | CNA_PLATFORM — 3 implementations |
| Graphics renderer | Who turns CNA draw calls into pixels? | CNA_GRAPHICS_RENDERER and optional CNA_GRAPHICS_RENDERERS — 14 identities |
| Audio implementation | Who opens playback and recording devices, and (for two values) mixes? | CNA_AUDIO_PLATFORM — 3 implementations |
| (switch) SDL availability | Is SDL configured, built and linked at all? | CNA_ENABLE_SDL = AUTO (default), ON or OFF |
The platform implementation implements CNA::Platform::IPlatform and supplies narrow services to a renderer. It does not decide the renderer, audio implementation or target OS. This is also why the old public CNA::Platform enum became CNA::TargetPlatform (Desktop, Android, iOS, Web): CNA::Platform now names the host-integration namespace. The axes are independent with hard exclusions, listed under combination rules.
Platform and audio implementations
| Option | Implemented values | Default | Boundary |
|---|---|---|---|
CNA_PLATFORM | SDL3, HEADLESS, TERMINAL | SDL3 on every OS | SDL3 is the one windowing implementation: Windows, X11, Wayland, macOS, iOS, Android and the browser are reached through SDL’s own video drivers. TERMINAL is offered only off Windows (POSIX termios). Any other value fails configuration by name — there is no fallback to SDL3. Values are case-sensitive. |
CNA_AUDIO_PLATFORM | SDL3, NULL, ALSA | SDL3 on every OS | All three select low-level device code. SDL3 and ALSA also define SOUND_ENABLED and provide a mixer (SDL3_mixer, or CNA's own mixer for ALSA). NULL is not a feature-equivalent game-audio backend. ALSA is Linux-only. OPENAL and WASAPI are reserved and rejected. |
CNA_ENABLE_SDL | AUTO, ON, OFF | AUTO | AUTO and ON configure SDL3 as before. OFF fetches, builds and links no SDL, and refuses any selection that genuinely needs it (platform or audio SDL3; renderers SDL_RENDERER, SDL_GPU, FNA3D). |
# A display-free simulation that still uses SDL3 audio
cmake -S . -B build-headless \
-DCNA_PLATFORM=HEADLESS \
-DCNA_AUDIO_PLATFORM=SDL3 \
-DCNA_GRAPHICS_RENDERER=HEADLESS
# No SDL anywhere: a windowless build with ALSA audio and CNA's own mixer (Linux)
cmake -S . -B build-sdl-free -G Ninja \
-DCNA_ENABLE_SDL=OFF \
-DCNA_PLATFORM=HEADLESS \
-DCNA_AUDIO_PLATFORM=ALSA \
-DCNA_GRAPHICS_RENDERER=HEADLESS
# POSIX terminal host: CPU renderers only
cmake -S . -B build-terminal \
-DCNA_PLATFORM=TERMINAL \
-DCNA_AUDIO_PLATFORM=NULL \
-DCNA_GRAPHICS_RENDERER=SOFTWARE
Audio selection is ahead of audio parity. In modules/CMakeLists.txt, SOUND_ENABLED is defined for CNA_AUDIO_PLATFORM=SDL3 and CNA_AUDIO_PLATFORM=ALSA only. NULL builds omit the mixer, decoder and mixer-dependent tests; the XNA audio facade remains present but does not gain production playback from the Null device class (SoundEffect::Play() returns false). Null is therefore useful for deterministic configuration and device-contract work, not proof that a silent mixer consumes normal game audio. Audio System has the full implementation matrix.
The three platform implementations
| Implementation | What it is | Third-party inputs | Automatic CI evidence |
|---|---|---|---|
SDL3 Default | The one windowing implementation, on every target: Windows, X11 and Wayland sessions, macOS, iOS, Android and the browser through SDL’s own video drivers. A renderer that needs a native handle (HWND; X11 Display* and window; Wayland wl_display* and wl_surface*; a Cocoa or UIKit window) gets it from the SDL3 window through NativeWindowHandle. | Vendored SDL3 (shared on Linux, macOS, Windows and Android; static on iOS and Emscripten) | Many workflows: platform matrix (OPENGLES3, VULKAN, SOFTWARE cells, with the SDL3 window suite also run on SDL’s x11 driver under Xvfb and its wayland driver under a headless Weston), input, general tests, Apple, Emscripten; the native-MSVC Windows job is manual |
HEADLESS | One window object, no optional services (only the always-present file-system and system-information services), every capability false. Always compiled in. | None | Platform matrix (including an SDL-free Headless + ALSA job), multi-renderer CI |
TERMINAL | POSIX-only terminal host (termios/poll, Kitty keyboard probe); presents finished CPU frames. One window. | None | Platform matrix cell plus a pseudo-TTY demo integration test |
The three audio implementations at a glance
| Value | Mixer (SOUND_ENABLED) | OS | Capture | Notes |
|---|---|---|---|---|
SDL3 (default) | Yes — SDL3_mixer | Every target | Yes | Needs SDL, so it cannot be combined with CNA_ENABLE_SDL=OFF. |
NULL | No | All | No | Deterministic silent transport; the only SDL-free choice off Linux. |
ALSA | Yes — CNA's own mixer | Linux only | Yes | No SDL. libasound is loaded at run time; CNA_AUDIO_DEVICE and CNA_AUDIO_RECORDING_DEVICE pick devices. |
Combination rules and hard exclusions
TERMINALaccepts only CPU renderers:SOFTWARE,HEADLESS,STUB.CNA_ENABLE_SDL=OFFrefuses platformSDL3, audioSDL3and the three SDL-linked renderers, with a message naming the reason.- Platform gates:
TERMINALonly off Windows;ALSAonly on Linux. A windowed build on any operating system usesSDL3. - Renderer combinations in
CNA_GRAPHICS_RENDERERSobey one rule: identities gated to different operating systems cannot be mixed. See Runtime Renderer Selection. - A name outside the 14 public renderer identities is a configure-time error.
Window systems through SDL3, and SDL-free builds
SDL3 is CNA’s one windowing implementation, and the window system is SDL’s choice of video driver rather than a separate CNA platform: Windows uses SDL’s windows driver, a Linux desktop its x11 or wayland driver, and macOS, iOS, Android and the browser SDL’s own drivers for them. The native handles a renderer needs still reach it unchanged — an HWND, an X11 Display* and window, a Wayland wl_display* and wl_surface* — through NativeWindowHandle and its TryGetWin32/TryGetX11/TryGetWayland accessors (and CNA_NATIVE_WINDOW_SYSTEM_* in the C API). The dedicated page, Windows, X11 and Wayland, has the details and the evidence.
X11 through SDL3
In CISDL’s x11 video driver on an Xorg session (or Xwayland). CnaPlatformSdl3X11Tests runs the SDL3 window suite on a private Xvfb and checks that the Display* and window a renderer receives match the driver; the platform workflow runs it on every push. The recorded local validation ran seven renderers’ 2D and 3D demos on it.
Wayland through SDL3
In CISDL’s wayland video driver on a Wayland session, provided the Wayland development packages were present when the vendored SDL3 was first configured. CnaPlatformSdl3WaylandTests runs the same window suite on a private headless Weston and checks the wl_display* and wl_surface* a renderer receives.
Windows through SDL3
Manual CISDL’s windows video driver, with the HWND passed to the Direct3D and Vulkan renderers. The sdl3-windows job builds CNA natively with MSVC and runs the platform module suite on GitHub’s windows-latest image; it runs on manual dispatch, and its recorded run passed. A mingw-w64 cross-build of the window suite also passed under Wine.
SDL-free builds
In CI-DCNA_ENABLE_SDL=OFF builds a binary with no SDL in it, for servers, test runners and terminal games. It has no window: the platform is HEADLESS or TERMINAL, the renderer HEADLESS, SOFTWARE or STUB, and audio NULL or (on Linux) ALSA. A platform job builds a Headless + ALSA tree this way and checks that nothing links SDL.
Two other tutorials cover the remaining pieces: Tutorial 137 (ALSA audio with CNA's own mixer) and Tutorial 139 (the POSIX terminal platform).
Capability matrix (32 flags)
Every platform reports a fixed set of 32 boolean capabilities through IPlatform::GetCapabilities(). A capability is true only when the service behind it is really wired up; a false one means the call refuses deterministically with PlatformNotSupportedException (never a silent no-op). “If…” entries depend on the machine and are decided once, when the platform is created. Three flags — dragAndDrop, primarySelection, clipboardData — were appended since alpha.1's 29.
| Capability flag | SDL3 | Terminal | Headless |
|---|---|---|---|
multipleWindows | Yes | No (one) | No (one) |
highDpi | Yes | No | No |
multipleDisplays | Yes | No | No |
borderlessFullscreen | Yes | No | No |
nativeWindowHandle | Yes | No | No |
surfacePresentation | Yes | If stdout is a TTY | No |
openGlContext | Yes | No | No |
vulkanSurface | If Vulkan is available | No | No |
clipboard | Yes | No | No |
clipboardData | Yes | No | No |
dragAndDrop | Yes | No | No |
primarySelection | Yes (Unix desktops) | No | No |
textInput | Yes | No | No |
ime | Yes | No | No |
exactKeyboardState | Yes | If the Kitty keyboard protocol answers | No |
pixelAccurateMouse | Yes | No (cells) | No |
relativeMouse | Yes | No | No |
cursorShapes | Yes | No | No |
globalPointer | Yes | No | No |
inputDeviceEnumeration | Yes | No | No |
gamepad | Yes | No | No |
joystick | Yes | No | No |
gamepadRumble | Yes | No | No |
gamepadSensors | Yes | No | No |
haptics | Yes | No | No |
sensors | Yes | No | No |
powerInfo | Yes | No | No |
messageBox | Yes | No | No |
nativeFileDialog | Yes | No | No |
tray | If supported | No | No |
camera | If supported | No | No |
managedEntrypoint | Yes (Android and iOS main) | No | No |
What "supported" means here
"Supported" is doing a lot of work in most framework documentation. This page avoids the word and names the level instead, because the difference between "there is code for it" and "someone ran it" is exactly the difference that costs you a weekend.
| Level | What it means |
|---|---|
| Code path exists | Sources and CMake wiring are present for the target. |
| Configures | CMake accepts the target and the chosen renderer, and produces a build system. |
| Cross-compiles | A full build completes from a different host operating system. |
| Runs under Wine or an emulator | The binaries execute and render, but not on the operating system they target. |
| Runs on real hardware | Someone ran it on the actual platform. |
| Covered by CI | A workflow builds or tests it automatically on push and pull request — the only level that keeps working without anyone remembering to check. |
These levels do not imply each other. The repository contains MinGW/Wine paths, but no automatic workflow uses them; the native-Windows lanes still require manual dispatch. macOS and iOS have build workflows that trigger automatically, and a recorded campaign on a physical Mac (see macOS). Android has source/NDK wiring and no workflow.
Platform matrix
| Platform | Renderers | Highest level reached | Automatic CI |
|---|---|---|---|
| Linux | Every renderer that is not Windows-only (2), Apple-only (1) or Emscripten-only (1) — 10 of the 14 identities. Default: OPENGLES3. |
Runs on real hardware | Yes — multiple focused workflows on ubuntu-* runners (Xvfb and Mesa software drivers) |
| Windows | The 2 Windows-only renderers (DIRECTX9, DIRECTX11), plus the portable ones. Default: SDL_RENDERER. |
Native MSVC builds run on GitHub’s Windows image by manual dispatch; Direct3D 11 validated by hand on one physical Windows 11 machine | No automatic Windows job — the MSVC Direct3D 11, SDL3 platform and content workflows are manual dispatch only |
| Web (Emscripten) | WEBGL2, plus WEBGPU. Default: WEBGL2. |
Build workflow configured; 90 C++ sample builds played in Chrome | An Emscripten multi-renderer build/link lane (WEBGL2 + WEBGPU); runtime evidence is the published sample gallery, each build run in Chrome when it was published, and CNA.NET’s headless-Chromium runs |
| macOS | METAL (Apple-only), OPENGL33, WEBGPU, SDL_GPU, FNA3D, SOFTWARE and the other portable renderers. Default: SDL_RENDERER. |
Runs on real hardware — full test trees on a physical Mac mini M4 (see macOS) | Yes — Apple and Metal workflows on GitHub’s macos-26 runners (a paravirtual GPU); no green hosted run after the campaign’s last CI change is recorded |
| Android | Portable renderers via the NDK toolchain. Default: SDL_RENDERER. |
Code paths and NDK sensor implementations exist | No CI of any kind |
| iOS / tvOS | SDL_RENDERER and METAL (the iOS allow-list); tvOS has no validated path. |
iOS Simulator execution with a pixel probe; device build final-linked, never run | iOS device and simulator builds for both renderers; the simulator legs launch a smoke app and (for METAL) the pixel probe. No physical-device claim. |
A single renderer remains the default build. The opt-in CNA_GRAPHICS_RENDERERS mode can link compatible sets, but it does not erase per-renderer capability or platform boundaries.
Status by operating system and platform implementation
Only what CI or code proves. CI = an automatic workflow (push or pull request to next, develop, main) builds or tests it. Manual CI = a workflow exists but runs only on workflow_dispatch. Code = sources and CMake wiring, no workflow. Local = only in-tree documents or scripts claim validation.
| Target | Platform implementations available | Real / tested | Experimental | Not proven |
|---|---|---|---|---|
| Linux, X11 session (Xorg) | SDL3 (default; SDL’s x11 driver), HEADLESS, TERMINAL |
CI SDL3 × OPENGLES3/VULKAN/SOFTWARE/SDL_RENDERER, and the SDL3 window suite on the x11 driver; an SDL-free Headless + ALSA build. All on Xvfb. Local seven renderers’ 2D and 3D demos on SDL’s x11 driver (2026-10-06). |
— | Real desktops and physical GPUs are not a CI gate; the GPU pixel/oracle matrix is not gated. |
| Linux, Wayland session | SDL3 (SDL’s wayland driver, which exists only if the Wayland development packages were present when SDL was first built; otherwise X11 through Xwayland) |
CI the SDL3 window suite on the wayland driver under a private headless Weston. Local seven renderers’ 2D and 3D demos on SDL’s wayland driver (2026-10-06). |
— | Real compositors (GNOME, KDE) are not a CI gate. |
| Windows | SDL3 (default; SDL’s windows driver), HEADLESS |
Manual CI MSVC build of DIRECTX11 (300 CTest entries passed on windows-latest, 2026-10-06); the SDL3 platform suite on MSVC; a Headless content-pipeline build. |
— | FFmpeg video (never built for Windows); WASAPI audio (reserved); XInput. |
| macOS | SDL3 (default; SDL’s Cocoa driver). CMake also offers HEADLESS and TERMINAL, which no macOS lane exercises. |
Real hardware A physical Mac mini M4 (Apple M4, 16 GB, macOS 27.0.1, Xcode 27, Apple clang 21) ran seven full test trees with no failure: METAL 11,015, SOFTWARE 11,084, OPENGL33 with compiled effects 11,916, SDL_GPU 10,962, WEBGPU 10,968, FNA3D 10,875, SDL_RENDERER 10,940 (each with its named skips). CI, configured macos-26 builds of SDL_RENDERER with portable suites and an .app launch, and of METAL with its ^Metal tests. Deployment floor 13.3. |
— | On-screen presentation (the M4 console was locked), Intel Macs, VULKAN/MoltenVK, and any green hosted run after the campaign. |
| iOS | SDL3 (default; SDL’s UIKit driver). HEADLESS and TERMINAL are not exercised on iOS. |
Simulator iOS Simulator (iPhone 17 profile) on the M4: cna_ios_smoke runs, and cna_ios_pixel_probe on METAL reads back exact pixels. Build only device apps for SDL_RENDERER and METAL final-link. Deployment floor 16.3; networking must be off (CNA_ENABLE_NET=OFF). |
The whole target is experimental | Any physical iPhone or iPad: device pixels, touch, audio, storage, background and resume, performance; tvOS. |
| Android | SDL3 (default; the only one CI configures). CMake also offers HEADLESS and TERMINAL; none is exercised on Android. |
Code; emulator runs outside CI NDK sensors, the SDL entry point and a Gradle demo project (minSdk 24, arm64-v8a); lifecycle, Back-button, portrait and APK-content fixes in this snapshot were found by running samples and games on the x86_64 Android emulator. No workflow, no preset. | — | Physical devices, GL context loss, and everything at run time beyond those emulator runs. |
| WebAssembly (Emscripten) | SDL3 (default; the only one CI configures). CMake also offers HEADLESS and TERMINAL; none is exercised on the web. |
CI workflows, configured one bundle with WEBGL2;WEBGPU built and its renderer archives, registry entries and JS selection surface asserted; emsdk 6.0.3; Draco off. |
Threads option CNA_ENABLE_EMSCRIPTEN_THREADS; CNA_EMSCRIPTEN_USE_WASMFS=OFF for persistent saves in a threaded build |
Video (no FFmpeg build), sanitizers, any CNA_PLATFORM other than SDL3. |
| POSIX terminal | TERMINAL |
CI TerminalPlatform unit tests plus a pseudo-TTY run of the 2D demo with SOFTWARE. |
— | The Windows console (not offered). |
| Headless (any host) | HEADLESS |
CI platform matrix and multi-renderer jobs. | — | — |
Renderer availability by operating system
Which identities CMake will accept for each target. “Gated” means a hard FATAL_ERROR on any other target; “not gated” means CMake has no operating-system rule (the renderer may still need its own dependency: a Vulkan loader, a fetched library, a sibling checkout). This is configure-time acceptance, not a statement that every accepted pair has been run.
| Renderer group | Linux | Windows | macOS | iOS | Android | Web |
|---|---|---|---|---|---|---|
DIRECTX9, DIRECTX11 (2, Windows-only) | Refused | Gated to here (native or mingw-w64) | Refused | Refused | Refused | Refused |
WEBGL2 (1, Emscripten-only) | Refused | Refused | Refused | Refused | Refused | Gated to here. Default WEBGL2 |
METAL (1, Apple-only) | Refused | Refused | Accepted | Accepted (allow-listed) | Refused | Refused |
OPENGLES3, OPENGL33 (EasyGL; need ../easy-gl and ../meta-gl) | Default is OPENGLES3 | Accepted (CMake warns OPENGLES3 is “primarily tested on Linux”) | Accepted (same warning) | Refused (allow-list) | Accepted | Refused (use WEBGL2) |
SDL_RENDERER | Accepted | Default | Default | Default (allow-listed) | Default | Not gated |
VULKAN, SDL_GPU, WEBGPU, FNA3D, SOFTWARE, HEADLESS, STUB | Not gated | Not gated | Not gated | Refused (allow-list) | Not gated | Not gated by CMake; only WEBGL2 and WEBGPU are built by CI |
That is 2 + 1 + 1 platform-gated identities and 10 that are not gated by operating system, for the 14 in total. The iOS allow-list can be overridden with CNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ON for experiments only; that is not a support claim. TERMINAL further restricts the choice to SOFTWARE, HEADLESS and STUB. Which native window services a renderer needs is on Windows, X11 and Wayland.
Default selection per operating system
| Target | CNA_PLATFORM | CNA_AUDIO_PLATFORM | CNA_GRAPHICS_RENDERER | CNA_ENABLE_VIDEO (AUTO resolves to) |
|---|---|---|---|---|
| Linux | SDL3 | SDL3 | OPENGLES3 (needs the easy-gl and meta-gl siblings) | On if the four FFmpeg pkg-config modules are found |
| Windows (MSVC or MinGW) | SDL3 | SDL3 | SDL_RENDERER | Off (unsupported target) |
| macOS | SDL3 | SDL3 | SDL_RENDERER | On if FFmpeg is found (for example from Homebrew) |
| iOS | SDL3 | SDL3 | SDL_RENDERER (METAL is the other allowed) | Off |
| Android | SDL3 | SDL3 | SDL_RENDERER | Off |
| Emscripten | SDL3 | SDL3 | WEBGL2 | Off |
There is no per-OS default for the platform or audio axes — both are SDL3 everywhere — and no CNA_PLATFORM environment variable. Only the renderer has run-time selection (CNA_GRAPHICS_RENDERER in the environment, in a multi-renderer build).
Linux
Linux is CNA's primary development platform and carries most of its CI. It is where the largest number of renderers can be selected, where all three platform implementations are offered, and where the project is developed on real hardware — the default renderer here is OPENGLES3, one of the three EasyGL profiles.
SDL3 (the default) works on X11 and Wayland sessions alike, through SDL’s x11 and wayland video drivers; SDL_VIDEO_DRIVER chooses between them when both are available. HEADLESS and TERMINAL need no display server. See Windows, X11 and Wayland.
The Windows-gated renderers are also driven from Linux: they are cross-compiled with MinGW-w64 and executed under Wine, so a Linux workstation is in practice the machine where the Direct3D ladder gets exercised too. See Windows for what that does and does not prove.
Practical requirements: the sharp-runtime sibling checkout (on the branch matching CNA’s) for every build, easy-gl plus meta-gl for the default renderer, a C++23 compiler (CI uses GCC 14), and, for a windowed build, the X11/GL/Vulkan/ALSA/D-Bus development packages so the vendored SDL3 gets real video drivers. FFmpeg development packages are optional now. Building has the exact commands and package lists.
Windows
Windows has the largest platform-exclusive renderer set in the project: 2 renderers refuse to configure anywhere else — DIRECTX9 and DIRECTX11. The default renderer on Windows is SDL_RENDERER. Windows windows come from SDL3’s windows video driver, which hands the Direct3D and Vulkan renderers the HWND they present into; HEADLESS is the windowless alternative. An SDL-free Windows build is therefore windowless, and must also use CNA_AUDIO_PLATFORM=NULL.
The repository provides MinGW cross-build and Wine/DXVK execution paths, but none is an automatic workflow in this snapshot. The native MSVC workflows — DIRECTX11, the SDL3 platform suite and a Headless content-pipeline build — are all manual dispatch; their recorded dispatch on GitHub’s windows-latest image (2026-10-06) passed. Treat runtime claims as results from the exact manual configuration, not as continuous native-Windows coverage.
Two further Windows facts worth knowing before you plan a release:
- FFmpeg video is never built for Windows.
CNA_ENABLE_VIDEO=AUTOsilently falls back to the no-video backend on Windows and MinGW, andONis a configure error.VideoandVideoPlayerstill compile and link; without a decoder they throwSystem::NotSupportedExceptionat run time. - The MinGW cross-build is not covered by CI. Apart from a MinGW header-compatibility check for the experimental C API, building the engine for Windows is exercised by hand (MinGW + Wine/DXVK) or by the manual MSVC workflows.
Web (Emscripten)
The web target is real and has a browser build workflow. WEBGL2 is Emscripten-only and the default; WEBGPU also has a browser route. The strongest evidence is the 90 C++ sample builds playable at samples.libcna.com; saves persist in the browser’s IndexedDB, and games that start threads use a threaded build. The multi-renderer Emscripten job configures and links a WEBGL2 + WEBGPU bundle, and the workflow pins emsdk 6.0.3. The platform is SDL3 here by default and in every browser workflow (CMake’s platform selection would also list HEADLESS and TERMINAL, neither of which is exercised on the web); CNA_PLATFORM=EMSCRIPTEN is reserved and refused.
Saves persist on the web. The storage module mounts the browser’s IndexedDB (IDBFS) for StorageDevice (/cna-storage) and isolated storage (/save) and restores it before main(). Threaded builds keep WasmFS by default and need -DCNA_EMSCRIPTEN_USE_WASMFS=OFF for persistence; if the mount fails, StorageDevice reports the device as not connected. See Storage.
Your Game object no longer must be heap-allocated on the web. Earlier builds unwound the caller's stack, so a stack-allocated Game was silently corrupted. In this snapshot Game::Run() blocks on the caller's own stack through Asyncify, so an ordinary local MyGame game; game.Run(); is supported. CNA-owned application executables link Asyncify automatically; an external final executable that uses a blocking Game::Run() must link CNA::EmscriptenAsyncify.
Video links but cannot decode here: the Video/VideoPlayer types are part of every build, no FFmpeg backend exists for Emscripten, and playback throws System::NotSupportedException at run time. Networking discovery does not work on Emscripten either.
Serve the generated .html, .js and .wasm from a local web server; browsers block direct file:// access. Configure with emcmake: emcmake cmake --preset web — the web preset carries no toolchain file, so cmake --preset web alone would configure natively.
macOS
macOS has a recorded full-suite run on real hardware. CNA’s Apple stabilization campaign ran on a physical Mac mini M4 (Apple M4, arm64, 16 GB, macOS 27.0.1, Xcode 27, Apple clang 21), on SDL3 — Apple is reached through SDL’s Cocoa video driver, not through a CNA backend of its own. Seven renderer trees ran their complete CTest suites there with no failing test; every skip declines by name:
| Renderer tree on the Mac mini M4 | Tests passed | Skipped by name | cna-samples matrix |
|---|---|---|---|
METAL | 11,015 / 11,015 | 114 | 91 / 91 |
SOFTWARE | 11,084 / 11,084 | 133 | — |
OPENGL33 (compiled effects on) | 11,916 / 11,916 | 128 | 91 / 91 |
SDL_GPU (SDL’s Metal driver) | 10,962 / 10,962 | 295 | 90 / 91 (LensFlare needs occlusion queries) |
WEBGPU (wgpu-native on Metal) | 10,968 / 10,968 | 279 | 91 / 91 |
FNA3D (FNA3D’s Metal driver) | 10,875 / 10,875 | 433 | 90 / 91 (LensFlare) |
SDL_RENDERER | 10,940 / 10,940 | 966 (2D-only refusals) | — |
The runs were made with the console locked, so macOS reported every window occluded: the readback-based pixel tests ran, but on-screen presentation and vsync pacing were never observed. OPENGLES3 does not apply (macOS has no native OpenGL ES; OPENGL33 uses Apple’s 4.1 core context), VULKAN through MoltenVK was out of scope, no Intel Mac was run, and WEBGL2 is a browser renderer. CNA’s own verdict on METAL is Supported but not primary-production: compiled XNA effects need -DCNA_METAL_COMPILED_EFFECTS=ON, custom MSL effects are SpriteBatch-scoped, PresentInterval.Two presents as One, the sampler LOD bias needs the macOS 26 SDK and OS, and no soak run has bounded its caches. See Graphics Renderers and Tutorial 109.
CI is a separate, narrower layer: apple-ci.yml builds CnaTests on SDL_RENDERER on GitHub’s macos-26 runners, runs the portable suites and launches a self-contained .app, and metal-macos-ci.yml builds METAL and runs its ^Metal tests on the runners’ paravirtual GPU, which cannot run Metal shader validation. CNA records no green hosted run after the campaign’s last workflow change.
Practical notes. The macOS deployment floor is 13.3 (a lower CNA_MACOS_DEPLOYMENT_TARGET is a configure error, because CNA needs Apple’s floating-point std::to_chars); Xcode 15.4 is too old, CI uses Xcode 26. FFmpeg is optional (CNA_ENABLE_VIDEO=AUTO uses it when found), but Homebrew’s FFmpeg is built for the host macOS, so a binary linked against it does not start on an older macOS whatever the deployment target says; use CNA_ENABLE_VIDEO=OFF or an FFmpeg built for the floor. Online Gamer Services sessions need a libcurl with WebSocket support (Homebrew’s curl): Apple’s system libcurl lacks it, and the relay then refuses by name. SystemLink discovery between processes works on macOS. SDL3 is the platform; CMake also offers HEADLESS and TERMINAL, which no macOS lane exercises.
Android
Android is genuinely wired rather than aspirational: the NDK toolchain path is in the build system, the code paths exist, and the NDK sensor implementations are real — Compass and Motion are in fact Android-only and report NotSupported elsewhere. See Sensors.
Android has no CI at all — zero jobs, zero presets. Every Android build is one somebody ran by hand, and nothing catches an Android regression automatically. The only Android build project in the repository is the Devices demo (Gradle: minSdk 24, target/compile SDK 35, NDK 30.0.14904198, arm64-v8a only). CNA's own Android status note, last updated before alpha.1, records an NDK cross-build failing inside two sibling sharp-runtime files (an unused private field under -Werror in FileStream, and std::chrono::clock_cast in FileSystemInfo.cpp). Reading sharp-runtime next @ 41b918c9 shows both causes gone (the field no longer exists and clock_cast is confined to Microsoft's STL), but we did not re-run an NDK build against it, so treat Android builds as unverified. Video is unavailable here too.
iOS and tvOS
iOS support is experimental. The snapshot ships an iOS toolchain file (cmake/toolchains/ios.cmake, with CNA_IOS_SIMULATOR=ON for the simulator SDK) and admits two renderers: SDL_RENDERER and the native METAL renderer, which attaches its CAMetalLayer view to SDL’s UIWindow. A configure needs -DCNA_ENABLE_NET=OFF (the iOS SDK ships no libcurl), so iOS builds have no networking, and FFmpeg video is off. The deployment floor is iOS 16.3.
What has been demonstrated, at its real level:
- iOS Simulator execution with pixel evidence. In the iOS Simulator (iPhone 17 profile) on the Mac mini M4,
cna_ios_smokeruns andcna_ios_pixel_probereads back theMETALframe. The first campaign run was exact on 29 of 31 launches, the two all-zero results being the first two after the initial install; the cause was later found in SDL’s UIKit view-frame handling and patched in CNA’s vendored SDL, after which the stricter probe (frame 1 and the ten frames after it) was exact in 8 of 8 launches. - A final-linked device build. Device apps for
SDL_RENDERERandMETALcompile and link (arm64, iOS 16.3, system frameworks only). They were never run: there was no physical device or signing identity. - CI, as configured.
apple-ci.ymlbuilds device and simulator apps for both renderers and, on the simulator, launches the smoke app and theMETALpixel probe.
No physical iPhone or iPad has run CNA, so nothing here establishes device pixels, real touch input, audio, storage, background and resume, or performance. CNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ON merely permits experiments outside the allow-list; it is not a support claim. tvOS has no validated path.
What CI actually covers
This snapshot contains 18 GitHub Actions workflow files (24 jobs; 16 files run automatically on pushes and pull requests to next, develop and main, and two are manual or branch-specific). They cover general tests, platform conformance, multi-renderer Linux and Emscripten builds, Apple/Metal, declared C API compatibility/release gates, and focused renderer/subsystem checks. Trigger and evidence still matter more than the raw file count. The general job configures OPENGLES3, runs a full default build and an unfiltered ctest, and tolerates a single named known failure.
Configured is not the same as passing. Statements on this page describe what the workflow files at this snapshot configure, build, run and assert; we read those files and did not inspect run results, except where a run recorded in CNA’s own plans is cited. Every workflow now takes sharp-runtime (and, where needed, easy-gl and meta-gl) from the branch matching CNA’s own — the pushed branch, then next, then develop — through scripts/ci/sibling_branch.sh, so no lane builds against a stale sibling pin.
| Workflow | Trigger | Runner | What it does |
|---|---|---|---|
| General, input, devices and focused renderer checks | Mostly push / PR | Linux | Unit, build, browser and renderer-specific contracts; exact filters vary by workflow. |
multi-renderer-ci.yml and emscripten-multi-renderer-ci.yml | Push / PR | Linux / Emscripten | Runtime-selection sets, combination rules and the Emscripten WEBGL2 + WEBGPU build/link check. |
platform-ci.yml | Push / PR (one job manual) | Linux; Windows for the manual job | Platform abstraction and SDL3/Headless/Terminal combinations; the SDL3 window suite on SDL’s x11 driver (private Xvfb) and wayland driver (private headless Weston); an SDL-free Headless + ALSA build; the CNA_ENABLE_SDL matrix; and, on manual dispatch, the SDL3 platform suite built natively with MSVC. |
apple-ci.yml and metal-macos-ci.yml | Push / PR | macOS (macos-26) | macOS SDL_RENDERER portable suites and an .app launch; METAL build and ^Metal tests; iOS device final-link and simulator launch for SDL_RENDERER and METAL. |
Five c-api-*.yml workflows | Push / PR | Linux | ABI baseline, compatibility, coverage, limitations and release-gate checks. No workflow builds the C API library. |
d3d-windows-ci.yml and content-pipeline-windows-ci.yml | Manual only | Windows | Native MSVC DIRECTX11 and Headless content-pipeline suites (the last also runs on pushes to one named branch). |
How to read the matrix
The snapshot has much broader automated coverage than alpha.1, but a workflow is not universal proof:
- The Windows Direct3D, SDL3-on-Windows and content-pipeline lanes remain manual, so their existence is not an automatic merge gate.
- The iOS device lane final-links; the simulator lane launches a smoke app. Neither establishes physical-device or full-game correctness.
- The Emscripten multi-renderer lane covers configuration and linking; the runtime evidence for the web is the published sample gallery and CNA.NET’s headless-Chromium runs.
- Android has source and NDK wiring but no automatic workflow.
- Every Linux job runs on a virtual X server (or a headless Weston) with Mesa’s software drivers, and the macOS jobs on GitHub’s virtual Macs. None provides a physical-GPU or real-desktop run; the physical-hardware evidence comes from recorded campaigns.
Not exercised by any workflow: Android, tvOS, CNA_BUILD_C_API=ON builds, Clang on Linux (except C header compatibility), CNA_ENABLE_VIDEO=OFF on Linux, the full engine + DirectX under Wine/MinGW, and the GPU pixel/oracle corpus (run locally, by hand). Verification & Known Issues covers the wider testing picture, and Graphics Renderers the per-renderer side of it.
Where video is available
Video and VideoPlayer exist in every build and link everywhere, but decoding is available on Linux and macOS only, and only where the optional FFmpeg backend was compiled in: CNA_ENABLE_VIDEO is ON, or AUTO found libavcodec, libavformat, libavutil and libswresample. On Windows, Emscripten, Android and iOS FFmpeg is never built (AUTO falls back silently, ON is a configure error). Without a backend the calls throw System::NotSupportedException at run time — a run-time exception, not a link error. Audio and MediaPlayer song playback do not depend on FFmpeg. See Video Playback.
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 Mac mini M4 qualification, the iOS Simulator probe and the Apple workflows show.
- Configuring CNA per target: routes, toolchains and what a green build proves — How the Linux, Windows (MSVC and MinGW-w64 with Wine), Android, Emscripten and Apple routes configure CNA, run its tests, and what a successful configure, build or test run on each one proves.
- 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.
- The web target: Emscripten build contract, browser loop, storage and renderer evidence — CNA's Emscripten build contract (exception ABI, Asyncify, threads), the Asyncify browser loop, content and IndexedDB-backed save storage in the virtual file system, web networking, and the evidence per browser renderer.
- 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-041: A relative ContentManager root still follows the working directory on the non-SDL3 platforms, and the loose tier cannot see Android packaged assets — ContentManager now roots a relative RootDirectory under TitleLocation and reads .cnb through the platform on Android, but TitleLocation is the working directory on the non-SDL3 platforms and the loose tier still gates on
- CNA-BUG-142: Without a mixer (NULL audio) songs never end: queued copies carry no Duration, so the elapsed-time fallback never fires — LoadSong copies only a Song's file and name, so every queued Song has a zero Duration and, in builds without SOUND_ENABLED, DetectSongEndedByElapsedTime never reports an end: MediaPlayer stays Playing and the queue never
- CNA-BUG-162: The phone notification channel and sender never initialise Winsock, cannot detect INVALID_SOCKET and have no Windows test — modules/phone calls no WSAStartup, compares the unsigned SOCKET with zero and names no Winsock library, and its socket tests are compiled out on WIN32, so on Windows Open and SendAsync work only if something else initial
- 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-192: metal-macos-ci.yml's push path filter names three CMake files that no longer exist and omits the files that now register the Metal tests — The Metal workflow's push filter lists three removed CMake files, while edits to modules/renderers/metal/CMakeLists.txt or its examples/CMakeLists.txt, where the Metal_* CTests are registered, do not trigger it.
- CNA-GAP-054: The terminal window title is stored but never emitted, so SetTitle has no visible effect — TerminalWindow::SetTitle records the title and defers the OSC emission to the session, but the session prologue emits no title sequence and nothing else writes one, so a terminal window title is never shown.
- CNA-GAP-055: TerminalSession does not restore or re-establish the terminal on job-control stop and continue (SIGTSTP/SIGCONT) — The session's restoring signal set covers termination and crash signals but not SIGTSTP/SIGCONT, so a Ctrl-Z that suspends a terminal game leaves the terminal in raw mode, the alternate screen and mouse reporting until t
- CNA-PLAT-016: On Apple GPUs SamplerState.MipMapLevelOfDetailBias is refused by METAL below macOS/iOS 26, in builds against an older SDK and on devices that ignore it, and does not reach the GPU on FNA3D's Metal-backed driver — METAL throws NotSupportedException for a non-zero sampler LOD bias unless the OS and the build SDK are 26 or later and the device honours the bias; FNA3D on Apple hands the bias to SDL's Metal backend, which ignores it.
- CNA-PLAT-017: FNA3D has no occlusion queries on its SDL_GPU driver, the driver FNA3D tries first and the one it runs on Apple — Fna3dRenderer probes queries only on FNA3D's OpenGL and Direct3D 11 drivers; on its SDL_GPU driver (FNA3D's first choice, and its Metal route on Apple) OcclusionQuery is reported unsupported and its constructor refuses,
- CNA-VGAP-008: Android and iOS have no device evidence at this snapshot: no Android workflow, iOS only in the Simulator, and lifecycle and packaged-content loading untested on a device — Android has no preset or workflow; iOS evidence is device final-links plus Simulator launches (a one-frame smoke for SDL_RENDERER and METAL, and on METAL a pixel probe), no physical iPhone or iPad has run CNA, lifecycle
- 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
- CNA-VGAP-020: The hand-written CnaPlatformTests filter misses TerminalPresenterThroughPlatformTest and nine other platform suites, so the platform CI cells never select them — CnaPlatformTests selects suites by hand-written name tokens: TerminalPresenter.* misses TerminalPresenterThroughPlatformTest, and nine more platform suites (key codes, scancodes, filesystem, sensor session, SDL3 device s