Support for Apple's two platforms, treated as one build/runtime subsystem below every renderer.
The renderer question is separate and stays in plan_metal.md / docs/metal-renderer.md.
The current contract and validation boundary are maintained in
docs/apple-platforms.md.
-
macOS and iOS are one subsystem, not two. They share a compiler, a sysroot mechanism, a bundle format,
SDL_GetPrefPathsemantics and a lifecycle model. Splitting them would duplicate every one of those and let the two drift apart.cmake/ApplePlatform.cmakeis the single place that knows which Apple target a configure is for. -
iOS support is experimental and every claim names its evidence. The workflow final-links a real application for device and simulator and runs one framework frame in the simulator. That is initialization/runtime smoke evidence, not physical-device, pixel, input, audio, storage or performance evidence. The project's existing habit of separating "compiles" from "works" is what this follows.
-
An allow-list, not a deny-list, for iOS renderers. CNA has 46 renderer identities and almost all of them are impossible on iOS. Enumerating the impossible ones would rot the moment a renderer is added; enumerating what CNA actually wires up cannot. A renderer outside the list fails at configure time with a message naming the platform, instead of failing deep inside a third-party dependency that was never configured for an iOS sysroot.
-
One repository-owned bundle sweep instead of ~200 edited example registrations. iOS products must be
.appbundles with anInfo.plist. The module-local example/tool/test registrations know nothing about Apple and should not have to. The top-levelCMakeLists.txtwalks the finished CNA buildsystem once and hands every CNA-owned executable its bundle configuration. A downstream target created outside CNA's source tree callscna_apple_configure_bundle()explicitly; the helper resolves plist paths relative to CNA, not the outer source tree. -
macOS keeps plain executables. Every example, tool and ctest binary in this repository is invoked by path; a
.applayout would break all of them. Bundling on macOS is available behindCNA_APPLE_BUNDLE_MACOS_EXECUTABLES=ONfor shipping a real application. The opt-in bundle copies non-system dylibs intoContents/Frameworksand fixes their install names. -
The mobile lifecycle deviates from FNA deliberately. FNA tracks only
IsActiveon the background/foreground events. iOS terminates an application that submits GPU work after entering the background, so on mobile the loop has to actually stop. The deviation is guarded by the compile-timeCNA::isMobilePlatform(), so desktop behavior is byte-identical to before, and it is documented at the site inGame.cppas the checklist requires. -
The platform layer is testable without a Mac. Every line of the Apple CMake code is behind
if(APPLE)and therefore unreachable to most of this project's developers and to every Linux CI job.scripts/check-apple-platform-cmake.shparses it in cmake script mode and exercises the allow-list decisions, so the layer has a gate that runs on every push rather than only when someone happens to configure on macOS. -
All SDL components are linked statically on iOS. SDL3, SDL3_image and SDL3_mixer each have independent shared/static switches. A dylib inside an
.appneedsFrameworks/embedding, an@rpathinstall name and separate signing, so all three are forced static and CI rejects a final app with a dynamic SDL dependency. -
Deployment floors follow the libraries actually used. Floating-point
std::to_charsin CNA/sharp-runtime is unavailable in Apple libc++ before macOS 13.3 and iOS 16.3. Those versions are hard floors; suppressing availability diagnostics would create older binaries with missing runtime symbols.
| ID | Task | Status |
|---|---|---|
APPLE-1 |
cmake/ApplePlatform.cmake: Apple target detection (macOS / iOS device / iOS simulator), deployment-target defaults, bundle identity options, rejection of tvOS/watchOS/visionOS |
Done |
APPLE-2 |
cmake/toolchains/ios.cmake: device and simulator toolchain, architecture selection, re-rooted find rules |
Done |
APPLE-3 |
.app bundle generation: downstream-safe helper, CNA-owned target sweep, plist templates, macOS dylib embedding/fixup plus bundled-app launch/dependency CI |
Done |
APPLE-4 |
Conservative iOS renderer allow-list (SDL_RENDERER only) + CNA_APPLE_ALLOW_UNVALIDATED_RENDERER escape hatch |
Done |
APPLE-5 |
Platform-conditional build surface: deployment/architecture/sysroot-keyed SDL cache, static SDL3/image/mixer, Apple toolchain propagation, FFmpeg off on iOS, multi-process tests excluded | Done |
APPLE-6 |
CNA/TargetPlatform.hpp: CNA_TARGET_APPLE/_MACOS/_IOS macros, isApplePlatform(), isMobilePlatform(), getCurrentPlatformName() |
Done |
APPLE-7 |
Mobile application lifecycle in Game: suspend/resume timing, termination/low-memory handling, focused event-state test |
Done |
APPLE-8 |
CNA/Platform/Entrypoint.hpp: pull in <SDL3/SDL_main.h> on iOS so UIKit owns the process |
Done |
APPLE-9 |
.github/workflows/apple-ci.yml: host-portable checks, macOS build/tests, final-linked iOS device/simulator app validation, simulator install/launch |
Done — run 31736845749 |
APPLE-9a |
scripts/check-apple-platform-cmake.sh: exercises the Apple CMake layer from any host, since every line of it is behind if(APPLE) and unreachable otherwise |
Done — passes on Linux |
APPLE-10 |
modules/core/tests/CNA/TargetPlatformTests.cpp: platform-helper coverage, including the macOS/iOS-specific expectations |
Done |
APPLE-11 |
METAL on iOS: refused by default, configurable through CNA_APPLE_ALLOW_UNVALIDATED_RENDERER |
Done (gate only) |
APPLE-12 |
Storage-root fallback follows the Apple convention instead of the Linux XDG layout | Done |
APPLE-13 |
Minimal CNA/Platform/Entrypoint + Game::RunOneFrame() app is final-linked for both Apple platforms and launched from macOS bundle/simulator CI |
Done — run 31736845749 |
APPLE-15 |
Initial orientation set is seeded before the video subsystem starts; later SupportedOrientations changes reach the OS through IPlatformWindow::SetSupportedOrientations and invalidate UIKit's cached answer |
Done |
APPLE-20 |
Enforce macOS 13.3 / iOS 16.3 libc++ availability floors and request only CNA's sharp-runtime component closure | Done |
next introduced the CNA-owned platform contract (plan_platform.md), which decoupled
modules/core, modules/runtime, modules/graphics and modules/storage from SDL entirely and
put a hard 0/0 SDL ratchet on production sources. Several APPLE rows were written against the
pre-contract layout and were re-stated rather than merged verbatim:
| Was | Now |
|---|---|
CNA/Platform.hpp, CNA::Platform enum |
CNA/TargetPlatform.hpp, CNA::TargetPlatform (CNA::Platform is now a namespace) |
CNA_PLATFORM_APPLE/_MACOS/_IOS |
CNA_TARGET_APPLE/_MACOS/_IOS, so the target-OS axis stops colliding with CNA_PLATFORM_<IMPL> |
CNA/Entrypoint.hpp (modules/core) |
CNA/Platform/Entrypoint.hpp (modules/platform), keyed on CNA_PLATFORM_SDL3 + CNA_TARGET_IOS |
modules/runtime/src/AppleOrientation.mm |
modules/platform/src/Sdl3/Sdl3AppleOrientation.mm |
Orientation hint written from GameWindow/GraphicsDevice |
IPlatformWindow::SetSupportedOrientations + seeding in Sdl3Platform's constructor |
SDL lifecycle constants in Game::PollEvents |
CNA::Platform::AppLifecycleEvent; AppLifecycleKind::Terminating added to the contract |
SDL_WaitEventTimeout in the suspended loop |
IPlatform::Delay + PollEvents (the contract has no blocking wait, by design) |
Tests injecting SDL_PushEvent / creating SDL_Window |
Scripted PlatformEvents via PlatformTestDecorator, and the Headless platform |
Two behavioural notes from the re-statement:
- Suspension is now driven by
WillEnterBackground/DidEnterForegroundrather than the tighterDidEnterBackground/WillEnterForegroundpair, because those are the transitions the platform contract exposes. It suspends marginally earlier and resumes marginally later, which is the safe direction on both iOS and Android. - The suspended wait polls every 16 ms instead of blocking up to 250 ms, so resume latency is bounded by a frame rather than by the old timeout.
| Claim | Evidence |
|---|---|
| The Apple CMake layer parses and its policies stay connected | Linux smoke check: renderer gate/override, non-Apple no-ops, deployment floors, all static SDL switches, bundle fixup and orientation bridge |
| Non-Apple configures are unaffected | Re-verified after the next merge on a Linux FNA3D build (cmake-build-next-fna3d, Xvfb + llvmpipe): TargetPlatformTest.* / GameWindowTest.* / GameTest.* / Storage* / *DesktopOS* / *PlatformEvent* / *Sdl3EventMapper* all pass, and the full corpus runs 6774 tests with 6754 passing. The 3 failures are the documented environmental set (PollEventsClearsStaleCallerContent/SDL3 passes standalone; the two GameEventSemanticsGoldenTest Headless/Terminal cases need a renderer that can build a windowless device, which FNA3D refuses). The SDL3 parameterization of that golden test passes, which is what shows the lifecycle re-statement preserves the captured event semantics. |
| macOS builds and its platform contracts hold | Green macos-build in run 31736845749: CnaTests, focused suites, self-contained bundle verification and launch |
| CNA final-links an iOS device application | Green ios-build/device in the same run: Mach-O platform, plist, entry-point symbols and dependency-closure checks |
| A minimal CNA application runs in the iOS simulator | Green ios-build/simulator in the same run: install, launch and CNA_APPLE_SMOKE_OK after one Game frame |
| A CNA application runs on physical iPhone/iPad hardware | None. Not attempted. |
These are unstarted, not attempted-and-failed:
- Run a representative content-bearing CNA game/example for multiple frames in the iOS simulator; the current smoke app deliberately exits after one empty framework frame.
APPLE-14— exercise touch on iOS. The path already exists and is platform-neutral (SdlInputBridgetranslates SDL finger events intoTouchPanel/GestureDetector, andGraphicsDevicekeeps the display metrics in sync), so this is verification work, not implementation work — but until it runs, iOS input is unobserved and iOS has no keyboard/mouse fallback to hide behind.APPLE-16— app icons / asset catalog,.ipapackaging, signing documentation.APPLE-17— decide whetherMETALbecomes the supported iOS renderer, which requires the Metal renderer's own macOS contract gaps (plan_metal.md) to be closed first.APPLE-18— Mac Catalyst / tvOS, if ever wanted. Currently rejected at configure time on purpose, so that nothing pretends to target them.APPLE-19— safe-area insets. The notch and the home indicator are not exposed to the game, so full-screen UI can sit underneath them.SDL_GetWindowSafeAreais the obvious source.