diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index 5168cf75..78dc005a 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -405,6 +405,11 @@ pub struct IddPushCapturer { /// `None` = no cursor channel this session, or an older driver — the flip degrades to a /// logged no-op (exclusion stays as-is). cursor_forward_sender: Option, + /// Retained delivery sender (`IOCTL_SET_CURSOR_CHANNEL`) for RE-delivery: a driver-side + /// monitor re-arrival (match-window re-arrival resize, a sibling session recreating the + /// 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, width: u32, height: u32, slots: Vec, @@ -978,46 +983,13 @@ impl IddPushCapturer { // is NON-fatal — the driver never declares the hardware cursor without this delivery, // so the session degrades to today's composited pointer (and the forwarder simply // never sees a live overlay). - let cursor_shared = cursor_sender.and_then(|send_cursor| { + let cursor_shared = cursor_sender.as_ref().and_then(|send_cursor| { match cursor::CursorShared::create(target.target_id) { Ok(cs) => { - // Duplication safety: `section_handle` is live for `cs`'s lifetime - // (owned mapping); already inside open_on's unsafe region. - let dup = broker.dup_into_public(cs.section_handle()); - match dup { - Ok(value) => { - let req = pf_driver_proto::control::SetCursorChannelRequest { - target_id: target.target_id, - _pad: 0, - header_handle: value, - }; - match send_cursor(&req) { - Ok(()) => { - tracing::info!( - target_id = target.target_id, - "IDD push(host): cursor channel delivered — driver \ - declares the hardware cursor" - ); - Some(cs) - } - Err(e) => { - broker.close_remote_public(value); - tracing::warn!( - "cursor channel delivery failed (composited cursor \ - stays): {e:#}" - ); - None - } - } - } - Err(e) => { - tracing::warn!( - "cursor section duplication failed (composited cursor \ - stays): {e:#}" - ); - None - } - } + // Deliver via the shared helper (also used for RE-delivery after a + // driver-side monitor re-arrival destroyed the worker). + deliver_cursor_channel(&broker, target.target_id, &cs, send_cursor) + .then_some(cs) } Err(e) => { tracing::warn!( @@ -1099,6 +1071,7 @@ impl IddPushCapturer { cursor_shared, cursor_poll, cursor_forward_sender, + cursor_sender, // 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(), @@ -1443,6 +1416,12 @@ impl IddPushCapturer { "IDD push: frame-channel re-delivery failed after ring recreate" ); } + // Ring recreates ride display churn that can also have re-arrived the MONITOR driver-side + // (destroying its cursor worker with it) — re-deliver the surviving cursor section so the + // 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); + } self.last_seq = 0; self.out_ring.clear(); // the output format changed → rebuild lazily at the new format self.video_conv = None; // converters are sized + HDR-specific → rebuild at the new mode @@ -2087,6 +2066,44 @@ impl std::fmt::Display for AttachTexFail { impl std::error::Error for AttachTexFail {} +/// Duplicate `cs`'s section into the driver's WUDFHost and send `IOCTL_SET_CURSOR_CHANNEL`. +/// `true` = the driver adopted it (worker declared per its `cursor_forward_on` state). Shared by +/// the open-time delivery and every RE-delivery (ring recreate / flip NOT_FOUND) — the request is +/// idempotent driver-side (a replaced worker is stopped + joined). +fn deliver_cursor_channel( + broker: &ChannelBroker, + target_id: u32, + cs: &cursor::CursorShared, + send_cursor: &crate::CursorChannelSender, +) -> bool { + let value = match broker.dup_into_public(cs.section_handle()) { + Ok(v) => v, + Err(e) => { + tracing::warn!("cursor section duplication failed (composited cursor stays): {e:#}"); + return false; + } + }; + let req = pf_driver_proto::control::SetCursorChannelRequest { + target_id, + _pad: 0, + header_handle: value, + }; + match send_cursor(&req) { + Ok(()) => { + tracing::info!( + target_id, + "IDD push(host): cursor channel delivered — driver declares the hardware cursor" + ); + true + } + Err(e) => { + broker.close_remote_public(value); + tracing::warn!("cursor channel delivery failed (composited cursor stays): {e:#}"); + false + } + } +} + impl Capturer for IddPushCapturer { fn cursor(&mut self) -> Option { // A LIVE poller is the sole source — even while it still reports `None` (pre-first-shape): @@ -2118,7 +2135,20 @@ impl Capturer for IddPushCapturer { target_id: self.target_id, enable: on as u32, }; - match send(&req) { + let mut result = send(&req); + if result.is_err() { + // NOT_FOUND usually means the driver-side monitor RE-ARRIVED since delivery + // (match-window re-arrival resize / a sibling session recreating the shared slot): + // the fresh entry has no cursor worker. Re-deliver the surviving section — the + // driver spawns a new worker (declared per its flag) — then retry the flip once. + if let (Some(cs), Some(sc)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref()) + { + if deliver_cursor_channel(&self.broker, self.target_id, cs, sc) { + result = send(&req); + } + } + } + match result { Ok(()) => tracing::info!( target_id = self.target_id, enable = on, diff --git a/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs b/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs index 27ba5c04..380ef7d7 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/cursor_worker.rs @@ -164,7 +164,11 @@ impl Drop for CursorWorker { /// delivered section. `None` on any failure (mapping/magic/event/DDI) — the caller logs and the /// session simply keeps the composited-cursor behavior (the host times out waiting for a first /// seqlock publish and falls back the same way). -pub fn setup_and_spawn(monitor: iddcx::IDDCX_MONITOR, ch: CursorChannel) -> Option { +pub fn setup_and_spawn( + monitor: iddcx::IDDCX_MONITOR, + ch: CursorChannel, + declare: bool, +) -> Option { // Map the host-created section. FILE_MAP_READ|WRITE: we write, the host reads. let mapping = HANDLE(ch.handle as *mut core::ffi::c_void); // SAFETY: `mapping` is the duplicated section handle we own; size is the fixed contract size. @@ -209,22 +213,29 @@ pub fn setup_and_spawn(monitor: iddcx::IDDCX_MONITOR, ch: CursorChannel) -> Opti }; // One caps definition for initial setup AND the per-mode-commit re-setup — see - // `setup_hardware_cursor` for the FULL rationale. - let st = setup_hardware_cursor(monitor, data_evt.0 as isize); - if !nt_success(st) { - dbglog!( - "[pf-vd] cursor: IddCxMonitorSetupHardwareCursor failed 0x{:08x}", - st as u32 - ); - // SAFETY: cleaning up the resources created above. - unsafe { - let _ = CloseHandle(data_evt); - let _ = CloseHandle(stop_evt); - let _ = UnmapViewOfFile(view); + // `setup_hardware_cursor` for the FULL rationale. `declare = false` (a delivery landing + // while the session is in the COMPOSITE render mode) skips the declaration: the worker + // spawns anyway so a later enable-flip has its event to declare against; its queries just + // fail NOT_SUPPORTED until then (logged once, harmless). + if declare { + let st = setup_hardware_cursor(monitor, data_evt.0 as isize); + if !nt_success(st) { + dbglog!( + "[pf-vd] cursor: IddCxMonitorSetupHardwareCursor failed 0x{:08x}", + st as u32 + ); + // SAFETY: cleaning up the resources created above. + unsafe { + let _ = CloseHandle(data_evt); + let _ = CloseHandle(stop_evt); + let _ = UnmapViewOfFile(view); + } + return None; } - return None; + dbglog!("[pf-vd] cursor: hardware cursor declared — worker starting"); + } else { + dbglog!("[pf-vd] cursor: channel adopted UNdeclared (composite mode) — worker starting"); } - dbglog!("[pf-vd] cursor: hardware cursor declared — worker starting"); // Ownership crossing into the thread as plain values (HANDLE/pointer aren't Send). let monitor_v = monitor as usize; diff --git a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs index 4381a678..29531d97 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/monitor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/monitor.rs @@ -392,7 +392,7 @@ pub fn set_cursor_channel( if target_id == 0 { return Err(ch); } - let (monitor_obj, old_worker) = { + let (monitor_obj, declare, old_worker) = { let mut lock = lock_monitors(); let Some(m) = lock .iter_mut() @@ -403,10 +403,13 @@ pub fn set_cursor_channel( let Some(obj) = m.object else { return Err(ch); }; - (obj, m.cursor_worker.take()) + (obj, m.cursor_forward_on, m.cursor_worker.take()) }; drop(old_worker); // stop+join a replaced worker before the new setup - let Some(worker) = crate::cursor_worker::setup_and_spawn(monitor_obj, ch) else { + // `declare = false`: the session is currently in the COMPOSITE render mode (the mid-stream + // flip) — adopt the channel and spawn the worker WITHOUT declaring the hardware cursor, so + // DWM keeps compositing; a later enable-flip declares against the worker's event. + let Some(worker) = crate::cursor_worker::setup_and_spawn(monitor_obj, ch, declare) else { // setup_and_spawn consumed + cleaned the channel; report success=false via NOT_FOUND at // 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 @@ -531,24 +534,36 @@ pub fn resetup_cursor(object: iddcx::IDDCX_MONITOR) { /// host logs and keeps its current behavior. Flag update UNDER the lock; DDI call OUTSIDE it /// (it can re-enter the mode callbacks, which take this lock). pub fn set_cursor_forward(target_id: u32, enable: bool) -> bool { + // The flip is STATE, not an edge on one monitor generation: set the flag on the matching + // hw-cursor entry even when it has no live worker yet (a re-arrived monitor before the + // host's channel re-delivery) — the flag then steers the next delivery/re-setup. Only a + // present worker gets the immediate declare/un-declare DDI call. let found = { let mut lock = lock_monitors(); lock.iter_mut() - .find(|m| m.target_id == target_id && m.cursor_worker.is_some()) + .find(|m| m.target_id == target_id && m.hw_cursor) .map(|m| { m.cursor_forward_on = enable; (m.object, m.cursor_worker.as_ref().map(|w| w.data_event())) }) }; - let Some((Some(object), Some(ev))) = found else { - return false; + let Some((object, worker_ev)) = found else { + return false; // no hw-cursor monitor with this target at all }; - 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 (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}"); + } + _ => dbglog!( + "[pf-vd] cursor: forward flip enable={enable} stored (no live worker — applies at \ + the next channel delivery)" + ), + } true }