Tutorial 109: macOS on Apple Silicon
What you’ll learn
- Which renderers a macOS build can use:
SDL_GPU(the default, on SDL’s Metal driver),OPENGL33,WEBGPU,FNA3DandSOFTWARE. - How to build and test on an Apple-silicon Mac, and what the Apple floors and dependencies are.
- What each renderer does and does not do there — shaders, occlusion queries, back-buffer density.
- What CNA’s macOS CI checks, what an earlier campaign on a physical Mac mini M4 recorded, and what neither proves at this snapshot.
Before you start — Tutorial 80: Cross-Platform Build Guide covers CNA's per-platform build story; this tutorial is the macOS chapter of it.
CNA has no macOS-only renderer. A macOS build uses the same renderers as Linux, through SDL3’s Cocoa video driver: SDL_GPU — CNA’s modern native path, on SDL’s Metal GPU driver — is the default, and OPENGL33, WEBGPU, FNA3D and SOFTWARE are the alternatives. OPENGLES3 does not apply (macOS has no native OpenGL ES), DIRECTX9 is Windows-only and WEBGL2 browser-only.
The default is not re-qualified on Apple hardware at this snapshot. SDL_GPU is the macOS and iOS default, but CNA’s handoff lists a re-run on Apple hardware as not done; the gating macOS CI leg that runs its smoke set on Metal had not recorded a run at this snapshot, and SDL_GPU has never been built for iOS. SOFTWARE is the renderer the macOS CI job builds and runs. Test your game on a real Mac before you rely on any renderer there.
Apple targets, floors and what is refused
macOS on Apple silicon (arm64) is the Apple target CNA builds and tests natively. Intel and universal macOS builds are configurable but not CI-verified. iOS and iPadOS are experimental (below), and tvOS, watchOS and visionOS are refused at configure time.
Deployment floor. CNA's macOS floor is 13.3 (CNA_MACOS_DEPLOYMENT_TARGET; iOS 16.3), a hard lower bound because CNA and sharp-runtime use Apple’s floating-point std::to_chars. A binary that links Homebrew's FFmpeg starts only on the macOS that FFmpeg was built for, whatever the floor says; build with -DCNA_ENABLE_VIDEO=OFF or an FFmpeg built for the floor if that matters.
iOS/iPadOS. On iOS the Apple allow-list admits exactly two renderers, SOFTWARE and SDL_GPU; any other renderer is a configure error unless the explicitly unsupported -DCNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ON is given. SDL_GPU is allowed but has never been built for iOS. CI final-links a one-frame smoke app with SOFTWARE for device and simulator and launches the simulator build; the device build has never run on a physical iPhone or iPad. An iOS configure needs -DCNA_ENABLE_NET=OFF, because the iOS SDK ships no libcurl.
What SDL does, and where the renderer takes over
The division of labour is worth knowing if you are debugging a window-level problem:
| SDL3 provides | The renderer owns |
|---|---|
The window itself (Cocoa on macOS, UIKit on iOS), events and input — CNA has no Cocoa, AppKit or UIKit backend of its own. With SDL_GPU, SDL also owns the GPU device: its Metal driver creates the Metal view, the command buffers and the swap chain. |
CNA’s mapping of XNA 4.0 onto that API: resources, render targets, state, the stock and PBR effects and presentation. OPENGL33 draws through the GL context the SDL3 platform creates, WEBGPU through wgpu-native, FNA3D through FNA3D’s own driver, and SOFTWARE into its own CPU framebuffer. |
SDL_GPU feeds SDL SPIR-V; on Apple, SDL_shadercross (fetched, with SPIRV-Cross, and on by default there through CNA_SDL_GPU_SHADERCROSS) translates the renderer’s stock shaders for SDL’s Metal driver. On Apple, SDL's Cocoa and UIKit drivers create windows only on the process's main thread, so create and run the game there.
No Retina back buffer. CNA creates the game window without SDL’s high-pixel-density flag, so, by SDL3’s documented behaviour, the back buffer is sized in points and the system scales it to the display — XNA's model of a back buffer sized in window pixels. Retina-resolution rendering is not offered at this snapshot and has not been measured.
Building it
brew install cmake ninja ccache ffmpeg nlohmann-json
# FFmpeg is optional; nlohmann-json is needed by Gamer Services and networking
# Next to cna/: sharp-runtime on its next branch, as on every platform
cmake -S . -B build-macos \
-DCNA_GRAPHICS_RENDERER=SDL_GPU \
-DCNA_BUILD_TESTS=ON \
-DCNA_USE_CCACHE=ON
cmake --build build-macos --parallel
# The configuration macOS CI builds and runs: the CPU renderer
cmake -S . -B build-macos-software -DCNA_GRAPHICS_RENDERER=SOFTWARE -DCNA_BUILD_TESTS=ON
cmake --build build-macos-software --parallel
The repository’s macos preset (cmake --preset macos) is a Release SDL_GPU build with tests and examples on. OPENGL33 additionally needs the easy-gl and meta-gl sibling checkouts (v0.1.1 and v0.4.1); WEBGPU downloads its pinned wgpu-native release, whose macOS package CNA lists as unverified; FNA3D fetches FNA3D and MojoShader. The toolchain must be recent: CNA’s macOS CI uses Xcode 26 on the macos-26 runner, and Xcode 15.4 is too old (its library has no std::jthread). Like every CNA build you need the sharp-runtime sibling checkout beside cna/, on its next branch for this snapshot. Desktop networking needs a libcurl: macOS's own works for TLS but has no WebSocket support, so a build that found it refuses the online relay by name — install Homebrew's curl if you need online play.
What each renderer does on a Mac
Capabilities are answered per renderer, not per platform, so the capability matrix applies on macOS too. The differences that matter most for a Mac game:
| Renderer | Custom ShaderEffect | Occlusion queries | Notes on Apple |
|---|---|---|---|
SDL_GPU (default) | No: compiled out of Apple builds (no runtime GLSL → SPIR-V → MSL path) | No (SDL’s GPU API has none) | Compiled XNA effects with -DCNA_SDL_GPU_COMPILED_EFFECTS=ON; BlendState.MultiSampleMask only all-or-none (a partial mask is refused); CNA recorded in the early-October campaign that SDL’s Metal driver ignores sampler LOD bias |
OPENGL33 | Yes, desktop GLSL | Yes | Runs on Apple’s OpenGL 4.1 core context, which stores 24-bit depth as float32, so a constant DepthBias is exact only for depths in [0.5, 1); compiled effects with CNA_EASYGL_COMPILED_EFFECTS |
WEBGPU | Yes, WGSL | Yes; on wgpu-native’s Metal driver CNA recorded the count as a boolean | wgpu-native drives Metal; its macOS package is unverified at this snapshot; MultiSampleMask ignored there |
FNA3D | No (compiled XNA effects only, always on) | No on FNA3D’s Metal-backed driver | FNA3D chooses its own driver; no float render targets |
SOFTWARE | No (accepted, never executed) | Yes | CPU rasterizer, off-screen; read pixels back with GetBackBufferData(). The renderer macOS CI exercises |
Remember that XNA's default Reach profile still refuses HiDef features (MRT, occlusion queries, float targets) until you request HiDef. As Tutorial 101 explains, branch on capability answers and the renderer capability profile rather than on the renderer name.
Custom shaders on macOS
The default renderer is the one that cannot run a custom ShaderEffect on a Mac: SDL_GPU compiles that path out of Apple builds and reports CustomEffects false. If your game needs its own shaders on macOS, you have two routes:
- Pick a renderer that executes
ShaderEffectsource —OPENGL33(desktop GLSL) orWEBGPU(WGSL) — and supply that dialect (Tutorial 151 shows how to ship one variant per dialect). - Use compiled XNA effects: build with the renderer’s compiled-effects option (
CNA_SDL_GPU_COMPILED_EFFECTS,CNA_EASYGL_COMPILED_EFFECTS,CNA_WEBGPU_COMPILED_EFFECTS,CNA_SOFTWARE_COMPILED_EFFECTS; all defaultOFF) or useFNA3D, where they are always on, and load your game’s own.fxb/XNB effects (Tutorial 128).
#include "CNA/GraphicsCapability.hpp"
using CNA::GraphicsCapability;
void MyGame::LoadContent()
{
auto& device = getGraphicsDeviceProperty();
// False on SDL_GPU in an Apple build: ShaderEffect is compiled out there.
if (device.SupportsCapability(GraphicsCapability::CustomEffects))
gradeEffect_ = std::make_unique<ShaderEffect>(device, kVertexGlsl, kFragmentGlsl);
// Stock effects work on every renderer that draws.
basicEffect_ = std::make_unique<BasicEffect>(device);
}
What macOS CI checks
Apple coverage lives in one automatically triggered workflow, apple-ci.yml, on GitHub's macos-26 runner (plus a Linux job that checks the Apple CMake layer). Its legs claim deliberately unequal things:
| Leg | What it runs | Status at this snapshot |
|---|---|---|
macos-build | A native SOFTWARE build of CnaTests and the platform, window, lifecycle, storage and desktop-OS suites; a separate tree bundles, final-links and launches cna_macos_smoke.app | Gating |
macos-sdlgpu | SDL_GPU on SDL’s Metal driver: device, swap chain, drawing and read-back through a focused smoke set (scripts/ci/run-sdlgpu-smoke.sh), each program required to exit 0 | Gating, but no recorded run yet; a smoke set, not the full SDL_GPU Apple qualification |
ios-build (device, simulator) | Final-links cna_ios_smoke.app with SOFTWARE for both sysroots; the simulator leg installs and launches it for one frame. Two more legs try the same with SDL_GPU | SOFTWARE legs gating; SDL_GPU legs non-gating |
CI evidence here is compile, bundle and bounded smoke evidence. It does not observe on-screen presentation, frame pacing or pixel fidelity of a GPU renderer on a Mac.
What an earlier Mac mini M4 campaign recorded
Before this snapshot, CNA ran a qualification campaign on a physical Mac mini M4 (arm64, 16 GB, macOS 27.0.1, Xcode 27, Apple clang 21) on 7–8 October 2026, recorded task by task in plans/plan_apple_m4.md and summarised in docs/apple-platforms.md. Its full-tree runs for the renderers this snapshot still has, recorded by CNA and not re-run for this page:
| Renderer on the M4 | Full CNA test tree | Recorded limitations |
|---|---|---|
SDL_GPU (SDL’s Metal driver) | 10,962 of 10,962, 295 declining by name | No occlusion queries; SDL’s Metal driver ignores sampler LOD bias |
OPENGL33 (with compiled effects) | 11,916 of 11,916, 128 declining | Float depth storage limits DepthBias precision; an over-cap anisotropy is clamped by the driver |
WEBGPU (wgpu-native on Metal) | 10,968 of 10,968, 279 declining | MultiSampleMask ignored; occlusion counted as a boolean |
SOFTWARE | 11,084 of 11,084, 133 declining | None recorded |
FNA3D (FNA3D’s Metal driver) | 10,875 of 10,875, 433 declining | No occlusion queries; float render-target formats not promoted |
How far that reaches. These are one machine’s results from a campaign that ran before the renderer changes in this snapshot, and CNA has not repeated them since. The runs were made with the console session locked, which makes macOS report every window as occluded: output was verified by reading back pixels, but on-screen presentation and PresentInterval pacing were never observed. No Intel Mac was used, and no physical iOS device ran any renderer. Treat the table as history that explains why these renderers are expected to work on a Mac, not as qualification of this snapshot.
Refusals, not silent degradation
Where a renderer cannot serve a request on a Mac, CNA’s rule is to fail by name rather than render something plausible-but-wrong: an OcclusionQuery constructor on SDL_GPU throws NotSupportedException, so does a partial BlendState.MultiSampleMask on a multisampled SDL_GPU target, and a texture format a renderer does not classify throws NotSupportedException. One exception is worth knowing: SOFTWARE accepts a custom ShaderEffect and silently draws with its fixed stock path.
What remains open
SDL_GPU, the default, has not been re-qualified on Apple hardware, and its gating macOS smoke leg had no recorded run at this snapshot.ShaderEffectis compiled out of AppleSDL_GPUbuilds, andSDL_GPUhas no occlusion queries.- No Retina-resolution back buffer, no safe-area handling on iOS, and no app-icon, asset-catalog or
.ipapackaging. - No current Intel-macOS CI run; x86_64 and universal builds are configured but unverified.
- iOS:
SDL_GPUhas never been built for it; the evidence is a one-frameSOFTWAREsimulator smoke run.
What you can build on today
A 2D or 3D XNA-style game builds and runs on an Apple-silicon Mac with any of the five renderers above; SOFTWARE is the one CI keeps green there, and the GPU renderers carry the earlier campaign’s record. Budget for the evidence gap rather than a feature gap: test your game on a real, unlocked Mac display (presentation and pacing were never observed by CNA), on the macOS versions you intend to ship to, and on Intel Macs if you target them.
Keeping a macOS build in a portable codebase
A macOS build is normally a single-renderer configuration like any other, and nothing in it is Apple-only, so the same source tree builds for Linux, Windows and the web. If you want a fallback on the Mac, compile several renderers into one binary (Tutorial 126) and let run-time selection fall back from SDL_GPU to OPENGL33 or SOFTWARE. Capability queries keep one source tree honest across the configurations:
// Resolve once at startup and cache the answers; do not query per frame.
struct RenderPathPlan
{
bool deferred = false;
bool antialiased = false;
bool occlusionCull = false;
};
RenderPathPlan PlanRenderPath(GraphicsDevice& device)
{
RenderPathPlan plan;
plan.deferred = device.SupportsCapability(GraphicsCapability::MultipleRenderTargets);
plan.antialiased = device.SupportsCapability(GraphicsCapability::MultiSampleAntiAliasing);
plan.occlusionCull = device.SupportsCapability(GraphicsCapability::OcclusionQuery);
return plan; // On SDL_GPU, occlusionCull is false on every platform.
}
Two habits pay off. Log device.GetGraphicsRendererName() at startup — it answers for the renderer the device actually resolved to, so a bug report from a Mac user tells you immediately which renderer produced it. And keep a CPU renderer (SOFTWARE) in your regression matrix as well, so image-level tests also run where no GPU is available.
Where to go next
- Tutorial 131: The SDL_GPU Renderer
- Tutorial 101: Querying Renderer Capabilities
- Tutorial 128: Compiled XNA Effects
- Tutorial 107: CPU-Only Renderers
- Tutorial 72: Choosing a Renderer
- Tutorial 80: Cross-Platform Build Guide
- Platforms reference, Renderers reference and Verification
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.