From f6712291269386a9459760679a1f58d0241526b2 Mon Sep 17 00:00:00 2001 From: Antoni Sawicki Date: Fri, 7 Aug 2026 00:46:22 -0700 Subject: [PATCH] =?UTF-8?q?macos:=20fix=20abort=20on=20first=20frame=20?= =?UTF-8?q?=E2=80=94=20capture=20window=20handle=20on=20the=20main=20threa?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit winit 0.30 (bumped in 6bbb609) returns HandleError::Unavailable from window_handle() on macOS when called off the main thread; winit 0.29 handed out the NSView pointer from any thread. GlRenderer::init_gl() runs on the REX3-Refresh thread — lazily, from the first present(), which is gated on the guest programming a video mode — so the CLI aborted on the first frame: thread 'REX3-Refresh' panicked at src/ui.rs:99: surface attributes: Unavailable Capture the RawWindowHandle once in Ui::new() on the main thread (it was already fetched there for GL context creation) and build the surface attributes from it in init_gl() instead of calling build_surface_attributes(), which re-queries the handle. glutin's CGL create_window_surface only reads the handle out of the attrs, so surface creation itself stays on the refresh thread per the existing ownership model. headless_gl.rs keeps its .ok()? pattern: it creates its own EventLoop on the calling thread, which already degrades gracefully off-main on macOS. --- ...init-030-window-handle-main-thread-only.md | 43 +++++++++++++++++++ src/ui.rs | 18 ++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 rules/macos/winit-030-window-handle-main-thread-only.md 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(),