From c94e2197d16b1e768c5e844d1ebe6d744bccd9ec Mon Sep 17 00:00:00 2001 From: JohnM <234386938+treuenten@users.noreply.github.com> Date: Sat, 10 Oct 2026 05:23:47 +0800 Subject: [PATCH] Let pipelined rendering run on macOS by creating the surface during extraction IW4L_PIPELINED_RENDERING=1 panicked on the first frame on macOS ("can only access NSView on the main thread" in raw-window-metal), which is why the default is off there. Bevy's create_surfaces takes a NonSendMarker so that the multi-threaded executor hands it back to the main thread, but the render app's Render schedule runs on a single-threaded executor. With pipelining on that schedule runs on the render thread, so create_surfaces built the CAMetalLayer there and touched the NSView off the main thread. With pipelining on, macOS now also runs create_surfaces in the render app's ExtractSchedule, after extract_cameras and so after the windows are extracted. Extraction runs on the main thread, so the surface is created and configured there. The Render schedule's instance then finds it configured and skips, or after a resize reconfigures it, which only sets CAMetalLayer properties and is allowed off the main thread. The macOS default stays off, so nothing changes unless IW4L_PIPELINED_RENDERING=1 is set. docs/BUILD.md now says how to turn it on instead of pointing at the plugins.rs explanation this removes. Measured on an Apple M3 (mp_terminal, 9 bots, uncapped present, bench recorder), pipelining off -> on: 87 -> 105 fps average, frame p50 10.8 -> 8.3 ms, p99 19.4 -> 18.4 ms, worst frames about the same (36 -> 34 ms), main/render overlap 5.4 ms per frame, presented state one frame older. Co-Authored-By: Claude Opus 5.5 --- crates/bootstrap/src/plugins.rs | 16 +++++++++++----- docs/BUILD.md | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bootstrap/src/plugins.rs b/crates/bootstrap/src/plugins.rs index 1b74df6..71cf212 100644 --- a/crates/bootstrap/src/plugins.rs +++ b/crates/bootstrap/src/plugins.rs @@ -80,6 +80,16 @@ pub fn add_runtime_plugins_with_role(app: &mut App, role: RuntimeRole) { render_app.edit_schedule(bevy::render::ExtractSchedule, |schedule| { schedule.set_executor(bevy::ecs::schedule::SingleThreadedExecutor::new()); }); + if cfg!(target_os = "macos") && pipelined_rendering() { + // The Metal surface must be created on the main thread; pipelined, only + // extraction runs there. + render_app.add_systems( + bevy::render::ExtractSchedule, + bevy::render::view::create_surfaces + .run_if(bevy::render::view::need_surface_configuration) + .after(bevy::render::camera::extract_cameras), + ); + } } } @@ -128,11 +138,7 @@ const PIPELINED_RENDERING_ENV: &str = "IW4L_PIPELINED_RENDERING"; /// boundary; the bounded render channel permits one outstanding frame. /// Set IW4L_PIPELINED_RENDERING=0 for synchronous presentation. /// -/// Off by default on macOS: AppKit only lets the main thread touch the NSView -/// behind the Metal surface. Bevy hands `create_surfaces` back to the main -/// thread through the multi-threaded executor, which the single-threaded -/// `Render` schedule above bypasses, so the render thread would create it and -/// panic in `raw-window-metal`. +/// Off by default on macOS. fn pipelined_rendering() -> bool { match std::env::var_os(PIPELINED_RENDERING_ENV) { None => !cfg!(target_os = "macos"), diff --git a/docs/BUILD.md b/docs/BUILD.md index 43d543e..c622514 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -33,7 +33,7 @@ macOS needs only the Xcode command line tools (`xcode-select --install`) and rustup: winit, wgpu (Metal), CoreAudio and gilrs link system frameworks, and the `x11` / `wayland` features compile to nothing. Menu and `make map mp_boneyard` run on Apple silicon (M2 Max, Metal). Pipelined -rendering is off by default there (`bootstrap/src/plugins.rs` says why). +rendering is off by default there; `IW4L_PIPELINED_RENDERING=1` turns it on. Game data: the Windows depot of a Steam copy, `steamcmd +@sSteamCmdForcePlatformType windows +force_install_dir ~/Games/MW2 +login +app_update 10190 +quit`, then `IW4L_GAMES=~/Games`.