From 2ae08181deca5bb82b330e9dcf0e0b7cb819c325 Mon Sep 17 00:00:00 2001 From: b5l <35809732+b5l@users.noreply.github.com> Date: Wed, 6 May 2026 05:40:02 +0200 Subject: [PATCH] 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 Co-authored-by: John Tur --- crates/gpui_linux/src/linux/wayland/window.rs | 15 ++++++++++++--- crates/gpui_wgpu/src/wgpu_renderer.rs | 18 +++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 857289a1d9..37d0f492d2 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -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, resize_throttle: bool, in_progress_window_controls: Option, @@ -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 { diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index da7e71c726..08f30dc009 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -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; } }