You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #106. watchos-game-loop solved the main-thread ownership problem but assumed a UIKit-style programmatic view presentation model that doesn't apply on watchOS. For any bloom-style custom-rendering app on the watch, the native lib needs to own the @main struct App: App scene tree so SwiftUI (SceneView for 3D / Canvas for 2D) can be the rendering surface.
Why the current paths both fall short
Default watchOS path (Add watchOS compile target (Digital Crown input) #105) — Perry ships its own PerryWatchApp.swift with @main struct PerryApp: App that polls a perry-ui-watchos scene tree. Great for declarative widget UIs, but locks the renderer to SwiftUI's built-in primitives polled from a scene tree — no way for bloom to populate a SceneView or drive a Canvas imperatively from its own game loop.
watchos-game-loop (Add --features watchos-game-loop (Metal-surface apps on watchOS) #106) — drops PerryWatchApp.swift, spawns the user's TS on a game thread, calls WKApplicationMain with a minimal NSObject <WKApplicationDelegate>. This works for the classic WatchKit model (storyboards + WKInterfaceController) but watchOS has no programmatic way to present a SwiftUI root from applicationDidFinishLaunching. UIHostingController / UIViewRepresentable don't exist on watchOS. WKInterfaceController is storyboard-only. So the game-loop path today launches a black-screen .app with no rendering path available.
Proposed design: --features watchos-swift-app
Let the native library ship its own @main struct App: App Swift file. Perry's job shrinks to: compile it, drop the auto-generated PerryWatchApp.swift, rename TS _main → _perry_user_main, and link SwiftUI + WatchKit frameworks.
Concretely:
When --features watchos-swift-app is set:
Perry does not emit crates/perry-ui-watchos/swift/PerryWatchApp.swift.
Perry does not provide a C main() (there is no watchos_swift_app.rs runtime crate — the native lib's @main is the real entry).
TS entry's _main still gets renamed to _perry_user_main so the Swift side can call it on a background thread.
Perry looks for a new optional entry in the native lib manifest — perry.nativeLibrary.targets.watchos.swift_sources (array of paths relative to the crate dir) — and compiles them with swiftc -parse-as-library -emit-object using the appropriate watchos / watchos-simulator SDK and triple, then links the resulting .o files into the final binary.
Link line includes -framework SwiftUI -framework WatchKit -framework SceneKit (and swift runtime via -L \$(xcrun --show-sdk-path --sdk <sdk>)/usr/lib/swift -lswiftCore — the swift standard library paths).
Native lib (bloom-watchos) provides:
BloomWatchApp.swift with @main struct BloomWatchApp: App { ... } and a WKApplicationDelegateAdaptor if it wants an app delegate.
Inside the App's init() or the root view's .task { ... }, calls an exported C function like bloom_watchos_start_game_loop() that spawns the game thread and invokes _perry_user_main on it.
Root view uses whatever combination of SceneView (3D via SceneKit), Canvas (2D), overlays, .digitalCrownRotation(...), .onTapGesture to render and collect input. These feed bloom via C hooks the native lib already exports.
…launches a BloomWatchApp whose root SceneView(scene: bloomScene) renders a bloom-populated SCNScene on the watch simulator, with the game loop driving bloom_scene_* FFI from TS on a background thread. Digital Crown rotation reaches TS via getCrownRotation().
Why this instead of method-swizzling the delegate or relying on storyboards
Swizzling gets input flowing but still can't host a SwiftUI root programmatically — there's no API for that on watchOS. This is the blocker.
Storyboards work but require authoring .storyboard files and embedding them in the bundle. That's fine for one app, but for a game engine targeting many projects it's an ergonomic regression vs "one Swift file in the native crate."
Compatibility
watchos-game-loop stays useful for classic WKInterfaceController apps — don't remove it.
Default watchos path (Perry's own SwiftUI tree) stays the default for UI-style apps.
watchos-swift-app is purely additive — a third modality for engine-backed renderers.
Summary
Follow-up to #106.
watchos-game-loopsolved the main-thread ownership problem but assumed a UIKit-style programmatic view presentation model that doesn't apply on watchOS. For any bloom-style custom-rendering app on the watch, the native lib needs to own the@main struct App: Appscene tree so SwiftUI (SceneViewfor 3D /Canvasfor 2D) can be the rendering surface.Why the current paths both fall short
Default watchOS path (Add watchOS compile target (Digital Crown input) #105) — Perry ships its own
PerryWatchApp.swiftwith@main struct PerryApp: Appthat polls aperry-ui-watchosscene tree. Great for declarative widget UIs, but locks the renderer to SwiftUI's built-in primitives polled from a scene tree — no way for bloom to populate aSceneViewor drive aCanvasimperatively from its own game loop.watchos-game-loop(Add --features watchos-game-loop (Metal-surface apps on watchOS) #106) — dropsPerryWatchApp.swift, spawns the user's TS on a game thread, callsWKApplicationMainwith a minimalNSObject <WKApplicationDelegate>. This works for the classic WatchKit model (storyboards +WKInterfaceController) but watchOS has no programmatic way to present a SwiftUI root fromapplicationDidFinishLaunching.UIHostingController/UIViewRepresentabledon't exist on watchOS.WKInterfaceControlleris storyboard-only. So the game-loop path today launches a black-screen.appwith no rendering path available.Proposed design:
--features watchos-swift-appLet the native library ship its own
@main struct App: AppSwift file. Perry's job shrinks to: compile it, drop the auto-generatedPerryWatchApp.swift, rename TS_main→_perry_user_main, and link SwiftUI + WatchKit frameworks.Concretely:
When
--features watchos-swift-appis set:crates/perry-ui-watchos/swift/PerryWatchApp.swift.main()(there is nowatchos_swift_app.rsruntime crate — the native lib's@mainis the real entry)._mainstill gets renamed to_perry_user_mainso the Swift side can call it on a background thread.perry.nativeLibrary.targets.watchos.swift_sources(array of paths relative to the crate dir) — and compiles them withswiftc -parse-as-library -emit-objectusing the appropriatewatchos/watchos-simulatorSDK and triple, then links the resulting.ofiles into the final binary.-framework SwiftUI -framework WatchKit -framework SceneKit(and swift runtime via-L \$(xcrun --show-sdk-path --sdk <sdk>)/usr/lib/swift -lswiftCore— the swift standard library paths).Native lib (bloom-watchos) provides:
BloomWatchApp.swiftwith@main struct BloomWatchApp: App { ... }and aWKApplicationDelegateAdaptorif it wants an app delegate.init()or the root view's.task { ... }, calls an exported C function likebloom_watchos_start_game_loop()that spawns the game thread and invokes_perry_user_mainon it.SceneView(3D via SceneKit),Canvas(2D), overlays,.digitalCrownRotation(...),.onTapGestureto render and collect input. These feed bloom via C hooks the native lib already exports.Acceptance
…launches a
BloomWatchAppwhose rootSceneView(scene: bloomScene)renders a bloom-populatedSCNSceneon the watch simulator, with the game loop drivingbloom_scene_*FFI from TS on a background thread. Digital Crown rotation reaches TS viagetCrownRotation().Why this instead of method-swizzling the delegate or relying on storyboards
.storyboardfiles and embedding them in the bundle. That's fine for one app, but for a game engine targeting many projects it's an ergonomic regression vs "one Swift file in the native crate."Compatibility
watchos-game-loopstays useful for classicWKInterfaceControllerapps — don't remove it.watchos-swift-appis purely additive — a third modality for engine-backed renderers.Related
--target watchos[-simulator]--features watchos-game-loopfind_native_librarywatchos mapping670324d—Platform.WATCHOS+getCrownRotationplumbing