From b50e4767de1139e9207fd86f57903df68dd693a2 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 22 Jul 2026 14:12:03 +0200 Subject: [PATCH] =?UTF-8?q?fix(windows/cursor-flip):=20flag=20+=20forced?= =?UTF-8?q?=20same-mode=20re-commit=20=E2=80=94=20the=20working=20un-decla?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The empty-caps un-declare is REJECTED by IddCx (STATUS_INVALID_PARAMETER, on-glass driver 9.9.0722.1407) — there is no un-declare DDI. The composite flip now works with the only lever the OS gives us: every mode COMMIT reverts the path to the software cursor, and the driver skips its per-commit re-declare while cursor_forward_on is off. So: - driver: disable stores the flag only (no DDI call); enable still declares immediately against the live worker's event. - host capturer: after a successful disable flip, force a same-mode re-commit (win_display::force_mode_reenumeration) — DWM composites the pointer from the very next commit; the swap-chain flap is the class the driver's preserved-publisher machinery already rides out. - state now survives re-arrivals end-to-end: the capturer caches the APPLIED state (cleared on every channel (re)delivery, which re-created driver entries need — their flag defaults to declared), and the stream loop re-applies every tick instead of edge-gating (steady-state cost: one Option compare). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/pf-capture/src/windows/idd_push.rs | 52 ++++++++++++++++--- crates/punktfunk-host/src/native/stream.rs | 9 ++-- .../drivers/pf-vdisplay/src/cursor_worker.rs | 31 +++-------- .../drivers/pf-vdisplay/src/monitor.rs | 27 ++++++---- 4 files changed, 74 insertions(+), 45 deletions(-) diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index 2b83b77d..3648251b 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -410,6 +410,12 @@ pub struct IddPushCapturer { /// shared slot) destroys the driver's cursor worker — the section here survives, so the /// channel is re-delivered on ring recreates and on a flip that reports NOT_FOUND. cursor_sender: Option, + /// The last cursor-render state APPLIED to the driver (`None` = never / must re-apply). + /// The stream loop calls [`Capturer::set_cursor_forward`] every tick; this cache makes + /// that an Option compare in steady state, and resetting it to `None` after any channel + /// (re)delivery makes the state survive driver-side monitor re-arrivals (whose fresh + /// entries default to declared-at-delivery). + cursor_forward_applied: Option, width: u32, height: u32, slots: Vec, @@ -1072,6 +1078,9 @@ impl IddPushCapturer { cursor_poll, cursor_forward_sender, cursor_sender, + // Open-time deliveries declare (the driver entry's flag starts true); the first + // set_cursor_forward tick reconciles to the session's actual state. + cursor_forward_applied: None, // Held from BEFORE the first-frame gate (the display must not idle off while we // wait for the first compose) until the capturer drops with the session. _display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(), @@ -1421,6 +1430,9 @@ impl IddPushCapturer { // hardware-cursor declaration follows the CURRENT monitor generation. if let (Some(cs), Some(send)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref()) { let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send); + // The fresh driver worker declared per the (possibly re-created, default-true) + // entry flag — force the next tick to re-apply the session's actual render state. + self.cursor_forward_applied = None; } self.last_seq = 0; self.out_ring.clear(); // the output format changed → rebuild lazily at the new format @@ -2121,10 +2133,16 @@ impl Capturer for IddPushCapturer { fn set_cursor_forward(&mut self, on: bool) { // No cursor channel this session (or delivery failed): the driver never declared the - // hardware cursor, DWM composites already — both flip directions are no-ops. - if self.cursor_shared.is_none() { + // hardware cursor, DWM composites already — both flip directions are no-ops. Called + // EVERY encode tick; the `cursor_forward_applied` cache makes the steady state one + // Option compare, and a channel (re)delivery clears it so re-created driver entries + // get the state re-applied. + if self.cursor_shared.is_none() || self.cursor_forward_applied == Some(on) { return; } + // Every exit below counts as applied: failures give up until the next edge/redelivery + // instead of retrying (and re-warning) sixty times a second. + self.cursor_forward_applied = Some(on); let Some(send) = self.cursor_forward_sender.as_ref() else { tracing::warn!( on, @@ -2151,11 +2169,31 @@ impl Capturer for IddPushCapturer { } } match result { - Ok(()) => tracing::info!( - target_id = self.target_id, - enable = on, - "IDD push(host): cursor forward flip delivered to the driver" - ), + Ok(()) => { + tracing::info!( + target_id = self.target_id, + enable = on, + "IDD push(host): cursor forward flip delivered to the driver" + ); + if !on { + // There is no IddCx un-declare DDI (empty-caps re-setup is rejected + // INVALID_PARAMETER — on-glass). The OS reverts to the SOFTWARE cursor on + // every mode commit, and the driver now skips its per-commit re-declare — + // so force a same-mode re-commit to make DWM composite the pointer NOW. + // The swap-chain flap this causes is the class the driver's preserved- + // publisher machinery already rides out. + // SAFETY: read-only CCD query + a same-config SetDisplayConfig apply over + // owned locals; mid-session there is no concurrent topology mutator (the + // manager's isolate/layout writes are session-setup-scoped). + let committed = + unsafe { pf_win_display::win_display::force_mode_reenumeration() }; + tracing::info!( + committed, + "cursor composite flip: forced same-mode re-commit (software cursor \ + from here — DWM composites the pointer)" + ); + } + } Err(e) => tracing::warn!( target_id = self.target_id, enable = on, diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index fa35834d..3ac255f6 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -2033,11 +2033,14 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option i32 { - let caps = iddcx::IDDCX_CURSOR_CAPS { - Size: core::mem::size_of::() as u32, - ColorXorCursorSupport: iddcx::IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE, - MaxX: 0, - MaxY: 0, - AlphaCursorSupport: 0, - }; - let setup = iddcx::IDARG_IN_SETUP_HWCURSOR { - CursorInfo: caps, - hNewCursorDataAvailable: data_event as *mut core::ffi::c_void, - }; - // SAFETY: same contract as [`setup_hardware_cursor`] — live monitor, `setup` outlives the - // call, live event handle. - unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) } -} +// NOTE: there is NO un-declare path. Re-issuing `IddCxMonitorSetupHardwareCursor` with empty +// caps (no alpha, XOR NONE, zero max dims) is rejected `STATUS_INVALID_PARAMETER` — observed +// on-glass (26100, driver 9.9.0722.1407). The composite flip therefore works by FLAG + MODE +// RE-COMMIT: `monitor::set_cursor_forward(false)` stores the flag, the host forces a same-mode +// re-commit, and the OS's per-commit software-cursor default sticks because +// `monitor::resetup_cursor` skips the flagged monitor. // SAFETY: `stop` is an event handle value; the worker owns every other resource. unsafe impl Send for CursorWorker {} diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index 29531d97..f8a85b78 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -550,18 +550,25 @@ pub fn set_cursor_forward(target_id: u32, enable: bool) -> bool { let Some((object, worker_ev)) = found else { return false; // no hw-cursor monitor with this target at all }; - match (object, worker_ev) { - (Some(object), Some(ev)) => { - let st = if enable { - crate::cursor_worker::setup_hardware_cursor(object, ev) - } else { - crate::cursor_worker::unsetup_hardware_cursor(object, ev) - }; - dbglog!("[pf-vd] cursor: forward flip enable={enable} -> {st:#x}"); + match (enable, object, worker_ev) { + (true, Some(object), Some(ev)) => { + // 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}"); + } + (false, _, _) => { + // There is NO un-declare DDI: re-issuing the setup with empty caps is rejected + // INVALID_PARAMETER (observed on-glass, 26100). The stored flag alone steers — the + // HOST forces a same-mode re-commit, whose software-cursor default then STICKS + // because [`resetup_cursor`] skips this monitor while the flag is off. + dbglog!( + "[pf-vd] cursor: forward flip enable=0 stored — awaiting the host's mode \ + re-commit (software cursor from then on)" + ); } _ => dbglog!( - "[pf-vd] cursor: forward flip enable={enable} stored (no live worker — applies at \ - the next channel delivery)" + "[pf-vd] cursor: forward flip enable=1 stored (no live worker — applies at the \ + next channel delivery)" ), } true