feat(windows): IddCx hardware-cursor channel — remote-desktop sweep M2c
Brings the cursor channel to Windows hosts. The pf-vdisplay driver declares an IddCx hardware cursor for sessions that negotiated cursor-forward — DWM then EXCLUDES the pointer from the IDD frame and delivers shape/position out-of-band, into the same CursorOverlay → forwarder → wire → client pipeline the Linux portal path uses. - pf-driver-proto v5 (additive, host floor stays 3): AddRequest's spare tail becomes hw_cursor (same size/offsets); IOCTL_SET_CURSOR_CHANNEL delivers a host-created CursorShm section (64-byte seqlock header + 256² shape buffer, layout pinned + tested). No event crosses the boundary — the host polls at encode-tick pace. - driver: wdk-iddcx grows the two cursor DDI wrappers; a per-monitor cursor worker (event wait → QueryHardwareCursor → seqlock publish) starts only when BOTH the ADD asked and the channel arrived, so a failed delivery leaves DWM compositing as today. Shape bytes ship raw (BGRA/masked + pitch); the host converts. - host: the section rides the existing sealed-channel broker (least- privilege dup, remote reap on failure); IddPushCapturer::cursor() seqlock-reads → CursorOverlay (BGRA→RGBA, masked-color approximation, desktop→frame origin shift, per-shape conversion cache). New Capturer::cursor() trait hook — the encode loop prefers it over the frame-attached overlay because hardware-cursor moves produce NO new frame on a static desktop. hw_cursor survives the re-arrival resize (carried on the manager's Monitor). - negotiation: cursor_forward grows the Windows arm (client cap ∧ driver proto ≥ 5, probed once via the control device); SessionPlan carries cursor_forward → OutputFormat.hw_cursor. - drive-by: wdk-probe's two pre-existing same-type casts (clippy) and pf-vdisplay's stale spike-test refs (crate::win_display moved to pf-win-display; tracing-subscriber was never a dep) repaired. Verified: proto tests on Mac AND MSVC (15/15 incl. the CursorShm layout pin); clippy -D warnings for proto/frame/capture/vdisplay/host on Linux (.21) and native Windows (.173); the DRIVER workspace clippy -D warnings green against the real WDK 10.0.26100 bindgen (DDI names, enum variants and IDARG layouts all bind). On-box driver deploy + on-glass validation follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,23 @@ pub fn capture_virtual_output(
|
||||
// proactively enables advanced color and selects the per-frame conversion. There is NO fallback:
|
||||
// if it can't open or the driver doesn't attach, the session fails cleanly and the client
|
||||
// reconnects.
|
||||
// Cursor-forward sessions (M2c): hand the capturer the v5 cursor-channel delivery closure —
|
||||
// its presence opts the session in (the capturer creates + delivers the CursorShm section,
|
||||
// the driver declares the IddCx hardware cursor). Built exactly like `sender` above.
|
||||
let cursor_sender: Option<pf_capture::CursorChannelSender> = want.hw_cursor.then(|| {
|
||||
std::sync::Arc::new(
|
||||
move |req: &pf_driver_proto::control::SetCursorChannelRequest| {
|
||||
// SAFETY: `control_raw` is the pf-vdisplay control handle resolved above; it is
|
||||
// never closed for the process lifetime (`send_cursor_channel`'s precondition).
|
||||
unsafe {
|
||||
crate::vdisplay::driver::send_cursor_channel(
|
||||
windows::Win32::Foundation::HANDLE(control_raw as *mut core::ffi::c_void),
|
||||
req,
|
||||
)
|
||||
}
|
||||
},
|
||||
) as pf_capture::CursorChannelSender
|
||||
});
|
||||
pf_capture::open_idd_push(
|
||||
target,
|
||||
pref,
|
||||
@@ -165,6 +182,7 @@ pub fn capture_virtual_output(
|
||||
want.pyrowave,
|
||||
keep,
|
||||
sender,
|
||||
cursor_sender,
|
||||
)
|
||||
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
|
||||
}
|
||||
|
||||
@@ -10,18 +10,35 @@ use super::*;
|
||||
|
||||
/// Whether this session forwards the cursor out-of-band (design/remote-desktop-sweep.md M2):
|
||||
/// the client asked ([`CLIENT_CAP_CURSOR`](punktfunk_core::quic::CLIENT_CAP_CURSOR)) AND the
|
||||
/// capture path can deliver cursor metadata separately from the frame — today that is the
|
||||
/// Linux portal `SPA_META_Cursor` path only: not gamescope (its capture paints no cursor at
|
||||
/// all), not Windows (DWM composites into the IDD frame — M2c). THE single predicate: the
|
||||
/// Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off wiring both read it,
|
||||
/// so they can never disagree.
|
||||
/// capture path can deliver cursor metadata separately from the frame — the Linux portal
|
||||
/// `SPA_META_Cursor` path (not gamescope, whose capture paints no cursor at all), or Windows
|
||||
/// with a proto-v5 pf-vdisplay driver (the IddCx hardware-cursor channel, M2c). THE single
|
||||
/// predicate: the Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off
|
||||
/// wiring both read it, so they can never disagree.
|
||||
pub(super) fn cursor_forward(
|
||||
client_caps: u8,
|
||||
compositor: Option<crate::vdisplay::Compositor>,
|
||||
) -> bool {
|
||||
cfg!(target_os = "linux")
|
||||
&& client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR != 0
|
||||
&& compositor.is_some_and(|c| c != crate::vdisplay::Compositor::Gamescope)
|
||||
if client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR == 0 {
|
||||
return false;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
compositor.is_some_and(|c| c != crate::vdisplay::Compositor::Gamescope)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows (M2c): the pf-vdisplay driver must speak the v5 hardware-cursor channel —
|
||||
// DWM composites the pointer into the IDD frame otherwise, and forwarding a second
|
||||
// copy would double it. The probe latches by opening the control device once.
|
||||
let _ = compositor;
|
||||
crate::vdisplay::manager::hw_cursor_capable()
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
let _ = compositor;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the Hello→Welcome→Start negotiation. Borrows the control streams (the caller keeps them for
|
||||
@@ -510,6 +527,9 @@ pub(super) async fn negotiate(
|
||||
let (ctx_tx, ctx_rx) = std::sync::mpsc::sync_channel::<SessionContext>(1);
|
||||
let client_identity = endpoint::peer_fingerprint(conn);
|
||||
let client_hdr = hello.display_hdr;
|
||||
// Same predicate the Welcome's HOST_CAP_CURSOR bit used — the prepared display and
|
||||
// the session wiring must agree with what we just advertised.
|
||||
let cursor_fw = cursor_forward(hello.client_caps, Some(comp));
|
||||
let (mode, shard_payload) = (hello.mode, welcome.shard_payload);
|
||||
let trace = bringup.clone();
|
||||
std::thread::Builder::new()
|
||||
@@ -520,6 +540,7 @@ pub(super) async fn negotiate(
|
||||
mode,
|
||||
client_identity,
|
||||
client_hdr,
|
||||
cursor_fw,
|
||||
bitrate_kbps,
|
||||
bit_depth,
|
||||
chroma,
|
||||
|
||||
@@ -999,6 +999,7 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
// the client is not drawing it locally (the M2 cursor channel — blending too would
|
||||
// show it twice).
|
||||
ctx.compositor != pf_vdisplay::Compositor::Gamescope && !ctx.cursor_forward,
|
||||
ctx.cursor_forward,
|
||||
);
|
||||
// PyroWave rides the datagram-aligned wire mode (§4.4): every encoder this session opens
|
||||
// packetizes at the negotiated shard payload, so a lost datagram costs blocks, not frames.
|
||||
@@ -1089,6 +1090,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
// panel instead of the driver's built-in ~1000-nit placeholder. No-op on Linux
|
||||
// backends and for older/SDR clients.
|
||||
vd.set_client_hdr(client_hdr);
|
||||
// Cursor-forward sessions ask the backend for an out-of-band hardware cursor
|
||||
// (Windows pf-vdisplay / IddCx; no-op on Linux — the portal already separates it).
|
||||
vd.set_hw_cursor(cursor_forward);
|
||||
// Deliberate-quit wiring (Windows pf-vdisplay; no-op elsewhere): every lease the
|
||||
// backend mints — the retry-hold below AND the capturer's — carries the session's quit
|
||||
// flag, so a user "stop" (⌘D → the QUIT close code) tears the virtual monitor down the
|
||||
@@ -2007,10 +2011,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
}
|
||||
// Cursor channel (M2): every iteration — new frame OR repeat — states the pointer
|
||||
// (self-healing under datagram loss) and forwards a changed shape via the control
|
||||
// bridge. `frame` is the newest bound frame either way. A hidden-but-known pointer
|
||||
// (overlay with `visible: false`) is the M3 relative-mode hint.
|
||||
// bridge. A hidden-but-known pointer (overlay with `visible: false`) is the M3
|
||||
// relative-mode hint. The capturer's LIVE cursor (the Windows hardware-cursor channel,
|
||||
// where pointer-only moves produce no frame) outranks the frame-attached overlay
|
||||
// (the Linux portal path); `frame` is the newest bound frame either way.
|
||||
if let Some(fwd) = cursor_fwd.as_mut() {
|
||||
fwd.tick(frame.cursor.as_ref(), &conn, &cursor_shape_tx);
|
||||
let live = capturer.cursor();
|
||||
fwd.tick(
|
||||
live.as_ref().or(frame.cursor.as_ref()),
|
||||
&conn,
|
||||
&cursor_shape_tx,
|
||||
);
|
||||
}
|
||||
// The overlay now surfaces hidden pointers too (for the hint above) — strip them
|
||||
// HERE, after forwarding, so no blend path ever draws an invisible cursor.
|
||||
@@ -2675,6 +2686,7 @@ pub(super) fn prepare_display(
|
||||
mode: punktfunk_core::Mode,
|
||||
client_identity: Option<[u8; 32]>,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
cursor_forward: bool,
|
||||
bitrate_kbps: u32,
|
||||
bit_depth: u8,
|
||||
chroma: crate::encode::ChromaFormat,
|
||||
@@ -2691,7 +2703,8 @@ pub(super) fn prepare_display(
|
||||
bit_depth,
|
||||
chroma,
|
||||
codec,
|
||||
compositor != pf_vdisplay::Compositor::Gamescope,
|
||||
compositor != pf_vdisplay::Compositor::Gamescope && !cursor_forward,
|
||||
cursor_forward,
|
||||
);
|
||||
if codec == crate::encode::Codec::PyroWave {
|
||||
plan.wire_chunk = Some(shard_payload as usize);
|
||||
@@ -2699,6 +2712,7 @@ pub(super) fn prepare_display(
|
||||
let mut vd = crate::vdisplay::open(compositor)?;
|
||||
vd.set_client_identity(client_identity);
|
||||
vd.set_client_hdr(client_hdr);
|
||||
vd.set_hw_cursor(cursor_forward);
|
||||
vd.set_quit_flag(quit.clone());
|
||||
// Slot-scoped setup serialization + reconnect preempt — see the inline arm in
|
||||
// `virtual_stream` for the full rationale; released when this fn returns.
|
||||
|
||||
@@ -108,6 +108,10 @@ pub struct SessionPlan {
|
||||
/// Encoders whose fast path cannot blend (the Vulkan EFC RGB-direct source) stay on their
|
||||
/// blending path when this is set, so the pointer never silently vanishes from the stream.
|
||||
pub cursor_blend: bool,
|
||||
/// The session negotiated the cursor-forward channel (M2/M2c): the client draws the pointer
|
||||
/// locally, so `cursor_blend` is off AND (on Windows) the capturer sets the driver's
|
||||
/// hardware cursor up via [`OutputFormat::hw_cursor`](pf_frame::OutputFormat).
|
||||
pub cursor_forward: bool,
|
||||
}
|
||||
|
||||
impl SessionPlan {
|
||||
@@ -118,6 +122,7 @@ impl SessionPlan {
|
||||
chroma: crate::encode::ChromaFormat,
|
||||
codec: crate::encode::Codec,
|
||||
cursor_blend: bool,
|
||||
cursor_forward: bool,
|
||||
) -> Self {
|
||||
SessionPlan {
|
||||
capture: CaptureBackend::resolve(),
|
||||
@@ -129,6 +134,7 @@ impl SessionPlan {
|
||||
codec,
|
||||
wire_chunk: None,
|
||||
cursor_blend,
|
||||
cursor_forward,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +177,7 @@ impl SessionPlan {
|
||||
crate::capture::OutputFormat {
|
||||
gpu,
|
||||
hdr: self.hdr,
|
||||
hw_cursor: self.cursor_forward,
|
||||
// 4:4:4 needs a full-chroma source: on Windows this keeps the capturer on RGB (not the
|
||||
// default NV12/P010 video-engine output) so NVENC can CSC to 4:4:4.
|
||||
chroma_444: self.chroma.is_444(),
|
||||
|
||||
Reference in New Issue
Block a user