fix(host/linux): park the seat pointer on the streamed surface — a capture-mode client cannot

ROOT CAUSE of the GNOME capture-mode cursorless stream (and the same
latent hole on KWin, whose screencast also gates cursor data on
includesCursor): a virtual output is created fresh per session while the
seat pointer stays wherever it was — usually a physical monitor. A
capture-model (pointer-lock) client sends only RELATIVE deltas, so
nothing ever moves the pointer INTO the streamed output: input lands on
the wrong monitor, Mutter suppresses SPA_META_Cursor entirely
(should_cursor_metadata_be_set: visible AND in-stream), and both the
embedded and the encoder-blend composite models stream cursorless.

The stream loop now parks the pointer at the streamed surface's centre
through the session's own input pipeline (capability routing, region
ladder, anchor — the path client events take): armed per (re)built
display and by the capture-model flip, retried on a schedule because the
first park of a session can land on a still-cold EIS connection (devices
not resumed — the injector drops it; observed on-glass), and kept trying
while a capture-model session still has no live overlay. A desktop-model
client overrides the park with its first absolute move.

Verified end-to-end on GNOME 50.3/Mutter (RTX 5070 Ti): the reference
client in capture mode now shows the host-composited cursor moving in
the decoded H.265 dump, and the PyroWave session hands the overlay to
the wavelet CSC blend. One-shot per-session/per-stream breadcrumbs
(first meta region, first bitmap, first overlay handed to the blend /
still-cursorless) make the next field triage a grep instead of a rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 17:59:03 +02:00
co-authored by Claude Fable 5
parent 39aa0c57ce
commit 751fbd6c47
3 changed files with 164 additions and 2 deletions
+18
View File
@@ -35,6 +35,10 @@ pub(super) struct CursorState {
/// channel (the blend path uses the pre-adjusted `x`/`y` and never reads it).
hot_x: i32,
hot_y: i32,
/// One-shot breadcrumb latch: this stream saw a `SPA_META_Cursor` region (the Meta param
/// negotiated). Per-stream deliberately — a host serves many sessions per process, and a
/// process-wide latch made the second session's triage read as "no meta".
seen_meta: bool,
}
impl CursorState {
@@ -93,6 +97,13 @@ pub(super) fn update_cursor_meta(cursor: &mut CursorState, spa_buf: *mut spa::sy
if meta.is_null() {
return;
}
// One-shot breadcrumb (per stream): the producer DID attach a cursor-meta region (the Meta
// param negotiated). Field triage for a cursorless stream starts by grepping for this line
// — its absence means the negotiation dropped the meta, not that the pointer never moved.
if !cursor.seen_meta {
cursor.seen_meta = true;
tracing::info!("cursor meta: first SPA_META_Cursor region observed on this stream");
}
// SAFETY: `meta` is non-null and points into the held buffer's metadata array.
let (region_size, data) = unsafe { ((*meta).size as usize, (*meta).data as *const u8) };
if data.is_null() || region_size < std::mem::size_of::<spa::sys::spa_meta_cursor>() {
@@ -180,6 +191,12 @@ pub(super) fn update_cursor_meta(cursor: &mut CursorState, spa_buf: *mut spa::sy
cursor.bw = bw;
cursor.bh = bh;
cursor.serial = cursor.serial.wrapping_add(1);
// One-shot sibling of the region breadcrumb above (per stream, via the serial's 0→1 edge):
// the first BITMAP — before this line has fired, `overlay()` is `None` and every
// blend/forward path is cursorless by construction.
if cursor.serial == 1 {
tracing::info!(w = bw, h = bh, "cursor meta: first cursor bitmap received");
}
}
/// The byte range inside the producer's cursor-meta region that a `bh`-row, `row`-wide,
@@ -349,6 +366,7 @@ mod tests {
serial: 1,
hot_x: 0,
hot_y: 0,
seen_meta: true,
}
}