fix(pf-vdisplay/driver): report cursor_excluded adapter-wide — the declare's exclusion is not per-target
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m20s
windows-drivers / probe-and-proto (push) Successful in 1m2s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
windows-drivers / driver-build (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
ci / bench (push) Successful in 6m3s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 12m20s
arch / build-publish (push) Successful in 12m47s
deb / build-publish-host (push) Successful in 13m7s
android / android (push) Successful in 16m14s
windows-host / package (push) Successful in 15m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m8s
ci / rust (push) Successful in 26m6s
docker / deploy-docs (push) Canceled after 25s

Field report (iPad via GameStream, 2026-07-23): cursor missing from the
stream. The session's fresh target 257 reported cursor_excluded=false
(DECLARED_TARGETS is keyed per target and only 259 had declared), so the
host skipped forced-composite — but DWM's pointer exclusion after an
IddCx hardware-cursor declare reaches EVERY later monitor of the adapter,
not just the declaring target (proven on-glass: declare on 259 under
WUDFHost 2192, then 257's frames streamed pointer-free with CURSOR_SHOWING
set and SM_MOUSEPRESENT true). GameStream/Moonlight clients cannot draw a
forwarded cursor, so those sessions were silently cursor-less.

ADD replies now report any-declare-this-WUDFHost-life (the state's real
scope — it dies with the adapter reset, same as before); per-target ids
stay only as the dbglog audit trail. Channel-less sessions on a declared
adapter then hit the existing forced-composite gate (GDI poller + blend)
and carry the pointer in-video. Host code unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:58:54 +02:00
co-authored by Claude Fable 5
parent 35c2c1450e
commit 248e1cbf08
4 changed files with 39 additions and 29 deletions
+11 -9
View File
@@ -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 /// 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. /// 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): /// v6 tail ext (no bump, the `AddRequest` luminance-tail discipline):
/// [`control::AddReply::cursor_excluded`] — the driver reports whether the ADDed monitor's OS /// [`control::AddReply::cursor_excluded`] — the driver reports whether its ADAPTER already
/// target already carries a hardware-cursor declare from an earlier session. A declare is /// carries a hardware-cursor declare from an earlier session. A declare is IRREVOCABLE
/// IRREVOCABLE for the target's life (remote-desktop-sweep §8.6, proven on-glass): DWM never /// (remote-desktop-sweep §8.6, proven on-glass) and its exclusion reaches EVERY later monitor of
/// composites the software cursor back into that target's frames, and the sticky state survives /// the adapter, not just the declaring target (on-glass 2026-07-23: a declare on one target left
/// monitor REMOVE→ADD because the host hands every client a STABLE target id. The host uses the /// a different client's fresh target cursor-less): DWM never composites the software cursor back
/// flag to self-composite the pointer (GDI poller + blend) in sessions that never negotiate the /// 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 /// 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), /// 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). /// 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 /// `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. /// WUDFHost restart between sessions can never leave the host duplicating into a dead process.
pub wudf_pid: u32, pub wudf_pid: u32,
/// Non-zero = this monitor's OS target already carries an IRREVOCABLE hardware-cursor /// Non-zero = the ADAPTER already carries an IRREVOCABLE hardware-cursor declare from an
/// declare from an earlier session (remote-desktop-sweep §8.6): DWM excludes the pointer /// earlier session (remote-desktop-sweep §8.6; reach is adapter-wide, not per-target —
/// from every frame on this target, forever, and a session without the cursor channel must /// 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 /// 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` /// [`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 /// luminance tail: an un-upgraded driver writes only the legacy prefix (the host's
+6 -4
View File
@@ -32,10 +32,12 @@ pub struct WinCaptureTarget {
/// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown /// 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). /// (a pre-v2 pairing can't occur — the version handshake is hard — so this only guards misuse).
pub wudf_pid: u32, pub wudf_pid: u32,
/// The ADD reply flagged this target as carrying an IRREVOCABLE IddCx hardware-cursor declare /// The ADD reply flagged the ADAPTER as carrying an IRREVOCABLE IddCx hardware-cursor declare
/// from an earlier session (remote-desktop-sweep §8.6): DWM excludes the pointer from its /// from an earlier session (remote-desktop-sweep §8.6; reach is adapter-wide, not per-target —
/// frames forever, so a session WITHOUT the cursor channel must self-composite (the IDD-push /// on-glass 2026-07-23, a GameStream session's fresh target streamed cursor-less): DWM
/// capturer's forced-composite gate) or the streamed desktop has no cursor at all. /// 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, pub cursor_excluded: bool,
} }
@@ -165,9 +165,11 @@ unsafe fn add(request: WDFREQUEST) {
// This WUDFHost's pid — where the host duplicates the sealed frame channel's handles INTO // 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). // (`ProcessSharingDisabled`: this process is exclusively ours and dies with the device).
wudf_pid: std::process::id(), wudf_pid: std::process::id(),
// The target already carries an irrevocable hardware-cursor declare from an earlier // The ADAPTER already carries an irrevocable hardware-cursor declare from an earlier
// session — a channel-less session must self-composite the pointer (§8.6 gap). // session — DWM's exclusion reaches every later monitor, not just the declaring target
cursor_excluded: crate::monitor::target_declared(target_id) as u32, // (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 // 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. // legacy 20-byte buffer — write the prefix it asked for instead of failing its ADD.
@@ -200,15 +200,18 @@ fn cursor_forward_desired(target_id: u32) -> bool {
} }
/// OS target ids on which `IddCxMonitorSetupHardwareCursor` ever SUCCEEDED. A declare is /// 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 /// IRREVOCABLE (no un-declare DDI; empty-caps re-setup is rejected; even a successful same-mode
/// successful same-mode re-commit never brings DWM's composited pointer back — all proven /// re-commit never brings DWM's composited pointer back — all proven on-glass,
/// on-glass, remote-desktop-sweep §8.6) and it survives monitor REMOVE→ADD because the host hands /// remote-desktop-sweep §8.6) and its EXCLUSION REACH IS THE WHOLE ADAPTER, not the declaring
/// each client a STABLE target id. Reported back on every ADD /// target: a declare on one target leaves every LATER monitor's frames pointer-free too, even a
/// ([`AddReply::cursor_excluded`](pf_driver_proto::control::AddReply)) so a session that never /// fresh never-declared target id (proven on-glass 2026-07-23: declare on 259, then a GameStream
/// negotiates the cursor channel knows it must self-composite the pointer instead of streaming a /// session's fresh 257 streamed a cursor-less desktop). ADD replies therefore report
/// cursor-less desktop. Never cleared: the state's true scope is this WUDFHost's life /// [`AddReply::cursor_excluded`](pf_driver_proto::control::AddReply) from [`any_declared`] —
/// (`ProcessSharingDisabled` — the process, and with it the adapter and every sticky declare, /// a session that never negotiates the cursor channel must self-composite the pointer instead
/// dies on adapter reset), which is exactly when this static resets too. Bounded: ≤ 16 ids. /// 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<Vec<u32>> = Mutex::new(Vec::new()); static DECLARED_TARGETS: Mutex<Vec<u32>> = Mutex::new(Vec::new());
/// Record a SUCCESSFUL hardware-cursor declare on `target_id` (see [`DECLARED_TARGETS`]). /// 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`]). /// True if ANY target ever had a hardware cursor declared in this WUDFHost's life — the
pub fn target_declared(target_id: u32) -> bool { /// adapter-wide exclusion reach (see [`DECLARED_TARGETS`]).
DECLARED_TARGETS pub fn any_declared() -> bool {
!DECLARED_TARGETS
.lock() .lock()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
.contains(&target_id) .is_empty()
} }
/// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]). /// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]).