Skip to content

Make games run on watchOS (FFI stubs, string FFI, file reading, 2D camera)#60

Merged
proggeramlug merged 2 commits into
mainfrom
fix/watchos-game-support
Jun 8, 2026
Merged

Make games run on watchOS (FFI stubs, string FFI, file reading, 2D camera)#60
proggeramlug merged 2 commits into
mainfrom
fix/watchos-game-support

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Brings a real 2D game (Bloom Jump) up on the watchOS simulator — title screen and gameplay (player, ground tiles, coins, HUD) rendering through the SwiftUI-Canvas path. Five issues stood between "compiles" and "plays":

# Symptom Fix
1 Link fails: undefined _bloom_add_post_pass, _bloom_set_wind, … gen_stubs.js threw on string params so ffi_stubs.rs went stale — map stringi64, add two real impls to OVERRIDES, regenerate
2 Link fails: 4 more undefined _bloom_* bloom_set_material_params / splat_impulse / profiler_* are declared in core/index.ts but missing from the manifest — hand-written stubs in ffi_stubs_manual.rs
3 Text truncates ("BLOOM JUMP" → "BLOOM"); levels never load StringHeader was 16 bytes; Perry 0.5.x's is 20 (missing flags). perry_str read every string 4 bytes early. Add flags
4 Entering a level crashes (SIGSEGV at …fff8) bloom_read_file returned null; implement it (resolve bundle path, read, return a real StringHeader via alloc_perry_string, empty-never-null on miss)
5 Gameplay shows only sky+HUD, no tiles bloom_begin/end_mode_2d were no-ops, so world draws used raw coords. Emit BEGIN_2D/END_2D markers; the Canvas applies the camera's affine transform

Also drops the watchOS metal_sources post-fx shader — SCNTechnique/SCNRenderer is absent from the watchOS SDK so it can't run, and compiling it requires the separately-downloadable Metal Toolchain. The app renders fine without the metallib.

Validation

Built for watchos-simulator (--features watchos-swift-app) and ran on an Apple Watch Series 11 (watchOS 26) simulator:

  • Title screen: full text, correct layout.
  • Gameplay: player on grass/dirt tiles, coins, parallax, HUD — framed correctly.

Notes

  • The 4 manifest-missing functions (issue Per-mesh velocity buffer + motion blur #2) are stubbed locally rather than added to package.json's nativeLibrary.functions, because Perry validates that manifest cross-platform and this change was only validated on watchOS. Adding them to the manifest (so gen_stubs.js emits them) is a reasonable follow-up for someone who can test the other targets.
  • Requires a companion Perry fix for --features watchos-swift-app link reliability (the _main → __perry_user_main entry-object rename) — submitted to PerryTS/perry separately.

gen_stubs.js threw on `string` params (rustType only knew f64/i64/void), so
it crashed before writing src/ffi_stubs.rs and the checked-in file went stale —
a game linking against the watchOS lib failed with undefined `_bloom_*`
(post-pass, ssao, wind, …). Map `string` -> i64 (Perry passes string pointers
in an integer register) for both params and returns, and add bloom_get_language
/ bloom_set_direct_2d_mode to OVERRIDES (real impls in lib.rs that regen would
otherwise duplicate). Regenerate.

Four functions (bloom_set_material_params, bloom_splat_impulse,
bloom_profiler_frame_history, bloom_profiler_overlay_text) are declared in the
engine's core/index.ts but absent from package.json's nativeLibrary.functions,
so gen_stubs can't see them — add hand-written no-op stubs in
src/ffi_stubs_manual.rs (perry validates the manifest cross-platform, so the
manifest gap is left for a separate change).
Three runtime gaps that kept games from actually rendering on watchOS:

- StringHeader was 16 bytes (4×u32), but Perry 0.5.x's canonical header is 20
  bytes (5×u32 incl `flags`, data at +20). `perry_str` read every incoming
  string 4 bytes early — text rendered with a 4-null prefix + truncated tail
  ("BLOOM JUMP" -> "BLOOM") and file paths came back corrupted so levels
  never loaded. Add the missing `flags` field.

- bloom_read_file was a stub returning 0 (null); the game's inline `.length`
  then dereferenced null and crashed. Implement it: resolve the bundle-relative
  path, read the file, and return a real StringHeader via a new
  alloc_perry_string (empty string on a miss, never null).

- bloom_begin_mode_2d / bloom_end_mode_2d were no-ops, so world-space draws
  (tiles, sprites) rendered at raw coordinates off-screen while the screen-space
  HUD showed. Emit BEGIN_2D/END_2D marker commands carrying the camera; the
  SwiftUI Canvas applies the matching affine transform (world -> screen) to
  every command in between.

Also drop the watchOS `metal_sources` post-fx shader: SCNTechnique/SCNRenderer
is absent from the watchOS SDK so it can't run, and compiling it needs the
separately-downloadable Metal Toolchain. The app renders fine without the
metallib.
@proggeramlug proggeramlug merged commit bbd0179 into main Jun 8, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant