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:
parent
e0561e1820
commit
e541ba267f
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue