From b20184462d06e8b28945383d7aabefbb7ed47ebe Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 18:36:41 +0200 Subject: [PATCH] fix(windows/cursor): gate the between-session clean on STREAMING sessions, not keep-alive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on .173: the guard never fired in the one case it exists for. After a desktop-mode session disconnects its monitor LINGERS, so `no_live_displays()` — which counted keep-alive slots as held — refused the restart on exactly the reconnect that needed it, and the capture session went back to `cursor_excluded=true` and forced compositing. Only a streaming session (SlotState::Active) can be damaged by the restart. A lingering/pinned monitor has no session attached and a reconnect preempts and recreates it anyway ("a reused IddCx swap-chain is dead"), so the restart destroys nothing that was going to survive. --- .../src/vdisplay/windows/manager.rs | 24 ++++++++++++------- .../src/vdisplay/windows/pf_vdisplay.rs | 11 +++++---- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs index 0d9686b4..c5c2d331 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs @@ -421,23 +421,31 @@ pub fn hw_cursor_capable() -> bool { m.driver_proto.load(Ordering::Relaxed) >= 5 } -/// Does this host currently hold NO virtual display at all (no live session, no keep-alive slot)? +/// Is NO session currently streaming to a virtual display? /// /// The safety question for anything that tears the adapter down — notably -/// [`crate::driver::clean_cursor_for_next_session`], whose `pnputil /restart-device` would take -/// every monitor on the adapter with it. Deliberately counts KEPT slots as well as streaming ones: -/// a keep-alive monitor belongs to a client that is expected back, and destroying it under them is -/// exactly the kind of cross-session damage the cursor clean-up exists to avoid causing. -pub fn no_live_displays() -> bool { +/// [`crate::driver::clean_cursor_for_next_session`], whose `pnputil /restart-device` takes every +/// monitor on the adapter with it. Only [`SlotState::Active`] counts: that is a session with live +/// references, and destroying its monitor mid-stream is the cross-session damage worth refusing. +/// +/// `Lingering`/`Pinned` slots deliberately do NOT count. They are keep-alive monitors with no +/// session attached, and a reconnect **already** preempts and recreates them — "a reused IddCx +/// swap-chain is dead" (see [`SlotState::Pinned`]) — so a device restart destroys nothing the +/// reconnect was not going to destroy anyway. Counting them was too conservative to be useful: the +/// case this gate exists for is exactly *disconnect from a desktop session, reconnect in capture +/// mode*, and the disconnected session's monitor is lingering at precisely that moment, so the +/// clean-up could never fire when it was most wanted (observed on `.173`, 2026-08-08). +pub fn no_active_sessions() -> bool { match VDM.get() { // Before the first backend open there is nothing to protect. None => true, - Some(m) => m + Some(m) => !m .state .lock() .unwrap_or_else(|e| e.into_inner()) .slots - .is_empty(), + .values() + .any(|s| matches!(s, SlotState::Active { .. })), } } diff --git a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs index 5bd6b696..90d3b3a3 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs @@ -212,11 +212,12 @@ pub fn clean_cursor_for_next_session(session_wants_declare: bool) -> bool { if session_wants_declare || !CURSOR_DECLARED.load(Ordering::Relaxed) { return false; } - // Conservative: a KEPT keep-alive slot counts as held, so a client that is expected back does - // not lose its monitor to another client's cursor preference. The cost of being wrong here is - // someone else's session dying; the cost of skipping is one session compositing its own - // pointer, which is merely the old behaviour. - if !super::manager::no_live_displays() { + // Refuse only while another session is STREAMING — a keep-alive (lingering/pinned) monitor has + // no session attached and a reconnect recreates it regardless, so restarting the adapter costs + // it nothing. Gating on keep-alive too made this dead code in the one case it exists for: after + // a desktop session disconnects its monitor LINGERS, which is exactly when the next + // capture-mode connect needs the declare gone (observed on .173). + if !super::manager::no_active_sessions() { tracing::info!( "cursor: this session wants no hardware cursor and an earlier one declared, but a display is still held (live or keep-alive) — skipping the adapter restart, so the pointer stays host-composited for this session" );