Crash in iOS when swiping up for the control center
MichaelHills opened this issue Β· 24 comments
On iOS 12.4.8 on my iPhone 6, using Bevy on winit, if I swipe up for the control center immediately after app load I get a crash. If I first touch the screen to fire some touch events before trying to open the control center, then there is no crash and everything works as normal.
I'm trying to reproduce this directly on winit, but haven't been able to yet. Thought I'd post first anyway to see if this sounds familiar. I've googled various parts of the stack trace with no success.
It's really bizarre because the crash is inside iOS libraries. Perhaps the gesture recogniser is registered on the winit-defined UIView / UIViewController and some interaction there is the problem. Unfortunately without any source to the stack trace I don't know what this NSArray is. I suspect it's just accumulating touch events as part of the "delay touch near edge of screen" logic.
Exception NSException * "*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil" 0x0000000282d89110
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001bd3239c0 libobjc.A.dylib`objc_exception_throw
frame #1: 0x00000001be0c4bec CoreFoundation`_CFThrowFormattedException + 112
frame #2: 0x00000001be0358ac CoreFoundation`-[__NSArrayM insertObject:atIndex:] + 1212
frame #3: 0x00000001ea565868 UIKitCore`-[UIGestureRecognizer _delayTouch:forEvent:] + 216
frame #4: 0x00000001ea5813c0 UIKitCore`-[_UISystemGestureGateGestureRecognizer _delayTouch:forEvent:] + 140
frame #5: 0x00000001ea565a68 UIKitCore`-[UIGestureRecognizer _delayTouchesForEvent:inPhase:] + 220
frame #6: 0x00000001ea565c98 UIKitCore`-[UIGestureRecognizer _delayTouchesForEventIfNeeded:] + 100
frame #7: 0x00000001ea566a9c UIKitCore`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 504
frame #8: 0x00000001ea55ac78 UIKitCore`_UIGestureEnvironmentUpdate + 2180
frame #9: 0x00000001ea55a3a8 UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 384
frame #10: 0x00000001ea55a188 UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 204
frame #11: 0x00000001ea9727d0 UIKitCore`-[UIWindow sendEvent:] + 3112
frame #12: 0x00000001ea95285c UIKitCore`-[UIApplication sendEvent:] + 340
frame #13: 0x00000001eaa189d4 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1768
frame #14: 0x00000001eaa1b100 UIKitCore`__handleEventQueueInternal + 4828
frame #15: 0x00000001eaa14330 UIKitCore`__handleHIDEventFetcherDrain + 152
frame #16: 0x00000001be0dcf1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #17: 0x00000001be0dce9c CoreFoundation`__CFRunLoopDoSource0 + 88
frame #18: 0x00000001be0dc784 CoreFoundation`__CFRunLoopDoSources0 + 176
frame #19: 0x00000001be0d76c0 CoreFoundation`__CFRunLoopRun + 1004
frame #20: 0x00000001be0d6fb4 CoreFoundation`CFRunLoopRunSpecific + 436
frame #21: 0x00000001c02d879c GraphicsServices`GSEventRunModal + 104
frame #22: 0x00000001ea938c38 UIKitCore`UIApplicationMain + 212
* frame #23: 0x0000000101dc0910 Callisto`cart_tmp_winit::platform_impl::platform::event_loop::EventLoop$LT$T$GT$::run::h2a3bed959aaaa659(self=EventLoop<()> @ 0x000000016ef50bb0, event_handler=closure-1 @ 0x000000016ef50bd0) at event_loop.rs:116:13
frame #24: 0x0000000101de5b4c Callisto`cart_tmp_winit::event_loop::EventLoop$LT$T$GT$::run::h8c8ef82fc7ef7463(self=<unavailable>, event_handler=<unavailable>) at event_loop.rs:149:9
frame #25: 0x0000000101de1724 Callisto`bevy_winit::run::h215a617795758f71(event_loop=<unavailable>, event_handler=<unavailable>) at lib.rs:43:5
frame #26: 0x0000000101dd1d28 Callisto`bevy_winit::winit_runner::h90a304e554acabdc(app=App @ 0x000000016ef51c28) at lib.rs:244:9
frame #27: 0x0000000101dda810 Callisto`core::ops::function::Fn::call::ha1caefc47bb26836((null)=0x0000000000000001, (null)=(bevy_app::app::App) @ 0x000000016ef51c28) at function.rs:70:5
frame #28: 0x0000000102de6698 Callisto`_$LT$alloc..boxed..Box$LT$F$GT$$u20$as$u20$core..ops..function..Fn$LT$A$GT$$GT$::call::h88bacb7a918cef10(self=0x000000016ef52628, args=(bevy_app::app::App) @ 0x000000016ef51f58) at boxed.rs:1056:9
frame #29: 0x0000000102de8ca8 Callisto`bevy_app::app::App::run::h0e84ac1eed74c29a(self=App @ 0x000000016ef52ee8) at app.rs:80:9
frame #30: 0x0000000102dcf734 Callisto`bevy_app::app_builder::AppBuilder::run::hed1780d15d7a0af0(self=0x000000016ef53558) at app_builder.rs:45:9
frame #31: 0x0000000100f06808 Callisto`bevy_main at lib.rs:48:5
frame #32: 0x0000000100eb3d5c Callisto`main at main.swift:17:1
frame #33: 0x00000001bdb9a8e0 libdyld.dylib`start + 4
I was able to reproduce this using just the wgpu triangle example on winit. By just having this call to swap_chain.get_current_frame()
in RedrawRequested
I can get it to crash. Commenting out get_current_frame()
such that RedrawRequested
is empty will make the crash go away.
Event::RedrawRequested(_) => {
let frame = swap_chain
.get_current_frame()
.expect("Failed to acquire next swap chain texture")
.output;
So seemingly wgpu somehow triggers the crash (and bevy uses wgpu so that probably explains my original crash). I don't really see what wgpu has to do with this though.
On Bevy and iPhone 11 I can reproduce this crash by swiping in from any side of the screen. Specifically need to come in from the outside edge. It kind of sounds like this might be related to the preferredScreenEdgesDeferringSystemGestures
functionality. Haven't had a chance to go back to the wgpu/winit example though and try swiping from any edge.
@MichaelHills I am looking into this as well. I've noticed if you touch the screen prior to dragging in from the edges it will prevent the crash. I have put a bunch of traces into winit side of the UIApplication/View Controller, but haven't been able to determine much that route. None of my traces in the winit view class or the delegate are triggered before the crash (I was assuming I would see something on the touch handler). My best guess right now, and I'll reiterate it's only a guess, is there's some uninitialized array in the UIGestureRecognizer that somehow gets initialized when you provide a valid touch inside the app. I need to figure out some better way of troubleshooting this though. It all feels pretty opaque right now. I.E. I am not sure where exactly to put a break to begin stepping through because everything on the rust side is abstracted by run
of the event loop.
Wow nice work. I'll try it when I get a chance. :)
Bug and trouble shooting
I've been debugging the crash bug for hours. It was reported by others and had a quick fix by setting ScreenEdge::ALL. Since the crash is reproducible, so I tried to tackle it down or at least find the root cause, I think I got something.
Currently, we create UIWindow first, then run UIApplicationMain, which is problematic, since UIApplicationMain actually setup some global state that UIKit requires, like following
Stack:
[UIGestureEnvironment init]
[UIApplication init]
UIApplicationInstantiateSingleton
_UIApplicationMainPreparations
UIApplicationMain
UIGestureEnvironment is also a singleton which is inited and stored inside UIApplication. Also, it is stored in each UIGestureRecognizer instance if it is already created. And UIGestureRecognizer is created when we create UIWindow, which means at that time, the global didn't created, so all the recognizer with _gestureEnvironment
property as nil.
When ScreenEdge preference is 0, it triggers some logic of delay touch handling, and in that code path, it read the property as nil, which crashed when add into a nsarray.
How to fix:
UIWindow/UIView should be created by method on ApplicationDelegate, since UIApplicationMain will never return.
Other
Some evidence (crashed is the bevy_ios_example. good is a normal created ios app):
crashed:
(lldb) po ((_UISystemGestureGateGestureRecognizer*)0x15fe115f0) -> _gestureEnvironment
nil
Good
(lldb) po ((_UISystemGestureGateGestureRecognizer *)0x101808750) -> _gestureEnvironment
<UIGestureEnvironment: 0x281ee4af0>
we create UIWindow first, then run UIApplicationMain, which is problematic, since UIApplicationMain actually setup some global state that UIKit requires, like following
This sounds a lot like the same issue we have on macOS, which is documented in the bottom of the readme for now; can you try the suggested workaround (creating your window inside StartCause(Init)
?
@madsmtm the window is created by winit, before it calls uiapplicationmain. I donβt think user has the chance to modify where to initialize the window in ios platform.
Hmm, are you sure? I'm fairly certain you should be able to do something like this?
use winit::{
event::{Event, StartCause, WindowEvent},
event_loop::EventLoop,
window::Window,
};
fn main() {
let event_loop = EventLoop::new();
let mut window = None;
event_loop.run(move |event, event_loop, control_flow| {
control_flow.set_wait();
match event {
Event::NewEvents(StartCause::Init) => {
window = Some(Window::new(&event_loop).unwrap());
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
} if Some(window_id) == window.as_ref().map(|w| w.id()) => control_flow.set_exit(),
_ => (),
}
});
}
@madsmtm I'm not sure, I just started read winit code the moment hit by the crash. But I see code in app_state, which implements will_launch_transition
or did_finish_launching_transition
, they are taking window from predefined state, which I think is created and managed by winit itself?
AppStateImpl::NotLaunched {
queued_windows,
queued_events,
queued_gpu_redraws,
} => (queued_windows, queued_events, queued_gpu_redraws),
EDIT: typo
@madsmtm Thanks for the advice. From code here, seems the crate supports user create a new window instance and replace the one winit created. Seems a bit hacky in my mind... Do you know why it designed like this?
winit/src/platform_impl/ios/app_state.rs
Lines 587 to 604 in 0fca8b0
Because it's possible to create windows before StartCause::Init
on other platforms, so there hasn't been much incentive to change the API to something less ergonomic (but more correct) - it is something I'm quite annoyed with though, so I'll probably write an issue and discuss a few solutions with the other maintainers at some point.
We now suggest to create a window from inside Resume
and it's documented. The main issue that we shouldn't prevent window creation on other platforms since ios is a bit special here.
The same applies for android, since it also should create from the Resume
event.
Probably the right way is to fail
window creation on ios from the wrong place? Same for Android?
@kchibisov Thanks, any link for the documentation? I scanned the code but didn't find anything. In readme, the window is created outside of the event loop.
Resume
event.
Oh right, yeah, that one.
Probably the right way is to
fail
window creation on ios from the wrong place? Same for Android?
I'm not sure it's technically possible to know if the main event loop is running on iOS, but if it is, we should definitely do this!
I'm not sure it's technically possible to know if the main event loop is running on iOS, but if it is, we should definitely do this!
You can run
event_loop in winit only once, so you sort of know. Just set a global AtomicBool
from the run
invokation.
Sorry if this is not appropriate to ask here, but does anyone have a sort of minimal example of an iOS winit app? I'm not getting any touch events firing at all. I do see window resize events/resume events/etc, so I assume everything else is functioning.
Sorry if this is not appropriate to ask here, but does anyone have a sort of minimal example of an iOS winit app? I'm not getting any touch events firing at all. I do see window resize events/resume events/etc, so I assume everything else is functioning.
Did you get any further with this? I'd also appreciate an example or guide (and for Android)
@dominictobias the android is dead simple though. You could look at the https://github.com/rust-windowing/glutin/blob/master/glutin_examples/examples/android.rs . And that's the only thing you'd need, the rest is the same as on any other OS.
Not familiar with how to setup iOS example.
@dominictobias the android is dead simple though. You could look at the https://github.com/rust-windowing/glutin/blob/master/glutin_examples/examples/android.rs . And that's the only thing you'd need, the rest is the same as on any other OS.
Not familiar with how to setup iOS example.
Ah nice thanks. I didn't try yet I can prob figure iOS out then