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:
@@ -134,6 +134,13 @@ pub trait VirtualDisplay: Send {
|
||||
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint
|
||||
/// per-monitor EDIDs today (the Linux compositors' virtual outputs take no EDID from us).
|
||||
fn set_client_hdr(&mut self, _hdr: Option<punktfunk_core::quic::HdrMeta>) {}
|
||||
/// Ask the backend for an out-of-band HARDWARE CURSOR on the created output (the M2c cursor
|
||||
/// channel): the compositor/OS stops compositing the pointer into captured frames and the
|
||||
/// capture layer surfaces shape/position separately. Carried on the backend instance; set
|
||||
/// once before [`create`](Self::create). Default: no-op — only the Windows pf-vdisplay
|
||||
/// backend (IddCx hardware cursor, driver proto v5) implements it; the Linux portal path
|
||||
/// gets the same split from `SPA_META_Cursor` without asking.
|
||||
fn set_hw_cursor(&mut self, _on: bool) {}
|
||||
/// The stable identity slot the backend resolved for the most recent [`create`](Self::create) —
|
||||
/// the per-client id the identity policy assigned (`Some`), or `None` for shared/anonymous. The
|
||||
/// registry reads it right after `create` to key the display's group **arrangement** (manual
|
||||
|
||||
@@ -71,6 +71,9 @@ struct Monitor {
|
||||
/// selectable). The pin is never re-issued on reuse, so this is what the driver still renders
|
||||
/// on — [`warn_if_pick_moved`] compares the CURRENT pick against it.
|
||||
render_pin: Option<LUID>,
|
||||
/// This monitor was ADDed with the v5 hardware-cursor flag (already driver-proto-gated) —
|
||||
/// preserved across a re-arrival resize so the recreated monitor keeps the cursor channel.
|
||||
hw_cursor: bool,
|
||||
/// The driver's WUDFHost pid (from the ADD reply) — carried into [`WinCaptureTarget`] so the
|
||||
/// IDD-push capturer knows where to duplicate the sealed frame channel's handles. The SAME
|
||||
/// process for every parallel monitor (one devnode → one WUDFHost hosts all publishers), which
|
||||
@@ -267,6 +270,20 @@ pub fn vdm() -> &'static VirtualDisplayManager {
|
||||
/// for the process lifetime — a dead one is RETIRED (kept alive, see [`DeviceSlot`]), so a stale copy
|
||||
/// can only fail IOCTLs, never dangle. `None` before the first backend open — impossible for a
|
||||
/// capturer, which only exists on a monitor the manager created.
|
||||
/// Can this host's pf-vdisplay driver run the v5 hardware-cursor channel? Reads the
|
||||
/// handshake-latched protocol version, opening the control device once if no session has
|
||||
/// opened it yet this service run (the same open every session performs anyway) — so the
|
||||
/// Welcome-time capability decision never guesses. `false` when the driver is missing/stale.
|
||||
pub fn hw_cursor_capable() -> bool {
|
||||
let m = vdm();
|
||||
let v = m.driver_proto.load(Ordering::Relaxed);
|
||||
if v != 0 {
|
||||
return v >= 5;
|
||||
}
|
||||
let _ = m.ensure_device();
|
||||
m.driver_proto.load(Ordering::Relaxed) >= 5
|
||||
}
|
||||
|
||||
pub fn control_device_handle() -> Option<HANDLE> {
|
||||
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
||||
}
|
||||
@@ -390,6 +407,7 @@ impl VirtualDisplayManager {
|
||||
mode: Mode,
|
||||
client_fp: Option<[u8; 32]>,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
hw_cursor: bool,
|
||||
quit: Option<Arc<AtomicBool>>,
|
||||
) -> Result<VirtualOutput> {
|
||||
// Console-session guard: a host outside the ACTIVE console session cannot drive the display
|
||||
@@ -622,7 +640,9 @@ impl VirtualDisplayManager {
|
||||
// SAFETY: `create_monitor` requires `dev` to be a valid control handle; `dev` is the handle
|
||||
// `ensure_device()` returned above (cached handles are never closed — a dead one is retired,
|
||||
// kept alive; see `DeviceSlot`), and we hold the `state` lock.
|
||||
let mon = match unsafe { self.create_monitor(dev, mode, slot, client_hdr, &mut inner) } {
|
||||
let mon = match unsafe {
|
||||
self.create_monitor(dev, mode, slot, client_hdr, hw_cursor, &mut inner)
|
||||
} {
|
||||
// The cached device died under us (driver upgrade / WUDFHost restart, detected only
|
||||
// now — e.g. the host sat idle past the pinger-less window). Retire it, reopen, and
|
||||
// retry ONCE so the reconnect-after-driver-restart succeeds first try instead of
|
||||
@@ -635,7 +655,7 @@ impl VirtualDisplayManager {
|
||||
);
|
||||
// SAFETY: as above — `dev` is the handle the reopening `ensure_device` just
|
||||
// returned, and the `state` lock is still held.
|
||||
unsafe { self.create_monitor(dev, mode, slot, client_hdr, &mut inner)? }
|
||||
unsafe { self.create_monitor(dev, mode, slot, client_hdr, hw_cursor, &mut inner)? }
|
||||
}
|
||||
r => r?,
|
||||
};
|
||||
@@ -861,6 +881,7 @@ impl VirtualDisplayManager {
|
||||
mode: Mode,
|
||||
slot: u32,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
hw_cursor: bool,
|
||||
inner: &mut MgrInner,
|
||||
) -> Result<Monitor> {
|
||||
// The slot id doubles as the driver-preferred monitor id (EDID serial / ConnectorIndex), so
|
||||
@@ -868,13 +889,17 @@ impl VirtualDisplayManager {
|
||||
// `0` (anonymous) = the driver auto-allocates the lowest-free id.
|
||||
let preferred_id = slot;
|
||||
let render_pin = resolve_render_pin();
|
||||
// Hardware cursor only against a driver that implements the v5 channel: an older driver
|
||||
// ignores the AddRequest field anyway (composited cursor), but gating here keeps the
|
||||
// capture layer from creating + delivering a section nobody will ever publish into.
|
||||
let hw_cursor = hw_cursor && self.driver_proto.load(Ordering::Relaxed) >= 5;
|
||||
// SAFETY: `create_monitor`'s own `# Safety` contract guarantees `dev` is the live control
|
||||
// handle; we forward it unchanged to `add_monitor`, whose precondition is exactly that.
|
||||
// `render_pin` is an `Option<LUID>` by value (plain `Copy`), so no borrowed memory
|
||||
// crosses the call.
|
||||
let added = unsafe {
|
||||
self.driver
|
||||
.add_monitor(dev, mode, render_pin, preferred_id, client_hdr)?
|
||||
.add_monitor(dev, mode, render_pin, preferred_id, client_hdr, hw_cursor)?
|
||||
};
|
||||
|
||||
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
|
||||
@@ -1061,6 +1086,7 @@ impl VirtualDisplayManager {
|
||||
resolved_monitor_id: added.resolved_monitor_id,
|
||||
position: (0, 0),
|
||||
gen: self.gen.fetch_add(1, Ordering::Relaxed),
|
||||
hw_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1231,7 +1257,7 @@ impl VirtualDisplayManager {
|
||||
// values passed by value — no borrow crosses the call.
|
||||
let added = unsafe {
|
||||
self.driver
|
||||
.add_monitor(dev, mode, render_pin, slot, client_hdr)
|
||||
.add_monitor(dev, mode, render_pin, slot, client_hdr, old.hw_cursor)
|
||||
.context("re-arrival ADD at the new mode")?
|
||||
};
|
||||
self.ensure_pinger();
|
||||
@@ -1283,6 +1309,7 @@ impl VirtualDisplayManager {
|
||||
resolved_monitor_id: added.resolved_monitor_id,
|
||||
position: old.position,
|
||||
gen: old.gen,
|
||||
hw_cursor: old.hw_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
||||
render_luid: Option<LUID>,
|
||||
preferred_monitor_id: u32,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
hw_cursor: bool,
|
||||
) -> Result<AddedMonitor>;
|
||||
/// Refresh the LIVE monitor `key`'s advertised mode list to lead with `mode` (the in-place
|
||||
/// mid-stream resize, latency plan P2 — pf-vdisplay `IOCTL_UPDATE_MODES`, driver protocol v4).
|
||||
|
||||
@@ -234,6 +234,30 @@ pub unsafe fn send_frame_channel(dev: HANDLE, req: &control::SetFrameChannelRequ
|
||||
.context("pf-vdisplay SET_FRAME_CHANNEL")
|
||||
}
|
||||
|
||||
/// Deliver a monitor's hardware-cursor section (`IOCTL_SET_CURSOR_CHANNEL`, proto v5) — the
|
||||
/// cursor sibling of [`send_frame_channel`], same delivery/ownership contract.
|
||||
///
|
||||
/// # Safety
|
||||
/// `dev` must be a live pf-vdisplay control handle (see [`super::manager::control_device_handle`]).
|
||||
pub unsafe fn send_cursor_channel(
|
||||
dev: HANDLE,
|
||||
req: &control::SetCursorChannelRequest,
|
||||
) -> Result<()> {
|
||||
let mut none: [u8; 0] = [];
|
||||
// SAFETY: per this fn's contract `dev` is the live control handle; `bytes_of(req)` borrows the
|
||||
// caller's request across this synchronous call; no output buffer.
|
||||
unsafe {
|
||||
ioctl(
|
||||
dev,
|
||||
control::IOCTL_SET_CURSOR_CHANNEL,
|
||||
bytemuck::bytes_of(req),
|
||||
&mut none,
|
||||
)
|
||||
}
|
||||
.map(|_| ())
|
||||
.context("pf-vdisplay SET_CURSOR_CHANNEL")
|
||||
}
|
||||
|
||||
/// RAII over a SetupAPI device-info list: every exit path of [`open_device`] destroys it (the error
|
||||
/// paths used to leak one `HDEVINFO` per failed open — and a driverless / mid-upgrade box probes
|
||||
/// repeatedly).
|
||||
@@ -460,6 +484,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
render_luid: Option<LUID>,
|
||||
preferred_monitor_id: u32,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
hw_cursor: bool,
|
||||
) -> Result<AddedMonitor> {
|
||||
let session_id = next_session_id();
|
||||
// The client display's volume rides into the monitor's EDID CTA HDR block; all-zero =
|
||||
@@ -485,7 +510,11 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
max_luminance_nits,
|
||||
max_frame_avg_nits,
|
||||
min_luminance_millinits,
|
||||
_reserved: 0,
|
||||
// v5 cursor channel: the driver declares an IddCx hardware cursor for this monitor
|
||||
// (DWM stops compositing the pointer into the frame); the capture layer delivers the
|
||||
// CursorShm section right after its ring. Zero toward older drivers is harmless —
|
||||
// the host only sets this when the handshake-reported proto is >= 5.
|
||||
hw_cursor: hw_cursor as u32,
|
||||
};
|
||||
// SET_RENDER_ADAPTER (opt-in; pf-vdisplay IMPLEMENTS it). Non-fatal on failure: the driver reports
|
||||
// its real render LUID in the shared header, so the host binds correctly even if this is ignored.
|
||||
@@ -682,6 +711,10 @@ pub struct PfVdisplayDisplay {
|
||||
/// freshly created monitor's EDID advertises this volume so host apps tone-map to the client's
|
||||
/// real panel.
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
/// Declare an IddCx hardware cursor on the created monitor (the M2c cursor channel). Set by
|
||||
/// [`set_hw_cursor`](VirtualDisplay::set_hw_cursor) before `create`; only honored when the
|
||||
/// driver handshake reported proto >= 5.
|
||||
hw_cursor: bool,
|
||||
/// The session's deliberate-quit flag (`None` = no signal → the linger policy applies). Set by
|
||||
/// [`set_quit_flag`](VirtualDisplay::set_quit_flag) before `create`; rides into every lease this
|
||||
/// backend mints so a user "stop" tears the monitor down immediately instead of lingering.
|
||||
@@ -694,6 +727,7 @@ impl PfVdisplayDisplay {
|
||||
Ok(Self {
|
||||
client_fp: None,
|
||||
client_hdr: None,
|
||||
hw_cursor: false,
|
||||
quit: None,
|
||||
})
|
||||
}
|
||||
@@ -712,12 +746,22 @@ impl VirtualDisplay for PfVdisplayDisplay {
|
||||
self.client_hdr = hdr;
|
||||
}
|
||||
|
||||
fn set_hw_cursor(&mut self, on: bool) {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
|
||||
self.quit = Some(quit);
|
||||
}
|
||||
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||
super::manager::vdm().acquire(mode, self.client_fp, self.client_hdr, self.quit.clone())
|
||||
super::manager::vdm().acquire(
|
||||
mode,
|
||||
self.client_fp,
|
||||
self.client_hdr,
|
||||
self.hw_cursor,
|
||||
self.quit.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,17 +848,12 @@ mod tests {
|
||||
// Live-run diagnostics: surface the manager/backend tracing (activation ladder, settle
|
||||
// waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the
|
||||
// first on-glass run blind.
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "debug".into()),
|
||||
)
|
||||
.try_init();
|
||||
// (tracing-subscriber is not a dep of this crate — run the host binary for traced runs.)
|
||||
// Context probe: can this process see the CCD active-path set at all? (`None` = the query
|
||||
// itself fails in this session/window-station — the whole ladder would be blind, and a
|
||||
// "monitor never activated" verdict would be an artifact of the test context.)
|
||||
// SAFETY: CCD query over an owned empty slice (test-only diagnostics).
|
||||
let active0 = unsafe { crate::win_display::count_other_active(&[]) };
|
||||
let active0 = unsafe { pf_win_display::win_display::count_other_active(&[]) };
|
||||
println!("spike: CCD active paths visible before create: {active0:?}");
|
||||
let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay");
|
||||
let first = vd
|
||||
@@ -847,7 +886,7 @@ mod tests {
|
||||
.target_id;
|
||||
let in_place = t1 == t2;
|
||||
// SAFETY: CCD query over a Copy target id (test-only diagnostics).
|
||||
let active = unsafe { crate::win_display::active_resolution(t2) };
|
||||
let active = unsafe { pf_win_display::win_display::active_resolution(t2) };
|
||||
println!(
|
||||
"in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \
|
||||
active resolution now {active:?}"
|
||||
|
||||
Reference in New Issue
Block a user