fix(windows/cursor-flip): survive driver-side monitor re-arrival — flag-based flip + channel re-delivery
On-glass (.173, match-window session): every window-size convergence re-creates the driver-side monitor (re-arrival resize / sibling-session slot churn), destroying the cursor worker the channel was delivered to — the flip then died NOT_FOUND (no entry with target_id ∧ worker) and the declared state was lost entirely. Driver: set_cursor_forward becomes STATE, not an edge on one monitor generation — find by target_id ∧ hw_cursor, store cursor_forward_on even without a live worker (it steers the next delivery/re-setup); declare/un-declare only when a worker exists. Channel delivery honors the flag: setup_and_spawn(declare=false) adopts + spawns WITHOUT declaring (composite mode), so a later enable-flip declares against the worker's event. Host: retain the SET_CURSOR_CHANNEL sender; extract deliver_cursor_channel and RE-deliver the surviving section on every ring recreate and — with one retry — when a flip IOCTL fails (the re-arrived entry gets a fresh worker, declared per its flag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<crate::CursorForwardSender>,
|
||||
/// 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<crate::CursorChannelSender>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
slots: Vec<HostSlot>,
|
||||
@@ -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<pf_frame::CursorOverlay> {
|
||||
// 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,
|
||||
|
||||
@@ -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<CursorWorker> {
|
||||
pub fn setup_and_spawn(
|
||||
monitor: iddcx::IDDCX_MONITOR,
|
||||
ch: CursorChannel,
|
||||
declare: bool,
|
||||
) -> Option<CursorWorker> {
|
||||
// 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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user