From ceacbc00dd1c5aad02c3eb326f1f3080d755bf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 13 Jun 2026 09:38:34 +0200 Subject: [PATCH] docs: add watchOS target guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit watchOS is the one platform without Metal/wgpu — the engine emits a draw list that a SwiftUI Canvas rasterizes. Document the architecture, the watchos-swift-app build path and three arches (sim / arm64 S9+ / arm64_32 S4-8), the watchOS 10.0 deployment floor (onChange API), Crown + tap input, the 2D camera marker scheme, localization, and limitations (no Metal post-fx, no 3D, sim can't run device-arch builds). Mirrors the existing web-target.md; linked from the README Platforms table. --- README.md | 1 + docs/watchos-target.md | 130 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 docs/watchos-target.md diff --git a/README.md b/README.md index 21afeae..d911748 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ setting the bar. ([full design rationale](docs/design-api.md)) | Linux | Vulkan / OpenGL | Keyboard + mouse | | iOS | Metal | Touch + gamepad | | tvOS | Metal | Siri Remote + gamepad | +| watchOS | SwiftUI Canvas (no Metal) | Digital Crown + taps ([docs](docs/watchos-target.md)) | | Android | Vulkan / OpenGL ES | Touch + gamepad | | **Web** | **WebGPU / WebGL** | **Keyboard + mouse + touch + gamepad** | diff --git a/docs/watchos-target.md b/docs/watchos-target.md new file mode 100644 index 0000000..a93dc04 --- /dev/null +++ b/docs/watchos-target.md @@ -0,0 +1,130 @@ +# watchOS Target + +Bloom games can run on Apple Watch. Unlike every other Bloom platform, watchOS +has **no Metal/wgpu** available to third-party apps, so the watch target does not +use the wgpu renderer at all. Instead the engine emits a **draw list** that a +SwiftUI `Canvas` rasterizes — the game's imperative draw calls work unchanged. + +## Architecture + +``` +Game.ts ─(perry --target watchos --features watchos-swift-app)─┐ + │ FFI + ▼ + bloom_draw_*() calls ──> draw-command list (Rust, native/watchos) + │ + │ snapshot per frame + ▼ + BloomWatchApp.swift ── SwiftUI Canvas rasterizes the list + │ + ▼ + Apple Watch: Canvas + Digital Crown + taps +``` + +`native/watchos/` is a small crate (no wgpu, no Jolt) that turns `bloom_draw_rect`, +`bloom_draw_texture`, text, etc. into a flat draw-command buffer. `BloomWatchApp.swift` +owns the `@main struct App: App`, spawns the game on a background thread, and on +each frame copies the latest draw list and replays it into a SwiftUI `Canvas`. + +## Building + +watchOS builds go through Perry. The engine's watch crate (`native/watchos`) and +Perry's runtime are tier-3 Rust targets built with nightly `-Z build-std`. See +the Perry [watchOS platform docs](../../perry/perry/docs/src/platforms/watchos.md) +for the full toolchain setup; the engine-specific parts are: + +- Compile the game with **`--features watchos-swift-app`** so the engine's + `@main` SwiftUI shell is the process entry (not Perry's default UI-tree shell). +- Three architectures: `watchos-simulator` (arm64 sim), `watchos` (arm64, + Series 9+), and arm64_32 (Series 4–8/SE, via `PERRY_WATCHOS_ARM64_32=1`). A fat + `lipo` of the latter two covers every watch in one App Store build. + +```bash +# engine watch crate for the simulator +cargo +nightly build -Z build-std=std,panic_abort --release \ + --target aarch64-apple-watchos-sim # (native/watchos) + +PERRY_RUNTIME_DIR=/target/aarch64-apple-watchos-sim/release \ + perry compile main.ts -o game --target watchos-simulator --features watchos-swift-app +``` + +> **Deployment-target floor: watchOS 10.0.** `BloomWatchApp.swift` uses +> SwiftUI's `onChange(of:initial:)`, which is watchOS 10+. Builds below 10.0 +> fail to compile the Swift shell, so `PERRY_WATCHOS_MIN` cannot go lower than +> `10.0`. + +## Game Loop + +The watch shell drives frames; the game's `runGame()` callback works as on every +other platform: + +```typescript +runGame((dt) => { + clearBackground(Colors.SKYBLUE); + drawRect(playerX, playerY, 16, 16, Colors.RED); +}); +``` + +The blocking `while (!windowShouldClose())` loop is not used on watchOS — the +SwiftUI shell owns the run loop and calls into the game thread. + +## Input + +watchOS has no keyboard or pointer. Two input sources are bridged: + +```typescript +const turn = getCrownRotation(); // Digital Crown delta (radians) since last call +const touches = getTouchCount(); // taps on the watch face +``` + +- **Digital Crown** — Swift's `.digitalCrownRotation` reports a delta each frame + via `bloom_watchos_crown_delta`; read it with `getCrownRotation()`. Reading + consumes the accumulator. +- **Taps** — surfaced through the same touch API as iOS (`getTouchCount()` / + `getTouchX/Y()`), so `isWatch()` branches can treat any tap as e.g. "jump". + +Use `isWatch()` (or `getPlatform() === Platform.WATCH`) to gate watch input. + +## 2D Camera + +`beginMode2D()` / `endMode2D()` are supported: the engine emits `BEGIN_2D` / +`END_2D` marker commands carrying the camera offset, target, and zoom, and the +SwiftUI Canvas applies the matching `CGAffineTransform` while replaying the draw +list between the markers. This lets a side-scroller frame the world correctly on +the small screen — set the zoom so a sensible number of tiles fit the watch +width. + +## Assets + +Asset files ship inside the `.app` bundle; the native layer resolves relative +paths against the bundle resource path. Textures, sounds, fonts, and level/text +files via `readFile` all work. Audio uses a watchOS-native mixer +(`BloomWatchAudio.swift`). + +## Localization + +The user's language is reported from Swift at launch (`Locale.preferredLanguages`) +through `bloom_watchos_set_language`, so `getLanguage()` returns the real device +language and the game's i18n works as on other platforms. (Before engine #63 this +was hardcoded to English.) + +## Limitations + +- **No Metal post-processing** — the `bloom_postfx.metal` chromatic-aberration / + film-grain / sun-shaft pass is unavailable (SCNTechnique/SCNRenderer absent + from the watchOS SDK). Games run fine without it. +- **No 3D** — the Canvas rasterizer is 2D only; the wgpu/Jolt paths are not built. +- **Small screen & RAM** — design for 40–49 mm faces and keep memory modest. +- **Simulator can't run device-arch builds** — the sim is arm64; an arm64_32 + build only runs on real pre-S9 hardware. + +## How It Works + +The watch crate (`native/watchos/src/lib.rs`) keeps a `WatchState` with the +draw-command buffer, input accumulators, screen size, and the reported language. +`bloom_draw_*` append commands; `bloom_watchos_copy_draw_list` snapshots them for +Swift each frame. Strings cross the FFI boundary using Perry's 20-byte +`StringHeader` layout (both incoming args and returned strings — `read_file` +returns paths/level data this way). Because the renderer is just a draw-list +replayer, the same game code that targets desktop and mobile runs on the watch +unchanged.