Tutorial 80: Cross-Platform Build Guide

CNA — C++ XNA 4.0 reimplementation

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 startTutorial 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 50 renderer identities in total, and the platform gates that decide which are even configurable are covered in Tutorial 72.

Platform Typical renderer Level Notes
Linux OPENGLES3 (default), VULKAN, SDL_RENDERER Primary Development and CI platform, on real hardware. Most renderers are Linux-configurable.
Windows 14 Windows-only renderers, plus the portable ones Supported, thinly tested MSVC workflows and the repository's Wine/DXVK or vkd3d-proton routes are manual in alpha.1; there is no continuous native-Windows or Wine gate.
macOS METAL Supported, with real CI METAL is macOS-gated and has an automatically triggered macos-14 CI job — the only renderer that does.
Android OPENGLES3 Code paths exist, unverified NDK sensor backends and build wiring are present. No CI at all. See Tutorial 82.
Web (Emscripten) WEBGL2 (default), WEBGL1, CANVAS, HTML_DOM, SVG_DOM Supported, with caveats HTML_DOM has real browser CI. No save persistence, no video, and Game must be heap-allocated — see Tutorial 81.
iOS / tvOS Not supported No toolchain file exists. The METAL gate states plainly that iOS and tvOS remain unvalidated.

Prerequisites that catch people out

  • Sibling checkouts, not submodules. Every build needs ../sharp-runtime next to cna/. Because the Linux default renderer is OPENGLES3, a default Linux build also needs ../easy-gl and ../meta-gl. FREEDIRECT additionally needs ../free-direct (which uses ../free-api).
  • FFmpeg is a hard requirement on Linux and macOS. No option disables it — configure fails without libavcodec-dev, libavformat-dev, libavutil-dev and libswresample-dev.
  • Video is absent entirely on Windows, Web and Android. The video translation units are excluded from those builds. The headers still exist, so code calling Video/VideoPlayer compiles and then fails to link.
  • Submodules: git submodule update --init. Non-recursive is correct, and much faster.
  • The C++ framework has no general install/export package. C++ consumers add_subdirectory the CNA tree. The experimental C layer declares a separate package, but its alpha.1 implementation is compile-blocked by the incomplete C renderer map.

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)
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
cmake -S . -B build-wasm \
  -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
  -DCNA_GRAPHICS_RENDERER=WEBGL2

# Android (requires NDK)
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=OPENGLES3

# macOS native
cmake -S . -B build-macos -DCNA_GRAPHICS_RENDERER=METAL

# 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 "Microsoft/Xna/Framework/Game.hpp"

void MyGame::LoadContent() override {
#if defined(_WIN32)
    // Windows-specific: load from AppData
    auto savePath = std::string(getenv("APPDATA")) + "\\MyGame\\";
#elif defined(__EMSCRIPTEN__)
    // WebAssembly: an in-memory virtual filesystem. NOTHING written here
    // survives a page reload — CNA mounts no IDBFS and never calls FS.syncfs.
    // See Tutorial 81 before designing a save system for the web.
    auto savePath = std::string("/saves/");
#elif defined(__ANDROID__)
    // Android: use SDL storage path
    auto savePath = std::string(SDL_GetPrefPath("com.example", "MyGame"));
#else
    // Linux / macOS
    auto savePath = std::string(getenv("HOME")) + "/.local/share/MyGame/";
#endif
    // ... use savePath ...
}

Path separators

CNA's ContentManager accepts both / and \ on all platforms. Internally paths are normalized. Prefer / in your code and avoid hardcoding \. Use SDL_GetBasePath() to find the executable directory at runtime in a platform-neutral way.

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

Alpha.1 separates the target operating system, graphics renderer, host platform and audio I/O. SDL3 remains the default host and audio implementation, but CNA_PLATFORM can select SDL2, HEADLESS or POSIX-only TERMINAL, while CNA_AUDIO_PLATFORM can select SDL2 or NULL. These axes have explicit compatibility rules, so do not infer host behaviour from the renderer name alone. At alpha.1 only the SDL3 audio choice defines SOUND_ENABLED and links the SDL3_mixer-backed XNA playback engine; SDL2 and Null expose lower-level device selections, not equivalent high-level playback.

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)
add_subdirectory(../cna cna-build)

add_executable(MyGame main.cpp)
target_link_libraries(MyGame PRIVATE CNA::CNA)

# Platform-specific link libraries
if(WIN32)
    target_link_libraries(MyGame PRIVATE winmm)
elseif(ANDROID)
    target_link_libraries(MyGame PRIVATE android log)
elseif(EMSCRIPTEN)
    set_target_properties(MyGame PROPERTIES
        SUFFIX ".html"
        LINK_FLAGS "-s USE_SDL=3 -s FULL_ES3=1 --shell-file shell.html"
    )
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;
}