1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Fix random hangs by improving wgpu::Surface lifecycle handling (#8171)

### Related

* Closes #8134.
* Related to #5136.

Possibly fixes:

* #8123
* #5145 

### What

We did not properly handle the variants of
[`CurrentSurfaceTexture`](https://docs.rs/wgpu/latest/wgpu/enum.CurrentSurfaceTexture.html)
and always returned `SkipFrame`.

Because of this `egui` could end up in a state where frames are always
skipped after observing `Outdated`, without the chance to recover
(unless an event arrives from the outside).

> [!NOTE]
> This is not Wayland-specific, but could happen on all platforms. It
just happens frequently for Wayland compositors that directly resize a
window after creation (such as tiling/scrolling compositors like
`hyprland` and `niri`).

This PR improves this by separating the code paths for `Outdated` and
`Lost`, to help recover from those events.
This commit is contained in:
Jochen Görtler
2026-05-19 11:06:09 +02:00
committed by GitHub
parent 82aaef3530
commit 7dba2e99fa
3 changed files with 163 additions and 45 deletions

View File

@@ -12,6 +12,7 @@ use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
@@ -23,6 +24,7 @@ pub(crate) struct WebPainterWgpu {
capture_rx: CaptureReceiver,
ctx: egui::Context,
needs_reconfigure: bool,
needs_recreate: bool,
}
/// Owned web display handle that is `Send + Sync`.
@@ -129,6 +131,7 @@ impl WebPainterWgpu {
Ok(Self {
canvas,
instance,
render_state: Some(render_state),
surface,
surface_configuration,
@@ -140,6 +143,7 @@ impl WebPainterWgpu {
capture_rx,
ctx,
needs_reconfigure: false,
needs_recreate: false,
})
}
}
@@ -173,6 +177,24 @@ impl WebPainter for WebPainterWgpu {
));
};
// If the previous frame produced `CurrentSurfaceTexture::Lost`, drop and recreate the
// surface from the canvas before re-borrowing `self.render_state` for the rest of paint.
if self.needs_recreate {
self.needs_recreate = false;
match self
.instance
.create_surface(wgpu::SurfaceTarget::Canvas(self.canvas.clone()))
{
Ok(new_surface) => {
new_surface.configure(&render_state.device, &self.surface_configuration);
self.surface = new_surface;
}
Err(err) => {
log::error!("Failed to recreate wgpu surface for canvas: {err}");
}
}
}
let mut encoder =
render_state
.device
@@ -239,10 +261,18 @@ impl WebPainter for WebPainterWgpu {
}
other => {
match (*self.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
SurfaceErrorAction::Reconfigure => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
}
SurfaceErrorAction::RecreateSurface => {
// Full recovery needs `&mut self`, which conflicts with the live
// `render_state` / `self.surface` borrows here. Defer to the top
// of the next paint via the `needs_recreate` flag, and request a
// repaint so the next frame actually invokes `paint` to consume it.
self.needs_recreate = true;
self.ctx.request_repaint();
}
SurfaceErrorAction::SkipFrame => {}
}
return Ok(());