Skip to content

Add --features watchos-game-loop (Metal-surface apps on watchOS) #106

@proggeramlug

Description

@proggeramlug

Summary

Follow-up to #105. That issue added --target watchos[-simulator], but the resulting app shell (PerryWatchApp.swift) is hardcoded to the perry-ui-watchos SwiftUI scene tree. Bloom-style game-loop apps need an escape hatch analogous to --features ios-game-loop, so a native library can own the main thread's view and render with wgpu/Metal.

Why

Bloom Jump (and any wgpu-based engine targeting the watch) needs:

  • A CAMetalLayer attached to the watch's interface, not a SwiftUI view tree.
  • The user's TS entry point running on a background "game" thread, blocking in the bloom runGame loop.
  • WKApplicationMain (or equivalent modern SwiftUI App) on the main thread — because WatchKit, like UIKit, requires it.

Today, the only watchOS app shape Perry emits is the SwiftUI scene-tree app from crates/perry-ui-watchos/swift/PerryWatchApp.swift. Bloom can't plug into that — it would have to fight the SwiftUI runtime for the view and the main thread.

Proposed design (mirrors ios-game-loop)

Precedent lives in crates/perry-runtime/src/ios_game_loop.rs:

  1. When --features watchos-game-loop is set, perry compile renames _main_perry_user_main in the entry object (same objcopy path already used for ios-game-loop; the watchos branch already rides on _main → _perry_main_init logic fixed in Add watchOS compile target (Digital Crown input) #105 — this adds the user-main rename on top).
  2. A new perry-runtime/src/watchos_game_loop.rs provides the real main():
    • Spawns _perry_user_main on a background thread.
    • Registers (or defers to the native lib for) a minimal WKApplicationDelegate / SwiftUI App whose body hosts a UIViewRepresentable wrapping a Metal-backed view.
    • Calls WKApplicationMain on the main thread.
  3. Declare two FFI hooks the native lib implements, analogous to the iOS pair:
    • perry_register_native_classes() — native lib registers its WatchKit view/controller classes.
    • perry_scene_will_connect(scene) — native lib grabs the scene/view and sets up the Metal layer + wgpu surface.

What the native (bloom) side provides

  • native/watchos/ crate with a WKHostingController (or UIViewRepresentable) that creates a CAMetalLayer, sized from WKInterfaceDevice.current().screenBounds.
  • A WKCrownSequencer delegate that calls engine().input.accumulate_crown_rotation(delta) (already plumbed — see bloom commit [670324d](https://github.com/... pending push)).
  • Implementations of all ~130 bloom_* FFI functions (mostly delegating to bloom-shared; only the platform-specific app/input bits are watchos-specific).

Info.plist + entitlements

The Info.plist shape #105 already emits (WKApplication=true, UIDeviceFamily=[4]) is compatible — only NSPrincipalClass or the main-thread entry delegate name needs to swap when the feature flag is on.

Acceptance

perry compile --target watchos-simulator --features watchos-game-loop src/main.ts -o BloomJumpWatch
xcrun simctl install booted BloomJumpWatch.app
xcrun simctl launch booted com.bloom.jump.watch

…launches on a watchOS simulator, clears to a non-black color from bloom's runGame loop (proves the Metal surface is live), and getCrownRotation() returns non-zero values when the crown is rotated in the simulator (Cmd+Shift+Up/Down or wheel scroll).

Related

  • Add watchOS compile target (Digital Crown input) #105 — watchOS target (closed)
  • crates/perry-runtime/src/ios_game_loop.rs — iOS precedent to mirror
  • Bloom engine commit 670324dPlatform.WATCHOS, getCrownRotation(), consume_crown_rotation accumulator, all non-watch backends stubbed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions