diff --git a/rules/macos/winit-030-window-handle-main-thread-only.md b/rules/macos/winit-030-window-handle-main-thread-only.md new file mode 100644 index 0000000..22b1e45 --- /dev/null +++ b/rules/macos/winit-030-window-handle-main-thread-only.md @@ -0,0 +1,43 @@ +# winit 0.30 macOS: window_handle() only works on the main thread + +**Keywords:** winit,0.30,0.29,macos,appkit,raw_window_handle,window_handle,HandleError,Unavailable,build_surface_attributes,glutin,glutin-winit,surface,init_gl,refresh,thread,panic,abort,MainThreadMarker +**Category:** macos + +## Symptom + +CLI panics on the first presented frame — right after the guest programs a video +mode (`Rex3: Resolution changed to ...`), because `present()` is gated on +`screen.width > 0`: + + thread 'REX3-Refresh' panicked at src/ui.rs: + surface attributes: Unavailable + +## Cause + +The winit 0.29 → 0.30 bump changed macOS behavior. In 0.29, +`raw_window_handle()` returned the NSView pointer from any thread. In 0.30 the +window delegate is `MainThreadBound` and both `window_handle()` / +`raw_window_handle()` return `Err(HandleError::Unavailable)` off the main +thread (`platform_impl/macos/window.rs`, gated on `MainThreadMarker::new()`). + +`glutin_winit::GlWindow::build_surface_attributes` calls `window_handle()` +internally, so it must not be called from the REX3-Refresh thread — but that is +exactly where `GlRenderer::init_gl()` runs (lazily, from the first `present()`). + +## Rule + +Capture `RawWindowHandle` **once on the main thread** in `Ui::new()` (same place +the GL context is created — see the `not_current_context` field comment), stash +it in `GlRenderer`, and build surface attributes in `init_gl()` with +`SurfaceAttributesBuilder::build(cached_handle, w, h)` from +`window.inner_size()`. Never call `build_surface_attributes()` / +`window_handle()` off the main thread on macOS. + +glutin 0.32's CGL `create_window_surface` only reads the handle out of the +attrs (no main-thread check of its own), so surface creation itself may stay on +the refresh thread — consistent with the GL-ownership rule in +`rules/gui/gl-teardown-must-run-on-the-refresh-thread.md`. + +`headless_gl.rs` intentionally keeps the `.ok()?` pattern: it creates its own +`EventLoop` on the calling thread, which already fails gracefully off-main on +macOS — it degrades to `None` instead of crashing. Don't "fix" it the same way. diff --git a/src/ui.rs b/src/ui.rs index cbf6549..6c4974b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,9 +20,9 @@ use glutin::config::ConfigTemplateBuilder; use glutin::context::{ContextApi, ContextAttributesBuilder, GlProfile, NotCurrentContext, PossiblyCurrentContext, Version}; use glutin::display::GetGlDisplay; use glutin::prelude::*; -use glutin::surface::{GlSurface, SwapInterval, WindowSurface, Surface}; -use glutin_winit::{DisplayBuilder, GlWindow}; -use raw_window_handle::HasRawWindowHandle; +use glutin::surface::{GlSurface, SurfaceAttributesBuilder, SwapInterval, WindowSurface, Surface}; +use glutin_winit::DisplayBuilder; +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; use std::num::NonZeroU32; use std::ffi::CString; @@ -67,6 +67,10 @@ struct GlRenderer { // that validate against the drawable/FBConfig being issued from a thread // other than the one that created the X11 connection/window. not_current_context: Option, + // Captured on the main thread in Ui::new(): winit 0.30 macOS returns + // HandleError::Unavailable from window_handle() on any other thread, and + // init_gl() runs on the refresh thread. + raw_window_handle: RawWindowHandle, gl_tier: GlTier, window_size: Arc>>, scale_snap: Arc>>, @@ -96,7 +100,12 @@ impl GlRenderer { let not_current_gl_context = self.not_current_context.take() .expect("GL context missing — init_gl() called more than once"); - let attrs = self.window.build_surface_attributes(Default::default()).expect("surface attributes"); + let size = self.window.inner_size(); + let attrs = SurfaceAttributesBuilder::::new().build( + self.raw_window_handle, + NonZeroU32::new(size.width.max(1)).unwrap(), + NonZeroU32::new(size.height.max(1)).unwrap(), + ); let gl_surface = unsafe { gl_display .create_window_surface(&self.gl_config, &attrs) @@ -685,6 +694,7 @@ impl Ui { window: window.clone(), gl_config, not_current_context: Some(not_current_context), + raw_window_handle, gl_tier, window_size: window_size.clone(), scale_snap: scale_snap.clone(),