fix(vdisplay/capture): channel-less sessions composite the pointer on a sticky-declared target

A declared IddCx hardware cursor is IRREVOCABLE for its OS target's life
(§8.6), and the sticky exclusion survives monitor REMOVE→ADD because each
client gets a STABLE target id — so once any desktop-mode session declared,
every later pure-capture session on that target streamed a cursor-less
desktop: DWM excluded the pointer, no channel forwarded it, no blend drew it
(the exact no-regression gap §8.6's per-session cap gate cannot see).

Driver: track every successful SetupHardwareCursor per target
(DECLARED_TARGETS — scoped to the WUDFHost's life, exactly the sticky
state's scope) and report it in a new AddReply::cursor_excluded tail field
(dual-size discipline, both skews degrade cleanly; no proto bump).

Host: the flag rides AddedMonitor → Monitor → WinCaptureTarget; a session
WITHOUT the cursor channel on a flagged target forces composite mode in the
IDD-push capturer — GDI poller + blend for the session's life, pinned on
(set_cursor_forward cannot clear it: with no client drawing, un-compositing
would erase the pointer entirely).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:47:53 +02:00
co-authored by Claude Fable 5
parent d5ae8dcc3e
commit 7058647264
9 changed files with 157 additions and 20 deletions
@@ -165,9 +165,14 @@ unsafe fn add(request: WDFREQUEST) {
// This WUDFHost's pid — where the host duplicates the sealed frame channel's handles INTO
// (`ProcessSharingDisabled`: this process is exclusively ours and dies with the device).
wudf_pid: std::process::id(),
// The target already carries an irrevocable hardware-cursor declare from an earlier
// session — a channel-less session must self-composite the pointer (§8.6 gap).
cursor_excluded: crate::monitor::target_declared(target_id) as u32,
};
// Dual-size reply (the `cursor_excluded` tail ext): an un-upgraded host retrieves only the
// legacy 20-byte buffer — write the prefix it asked for instead of failing its ADD.
// SAFETY: `request` is the framework WDFREQUEST.
unsafe { write_output_complete(request, &reply) };
unsafe { write_output_prefix_complete(request, &reply, control::ADD_REPLY_LEGACY_SIZE) };
}
/// `IOCTL_SET_FRAME_CHANNEL`: adopt the handle values the host duplicated into this process and stash
@@ -356,6 +361,18 @@ unsafe fn read_input<T: Copy>(request: WDFREQUEST) -> Option<T> {
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn write_output_complete<T: Copy>(request: WDFREQUEST, value: &T) {
// SAFETY: forwarded per this function's own contract; min = the full struct.
unsafe { write_output_prefix_complete(request, value, core::mem::size_of::<T>()) }
}
/// [`write_output_complete`] for a reply with an APPENDED tail field (the `AddReply
/// cursor_excluded` dual-size discipline): accepts any output buffer of at least `min_size`
/// bytes and writes `min(buffer, size_of::<T>())` bytes of the struct — an un-upgraded host
/// retrieving only the legacy prefix gets exactly that prefix instead of a failed IOCTL.
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn write_output_prefix_complete<T: Copy>(request: WDFREQUEST, value: &T, min_size: usize) {
let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
let mut len: usize = 0;
// SAFETY: `request` valid; `buf`/`len` are out-params written by the framework.
@@ -363,7 +380,7 @@ unsafe fn write_output_complete<T: Copy>(request: WDFREQUEST, value: &T) {
call_unsafe_wdf_function_binding!(
WdfRequestRetrieveOutputBuffer,
request,
core::mem::size_of::<T>(),
min_size,
&mut buf,
&mut len
)
@@ -372,9 +389,13 @@ unsafe fn write_output_complete<T: Copy>(request: WDFREQUEST, value: &T) {
complete(request, st);
return;
}
// SAFETY: `buf` has >= size_of::<T>() writable bytes; T is a Pod control struct.
unsafe { buf.cast::<T>().write_unaligned(*value) };
complete_info(request, STATUS_SUCCESS, core::mem::size_of::<T>());
let take = len.min(core::mem::size_of::<T>());
// SAFETY: the framework guarantees `buf` has >= `len` writable bytes and `take <= len`;
// T is a Pod control struct, so any byte prefix of it is valid to copy out.
unsafe {
core::ptr::copy_nonoverlapping((value as *const T).cast::<u8>(), buf.cast::<u8>(), take);
}
complete_info(request, STATUS_SUCCESS, take);
}
/// Complete a request with just a status (no output).