From 8798519c324fe07bcc53b3075a5179a33fce401d Mon Sep 17 00:00:00 2001 From: Ali Sakanak Date: Wed, 14 Oct 2026 23:00:55 +0200 Subject: [PATCH] Pipeline macOS rendering with a frame latency of two The macOS surface workaround ran before Bevy 0.20 applied the window extraction commands, so the first pipelined frame still created the Metal surface on the render thread and panicked. Applying them first lets pipelining run, and a frame latency of two stops the render thread waiting on the display before every frame; with a latency of one pipelining gains nothing. Apple M4 Pro, mp_rust, 8 bots, bench recorder, 3456x2168 fullscreen: uncapped 95 -> 238 fps (frame p95 12.9 -> 7.0 ms); vsync on 71 fps alternating between 60 and 120 -> steady 120 (p95 20.5 -> 8.7 ms). Co-Authored-By: Claude Opus 5.5 --- crates/bootstrap/src/plugins.rs | 27 +++++++++++++++++---------- docs/BUILD.md | 5 +++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/bootstrap/src/plugins.rs b/crates/bootstrap/src/plugins.rs index 6f523e4..5ee7e5c 100644 --- a/crates/bootstrap/src/plugins.rs +++ b/crates/bootstrap/src/plugins.rs @@ -68,11 +68,17 @@ pub fn add_runtime_plugins_with_role(app: &mut App, role: RuntimeRole) { }); if cfg!(target_os = "macos") && pipelined_rendering() { // The Metal surface must be created on the main thread; pipelined, only - // extraction runs there. + // extraction runs there. Window extraction inserts `ExtractedWindow` + // through commands, so they are applied first or the first frame finds + // no window here and the render thread creates the surface instead. render_app.add_systems( bevy::render::ExtractSchedule, - bevy::render::view::create_surfaces - .run_if(bevy::render::view::need_surface_configuration) + ( + bevy::ecs::schedule::ApplyDeferred, + bevy::render::view::create_surfaces + .run_if(bevy::render::view::need_surface_configuration), + ) + .chain() .after(bevy::render::camera::extract_cameras), ); } @@ -123,10 +129,9 @@ const PIPELINED_RENDERING_ENV: &str = "IW4L_PIPELINED_RENDERING"; /// Overlap rendering with the next main frame. Extraction remains the ownership /// boundary; the bounded render channel permits one outstanding frame. /// Set IW4L_PIPELINED_RENDERING=0 for synchronous presentation. -/// fn pipelined_rendering() -> bool { match std::env::var_os(PIPELINED_RENDERING_ENV) { - None => !cfg!(target_os = "macos"), + None => true, Some(_) => perf::switch(PIPELINED_RENDERING_ENV), } } @@ -138,16 +143,18 @@ const FRAME_LATENCY_ENV: &str = "IW4L_FRAME_LATENCY"; /// wgpu calls this a hint and the backend is free to clamp it: on Vulkan it is /// bound to the number of swapchain images, so a run that asked for two did /// not necessarily get two, and only a measurement says which. One is the -/// default because it is what the runtime shipped; the variable exists so the -/// other arm needs no rebuild, and the manifest records the number that was -/// asked for — never the number the driver granted, which this process cannot -/// read back. +/// default because it is what the runtime shipped, except on macOS: there a +/// latency of one leaves a single free drawable, so the render thread waits for +/// the display before every frame and pipelined rendering gains nothing. The +/// variable exists so the other arm needs no rebuild, and the manifest records +/// the number that was asked for — never the number the driver granted, which +/// this process cannot read back. /// /// The value is a count, not a switch: anything unparseable or zero is the /// default, and says so rather than silently picking an arm. Read once, so the /// window and the manifest cannot disagree and the complaint is made once. pub(crate) fn frame_latency() -> u32 { - const DEFAULT: u32 = 1; + const DEFAULT: u32 = if cfg!(target_os = "macos") { 2 } else { 1 }; static FRAMES: std::sync::OnceLock = std::sync::OnceLock::new(); *FRAMES.get_or_init(|| { let Some(asked) = std::env::var_os(FRAME_LATENCY_ENV) else { diff --git a/docs/BUILD.md b/docs/BUILD.md index c622514..02e5aad 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -32,8 +32,9 @@ make setup-windows # rustup target + cargo-xwin, once 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; `IW4L_PIPELINED_RENDERING=1` turns it on. +`make map mp_boneyard` run on Apple silicon (M2 Max, M4 Pro, Metal). Rendering +is pipelined with a frame latency of 2 there; `IW4L_PIPELINED_RENDERING=0` and +`IW4L_FRAME_LATENCY=1` restore the serialised arm. 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`.