Fix jittering during window resize on MacOS for WGPU/Metal (#7641)

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
ASPCartman 2025-11-01 14:55:56 +03:00 committed by Emil Ernerfeldt
parent e0561e1820
commit e541ba267f
3 changed files with 88 additions and 10 deletions

View File

@ -71,6 +71,7 @@ pub struct SharedState {
painter: egui_wgpu::winit::Painter, painter: egui_wgpu::winit::Painter,
viewport_from_window: HashMap<WindowId, ViewportId>, viewport_from_window: HashMap<WindowId, ViewportId>,
focused_viewport: Option<ViewportId>, focused_viewport: Option<ViewportId>,
resized_viewport: Option<ViewportId>,
} }
pub type Viewports = egui::OrderedViewportIdMap<Viewport>; pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
@ -302,6 +303,7 @@ impl<'app> WgpuWinitApp<'app> {
viewports, viewports,
painter, painter,
focused_viewport: Some(ViewportId::ROOT), focused_viewport: Some(ViewportId::ROOT),
resized_viewport: None,
})); }));
{ {
@ -763,20 +765,34 @@ impl WgpuWinitRunning<'_> {
let viewport_id = shared.viewport_from_window.get(&window_id).copied(); let viewport_id = shared.viewport_from_window.get(&window_id).copied();
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the // On Windows, if a window is resized by the user, it should repaint synchronously, inside the
// event handler. // event handler. If this is not done, the compositor will assume that the window does not want
// // to redraw and continue ahead.
// If this is not done, the compositor will assume that the window does not want to redraw,
// and continue ahead.
// //
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
// new frames to the compositor in time. // new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
//
// The flickering is technically glutin or glow's fault, but we should be responding properly
// to resizes anyway, as doing so avoids dropping frames. // to resizes anyway, as doing so avoids dropping frames.
// //
// See: https://github.com/emilk/egui/issues/903 // See: https://github.com/emilk/egui/issues/903
let mut repaint_asap = false; let mut repaint_asap = false;
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
// the CoreAnimation transactions driving the window resize process.
//
// 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.
//
// Accidental state change during the resize process due to an unexpected event fire
// is ok, state will switch back upon next resize event.
//
// See: https://github.com/emilk/egui/issues/903
if let Some(id) = viewport_id
&& shared.resized_viewport == viewport_id
{
shared.painter.on_window_resize_state_change(id, false);
shared.resized_viewport = None;
}
match event { match event {
winit::event::WindowEvent::Focused(focused) => { winit::event::WindowEvent::Focused(focused) => {
let focused = if cfg!(target_os = "macos") let focused = if cfg!(target_os = "macos")
@ -799,14 +815,18 @@ impl WgpuWinitRunning<'_> {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows. // Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208 // See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows. // This solves an issue where the app would panic when minimizing on Windows.
if let Some(viewport_id) = viewport_id if let Some(id) = viewport_id
&& let (Some(width), Some(height)) = ( && let (Some(width), Some(height)) = (
NonZeroU32::new(physical_size.width), NonZeroU32::new(physical_size.width),
NonZeroU32::new(physical_size.height), NonZeroU32::new(physical_size.height),
) )
{ {
if shared.resized_viewport != viewport_id {
shared.resized_viewport = viewport_id;
shared.painter.on_window_resize_state_change(id, true);
}
shared.painter.on_window_resized(id, width, height);
repaint_asap = true; repaint_asap = true;
shared.painter.on_window_resized(viewport_id, width, height);
} }
} }

View File

@ -25,7 +25,7 @@ all-features = true
rustdoc-args = ["--generate-link-to-definition"] rustdoc-args = ["--generate-link-to-definition"]
[features] [features]
default = ["fragile-send-sync-non-atomic-wasm", "wgpu/default"] default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` ## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
winit = ["dep:winit", "winit/rwh_06"] winit = ["dep:winit", "winit/rwh_06"]
@ -43,6 +43,9 @@ x11 = ["winit?/x11"]
## Thus that usage is guarded against with compiler errors in wgpu. ## Thus that usage is guarded against with compiler errors in wgpu.
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] 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.
macos-window-resize-jitter-fix = ["wgpu/metal"]
[dependencies] [dependencies]
egui = { workspace = true, default-features = false } egui = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false, features = ["bytemuck"] } epaint = { workspace = true, default-features = false, features = ["bytemuck"] }

View File

@ -14,6 +14,7 @@ struct SurfaceState {
alpha_mode: wgpu::CompositeAlphaMode, alpha_mode: wgpu::CompositeAlphaMode,
width: u32, width: u32,
height: u32, height: u32,
resizing: bool,
} }
/// Everything you need to paint egui with [`wgpu`] on [`winit`]. /// Everything you need to paint egui with [`wgpu`] on [`winit`].
@ -230,6 +231,7 @@ impl Painter {
width: size.width, width: size.width,
height: size.height, height: size.height,
alpha_mode, alpha_mode,
resizing: false,
}, },
); );
let Some(width) = NonZeroU32::new(size.width) else { let Some(width) = NonZeroU32::new(size.width) else {
@ -326,6 +328,59 @@ impl Painter {
} }
} }
/// Handles changes of the resizing state.
///
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
profiling::function_scope!();
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
return;
};
if state.resizing == resizing {
if resizing {
log::debug!(
"Painter::on_window_resize_state_change() redundant call while resizing"
);
} else {
log::debug!(
"Painter::on_window_resize_state_change() redundant call after resizing"
);
}
return;
}
// 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
// is common across rendering backends, the solution for wgpu/metal is known.
//
// See https://github.com/emilk/egui/issues/903
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: The cast is checked with if condition. If the used backend is not metal
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
// This is how wgpu currently exposes this backend-specific flag.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
let raw =
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
(*raw).present_with_transaction = resizing;
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
);
}
}
}
state.resizing = resizing;
}
pub fn on_window_resized( pub fn on_window_resized(
&mut self, &mut self,
viewport_id: ViewportId, viewport_id: ViewportId,