feat(core+host+client): cursor channel — remote-desktop sweep M2a+M2b
The host cursor stops riding the video and becomes a real OS cursor on the client (the Parsec/RDP model): pointer feel no longer pays the capture→encode→network→decode→present round trip. Wire (M2a): - Hello grows a client_caps trailing byte (CLIENT_CAP_CURSOR) after the fixed display_hdr block — presence disambiguated by remaining length, which caps the post-HDR tail at 27 bytes (documented); Welcome answers HOST_CAP_CURSOR (capable-and-asked, the 444/clipboard precedent). - CursorShape (0x50, control stream): serial + dims + hotspot + straight RGBA, ≤120px/side so the u16 frame always fits (128² would overshoot); client caches by serial — re-showing a known shape costs 14 bytes, not a bitmap (RDP pointer-cache for free). - CursorState (0xD0 datagram): serial + visible/relative_hint flags + position, sent once per encode-loop tick — latest-wins, self-healing under loss, no refresh timer. relative_hint is reserved for M3. - Client core: two new planes (control-task + datagram-task arms) → next_cursor_shape/next_cursor_state; connect() grows client_caps (C ABI passes 0 until the v11 cursor poll fns exist). Host (M2b, Linux portal only): - handshake::cursor_forward is THE predicate (client asked ∧ Linux ∧ compositor ≠ gamescope) — Welcome bit and session wiring both read it. - SessionPlan.cursor_blend goes false for a forwarding session; the encode loop ticks a CursorForwarder every iteration: shape-serial diff → control-task bridge (mirrors probe_result), state datagram → conn. - CursorOverlay/capture CursorState carry the hotspot through (nearest-neighbor downscale backstop for XL cursors, unit-tested). Presenter: - CursorChannel drains both planes per loop iteration; shapes become SDL color cursors (from_surface + hotspot), applied while the desktop mouse model is engaged; visibility follows the host; capture/released hands back the system cursor. Sessions advertise the cap when they START in desktop mode. Verified on .21: fmt + clippy -D warnings (7 crates) + tests green (core 218 incl. new wire roundtrips, host 245 incl. e2e + forwarder downscale tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ pub(super) async fn run(
|
||||
probe_tx: std::sync::mpsc::Sender<ProbeRequest>,
|
||||
mut probe_result_rx: tokio::sync::mpsc::UnboundedReceiver<ProbeResult>,
|
||||
mut reconfig_result_rx: tokio::sync::mpsc::UnboundedReceiver<Reconfigured>,
|
||||
mut cursor_shape_rx: tokio::sync::mpsc::UnboundedReceiver<punktfunk_core::quic::CursorShape>,
|
||||
clip_enabled: Arc<AtomicBool>,
|
||||
clip: pf_clipboard::ClipCoord,
|
||||
) {
|
||||
@@ -262,6 +263,15 @@ pub(super) async fn run(
|
||||
break;
|
||||
}
|
||||
}
|
||||
shape = cursor_shape_rx.recv() => {
|
||||
// Cursor-forward bridge (M2): the encode loop diffed a new pointer bitmap.
|
||||
// Rare (shape changes are human-paced); ≤ ~58 KiB fits the u16 frame by
|
||||
// construction (cursor_fwd downscales).
|
||||
let Some(shape) = shape else { break }; // data plane gone
|
||||
if io::write_msg(&mut ctrl_send, &shape.encode()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
offer = clip_offer_rx.recv(), if !clip_offer_closed => {
|
||||
// Host copied → the coordinator minted a `ClipOffer`; forward it to the client
|
||||
// (only while sync is on — a race with a just-received disable would otherwise
|
||||
|
||||
Reference in New Issue
Block a user