rust-windowing / winit

Window handling library in pure Rust

Home Page:https://docs.rs/winit/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Crash in iOS when swiping up for the control center

MichaelHills opened this issue · 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.

This also seems related to #1613 — backtrace at a glance is the same.

#1843 Resolved this on my end. Would enjoy some other 👀

Wow nice work. I'll try it when I get a chance. :)

commented

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>

Crashed
Screen Shot 2022-09-04 at 22 25 53

Good call stack
Screen Shot 2022-09-04 at 22 24 46

commented

Good full stack, we can see the window is created inside UIApplicationMain.

Screen Shot 2022-09-04 at 22 37 27

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)?

commented

@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(),
            _ => (),
        }
    });
}
commented

@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

commented

@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?

let (windows, events) = AppState::get_mut().did_finish_launching_transition();
let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents(
StartCause::Init,
)))
.chain(events);
handle_nonuser_events(events);
// the above window dance hack, could possibly trigger new windows to be created.
// we can just set those windows up normally, as they were created after didFinishLaunching
for window in windows {
let count: NSUInteger = msg_send![window, retainCount];
// make sure the window is still referenced
if count > 1 {
let _: () = msg_send![window, makeKeyAndVisible];
}
let _: () = msg_send![window, release];
}

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?

commented

@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