feat(linux/capture): serve the LIVE cursor overlay through Capturer::cursor
The forwarder read only frame.cursor — the overlay attached to the LAST EMITTED frame. Mutter's pointer-only buffers update the capture-side cursor state but are skipped as frames, so between damage frames the forwarder re-sent a stale snapshot: on-glass the client cursor appeared exactly when clicking or crossing hover targets (damage) and froze/vanished otherwise — the same pointer-only-motion gap the Windows IddCx channel fills, which is why the encode tick already prefers the capturer's LIVE cursor. Publish the overlay from every dequeued buffer (frames or not) into a shared slot and implement the Capturer::cursor hook on PortalCapturer. Host-composite mode on Linux still updates the blended pointer only on damage frames (no re-encode of a static desktop on cursor motion) — the regen-on-cursor-change the Windows path has is a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,12 @@ pub struct PortalCapturer {
|
|||||||
/// renegotiation before declaring the source lost. Cleared whenever a frame arrives or the stream
|
/// renegotiation before declaring the source lost. Cleared whenever a frame arrives or the stream
|
||||||
/// is `Streaming`.
|
/// is `Streaming`.
|
||||||
stall_since: Option<std::time::Instant>,
|
stall_since: Option<std::time::Instant>,
|
||||||
|
/// The LIVE cursor overlay, published by the PipeWire thread from every buffer's
|
||||||
|
/// `SPA_META_Cursor` — including the cursor-only "corrupted" buffers that never become
|
||||||
|
/// frames. [`Capturer::cursor`] serves it so the encode loop's forwarder tracks pointer-only
|
||||||
|
/// motion on a static desktop; the frame-attached overlay alone goes stale between damage
|
||||||
|
/// frames (the same gap the Windows IddCx channel fills, and why the tick prefers LIVE).
|
||||||
|
cursor_live: Arc<std::sync::Mutex<Option<pf_frame::CursorOverlay>>>,
|
||||||
/// True when this capture runs the VAAPI dmabuf passthrough (a LINEAR-dmabuf-only offer). If
|
/// True when this capture runs the VAAPI dmabuf passthrough (a LINEAR-dmabuf-only offer). If
|
||||||
/// that offer never negotiates, [`next_frame`](Capturer::next_frame)'s timeout branch latches
|
/// that offer never negotiates, [`next_frame`](Capturer::next_frame)'s timeout branch latches
|
||||||
/// the process-wide downgrade ([`pf_zerocopy::note_vaapi_dmabuf_failed`]) so the pipeline
|
/// the process-wide downgrade ([`pf_zerocopy::note_vaapi_dmabuf_failed`]) so the pipeline
|
||||||
@@ -176,6 +182,8 @@ struct PwHandles {
|
|||||||
hdr_offer: bool,
|
hdr_offer: bool,
|
||||||
/// See [`PortalCapturer::hdr_negotiated`].
|
/// See [`PortalCapturer::hdr_negotiated`].
|
||||||
hdr_negotiated: Arc<AtomicBool>,
|
hdr_negotiated: Arc<AtomicBool>,
|
||||||
|
/// See [`PortalCapturer::cursor_live`].
|
||||||
|
cursor_live: Arc<std::sync::Mutex<Option<pf_frame::CursorOverlay>>>,
|
||||||
quit: ::pipewire::channel::Sender<()>,
|
quit: ::pipewire::channel::Sender<()>,
|
||||||
join: thread::JoinHandle<()>,
|
join: thread::JoinHandle<()>,
|
||||||
}
|
}
|
||||||
@@ -195,6 +203,7 @@ impl PwHandles {
|
|||||||
vaapi_dmabuf: self.vaapi_dmabuf,
|
vaapi_dmabuf: self.vaapi_dmabuf,
|
||||||
hdr_offer: self.hdr_offer,
|
hdr_offer: self.hdr_offer,
|
||||||
hdr_negotiated: self.hdr_negotiated,
|
hdr_negotiated: self.hdr_negotiated,
|
||||||
|
cursor_live: self.cursor_live,
|
||||||
node_id,
|
node_id,
|
||||||
quit: Some(self.quit),
|
quit: Some(self.quit),
|
||||||
join: Some(self.join),
|
join: Some(self.join),
|
||||||
@@ -237,6 +246,8 @@ fn spawn_pipewire(
|
|||||||
let broken_cb = broken.clone();
|
let broken_cb = broken.clone();
|
||||||
let hdr_negotiated = Arc::new(AtomicBool::new(false));
|
let hdr_negotiated = Arc::new(AtomicBool::new(false));
|
||||||
let hdr_negotiated_cb = hdr_negotiated.clone();
|
let hdr_negotiated_cb = hdr_negotiated.clone();
|
||||||
|
let cursor_live = Arc::new(std::sync::Mutex::new(None::<pf_frame::CursorOverlay>));
|
||||||
|
let cursor_live_cb = cursor_live.clone();
|
||||||
// pipewire's own cross-thread channel: the receiver attaches to the loop and quits it; the
|
// pipewire's own cross-thread channel: the receiver attaches to the loop and quits it; the
|
||||||
// sender lives on the capturer and fires in its `Drop`. Absolute `::pipewire` path — the
|
// sender lives on the capturer and fires in its `Drop`. Absolute `::pipewire` path — the
|
||||||
// inner `mod pipewire` shadows the crate name at this scope.
|
// inner `mod pipewire` shadows the crate name at this scope.
|
||||||
@@ -272,6 +283,7 @@ fn spawn_pipewire(
|
|||||||
streaming_cb,
|
streaming_cb,
|
||||||
broken_cb,
|
broken_cb,
|
||||||
hdr_negotiated_cb,
|
hdr_negotiated_cb,
|
||||||
|
cursor_live_cb,
|
||||||
zerocopy,
|
zerocopy,
|
||||||
want_444,
|
want_444,
|
||||||
want_hdr,
|
want_hdr,
|
||||||
@@ -292,6 +304,7 @@ fn spawn_pipewire(
|
|||||||
vaapi_dmabuf,
|
vaapi_dmabuf,
|
||||||
hdr_offer: want_hdr,
|
hdr_offer: want_hdr,
|
||||||
hdr_negotiated,
|
hdr_negotiated,
|
||||||
|
cursor_live,
|
||||||
quit: quit_tx,
|
quit: quit_tx,
|
||||||
join,
|
join,
|
||||||
})
|
})
|
||||||
@@ -302,6 +315,12 @@ impl Capturer for PortalCapturer {
|
|||||||
self.frame_within(Duration::from_secs(10))
|
self.frame_within(Duration::from_secs(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cursor(&mut self) -> Option<pf_frame::CursorOverlay> {
|
||||||
|
// The PipeWire thread's live cursor slot (fed by every buffer's meta, frames or not) —
|
||||||
|
// lets the forwarder track pointer-only motion on a static desktop. See `cursor_live`.
|
||||||
|
self.cursor_live.lock().ok().and_then(|slot| slot.clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn next_frame_within(&mut self, budget: Duration) -> Result<CapturedFrame> {
|
fn next_frame_within(&mut self, budget: Duration) -> Result<CapturedFrame> {
|
||||||
self.frame_within(budget)
|
self.frame_within(budget)
|
||||||
}
|
}
|
||||||
@@ -945,6 +964,9 @@ mod pipewire {
|
|||||||
dbg_log_n: u64,
|
dbg_log_n: u64,
|
||||||
/// Cursor-as-metadata state, composited into the CPU de-pad path (see `consume_frame`).
|
/// Cursor-as-metadata state, composited into the CPU de-pad path (see `consume_frame`).
|
||||||
cursor: CursorState,
|
cursor: CursorState,
|
||||||
|
/// LIVE overlay slot shared with [`super::PortalCapturer::cursor_live`] — refreshed after
|
||||||
|
/// every `update_cursor_meta`, including from cursor-only buffers that never become frames.
|
||||||
|
cursor_live: Arc<std::sync::Mutex<Option<pf_frame::CursorOverlay>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consecutive tiled-import failures (worker alive, e.g. a per-buffer `EGL_BAD_MATCH`) before
|
/// Consecutive tiled-import failures (worker alive, e.g. a per-buffer `EGL_BAD_MATCH`) before
|
||||||
@@ -2065,6 +2087,9 @@ mod pipewire {
|
|||||||
streaming: Arc<AtomicBool>,
|
streaming: Arc<AtomicBool>,
|
||||||
broken: Arc<AtomicBool>,
|
broken: Arc<AtomicBool>,
|
||||||
hdr_negotiated: Arc<AtomicBool>,
|
hdr_negotiated: Arc<AtomicBool>,
|
||||||
|
// LIVE cursor publisher (see `PortalCapturer::cursor_live`): refreshed from every
|
||||||
|
// dequeued buffer's cursor meta, frames or not.
|
||||||
|
cursor_live: Arc<std::sync::Mutex<Option<pf_frame::CursorOverlay>>>,
|
||||||
zerocopy: bool,
|
zerocopy: bool,
|
||||||
// 4:4:4 session: tiled dmabufs take the worker's planar-YUV444 GPU convert.
|
// 4:4:4 session: tiled dmabufs take the worker's planar-YUV444 GPU convert.
|
||||||
want_444: bool,
|
want_444: bool,
|
||||||
@@ -2260,6 +2285,7 @@ mod pipewire {
|
|||||||
linear_nv12_failed: false,
|
linear_nv12_failed: false,
|
||||||
dbg_log_n: 0,
|
dbg_log_n: 0,
|
||||||
cursor: CursorState::default(),
|
cursor: CursorState::default(),
|
||||||
|
cursor_live,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = pw::stream::StreamBox::new(
|
let stream = pw::stream::StreamBox::new(
|
||||||
@@ -2384,6 +2410,12 @@ mod pipewire {
|
|||||||
// pointer-only movements as metadata-only "corrupted" buffers we drop for their
|
// pointer-only movements as metadata-only "corrupted" buffers we drop for their
|
||||||
// frame, but their cursor meta is fresh and must still move our overlay.
|
// frame, but their cursor meta is fresh and must still move our overlay.
|
||||||
update_cursor_meta(&mut ud.cursor, spa_buf);
|
update_cursor_meta(&mut ud.cursor, spa_buf);
|
||||||
|
// Publish the LIVE overlay (frames or not): the encode loop's forwarder polls
|
||||||
|
// it per tick, so pointer-only motion tracks on a static desktop — the
|
||||||
|
// frame-attached overlay alone stales between damage frames.
|
||||||
|
if let Ok(mut slot) = ud.cursor_live.lock() {
|
||||||
|
*slot = ud.cursor.overlay();
|
||||||
|
}
|
||||||
|
|
||||||
// Inspect the newest buffer's header + first chunk for the diagnostic and the
|
// Inspect the newest buffer's header + first chunk for the diagnostic and the
|
||||||
// CORRUPTED skip. SPA_META_Header is optional — `hdr` may be null.
|
// CORRUPTED skip. SPA_META_Header is optional — `hdr` may be null.
|
||||||
|
|||||||
Reference in New Issue
Block a user