gpui_linux: Force a full render on X11 Expose after GPU recovery (#64570)

# Objective

Zed on X11 could crash with `index out of bounds: the len is 0 but the
index is 0` in `WgpuAtlas::get_texture_info` right after recovering from
a lost GPU device (Sentry ZED-BVQ, Linear FR-488).

## Solution

When the device is lost, `X11Window::draw` recreates the renderer, which
clears the sprite atlas, and sets `force_render_after_recovery` so the
next frame rebuilds the scene instead of replaying the cached one. Only
the periodic refresh timer consumed that flag though. An `Expose` event
handled in the same event loop turn (a compositor repainting after the
same GPU reset, for example) requested a presentation-only frame with
`force_render: false`, so GPUI presented the previously rendered scene,
whose sprites still pointed at atlas textures that no longer exist.

We now consume the flag in `X11WindowStatePtr::refresh`, so every frame
request, Expose or timer, forces a full render after recovery. The field
is private now so a caller can't bypass it again.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed a crash on Linux (X11) that could occur when a window was
redrawn immediately after recovering from a lost GPU device.
- [GPUI] Fixed a crash on Linux (X11) when an `Expose` event presented a
cached frame immediately after GPU device recovery.
This commit is contained in:
Agus Zubiaga authored and GitHub committed 2026-09-22 00:11:23 +00:00
1 parent 1df382558f
commit 9d801eec7a
2 files changed
+7 -6

No files matched your search

+1 -4
View File
@@ -2007,14 +2007,11 @@ impl X11ClientState {
let mut state = client.0.borrow_mut();
let xcb_connection = state.xcb_connection.clone();
if let Some(window) = state.windows.get_mut(&x_window) {
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: false,
force_render,
force_render: false,
});
}
xcb_connection
+6 -2
View File
@@ -283,7 +283,7 @@ pub struct X11WindowState {
/// `VisibilityNotify`; this is the last value it reported.
visibility: WindowVisibility,
hovered: bool,
pub(crate) force_render_after_recovery: bool,
force_render_after_recovery: bool,
fullscreen: bool,
client_side_decorations_supported: bool,
decorations: WindowDecorations,
@@ -1181,9 +1181,13 @@ impl X11WindowStatePtr {
}
}
pub fn refresh(&self, request_frame_options: RequestFrameOptions) {
pub fn refresh(&self, mut request_frame_options: RequestFrameOptions) {
let callback = self.callbacks.borrow_mut().request_frame.take();
if let Some(mut fun) = callback {
// Expose events can present a frame before the refresh timer runs,
// so every frame request must rebuild stale atlas references after recovery.
request_frame_options.force_render |=
std::mem::take(&mut self.state.borrow_mut().force_render_after_recovery);
fun(request_frame_options);
self.callbacks.borrow_mut().request_frame = Some(fun);
}