Fix iOS startup crash: retry instead of panicking when the winit window is not registered yet - #14
Conversation
…s not registered yet
On iOS the winit UIWindow is not guaranteed to be registered in
WINIT_WINDOWS during the first frame, so the run-once Startup init
system could hit .expect("invalid window handle") and abort the app
(the panic cannot unwind through winit's extern "C" CFRunLoop
observer). Observed on ~50% of cold launches on iOS 26.5.
init now runs in Update, skips the frame when the window handle is not
yet available, and disables itself via a Local<bool> once the insets
have been read.
|
Great find! I wonder if there is otherwise a reliable window-created message/event we could subscribe to instead |
bevy_winit writes WindowCreated immediately after registering the window in WINIT_WINDOWS (bevy_winit/src/system.rs create_windows), so subscribing to that message is the earliest point where the handle is guaranteed to be available. Replaces the Local<bool> polling latch.
|
Good call — there is exactly such a signal on the Bevy side: Reworked in 6e5a7ad: I considered subscribing to a native UIKit notification instead, but |
|
Validated the |
Problem
IosSafeAreaPluginschedules itsinitsystem inStartup, which runs exactly once, and does:On iOS the winit
UIWindowis not guaranteed to be registered inWINIT_WINDOWSby the timeStartupruns — window creation is event-loop-driven and can complete a frame or two after Bevy's first update. When the timing lands wrong,get_windowreturnsNoneand the.expectpanics on the main thread. The unwind then hits winit's non-unwindingextern "C"CFRunLoop observer (panic_cannot_unwind), so the app dies with SIGABRT.In our app (Bevy 0.18.1, iPhone 12 Pro Max, iOS 26.5, TestFlight release builds) this reproduced on ~50% of cold launches — a coin flip per launch depending on whether the OS window registration beat Bevy's first update. Sentry backtrace of the panic origin:
The
PrimaryWindowentity exists at that point (theSingleparam resolves) — it's specifically the winit-side registry entry that isn't there yet, a precondition the plugin can't control. The code already handles the very next step (raw_window.window_handle()) gracefully withif let Ok(...); only the registry lookup panics.Fix
initinUpdateinstead ofStartup, self-disabling via aLocal<bool>latch once the insets have been read (a single bool check per frame afterwards)..expectwith alet Some(...) else { return None }skip-and-retry: if the window isn't registered yet, try again next frame.Behavior is unchanged for every launch that works today; launches that previously crashed now pick up the safe-area insets one or two frames later. Design tradeoff: the system retries by polling rather than subscribing to
WindowCreatedevents — simpler and the steady-state cost is negligible. Happy to rework it event-driven if you prefer.Verification
cargo check --target aarch64-apple-iosclean on this branch.A patch release with this fix would let us drop our vendored copy — thank you!
🤖 Generated with Claude Code