Tutorial 80: Cross-Platform Build Guide
What you’ll learn
- Which targets CNA supports, at what level, and what each needs from the build.
- CMake toolchain files, and guarding platform code with
#ifdef. - Path separators and case-sensitive file systems — the two portability bugs that bite first.
- Running OpenGL ES on the desktop through Mesa.
Before you start — Tutorial 20: Building and Running Your Game — this generalises that single-platform build to the rest of the matrix.
Platform matrix
These are support levels as they actually stand, not aspirations. The renderer column names one sensible choice per platform — CNA has 14 renderer identities in total, and the platform gates that decide which are even configurable are covered in Tutorial 72. Windows-only renderers: DIRECTX9 and DIRECTX11. Emscripten-only: WEBGL2. Apple-only: METAL (macOS and iOS). On iOS the allow-list is SDL_RENDERER and METAL. The default is WEBGL2 under Emscripten, OPENGLES3 on Linux and SDL_RENDERER everywhere else. The rows below are the target operating system; the platform-implementation and audio axes follow in Platform and audio layers.
| Platform | Typical renderer | Level | Notes |
|---|---|---|---|
| Linux | OPENGLES3 (default), VULKAN, SDL_RENDERER, OPENGL33, SOFTWARE, and the rest |
Primary | Development and CI platform. Every renderer except the four gated to Windows, Emscripten or Apple is Linux-configurable. Platform layers: SDL3 (default; X11 and Wayland sessions through SDL’s video drivers), TERMINAL, HEADLESS. CI runs under Xvfb with Mesa software drivers, so the GPU pixel matrix is not an automatic gate. |
| Windows | SDL_RENDERER (default); the two Windows-only renderers; the portable ones |
Supported, thinly tested | The native MSVC workflows (Direct3D 11, the SDL3 platform on Windows, and the content pipeline) do not run on ordinary pushes, and no automatic workflow builds for Windows. Windowing, input, IME and gamepads come from SDL3; a build without SDL has no window, and its audio must be NULL. Video is never built. |
| macOS | SDL_RENDERER (default), METAL |
Supported, build workflows configured | apple-ci builds the SDL_RENDERER configuration on macos-26 automatically, runs the portable suites and launches a self-contained .app bundle; metal-macos-ci builds METAL and runs its ^Metal tests (see Platform Support). Deployment floor macOS 13.3. Windowing comes from the SDL3 platform layer. |
| Android | SDL_RENDERER (default) |
Code paths exist, unverified | NDK sensor backends and build wiring are present, plus one Gradle demo project. No CI and no CMake preset. See Tutorial 82. |
| Web (Emscripten) | WEBGL2 (default), WEBGPU (experimental) |
Supported, with caveats | A multi-renderer build/link workflow covers WEBGL2;WEBGPU and does not run the bundle in a browser (see Platform Support). Saves persist through IndexedDB (a threaded build needs -DCNA_EMSCRIPTEN_USE_WASMFS=OFF); no video — see Tutorial 81. A stack-allocated Game is fine in this snapshot. |
| iOS | SDL_RENDERER and METAL (the two allowed) |
Experimental | cmake/toolchains/ios.cmake and the ios and ios-simulator presets exist. The Apple workflow final-links device apps and installs and launches a one-frame simulator smoke app, for both renderers; that says nothing about physical devices, input, audio or pixels. Deployment floor iOS 16.3. tvOS is unsupported. |
Prerequisites that catch people out
- Sibling checkouts, not submodules. Every build needs
../sharp-runtimenext tocna/, on itsapple/m4-stabilizationbranch, and CNA itself must be itsapple/m4-stabilizationbranch (git clone -b apple/m4-stabilization); the GitHub default branches are older. Because the Linux default renderer isOPENGLES3, a default Linux build also needs../easy-gl(develop) and../meta-gl(apple/m4-stabilization) — as does any of the three GL identities. - FFmpeg is optional.
CNA_ENABLE_VIDEOdefaults toAUTO: FFmpeg video is built only when its four development packages are found. It is never built for Windows, Emscripten, Android or iOS.VideoandVideoPlayerstill link everywhere; without a backend they throwNotSupportedExceptionwhen used, and theCNA_VIDEO_AVAILABLEcompile definition tells you at build time. - Development packages before the first configure. The vendored SDL3 is built at first configure with whichever X11, Wayland, OpenGL and audio development packages exist at that moment, and caches the result in
.sdl-prebuilt-*(see Tutorial 02 for the package list). - Submodules:
git submodule update --initfetches five (SDL, SDL_image, SDL_mixer, draco, googletest). Non-recursive is correct, and much faster. - The C++ framework has no general install/export package. C++ consumers
add_subdirectorythe CNA tree. The experimental C layer (CNA_BUILD_C_API=ON, which requiresCNA_ENABLE_NET=ON) declares a separate package, but no CI job builds it, so it is not a shipping path.
CMake toolchains
CNA uses a standard CMake build with optional toolchain files for cross-compilation. Select the appropriate
toolchain file for your target and pass -DCNA_GRAPHICS_RENDERER=OPENGLES3 (or VULKAN
/ SDL_RENDERER) to choose the renderer. The equivalent single-option form
-DCNA_RENDERER_OPENGLES3=ON also works — use one or the other, never both.
# Linux native (default) -- clone CNA and sharp-runtime with -b apple/m4-stabilization
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGLES3
# Windows cross from Linux using MinGW
cmake -S . -B build-win \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
-DCNA_GRAPHICS_RENDERER=DIRECTX11
# WebAssembly via Emscripten (CI uses emsdk 6.0.3)
emcmake cmake -S . -B build-wasm \
-DCNA_GRAPHICS_RENDERER=WEBGL2 \
-DCMAKE_BUILD_TYPE=Release
# (or pass -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake)
# Android (requires NDK; the tree's only Android project pins NDK 30, API 24, arm64-v8a)
cmake -S . -B build-android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-24 \
-DCNA_GRAPHICS_RENDERER=SDL_RENDERER
# macOS native (SDL_RENDERER; METAL is the macOS-only native renderer)
cmake --preset macos
# ... or: cmake -S . -B build-macos -DCNA_GRAPHICS_RENDERER=METAL
# iOS (macOS host with Xcode): device and simulator presets, SDL_RENDERER only
cmake --preset ios
cmake --preset ios-simulator
# Build the whole directory, or a real target such as CnaTests --
# "--target CNA" no longer works: CNA is an INTERFACE library with no sources.
cmake --build build
Conditional platform code with #ifdef
#include <cstdlib>
#include <string>
// Where a hand-rolled save directory would live on each target. Prefer
// StorageDevice/StorageContainer (Tutorial 19): CNA resolves the per-user root
// for you, the same way on every windowing platform.
std::string SaveDirectory()
{
#if defined(_WIN32)
// Windows: %LOCALAPPDATA%
const char* base = std::getenv("LOCALAPPDATA");
return std::string(base ? base : ".") + "\\MyGame\\";
#elif defined(__EMSCRIPTEN__)
// WebAssembly: CNA mounts IndexedDB-backed storage for StorageDevice
// (/cna-storage) and IsolatedStorage (/save) and restores it before main().
// A hand-built path outside those mounts is in memory only and does not
// survive a page reload. Threaded builds need CNA_EMSCRIPTEN_USE_WASMFS=OFF.
return ""; // use StorageDevice instead of a hand-built path
#elif defined(__ANDROID__)
// Android: HOME is not defined and the working directory is not writable.
// StorageDevice asks the platform for the app's private files directory.
return ""; // use StorageDevice instead of a hand-built path
#elif defined(__APPLE__)
const char* home = std::getenv("HOME");
return std::string(home ? home : ".") + "/Library/Application Support/MyGame/";
#else
// Linux and other Unix: ~/.local/share (StorageDevice also honours $XDG_DATA_HOME)
const char* home = std::getenv("HOME");
return std::string(home ? home : ".") + "/.local/share/MyGame/";
#endif
}
Path separators
CNA's ContentManager accepts both / and \ on all platforms. Internally
paths are normalized. Prefer / in your code and avoid hardcoding \. To find the executable directory at
runtime in a platform-neutral way, ask your platform layer (with the default SDL3 platform that is SDL_GetBasePath()); a windowless build configured with CNA_ENABLE_SDL=OFF has no SDL, so do not depend on it in game code that must build there.
File system case sensitivity
Linux file systems are case-sensitive; Windows NTFS is case-insensitive by default. Always use consistent
casing for asset filenames (prefer all lowercase). A common portability bug is naming a file
Assets/Textures/Logo.png on Windows but referencing it as assets/textures/logo.png
on Linux — the Windows build loads it fine while the Linux build silently fails.
OpenGL ES on desktop (Mesa)
CNA's OPENGLES3 renderer targets OpenGL ES 3.0, which runs on desktop Linux via Mesa's GLES implementation.
No special configuration is needed; Mesa exposes GLES through the same EGL/GLX path used for desktop OpenGL.
# Verify GLES support:
glxinfo | grep "OpenGL ES"
# Expected: OpenGL ES profile version string: OpenGL ES 3.2 Mesa ...
Platform and audio layers
CNA separates the target operating system, the graphics renderer, the host-platform implementation
(CNA_PLATFORM) and the audio implementation (CNA_AUDIO_PLATFORM). SDL3 is the default
for both and CNA’s one windowing implementation (it reaches Windows, X11, Wayland, macOS, iOS, Android and the browser through SDL’s own video drivers); the headless and terminal platforms have no window, and a native
ALSA audio backend has CNA’s own mixer. These axes have explicit compatibility rules, so do not infer host
behaviour from the renderer name alone.
| Option | Values | Offered when |
|---|---|---|
CNA_PLATFORM | SDL3 (default), HEADLESS | always |
TERMINAL | non-Windows targets; CPU and no-output renderers only (SOFTWARE, HEADLESS, STUB) | |
CNA_AUDIO_PLATFORM | SDL3 (default), NULL | always |
ALSA | Linux only | |
CNA_ENABLE_SDL | AUTO (default), ON, OFF | OFF configures no SDL and refuses the SDL3 platform, SDL3 audio and the three SDL-linking renderers (SDL_RENDERER, SDL_GPU, FNA3D) |
SDL12 and EMSCRIPTEN are reserved platform values and OPENAL and WASAPI are
reserved audio values; all four fail configuration rather than falling back. Only the SDL3 and ALSA audio values define SOUND_ENABLED and provide a mixer for the
XNA playback classes; NULL exposes a lower-level device selection, not equivalent high-level playback.
Windows and macOS have no SDL-free audio path, so an SDL-free (windowless) build there uses NULL audio.
The Platforms reference has the full matrices, the
Windows, X11 and Wayland page covers how SDL3 reaches each window system and what an SDL-free build is, and
Tutorial 127 walks the selection rules.
Two recipes that CNA’s own CI configures and runs (see Tutorial 138 for the SDL-free walkthrough):
# No SDL at all: windowless, ALSA audio through CNA's own mixer, no-output renderer
cmake -S . -B build-nosdl -DCMAKE_BUILD_TYPE=Debug \
-DCNA_ENABLE_SDL=OFF -DCNA_PLATFORM=HEADLESS \
-DCNA_AUDIO_PLATFORM=ALSA -DCNA_GRAPHICS_RENDERER=HEADLESS
# POSIX terminal: the SOFTWARE rasterizer presents frames to a TTY
cmake -S . -B build-term -DCMAKE_BUILD_TYPE=Debug \
-DCNA_PLATFORM=TERMINAL -DCNA_AUDIO_PLATFORM=NULL -DCNA_GRAPHICS_RENDERER=SOFTWARE
CMakeLists.txt with platform detection
cmake_minimum_required(VERSION 3.20)
project(MyGame CXX)
set(CMAKE_CXX_STANDARD 23)
# Add the CNA source checkout (the C++ framework has no general installed package),
# without CNA's own tests and demos
set(CNA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(../cna cna-build)
add_executable(MyGame main.cpp)
target_link_libraries(MyGame PRIVATE CNA) # the target is CNA; there is no CNA::CNA alias
# Platform-specific finishing touches. The Android system libraries (android, log)
# and MinGW's winmm arrive transitively through CNA; nothing to add here for them.
if(WIN32)
# The vendored SDL3 is a shared library on Windows: copy the DLLs beside the executable
if(COMMAND cna_copy_sdl_runtime)
cna_copy_sdl_runtime(MyGame)
endif()
elseif(EMSCRIPTEN)
set_target_properties(MyGame PROPERTIES SUFFIX ".html")
# Blocking Game::Run() needs Asyncify (-sASYNCIFY=1)
target_link_libraries(MyGame PRIVATE CNA::EmscriptenAsyncify)
target_link_options(MyGame PRIVATE -sALLOW_MEMORY_GROWTH=1
"SHELL:--shell-file ${CMAKE_SOURCE_DIR}/shell.html")
cna_apply_emscripten_renderer_link_contract(MyGame) # WebGL version flags for WEBGL2
endif()
# Detect 64-bit build
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
message(STATUS "Building 64-bit")
else()
message(STATUS "Building 32-bit")
endif()
Cross-platform file path helper
#include <string>
#include <algorithm>
// Normalize path separators to forward slash
std::string NormalizePath(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
return path;
}
// Join two path segments safely
std::string JoinPath(const std::string& base, const std::string& rel) {
if (base.empty()) return rel;
std::string result = NormalizePath(base);
if (result.back() != '/') result += '/';
result += NormalizePath(rel);
return result;
}