From 3cbaa889f3a523abc7b9605a84549aa3a4e46665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Rouleau?= Date: Sat, 13 Jun 2026 10:56:27 -0400 Subject: [PATCH 1/2] ios: implement required mtkView:drawableSizeWillChange: selector `MTKViewDelegate` declares two `@required` methods: `mtkView:drawableSizeWillChange:` and `drawInMTKView:`. The `QuadViewDlg` class only registered the latter, so any actual drawable-size change (window resize, rotation under a non-locked orientation set, split-view drag on iPad) crashed: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[QuadViewDlg mtkView:drawableSizeWillChange:]: unrecognized selector sent to instance 0x...' ... [MTKView _resizeDrawable] ... [UIView setFrame:] ... [UIWindow _rotateWindowToOrientation:...] Reproduced on the iPad Air 11-inch simulator on iOS 27 by rotating the device. Did not reproduce on the iPhone 17 simulator there because CHOMP's Info.plist locks orientation, so the drawable size never actually changes. Implementation is a stub: `draw_in_rect` already polls `UIScreen.mainScreen.bounds` every frame and emits `Message::Resize` on a delta, so a no-op satisfies the protocol without changing the existing resize path. --- src/native/ios.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/native/ios.rs b/src/native/ios.rs index 53bf8539..019ace37 100644 --- a/src/native/ios.rs +++ b/src/native/ios.rs @@ -371,6 +371,19 @@ pub fn define_glk_or_mtk_view_dlg(superclass: &Class) -> *const Class { draw_in_rect(this, s, o, nil); } + // `MTKViewDelegate` requires both `drawInMTKView:` AND + // `mtkView:drawableSizeWillChange:`. MTKView invokes the + // size-change selector before the first `drawInMTKView:` after + // any drawable-size change (window resize, rotation under a + // non-locked orientation set, split-view drag on iPad). Without + // an implementation, `_resizeDrawable` raises + // `NSInvalidArgumentException` and crashes the process. + // + // The real resize handling lives in `draw_in_rect`, which polls + // `UIScreen.mainScreen.bounds` every frame and emits + // `Message::Resize` on a delta — so a stub is enough here. + extern "C" fn drawable_size_will_change(_: &Object, _: Sel, _: ObjcId, _: NSSize) {} + unsafe { decl.add_method( sel!(glkView: drawInRect:), @@ -381,6 +394,11 @@ pub fn define_glk_or_mtk_view_dlg(superclass: &Class) -> *const Class { sel!(drawInMTKView:), draw_in_rect2 as extern "C" fn(&Object, Sel, ObjcId), ); + + decl.add_method( + sel!(mtkView: drawableSizeWillChange:), + drawable_size_will_change as extern "C" fn(&Object, Sel, ObjcId, NSSize), + ); } decl.add_ivar::<*mut c_void>("display_ptr"); From b83cdd2524fd22b0bd2f144524d159639a30ab6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Rouleau?= Date: Sat, 13 Jun 2026 11:09:48 -0400 Subject: [PATCH 2/2] ios: sync drawable size + emit Resize in drawableSizeWillChange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Apple's MTKView contract, `drawableSizeWillChange:` is invoked before the next `drawInMTKView:` whenever the drawable's pixel dimensions change (window resize, rotation under a non-locked orientation set, split-view drag on iPad). The previous commit added a no-op stub to satisfy the protocol; that prevents the "unrecognized selector" crash but leaves `native_display`'s stored dimensions stale until the next frame's `UIScreen.mainScreen.bounds` poll catches up. During iPad rotation animations the stored dimensions stay one frame behind the resized drawable. Anything that depends on the framebuffer dimensions for the current pass — `setScissorRect` is the common case — gets computed against the old size and fails Metal validation: -[MTLDebugRenderCommandEncoder setScissorRect:]: failed assertion `Set Scissor Rect Validation (rect.y(688) + rect.height(2064))(2752) must be <= render pass height(2064)' Update `native_display`'s `screen_width`/`screen_height` from the size argument immediately and send `Message::Resize` so the event handler resizes before the next draw. `draw_in_rect`'s own `UIScreen.bounds` poll still runs as a fallback (e.g. for full-screen size changes that don't go through the delegate). --- src/native/ios.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/native/ios.rs b/src/native/ios.rs index 019ace37..b4048fc3 100644 --- a/src/native/ios.rs +++ b/src/native/ios.rs @@ -371,18 +371,30 @@ pub fn define_glk_or_mtk_view_dlg(superclass: &Class) -> *const Class { draw_in_rect(this, s, o, nil); } - // `MTKViewDelegate` requires both `drawInMTKView:` AND - // `mtkView:drawableSizeWillChange:`. MTKView invokes the - // size-change selector before the first `drawInMTKView:` after - // any drawable-size change (window resize, rotation under a - // non-locked orientation set, split-view drag on iPad). Without - // an implementation, `_resizeDrawable` raises - // `NSInvalidArgumentException` and crashes the process. - // - // The real resize handling lives in `draw_in_rect`, which polls - // `UIScreen.mainScreen.bounds` every frame and emits - // `Message::Resize` on a delta — so a stub is enough here. - extern "C" fn drawable_size_will_change(_: &Object, _: Sel, _: ObjcId, _: NSSize) {} + // `MTKViewDelegate` requires this alongside `drawInMTKView:`; + // missing it crashes `_resizeDrawable` with + // `NSInvalidArgumentException`. Sync `native_display` here so + // the next frame's `setScissorRect` (and anything else reading + // `screen_size`) matches the new drawable — `draw_in_rect`'s + // `UIScreen.bounds` poll is the fallback but lags drawableSize + // during rotation animations. + extern "C" fn drawable_size_will_change(_: &Object, _: Sel, _: ObjcId, size: NSSize) { + let width = size.width as i32; + let height = size.height as i32; + let changed = { + let mut display = native_display().lock().unwrap(); + let changed = + display.screen_width != width || display.screen_height != height; + if changed { + display.screen_width = width; + display.screen_height = height; + } + changed + }; + if changed { + send_message(Message::Resize { width, height }); + } + } unsafe { decl.add_method(