gpui_linux: Force scene rebuild after GPU device recovery (#52389)

Lukas Wirth created

After GPU device recovery clears the atlas, the next frame could
re-present a stale scene via the needs_present path (which skips scene
rebuilding). The stale scene references texture IDs that no longer exist
in the cleared atlas, causing an index-out-of-bounds panic.

Fix this by setting a force_render_after_recovery flag when device
recovery completes. The platform refresh loop reads this flag and passes
force_render: true in RequestFrameOptions, ensuring GPUI rebuilds the
scene before presenting.

Fixes ZED-5QT

Release Notes:

- N/A or Added/Fixed/Improved ...

Change summary

crates/gpui_linux/src/linux/wayland/window.rs | 10 +++++++++-
crates/gpui_linux/src/linux/x11/client.rs     |  5 ++++-
crates/gpui_linux/src/linux/x11/window.rs     |  3 +++
crates/gpui_windows/src/events.rs             |  9 +++++----
4 files changed, 21 insertions(+), 6 deletions(-)

Detailed changes

crates/gpui_linux/src/linux/wayland/window.rs 🔗

@@ -116,6 +116,7 @@ pub struct WaylandWindowState {
     handle: AnyWindowHandle,
     active: bool,
     hovered: bool,
+    pub(crate) force_render_after_recovery: bool,
     in_progress_configure: Option<InProgressConfigure>,
     resize_throttle: bool,
     in_progress_window_controls: Option<WindowControls>,
@@ -389,6 +390,7 @@ impl WaylandWindowState {
             handle,
             active: false,
             hovered: false,
+            force_render_after_recovery: false,
             in_progress_window_controls: None,
             window_controls: WindowControls::default(),
             client_inset: None,
@@ -570,11 +572,16 @@ impl WaylandWindowStatePtr {
         let mut state = self.state.borrow_mut();
         state.surface.frame(&state.globals.qh, state.surface.id());
         state.resize_throttle = false;
+        let force_render = state.force_render_after_recovery;
+        state.force_render_after_recovery = false;
         drop(state);
 
         let mut cb = self.callbacks.borrow_mut();
         if let Some(fun) = cb.request_frame.as_mut() {
-            fun(Default::default());
+            fun(RequestFrameOptions {
+                force_render,
+                ..Default::default()
+            });
         }
     }
 
@@ -1372,6 +1379,7 @@ impl PlatformWindow for WaylandWindow {
 
             // The current scene references atlas textures that were cleared during recovery.
             // Skip this frame and let the next frame rebuild the scene with fresh textures.
+            state.force_render_after_recovery = true;
             return;
         }
 

crates/gpui_linux/src/linux/x11/client.rs 🔗

@@ -1883,11 +1883,14 @@ impl X11ClientState {
                         if let Some(window) = state.windows.get_mut(&x_window) {
                             let expose_event_received = window.expose_event_received;
                             window.expose_event_received = false;
+                            let force_render = std::mem::take(
+                                &mut window.window.state.borrow_mut().force_render_after_recovery,
+                            );
                             let window = window.window.clone();
                             drop(state);
                             window.refresh(RequestFrameOptions {
                                 require_presentation: expose_event_received,
-                                force_render: false,
+                                force_render,
                             });
                         }
                         xcb_connection

crates/gpui_linux/src/linux/x11/window.rs 🔗

@@ -277,6 +277,7 @@ pub struct X11WindowState {
     hidden: bool,
     active: bool,
     hovered: bool,
+    pub(crate) force_render_after_recovery: bool,
     fullscreen: bool,
     client_side_decorations_supported: bool,
     decorations: WindowDecorations,
@@ -750,6 +751,7 @@ impl X11WindowState {
                 input_handler: None,
                 active: false,
                 hovered: false,
+                force_render_after_recovery: false,
                 fullscreen: false,
                 maximized_vertical: false,
                 maximized_horizontal: false,
@@ -1637,6 +1639,7 @@ impl PlatformWindow for X11Window {
 
             // The current scene references atlas textures that were cleared during recovery.
             // Skip this frame and let the next frame rebuild the scene with fresh textures.
+            inner.force_render_after_recovery = true;
             return;
         }
 

crates/gpui_windows/src/events.rs 🔗

@@ -1143,10 +1143,11 @@ impl WindowsWindowInner {
     fn draw_window(&self, handle: HWND, force_render: bool) -> Option<isize> {
         let mut request_frame = self.state.callbacks.request_frame.take()?;
 
-        // we are instructing gpui to force render a frame, this will
-        // re-populate all the gpu textures for us so we can resume drawing in
-        // case we disabled drawing earlier due to a device loss
-        self.state.renderer.borrow_mut().mark_drawable();
+        if force_render {
+            // Re-enable drawing after a device loss recovery. The forced render
+            // will rebuild the scene with fresh atlas textures.
+            self.state.renderer.borrow_mut().mark_drawable();
+        }
         request_frame(RequestFrameOptions {
             require_presentation: false,
             force_render,