gpui_linux: Fix Wayland flickering under CPU load by skipping redundant surface commit (#54214)

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 is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #32792 
Closes #38266 
Closes #54133

Release Notes:

- Fixed graphical corruption that could occur when using Wayland

----

**What**: Fixes flickering on Wayland (Sway/wlroots) under CPU load
(e.g. rust-analyzer running). The bug only reproduces in release builds
- debug builds are too slow to hit the race window.

Environment where this was reproduced: Intel GPU (both Xe KMD and i915),
Mesa 26.0.4, Sway 1.11.

**Why**: When wgpu presents a frame on Wayland+Vulkan, it calls
`vkQueuePresentKHR,` which - as required by the Vulkan spec -
synchronously issues `wl_surface.attach`, `wl_surface.damage`, and
`wl_surface.commit` to the compositor before returning. The commit also
picks up any pending frame callbacks.

Zed's `completed_frame()` independently calls `state.surface.commit()`.
This is a redundant second commit on the same surface. Under load, the
Wayland socket dispatch can be delayed enough that both commits are in
flight in close succession, and the ordering becomes timing-dependent.
When Zed's commit reaches the compositor before Mesa's attach+commit
sequence has been fully flushed, the compositor sees a commit with no
buffer attached, fires `wl_callback::Done` immediately, and Zed starts
the next frame too early - Mesa's real buffer arrives late, causing the
visible flicker.

Under no load, Mesa's synchronous commit consistently reaches the
compositor first, so the bug doesn't appear.

**Fix**: Track whether `renderer.draw()` actually called
`frame.present()`. When it did, Mesa owns the `wl_surface.commit()` for
that frame - skip Zed's commit in `completed_frame()`. Only commit
ourselves when wgpu didn't present (surface not configured, lost,
occluded, etc.) - in those cases Mesa won't commit, and we need to keep
the frame callback alive.

---------

Co-authored-by: Benjamin Laib <b5l@users.noreply.github.com>
Co-authored-by: John Tur <john-tur@outlook.com>
This commit is contained in:
authored and GitHub committed 2026-05-06 03:40:02 +00:00
1 parent 5fb7238037
commit 2ae08181de
2 files changed
+21 -12

No files matched your search

+12 -3
View File
@@ -117,6 +117,7 @@ pub struct WaylandWindowState {
active: bool,
hovered: bool,
pub(crate) force_render_after_recovery: bool,
renderer_presented: bool,
in_progress_configure: Option<InProgressConfigure>,
resize_throttle: bool,
in_progress_window_controls: Option<WindowControls>,
@@ -392,6 +393,7 @@ impl WaylandWindowState {
active: false,
hovered: false,
force_render_after_recovery: false,
renderer_presented: false,
in_progress_window_controls: None,
window_controls: WindowControls::default(),
client_inset: None,
@@ -1398,7 +1400,7 @@ impl PlatformWindow for WaylandWindow {
return;
}
state.renderer.draw(scene);
state.renderer_presented = state.renderer.draw(scene);
if state.renderer.needs_redraw() {
state.force_render_after_recovery = true;
@@ -1406,8 +1408,15 @@ impl PlatformWindow for WaylandWindow {
}
fn completed_frame(&self) {
let state = self.borrow();
state.surface.commit();
let mut state = self.borrow_mut();
// Work around a bug in old versions of wlroots where committing without a buffer attached
// can cause invalid synchronization that leads to graphical corruption.
if !state.renderer_presented {
state.surface.commit();
}
state.renderer_presented = false;
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
+9 -9
View File
@@ -1079,13 +1079,13 @@ impl WgpuRenderer {
self.max_texture_size
}
pub fn draw(&mut self, scene: &Scene) {
pub fn draw(&mut self, scene: &Scene) -> bool {
// Bail out early if the surface has been unconfigured (e.g. during
// Android background/rotation transitions). Attempting to acquire
// a texture from an unconfigured surface can block indefinitely on
// some drivers (Adreno).
if !self.surface_configured {
return;
return false;
}
let last_error = self.last_error.lock().unwrap().take();
@@ -1106,7 +1106,7 @@ impl WgpuRenderer {
self.atlas.clear();
self.needs_redraw = true;
self.failed_frame_count = 0;
return;
return false;
}
} else {
self.failed_frame_count = 0;
@@ -1124,7 +1124,7 @@ impl WgpuRenderer {
resources
.surface
.configure(&resources.device, &surface_config);
return;
return false;
}
wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
let surface_config = self.surface_config.clone();
@@ -1132,15 +1132,15 @@ impl WgpuRenderer {
resources
.surface
.configure(&resources.device, &surface_config);
return;
return false;
}
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
return;
return false;
}
wgpu::CurrentSurfaceTexture::Validation => {
*self.last_error.lock().unwrap() =
Some("Surface texture validation error".to_string());
return;
return false;
}
};
@@ -1321,7 +1321,7 @@ impl WgpuRenderer {
self.instance_buffer_capacity
);
frame.present();
return;
return true;
}
self.grow_instance_buffer();
continue;
@@ -1331,7 +1331,7 @@ impl WgpuRenderer {
.queue
.submit(std::iter::once(encoder.finish()));
frame.present();
return;
return true;
}
}