diff --git a/packaging/windows/drivers/pf-vdisplay/src/control.rs b/packaging/windows/drivers/pf-vdisplay/src/control.rs index cd550e37..3e59b1b8 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/control.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/control.rs @@ -99,6 +99,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) { control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) }, // SAFETY: `request` is the framework WDFREQUEST. control::IOCTL_SET_CURSOR_CHANNEL => unsafe { set_cursor_channel(request) }, + // SAFETY: `request` is the framework WDFREQUEST. + control::IOCTL_SET_CURSOR_FORWARD => unsafe { set_cursor_forward(request) }, _ => complete(request, STATUS_NOT_FOUND), } } @@ -206,6 +208,28 @@ unsafe fn set_cursor_channel(request: WDFREQUEST) { } } +/// `IOCTL_SET_CURSOR_FORWARD` (v6): the mid-stream cursor-render flip — (un)declare a LIVE +/// monitor's hardware cursor as the client's mouse model demands. +/// +/// # Safety +/// `request` is the framework `WDFREQUEST`. +unsafe fn set_cursor_forward(request: WDFREQUEST) { + // SAFETY: `request` is the framework WDFREQUEST. + let Some(req) = (unsafe { read_input::(request) }) else { + complete(request, STATUS_INVALID_PARAMETER); + return; + }; + if crate::monitor::set_cursor_forward(req.target_id, req.enable != 0) { + complete(request, STATUS_SUCCESS); + } else { + dbglog!( + "[pf-vd] SET_CURSOR_FORWARD: no cursor-channel monitor with target_id {} — rejecting", + req.target_id + ); + complete(request, STATUS_NOT_FOUND); + } +} + unsafe fn set_frame_channel(request: WDFREQUEST) { // SAFETY: `request` is the framework WDFREQUEST. let Some(req) = (unsafe { read_input::(request) }) else { diff --git a/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs b/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs index 87f69ba1..27ba5c04 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs @@ -115,6 +115,32 @@ pub fn setup_hardware_cursor(monitor: iddcx::IDDCX_MONITOR, data_event: isize) - unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) } } +/// UN-declare the hardware cursor (the mid-stream cursor-render flip, proto v6): re-issue the +/// setup with EMPTY caps — no alpha, no XOR, zero max dims — asking the OS to stop routing the +/// pointer to this driver and fall back to the software cursor (composited into the desktop +/// image, which is exactly what the capture mouse model wants in-frame). The IddCx DDI has no +/// documented un-setup; empty caps is the candidate mechanism — the returned status is logged +/// by the caller, and if the OS refuses it the fallback is [`crate::monitor::resetup_cursor`]'s +/// skip on the next mode commit (the OS reverts to software cursor at every commit by default). +/// The data event stays the worker's live handle; a query while un-declared just reports +/// nothing new (the host ignores the shm in composite mode). +pub fn unsetup_hardware_cursor(monitor: iddcx::IDDCX_MONITOR, data_event: isize) -> i32 { + let caps = iddcx::IDDCX_CURSOR_CAPS { + Size: core::mem::size_of::() as u32, + ColorXorCursorSupport: iddcx::IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE, + MaxX: 0, + MaxY: 0, + AlphaCursorSupport: 0, + }; + let setup = iddcx::IDARG_IN_SETUP_HWCURSOR { + CursorInfo: caps, + hNewCursorDataAvailable: data_event as *mut core::ffi::c_void, + }; + // SAFETY: same contract as [`setup_hardware_cursor`] — live monitor, `setup` outlives the + // call, live event handle. + unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) } +} + // SAFETY: `stop` is an event handle value; the worker owns every other resource. unsafe impl Send for CursorWorker {} diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index 5752c584..4381a678 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -80,6 +80,11 @@ pub struct MonitorObject { pub hw_cursor: bool, /// The live cursor query→publish worker (drops = stop+join) — set by [`set_cursor_channel`]. pub cursor_worker: Option, + /// The mid-stream cursor-render flip (`IOCTL_SET_CURSOR_FORWARD`, proto v6): while `false` + /// the hardware cursor stays UN-declared (DWM composites the pointer — the capture mouse + /// model) and [`resetup_cursor`] skips its per-mode-commit re-declare. Starts `true` — the + /// channel delivery declares. + pub cursor_forward_on: bool, /// When the entry was created — the watchdog skips still-initializing monitors. pub created_at: Instant, } @@ -506,6 +511,9 @@ pub fn resetup_cursor(object: iddcx::IDDCX_MONITOR) { let lock = lock_monitors(); lock.iter() .find(|m| m.object == Some(object)) + // A composite-mode monitor (`cursor_forward_on == false`, the mid-stream flip) must + // NOT re-declare — the commit's software-cursor default is exactly what it wants. + .filter(|m| m.cursor_forward_on) .and_then(|m| m.cursor_worker.as_ref()) .map(|w| w.data_event()) }; @@ -515,6 +523,35 @@ pub fn resetup_cursor(object: iddcx::IDDCX_MONITOR) { } } +/// The mid-stream cursor-render flip (`IOCTL_SET_CURSOR_FORWARD`, proto v6): `enable` declares +/// the hardware cursor again (DWM excludes the pointer; per-mode-commit re-declares resume); +/// disable re-issues the setup with EMPTY caps — asking the OS to route the cursor back to the +/// software path (composited frames) — and stops the per-commit re-declare. `false` when no +/// monitor with `target_id` has a live cursor worker (channel never delivered / gone) — the +/// host logs and keeps its current behavior. Flag update UNDER the lock; DDI call OUTSIDE it +/// (it can re-enter the mode callbacks, which take this lock). +pub fn set_cursor_forward(target_id: u32, enable: bool) -> bool { + let found = { + let mut lock = lock_monitors(); + lock.iter_mut() + .find(|m| m.target_id == target_id && m.cursor_worker.is_some()) + .map(|m| { + m.cursor_forward_on = enable; + (m.object, m.cursor_worker.as_ref().map(|w| w.data_event())) + }) + }; + let Some((Some(object), Some(ev))) = found else { + return false; + }; + let st = if enable { + crate::cursor_worker::setup_hardware_cursor(object, ev) + } else { + crate::cursor_worker::unsetup_hardware_cursor(object, ev) + }; + dbglog!("[pf-vd] cursor: forward flip enable={enable} -> {st:#x}"); + true +} + /// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor /// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must /// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a @@ -617,6 +654,7 @@ pub fn create_monitor( preserved_stash: None, hw_cursor, cursor_worker: None, + cursor_forward_on: true, created_at: Instant::now(), }); id