Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions rules/macos/winit-030-window-handle-main-thread-only.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 14 additions & 4 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<NotCurrentContext>,
// 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<Mutex<Option<(u32, u32)>>>,
scale_snap: Arc<Mutex<Option<ScaleSnap>>>,
Expand Down Expand Up @@ -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::<WindowSurface>::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)
Expand Down Expand Up @@ -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(),
Expand Down
Loading