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
@@ -199,6 +199,35 @@ fn cursor_forward_desired(target_id: u32) -> bool {
.unwrap_or(true)
}
/// OS target ids on which `IddCxMonitorSetupHardwareCursor` ever SUCCEEDED. A declare is
/// IRREVOCABLE for the target's life (no un-declare DDI; empty-caps re-setup is rejected; even a
/// successful same-mode re-commit never brings DWM's composited pointer back — all proven
/// on-glass, remote-desktop-sweep §8.6) and it survives monitor REMOVE→ADD because the host hands
/// each client a STABLE target id. Reported back on every ADD
/// ([`AddReply::cursor_excluded`](pf_driver_proto::control::AddReply)) so a session that never
/// negotiates the cursor channel knows it must self-composite the pointer instead of streaming a
/// cursor-less desktop. Never cleared: the state's true scope is this WUDFHost's life
/// (`ProcessSharingDisabled` — the process, and with it the adapter and every sticky declare,
/// dies on adapter reset), which is exactly when this static resets too. Bounded: ≤ 16 ids.
static DECLARED_TARGETS: Mutex<Vec<u32>> = Mutex::new(Vec::new());
/// Record a SUCCESSFUL hardware-cursor declare on `target_id` (see [`DECLARED_TARGETS`]).
fn mark_declared(target_id: u32) {
let mut declared = DECLARED_TARGETS.lock().unwrap_or_else(|e| e.into_inner());
if !declared.contains(&target_id) {
declared.push(target_id);
dbglog!("[pf-vd] cursor: target {target_id} marked hardware-cursor declared (irrevocable)");
}
}
/// True if `target_id` ever had a hardware cursor declared (see [`DECLARED_TARGETS`]).
pub fn target_declared(target_id: u32) -> bool {
DECLARED_TARGETS
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(&target_id)
}
/// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]).
fn remember_modes(id: u32, modes: &[Mode]) {
let mut hist = MODE_HISTORY.lock().unwrap_or_else(|e| e.into_inner());
@@ -432,6 +461,10 @@ pub fn set_cursor_channel(
// 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
};
if declare {
// The worker only spawns after `IddCxMonitorSetupHardwareCursor` succeeded.
mark_declared(target_id);
}
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.cursor_worker = Some(worker);
@@ -535,12 +568,18 @@ pub fn resetup_cursor(object: iddcx::IDDCX_MONITOR) {
// 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())
.and_then(|m| {
m.cursor_worker
.as_ref()
.map(|w| (w.data_event(), m.target_id))
})
};
if let Some(ev) = data_event {
if let Some((ev, target_id)) = data_event {
let st = crate::cursor_worker::setup_hardware_cursor(object, ev);
dbglog!("[pf-vd] cursor: re-setup on swap-chain assign -> {st:#x}");
if wdk_iddcx::nt_success(st) {
mark_declared(target_id);
}
}
}
@@ -591,6 +630,9 @@ pub fn set_cursor_forward(target_id: u32, enable: bool) -> bool {
// Enable declares immediately against the live worker's event (works any time).
let st = crate::cursor_worker::setup_hardware_cursor(object, ev);
dbglog!("[pf-vd] cursor: forward flip enable=1 (declare) -> {st:#x}");
if wdk_iddcx::nt_success(st) {
mark_declared(target_id);
}
}
(false, _, _) => {
// There is NO un-declare DDI: re-issuing the setup with empty caps is rejected