mirror of
https://github.com/emilk/egui.git
synced 2026-07-04 05:47:26 +00:00
Fix macOS wgpu live resize with low-latency surfaces (#8229)
## Summary This fixes macOS live-resize behavior for the `eframe`/`egui-wgpu` path when using the low-latency wgpu surface configuration. The problem I was seeing is that native window resize can look visibly below the baseline expected from a desktop GUI: stale or stretched frames (manifesting as wobble/jitter), or severe lag while dragging a window edge. The fix has three parts: - use `CAMetalLayer.presentsWithTransaction` during live resize to avoid stale/stretched frames - temporarily use at least `desired_maximum_frame_latency = 2` while live resize is active, so transaction presentation does not stall when the app normally uses `SurfaceConfig::LOW_LATENCY` - treat macOS `WindowEvent::Moved` as part of the live-resize event stream, since resizing from the top or left edge changes the window origin This PR depends on the winit-side AppKit live-resize timing fix in [rust-windowing/winit#4588](https://github.com/rust-windowing/winit/pull/4588) A renderer-only frame-latency change is not enough by itself. The temporary latency bump only solves the drawable starvation caused by combining `presentsWithTransaction` with `SurfaceConfig::LOW_LATENCY`. It does not change when winit emits resize/redraw events, whether redraws are delivered during AppKit's live-resize event-tracking loop, or whether the surface size is derived from the current backing rect. That is why the winit fix is needed first: it makes the windowing layer report the current AppKit backing size and request redraws from the live-resize/display callbacks. egui-wgpu still needs this PR on top because winit does not own the wgpu `Surface` or the underlying `CAMetalLayer` presentation policy. In other words: winit fixes when the windowing layer reports resize/redraw work, while this PR fixes how egui-wgpu presents Metal-backed wgpu frames during that resize. ## Why change the existing feature? The existing `macos-window-resize-jitter-fix` feature addresses one symptom by enabling transaction presentation during resize, but it is not enough for the low-latency wgpu path. In particular, `presentsWithTransaction` and `SurfaceConfig::LOW_LATENCY` interact poorly during AppKit live resize. The old code avoids that by [skipping transaction presentation when latency is `1`](https://github.com/emilk/egui/blob/71c4ff3c337a08bee934f249463d6701bf76b420/crates/egui-wgpu/src/winit.rs#L417), but that means low-latency users get the resize jitter/wobble back. This PR keeps the low-latency path normally, but temporarily bumps frame latency only while live resize is active. That gives the resize path enough drawable slack without changing normal interaction latency. I removed the `macos-window-resize-jitter-fix` feature because this seems like the behavior the macOS wgpu path should have by default, not a separate opt-in. If keeping the feature as a no-op compatibility alias is preferred, I can adjust the PR. ## Validation I created a small demo app that somewhat resembles the layout of my actual app and highlights both horizontal and vertical resize jitter: - a borderless macOS window - a simple toolbar - a scrolling side list - `SurfaceConfig::LOW_LATENCY` The toolbar and list make stale or stretched frames easy to see during native resize. The jitter is visible even on the traffic light buttons. Recordings: ### Before 1: no transaction presentation, low latency Shows jitter/wobble and stale/stretched frames during live resize. https://github.com/user-attachments/assets/2cf4467b-e14c-4f41-8021-0b8c23f41004 ### Before 2: transaction presentation with low latency Shows the other failure mode: live resize can become severely laggy when transaction presentation is used while keeping `SurfaceConfig::LOW_LATENCY`. https://github.com/user-attachments/assets/2f866790-f472-4ede-a3c0-480e8f0f041a ### After: patched egui-wgpu + patched winit, low latency No visible wobble/jitter and no severe live-resize lag. https://github.com/user-attachments/assets/59e46e9f-7906-4b5c-a6c7-1d09eae644cd --------- Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
This commit is contained in:
committed by
GitHub
parent
2e26b70ae9
commit
a8d09eb60d
@@ -844,13 +844,18 @@ impl WgpuWinitRunning<'_> {
|
||||
//
|
||||
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
|
||||
// resize lifecycle, yet winit does not provide any events for that. To work around,
|
||||
// the last resized viewport is tracked until any next non-resize event is received.
|
||||
// the last resized viewport is tracked until a later event outside the live resize stream
|
||||
// is received.
|
||||
//
|
||||
// Accidental state change during the resize process due to an unexpected event fire
|
||||
// is ok, state will switch back upon next resize event.
|
||||
// AppKit can emit `Moved` events during top/left live resize because the window origin
|
||||
// changes along with the content size. Treat those as part of live resize on macOS.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
if let Some(id) = viewport_id
|
||||
let event_keeps_resize_active = matches!(event, winit::event::WindowEvent::Resized(_))
|
||||
|| (cfg!(target_os = "macos") && matches!(event, winit::event::WindowEvent::Moved(_)));
|
||||
|
||||
if !event_keeps_resize_active
|
||||
&& let Some(id) = viewport_id
|
||||
&& shared.resized_viewport == viewport_id
|
||||
{
|
||||
shared.painter.on_window_resize_state_change(id, false);
|
||||
|
||||
@@ -51,7 +51,9 @@ x11 = ["winit?/x11"]
|
||||
## Thus that usage is guarded against with compiler errors in wgpu.
|
||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
||||
|
||||
## Enables `present_with_transaction` surface flag temporary during window resize on MacOS.
|
||||
## Enables the macOS live-resize jitter fix, which uses `present_with_transaction`.
|
||||
## This requires the `wgpu/metal` backend. Disable this feature if you want to use egui-wgpu
|
||||
## with a different backend (e.g. Vulkan or GL) on macOS without pulling in Metal.
|
||||
macos-window-resize-jitter-fix = ["wgpu/metal"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -104,6 +104,15 @@ impl Painter {
|
||||
desired_maximum_frame_latency,
|
||||
} = *config;
|
||||
|
||||
// Transaction presentation can hold a drawable during AppKit live resize. Keep the
|
||||
// configured low-latency path normally, but use three Metal drawables while resizing.
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
let desired_maximum_frame_latency = if surface_state.resizing {
|
||||
Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
|
||||
} else {
|
||||
desired_maximum_frame_latency
|
||||
};
|
||||
|
||||
let width = surface_state.width;
|
||||
let height = surface_state.height;
|
||||
|
||||
@@ -406,6 +415,9 @@ impl Painter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set before reconfiguring so macOS live resize uses the temporary latency bump above.
|
||||
state.resizing = resizing;
|
||||
|
||||
// Resizing is a bit tricky on macOS.
|
||||
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
|
||||
// flag to avoid jittering during the resize. Even though resize jittering on macOS
|
||||
@@ -414,34 +426,24 @@ impl Painter {
|
||||
// See https://github.com/emilk/egui/issues/903
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
{
|
||||
// setPresentsWithTransaction causes hangs when desired_maximum_frame_latency == 1
|
||||
let is_low_latency = self
|
||||
.render_state
|
||||
.as_ref()
|
||||
.is_some_and(|rs| rs.surface_config.desired_maximum_frame_latency == Some(1));
|
||||
if !is_low_latency {
|
||||
// SAFETY: The cast is checked with if condition. If the used backend is not metal
|
||||
// it gracefully fails.
|
||||
// SAFETY: `as_hal::<Metal>()` returns `None` unless this surface is backed by wgpu's
|
||||
// Metal backend.
|
||||
unsafe {
|
||||
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
|
||||
if let (Some(render_state), Some(hal_surface)) = (
|
||||
self.render_state.as_ref(),
|
||||
state.surface.as_hal::<wgpu::hal::api::Metal>(),
|
||||
) {
|
||||
hal_surface
|
||||
.render_layer()
|
||||
.lock()
|
||||
.setPresentsWithTransaction(resizing);
|
||||
|
||||
Self::configure_surface(
|
||||
state,
|
||||
self.render_state.as_ref().unwrap(),
|
||||
&self.config.surface,
|
||||
);
|
||||
Self::configure_surface(state, render_state, &self.config.surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.resizing = resizing;
|
||||
}
|
||||
|
||||
pub fn on_window_resized(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
|
||||
Reference in New Issue
Block a user