Files
punktfunk/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs
T
enricobuehlerandClaude Opus 4.8 b50e4767de fix(windows/cursor-flip): flag + forced same-mode re-commit — the working un-declare
The empty-caps un-declare is REJECTED by IddCx (STATUS_INVALID_PARAMETER,
on-glass driver 9.9.0722.1407) — there is no un-declare DDI. The
composite flip now works with the only lever the OS gives us: every
mode COMMIT reverts the path to the software cursor, and the driver
skips its per-commit re-declare while cursor_forward_on is off. So:

- driver: disable stores the flag only (no DDI call); enable still
  declares immediately against the live worker's event.
- host capturer: after a successful disable flip, force a same-mode
  re-commit (win_display::force_mode_reenumeration) — DWM composites
  the pointer from the very next commit; the swap-chain flap is the
  class the driver's preserved-publisher machinery already rides out.
- state now survives re-arrivals end-to-end: the capturer caches the
  APPLIED state (cleared on every channel (re)delivery, which
  re-created driver entries need — their flag defaults to declared),
  and the stream loop re-applies every tick instead of edge-gating
  (steady-state cost: one Option compare).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00

363 lines
17 KiB
Rust

//! 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, WAIT_TIMEOUT};
use windows::Win32::System::Memory::{
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
};
use windows::Win32::System::Threading::{CreateEventW, 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,
/// The OS cursor-data event handle VALUE — kept so a later swap-chain (re)assignment can
/// RE-ISSUE [`IddCxMonitorSetupHardwareCursor`] on the freshly committed path (the setup is
/// per-mode-commit: the OS silently reverts to software cursor on every mode commit). The
/// worker thread still owns closing it on exit; this is a read-only copy for re-setup.
data_event: isize,
join: Option<std::thread::JoinHandle<()>>,
}
impl CursorWorker {
/// The OS cursor-data event, for [`setup_hardware_cursor`] re-issue.
pub fn data_event(&self) -> isize {
self.data_event
}
}
/// Declare (or RE-declare) the hardware cursor for `monitor` against `data_event`. Called at
/// initial channel delivery AND on every swap-chain assignment (a mode commit reverts the path
/// to software cursor). Must run OUTSIDE the monitors lock — the DDI may call back into the
/// mode callbacks. Returns the DDI status.
pub fn setup_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,
// On-glass finding (2026-07-22, .173): the hardware-cursor query delivers ONLY
// `IDDCX_CURSOR_SHAPE_TYPE_ALPHA` shapes, in EVERY configuration — all three
// ColorXorCursorSupport levels, event-driven AND ~30 Hz polled. Masked/monochrome
// cursors (I-beam, resize, SIZEALL, app hand cursors) never arrive: NONE rejects the
// query outright (STATUS_NOT_SUPPORTED); EMULATION software-composites them into the
// frame (double cursor vs the client's stale local shape); FULL excludes them from the
// frame but never delivers them (shape freezes on the last alpha cursor). FULL is the
// right resting state: the frame stays cursor-free for ALL cursor types, and the
// full-fidelity SHAPE comes from the session-side cursor source in the host, not from
// this query (design/remote-desktop-sweep.md §8).
ColorXorCursorSupport: iddcx::IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_FULL,
MaxX: CURSOR_SHAPE_MAX,
MaxY: CURSOR_SHAPE_MAX,
AlphaCursorSupport: 1,
};
let setup = iddcx::IDARG_IN_SETUP_HWCURSOR {
CursorInfo: caps,
hNewCursorDataAvailable: data_event as *mut core::ffi::c_void,
};
// SAFETY: `monitor` is a live IddCx monitor; `setup` outlives the call; `data_event` is a
// live event handle (the worker owns it for its lifetime, and re-setup only runs while the
// worker is present).
unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) }
}
// NOTE: there is NO un-declare path. Re-issuing `IddCxMonitorSetupHardwareCursor` with empty
// caps (no alpha, XOR NONE, zero max dims) is rejected `STATUS_INVALID_PARAMETER` — observed
// on-glass (26100, driver 9.9.0722.1407). The composite flip therefore works by FLAG + MODE
// RE-COMMIT: `monitor::set_cursor_forward(false)` stores the flag, the host forces a same-mode
// re-commit, and the OS's per-commit software-cursor default sticks because
// `monitor::resetup_cursor` skips the flagged monitor.
// 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,
declare: bool,
) -> 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;
}
}
};
// One caps definition for initial setup AND the per-mode-commit re-setup — see
// `setup_hardware_cursor` for the FULL rationale. `declare = false` (a delivery landing
// while the session is in the COMPOSITE render mode) skips the declaration: the worker
// spawns anyway so a later enable-flip has its event to declare against; its queries just
// fail NOT_SUPPORTED until then (logged once, harmless).
if declare {
let st = setup_hardware_cursor(monitor, data_evt.0 as isize);
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");
} else {
dbglog!("[pf-vd] cursor: channel adopted UNdeclared (composite mode) — 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,
data_event: data_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 mut published = false;
let handles = [
HANDLE(stop_v as *mut core::ffi::c_void),
HANDLE(data_v as *mut core::ffi::c_void),
];
loop {
// POLL, not pure event-wait: the OS fires `hNewCursorDataAvailable` only for cursors it
// routes through the hardware plane — masked/monochrome cursors (I-beam, hand, move,
// resize) don't fire it. Polling does NOT recover them either (they are simply not in
// this query's world — see the caps comment above); the ~30 Hz timeout just keeps the
// seqlock's position/visibility fresh across missed events, and the query with the
// current `LastShapeId` is a cheap no-op when nothing changed.
const POLL_MS: u32 = 33;
// SAFETY: both handles are live for the worker's lifetime (owner drops after join).
let w = unsafe { WaitForMultipleObjects(&handles, false, POLL_MS) };
if w == WAIT_OBJECT_0 {
return; // stop
}
// Query on the data event OR the poll timeout; anything else is a wait failure.
if w != WAIT_TIMEOUT && w.0 != WAIT_OBJECT_0.0 + 1 {
dbglog!("[pf-vd] cursor: wait returned {:#x} — worker exiting", w.0);
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). v3: the
// base query DDI slot is stubbed to NOT_SUPPORTED on current IddCx.
let mut out: iddcx::IDARG_OUT_QUERY_HWCURSOR3 = unsafe { core::mem::zeroed() };
// SAFETY: `monitor` is live (departure drops this worker FIRST), args outlive the call.
let st =
unsafe { wdk_iddcx::IddCxMonitorQueryHardwareCursor3(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;
}
query_warned = false;
if !published {
published = true;
dbglog!("[pf-vd] cursor: publishes live");
}
// Log each distinct SHAPE (human-paced): type (1=masked_color, 2=alpha), dims,
// visibility. Shows which cursors reach us (does VSCode's hand arrive?) and their
// type (masked ⇒ the OS may still software-composite it → the double-cursor report).
if out.IsCursorShapeUpdated != 0 {
dbglog!(
"[pf-vd] cursor SHAPE id={} type={} {}x{} vis={} posvalid={}",
out.CursorShapeInfo.ShapeId,
out.CursorShapeInfo.CursorType as u32,
out.CursorShapeInfo.Width,
out.CursorShapeInfo.Height,
out.IsCursorVisible,
out.PositionValid,
);
}
// 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
});
// v3 `X`/`Y` are only meaningful when `PositionValid`; otherwise keep the prior
// position (a position-invalid tick still carries a shape/visibility update).
if out.PositionValid != 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
}
}