mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
Fix random hangs by improving wgpu::Surface lifecycle handling (#8171)
Backport of #8171 for the 0.34.3 patch release, adapted to avoid the breaking API changes on main (the `window` argument to the public `Painter::paint_and_update_textures` and the new `SurfaceErrorAction` variant). The owned window handle is stashed in `SurfaceState` instead. Co-authored-by: Jochen Görtler <grtlr@users.noreply.github.com>
This commit is contained in:
@@ -266,7 +266,12 @@ pub enum SurfaceErrorAction {
|
|||||||
/// Do nothing and skip the current frame.
|
/// Do nothing and skip the current frame.
|
||||||
SkipFrame,
|
SkipFrame,
|
||||||
|
|
||||||
/// Instructs egui to recreate the surface, then skip the current frame.
|
/// Recover the surface, then skip the current frame.
|
||||||
|
///
|
||||||
|
/// egui reconfigures the existing surface for [`wgpu::CurrentSurfaceTexture::Outdated`], and
|
||||||
|
/// drops & recreates it via [`wgpu::Instance::create_surface`] for
|
||||||
|
/// [`wgpu::CurrentSurfaceTexture::Lost`] (where reconfiguring the same surface object cannot
|
||||||
|
/// recover).
|
||||||
RecreateSurface,
|
RecreateSurface,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,23 +337,28 @@ impl Default for WgpuConfiguration {
|
|||||||
// No display handle available at this point — callers should replace this with
|
// No display handle available at this point — callers should replace this with
|
||||||
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
|
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
|
||||||
wgpu_setup: WgpuSetup::without_display_handle(),
|
wgpu_setup: WgpuSetup::without_display_handle(),
|
||||||
on_surface_status: Arc::new(|status| {
|
on_surface_status: Arc::new(|status| match status {
|
||||||
match status {
|
|
||||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||||
// This error occurs when the app is minimized on Windows.
|
// The compositor changed the surface (resize, scale, output, …). wgpu
|
||||||
// Silently return here to prevent spamming the console with:
|
// requires us to reconfigure before the next acquire. Skipping would mean
|
||||||
// "The underlying surface has changed, and therefore the swap chain must be updated"
|
// we are stuck in `Outdated` forever (e.g. spinner not spinning on Wayland).
|
||||||
|
log::trace!("Dropped frame with error: {status:?}");
|
||||||
|
SurfaceErrorAction::RecreateSurface
|
||||||
|
}
|
||||||
|
wgpu::CurrentSurfaceTexture::Lost => {
|
||||||
|
// The underlying surface is gone and we need a fresh one from the `wgpu::Instance`.
|
||||||
|
log::debug!("Dropped frame with error: {status:?}");
|
||||||
|
SurfaceErrorAction::RecreateSurface
|
||||||
}
|
}
|
||||||
wgpu::CurrentSurfaceTexture::Occluded => {
|
wgpu::CurrentSurfaceTexture::Occluded => {
|
||||||
// This error occurs when the application is occluded (e.g. minimized or behind another window).
|
// App is hidden (minimized / behind another window). Skip silently.
|
||||||
log::debug!("Dropped frame with error: {status:?}");
|
log::trace!("Skipping frame due to occlusion.");
|
||||||
|
SurfaceErrorAction::SkipFrame
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("Dropped frame with error: {status:?}");
|
log::warn!("Dropped frame with error: {status:?}");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceErrorAction::SkipFrame
|
SurfaceErrorAction::SkipFrame
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ struct SurfaceState {
|
|||||||
height: u32,
|
height: u32,
|
||||||
resizing: bool,
|
resizing: bool,
|
||||||
needs_reconfigure: bool,
|
needs_reconfigure: bool,
|
||||||
|
|
||||||
|
// Set when the previous frame observed `wgpu::CurrentSurfaceTexture::Lost`. Triggers a
|
||||||
|
// full surface recreation at the start of the next frame (see `recreate_surface`).
|
||||||
|
needs_recreate: bool,
|
||||||
|
|
||||||
|
// Backport of #8171 for the 0.34.3 patch release: recovering from a `Lost` surface requires
|
||||||
|
// dropping the old surface and creating a fresh one, which needs the window handle. On `main`
|
||||||
|
// the window is passed into `paint_and_update_textures`, but adding that argument here would be
|
||||||
|
// a breaking change to a public method, so for the patch we stash an owned handle instead.
|
||||||
|
// `None` when the surface was created via `set_window_unsafe` (no owned window available).
|
||||||
|
window_for_surface_recreation: Option<Arc<winit::window::Window>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||||
@@ -153,8 +164,9 @@ impl Painter {
|
|||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
if !self.surfaces.contains_key(&viewport_id) {
|
if !self.surfaces.contains_key(&viewport_id) {
|
||||||
let surface = self.instance.create_surface(window)?;
|
let surface = self.instance.create_surface(Arc::clone(&window))?;
|
||||||
self.add_surface(surface, viewport_id, size).await?;
|
self.add_surface(surface, viewport_id, size, Some(window))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("No window - clearing all surfaces");
|
log::warn!("No window - clearing all surfaces");
|
||||||
@@ -183,7 +195,7 @@ impl Painter {
|
|||||||
self.instance
|
self.instance
|
||||||
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
|
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
|
||||||
};
|
};
|
||||||
self.add_surface(surface, viewport_id, size).await?;
|
self.add_surface(surface, viewport_id, size, None).await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("No window - clearing all surfaces");
|
log::warn!("No window - clearing all surfaces");
|
||||||
@@ -197,10 +209,9 @@ impl Painter {
|
|||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
viewport_id: ViewportId,
|
viewport_id: ViewportId,
|
||||||
size: winit::dpi::PhysicalSize<u32>,
|
size: winit::dpi::PhysicalSize<u32>,
|
||||||
|
window_for_surface_recreation: Option<Arc<winit::window::Window>>,
|
||||||
) -> Result<(), crate::WgpuError> {
|
) -> Result<(), crate::WgpuError> {
|
||||||
let render_state = if let Some(render_state) = &self.render_state {
|
if self.render_state.is_none() {
|
||||||
render_state
|
|
||||||
} else {
|
|
||||||
let render_state = RenderState::create(
|
let render_state = RenderState::create(
|
||||||
&self.configuration,
|
&self.configuration,
|
||||||
&self.instance,
|
&self.instance,
|
||||||
@@ -208,10 +219,39 @@ impl Painter {
|
|||||||
self.options,
|
self.options,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
self.render_state.get_or_insert(render_state)
|
self.render_state = Some(render_state);
|
||||||
};
|
}
|
||||||
let alpha_mode = if self.support_transparent_backbuffer {
|
self.install_surface(
|
||||||
let supported_alpha_modes = surface.get_capabilities(&render_state.adapter).alpha_modes;
|
surface,
|
||||||
|
viewport_id,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
false,
|
||||||
|
window_for_surface_recreation,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
|
||||||
|
///
|
||||||
|
/// Render state must already be initialised before calling this.
|
||||||
|
fn install_surface(
|
||||||
|
&mut self,
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
viewport_id: ViewportId,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
resizing: bool,
|
||||||
|
window_for_surface_recreation: Option<Arc<winit::window::Window>>,
|
||||||
|
) {
|
||||||
|
let alpha_mode = {
|
||||||
|
let render_state = self
|
||||||
|
.render_state
|
||||||
|
.as_ref()
|
||||||
|
.expect("install_surface called before render_state initialization");
|
||||||
|
if self.support_transparent_backbuffer {
|
||||||
|
let supported_alpha_modes =
|
||||||
|
surface.get_capabilities(&render_state.adapter).alpha_modes;
|
||||||
|
|
||||||
// Prefer pre multiplied over post multiplied!
|
// Prefer pre multiplied over post multiplied!
|
||||||
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
|
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
|
||||||
@@ -226,27 +266,57 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wgpu::CompositeAlphaMode::Auto
|
wgpu::CompositeAlphaMode::Auto
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.surfaces.insert(
|
self.surfaces.insert(
|
||||||
viewport_id,
|
viewport_id,
|
||||||
SurfaceState {
|
SurfaceState {
|
||||||
surface,
|
surface,
|
||||||
width: size.width,
|
width,
|
||||||
height: size.height,
|
height,
|
||||||
alpha_mode,
|
alpha_mode,
|
||||||
resizing: false,
|
resizing,
|
||||||
needs_reconfigure: false,
|
needs_reconfigure: false,
|
||||||
|
needs_recreate: false,
|
||||||
|
window_for_surface_recreation,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let Some(width) = NonZeroU32::new(size.width) else {
|
let Some(width) = NonZeroU32::new(width) else {
|
||||||
log::debug!("The window width was zero; skipping generate textures");
|
log::debug!("The window width was zero; skipping generate textures");
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
let Some(height) = NonZeroU32::new(size.height) else {
|
let Some(height) = NonZeroU32::new(height) else {
|
||||||
log::debug!("The window height was zero; skipping generate textures");
|
log::debug!("The window height was zero; skipping generate textures");
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
|
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the stored
|
||||||
|
/// window via [`wgpu::Instance::create_surface`], then configure it.
|
||||||
|
///
|
||||||
|
/// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the existing
|
||||||
|
/// surface object cannot recover. Backport of #8171 for the 0.34.3 patch release; see the note
|
||||||
|
/// on [`SurfaceState::window_for_surface_recreation`].
|
||||||
|
fn recreate_surface(&mut self, viewport_id: ViewportId) -> Result<(), crate::WgpuError> {
|
||||||
|
profiling::function_scope!();
|
||||||
|
|
||||||
|
let Some(old_state) = self.surfaces.get(&viewport_id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(window) = old_state.window_for_surface_recreation.clone() else {
|
||||||
|
// Surface was created via `set_window_unsafe`; we have no owned window to recreate from.
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let width = old_state.width;
|
||||||
|
let height = old_state.height;
|
||||||
|
let resizing = old_state.resizing;
|
||||||
|
|
||||||
|
// Drop the old surface before creating the new one.
|
||||||
|
self.surfaces.remove(&viewport_id);
|
||||||
|
|
||||||
|
let surface = self.instance.create_surface(Arc::clone(&window))?;
|
||||||
|
self.install_surface(surface, viewport_id, width, height, resizing, Some(window));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +517,19 @@ impl Painter {
|
|||||||
let capture = !capture_data.is_empty();
|
let capture = !capture_data.is_empty();
|
||||||
let mut vsync_sec = 0.0;
|
let mut vsync_sec = 0.0;
|
||||||
|
|
||||||
|
// If the previous frame produced `CurrentSurfaceTexture::Lost`, the match below set
|
||||||
|
// `needs_recreate`. Recreate the surface now, before borrowing `render_state` / `surfaces`
|
||||||
|
// for the rest of the paint (see #8171).
|
||||||
|
if self
|
||||||
|
.surfaces
|
||||||
|
.get(&viewport_id)
|
||||||
|
.is_some_and(|s| s.needs_recreate)
|
||||||
|
&& let Err(err) = self.recreate_surface(viewport_id)
|
||||||
|
{
|
||||||
|
log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
|
||||||
|
return vsync_sec;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(render_state) = self.render_state.as_mut() else {
|
let Some(render_state) = self.render_state.as_mut() else {
|
||||||
return vsync_sec;
|
return vsync_sec;
|
||||||
};
|
};
|
||||||
@@ -516,7 +599,22 @@ impl Painter {
|
|||||||
other => {
|
other => {
|
||||||
match (*self.configuration.on_surface_status)(&other) {
|
match (*self.configuration.on_surface_status)(&other) {
|
||||||
SurfaceErrorAction::RecreateSurface => {
|
SurfaceErrorAction::RecreateSurface => {
|
||||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
if matches!(other, wgpu::CurrentSurfaceTexture::Lost) {
|
||||||
|
// The surface is gone; reconfiguring the same object cannot recover.
|
||||||
|
// We can't drop & recreate it here while `surface_state` /
|
||||||
|
// `render_state` are borrowed, so defer it to the start of the next
|
||||||
|
// frame (which we ensure arrives via `request_repaint_of`). See #8171.
|
||||||
|
surface_state.needs_recreate = true;
|
||||||
|
} else {
|
||||||
|
// `Outdated` (and other recoverable statuses): reconfiguring the
|
||||||
|
// existing surface is enough.
|
||||||
|
Self::configure_surface(
|
||||||
|
surface_state,
|
||||||
|
render_state,
|
||||||
|
&self.configuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.context.request_repaint_of(viewport_id);
|
||||||
}
|
}
|
||||||
SurfaceErrorAction::SkipFrame => {}
|
SurfaceErrorAction::SkipFrame => {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user