diff --git a/crates/pf-driver-proto/src/lib.rs b/crates/pf-driver-proto/src/lib.rs index e84152ed..8e471455 100644 --- a/crates/pf-driver-proto/src/lib.rs +++ b/crates/pf-driver-proto/src/lib.rs @@ -80,12 +80,13 @@ pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) { /// and the desktop model gets exclusion + forwarding. Nothing existing changed; against a v5 /// driver the unknown IOCTL fails and the host logs + keeps the declared-at-ADD behavior. /// v6 tail ext (no bump, the `AddRequest` luminance-tail discipline): -/// [`control::AddReply::cursor_excluded`] — the driver reports whether the ADDed monitor's OS -/// target already carries a hardware-cursor declare from an earlier session. A declare is -/// IRREVOCABLE for the target's life (remote-desktop-sweep §8.6, proven on-glass): DWM never -/// composites the software cursor back into that target's frames, and the sticky state survives -/// monitor REMOVE→ADD because the host hands every client a STABLE target id. The host uses the -/// flag to self-composite the pointer (GDI poller + blend) in sessions that never negotiate the +/// [`control::AddReply::cursor_excluded`] — the driver reports whether its ADAPTER already +/// carries a hardware-cursor declare from an earlier session. A declare is IRREVOCABLE +/// (remote-desktop-sweep §8.6, proven on-glass) and its exclusion reaches EVERY later monitor of +/// the adapter, not just the declaring target (on-glass 2026-07-23: a declare on one target left +/// a different client's fresh target cursor-less): DWM never composites the software cursor back +/// into any of the adapter's frames until the adapter resets. The host uses the flag to +/// self-composite the pointer (GDI poller + blend) in sessions that never negotiate the /// cursor channel — without it those sessions are silently cursor-less. Both skews degrade /// cleanly: an old driver writes only the 20-byte reply prefix (host reads `0` = unknown/clean), /// an old host retrieves a 20-byte buffer (driver writes just the prefix). @@ -217,9 +218,10 @@ pub mod control { /// `DuplicateHandle`, then [`IOCTL_SET_FRAME_CHANNEL`]). Reported per-ADD, not per-open, so a /// WUDFHost restart between sessions can never leave the host duplicating into a dead process. pub wudf_pid: u32, - /// Non-zero = this monitor's OS target already carries an IRREVOCABLE hardware-cursor - /// declare from an earlier session (remote-desktop-sweep §8.6): DWM excludes the pointer - /// from every frame on this target, forever, and a session without the cursor channel must + /// Non-zero = the ADAPTER already carries an IRREVOCABLE hardware-cursor declare from an + /// earlier session (remote-desktop-sweep §8.6; reach is adapter-wide, not per-target — + /// on-glass 2026-07-23): DWM excludes the pointer from every frame on every monitor until + /// the adapter resets, and a session without the cursor channel must /// self-composite (GDI poller + blend) or stream a cursor-less desktop. Appended after /// [`ADD_REPLY_LEGACY_SIZE`] under the same dual-size discipline as the `AddRequest` /// luminance tail: an un-upgraded driver writes only the legacy prefix (the host's diff --git a/crates/pf-frame/src/dxgi.rs b/crates/pf-frame/src/dxgi.rs index 54ae03e4..b7baa8f6 100644 --- a/crates/pf-frame/src/dxgi.rs +++ b/crates/pf-frame/src/dxgi.rs @@ -32,10 +32,12 @@ pub struct WinCaptureTarget { /// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown /// (a pre-v2 pairing can't occur — the version handshake is hard — so this only guards misuse). pub wudf_pid: u32, - /// The ADD reply flagged this target as carrying an IRREVOCABLE IddCx hardware-cursor declare - /// from an earlier session (remote-desktop-sweep §8.6): DWM excludes the pointer from its - /// frames forever, so a session WITHOUT the cursor channel must self-composite (the IDD-push - /// capturer's forced-composite gate) or the streamed desktop has no cursor at all. + /// The ADD reply flagged the ADAPTER as carrying an IRREVOCABLE IddCx hardware-cursor declare + /// from an earlier session (remote-desktop-sweep §8.6; reach is adapter-wide, not per-target — + /// on-glass 2026-07-23, a GameStream session's fresh target streamed cursor-less): DWM + /// excludes the pointer from its frames until adapter reset, so a session WITHOUT the cursor + /// channel must self-composite (the IDD-push capturer's forced-composite gate) or the + /// streamed desktop has no cursor at all. pub cursor_excluded: bool, } diff --git a/packaging/windows/drivers/pf-vdisplay/src/control.rs b/packaging/windows/drivers/pf-vdisplay/src/control.rs index 2b4642e9..8cc9cb9f 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/control.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/control.rs @@ -165,9 +165,11 @@ 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, + // The ADAPTER already carries an irrevocable hardware-cursor declare from an earlier + // session — DWM's exclusion reaches every later monitor, not just the declaring target + // (on-glass 2026-07-23: declare on 259 left a fresh GameStream 257 cursor-less) — so a + // channel-less session must self-composite the pointer (§8.6 gap, adapter-wide). + cursor_excluded: crate::monitor::any_declared() 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. diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index b0c6ee00..ab113241 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -200,15 +200,18 @@ fn cursor_forward_desired(target_id: u32) -> bool { } /// 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. +/// IRREVOCABLE (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 its EXCLUSION REACH IS THE WHOLE ADAPTER, not the declaring +/// target: a declare on one target leaves every LATER monitor's frames pointer-free too, even a +/// fresh never-declared target id (proven on-glass 2026-07-23: declare on 259, then a GameStream +/// session's fresh 257 streamed a cursor-less desktop). ADD replies therefore report +/// [`AddReply::cursor_excluded`](pf_driver_proto::control::AddReply) from [`any_declared`] — +/// a session that never negotiates the cursor channel 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. Per-target +/// ids are kept purely for the dbglog audit trail. Bounded: ≤ 16 ids. static DECLARED_TARGETS: Mutex> = Mutex::new(Vec::new()); /// Record a SUCCESSFUL hardware-cursor declare on `target_id` (see [`DECLARED_TARGETS`]). @@ -220,12 +223,13 @@ fn mark_declared(target_id: u32) { } } -/// True if `target_id` ever had a hardware cursor declared (see [`DECLARED_TARGETS`]). -pub fn target_declared(target_id: u32) -> bool { - DECLARED_TARGETS +/// True if ANY target ever had a hardware cursor declared in this WUDFHost's life — the +/// adapter-wide exclusion reach (see [`DECLARED_TARGETS`]). +pub fn any_declared() -> bool { + !DECLARED_TARGETS .lock() .unwrap_or_else(|e| e.into_inner()) - .contains(&target_id) + .is_empty() } /// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]).