fix(windows/cursor-flip): flag + forced same-mode re-commit — the working un-declare
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) <noreply@anthropic.com>
This commit is contained in:
@@ -410,6 +410,12 @@ pub struct IddPushCapturer {
|
|||||||
/// shared slot) destroys the driver's cursor worker — the section here survives, so 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.
|
/// channel is re-delivered on ring recreates and on a flip that reports NOT_FOUND.
|
||||||
cursor_sender: Option<crate::CursorChannelSender>,
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
|
/// 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<bool>,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
slots: Vec<HostSlot>,
|
slots: Vec<HostSlot>,
|
||||||
@@ -1072,6 +1078,9 @@ impl IddPushCapturer {
|
|||||||
cursor_poll,
|
cursor_poll,
|
||||||
cursor_forward_sender,
|
cursor_forward_sender,
|
||||||
cursor_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
|
// 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.
|
// wait for the first compose) until the capturer drops with the session.
|
||||||
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
||||||
@@ -1421,6 +1430,9 @@ impl IddPushCapturer {
|
|||||||
// hardware-cursor declaration follows the CURRENT monitor generation.
|
// hardware-cursor declaration follows the CURRENT monitor generation.
|
||||||
if let (Some(cs), Some(send)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref()) {
|
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);
|
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.last_seq = 0;
|
||||||
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
|
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) {
|
fn set_cursor_forward(&mut self, on: bool) {
|
||||||
// No cursor channel this session (or delivery failed): the driver never declared the
|
// No cursor channel this session (or delivery failed): the driver never declared the
|
||||||
// hardware cursor, DWM composites already — both flip directions are no-ops.
|
// hardware cursor, DWM composites already — both flip directions are no-ops. Called
|
||||||
if self.cursor_shared.is_none() {
|
// 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;
|
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 {
|
let Some(send) = self.cursor_forward_sender.as_ref() else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
on,
|
on,
|
||||||
@@ -2151,11 +2169,31 @@ impl Capturer for IddPushCapturer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => tracing::info!(
|
Ok(()) => {
|
||||||
target_id = self.target_id,
|
tracing::info!(
|
||||||
enable = on,
|
target_id = self.target_id,
|
||||||
"IDD push(host): cursor forward flip delivered to the driver"
|
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!(
|
Err(e) => tracing::warn!(
|
||||||
target_id = self.target_id,
|
target_id = self.target_id,
|
||||||
enable = on,
|
enable = on,
|
||||||
|
|||||||
@@ -2033,11 +2033,14 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
// frame-attached overlay (the Linux portal path).
|
// frame-attached overlay (the Linux portal path).
|
||||||
if let Some(fwd) = cursor_fwd.as_mut() {
|
if let Some(fwd) = cursor_fwd.as_mut() {
|
||||||
let client_draws = cursor_client_draws.load(Ordering::Relaxed);
|
let client_draws = cursor_client_draws.load(Ordering::Relaxed);
|
||||||
|
// EVERY tick, not edge-gated: the capturer caches the applied state (an Option
|
||||||
|
// compare in steady state) and clears it on channel re-deliveries — so the render
|
||||||
|
// state survives capturer rebuilds AND driver-side monitor re-arrivals, which an
|
||||||
|
// edge detector here silently lost. Windows IDD (un)declares the driver's hardware
|
||||||
|
// cursor; no-op on every other capturer.
|
||||||
|
capturer.set_cursor_forward(client_draws);
|
||||||
if client_draws != cursor_client_drew {
|
if client_draws != cursor_client_drew {
|
||||||
cursor_client_drew = client_draws;
|
cursor_client_drew = client_draws;
|
||||||
// Windows IDD: (un)declare the driver's hardware cursor so DWM excludes vs
|
|
||||||
// composites; no-op on every other capturer.
|
|
||||||
capturer.set_cursor_forward(client_draws);
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
client_draws,
|
client_draws,
|
||||||
"cursor render mode flipped ({})",
|
"cursor render mode flipped ({})",
|
||||||
|
|||||||
@@ -115,31 +115,12 @@ pub fn setup_hardware_cursor(monitor: iddcx::IDDCX_MONITOR, data_event: isize) -
|
|||||||
unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) }
|
unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UN-declare the hardware cursor (the mid-stream cursor-render flip, proto v6): re-issue the
|
// NOTE: there is NO un-declare path. Re-issuing `IddCxMonitorSetupHardwareCursor` with empty
|
||||||
/// setup with EMPTY caps — no alpha, no XOR, zero max dims — asking the OS to stop routing the
|
// caps (no alpha, XOR NONE, zero max dims) is rejected `STATUS_INVALID_PARAMETER` — observed
|
||||||
/// pointer to this driver and fall back to the software cursor (composited into the desktop
|
// on-glass (26100, driver 9.9.0722.1407). The composite flip therefore works by FLAG + MODE
|
||||||
/// image, which is exactly what the capture mouse model wants in-frame). The IddCx DDI has no
|
// RE-COMMIT: `monitor::set_cursor_forward(false)` stores the flag, the host forces a same-mode
|
||||||
/// documented un-setup; empty caps is the candidate mechanism — the returned status is logged
|
// re-commit, and the OS's per-commit software-cursor default sticks because
|
||||||
/// by the caller, and if the OS refuses it the fallback is [`crate::monitor::resetup_cursor`]'s
|
// `monitor::resetup_cursor` skips the flagged monitor.
|
||||||
/// skip on the next mode commit (the OS reverts to software cursor at every commit by default).
|
|
||||||
/// The data event stays the worker's live handle; a query while un-declared just reports
|
|
||||||
/// nothing new (the host ignores the shm in composite mode).
|
|
||||||
pub fn unsetup_hardware_cursor(monitor: iddcx::IDDCX_MONITOR, data_event: isize) -> i32 {
|
|
||||||
let caps = iddcx::IDDCX_CURSOR_CAPS {
|
|
||||||
Size: core::mem::size_of::<iddcx::IDDCX_CURSOR_CAPS>() 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) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: `stop` is an event handle value; the worker owns every other resource.
|
// SAFETY: `stop` is an event handle value; the worker owns every other resource.
|
||||||
unsafe impl Send for CursorWorker {}
|
unsafe impl Send for CursorWorker {}
|
||||||
|
|||||||
@@ -550,18 +550,25 @@ pub fn set_cursor_forward(target_id: u32, enable: bool) -> bool {
|
|||||||
let Some((object, worker_ev)) = found else {
|
let Some((object, worker_ev)) = found else {
|
||||||
return false; // no hw-cursor monitor with this target at all
|
return false; // no hw-cursor monitor with this target at all
|
||||||
};
|
};
|
||||||
match (object, worker_ev) {
|
match (enable, object, worker_ev) {
|
||||||
(Some(object), Some(ev)) => {
|
(true, Some(object), Some(ev)) => {
|
||||||
let st = if enable {
|
// Enable declares immediately against the live worker's event (works any time).
|
||||||
crate::cursor_worker::setup_hardware_cursor(object, ev)
|
let st = crate::cursor_worker::setup_hardware_cursor(object, ev);
|
||||||
} else {
|
dbglog!("[pf-vd] cursor: forward flip enable=1 (declare) -> {st:#x}");
|
||||||
crate::cursor_worker::unsetup_hardware_cursor(object, ev)
|
}
|
||||||
};
|
(false, _, _) => {
|
||||||
dbglog!("[pf-vd] cursor: forward flip enable={enable} -> {st:#x}");
|
// 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!(
|
_ => dbglog!(
|
||||||
"[pf-vd] cursor: forward flip enable={enable} stored (no live worker — applies at \
|
"[pf-vd] cursor: forward flip enable=1 stored (no live worker — applies at the \
|
||||||
the next channel delivery)"
|
next channel delivery)"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|||||||
Reference in New Issue
Block a user