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:
2026-07-22 23:43:23 +02:00
co-authored by Claude Fable 5
parent c3cbffe662
commit 3a34440a6b
21 changed files with 1009 additions and 36 deletions
+21
View File
@@ -69,6 +69,15 @@ pub trait Capturer: Send {
/// SDR / a backend that doesn't expose it (the default — Linux capture has no HDR path yet). /// SDR / a backend that doesn't expose it (the default — Linux capture has no HDR path yet).
/// The stream loop forwards this to the encoder (in-band SEI) and the client (`0xCE` datagram), /// The stream loop forwards this to the encoder (in-band SEI) and the client (`0xCE` datagram),
/// so the two stay a single source of truth. May change mid-session if the source is regraded. /// so the two stay a single source of truth. May change mid-session if the source is regraded.
/// The capture source's LIVE cursor state, when it arrives out-of-band from the frames
/// (the Windows IddCx hardware-cursor channel). Polled by the encode loop every tick and
/// preferred over `CapturedFrame::cursor` — with a hardware cursor, pointer-only moves
/// produce NO new frame, so the frame-attached overlay would go stale on a static desktop.
/// Default `None`: the Linux portal path attaches its cursor to frames instead.
fn cursor(&mut self) -> Option<pf_frame::CursorOverlay> {
None
}
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> { fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
None None
} }
@@ -367,6 +376,16 @@ pub type FrameChannelSender = std::sync::Arc<
dyn Fn(&pf_driver_proto::control::SetFrameChannelRequest) -> Result<()> + Send + Sync, dyn Fn(&pf_driver_proto::control::SetFrameChannelRequest) -> Result<()> + Send + Sync,
>; >;
/// Delivery closure for the v5 hardware-cursor channel (`IOCTL_SET_CURSOR_CHANNEL`) — same
/// facade contract as [`FrameChannelSender`]. `Some` also OPTS THE SESSION IN: the capturer
/// creates + delivers the cursor section only when the host hands it a sender (the negotiated
/// cursor-forward sessions), and the driver only declares the hardware cursor once that
/// delivery lands — so a plain session keeps DWM's composited pointer untouched.
#[cfg(target_os = "windows")]
pub type CursorChannelSender = std::sync::Arc<
dyn Fn(&pf_driver_proto::control::SetCursorChannelRequest) -> Result<()> + Send + Sync,
>;
// One-time PipeWire library init, shared by the video (portal) and audio capture threads. // One-time PipeWire library init, shared by the video (portal) and audio capture threads.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod pwinit; pub mod pwinit;
@@ -450,6 +469,7 @@ pub fn open_idd_push(
pyrowave: bool, pyrowave: bool,
keepalive: Box<dyn Send>, keepalive: Box<dyn Send>,
sender: FrameChannelSender, sender: FrameChannelSender,
cursor_sender: Option<CursorChannelSender>,
) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> { ) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> {
idd_push::IddPushCapturer::open( idd_push::IddPushCapturer::open(
target, target,
@@ -459,6 +479,7 @@ pub fn open_idd_push(
pyrowave, pyrowave,
keepalive, keepalive,
sender, sender,
cursor_sender,
) )
.map(|c| Box::new(c) as Box<dyn Capturer>) .map(|c| Box::new(c) as Box<dyn Capturer>)
} }
+80 -1
View File
@@ -365,6 +365,8 @@ pub unsafe fn verify_is_wudfhost(process: HANDLE, wudf_pid: u32, what: &str) ->
#[path = "idd_push/channel.rs"] #[path = "idd_push/channel.rs"]
mod channel; mod channel;
#[path = "idd_push/cursor.rs"]
mod cursor;
#[path = "idd_push/descriptor.rs"] #[path = "idd_push/descriptor.rs"]
mod descriptor; mod descriptor;
#[path = "idd_push/stall.rs"] #[path = "idd_push/stall.rs"]
@@ -386,6 +388,10 @@ pub struct IddPushCapturer {
/// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open /// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open
/// and again on every ring recreate to deliver fresh duplicates. /// and again on every ring recreate to deliver fresh duplicates.
broker: ChannelBroker, broker: ChannelBroker,
/// The v5 hardware-cursor channel's host end (`Some` = delivered; the driver declared the
/// hardware cursor and seqlock-publishes into it). Survives ring recreates — the section is
/// independent of the frame ring's generation.
cursor_shared: Option<cursor::CursorShared>,
width: u32, width: u32,
height: u32, height: u32,
slots: Vec<HostSlot>, slots: Vec<HostSlot>,
@@ -603,6 +609,7 @@ impl IddPushCapturer {
/// instead of tearing the display down (audit §5.1 — no more 20 s black bail). "Failure" includes the /// instead of tearing the display down (audit §5.1 — no more 20 s black bail). "Failure" includes the
/// driver not attaching to the ring within a few seconds (e.g. a hybrid-GPU render mismatch). /// driver not attaching to the ring within a few seconds (e.g. a hybrid-GPU render mismatch).
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub fn open( pub fn open(
target: WinCaptureTarget, target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>, preferred: Option<(u32, u32, u32)>,
@@ -611,11 +618,20 @@ impl IddPushCapturer {
pyrowave: bool, pyrowave: bool,
keepalive: Box<dyn Send>, keepalive: Box<dyn Send>,
sender: crate::FrameChannelSender, sender: crate::FrameChannelSender,
cursor_sender: Option<crate::CursorChannelSender>,
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> { ) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so // The stall-attribution listener (idempotent): started with the first IDD-push capturer so
// the stall log can correlate DWM holes with OS display events for the session's lifetime. // the stall log can correlate DWM holes with OS display events for the session's lifetime.
pf_win_display::display_events::spawn_once(); pf_win_display::display_events::spawn_once();
match Self::open_inner(target, preferred, client_10bit, want_444, pyrowave, sender) { match Self::open_inner(
target,
preferred,
client_10bit,
want_444,
pyrowave,
sender,
cursor_sender,
) {
Ok(mut me) => { Ok(mut me) => {
me._keepalive = keepalive; me._keepalive = keepalive;
Ok(me) Ok(me)
@@ -632,6 +648,7 @@ impl IddPushCapturer {
want_444: bool, want_444: bool,
pyrowave: bool, pyrowave: bool,
sender: crate::FrameChannelSender, sender: crate::FrameChannelSender,
cursor_sender: Option<crate::CursorChannelSender>,
) -> Result<Self> { ) -> Result<Self> {
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the // The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor // selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
@@ -654,6 +671,7 @@ impl IddPushCapturer {
pyrowave, pyrowave,
luid, luid,
sender.clone(), sender.clone(),
cursor_sender.clone(),
) { ) {
Ok(me) => Ok(me), Ok(me) => Ok(me),
Err(e) => { Err(e) => {
@@ -687,6 +705,7 @@ impl IddPushCapturer {
pyrowave, pyrowave,
drv, drv,
sender, sender,
cursor_sender,
) )
.context("IDD-push rebind to the driver's reported render adapter") .context("IDD-push rebind to the driver's reported render adapter")
} }
@@ -702,6 +721,7 @@ impl IddPushCapturer {
pyrowave: bool, pyrowave: bool,
luid: LUID, luid: LUID,
sender: crate::FrameChannelSender, sender: crate::FrameChannelSender,
cursor_sender: Option<crate::CursorChannelSender>,
) -> Result<Self> { ) -> Result<Self> {
let (pw, ph, _hz) = preferred let (pw, ph, _hz) = preferred
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?; .context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
@@ -935,6 +955,60 @@ impl IddPushCapturer {
) )
.context("deliver IDD-push frame channel to the driver")?; .context("deliver IDD-push frame channel to the driver")?;
// v5 hardware-cursor channel (M2c): create + deliver the CursorShm section. Failure
// is NON-fatal — the driver never declares the hardware cursor without this delivery,
// so the session degrades to today's composited pointer (and the forwarder simply
// never sees a live overlay).
let cursor_shared = cursor_sender.and_then(|send_cursor| {
match cursor::CursorShared::create(target.target_id) {
Ok(cs) => {
// Duplication safety: `section_handle` is live for `cs`'s lifetime
// (owned mapping); already inside open_on's unsafe region.
let dup = broker.dup_into_public(cs.section_handle());
match dup {
Ok(value) => {
let req = pf_driver_proto::control::SetCursorChannelRequest {
target_id: target.target_id,
_pad: 0,
header_handle: value,
};
match send_cursor(&req) {
Ok(()) => {
tracing::info!(
target_id = target.target_id,
"IDD push(host): cursor channel delivered — driver \
declares the hardware cursor"
);
Some(cs)
}
Err(e) => {
broker.close_remote_public(value);
tracing::warn!(
"cursor channel delivery failed (composited cursor \
stays): {e:#}"
);
None
}
}
}
Err(e) => {
tracing::warn!(
"cursor section duplication failed (composited cursor \
stays): {e:#}"
);
None
}
}
}
Err(e) => {
tracing::warn!(
"cursor section creation failed (composited cursor stays): {e:#}"
);
None
}
}
});
tracing::info!( tracing::info!(
target_id = target.target_id, target_id = target.target_id,
wudf_pid = target.wudf_pid, wudf_pid = target.wudf_pid,
@@ -993,6 +1067,7 @@ impl IddPushCapturer {
last_seq: 0, last_seq: 0,
last_present: None, last_present: None,
status_logged: false, status_logged: false,
cursor_shared,
// Held from BEFORE the first-frame gate (the display must not idle off while we // Held from BEFORE the first-frame gate (the display must not idle off while we
// wait for the first compose) until the capturer drops with the session. // wait for the first compose) until the capturer drops with the session.
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(), _display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
@@ -1982,6 +2057,10 @@ impl std::fmt::Display for AttachTexFail {
impl std::error::Error for AttachTexFail {} impl std::error::Error for AttachTexFail {}
impl Capturer for IddPushCapturer { impl Capturer for IddPushCapturer {
fn cursor(&mut self) -> Option<pf_frame::CursorOverlay> {
self.cursor_shared.as_mut().and_then(|c| c.read())
}
fn next_frame(&mut self) -> Result<CapturedFrame> { fn next_frame(&mut self) -> Result<CapturedFrame> {
let deadline = Instant::now() + Duration::from_secs(20); let deadline = Instant::now() + Duration::from_secs(20);
loop { loop {
@@ -105,6 +105,22 @@ impl ChannelBroker {
Ok(out.0 as usize as u64) Ok(out.0 as usize as u64)
} }
/// Duplicate the cursor section into WUDFHost (v5 cursor channel) with the same
/// least-privilege section rights as the frame header. Thin `pub(super)` face over
/// [`dup_into`](Self::dup_into) for the cursor-delivery path in `open_on`.
///
/// # Safety
/// `h` must be a live handle of the current process.
pub(super) unsafe fn dup_into_public(&self, h: HANDLE) -> Result<u64> {
// SAFETY: forwarded contract — `h` is live per this fn's own contract.
unsafe { self.dup_into(h, Some(SECTION_MAP_RW)) }
}
/// [`close_remote`](Self::close_remote) for the cursor-delivery failure path.
pub(super) fn close_remote_public(&self, value: u64) {
self.close_remote(value);
}
/// Close a handle VALUE inside the WUDFHost table (the failure-path reaper): `DUPLICATE_CLOSE_SOURCE` /// Close a handle VALUE inside the WUDFHost table (the failure-path reaper): `DUPLICATE_CLOSE_SOURCE`
/// with no target closes the source handle regardless of the (ignored) result. /// with no target closes the source handle regardless of the (ignored) result.
fn close_remote(&self, value: u64) { fn close_remote(&self, value: u64) {
@@ -0,0 +1,194 @@
//! Host side of the v5 hardware-cursor channel (remote-desktop-sweep M2c): the capturer creates
//! an unnamed [`CursorShm`] section, delivers it to the pf-vdisplay driver (which declares an
//! IddCx hardware cursor — DWM then EXCLUDES the pointer from the frames we consume), and reads
//! the driver's seqlock publishes here at encode-tick pace, converting them into the same
//! [`pf_frame::CursorOverlay`] the Linux portal path produces — everything downstream (the
//! cursor forwarder, the wire, the client renderer) is shared.
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it.
#![deny(clippy::undocumented_unsafe_blocks)]
use super::*;
use pf_driver_proto::cursor::{
CursorShm, CURSOR_MAGIC, CURSOR_SHAPE_BYTES, CURSOR_SHAPE_MAX, CURSOR_SHAPE_OFFSET,
CURSOR_SHM_SIZE, CURSOR_TYPE_MASKED_COLOR,
};
use std::sync::atomic::AtomicU32;
/// The host end of one monitor's cursor channel: the section (we created it — the mapping stays
/// valid for the capturer's life) plus the reader's conversion cache.
pub(super) struct CursorShared {
section: MappedSection,
/// The monitor's desktop origin — IddCx reports positions in DESKTOP coordinates; the
/// overlay wants frame-relative. Fetched at attach (the virtual monitor's placement is
/// stable for the session; a topology change recreates the pipeline anyway).
origin: (i32, i32),
/// Conversion cache: the last `shape_id` whose pixels were converted, and the result.
/// Position-only updates (the common case) reuse it — a refcount bump, no pixel work.
cached_id: u32,
cached: Option<ConvertedShape>,
}
struct ConvertedShape {
rgba: std::sync::Arc<Vec<u8>>,
w: u32,
h: u32,
hot_x: u32,
hot_y: u32,
}
impl CursorShared {
/// Create + initialize the section (magic stamped, seq even/zero). The returned handle is
/// the section itself (owned by `self`); the caller duplicates it into the WUDFHost.
pub(super) fn create(target_id: u32) -> Result<CursorShared> {
// SAFETY: plain FFI. Unnamed pagefile-backed section, host-lifetime owned; the view is
// mapped once and unmapped never (the capturer's life = the session's life).
let section = unsafe {
let map = CreateFileMappingW(
INVALID_HANDLE_VALUE,
None,
PAGE_READWRITE,
0,
CURSOR_SHM_SIZE as u32,
PCWSTR::null(),
)
.context("CreateFileMapping(cursor)")?;
let map = OwnedHandle::from_raw_handle(map.0 as _);
let view = MapViewOfFile(
HANDLE(map.as_raw_handle()),
FILE_MAP_ALL_ACCESS,
0,
0,
CURSOR_SHM_SIZE,
);
if view.Value.is_null() {
bail!("MapViewOfFile failed for the cursor section");
}
let shm = view.Value.cast::<CursorShm>();
std::ptr::write_bytes(view.Value.cast::<u8>(), 0, CURSOR_SHM_SIZE);
// Magic LAST-ish (the driver validates it at adopt; seq 0 = even = consistent).
std::sync::atomic::fence(Ordering::Release);
(*shm).magic = CURSOR_MAGIC;
MappedSection { handle: map, view }
};
// Desktop origin of this monitor's source — for the desktop→frame coordinate shift.
// SAFETY: `source_desktop_rect` only runs the CCD QueryDisplayConfig FFI over owned
// locals (same call the compose-kick path makes).
let rect = unsafe { pf_win_display::win_display::source_desktop_rect(target_id) };
let origin = rect.map(|(x, y, _w, _h)| (x, y)).unwrap_or((0, 0));
Ok(CursorShared {
section,
origin,
cached_id: 0,
cached: None,
})
}
/// The section handle for the broker's duplication into the WUDFHost.
pub(super) fn section_handle(&self) -> HANDLE {
HANDLE(self.section.handle.as_raw_handle())
}
/// Seqlock-read the driver's latest publish → a frame-relative [`pf_frame::CursorOverlay`].
/// `None` until the first publish lands (or while the pointer has never been seen). A hidden
/// pointer returns `Some` with `visible: false` — the forwarder turns that into the client's
/// relative-mode hint, exactly like the Linux path.
pub(super) fn read(&mut self) -> Option<pf_frame::CursorOverlay> {
let shm = self.section.ptr::<CursorShm>();
// SAFETY: the view spans CURSOR_SHM_SIZE for self's lifetime; seq is 4-aligned in the
// fixed layout (offset 4).
let seq = unsafe { &*std::ptr::addr_of!((*shm).seq).cast::<AtomicU32>() };
for _ in 0..64 {
let s1 = seq.load(Ordering::Acquire);
if s1 == 0 {
return None; // no publish yet
}
if s1 & 1 != 0 {
std::hint::spin_loop();
continue; // writer mid-update
}
// SAFETY: header reads within the mapped view; consistency is validated by the
// seq re-check below (a torn read is discarded and retried).
let hdr = unsafe { std::ptr::read_volatile(shm) };
// Shape pixels: convert only when the OS minted a new shape id.
if hdr.visible != 0 && hdr.shape_id != self.cached_id {
let rows = hdr.height.min(CURSOR_SHAPE_MAX) as usize;
let width = hdr.width.min(CURSOR_SHAPE_MAX) as usize;
let pitch = (hdr.pitch as usize).min(CURSOR_SHAPE_BYTES / rows.max(1));
let mut raw = vec![0u8; rows * pitch];
// SAFETY: the shape region spans CURSOR_SHAPE_BYTES from CURSOR_SHAPE_OFFSET
// inside the mapped view; `rows * pitch` is clamped to it above.
unsafe {
std::ptr::copy_nonoverlapping(
self.section.ptr::<u8>().add(CURSOR_SHAPE_OFFSET),
raw.as_mut_ptr(),
rows * pitch,
);
}
// Discard the copy if the writer raced us mid-shape (seq moved) — retry.
if seq.load(Ordering::Acquire) != s1 {
continue;
}
self.cached = Some(convert_shape(&hdr, &raw, width, rows, pitch));
self.cached_id = hdr.shape_id;
} else if seq.load(Ordering::Acquire) != s1 {
continue;
}
let shape = self.cached.as_ref()?;
return Some(pf_frame::CursorOverlay {
x: hdr.x - self.origin.0,
y: hdr.y - self.origin.1,
w: shape.w,
h: shape.h,
rgba: shape.rgba.clone(),
serial: u64::from(hdr.shape_id),
hot_x: shape.hot_x,
hot_y: shape.hot_y,
visible: hdr.visible != 0,
});
}
None // persistent tearing (writer wedged mid-seq) — skip this tick
}
}
/// Convert the OS's 32-bpp pitch-strided shape rows into the overlay's packed straight RGBA.
/// ALPHA cursors are BGRA with straight per-pixel alpha (swap R↔B). MASKED_COLOR approximates:
/// alpha 0x00 = opaque color pixel; 0xFF = an XOR pixel we cannot honor client-side — rendered
/// as a translucent mid-gray so inversion cursors stay visible instead of vanishing.
fn convert_shape(
hdr: &CursorShm,
raw: &[u8],
width: usize,
rows: usize,
pitch: usize,
) -> ConvertedShape {
let masked = hdr.cursor_type == CURSOR_TYPE_MASKED_COLOR;
let mut rgba = Vec::with_capacity(width * rows * 4);
for y in 0..rows {
let row = &raw[y * pitch..];
for x in 0..width {
let o = x * 4;
if o + 4 > row.len() {
rgba.extend_from_slice(&[0, 0, 0, 0]);
continue;
}
let (b, g, r, a) = (row[o], row[o + 1], row[o + 2], row[o + 3]);
if masked {
if a == 0 {
rgba.extend_from_slice(&[r, g, b, 0xFF]);
} else {
rgba.extend_from_slice(&[0x80, 0x80, 0x80, 0xB4]);
}
} else {
rgba.extend_from_slice(&[r, g, b, a]);
}
}
}
ConvertedShape {
rgba: std::sync::Arc::new(rgba),
w: width as u32,
h: rows as u32,
hot_x: hdr.hot_x.min(width.saturating_sub(1) as u32),
hot_y: hdr.hot_y.min(rows.saturating_sub(1) as u32),
}
}
+152 -7
View File
@@ -66,7 +66,15 @@ pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
/// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it; /// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it;
/// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship /// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship
/// driver+host together, as ever. /// driver+host together, as ever.
pub const PROTOCOL_VERSION: u32 = 4; /// v5: ADDITIVE — the IddCx HARDWARE CURSOR channel (remote-desktop-sweep M2c):
/// [`control::AddRequest::hw_cursor`] (the former tail `_reserved` — same size, same offsets)
/// asks the driver to declare a hardware cursor for this monitor (DWM then EXCLUDES the pointer
/// from the desktop frame and delivers shape/position out-of-band), and
/// [`control::IOCTL_SET_CURSOR_CHANNEL`] delivers the host-created [`cursor::CursorShm`] section
/// the driver's cursor thread seqlock-publishes into. Nothing existing changed; the host gates
/// the feature on the handshake-reported version (`>= 5`) and keeps composited-cursor behavior
/// against older drivers.
pub const PROTOCOL_VERSION: u32 = 5;
/// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on /// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on
/// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the /// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the
@@ -110,6 +118,14 @@ pub mod control {
/// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3 /// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3
/// driver fails this unknown IOCTL → the host falls back to the re-arrival resize. /// driver fails this unknown IOCTL → the host falls back to the re-arrival resize.
pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907); pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907);
/// Deliver a monitor's hardware-cursor channel (v5): the handle VALUE of the unnamed
/// [`cursor::CursorShm`](crate::cursor) file mapping the host duplicated into the WUDFHost
/// (same delivery model as [`IOCTL_SET_FRAME_CHANNEL`], no event — the host polls the
/// seqlock at its encode-tick pace). Sent once after ADD, only for a monitor whose
/// [`AddRequest::hw_cursor`] was set. The driver maps it, calls
/// `IddCxMonitorSetupHardwareCursor`, and starts its cursor-query thread. Input
/// [`SetCursorChannelRequest`].
pub const IOCTL_SET_CURSOR_CHANNEL: u32 = ctl_code(0x908);
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns /// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this /// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
@@ -147,9 +163,13 @@ pub mod control {
/// The client display's min luminance in MILLI-nits (0.001 cd/m² — the CTA min-luminance /// The client display's min luminance in MILLI-nits (0.001 cd/m² — the CTA min-luminance
/// range lives well below 1 nit) → Desired Content Min Luminance. `0` = unknown. /// range lives well below 1 nit) → Desired Content Min Luminance. `0` = unknown.
pub min_luminance_millinits: u32, pub min_luminance_millinits: u32,
/// Pads the `u64`-aligned struct to a multiple of 8 (Pod forbids implicit tail padding); /// Non-zero = declare an IddCx HARDWARE CURSOR for this monitor (v5, remote-desktop-sweep
/// free expansion room for the next appended field. /// M2c): DWM stops compositing the pointer into the frame and the driver publishes
pub _reserved: u32, /// shape/position into the [`cursor::CursorShm`](crate::cursor) section delivered by
/// [`IOCTL_SET_CURSOR_CHANNEL`]. Byte-compatible with the old tail `_reserved` (offset 36):
/// an un-upgraded driver ignores it (cursor stays composited — the host already gates on
/// the handshake version, this is defense in depth), an un-upgraded host sends `0` (off).
pub hw_cursor: u32,
} }
/// [`AddRequest`]'s size before the client-HDR luminance tail — the prefix an un-upgraded /// [`AddRequest`]'s size before the client-HDR luminance tail — the prefix an un-upgraded
@@ -253,6 +273,18 @@ pub mod control {
/// at the compile-time maximum; `ring_len` says how many entries are live). /// at the compile-time maximum; `ring_len` says how many entries are live).
pub const RING_LEN_USIZE: usize = RING_LEN as usize; pub const RING_LEN_USIZE: usize = RING_LEN as usize;
/// `IOCTL_SET_CURSOR_CHANNEL` input (v5): the hardware-cursor section for one monitor.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
pub struct SetCursorChannelRequest {
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
pub target_id: u32,
pub _pad: u32,
/// The [`cursor::CursorShm`](crate::cursor) file-mapping handle VALUE, already duplicated
/// into the driver's WUDFHost process ([`AddReply::wudf_pid`]).
pub header_handle: u64,
}
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already // Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!` // rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss. // asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
@@ -269,6 +301,9 @@ pub mod control {
assert!(offset_of!(AddRequest, max_luminance_nits) == ADD_REQUEST_LEGACY_SIZE); assert!(offset_of!(AddRequest, max_luminance_nits) == ADD_REQUEST_LEGACY_SIZE);
assert!(offset_of!(AddRequest, max_frame_avg_nits) == 28); assert!(offset_of!(AddRequest, max_frame_avg_nits) == 28);
assert!(offset_of!(AddRequest, min_luminance_millinits) == 32); assert!(offset_of!(AddRequest, min_luminance_millinits) == 32);
// v5: the former tail `_reserved` — same offset, same total size (rename-only).
assert!(offset_of!(AddRequest, hw_cursor) == 36);
assert!(size_of::<AddRequest>() == 40);
assert!(size_of::<AddReply>() == 20); assert!(size_of::<AddReply>() == 20);
assert!(offset_of!(AddReply, adapter_luid_low) == 0); assert!(offset_of!(AddReply, adapter_luid_low) == 0);
@@ -288,6 +323,10 @@ pub mod control {
assert!(size_of::<RemoveRequest>() == 8); assert!(size_of::<RemoveRequest>() == 8);
assert!(offset_of!(RemoveRequest, session_id) == 0); assert!(offset_of!(RemoveRequest, session_id) == 0);
assert!(size_of::<SetCursorChannelRequest>() == 16);
assert!(offset_of!(SetCursorChannelRequest, target_id) == 0);
assert!(offset_of!(SetCursorChannelRequest, header_handle) == 8);
assert!(size_of::<UpdateModesRequest>() == 24); assert!(size_of::<UpdateModesRequest>() == 24);
assert!(offset_of!(UpdateModesRequest, session_id) == 0); assert!(offset_of!(UpdateModesRequest, session_id) == 0);
assert!(offset_of!(UpdateModesRequest, width) == 8); assert!(offset_of!(UpdateModesRequest, width) == 8);
@@ -960,6 +999,84 @@ pub mod mouse {
}; };
} }
/// The v5 hardware-cursor channel (remote-desktop-sweep M2c): one unnamed file mapping per
/// monitor, host-created, delivered by handle value ([`control::IOCTL_SET_CURSOR_CHANNEL`]).
/// The DRIVER's cursor thread (woken by its IddCx `hNewCursorDataAvailable` event) seqlock-writes
/// shape + position + visibility; the HOST reads at its encode-tick pace — no event crosses the
/// boundary. Writer: bump [`CursorShm::seq`] to ODD, write fields (+ shape bytes when the OS said
/// the shape changed), bump to EVEN. Reader: read seq (retry while odd), copy, re-read seq —
/// unchanged ⇒ consistent snapshot. Position-only updates never touch the shape bytes, so a
/// reader that skips unchanged `shape_id`s never copies torn pixels.
pub mod cursor {
use bytemuck::{Pod, Zeroable};
/// First field of [`CursorShm`] — `b"PFCU"` little-endian; anything else = not attached yet.
pub const CURSOR_MAGIC: u32 = u32::from_le_bytes(*b"PFCU");
/// Max cursor side (px) the driver declares to the OS (`IDDCX_CURSOR_CAPS::MaxX/MaxY`) and
/// the section's shape buffer is sized for. Windows XL accessibility cursors top out here;
/// the host's wire forwarder downscales to its own cap anyway.
pub const CURSOR_SHAPE_MAX: u32 = 256;
/// Shape-buffer bytes: 32-bpp at the declared max.
pub const CURSOR_SHAPE_BYTES: usize = (CURSOR_SHAPE_MAX * CURSOR_SHAPE_MAX * 4) as usize;
/// Byte offset of the shape pixels inside the section (one cache-line-ish header).
pub const CURSOR_SHAPE_OFFSET: usize = 64;
/// Total section size.
pub const CURSOR_SHM_SIZE: usize = CURSOR_SHAPE_OFFSET + CURSOR_SHAPE_BYTES;
/// `IDDCX_CURSOR_SHAPE_TYPE` values mirrored for the host (the driver writes the OS value
/// verbatim into [`CursorShm::cursor_type`]).
pub const CURSOR_TYPE_MASKED_COLOR: u32 = 1;
pub const CURSOR_TYPE_ALPHA: u32 = 2;
/// The section header (the shape pixels follow at [`CURSOR_SHAPE_OFFSET`]). `x`/`y` are the
/// shape's TOP-LEFT in desktop coordinates (the IddCx `IDARG_OUT_QUERY_HWCURSOR::X/Y`
/// convention — position hotspot, can be negative); `shape_id` is the OS's per-set counter
/// (bumps on every shape set, the overlay serial); pixels are the OS's 32-bpp rows at
/// `pitch` bytes (BGRA for ALPHA; color+mask for MASKED_COLOR — the host converts).
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
pub struct CursorShm {
pub magic: u32,
/// Seqlock: odd = writer mid-update.
pub seq: u32,
pub visible: u32,
pub cursor_type: u32,
pub x: i32,
pub y: i32,
pub shape_id: u32,
pub width: u32,
pub height: u32,
pub pitch: u32,
pub hot_x: u32,
pub hot_y: u32,
/// Reserved expansion room up to [`CURSOR_SHAPE_OFFSET`].
pub _reserved: [u32; 4],
}
// Layout is load-bearing across the process boundary — pin it.
const _: () = {
use core::mem::{offset_of, size_of};
assert!(size_of::<CursorShm>() == 64);
assert!(size_of::<CursorShm>() <= CURSOR_SHAPE_OFFSET);
assert!(offset_of!(CursorShm, magic) == 0);
assert!(offset_of!(CursorShm, seq) == 4);
assert!(offset_of!(CursorShm, visible) == 8);
assert!(offset_of!(CursorShm, cursor_type) == 12);
assert!(offset_of!(CursorShm, x) == 16);
assert!(offset_of!(CursorShm, y) == 20);
assert!(offset_of!(CursorShm, shape_id) == 24);
assert!(offset_of!(CursorShm, width) == 28);
assert!(offset_of!(CursorShm, height) == 32);
assert!(offset_of!(CursorShm, pitch) == 36);
assert!(offset_of!(CursorShm, hot_x) == 40);
assert!(offset_of!(CursorShm, hot_y) == 44);
};
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -1079,7 +1196,7 @@ mod tests {
max_luminance_nits: 800, max_luminance_nits: 800,
max_frame_avg_nits: 400, max_frame_avg_nits: 400,
min_luminance_millinits: 50, // 0.05 nits min_luminance_millinits: 50, // 0.05 nits
_reserved: 0, hw_cursor: 1,
}; };
let bytes = bytemuck::bytes_of(&req); let bytes = bytemuck::bytes_of(&req);
assert_eq!(bytes.len(), 40); assert_eq!(bytes.len(), 40);
@@ -1161,11 +1278,39 @@ mod tests {
req req
); );
assert_eq!(bytes[8..12], 2560u32.to_le_bytes()); assert_eq!(bytes[8..12], 2560u32.to_le_bytes());
// The compat window: v4 is additive over v3, so the host floor stays one below. // The compat window: v4/v5 are additive over v3, so the host floor stays at 3.
assert_eq!(PROTOCOL_VERSION, 4); assert_eq!(PROTOCOL_VERSION, 5);
assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3); assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3);
} }
#[test]
fn cursor_shm_layout_is_pinned() {
use cursor::*;
// The header must leave the shape offset intact whatever grows inside `_reserved`.
assert_eq!(core::mem::size_of::<CursorShm>(), 64);
assert_eq!(CURSOR_SHM_SIZE, 64 + 256 * 256 * 4);
assert_eq!(CURSOR_MAGIC, u32::from_le_bytes(*b"PFCU"));
// Seqlock snapshot discipline survives a bytemuck roundtrip.
let hdr = CursorShm {
magic: CURSOR_MAGIC,
seq: 2,
visible: 1,
cursor_type: CURSOR_TYPE_ALPHA,
x: -3,
y: 7,
shape_id: 42,
width: 32,
height: 32,
pitch: 128,
hot_x: 4,
hot_y: 5,
_reserved: [0; 4],
};
let bytes = bytemuck::bytes_of(&hdr);
assert_eq!(*bytemuck::from_bytes::<CursorShm>(bytes), hdr);
assert_eq!(bytes[16..20], (-3i32).to_le_bytes());
}
#[test] #[test]
fn gamepad_names_and_magics_are_stable() { fn gamepad_names_and_magics_are_stable() {
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0"); assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
+8
View File
@@ -152,6 +152,12 @@ pub struct OutputFormat {
/// codec's backend, and the fallback family) would misread the two-plane buffer as packed /// codec's backend, and the fallback family) would misread the two-plane buffer as packed
/// RGB. Always `false` on Windows (the IDD-push capturer owns its own formats). /// RGB. Always `false` on Windows (the IDD-push capturer owns its own formats).
pub nv12_native: bool, pub nv12_native: bool,
/// The session negotiated the cursor-forward channel (remote-desktop-sweep M2c): on Windows
/// the IDD-push capturer creates + delivers the driver's hardware-cursor section, so DWM
/// stops compositing the pointer into the frames and the capturer surfaces it via
/// `Capturer::cursor()` instead. Ignored on Linux (the portal's `SPA_META_Cursor` already
/// separates the pointer; the session plan's `cursor_blend` gate handles the rest).
pub hw_cursor: bool,
} }
impl OutputFormat { impl OutputFormat {
@@ -169,6 +175,8 @@ impl OutputFormat {
chroma_444: false, chroma_444: false,
// GameStream never negotiates PyroWave (native punktfunk/1 only). // GameStream never negotiates PyroWave (native punktfunk/1 only).
pyrowave: false, pyrowave: false,
// GameStream/spike sessions never negotiate the cursor channel.
hw_cursor: false,
// Conservative: the GameStream + spike paths don't resolve the codec here, and a // Conservative: the GameStream + spike paths don't resolve the codec here, and a
// Moonlight client may negotiate H264 (whose VAAPI backend can't ingest NV12) — so // Moonlight client may negotiate H264 (whose VAAPI backend can't ingest NV12) — so
// they never prefer the producer-native NV12 pod. The punktfunk/1 plane opts in via // they never prefer the producer-native NV12 pod. The punktfunk/1 plane opts in via
@@ -134,6 +134,13 @@ pub trait VirtualDisplay: Send {
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint /// 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). /// 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>) {} 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 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 /// 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 /// 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 /// 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. /// on — [`warn_if_pick_moved`] compares the CURRENT pick against it.
render_pin: Option<LUID>, 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 /// 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 /// 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 /// 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 /// 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 /// 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. /// 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> { pub fn control_device_handle() -> Option<HANDLE> {
VDM.get().and_then(VirtualDisplayManager::device_handle) VDM.get().and_then(VirtualDisplayManager::device_handle)
} }
@@ -390,6 +407,7 @@ impl VirtualDisplayManager {
mode: Mode, mode: Mode,
client_fp: Option<[u8; 32]>, client_fp: Option<[u8; 32]>,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
hw_cursor: bool,
quit: Option<Arc<AtomicBool>>, quit: Option<Arc<AtomicBool>>,
) -> Result<VirtualOutput> { ) -> Result<VirtualOutput> {
// Console-session guard: a host outside the ACTIVE console session cannot drive the display // 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 // 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, // `ensure_device()` returned above (cached handles are never closed — a dead one is retired,
// kept alive; see `DeviceSlot`), and we hold the `state` lock. // 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 // 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 // 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 // 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 // SAFETY: as above — `dev` is the handle the reopening `ensure_device` just
// returned, and the `state` lock is still held. // 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?, r => r?,
}; };
@@ -861,6 +881,7 @@ impl VirtualDisplayManager {
mode: Mode, mode: Mode,
slot: u32, slot: u32,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
hw_cursor: bool,
inner: &mut MgrInner, inner: &mut MgrInner,
) -> Result<Monitor> { ) -> Result<Monitor> {
// The slot id doubles as the driver-preferred monitor id (EDID serial / ConnectorIndex), so // 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. // `0` (anonymous) = the driver auto-allocates the lowest-free id.
let preferred_id = slot; let preferred_id = slot;
let render_pin = resolve_render_pin(); 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 // 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. // 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 // `render_pin` is an `Option<LUID>` by value (plain `Copy`), so no borrowed memory
// crosses the call. // crosses the call.
let added = unsafe { let added = unsafe {
self.driver 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. // 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, resolved_monitor_id: added.resolved_monitor_id,
position: (0, 0), position: (0, 0),
gen: self.gen.fetch_add(1, Ordering::Relaxed), 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. // values passed by value — no borrow crosses the call.
let added = unsafe { let added = unsafe {
self.driver 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")? .context("re-arrival ADD at the new mode")?
}; };
self.ensure_pinger(); self.ensure_pinger();
@@ -1283,6 +1309,7 @@ impl VirtualDisplayManager {
resolved_monitor_id: added.resolved_monitor_id, resolved_monitor_id: added.resolved_monitor_id,
position: old.position, position: old.position,
gen: old.gen, gen: old.gen,
hw_cursor: old.hw_cursor,
}) })
} }
@@ -56,6 +56,7 @@ pub(crate) trait VdisplayDriver: Send + Sync {
render_luid: Option<LUID>, render_luid: Option<LUID>,
preferred_monitor_id: u32, preferred_monitor_id: u32,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
hw_cursor: bool,
) -> Result<AddedMonitor>; ) -> Result<AddedMonitor>;
/// Refresh the LIVE monitor `key`'s advertised mode list to lead with `mode` (the in-place /// 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). /// 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") .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 /// 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 /// paths used to leak one `HDEVINFO` per failed open — and a driverless / mid-upgrade box probes
/// repeatedly). /// repeatedly).
@@ -460,6 +484,7 @@ impl VdisplayDriver for PfVdisplayDriver {
render_luid: Option<LUID>, render_luid: Option<LUID>,
preferred_monitor_id: u32, preferred_monitor_id: u32,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
hw_cursor: bool,
) -> Result<AddedMonitor> { ) -> Result<AddedMonitor> {
let session_id = next_session_id(); let session_id = next_session_id();
// The client display's volume rides into the monitor's EDID CTA HDR block; all-zero = // 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_luminance_nits,
max_frame_avg_nits, max_frame_avg_nits,
min_luminance_millinits, 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 // 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. // 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 /// freshly created monitor's EDID advertises this volume so host apps tone-map to the client's
/// real panel. /// real panel.
client_hdr: Option<punktfunk_core::quic::HdrMeta>, 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 /// 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 /// [`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. /// backend mints so a user "stop" tears the monitor down immediately instead of lingering.
@@ -694,6 +727,7 @@ impl PfVdisplayDisplay {
Ok(Self { Ok(Self {
client_fp: None, client_fp: None,
client_hdr: None, client_hdr: None,
hw_cursor: false,
quit: None, quit: None,
}) })
} }
@@ -712,12 +746,22 @@ impl VirtualDisplay for PfVdisplayDisplay {
self.client_hdr = hdr; 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>) { fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
self.quit = Some(quit); self.quit = Some(quit);
} }
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> { 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 // 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 // waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the
// first on-glass run blind. // first on-glass run blind.
let _ = tracing_subscriber::fmt() // (tracing-subscriber is not a dep of this crate — run the host binary for traced runs.)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "debug".into()),
)
.try_init();
// Context probe: can this process see the CCD active-path set at all? (`None` = the query // 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 // 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.) // "monitor never activated" verdict would be an artifact of the test context.)
// SAFETY: CCD query over an owned empty slice (test-only diagnostics). // 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:?}"); println!("spike: CCD active paths visible before create: {active0:?}");
let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay"); let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay");
let first = vd let first = vd
@@ -847,7 +886,7 @@ mod tests {
.target_id; .target_id;
let in_place = t1 == t2; let in_place = t1 == t2;
// SAFETY: CCD query over a Copy target id (test-only diagnostics). // 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!( println!(
"in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \ "in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \
active resolution now {active:?}" active resolution now {active:?}"
+18
View File
@@ -157,6 +157,23 @@ pub fn capture_virtual_output(
// proactively enables advanced color and selects the per-frame conversion. There is NO fallback: // 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 // if it can't open or the driver doesn't attach, the session fails cleanly and the client
// reconnects. // 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( pf_capture::open_idd_push(
target, target,
pref, pref,
@@ -165,6 +182,7 @@ pub fn capture_virtual_output(
want.pyrowave, want.pyrowave,
keep, keep,
sender, sender,
cursor_sender,
) )
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)")) .map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
} }
+29 -8
View File
@@ -10,18 +10,35 @@ use super::*;
/// Whether this session forwards the cursor out-of-band (design/remote-desktop-sweep.md M2): /// 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 /// 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 /// capture path can deliver cursor metadata separately from the frame — the Linux portal
/// Linux portal `SPA_META_Cursor` path only: not gamescope (its capture paints no cursor at /// `SPA_META_Cursor` path (not gamescope, whose capture paints no cursor at all), or Windows
/// all), not Windows (DWM composites into the IDD frame — M2c). THE single predicate: the /// with a proto-v5 pf-vdisplay driver (the IddCx hardware-cursor channel, M2c). THE single
/// Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off wiring both read it, /// predicate: the Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off
/// so they can never disagree. /// wiring both read it, so they can never disagree.
pub(super) fn cursor_forward( pub(super) fn cursor_forward(
client_caps: u8, client_caps: u8,
compositor: Option<crate::vdisplay::Compositor>, compositor: Option<crate::vdisplay::Compositor>,
) -> bool { ) -> bool {
cfg!(target_os = "linux") if client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR == 0 {
&& client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR != 0 return false;
&& compositor.is_some_and(|c| c != crate::vdisplay::Compositor::Gamescope) }
#[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 /// 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 (ctx_tx, ctx_rx) = std::sync::mpsc::sync_channel::<SessionContext>(1);
let client_identity = endpoint::peer_fingerprint(conn); let client_identity = endpoint::peer_fingerprint(conn);
let client_hdr = hello.display_hdr; 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 (mode, shard_payload) = (hello.mode, welcome.shard_payload);
let trace = bringup.clone(); let trace = bringup.clone();
std::thread::Builder::new() std::thread::Builder::new()
@@ -520,6 +540,7 @@ pub(super) async fn negotiate(
mode, mode,
client_identity, client_identity,
client_hdr, client_hdr,
cursor_fw,
bitrate_kbps, bitrate_kbps,
bit_depth, bit_depth,
chroma, chroma,
+18 -4
View File
@@ -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 // the client is not drawing it locally (the M2 cursor channel — blending too would
// show it twice). // show it twice).
ctx.compositor != pf_vdisplay::Compositor::Gamescope && !ctx.cursor_forward, 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 // 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. // 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 // panel instead of the driver's built-in ~1000-nit placeholder. No-op on Linux
// backends and for older/SDR clients. // backends and for older/SDR clients.
vd.set_client_hdr(client_hdr); 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 // 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 // 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 // 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 // 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 // (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 // bridge. A hidden-but-known pointer (overlay with `visible: false`) is the M3
// (overlay with `visible: false`) is the M3 relative-mode hint. // 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() { 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 // 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. // 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, mode: punktfunk_core::Mode,
client_identity: Option<[u8; 32]>, client_identity: Option<[u8; 32]>,
client_hdr: Option<punktfunk_core::quic::HdrMeta>, client_hdr: Option<punktfunk_core::quic::HdrMeta>,
cursor_forward: bool,
bitrate_kbps: u32, bitrate_kbps: u32,
bit_depth: u8, bit_depth: u8,
chroma: crate::encode::ChromaFormat, chroma: crate::encode::ChromaFormat,
@@ -2691,7 +2703,8 @@ pub(super) fn prepare_display(
bit_depth, bit_depth,
chroma, chroma,
codec, codec,
compositor != pf_vdisplay::Compositor::Gamescope, compositor != pf_vdisplay::Compositor::Gamescope && !cursor_forward,
cursor_forward,
); );
if codec == crate::encode::Codec::PyroWave { if codec == crate::encode::Codec::PyroWave {
plan.wire_chunk = Some(shard_payload as usize); plan.wire_chunk = Some(shard_payload as usize);
@@ -2699,6 +2712,7 @@ pub(super) fn prepare_display(
let mut vd = crate::vdisplay::open(compositor)?; let mut vd = crate::vdisplay::open(compositor)?;
vd.set_client_identity(client_identity); vd.set_client_identity(client_identity);
vd.set_client_hdr(client_hdr); vd.set_client_hdr(client_hdr);
vd.set_hw_cursor(cursor_forward);
vd.set_quit_flag(quit.clone()); vd.set_quit_flag(quit.clone());
// Slot-scoped setup serialization + reconnect preempt — see the inline arm in // Slot-scoped setup serialization + reconnect preempt — see the inline arm in
// `virtual_stream` for the full rationale; released when this fn returns. // `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 /// 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. /// blending path when this is set, so the pointer never silently vanishes from the stream.
pub cursor_blend: bool, 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 { impl SessionPlan {
@@ -118,6 +122,7 @@ impl SessionPlan {
chroma: crate::encode::ChromaFormat, chroma: crate::encode::ChromaFormat,
codec: crate::encode::Codec, codec: crate::encode::Codec,
cursor_blend: bool, cursor_blend: bool,
cursor_forward: bool,
) -> Self { ) -> Self {
SessionPlan { SessionPlan {
capture: CaptureBackend::resolve(), capture: CaptureBackend::resolve(),
@@ -129,6 +134,7 @@ impl SessionPlan {
codec, codec,
wire_chunk: None, wire_chunk: None,
cursor_blend, cursor_blend,
cursor_forward,
} }
} }
@@ -171,6 +177,7 @@ impl SessionPlan {
crate::capture::OutputFormat { crate::capture::OutputFormat {
gpu, gpu,
hdr: self.hdr, 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 // 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. // default NV12/P010 video-engine output) so NVENC can CSC to 4:4:4.
chroma_444: self.chroma.is_444(), chroma_444: self.chroma.is_444(),
@@ -33,6 +33,7 @@ thiserror = "2.0"
version = "0.58.0" version = "0.58.0"
features = [ features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_Security",
"Win32_System_Memory", "Win32_System_Memory",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D",
@@ -97,6 +97,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) }, control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) }, control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) },
// SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_CURSOR_CHANNEL => unsafe { set_cursor_channel(request) },
_ => complete(request, STATUS_NOT_FOUND), _ => complete(request, STATUS_NOT_FOUND),
} }
} }
@@ -148,6 +150,7 @@ unsafe fn add(request: WDFREQUEST) {
max_frame_avg_nits: req.max_frame_avg_nits, max_frame_avg_nits: req.max_frame_avg_nits,
min_millinits: req.min_luminance_millinits, min_millinits: req.min_luminance_millinits,
}, },
req.hw_cursor != 0,
) else { ) else {
complete(request, STATUS_NOT_FOUND); complete(request, STATUS_NOT_FOUND);
return; return;
@@ -174,6 +177,35 @@ unsafe fn add(request: WDFREQUEST) {
/// ///
/// # Safety /// # Safety
/// `request` is the framework `WDFREQUEST`. /// `request` is the framework `WDFREQUEST`.
/// `IOCTL_SET_CURSOR_CHANNEL` (v5): adopt a monitor's hardware-cursor section, declare the
/// hardware cursor to the OS, start the query→publish worker.
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn set_cursor_channel(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::SetCursorChannelRequest>(request) }) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
let Some(ch) = crate::cursor_worker::CursorChannel::from_request(&req) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
match crate::monitor::set_cursor_channel(req.target_id, ch) {
Ok(()) => complete(request, STATUS_SUCCESS),
Err(ch) => {
dbglog!(
"[pf-vd] SET_CURSOR_CHANNEL: no hw-cursor monitor with target_id {} — rejecting",
req.target_id
);
// NOT adopted: the host's error path reaps the duplicated handle remotely.
ch.into_unowned();
complete(request, STATUS_NOT_FOUND);
}
}
}
unsafe fn set_frame_channel(request: WDFREQUEST) { unsafe fn set_frame_channel(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else { let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
@@ -0,0 +1,277 @@
//! IddCx hardware-cursor worker (proto v5, remote-desktop-sweep M2c).
//!
//! When the host ADDs a monitor with `hw_cursor` set and delivers a [`CursorShm`] section
//! (`IOCTL_SET_CURSOR_CHANNEL`), we declare a hardware cursor to the OS
//! (`IddCxMonitorSetupHardwareCursor`) — DWM then EXCLUDES the pointer from the desktop image
//! it renders into our swap-chain and instead signals our event on every cursor change. This
//! worker thread drains those signals (`IddCxMonitorQueryHardwareCursor`) and seqlock-publishes
//! shape + position + visibility into the host-created section; the host polls it at its
//! encode-tick pace (no event crosses the process boundary).
//!
//! Coordinates are published VERBATIM in the OS's desktop space (`IDARG_OUT_QUERY_HWCURSOR::X/Y`
//! = the shape's top-left, can be negative); the host subtracts its monitor's desktop origin.
//! Shape pixels are the OS's 32-bpp rows at `Pitch` — BGRA for ALPHA cursors, color+mask for
//! MASKED_COLOR — copied raw; the host converts (keeping this thread dumb and allocation-free
//! after startup).
use core::sync::atomic::{AtomicU32, Ordering, fence};
use pf_driver_proto::cursor::{
CURSOR_MAGIC, CURSOR_SHAPE_BYTES, CURSOR_SHAPE_MAX, CURSOR_SHAPE_OFFSET, CURSOR_SHM_SIZE,
CursorShm,
};
use wdk_iddcx::nt_success;
use wdk_sys::iddcx;
use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0};
use windows::Win32::System::Memory::{
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
};
use windows::Win32::System::Threading::{CreateEventW, INFINITE, SetEvent, WaitForMultipleObjects};
/// The host's `IOCTL_SET_CURSOR_CHANNEL` delivery: the [`CursorShm`] mapping handle VALUE,
/// already duplicated into this WUDFHost process. Owning a `CursorChannel` means owning the
/// handle; `Drop` closes it unless [`into_unowned`](Self::into_unowned) disarmed that (the
/// not-adopted reject path, where the host reaps remotely) or the worker consumed it.
pub struct CursorChannel {
handle: u64,
owned: bool,
}
impl CursorChannel {
pub fn from_request(req: &pf_driver_proto::control::SetCursorChannelRequest) -> Option<Self> {
if req.header_handle == 0 {
return None;
}
Some(CursorChannel {
handle: req.header_handle,
owned: true,
})
}
/// Disarm the Drop (delivery rejected — the handle stays for the host to reap remotely).
pub fn into_unowned(mut self) {
self.owned = false;
}
}
impl Drop for CursorChannel {
fn drop(&mut self) {
if self.owned && self.handle != 0 {
// SAFETY: we own this duplicated handle value; closing at most once (owned is our flag).
unsafe {
let _ = CloseHandle(HANDLE(self.handle as *mut core::ffi::c_void));
}
}
}
}
/// The live worker: stops + joins on drop (monitor departure / replacement).
pub struct CursorWorker {
stop: isize,
join: Option<std::thread::JoinHandle<()>>,
}
// SAFETY: `stop` is an event handle value; the worker owns every other resource.
unsafe impl Send for CursorWorker {}
impl Drop for CursorWorker {
fn drop(&mut self) {
// SAFETY: `stop` is our owned manual-reset event; signal + join, then close.
unsafe {
let _ = SetEvent(HANDLE(self.stop as *mut core::ffi::c_void));
}
if let Some(j) = self.join.take() {
let _ = j.join();
}
// SAFETY: the worker has exited; nothing else references the handle.
unsafe {
let _ = CloseHandle(HANDLE(self.stop as *mut core::ffi::c_void));
}
}
}
/// Declare the hardware cursor for `monitor` and start the query→publish worker over the
/// delivered section. `None` on any failure (mapping/magic/event/DDI) — the caller logs and the
/// session simply keeps the composited-cursor behavior (the host times out waiting for a first
/// seqlock publish and falls back the same way).
pub fn setup_and_spawn(monitor: iddcx::IDDCX_MONITOR, ch: CursorChannel) -> Option<CursorWorker> {
// Map the host-created section. FILE_MAP_READ|WRITE: we write, the host reads.
let mapping = HANDLE(ch.handle as *mut core::ffi::c_void);
// SAFETY: `mapping` is the duplicated section handle we own; size is the fixed contract size.
let view = unsafe {
MapViewOfFile(
mapping,
FILE_MAP_READ | FILE_MAP_WRITE,
0,
0,
CURSOR_SHM_SIZE,
)
};
if view.Value.is_null() {
dbglog!("[pf-vd] cursor: MapViewOfFile failed — keeping composited cursor");
return None;
}
let shm = view.Value.cast::<CursorShm>();
// SAFETY: the view spans CURSOR_SHM_SIZE >= size_of::<CursorShm>(); reading the host stamp.
if unsafe { core::ptr::addr_of!((*shm).magic).read_volatile() } != CURSOR_MAGIC {
dbglog!("[pf-vd] cursor: section magic mismatch — rejecting");
// SAFETY: unmapping the view we just mapped.
unsafe {
let _ = UnmapViewOfFile(view);
}
return None;
}
// Auto-reset data event (the OS signals it per cursor update) + manual-reset stop event.
// SAFETY: plain event creation, no names, no security descriptor.
let (data_evt, stop_evt) = unsafe {
match (
CreateEventW(None, false, false, None),
CreateEventW(None, true, false, None),
) {
(Ok(d), Ok(s)) => (d, s),
_ => {
let _ = UnmapViewOfFile(view);
dbglog!("[pf-vd] cursor: event creation failed");
return None;
}
}
};
let caps = iddcx::IDDCX_CURSOR_CAPS {
Size: core::mem::size_of::<iddcx::IDDCX_CURSOR_CAPS>() as u32,
// Alpha covers every modern cursor; XOR/monochrome shapes arrive converted to masked
// color, which the host approximates. No XOR plane emulation on our side.
ColorXorCursorSupport: iddcx::IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE,
MaxX: CURSOR_SHAPE_MAX,
MaxY: CURSOR_SHAPE_MAX,
AlphaCursorSupport: 1,
};
let setup = iddcx::IDARG_IN_SETUP_HWCURSOR {
CursorInfo: caps,
hNewCursorDataAvailable: data_evt.0.cast(),
};
// SAFETY: `monitor` is a live IddCx monitor (post-create); `setup` outlives the call; the
// OS duplicates the event handle for its own signaling.
let st = unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) };
if !nt_success(st) {
dbglog!(
"[pf-vd] cursor: IddCxMonitorSetupHardwareCursor failed 0x{:08x}",
st as u32
);
// SAFETY: cleaning up the resources created above.
unsafe {
let _ = CloseHandle(data_evt);
let _ = CloseHandle(stop_evt);
let _ = UnmapViewOfFile(view);
}
return None;
}
dbglog!("[pf-vd] cursor: hardware cursor declared — worker starting");
// Ownership crossing into the thread as plain values (HANDLE/pointer aren't Send).
let monitor_v = monitor as usize;
let view_v = view.Value as usize;
let data_v = data_evt.0 as isize;
let stop_v = stop_evt.0 as isize;
let mapping_v = ch.handle;
ch.into_unowned(); // the worker owns the mapping handle from here (closed on exit below)
let join = std::thread::Builder::new()
.name("pf-vd-cursor".into())
.spawn(move || {
run_worker(monitor_v, view_v, data_v, stop_v);
// SAFETY: the worker is the sole owner of these at exit; close/unmap exactly once.
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: view_v as *mut core::ffi::c_void,
});
let _ = CloseHandle(HANDLE(data_v as *mut core::ffi::c_void));
let _ = CloseHandle(HANDLE(mapping_v as *mut core::ffi::c_void));
}
})
.ok()?;
Some(CursorWorker {
stop: stop_v,
join: Some(join),
})
}
/// The wait→query→publish loop. Exits when the stop event signals.
fn run_worker(monitor_v: usize, view_v: usize, data_v: isize, stop_v: isize) {
let monitor = monitor_v as iddcx::IDDCX_MONITOR;
let shm = view_v as *mut CursorShm;
let shape_dst = (view_v + CURSOR_SHAPE_OFFSET) as *mut u8;
let mut shape_buf = vec![0u8; CURSOR_SHAPE_BYTES];
let mut last_shape_id: u32 = 0;
let mut query_warned = false;
let handles = [
HANDLE(stop_v as *mut core::ffi::c_void),
HANDLE(data_v as *mut core::ffi::c_void),
];
loop {
// SAFETY: both handles are live for the worker's lifetime (owner drops after join).
let w = unsafe { WaitForMultipleObjects(&handles, false, INFINITE) };
if w == WAIT_OBJECT_0 {
return; // stop
}
if w.0 != WAIT_OBJECT_0.0 + 1 {
return; // wait failed — owner is tearing down
}
let in_args = iddcx::IDARG_IN_QUERY_HWCURSOR {
LastShapeId: last_shape_id,
ShapeBufferSizeInBytes: CURSOR_SHAPE_BYTES as u32,
pShapeBuffer: shape_buf.as_mut_ptr(),
};
// SAFETY: zero-init is a valid OUT arg (the OS writes every field it reports).
let mut out: iddcx::IDARG_OUT_QUERY_HWCURSOR = unsafe { core::mem::zeroed() };
// SAFETY: `monitor` is live (departure drops this worker FIRST), args outlive the call.
let st = unsafe { wdk_iddcx::IddCxMonitorQueryHardwareCursor(monitor, &in_args, &mut out) };
if !nt_success(st) {
if !query_warned {
query_warned = true;
dbglog!(
"[pf-vd] cursor: query failed 0x{:08x} (logged once)",
st as u32
);
}
continue;
}
// Seqlock publish: odd → write → even. The header alone changes on position moves;
// shape bytes are only rewritten when the OS says the image changed, so a reader that
// skips unchanged shape_ids never observes torn pixels.
// SAFETY: `shm` points at the mapped CursorShm for the worker's lifetime.
let seq = unsafe { &*core::ptr::addr_of!((*shm).seq).cast::<AtomicU32>() };
let s = seq.load(Ordering::Relaxed);
seq.store(s.wrapping_add(1), Ordering::Relaxed); // odd = mid-update
fence(Ordering::Release);
// SAFETY: exclusive writer (single worker per section); plain volatile field writes.
unsafe {
core::ptr::addr_of_mut!((*shm).visible).write_volatile(if out.IsCursorVisible != 0 {
1
} else {
0
});
core::ptr::addr_of_mut!((*shm).x).write_volatile(out.X);
core::ptr::addr_of_mut!((*shm).y).write_volatile(out.Y);
if out.IsCursorShapeUpdated != 0 && out.IsCursorVisible != 0 {
let info = &out.CursorShapeInfo;
let rows = info.Height.min(CURSOR_SHAPE_MAX);
let bytes = (rows as usize * info.Pitch as usize).min(CURSOR_SHAPE_BYTES);
core::ptr::copy_nonoverlapping(shape_buf.as_ptr(), shape_dst, bytes);
core::ptr::addr_of_mut!((*shm).cursor_type).write_volatile(info.CursorType as u32);
core::ptr::addr_of_mut!((*shm).width)
.write_volatile(info.Width.min(CURSOR_SHAPE_MAX));
core::ptr::addr_of_mut!((*shm).height).write_volatile(rows);
core::ptr::addr_of_mut!((*shm).pitch).write_volatile(info.Pitch);
core::ptr::addr_of_mut!((*shm).hot_x).write_volatile(info.XHot);
core::ptr::addr_of_mut!((*shm).hot_y).write_volatile(info.YHot);
core::ptr::addr_of_mut!((*shm).shape_id).write_volatile(info.ShapeId);
last_shape_id = info.ShapeId;
}
}
fence(Ordering::Release);
seq.store(s.wrapping_add(2), Ordering::Release); // even = consistent
}
}
@@ -22,6 +22,7 @@ mod log;
mod adapter; mod adapter;
mod callbacks; mod callbacks;
mod control; mod control;
mod cursor_worker;
mod direct_3d_device; mod direct_3d_device;
mod edid; mod edid;
mod entry; mod entry;
@@ -75,6 +75,11 @@ pub struct MonitorObject {
/// on the SAME adapter (same pooled device — a cross-device texture would be unusable). Dropped /// on the SAME adapter (same pooled device — a cross-device texture would be unusable). Dropped
/// with the `MonitorObject` on teardown (it holds only a driver-private texture, no handles). /// with the `MonitorObject` on teardown (it holds only a driver-private texture, no handles).
pub preserved_stash: Option<(crate::frame_transport::FrameStash, u32, i32)>, pub preserved_stash: Option<(crate::frame_transport::FrameStash, u32, i32)>,
/// The host asked for an IddCx hardware cursor (`AddRequest::hw_cursor`, proto v5): declared
/// once the cursor channel arrives (`IOCTL_SET_CURSOR_CHANNEL`) — see [`set_cursor_channel`].
pub hw_cursor: bool,
/// The live cursor query→publish worker (drops = stop+join) — set by [`set_cursor_channel`].
pub cursor_worker: Option<crate::cursor_worker::CursorWorker>,
/// When the entry was created — the watchdog skips still-initializing monitors. /// When the entry was created — the watchdog skips still-initializing monitors.
pub created_at: Instant, pub created_at: Instant,
} }
@@ -370,6 +375,45 @@ pub fn has_frame_channel(target_id: u32) -> bool {
.any(|m| m.target_id == target_id && m.frame_channel.is_some()) .any(|m| m.target_id == target_id && m.frame_channel.is_some())
} }
/// Adopt a hardware-cursor channel delivery (`IOCTL_SET_CURSOR_CHANNEL`, proto v5): declare the
/// hardware cursor to the OS and start the worker. Rejected (`Err(ch)`) when the monitor is
/// unknown, didn't ask for a cursor at ADD, or isn't created yet. A RE-delivery replaces the
/// worker (the old one stops+joins on drop) — the host only re-sends after recreating the
/// section. The IddCx setup runs OUTSIDE the monitors lock (it can call back into mode DDIs).
pub fn set_cursor_channel(
target_id: u32,
ch: crate::cursor_worker::CursorChannel,
) -> Result<(), crate::cursor_worker::CursorChannel> {
if target_id == 0 {
return Err(ch);
}
let (monitor_obj, old_worker) = {
let mut lock = lock_monitors();
let Some(m) = lock
.iter_mut()
.find(|m| m.target_id == target_id && m.hw_cursor)
else {
return Err(ch);
};
let Some(obj) = m.object else {
return Err(ch);
};
(obj, m.cursor_worker.take())
};
drop(old_worker); // stop+join a replaced worker before the new setup
let Some(worker) = crate::cursor_worker::setup_and_spawn(monitor_obj, ch) else {
// setup_and_spawn consumed + cleaned the channel; report success=false via NOT_FOUND at
// the dispatch layer is wrong here — the handles are gone, so this is a plain failure.
return Ok(()); // adopted (handles consumed); the host detects no-publish and moves on
};
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.cursor_worker = Some(worker);
}
// A monitor departed in the window: dropping `worker` here stops it cleanly.
Ok(())
}
/// Stash a swap-chain worker's still-live [`FramePublisher`](crate::frame_transport::FramePublisher) on /// Stash a swap-chain worker's still-live [`FramePublisher`](crate::frame_transport::FramePublisher) on
/// its monitor across a swap-chain unassign→reassign flap (STEP 6 sibling-join fix; see the field docs /// its monitor across a swap-chain unassign→reassign flap (STEP 6 sibling-join fix; see the field docs
/// on [`MonitorObject::preserved_publisher`]). Called from the EXITING worker thread — the caller must /// on [`MonitorObject::preserved_publisher`]). Called from the EXITING worker thread — the caller must
@@ -497,6 +541,7 @@ pub fn create_monitor(
refresh: u32, refresh: u32,
preferred_id: u32, preferred_id: u32,
client_lum: crate::edid::ClientLuminance, client_lum: crate::edid::ClientLuminance,
hw_cursor: bool,
) -> Option<(u32, u32, u32, i32)> { ) -> Option<(u32, u32, u32, i32)> {
let adapter = crate::adapter::adapter()?; let adapter = crate::adapter::adapter()?;
// Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart // Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart
@@ -550,6 +595,8 @@ pub fn create_monitor(
frame_channel: None, frame_channel: None,
preserved_publisher: None, preserved_publisher: None,
preserved_stash: None, preserved_stash: None,
hw_cursor,
cursor_worker: None,
created_at: Instant::now(), created_at: Instant::now(),
}); });
id id
@@ -133,6 +133,24 @@ iddcx_ddi!(
IddCxMonitorDeparture(monitor: iddcx::IDDCX_MONITOR) IddCxMonitorDeparture(monitor: iddcx::IDDCX_MONITOR)
@ IddCxMonitorDepartureTableIndex as PFN_IDDCXMONITORDEPARTURE @ IddCxMonitorDepartureTableIndex as PFN_IDDCXMONITORDEPARTURE
); );
iddcx_ddi!(
/// Declare hardware-cursor support for a monitor (proto v5 cursor channel): the OS stops
/// compositing the pointer into the desktop image and signals `hNewCursorDataAvailable`
/// on every cursor change instead (drained via [`IddCxMonitorQueryHardwareCursor`]).
IddCxMonitorSetupHardwareCursor(
monitor: iddcx::IDDCX_MONITOR,
in_args: *const iddcx::IDARG_IN_SETUP_HWCURSOR,
) @ IddCxMonitorSetupHardwareCursorTableIndex as PFN_IDDCXMONITORSETUPHARDWARECURSOR
);
iddcx_ddi!(
/// Drain one hardware-cursor update: position/visibility always; shape bytes are copied
/// into `in_args.pShapeBuffer` only when the OS's shape id moved past `LastShapeId`.
IddCxMonitorQueryHardwareCursor(
monitor: iddcx::IDDCX_MONITOR,
in_args: *const iddcx::IDARG_IN_QUERY_HWCURSOR,
out_args: *mut iddcx::IDARG_OUT_QUERY_HWCURSOR,
) @ IddCxMonitorQueryHardwareCursorTableIndex as PFN_IDDCXMONITORQUERYHARDWARECURSOR
);
iddcx_ddi!( iddcx_ddi!(
/// Set the preferred render adapter (LUID) for the virtual adapter. /// Set the preferred render adapter (LUID) for the virtual adapter.
IddCxAdapterSetRenderAdapter( IddCxAdapterSetRenderAdapter(
@@ -84,5 +84,5 @@ const _: fn() = || {
/// The FP16/HDR adapter flag + high-color-space target cap the driver MUST set (these gate the whole /// The FP16/HDR adapter flag + high-color-space target cap the driver MUST set (these gate the whole
/// `*2` callback requirement). Confirms the ModuleConsts paths the port uses — note these enums have NO /// `*2` callback requirement). Confirms the ModuleConsts paths the port uses — note these enums have NO
/// `_`-prefixed module (unlike `_IDDFUNCENUM`/`_IDDSTRUCTENUM`). /// `_`-prefixed module (unlike `_IDDFUNCENUM`/`_IDDSTRUCTENUM`).
const _: u32 = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16 as u32; const _: u32 = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16;
const _: u32 = iddcx::IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE as u32; const _: u32 = iddcx::IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE;