Skip to content

Commit bbd0179

Browse files
authored
Make games run on watchOS (FFI stubs, string FFI, file reading, 2D camera) (#60)
* fix(watchos): generate the full FFI stub set so games link 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). * fix(watchos): correct string FFI, implement file reading + 2D camera 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.
1 parent e941577 commit bbd0179

7 files changed

Lines changed: 440 additions & 71 deletions

File tree

native/watchos/gen_stubs.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const fns = pkg.perry.nativeLibrary.functions;
1111

1212
const OVERRIDES = new Set([
1313
// Platform + input
14-
'bloom_get_platform', 'bloom_get_crown_rotation',
14+
'bloom_get_platform', 'bloom_get_crown_rotation', 'bloom_get_language',
15+
'bloom_set_direct_2d_mode',
1516
'bloom_get_screen_width', 'bloom_get_screen_height',
1617
'bloom_get_touch_x', 'bloom_get_touch_y', 'bloom_get_touch_count',
1718
'bloom_get_delta_time', 'bloom_get_time', 'bloom_get_fps',
@@ -25,7 +26,8 @@ const OVERRIDES = new Set([
2526
'bloom_is_gamepad_available', 'bloom_get_gamepad_axis',
2627
'bloom_is_gamepad_button_pressed', 'bloom_is_gamepad_button_down', 'bloom_is_gamepad_button_released',
2728
'bloom_get_gamepad_axis_count',
28-
// 2D shapes
29+
// 2D camera + shapes
30+
'bloom_begin_mode_2d', 'bloom_end_mode_2d',
2931
'bloom_draw_rect', 'bloom_draw_rect_lines',
3032
'bloom_draw_circle', 'bloom_draw_circle_lines',
3133
'bloom_draw_line', 'bloom_draw_triangle', 'bloom_draw_poly',
@@ -69,13 +71,17 @@ const OVERRIDES = new Set([
6971
const rustType = (p) => {
7072
if (p === 'f64') return 'f64';
7173
if (p === 'i64') return 'i64';
74+
// Perry passes/returns heap strings as a pointer carried in an integer
75+
// register; the stub ignores it, so i64 is the correct ABI-compatible type.
76+
if (p === 'string') return 'i64';
7277
if (p === 'void') return '()';
7378
throw new Error(`Unknown param type: ${p}`);
7479
};
7580

7681
const defaultForReturn = (r) => {
7782
if (r === 'f64') return '0.0';
7883
if (r === 'i64') return '0';
84+
if (r === 'string') return '0';
7985
if (r === 'void') return '()';
8086
throw new Error(`Unknown return type: ${r}`);
8187
};

native/watchos/src/BloomWatchApp.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ let K_TEXTURE: Int32 = 7
176176
let K_TEXTURE_REC: Int32 = 8
177177
let K_TEXTURE_PRO: Int32 = 9
178178
let K_TEXT: Int32 = 10
179+
let K_BEGIN_2D: Int32 = 11
180+
let K_END_2D: Int32 = 12
179181

180182
// 3D primitives
181183
let K_CUBE: Int32 = 20
@@ -307,11 +309,30 @@ struct BloomRootView: View {
307309
// Pull this frame's commands. 2D draws render here; 3D
308310
// commands are filtered by BloomSceneView.
309311
let n = Int(bloom_watchos_copy_draw_list(drawBuf.baseAddress!, 4096))
312+
// `active` is the context world-space draws use: between a
313+
// BEGIN_2D / END_2D pair it carries the camera's affine
314+
// transform (world → screen); otherwise it's the plain
315+
// screen-space context.
316+
var active = ctx
310317
for i in 0..<n {
311318
let ptr = drawBuf.baseAddress!.advanced(by: i)
312319
let k = ptr.pointee.kind
313320
if k >= 20 && k <= 29 { continue } // 3D — handled by SceneView
314-
drawOne(ctx: ctx, cmdPtr: ptr)
321+
if k == K_BEGIN_2D {
322+
let zoom = ptr.pointee.size
323+
var t = CGAffineTransform(translationX: ptr.pointee.x, y: ptr.pointee.y)
324+
t = t.scaledBy(x: zoom, y: zoom)
325+
t = t.translatedBy(x: -ptr.pointee.w, y: -ptr.pointee.h)
326+
var w = ctx
327+
w.transform = t
328+
active = w
329+
continue
330+
}
331+
if k == K_END_2D {
332+
active = ctx
333+
continue
334+
}
335+
drawOne(ctx: active, cmdPtr: ptr)
315336
}
316337
}
317338
}

native/watchos/src/draw_list.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ pub mod kind {
3030
pub const TEXTURE_PRO: i32 = 9; // full: source, dest, origin, rotation
3131
pub const TEXT: i32 = 10;
3232

33+
// 2D camera mode markers. BEGIN carries the camera in reused fields:
34+
// x,y = screen offset; w,h = world target; size = zoom. The Swift Canvas
35+
// applies the matching affine transform to every command until END.
36+
pub const BEGIN_2D: i32 = 11;
37+
pub const END_2D: i32 = 12;
38+
3339
// 3D immediate-mode primitives (pos = x,y,z; w,h = scale; src_x,y,z = secondary).
3440
pub const CUBE: i32 = 20; // pos (x,y,z), size (w,h,size=depth)
3541
pub const CUBE_WIRES: i32 = 21;

0 commit comments

Comments
 (0)