feat(windows/vdisplay): the driver leg of the mid-stream cursor-render flip

Belongs to 493f4fae (which shipped only the crates/ side — this
packaging/ half was left unstaged, so the deployed driver silently
lacked the IOCTL and every flip died NOT_FOUND at dispatch):
IOCTL_SET_CURSOR_FORWARD dispatch + monitor::set_cursor_forward
(cursor_forward_on flag, resetup gate) + cursor_worker::
unsetup_hardware_cursor (empty-caps un-declare experiment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:43:23 +02:00
co-authored by Claude Opus 4.8
parent d86073dc6a
commit 030a779391
3 changed files with 88 additions and 0 deletions
@@ -99,6 +99,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) }, control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) },
// SAFETY: `request` is the framework WDFREQUEST. // SAFETY: `request` is the framework WDFREQUEST.
control::IOCTL_SET_CURSOR_CHANNEL => unsafe { set_cursor_channel(request) }, 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), _ => 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::<control::SetCursorForwardRequest>(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) { 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 {
@@ -115,6 +115,32 @@ pub fn setup_hardware_cursor(monitor: iddcx::IDDCX_MONITOR, data_event: isize) -
unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) } 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::<iddcx::IDDCX_CURSOR_CAPS>() 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. // SAFETY: `stop` is an event handle value; the worker owns every other resource.
unsafe impl Send for CursorWorker {} unsafe impl Send for CursorWorker {}
@@ -80,6 +80,11 @@ pub struct MonitorObject {
pub hw_cursor: bool, pub hw_cursor: bool,
/// The live cursor query→publish worker (drops = stop+join) — set by [`set_cursor_channel`]. /// The live cursor query→publish worker (drops = stop+join) — set by [`set_cursor_channel`].
pub cursor_worker: Option<crate::cursor_worker::CursorWorker>, pub cursor_worker: Option<crate::cursor_worker::CursorWorker>,
/// 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. /// When the entry was created — the watchdog skips still-initializing monitors.
pub created_at: Instant, pub created_at: Instant,
} }
@@ -506,6 +511,9 @@ pub fn resetup_cursor(object: iddcx::IDDCX_MONITOR) {
let lock = lock_monitors(); let lock = lock_monitors();
lock.iter() lock.iter()
.find(|m| m.object == Some(object)) .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()) .and_then(|m| m.cursor_worker.as_ref())
.map(|w| w.data_event()) .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 /// 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 /// 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 /// 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, preserved_stash: None,
hw_cursor, hw_cursor,
cursor_worker: None, cursor_worker: None,
cursor_forward_on: true,
created_at: Instant::now(), created_at: Instant::now(),
}); });
id id