diff options
| author | srdusr <[email protected]> | 2025-10-07 22:20:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-10-07 22:20:00 +0200 |
| commit | 9f07715b469a481849ee35ffa33d6dab34356c7d (patch) | |
| tree | 7080f57115b9ea3ccab43e458c34aa2acec74501 | |
| parent | 4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704 (diff) | |
| download | srdwm-9f07715b469a481849ee35ffa33d6dab34356c7d.tar.gz srdwm-9f07715b469a481849ee35ffa33d6dab34356c7d.zip | |
Fix intermittent cursor ghosting when crossing between monitors
Live report: the real cursor sometimes leaves a brief ghost behind right
after moving between monitors. The bare-metal render loop already forces
a full repaint (ages = [0, 0]) on a workspace switch or any window move/
resize/open/close/restack, both added earlier for the same underlying gap:
the damage tracker's own element diffing doesn't always catch a vacated
region on its own. Neither reset noticed the pointer leaving one monitor
for another - no window moved, no workspace changed - so that head's own
vacated cursor-sized region was left entirely to the tracker's diffing,
intermittently.
Adds UdevState::last_cursor_head, compared each frame the same way the
other two resets are; only the head the pointer just left gets forced back
to ages = [0, 0] (the newly-entered head draws a genuinely new element
there and diffs correctly on its own).
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 22 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 18 | ||||
| -rw-r--r-- | docs/TODO.md | 10 |
4 files changed, 51 insertions, 0 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs index b0a2639..22df0e1 100644 --- a/crates/wayland/src/udev/mod.rs +++ b/crates/wayland/src/udev/mod.rs @@ -278,6 +278,28 @@ pub(crate) struct UdevState { /// `None` before the first frame, same reasoning as `last_rendered_ /// workspace` above (renders fully regardless). pub(crate) last_rendered_layout: Option<u64>, + /// Index into `heads` of whichever head the pointer was actually drawn + /// on last frame (`None` before the first frame, or if it was on none + /// of them). Compared each frame in `render_udev_frame`, same pattern + /// as `last_rendered_workspace`/`last_rendered_layout` above, so that + /// when the pointer crosses from one monitor to another the head it + /// just *left* gets its own `ages` forced back to `[0, 0]` too. + /// + /// Needed because neither of those two other resets notices this + /// transition at all: no window moved, and the workspace didn't + /// change, so both stay silent while the cursor sprite simply drops + /// out of that head's `custom_elements` list from one frame to the + /// next. That leaves the departing head's vacated cursor-sized region + /// resting entirely on `OutputDamageTracker`'s own element diffing -- + /// already documented, for the same "an element disappeared" shape of + /// bug on a window vacating part of the screen, as not reliable on its + /// own (see `layout_signature`'s own doc comment above `render_udev_ + /// frame`). Reported live as an intermittent cursor "ghost" briefly + /// left behind right after moving the pointer between monitors -- + /// intermittent because it depends on whatever else that head's own + /// diffing already had queued that frame, exactly like the window + /// case did. + pub(crate) last_cursor_head: Option<usize>, /// Set only when `SRDWM_GPU=1` and `gpu::probe` succeeds on this /// hardware - see that function's own doc comment for exactly what /// it does and does not do yet. `None` (the default, every session diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs index b09bd4b..bf82b32 100644 --- a/crates/wayland/src/udev/platform.rs +++ b/crates/wayland/src/udev/platform.rs @@ -170,6 +170,7 @@ impl UdevPlatform { disabled_connectors: std::collections::HashSet::new(), last_rendered_workspace: None, last_rendered_layout: None, + last_cursor_head: None, gpu: gpu_context, }; diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index dc84a2e..f850a19 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -167,6 +167,24 @@ impl CompState { head.ages = [0, 0]; } } + // See `UdevState::last_cursor_head`'s own doc comment: neither reset + // above notices the pointer crossing from one monitor to another, + // so that head's own vacated cursor-sized region was left entirely + // to `OutputDamageTracker`'s own diffing - reported live as an + // intermittent cursor "ghost" briefly left behind on the monitor + // just departed. Only the head being *left* needs the forced + // repaint; the one being entered draws a genuinely new element + // there this frame regardless, which diffs correctly on its own. + let current_cursor_head = udev.heads.iter().position(|h| { + let local = (udev.pointer_pos.x as i32 - h.location.x, udev.pointer_pos.y as i32 - h.location.y); + local.0 >= 0 && local.1 >= 0 && local.0 < h.size.0 && local.1 < h.size.1 + }); + if !locked && udev.last_cursor_head != current_cursor_head { + if let Some(old) = udev.last_cursor_head.and_then(|i| udev.heads.get_mut(i)) { + old.ages = [0, 0]; + } + udev.last_cursor_head = current_cursor_head; + } // 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: diff --git a/docs/TODO.md b/docs/TODO.md index a4c8d14..1392761 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -13,6 +13,16 @@ that has the full story. Keep this list current as items close or open; update the source doc's own entry too, don't let this drift into a second stale copy the way `PANEL_SUPPORT_TODO.md` did. +## Real bug, root-caused and fixed: the cursor itself leaves a "ghost" briefly when crossing between monitors (2026-08-27) + +Reported live, separately from the secondary-cursor ghost above (same word, different bug - this one is the user's own single, real cursor): "sometimes I recognize ghosting cursor when moving between monitors." + +`render_udev_frame` already has two defensive resets that force a head's `ages` back to `[0, 0]` (a full repaint, bypassing damage-diffing) on a workspace switch and on any window move/resize/open/close/restack - both added previously for the exact same underlying shape of bug: `OutputDamageTracker`'s own element-level diffing "evidently doesn't always catch a vacated region on its own" (see `layout_signature`'s own doc comment, added for a window that left a stale titlebar fragment behind after un-maximizing). Neither reset covers the pointer crossing from one monitor to another: no window moved and the workspace didn't change, so the head being *left* just silently drops its cursor element from `custom_elements` one frame to the next, with nothing forcing that head to notice and repaint over it. Intermittent for the same reason the window case was: it depends on whatever else that head's own diffing already had queued that frame. + +Fixed with the same pattern already established for the other two cases: new `UdevState::last_cursor_head` (`Option<usize>`, the head index the pointer was actually drawn on last frame, found the same way `cursor::render_elements` itself bounds-checks). Compared each frame in `render_udev_frame`; when it changes, only the *old* head gets `ages = [0, 0]` forced - the newly-entered head draws a genuinely new element there this frame regardless, which the existing diffing already handles correctly on its own, so resetting it too would just be wasted work every time the mouse crosses a boundary. + +Full workspace build/test/clippy clean. Not yet live-verified - needs a restart and a real cross-monitor mouse move to confirm, same as everything else in this session. + ## Real bug, root-caused and fixed: an uncontrollable "ghost" secondary cursor, on by default (2026-08-27) Reported live: "I see two cursors on screen and I can't even control the other one/shouldn't really auto show. It's more of like if I use two inputs at same time or for agents to use one without interrupting me." Multi-cursor Phase 1 (`UdevState::secondary_cursors`) drew one extra cursor sprite per physical libinput pointer device that had ever reported a position, unconditionally, with no way to turn it off and no way to know which physical device it belonged to. |