PERF-WINDOW: the window queues one frame, not two (eframe's low-latency surface)

window_gpu::base_options() built the window's wgpu options from
egui_wgpu::WgpuConfiguration::default(), which is egui-wgpu's HIGH_THROUGHPUT
surface (desired_maximum_frame_latency 2; three drawables on Metal), not
eframe's default. eframe's NativeOptions::default() uses LOW_LATENCY
(latency 1). The window had eframe's default until #136 started building
the options by hand to choose the backends.

Start from eframe::NativeOptions::default().wgpu_options and change only
the backends, the DX12 compiler and the instance flags, as before. The
probed Windows configurations are built on the same options.

Measured on an M2 Max at 120 Hz (Metal System Trace, heavy develop drag,
presents joined to WindowServer's reads of the same IOSurface): the window
cycles 2 drawables instead of 3, and a frame reaches the compositor about
8.3 ms (one frame) sooner, 9.5-10.6 ms instead of 17.9-18.9 ms after it is
presented, at the same ~119 frames a second.

Test: window_gpu::tests::window_surface_keeps_one_frame_in_flight (fails
on the old options: latency Some(2) instead of Some(1)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jason Luker
2026-10-25 02:19:57 -07:00
co-authored by Claude Opus 5.5
parent 31ec62cd03
commit 2dd02f6f97
3 changed files with 42 additions and 1 deletions
+23 -1
View File
@@ -142,8 +142,13 @@ fn user_notice(failed: Option<WindowRenderer>, forced: Option<WindowRenderer>, p
}
/// eframe's defaults with the app's backends and DX12 shader compiler (issues #136, #471).
///
/// Taken from `NativeOptions::default()`, whose surface is egui-wgpu's `LOW_LATENCY` (one frame in
/// flight; two drawables on Metal). `WgpuConfiguration::default()` is `HIGH_THROUGHPUT` instead (two
/// frames queued, three drawables): every drag, slider and crop handle reached the screen a frame
/// later than eframe's default. Live window resizing still gets three drawables from egui-wgpu.
pub fn base_options() -> WgpuConfiguration {
let mut c = WgpuConfiguration::default();
let mut c = eframe::NativeOptions::default().wgpu_options;
if let WgpuSetup::CreateNew(n) = &mut c.wgpu_setup {
n.instance_descriptor.backends = backend::window_backends();
n.instance_descriptor.backend_options = backend::backend_options();
@@ -290,6 +295,23 @@ mod tests {
assert!(died.contains("last time") && !died.contains("Validation Error"), "{died}");
}
/// The window keeps eframe's low-latency surface: one frame queued (two Metal drawables), not
/// the high-throughput `WgpuConfiguration::default()`, which held input a frame longer. The
/// probed (Windows) and forced-backend configurations are built on the same options.
#[test]
fn window_surface_keeps_one_frame_in_flight() {
let surface = base_options().surface;
assert_eq!(surface, eframe::NativeOptions::default().wgpu_options.surface);
assert_eq!(surface, eframe::egui_wgpu::SurfaceConfig::LOW_LATENCY);
assert_eq!(surface.desired_maximum_frame_latency, Some(1));
assert_eq!(surface.present_mode, wgpu::PresentMode::AutoVsync);
assert_ne!(surface, WgpuConfiguration::default().surface, "egui-wgpu's own default is the high-throughput one");
// what `main` hands eframe (on Windows `choose` probes the adapters first; not here)
if !cfg!(windows) {
assert_eq!(choose(&Files::new(None)).wgpu_options.surface, surface);
}
}
/// The crash report: a pipeline that fails validation must come back as an error, never as
/// wgpu's default panic. Needs an adapter; skipped where there is none (CI without a GPU).
#[test]
+9
View File
@@ -113,6 +113,15 @@ renderer last. `LIGHTCRAFT_GPU_BACKEND=dx12 | vulkan | gl | glow` (or `WGPU_BACK
first because a broken Vulkan driver can crash the process uncatchably (#136). Compute keeps its own
device and falls back to the CPU as before.
**The window's surface: one frame in flight.** The window's wgpu options start from eframe's defaults
(`NativeOptions::default().wgpu_options`, `window_gpu::base_options`), whose surface is egui-wgpu's
`SurfaceConfig::LOW_LATENCY`: vsync, at most one frame queued ahead of the display (two drawables on
Metal). egui-wgpu's bare `WgpuConfiguration::default()` is `HIGH_THROUGHPUT` (two frames queued, three
drawables), and the window used it from #136 until this change, so a drag reached the screen one frame
(8.3 ms at 120 Hz, 16.7 ms at 60 Hz) later. The window's own GPU work is small (egui's meshes and
texture uploads; renders run on the compute device), which is what `LOW_LATENCY` is meant for.
During a live window resize egui-wgpu still uses three drawables on macOS.
**Troubleshooting a crash at startup (Windows).** Start LightCraft from a `.cmd` file or a terminal
with `set LIGHTCRAFT_GPU_BACKEND=dx12` (the default since #136), or `=off` to keep the GPU out of
rendering; `set VK_LOADER_DRIVERS_DISABLE=*igvk64*` (Vulkan loader) hides a specific Vulkan driver
+10
View File
@@ -1,5 +1,15 @@
# What's new in LightCraft
## 2026-10-25
### Drags reach the screen a frame sooner
- The window queues one frame ahead of the display again instead of two, as eframe does by default (since #136 it
used egui-wgpu's high-throughput setting). Sliders, crop handles, brushes and every other drag show up one display
frame sooner: 8.3 ms at 120 Hz, 16.7 ms at 60 Hz. On a MacBook Pro at 120 Hz, a heavy develop drag went from
about 18 ms to about 10 ms between a frame being handed to macOS and macOS compositing it, with the same 120 frames
a second. On a Mac the window also holds one drawable less: its width × height × 4 bytes, 15–22 MB for a
1200–1500-point-wide window on a Retina display.
## 2026-10-24
### Relative-date filters and smart albums follow the clock