diff options
| author | srdusr <[email protected]> | 2025-03-07 22:07:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-03-07 22:07:00 +0200 |
| commit | 9667308e6def22e9b1fa352357a788749ad92c74 (patch) | |
| tree | 5fa0158eba3b3f6fa71c410bc5d2b19f70c018aa /crates/wayland/src/udev | |
| parent | 4c97f8647b9543abefbed3ece209d335de6aa9d6 (diff) | |
| download | srdwm-9667308e6def22e9b1fa352357a788749ad92c74.tar.gz srdwm-9667308e6def22e9b1fa352357a788749ad92c74.zip | |
Fix stale ghost content after window move/resize/close on real hardware
The udev backend's damage tracking only forced a full repaint (ages =
[0, 0]) on a workspace switch or a VT-switch resume. An ordinary
move/resize/open/close/restack within the same workspace relied
entirely on OutputDamageTracker's own per-element diffing to compute
correct damage for the region a window vacated - which doesn't always
hold: maximizing a window over a second one, then un-maximizing, left
a persistent ghost of the second window's old titlebar/status text
sitting in the vacated corner, unchanged across multiple otherwise-
idle frames.
render_udev_frame now also hashes every visible window's id and rect
each frame and forces the same full-repaint reset whenever that
signature changes - the same "defensive, not a fix for a proven bug
in the diffing itself" reasoning the existing workspace-switch reset
already uses, just triggered by a second, complementary condition.
Diffstat (limited to 'crates/wayland/src/udev')
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 7 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 44 |
3 files changed, 51 insertions, 1 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs index 83ffa87..4762992 100644 --- a/crates/wayland/src/udev/mod.rs +++ b/crates/wayland/src/udev/mod.rs @@ -231,6 +231,13 @@ pub(crate) struct UdevState { /// regardless (every head starts with `ages: [0, 0]` - see /// `UdevHead`'s own field). pub(crate) last_rendered_workspace: Option<srdwm_core::WorkspaceId>, + /// Order-sensitive hash of every visible window's id and rect, compared + /// each frame in `render_udev_frame` to force `ages` back to `[0, 0]` + /// on any move/resize/open/close/restack - see that comparison's own + /// doc comment for the live-reproduced ghost-content bug this catches. + /// `None` before the first frame, same reasoning as `last_rendered_ + /// workspace` above (renders fully regardless). + pub(crate) last_rendered_layout: Option<u64>, } impl UdevState { diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs index 1d2ef0e..2868f15 100644 --- a/crates/wayland/src/udev/platform.rs +++ b/crates/wayland/src/udev/platform.rs @@ -130,6 +130,7 @@ impl UdevPlatform { session: session.clone(), disabled_connectors: std::collections::HashSet::new(), last_rendered_workspace: None, + last_rendered_layout: None, }; let mut state = CompState { diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index 398f06e..d8f0003 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -46,7 +46,40 @@ impl CompState { // happen per head below without needing `self` itself mutably) -- // see the per-head loop for why decoration/border buffers still get // looked up fresh per head (head-local `origin` translation). - let ids: Vec<srdwm_core::WindowId> = if locked { Vec::new() } else { self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect() }; + // `layout_signature` folds in every visible window's id and rect + // (order-sensitive, so a restack counts as a change too) - compared + // against `udev.last_rendered_layout` below to force `ages = [0, 0]` + // whenever a window moves, resizes, opens, closes, or restacks + // between one frame and the next. Without this, a window vacating + // part of the screen (closing, moving away, un-maximizing) can leave + // stale content from its old position baked into whichever DRM + // buffer isn't due to be written again for a while - confirmed + // live: maximizing a window over a second one, then un-maximizing, + // left a persistent ghost of the second window's old titlebar/status + // text sitting in the vacated corner, unchanged across multiple + // otherwise-idle frames, because `OutputDamageTracker`'s own element- + // level diffing (relied on by the workspace-switch reset's own doc + // comment above) evidently doesn't always catch a vacated region on + // its own - most visible right where a rounded corner's mask should + // have revealed the desktop underneath but instead revealed this. + // Same "defensive, not a fix for a proven bug in the diffing itself" + // reasoning as the workspace-switch reset already documents; this is + // just a second, complementary trigger for the same reset. + let (ids, layout_signature): (Vec<srdwm_core::WindowId>, u64) = if locked { + (Vec::new(), 0) + } else { + let wm = self.wm.borrow(); + let mut ids = Vec::new(); + let mut sig: u64 = 0xcbf29ce484222325; + for w in wm.visible_windows_front_to_back() { + ids.push(w.id); + for part in [w.id, w.geometry.x as u64, w.geometry.y as u64, w.geometry.width as u64, w.geometry.height as u64] { + sig ^= part; + sig = sig.wrapping_mul(0x100000001b3); + } + } + (ids, sig) + }; let focused = self.wm.borrow().focused_id(); // Stays default `false` here, unlike winit's `unwrap_or(true)` -- // see `rounded_corners_pixman`'s module doc comment for the real @@ -106,6 +139,15 @@ impl CompState { head.ages = [0, 0]; } } + // See `layout_signature`'s own doc comment above for what this + // catches that the workspace-switch reset above doesn't: any move, + // resize, open, close, or restack within the *same* workspace. + if !locked && udev.last_rendered_layout != Some(layout_signature) { + udev.last_rendered_layout = Some(layout_signature); + for head in &mut udev.heads { + head.ages = [0, 0]; + } + } // A head whose page-flip event never arrives (kernel-dropped, or a // DRM event this driver never sends for reasons this backend has no // visibility into) would otherwise sit in `flip_pending` forever: |