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 <noreply@anthropic.com>
This commit is contained in:
Ali Sakanak
2026-10-14 23:00:55 +02:00
parent fc3d8eaf60
commit 8798519c32
2 changed files with 20 additions and 12 deletions
+17 -10
View File
@@ -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<u32> = std::sync::OnceLock::new();
*FRAMES.get_or_init(|| {
let Some(asked) = std::env::var_os(FRAME_LATENCY_ENV) else {
+3 -2
View File
@@ -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
<user> +app_update 10190 +quit`, then `IW4L_GAMES=~/Games`.