feat(windows): IddCx hardware-cursor channel — remote-desktop sweep M2c
Brings the cursor channel to Windows hosts. The pf-vdisplay driver declares an IddCx hardware cursor for sessions that negotiated cursor-forward — DWM then EXCLUDES the pointer from the IDD frame and delivers shape/position out-of-band, into the same CursorOverlay → forwarder → wire → client pipeline the Linux portal path uses. - pf-driver-proto v5 (additive, host floor stays 3): AddRequest's spare tail becomes hw_cursor (same size/offsets); IOCTL_SET_CURSOR_CHANNEL delivers a host-created CursorShm section (64-byte seqlock header + 256² shape buffer, layout pinned + tested). No event crosses the boundary — the host polls at encode-tick pace. - driver: wdk-iddcx grows the two cursor DDI wrappers; a per-monitor cursor worker (event wait → QueryHardwareCursor → seqlock publish) starts only when BOTH the ADD asked and the channel arrived, so a failed delivery leaves DWM compositing as today. Shape bytes ship raw (BGRA/masked + pitch); the host converts. - host: the section rides the existing sealed-channel broker (least- privilege dup, remote reap on failure); IddPushCapturer::cursor() seqlock-reads → CursorOverlay (BGRA→RGBA, masked-color approximation, desktop→frame origin shift, per-shape conversion cache). New Capturer::cursor() trait hook — the encode loop prefers it over the frame-attached overlay because hardware-cursor moves produce NO new frame on a static desktop. hw_cursor survives the re-arrival resize (carried on the manager's Monitor). - negotiation: cursor_forward grows the Windows arm (client cap ∧ driver proto ≥ 5, probed once via the control device); SessionPlan carries cursor_forward → OutputFormat.hw_cursor. - drive-by: wdk-probe's two pre-existing same-type casts (clippy) and pf-vdisplay's stale spike-test refs (crate::win_display moved to pf-win-display; tracing-subscriber was never a dep) repaired. Verified: proto tests on Mac AND MSVC (15/15 incl. the CursorShm layout pin); clippy -D warnings for proto/frame/capture/vdisplay/host on Linux (.21) and native Windows (.173); the DRIVER workspace clippy -D warnings green against the real WDK 10.0.26100 bindgen (DDI names, enum variants and IDARG layouts all bind). On-box driver deploy + on-glass validation follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,15 @@ pub trait Capturer: Send {
|
|||||||
/// SDR / a backend that doesn't expose it (the default — Linux capture has no HDR path yet).
|
/// SDR / a backend that doesn't expose it (the default — Linux capture has no HDR path yet).
|
||||||
/// The stream loop forwards this to the encoder (in-band SEI) and the client (`0xCE` datagram),
|
/// The stream loop forwards this to the encoder (in-band SEI) and the client (`0xCE` datagram),
|
||||||
/// so the two stay a single source of truth. May change mid-session if the source is regraded.
|
/// so the two stay a single source of truth. May change mid-session if the source is regraded.
|
||||||
|
/// The capture source's LIVE cursor state, when it arrives out-of-band from the frames
|
||||||
|
/// (the Windows IddCx hardware-cursor channel). Polled by the encode loop every tick and
|
||||||
|
/// preferred over `CapturedFrame::cursor` — with a hardware cursor, pointer-only moves
|
||||||
|
/// produce NO new frame, so the frame-attached overlay would go stale on a static desktop.
|
||||||
|
/// Default `None`: the Linux portal path attaches its cursor to frames instead.
|
||||||
|
fn cursor(&mut self) -> Option<pf_frame::CursorOverlay> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -367,6 +376,16 @@ pub type FrameChannelSender = std::sync::Arc<
|
|||||||
dyn Fn(&pf_driver_proto::control::SetFrameChannelRequest) -> Result<()> + Send + Sync,
|
dyn Fn(&pf_driver_proto::control::SetFrameChannelRequest) -> Result<()> + Send + Sync,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/// Delivery closure for the v5 hardware-cursor channel (`IOCTL_SET_CURSOR_CHANNEL`) — same
|
||||||
|
/// facade contract as [`FrameChannelSender`]. `Some` also OPTS THE SESSION IN: the capturer
|
||||||
|
/// creates + delivers the cursor section only when the host hands it a sender (the negotiated
|
||||||
|
/// cursor-forward sessions), and the driver only declares the hardware cursor once that
|
||||||
|
/// delivery lands — so a plain session keeps DWM's composited pointer untouched.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub type CursorChannelSender = std::sync::Arc<
|
||||||
|
dyn Fn(&pf_driver_proto::control::SetCursorChannelRequest) -> Result<()> + Send + Sync,
|
||||||
|
>;
|
||||||
|
|
||||||
// One-time PipeWire library init, shared by the video (portal) and audio capture threads.
|
// One-time PipeWire library init, shared by the video (portal) and audio capture threads.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod pwinit;
|
pub mod pwinit;
|
||||||
@@ -450,6 +469,7 @@ pub fn open_idd_push(
|
|||||||
pyrowave: bool,
|
pyrowave: bool,
|
||||||
keepalive: Box<dyn Send>,
|
keepalive: Box<dyn Send>,
|
||||||
sender: FrameChannelSender,
|
sender: FrameChannelSender,
|
||||||
|
cursor_sender: Option<CursorChannelSender>,
|
||||||
) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> {
|
) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> {
|
||||||
idd_push::IddPushCapturer::open(
|
idd_push::IddPushCapturer::open(
|
||||||
target,
|
target,
|
||||||
@@ -459,6 +479,7 @@ pub fn open_idd_push(
|
|||||||
pyrowave,
|
pyrowave,
|
||||||
keepalive,
|
keepalive,
|
||||||
sender,
|
sender,
|
||||||
|
cursor_sender,
|
||||||
)
|
)
|
||||||
.map(|c| Box::new(c) as Box<dyn Capturer>)
|
.map(|c| Box::new(c) as Box<dyn Capturer>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,6 +365,8 @@ pub unsafe fn verify_is_wudfhost(process: HANDLE, wudf_pid: u32, what: &str) ->
|
|||||||
|
|
||||||
#[path = "idd_push/channel.rs"]
|
#[path = "idd_push/channel.rs"]
|
||||||
mod channel;
|
mod channel;
|
||||||
|
#[path = "idd_push/cursor.rs"]
|
||||||
|
mod cursor;
|
||||||
#[path = "idd_push/descriptor.rs"]
|
#[path = "idd_push/descriptor.rs"]
|
||||||
mod descriptor;
|
mod descriptor;
|
||||||
#[path = "idd_push/stall.rs"]
|
#[path = "idd_push/stall.rs"]
|
||||||
@@ -386,6 +388,10 @@ pub struct IddPushCapturer {
|
|||||||
/// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open
|
/// The sealed channel's handle-duplication broker (WUDFHost process + control device); used at open
|
||||||
/// and again on every ring recreate to deliver fresh duplicates.
|
/// and again on every ring recreate to deliver fresh duplicates.
|
||||||
broker: ChannelBroker,
|
broker: ChannelBroker,
|
||||||
|
/// The v5 hardware-cursor channel's host end (`Some` = delivered; the driver declared the
|
||||||
|
/// hardware cursor and seqlock-publishes into it). Survives ring recreates — the section is
|
||||||
|
/// independent of the frame ring's generation.
|
||||||
|
cursor_shared: Option<cursor::CursorShared>,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
slots: Vec<HostSlot>,
|
slots: Vec<HostSlot>,
|
||||||
@@ -603,6 +609,7 @@ impl IddPushCapturer {
|
|||||||
/// instead of tearing the display down (audit §5.1 — no more 20 s black bail). "Failure" includes the
|
/// instead of tearing the display down (audit §5.1 — no more 20 s black bail). "Failure" includes the
|
||||||
/// driver not attaching to the ring within a few seconds (e.g. a hybrid-GPU render mismatch).
|
/// driver not attaching to the ring within a few seconds (e.g. a hybrid-GPU render mismatch).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn open(
|
pub fn open(
|
||||||
target: WinCaptureTarget,
|
target: WinCaptureTarget,
|
||||||
preferred: Option<(u32, u32, u32)>,
|
preferred: Option<(u32, u32, u32)>,
|
||||||
@@ -611,11 +618,20 @@ impl IddPushCapturer {
|
|||||||
pyrowave: bool,
|
pyrowave: bool,
|
||||||
keepalive: Box<dyn Send>,
|
keepalive: Box<dyn Send>,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
|
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
|
||||||
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so
|
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so
|
||||||
// the stall log can correlate DWM holes with OS display events for the session's lifetime.
|
// the stall log can correlate DWM holes with OS display events for the session's lifetime.
|
||||||
pf_win_display::display_events::spawn_once();
|
pf_win_display::display_events::spawn_once();
|
||||||
match Self::open_inner(target, preferred, client_10bit, want_444, pyrowave, sender) {
|
match Self::open_inner(
|
||||||
|
target,
|
||||||
|
preferred,
|
||||||
|
client_10bit,
|
||||||
|
want_444,
|
||||||
|
pyrowave,
|
||||||
|
sender,
|
||||||
|
cursor_sender,
|
||||||
|
) {
|
||||||
Ok(mut me) => {
|
Ok(mut me) => {
|
||||||
me._keepalive = keepalive;
|
me._keepalive = keepalive;
|
||||||
Ok(me)
|
Ok(me)
|
||||||
@@ -632,6 +648,7 @@ impl IddPushCapturer {
|
|||||||
want_444: bool,
|
want_444: bool,
|
||||||
pyrowave: bool,
|
pyrowave: bool,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
|
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
|
||||||
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
|
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
|
||||||
@@ -654,6 +671,7 @@ impl IddPushCapturer {
|
|||||||
pyrowave,
|
pyrowave,
|
||||||
luid,
|
luid,
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
|
cursor_sender.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(me) => Ok(me),
|
Ok(me) => Ok(me),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -687,6 +705,7 @@ impl IddPushCapturer {
|
|||||||
pyrowave,
|
pyrowave,
|
||||||
drv,
|
drv,
|
||||||
sender,
|
sender,
|
||||||
|
cursor_sender,
|
||||||
)
|
)
|
||||||
.context("IDD-push rebind to the driver's reported render adapter")
|
.context("IDD-push rebind to the driver's reported render adapter")
|
||||||
}
|
}
|
||||||
@@ -702,6 +721,7 @@ impl IddPushCapturer {
|
|||||||
pyrowave: bool,
|
pyrowave: bool,
|
||||||
luid: LUID,
|
luid: LUID,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (pw, ph, _hz) = preferred
|
let (pw, ph, _hz) = preferred
|
||||||
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
||||||
@@ -935,6 +955,60 @@ impl IddPushCapturer {
|
|||||||
)
|
)
|
||||||
.context("deliver IDD-push frame channel to the driver")?;
|
.context("deliver IDD-push frame channel to the driver")?;
|
||||||
|
|
||||||
|
// v5 hardware-cursor channel (M2c): create + deliver the CursorShm section. Failure
|
||||||
|
// 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| {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"cursor section creation failed (composited cursor stays): {e:#}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target_id = target.target_id,
|
target_id = target.target_id,
|
||||||
wudf_pid = target.wudf_pid,
|
wudf_pid = target.wudf_pid,
|
||||||
@@ -993,6 +1067,7 @@ impl IddPushCapturer {
|
|||||||
last_seq: 0,
|
last_seq: 0,
|
||||||
last_present: None,
|
last_present: None,
|
||||||
status_logged: false,
|
status_logged: false,
|
||||||
|
cursor_shared,
|
||||||
// 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(),
|
||||||
@@ -1982,6 +2057,10 @@ impl std::fmt::Display for AttachTexFail {
|
|||||||
impl std::error::Error for AttachTexFail {}
|
impl std::error::Error for AttachTexFail {}
|
||||||
|
|
||||||
impl Capturer for IddPushCapturer {
|
impl Capturer for IddPushCapturer {
|
||||||
|
fn cursor(&mut self) -> Option<pf_frame::CursorOverlay> {
|
||||||
|
self.cursor_shared.as_mut().and_then(|c| c.read())
|
||||||
|
}
|
||||||
|
|
||||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(20);
|
let deadline = Instant::now() + Duration::from_secs(20);
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -105,6 +105,22 @@ impl ChannelBroker {
|
|||||||
Ok(out.0 as usize as u64)
|
Ok(out.0 as usize as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Duplicate the cursor section into WUDFHost (v5 cursor channel) with the same
|
||||||
|
/// least-privilege section rights as the frame header. Thin `pub(super)` face over
|
||||||
|
/// [`dup_into`](Self::dup_into) for the cursor-delivery path in `open_on`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `h` must be a live handle of the current process.
|
||||||
|
pub(super) unsafe fn dup_into_public(&self, h: HANDLE) -> Result<u64> {
|
||||||
|
// SAFETY: forwarded contract — `h` is live per this fn's own contract.
|
||||||
|
unsafe { self.dup_into(h, Some(SECTION_MAP_RW)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`close_remote`](Self::close_remote) for the cursor-delivery failure path.
|
||||||
|
pub(super) fn close_remote_public(&self, value: u64) {
|
||||||
|
self.close_remote(value);
|
||||||
|
}
|
||||||
|
|
||||||
/// Close a handle VALUE inside the WUDFHost table (the failure-path reaper): `DUPLICATE_CLOSE_SOURCE`
|
/// Close a handle VALUE inside the WUDFHost table (the failure-path reaper): `DUPLICATE_CLOSE_SOURCE`
|
||||||
/// with no target closes the source handle regardless of the (ignored) result.
|
/// with no target closes the source handle regardless of the (ignored) result.
|
||||||
fn close_remote(&self, value: u64) {
|
fn close_remote(&self, value: u64) {
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
//! Host side of the v5 hardware-cursor channel (remote-desktop-sweep M2c): the capturer creates
|
||||||
|
//! an unnamed [`CursorShm`] section, delivers it to the pf-vdisplay driver (which declares an
|
||||||
|
//! IddCx hardware cursor — DWM then EXCLUDES the pointer from the frames we consume), and reads
|
||||||
|
//! the driver's seqlock publishes here at encode-tick pace, converting them into the same
|
||||||
|
//! [`pf_frame::CursorOverlay`] the Linux portal path produces — everything downstream (the
|
||||||
|
//! cursor forwarder, the wire, the client renderer) is shared.
|
||||||
|
|
||||||
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it.
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use pf_driver_proto::cursor::{
|
||||||
|
CursorShm, CURSOR_MAGIC, CURSOR_SHAPE_BYTES, CURSOR_SHAPE_MAX, CURSOR_SHAPE_OFFSET,
|
||||||
|
CURSOR_SHM_SIZE, CURSOR_TYPE_MASKED_COLOR,
|
||||||
|
};
|
||||||
|
use std::sync::atomic::AtomicU32;
|
||||||
|
|
||||||
|
/// The host end of one monitor's cursor channel: the section (we created it — the mapping stays
|
||||||
|
/// valid for the capturer's life) plus the reader's conversion cache.
|
||||||
|
pub(super) struct CursorShared {
|
||||||
|
section: MappedSection,
|
||||||
|
/// The monitor's desktop origin — IddCx reports positions in DESKTOP coordinates; the
|
||||||
|
/// overlay wants frame-relative. Fetched at attach (the virtual monitor's placement is
|
||||||
|
/// stable for the session; a topology change recreates the pipeline anyway).
|
||||||
|
origin: (i32, i32),
|
||||||
|
/// Conversion cache: the last `shape_id` whose pixels were converted, and the result.
|
||||||
|
/// Position-only updates (the common case) reuse it — a refcount bump, no pixel work.
|
||||||
|
cached_id: u32,
|
||||||
|
cached: Option<ConvertedShape>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConvertedShape {
|
||||||
|
rgba: std::sync::Arc<Vec<u8>>,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
hot_x: u32,
|
||||||
|
hot_y: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorShared {
|
||||||
|
/// Create + initialize the section (magic stamped, seq even/zero). The returned handle is
|
||||||
|
/// the section itself (owned by `self`); the caller duplicates it into the WUDFHost.
|
||||||
|
pub(super) fn create(target_id: u32) -> Result<CursorShared> {
|
||||||
|
// SAFETY: plain FFI. Unnamed pagefile-backed section, host-lifetime owned; the view is
|
||||||
|
// mapped once and unmapped never (the capturer's life = the session's life).
|
||||||
|
let section = unsafe {
|
||||||
|
let map = CreateFileMappingW(
|
||||||
|
INVALID_HANDLE_VALUE,
|
||||||
|
None,
|
||||||
|
PAGE_READWRITE,
|
||||||
|
0,
|
||||||
|
CURSOR_SHM_SIZE as u32,
|
||||||
|
PCWSTR::null(),
|
||||||
|
)
|
||||||
|
.context("CreateFileMapping(cursor)")?;
|
||||||
|
let map = OwnedHandle::from_raw_handle(map.0 as _);
|
||||||
|
let view = MapViewOfFile(
|
||||||
|
HANDLE(map.as_raw_handle()),
|
||||||
|
FILE_MAP_ALL_ACCESS,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CURSOR_SHM_SIZE,
|
||||||
|
);
|
||||||
|
if view.Value.is_null() {
|
||||||
|
bail!("MapViewOfFile failed for the cursor section");
|
||||||
|
}
|
||||||
|
let shm = view.Value.cast::<CursorShm>();
|
||||||
|
std::ptr::write_bytes(view.Value.cast::<u8>(), 0, CURSOR_SHM_SIZE);
|
||||||
|
// Magic LAST-ish (the driver validates it at adopt; seq 0 = even = consistent).
|
||||||
|
std::sync::atomic::fence(Ordering::Release);
|
||||||
|
(*shm).magic = CURSOR_MAGIC;
|
||||||
|
MappedSection { handle: map, view }
|
||||||
|
};
|
||||||
|
// Desktop origin of this monitor's source — for the desktop→frame coordinate shift.
|
||||||
|
// SAFETY: `source_desktop_rect` only runs the CCD QueryDisplayConfig FFI over owned
|
||||||
|
// locals (same call the compose-kick path makes).
|
||||||
|
let rect = unsafe { pf_win_display::win_display::source_desktop_rect(target_id) };
|
||||||
|
let origin = rect.map(|(x, y, _w, _h)| (x, y)).unwrap_or((0, 0));
|
||||||
|
Ok(CursorShared {
|
||||||
|
section,
|
||||||
|
origin,
|
||||||
|
cached_id: 0,
|
||||||
|
cached: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The section handle for the broker's duplication into the WUDFHost.
|
||||||
|
pub(super) fn section_handle(&self) -> HANDLE {
|
||||||
|
HANDLE(self.section.handle.as_raw_handle())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seqlock-read the driver's latest publish → a frame-relative [`pf_frame::CursorOverlay`].
|
||||||
|
/// `None` until the first publish lands (or while the pointer has never been seen). A hidden
|
||||||
|
/// pointer returns `Some` with `visible: false` — the forwarder turns that into the client's
|
||||||
|
/// relative-mode hint, exactly like the Linux path.
|
||||||
|
pub(super) fn read(&mut self) -> Option<pf_frame::CursorOverlay> {
|
||||||
|
let shm = self.section.ptr::<CursorShm>();
|
||||||
|
// SAFETY: the view spans CURSOR_SHM_SIZE for self's lifetime; seq is 4-aligned in the
|
||||||
|
// fixed layout (offset 4).
|
||||||
|
let seq = unsafe { &*std::ptr::addr_of!((*shm).seq).cast::<AtomicU32>() };
|
||||||
|
for _ in 0..64 {
|
||||||
|
let s1 = seq.load(Ordering::Acquire);
|
||||||
|
if s1 == 0 {
|
||||||
|
return None; // no publish yet
|
||||||
|
}
|
||||||
|
if s1 & 1 != 0 {
|
||||||
|
std::hint::spin_loop();
|
||||||
|
continue; // writer mid-update
|
||||||
|
}
|
||||||
|
// SAFETY: header reads within the mapped view; consistency is validated by the
|
||||||
|
// seq re-check below (a torn read is discarded and retried).
|
||||||
|
let hdr = unsafe { std::ptr::read_volatile(shm) };
|
||||||
|
// Shape pixels: convert only when the OS minted a new shape id.
|
||||||
|
if hdr.visible != 0 && hdr.shape_id != self.cached_id {
|
||||||
|
let rows = hdr.height.min(CURSOR_SHAPE_MAX) as usize;
|
||||||
|
let width = hdr.width.min(CURSOR_SHAPE_MAX) as usize;
|
||||||
|
let pitch = (hdr.pitch as usize).min(CURSOR_SHAPE_BYTES / rows.max(1));
|
||||||
|
let mut raw = vec![0u8; rows * pitch];
|
||||||
|
// SAFETY: the shape region spans CURSOR_SHAPE_BYTES from CURSOR_SHAPE_OFFSET
|
||||||
|
// inside the mapped view; `rows * pitch` is clamped to it above.
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(
|
||||||
|
self.section.ptr::<u8>().add(CURSOR_SHAPE_OFFSET),
|
||||||
|
raw.as_mut_ptr(),
|
||||||
|
rows * pitch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Discard the copy if the writer raced us mid-shape (seq moved) — retry.
|
||||||
|
if seq.load(Ordering::Acquire) != s1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.cached = Some(convert_shape(&hdr, &raw, width, rows, pitch));
|
||||||
|
self.cached_id = hdr.shape_id;
|
||||||
|
} else if seq.load(Ordering::Acquire) != s1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let shape = self.cached.as_ref()?;
|
||||||
|
return Some(pf_frame::CursorOverlay {
|
||||||
|
x: hdr.x - self.origin.0,
|
||||||
|
y: hdr.y - self.origin.1,
|
||||||
|
w: shape.w,
|
||||||
|
h: shape.h,
|
||||||
|
rgba: shape.rgba.clone(),
|
||||||
|
serial: u64::from(hdr.shape_id),
|
||||||
|
hot_x: shape.hot_x,
|
||||||
|
hot_y: shape.hot_y,
|
||||||
|
visible: hdr.visible != 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None // persistent tearing (writer wedged mid-seq) — skip this tick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the OS's 32-bpp pitch-strided shape rows into the overlay's packed straight RGBA.
|
||||||
|
/// ALPHA cursors are BGRA with straight per-pixel alpha (swap R↔B). MASKED_COLOR approximates:
|
||||||
|
/// alpha 0x00 = opaque color pixel; 0xFF = an XOR pixel we cannot honor client-side — rendered
|
||||||
|
/// as a translucent mid-gray so inversion cursors stay visible instead of vanishing.
|
||||||
|
fn convert_shape(
|
||||||
|
hdr: &CursorShm,
|
||||||
|
raw: &[u8],
|
||||||
|
width: usize,
|
||||||
|
rows: usize,
|
||||||
|
pitch: usize,
|
||||||
|
) -> ConvertedShape {
|
||||||
|
let masked = hdr.cursor_type == CURSOR_TYPE_MASKED_COLOR;
|
||||||
|
let mut rgba = Vec::with_capacity(width * rows * 4);
|
||||||
|
for y in 0..rows {
|
||||||
|
let row = &raw[y * pitch..];
|
||||||
|
for x in 0..width {
|
||||||
|
let o = x * 4;
|
||||||
|
if o + 4 > row.len() {
|
||||||
|
rgba.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (b, g, r, a) = (row[o], row[o + 1], row[o + 2], row[o + 3]);
|
||||||
|
if masked {
|
||||||
|
if a == 0 {
|
||||||
|
rgba.extend_from_slice(&[r, g, b, 0xFF]);
|
||||||
|
} else {
|
||||||
|
rgba.extend_from_slice(&[0x80, 0x80, 0x80, 0xB4]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rgba.extend_from_slice(&[r, g, b, a]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConvertedShape {
|
||||||
|
rgba: std::sync::Arc::new(rgba),
|
||||||
|
w: width as u32,
|
||||||
|
h: rows as u32,
|
||||||
|
hot_x: hdr.hot_x.min(width.saturating_sub(1) as u32),
|
||||||
|
hot_y: hdr.hot_y.min(rows.saturating_sub(1) as u32),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,15 @@ pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
|
|||||||
/// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it;
|
/// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it;
|
||||||
/// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship
|
/// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship
|
||||||
/// driver+host together, as ever.
|
/// driver+host together, as ever.
|
||||||
pub const PROTOCOL_VERSION: u32 = 4;
|
/// v5: ADDITIVE — the IddCx HARDWARE CURSOR channel (remote-desktop-sweep M2c):
|
||||||
|
/// [`control::AddRequest::hw_cursor`] (the former tail `_reserved` — same size, same offsets)
|
||||||
|
/// asks the driver to declare a hardware cursor for this monitor (DWM then EXCLUDES the pointer
|
||||||
|
/// from the desktop frame and delivers shape/position out-of-band), and
|
||||||
|
/// [`control::IOCTL_SET_CURSOR_CHANNEL`] delivers the host-created [`cursor::CursorShm`] section
|
||||||
|
/// the driver's cursor thread seqlock-publishes into. Nothing existing changed; the host gates
|
||||||
|
/// the feature on the handshake-reported version (`>= 5`) and keeps composited-cursor behavior
|
||||||
|
/// against older drivers.
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 5;
|
||||||
|
|
||||||
/// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on
|
/// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on
|
||||||
/// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the
|
/// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the
|
||||||
@@ -110,6 +118,14 @@ pub mod control {
|
|||||||
/// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3
|
/// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3
|
||||||
/// driver fails this unknown IOCTL → the host falls back to the re-arrival resize.
|
/// driver fails this unknown IOCTL → the host falls back to the re-arrival resize.
|
||||||
pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907);
|
pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907);
|
||||||
|
/// Deliver a monitor's hardware-cursor channel (v5): the handle VALUE of the unnamed
|
||||||
|
/// [`cursor::CursorShm`](crate::cursor) file mapping the host duplicated into the WUDFHost
|
||||||
|
/// (same delivery model as [`IOCTL_SET_FRAME_CHANNEL`], no event — the host polls the
|
||||||
|
/// seqlock at its encode-tick pace). Sent once after ADD, only for a monitor whose
|
||||||
|
/// [`AddRequest::hw_cursor`] was set. The driver maps it, calls
|
||||||
|
/// `IddCxMonitorSetupHardwareCursor`, and starts its cursor-query thread. Input
|
||||||
|
/// [`SetCursorChannelRequest`].
|
||||||
|
pub const IOCTL_SET_CURSOR_CHANNEL: u32 = ctl_code(0x908);
|
||||||
|
|
||||||
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
|
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
|
||||||
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
|
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
|
||||||
@@ -147,9 +163,13 @@ pub mod control {
|
|||||||
/// The client display's min luminance in MILLI-nits (0.001 cd/m² — the CTA min-luminance
|
/// The client display's min luminance in MILLI-nits (0.001 cd/m² — the CTA min-luminance
|
||||||
/// range lives well below 1 nit) → Desired Content Min Luminance. `0` = unknown.
|
/// range lives well below 1 nit) → Desired Content Min Luminance. `0` = unknown.
|
||||||
pub min_luminance_millinits: u32,
|
pub min_luminance_millinits: u32,
|
||||||
/// Pads the `u64`-aligned struct to a multiple of 8 (Pod forbids implicit tail padding);
|
/// Non-zero = declare an IddCx HARDWARE CURSOR for this monitor (v5, remote-desktop-sweep
|
||||||
/// free expansion room for the next appended field.
|
/// M2c): DWM stops compositing the pointer into the frame and the driver publishes
|
||||||
pub _reserved: u32,
|
/// shape/position into the [`cursor::CursorShm`](crate::cursor) section delivered by
|
||||||
|
/// [`IOCTL_SET_CURSOR_CHANNEL`]. Byte-compatible with the old tail `_reserved` (offset 36):
|
||||||
|
/// an un-upgraded driver ignores it (cursor stays composited — the host already gates on
|
||||||
|
/// the handshake version, this is defense in depth), an un-upgraded host sends `0` (off).
|
||||||
|
pub hw_cursor: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`AddRequest`]'s size before the client-HDR luminance tail — the prefix an un-upgraded
|
/// [`AddRequest`]'s size before the client-HDR luminance tail — the prefix an un-upgraded
|
||||||
@@ -253,6 +273,18 @@ pub mod control {
|
|||||||
/// at the compile-time maximum; `ring_len` says how many entries are live).
|
/// at the compile-time maximum; `ring_len` says how many entries are live).
|
||||||
pub const RING_LEN_USIZE: usize = RING_LEN as usize;
|
pub const RING_LEN_USIZE: usize = RING_LEN as usize;
|
||||||
|
|
||||||
|
/// `IOCTL_SET_CURSOR_CHANNEL` input (v5): the hardware-cursor section for one monitor.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SetCursorChannelRequest {
|
||||||
|
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
|
||||||
|
pub target_id: u32,
|
||||||
|
pub _pad: u32,
|
||||||
|
/// The [`cursor::CursorShm`](crate::cursor) file-mapping handle VALUE, already duplicated
|
||||||
|
/// into the driver's WUDFHost process ([`AddReply::wudf_pid`]).
|
||||||
|
pub header_handle: u64,
|
||||||
|
}
|
||||||
|
|
||||||
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
|
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
|
||||||
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
|
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
|
||||||
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
|
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
|
||||||
@@ -269,6 +301,9 @@ pub mod control {
|
|||||||
assert!(offset_of!(AddRequest, max_luminance_nits) == ADD_REQUEST_LEGACY_SIZE);
|
assert!(offset_of!(AddRequest, max_luminance_nits) == ADD_REQUEST_LEGACY_SIZE);
|
||||||
assert!(offset_of!(AddRequest, max_frame_avg_nits) == 28);
|
assert!(offset_of!(AddRequest, max_frame_avg_nits) == 28);
|
||||||
assert!(offset_of!(AddRequest, min_luminance_millinits) == 32);
|
assert!(offset_of!(AddRequest, min_luminance_millinits) == 32);
|
||||||
|
// v5: the former tail `_reserved` — same offset, same total size (rename-only).
|
||||||
|
assert!(offset_of!(AddRequest, hw_cursor) == 36);
|
||||||
|
assert!(size_of::<AddRequest>() == 40);
|
||||||
|
|
||||||
assert!(size_of::<AddReply>() == 20);
|
assert!(size_of::<AddReply>() == 20);
|
||||||
assert!(offset_of!(AddReply, adapter_luid_low) == 0);
|
assert!(offset_of!(AddReply, adapter_luid_low) == 0);
|
||||||
@@ -288,6 +323,10 @@ pub mod control {
|
|||||||
assert!(size_of::<RemoveRequest>() == 8);
|
assert!(size_of::<RemoveRequest>() == 8);
|
||||||
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
||||||
|
|
||||||
|
assert!(size_of::<SetCursorChannelRequest>() == 16);
|
||||||
|
assert!(offset_of!(SetCursorChannelRequest, target_id) == 0);
|
||||||
|
assert!(offset_of!(SetCursorChannelRequest, header_handle) == 8);
|
||||||
|
|
||||||
assert!(size_of::<UpdateModesRequest>() == 24);
|
assert!(size_of::<UpdateModesRequest>() == 24);
|
||||||
assert!(offset_of!(UpdateModesRequest, session_id) == 0);
|
assert!(offset_of!(UpdateModesRequest, session_id) == 0);
|
||||||
assert!(offset_of!(UpdateModesRequest, width) == 8);
|
assert!(offset_of!(UpdateModesRequest, width) == 8);
|
||||||
@@ -960,6 +999,84 @@ pub mod mouse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The v5 hardware-cursor channel (remote-desktop-sweep M2c): one unnamed file mapping per
|
||||||
|
/// monitor, host-created, delivered by handle value ([`control::IOCTL_SET_CURSOR_CHANNEL`]).
|
||||||
|
/// The DRIVER's cursor thread (woken by its IddCx `hNewCursorDataAvailable` event) seqlock-writes
|
||||||
|
/// shape + position + visibility; the HOST reads at its encode-tick pace — no event crosses the
|
||||||
|
/// boundary. Writer: bump [`CursorShm::seq`] to ODD, write fields (+ shape bytes when the OS said
|
||||||
|
/// the shape changed), bump to EVEN. Reader: read seq (retry while odd), copy, re-read seq —
|
||||||
|
/// unchanged ⇒ consistent snapshot. Position-only updates never touch the shape bytes, so a
|
||||||
|
/// reader that skips unchanged `shape_id`s never copies torn pixels.
|
||||||
|
pub mod cursor {
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
|
/// First field of [`CursorShm`] — `b"PFCU"` little-endian; anything else = not attached yet.
|
||||||
|
pub const CURSOR_MAGIC: u32 = u32::from_le_bytes(*b"PFCU");
|
||||||
|
|
||||||
|
/// Max cursor side (px) the driver declares to the OS (`IDDCX_CURSOR_CAPS::MaxX/MaxY`) and
|
||||||
|
/// the section's shape buffer is sized for. Windows XL accessibility cursors top out here;
|
||||||
|
/// the host's wire forwarder downscales to its own cap anyway.
|
||||||
|
pub const CURSOR_SHAPE_MAX: u32 = 256;
|
||||||
|
|
||||||
|
/// Shape-buffer bytes: 32-bpp at the declared max.
|
||||||
|
pub const CURSOR_SHAPE_BYTES: usize = (CURSOR_SHAPE_MAX * CURSOR_SHAPE_MAX * 4) as usize;
|
||||||
|
|
||||||
|
/// Byte offset of the shape pixels inside the section (one cache-line-ish header).
|
||||||
|
pub const CURSOR_SHAPE_OFFSET: usize = 64;
|
||||||
|
|
||||||
|
/// Total section size.
|
||||||
|
pub const CURSOR_SHM_SIZE: usize = CURSOR_SHAPE_OFFSET + CURSOR_SHAPE_BYTES;
|
||||||
|
|
||||||
|
/// `IDDCX_CURSOR_SHAPE_TYPE` values mirrored for the host (the driver writes the OS value
|
||||||
|
/// verbatim into [`CursorShm::cursor_type`]).
|
||||||
|
pub const CURSOR_TYPE_MASKED_COLOR: u32 = 1;
|
||||||
|
pub const CURSOR_TYPE_ALPHA: u32 = 2;
|
||||||
|
|
||||||
|
/// The section header (the shape pixels follow at [`CURSOR_SHAPE_OFFSET`]). `x`/`y` are the
|
||||||
|
/// shape's TOP-LEFT in desktop coordinates (the IddCx `IDARG_OUT_QUERY_HWCURSOR::X/Y`
|
||||||
|
/// convention — position − hotspot, can be negative); `shape_id` is the OS's per-set counter
|
||||||
|
/// (bumps on every shape set, the overlay serial); pixels are the OS's 32-bpp rows at
|
||||||
|
/// `pitch` bytes (BGRA for ALPHA; color+mask for MASKED_COLOR — the host converts).
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CursorShm {
|
||||||
|
pub magic: u32,
|
||||||
|
/// Seqlock: odd = writer mid-update.
|
||||||
|
pub seq: u32,
|
||||||
|
pub visible: u32,
|
||||||
|
pub cursor_type: u32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub shape_id: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub pitch: u32,
|
||||||
|
pub hot_x: u32,
|
||||||
|
pub hot_y: u32,
|
||||||
|
/// Reserved expansion room up to [`CURSOR_SHAPE_OFFSET`].
|
||||||
|
pub _reserved: [u32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout is load-bearing across the process boundary — pin it.
|
||||||
|
const _: () = {
|
||||||
|
use core::mem::{offset_of, size_of};
|
||||||
|
assert!(size_of::<CursorShm>() == 64);
|
||||||
|
assert!(size_of::<CursorShm>() <= CURSOR_SHAPE_OFFSET);
|
||||||
|
assert!(offset_of!(CursorShm, magic) == 0);
|
||||||
|
assert!(offset_of!(CursorShm, seq) == 4);
|
||||||
|
assert!(offset_of!(CursorShm, visible) == 8);
|
||||||
|
assert!(offset_of!(CursorShm, cursor_type) == 12);
|
||||||
|
assert!(offset_of!(CursorShm, x) == 16);
|
||||||
|
assert!(offset_of!(CursorShm, y) == 20);
|
||||||
|
assert!(offset_of!(CursorShm, shape_id) == 24);
|
||||||
|
assert!(offset_of!(CursorShm, width) == 28);
|
||||||
|
assert!(offset_of!(CursorShm, height) == 32);
|
||||||
|
assert!(offset_of!(CursorShm, pitch) == 36);
|
||||||
|
assert!(offset_of!(CursorShm, hot_x) == 40);
|
||||||
|
assert!(offset_of!(CursorShm, hot_y) == 44);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1079,7 +1196,7 @@ mod tests {
|
|||||||
max_luminance_nits: 800,
|
max_luminance_nits: 800,
|
||||||
max_frame_avg_nits: 400,
|
max_frame_avg_nits: 400,
|
||||||
min_luminance_millinits: 50, // 0.05 nits
|
min_luminance_millinits: 50, // 0.05 nits
|
||||||
_reserved: 0,
|
hw_cursor: 1,
|
||||||
};
|
};
|
||||||
let bytes = bytemuck::bytes_of(&req);
|
let bytes = bytemuck::bytes_of(&req);
|
||||||
assert_eq!(bytes.len(), 40);
|
assert_eq!(bytes.len(), 40);
|
||||||
@@ -1161,11 +1278,39 @@ mod tests {
|
|||||||
req
|
req
|
||||||
);
|
);
|
||||||
assert_eq!(bytes[8..12], 2560u32.to_le_bytes());
|
assert_eq!(bytes[8..12], 2560u32.to_le_bytes());
|
||||||
// The compat window: v4 is additive over v3, so the host floor stays one below.
|
// The compat window: v4/v5 are additive over v3, so the host floor stays at 3.
|
||||||
assert_eq!(PROTOCOL_VERSION, 4);
|
assert_eq!(PROTOCOL_VERSION, 5);
|
||||||
assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3);
|
assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_shm_layout_is_pinned() {
|
||||||
|
use cursor::*;
|
||||||
|
// The header must leave the shape offset intact whatever grows inside `_reserved`.
|
||||||
|
assert_eq!(core::mem::size_of::<CursorShm>(), 64);
|
||||||
|
assert_eq!(CURSOR_SHM_SIZE, 64 + 256 * 256 * 4);
|
||||||
|
assert_eq!(CURSOR_MAGIC, u32::from_le_bytes(*b"PFCU"));
|
||||||
|
// Seqlock snapshot discipline survives a bytemuck roundtrip.
|
||||||
|
let hdr = CursorShm {
|
||||||
|
magic: CURSOR_MAGIC,
|
||||||
|
seq: 2,
|
||||||
|
visible: 1,
|
||||||
|
cursor_type: CURSOR_TYPE_ALPHA,
|
||||||
|
x: -3,
|
||||||
|
y: 7,
|
||||||
|
shape_id: 42,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
pitch: 128,
|
||||||
|
hot_x: 4,
|
||||||
|
hot_y: 5,
|
||||||
|
_reserved: [0; 4],
|
||||||
|
};
|
||||||
|
let bytes = bytemuck::bytes_of(&hdr);
|
||||||
|
assert_eq!(*bytemuck::from_bytes::<CursorShm>(bytes), hdr);
|
||||||
|
assert_eq!(bytes[16..20], (-3i32).to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gamepad_names_and_magics_are_stable() {
|
fn gamepad_names_and_magics_are_stable() {
|
||||||
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
||||||
|
|||||||
@@ -152,6 +152,12 @@ pub struct OutputFormat {
|
|||||||
/// codec's backend, and the fallback family) would misread the two-plane buffer as packed
|
/// codec's backend, and the fallback family) would misread the two-plane buffer as packed
|
||||||
/// RGB. Always `false` on Windows (the IDD-push capturer owns its own formats).
|
/// RGB. Always `false` on Windows (the IDD-push capturer owns its own formats).
|
||||||
pub nv12_native: bool,
|
pub nv12_native: bool,
|
||||||
|
/// The session negotiated the cursor-forward channel (remote-desktop-sweep M2c): on Windows
|
||||||
|
/// the IDD-push capturer creates + delivers the driver's hardware-cursor section, so DWM
|
||||||
|
/// stops compositing the pointer into the frames and the capturer surfaces it via
|
||||||
|
/// `Capturer::cursor()` instead. Ignored on Linux (the portal's `SPA_META_Cursor` already
|
||||||
|
/// separates the pointer; the session plan's `cursor_blend` gate handles the rest).
|
||||||
|
pub hw_cursor: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputFormat {
|
impl OutputFormat {
|
||||||
@@ -169,6 +175,8 @@ impl OutputFormat {
|
|||||||
chroma_444: false,
|
chroma_444: false,
|
||||||
// GameStream never negotiates PyroWave (native punktfunk/1 only).
|
// GameStream never negotiates PyroWave (native punktfunk/1 only).
|
||||||
pyrowave: false,
|
pyrowave: false,
|
||||||
|
// GameStream/spike sessions never negotiate the cursor channel.
|
||||||
|
hw_cursor: false,
|
||||||
// Conservative: the GameStream + spike paths don't resolve the codec here, and a
|
// Conservative: the GameStream + spike paths don't resolve the codec here, and a
|
||||||
// Moonlight client may negotiate H264 (whose VAAPI backend can't ingest NV12) — so
|
// Moonlight client may negotiate H264 (whose VAAPI backend can't ingest NV12) — so
|
||||||
// they never prefer the producer-native NV12 pod. The punktfunk/1 plane opts in via
|
// they never prefer the producer-native NV12 pod. The punktfunk/1 plane opts in via
|
||||||
|
|||||||
@@ -134,6 +134,13 @@ pub trait VirtualDisplay: Send {
|
|||||||
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint
|
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint
|
||||||
/// per-monitor EDIDs today (the Linux compositors' virtual outputs take no EDID from us).
|
/// per-monitor EDIDs today (the Linux compositors' virtual outputs take no EDID from us).
|
||||||
fn set_client_hdr(&mut self, _hdr: Option<punktfunk_core::quic::HdrMeta>) {}
|
fn set_client_hdr(&mut self, _hdr: Option<punktfunk_core::quic::HdrMeta>) {}
|
||||||
|
/// Ask the backend for an out-of-band HARDWARE CURSOR on the created output (the M2c cursor
|
||||||
|
/// channel): the compositor/OS stops compositing the pointer into captured frames and the
|
||||||
|
/// capture layer surfaces shape/position separately. Carried on the backend instance; set
|
||||||
|
/// once before [`create`](Self::create). Default: no-op — only the Windows pf-vdisplay
|
||||||
|
/// backend (IddCx hardware cursor, driver proto v5) implements it; the Linux portal path
|
||||||
|
/// gets the same split from `SPA_META_Cursor` without asking.
|
||||||
|
fn set_hw_cursor(&mut self, _on: bool) {}
|
||||||
/// The stable identity slot the backend resolved for the most recent [`create`](Self::create) —
|
/// The stable identity slot the backend resolved for the most recent [`create`](Self::create) —
|
||||||
/// the per-client id the identity policy assigned (`Some`), or `None` for shared/anonymous. The
|
/// the per-client id the identity policy assigned (`Some`), or `None` for shared/anonymous. The
|
||||||
/// registry reads it right after `create` to key the display's group **arrangement** (manual
|
/// registry reads it right after `create` to key the display's group **arrangement** (manual
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ struct Monitor {
|
|||||||
/// selectable). The pin is never re-issued on reuse, so this is what the driver still renders
|
/// selectable). The pin is never re-issued on reuse, so this is what the driver still renders
|
||||||
/// on — [`warn_if_pick_moved`] compares the CURRENT pick against it.
|
/// on — [`warn_if_pick_moved`] compares the CURRENT pick against it.
|
||||||
render_pin: Option<LUID>,
|
render_pin: Option<LUID>,
|
||||||
|
/// This monitor was ADDed with the v5 hardware-cursor flag (already driver-proto-gated) —
|
||||||
|
/// preserved across a re-arrival resize so the recreated monitor keeps the cursor channel.
|
||||||
|
hw_cursor: bool,
|
||||||
/// The driver's WUDFHost pid (from the ADD reply) — carried into [`WinCaptureTarget`] so the
|
/// The driver's WUDFHost pid (from the ADD reply) — carried into [`WinCaptureTarget`] so the
|
||||||
/// IDD-push capturer knows where to duplicate the sealed frame channel's handles. The SAME
|
/// IDD-push capturer knows where to duplicate the sealed frame channel's handles. The SAME
|
||||||
/// process for every parallel monitor (one devnode → one WUDFHost hosts all publishers), which
|
/// process for every parallel monitor (one devnode → one WUDFHost hosts all publishers), which
|
||||||
@@ -267,6 +270,20 @@ pub fn vdm() -> &'static VirtualDisplayManager {
|
|||||||
/// for the process lifetime — a dead one is RETIRED (kept alive, see [`DeviceSlot`]), so a stale copy
|
/// for the process lifetime — a dead one is RETIRED (kept alive, see [`DeviceSlot`]), so a stale copy
|
||||||
/// can only fail IOCTLs, never dangle. `None` before the first backend open — impossible for a
|
/// can only fail IOCTLs, never dangle. `None` before the first backend open — impossible for a
|
||||||
/// capturer, which only exists on a monitor the manager created.
|
/// capturer, which only exists on a monitor the manager created.
|
||||||
|
/// Can this host's pf-vdisplay driver run the v5 hardware-cursor channel? Reads the
|
||||||
|
/// handshake-latched protocol version, opening the control device once if no session has
|
||||||
|
/// opened it yet this service run (the same open every session performs anyway) — so the
|
||||||
|
/// Welcome-time capability decision never guesses. `false` when the driver is missing/stale.
|
||||||
|
pub fn hw_cursor_capable() -> bool {
|
||||||
|
let m = vdm();
|
||||||
|
let v = m.driver_proto.load(Ordering::Relaxed);
|
||||||
|
if v != 0 {
|
||||||
|
return v >= 5;
|
||||||
|
}
|
||||||
|
let _ = m.ensure_device();
|
||||||
|
m.driver_proto.load(Ordering::Relaxed) >= 5
|
||||||
|
}
|
||||||
|
|
||||||
pub fn control_device_handle() -> Option<HANDLE> {
|
pub fn control_device_handle() -> Option<HANDLE> {
|
||||||
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
||||||
}
|
}
|
||||||
@@ -390,6 +407,7 @@ impl VirtualDisplayManager {
|
|||||||
mode: Mode,
|
mode: Mode,
|
||||||
client_fp: Option<[u8; 32]>,
|
client_fp: Option<[u8; 32]>,
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
hw_cursor: bool,
|
||||||
quit: Option<Arc<AtomicBool>>,
|
quit: Option<Arc<AtomicBool>>,
|
||||||
) -> Result<VirtualOutput> {
|
) -> Result<VirtualOutput> {
|
||||||
// Console-session guard: a host outside the ACTIVE console session cannot drive the display
|
// Console-session guard: a host outside the ACTIVE console session cannot drive the display
|
||||||
@@ -622,7 +640,9 @@ impl VirtualDisplayManager {
|
|||||||
// SAFETY: `create_monitor` requires `dev` to be a valid control handle; `dev` is the handle
|
// SAFETY: `create_monitor` requires `dev` to be a valid control handle; `dev` is the handle
|
||||||
// `ensure_device()` returned above (cached handles are never closed — a dead one is retired,
|
// `ensure_device()` returned above (cached handles are never closed — a dead one is retired,
|
||||||
// kept alive; see `DeviceSlot`), and we hold the `state` lock.
|
// kept alive; see `DeviceSlot`), and we hold the `state` lock.
|
||||||
let mon = match unsafe { self.create_monitor(dev, mode, slot, client_hdr, &mut inner) } {
|
let mon = match unsafe {
|
||||||
|
self.create_monitor(dev, mode, slot, client_hdr, hw_cursor, &mut inner)
|
||||||
|
} {
|
||||||
// The cached device died under us (driver upgrade / WUDFHost restart, detected only
|
// The cached device died under us (driver upgrade / WUDFHost restart, detected only
|
||||||
// now — e.g. the host sat idle past the pinger-less window). Retire it, reopen, and
|
// now — e.g. the host sat idle past the pinger-less window). Retire it, reopen, and
|
||||||
// retry ONCE so the reconnect-after-driver-restart succeeds first try instead of
|
// retry ONCE so the reconnect-after-driver-restart succeeds first try instead of
|
||||||
@@ -635,7 +655,7 @@ impl VirtualDisplayManager {
|
|||||||
);
|
);
|
||||||
// SAFETY: as above — `dev` is the handle the reopening `ensure_device` just
|
// SAFETY: as above — `dev` is the handle the reopening `ensure_device` just
|
||||||
// returned, and the `state` lock is still held.
|
// returned, and the `state` lock is still held.
|
||||||
unsafe { self.create_monitor(dev, mode, slot, client_hdr, &mut inner)? }
|
unsafe { self.create_monitor(dev, mode, slot, client_hdr, hw_cursor, &mut inner)? }
|
||||||
}
|
}
|
||||||
r => r?,
|
r => r?,
|
||||||
};
|
};
|
||||||
@@ -861,6 +881,7 @@ impl VirtualDisplayManager {
|
|||||||
mode: Mode,
|
mode: Mode,
|
||||||
slot: u32,
|
slot: u32,
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
hw_cursor: bool,
|
||||||
inner: &mut MgrInner,
|
inner: &mut MgrInner,
|
||||||
) -> Result<Monitor> {
|
) -> Result<Monitor> {
|
||||||
// The slot id doubles as the driver-preferred monitor id (EDID serial / ConnectorIndex), so
|
// The slot id doubles as the driver-preferred monitor id (EDID serial / ConnectorIndex), so
|
||||||
@@ -868,13 +889,17 @@ impl VirtualDisplayManager {
|
|||||||
// `0` (anonymous) = the driver auto-allocates the lowest-free id.
|
// `0` (anonymous) = the driver auto-allocates the lowest-free id.
|
||||||
let preferred_id = slot;
|
let preferred_id = slot;
|
||||||
let render_pin = resolve_render_pin();
|
let render_pin = resolve_render_pin();
|
||||||
|
// Hardware cursor only against a driver that implements the v5 channel: an older driver
|
||||||
|
// ignores the AddRequest field anyway (composited cursor), but gating here keeps the
|
||||||
|
// capture layer from creating + delivering a section nobody will ever publish into.
|
||||||
|
let hw_cursor = hw_cursor && self.driver_proto.load(Ordering::Relaxed) >= 5;
|
||||||
// SAFETY: `create_monitor`'s own `# Safety` contract guarantees `dev` is the live control
|
// SAFETY: `create_monitor`'s own `# Safety` contract guarantees `dev` is the live control
|
||||||
// handle; we forward it unchanged to `add_monitor`, whose precondition is exactly that.
|
// handle; we forward it unchanged to `add_monitor`, whose precondition is exactly that.
|
||||||
// `render_pin` is an `Option<LUID>` by value (plain `Copy`), so no borrowed memory
|
// `render_pin` is an `Option<LUID>` by value (plain `Copy`), so no borrowed memory
|
||||||
// crosses the call.
|
// crosses the call.
|
||||||
let added = unsafe {
|
let added = unsafe {
|
||||||
self.driver
|
self.driver
|
||||||
.add_monitor(dev, mode, render_pin, preferred_id, client_hdr)?
|
.add_monitor(dev, mode, render_pin, preferred_id, client_hdr, hw_cursor)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
|
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
|
||||||
@@ -1061,6 +1086,7 @@ impl VirtualDisplayManager {
|
|||||||
resolved_monitor_id: added.resolved_monitor_id,
|
resolved_monitor_id: added.resolved_monitor_id,
|
||||||
position: (0, 0),
|
position: (0, 0),
|
||||||
gen: self.gen.fetch_add(1, Ordering::Relaxed),
|
gen: self.gen.fetch_add(1, Ordering::Relaxed),
|
||||||
|
hw_cursor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1257,7 @@ impl VirtualDisplayManager {
|
|||||||
// values passed by value — no borrow crosses the call.
|
// values passed by value — no borrow crosses the call.
|
||||||
let added = unsafe {
|
let added = unsafe {
|
||||||
self.driver
|
self.driver
|
||||||
.add_monitor(dev, mode, render_pin, slot, client_hdr)
|
.add_monitor(dev, mode, render_pin, slot, client_hdr, old.hw_cursor)
|
||||||
.context("re-arrival ADD at the new mode")?
|
.context("re-arrival ADD at the new mode")?
|
||||||
};
|
};
|
||||||
self.ensure_pinger();
|
self.ensure_pinger();
|
||||||
@@ -1283,6 +1309,7 @@ impl VirtualDisplayManager {
|
|||||||
resolved_monitor_id: added.resolved_monitor_id,
|
resolved_monitor_id: added.resolved_monitor_id,
|
||||||
position: old.position,
|
position: old.position,
|
||||||
gen: old.gen,
|
gen: old.gen,
|
||||||
|
hw_cursor: old.hw_cursor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ pub(crate) trait VdisplayDriver: Send + Sync {
|
|||||||
render_luid: Option<LUID>,
|
render_luid: Option<LUID>,
|
||||||
preferred_monitor_id: u32,
|
preferred_monitor_id: u32,
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
hw_cursor: bool,
|
||||||
) -> Result<AddedMonitor>;
|
) -> Result<AddedMonitor>;
|
||||||
/// Refresh the LIVE monitor `key`'s advertised mode list to lead with `mode` (the in-place
|
/// Refresh the LIVE monitor `key`'s advertised mode list to lead with `mode` (the in-place
|
||||||
/// mid-stream resize, latency plan P2 — pf-vdisplay `IOCTL_UPDATE_MODES`, driver protocol v4).
|
/// mid-stream resize, latency plan P2 — pf-vdisplay `IOCTL_UPDATE_MODES`, driver protocol v4).
|
||||||
|
|||||||
@@ -234,6 +234,30 @@ pub unsafe fn send_frame_channel(dev: HANDLE, req: &control::SetFrameChannelRequ
|
|||||||
.context("pf-vdisplay SET_FRAME_CHANNEL")
|
.context("pf-vdisplay SET_FRAME_CHANNEL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deliver a monitor's hardware-cursor section (`IOCTL_SET_CURSOR_CHANNEL`, proto v5) — the
|
||||||
|
/// cursor sibling of [`send_frame_channel`], same delivery/ownership contract.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `dev` must be a live pf-vdisplay control handle (see [`super::manager::control_device_handle`]).
|
||||||
|
pub unsafe fn send_cursor_channel(
|
||||||
|
dev: HANDLE,
|
||||||
|
req: &control::SetCursorChannelRequest,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut none: [u8; 0] = [];
|
||||||
|
// SAFETY: per this fn's contract `dev` is the live control handle; `bytes_of(req)` borrows the
|
||||||
|
// caller's request across this synchronous call; no output buffer.
|
||||||
|
unsafe {
|
||||||
|
ioctl(
|
||||||
|
dev,
|
||||||
|
control::IOCTL_SET_CURSOR_CHANNEL,
|
||||||
|
bytemuck::bytes_of(req),
|
||||||
|
&mut none,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map(|_| ())
|
||||||
|
.context("pf-vdisplay SET_CURSOR_CHANNEL")
|
||||||
|
}
|
||||||
|
|
||||||
/// RAII over a SetupAPI device-info list: every exit path of [`open_device`] destroys it (the error
|
/// RAII over a SetupAPI device-info list: every exit path of [`open_device`] destroys it (the error
|
||||||
/// paths used to leak one `HDEVINFO` per failed open — and a driverless / mid-upgrade box probes
|
/// paths used to leak one `HDEVINFO` per failed open — and a driverless / mid-upgrade box probes
|
||||||
/// repeatedly).
|
/// repeatedly).
|
||||||
@@ -460,6 +484,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
render_luid: Option<LUID>,
|
render_luid: Option<LUID>,
|
||||||
preferred_monitor_id: u32,
|
preferred_monitor_id: u32,
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
hw_cursor: bool,
|
||||||
) -> Result<AddedMonitor> {
|
) -> Result<AddedMonitor> {
|
||||||
let session_id = next_session_id();
|
let session_id = next_session_id();
|
||||||
// The client display's volume rides into the monitor's EDID CTA HDR block; all-zero =
|
// The client display's volume rides into the monitor's EDID CTA HDR block; all-zero =
|
||||||
@@ -485,7 +510,11 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
max_luminance_nits,
|
max_luminance_nits,
|
||||||
max_frame_avg_nits,
|
max_frame_avg_nits,
|
||||||
min_luminance_millinits,
|
min_luminance_millinits,
|
||||||
_reserved: 0,
|
// v5 cursor channel: the driver declares an IddCx hardware cursor for this monitor
|
||||||
|
// (DWM stops compositing the pointer into the frame); the capture layer delivers the
|
||||||
|
// CursorShm section right after its ring. Zero toward older drivers is harmless —
|
||||||
|
// the host only sets this when the handshake-reported proto is >= 5.
|
||||||
|
hw_cursor: hw_cursor as u32,
|
||||||
};
|
};
|
||||||
// SET_RENDER_ADAPTER (opt-in; pf-vdisplay IMPLEMENTS it). Non-fatal on failure: the driver reports
|
// SET_RENDER_ADAPTER (opt-in; pf-vdisplay IMPLEMENTS it). Non-fatal on failure: the driver reports
|
||||||
// its real render LUID in the shared header, so the host binds correctly even if this is ignored.
|
// its real render LUID in the shared header, so the host binds correctly even if this is ignored.
|
||||||
@@ -682,6 +711,10 @@ pub struct PfVdisplayDisplay {
|
|||||||
/// freshly created monitor's EDID advertises this volume so host apps tone-map to the client's
|
/// freshly created monitor's EDID advertises this volume so host apps tone-map to the client's
|
||||||
/// real panel.
|
/// real panel.
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
/// Declare an IddCx hardware cursor on the created monitor (the M2c cursor channel). Set by
|
||||||
|
/// [`set_hw_cursor`](VirtualDisplay::set_hw_cursor) before `create`; only honored when the
|
||||||
|
/// driver handshake reported proto >= 5.
|
||||||
|
hw_cursor: bool,
|
||||||
/// The session's deliberate-quit flag (`None` = no signal → the linger policy applies). Set by
|
/// The session's deliberate-quit flag (`None` = no signal → the linger policy applies). Set by
|
||||||
/// [`set_quit_flag`](VirtualDisplay::set_quit_flag) before `create`; rides into every lease this
|
/// [`set_quit_flag`](VirtualDisplay::set_quit_flag) before `create`; rides into every lease this
|
||||||
/// backend mints so a user "stop" tears the monitor down immediately instead of lingering.
|
/// backend mints so a user "stop" tears the monitor down immediately instead of lingering.
|
||||||
@@ -694,6 +727,7 @@ impl PfVdisplayDisplay {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
client_fp: None,
|
client_fp: None,
|
||||||
client_hdr: None,
|
client_hdr: None,
|
||||||
|
hw_cursor: false,
|
||||||
quit: None,
|
quit: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -712,12 +746,22 @@ impl VirtualDisplay for PfVdisplayDisplay {
|
|||||||
self.client_hdr = hdr;
|
self.client_hdr = hdr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_hw_cursor(&mut self, on: bool) {
|
||||||
|
self.hw_cursor = on;
|
||||||
|
}
|
||||||
|
|
||||||
fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
|
fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
|
||||||
self.quit = Some(quit);
|
self.quit = Some(quit);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||||
super::manager::vdm().acquire(mode, self.client_fp, self.client_hdr, self.quit.clone())
|
super::manager::vdm().acquire(
|
||||||
|
mode,
|
||||||
|
self.client_fp,
|
||||||
|
self.client_hdr,
|
||||||
|
self.hw_cursor,
|
||||||
|
self.quit.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,17 +848,12 @@ mod tests {
|
|||||||
// Live-run diagnostics: surface the manager/backend tracing (activation ladder, settle
|
// Live-run diagnostics: surface the manager/backend tracing (activation ladder, settle
|
||||||
// waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the
|
// waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the
|
||||||
// first on-glass run blind.
|
// first on-glass run blind.
|
||||||
let _ = tracing_subscriber::fmt()
|
// (tracing-subscriber is not a dep of this crate — run the host binary for traced runs.)
|
||||||
.with_env_filter(
|
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| "debug".into()),
|
|
||||||
)
|
|
||||||
.try_init();
|
|
||||||
// Context probe: can this process see the CCD active-path set at all? (`None` = the query
|
// Context probe: can this process see the CCD active-path set at all? (`None` = the query
|
||||||
// itself fails in this session/window-station — the whole ladder would be blind, and a
|
// itself fails in this session/window-station — the whole ladder would be blind, and a
|
||||||
// "monitor never activated" verdict would be an artifact of the test context.)
|
// "monitor never activated" verdict would be an artifact of the test context.)
|
||||||
// SAFETY: CCD query over an owned empty slice (test-only diagnostics).
|
// SAFETY: CCD query over an owned empty slice (test-only diagnostics).
|
||||||
let active0 = unsafe { crate::win_display::count_other_active(&[]) };
|
let active0 = unsafe { pf_win_display::win_display::count_other_active(&[]) };
|
||||||
println!("spike: CCD active paths visible before create: {active0:?}");
|
println!("spike: CCD active paths visible before create: {active0:?}");
|
||||||
let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay");
|
let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay");
|
||||||
let first = vd
|
let first = vd
|
||||||
@@ -847,7 +886,7 @@ mod tests {
|
|||||||
.target_id;
|
.target_id;
|
||||||
let in_place = t1 == t2;
|
let in_place = t1 == t2;
|
||||||
// SAFETY: CCD query over a Copy target id (test-only diagnostics).
|
// SAFETY: CCD query over a Copy target id (test-only diagnostics).
|
||||||
let active = unsafe { crate::win_display::active_resolution(t2) };
|
let active = unsafe { pf_win_display::win_display::active_resolution(t2) };
|
||||||
println!(
|
println!(
|
||||||
"in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \
|
"in-place resize spike: in_place={in_place} (target {t1} -> {t2}) took {resize_ms} ms, \
|
||||||
active resolution now {active:?}"
|
active resolution now {active:?}"
|
||||||
|
|||||||
@@ -157,6 +157,23 @@ pub fn capture_virtual_output(
|
|||||||
// proactively enables advanced color and selects the per-frame conversion. There is NO fallback:
|
// proactively enables advanced color and selects the per-frame conversion. There is NO fallback:
|
||||||
// if it can't open or the driver doesn't attach, the session fails cleanly and the client
|
// if it can't open or the driver doesn't attach, the session fails cleanly and the client
|
||||||
// reconnects.
|
// reconnects.
|
||||||
|
// Cursor-forward sessions (M2c): hand the capturer the v5 cursor-channel delivery closure —
|
||||||
|
// its presence opts the session in (the capturer creates + delivers the CursorShm section,
|
||||||
|
// the driver declares the IddCx hardware cursor). Built exactly like `sender` above.
|
||||||
|
let cursor_sender: Option<pf_capture::CursorChannelSender> = want.hw_cursor.then(|| {
|
||||||
|
std::sync::Arc::new(
|
||||||
|
move |req: &pf_driver_proto::control::SetCursorChannelRequest| {
|
||||||
|
// SAFETY: `control_raw` is the pf-vdisplay control handle resolved above; it is
|
||||||
|
// never closed for the process lifetime (`send_cursor_channel`'s precondition).
|
||||||
|
unsafe {
|
||||||
|
crate::vdisplay::driver::send_cursor_channel(
|
||||||
|
windows::Win32::Foundation::HANDLE(control_raw as *mut core::ffi::c_void),
|
||||||
|
req,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) as pf_capture::CursorChannelSender
|
||||||
|
});
|
||||||
pf_capture::open_idd_push(
|
pf_capture::open_idd_push(
|
||||||
target,
|
target,
|
||||||
pref,
|
pref,
|
||||||
@@ -165,6 +182,7 @@ pub fn capture_virtual_output(
|
|||||||
want.pyrowave,
|
want.pyrowave,
|
||||||
keep,
|
keep,
|
||||||
sender,
|
sender,
|
||||||
|
cursor_sender,
|
||||||
)
|
)
|
||||||
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
|
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,35 @@ use super::*;
|
|||||||
|
|
||||||
/// Whether this session forwards the cursor out-of-band (design/remote-desktop-sweep.md M2):
|
/// Whether this session forwards the cursor out-of-band (design/remote-desktop-sweep.md M2):
|
||||||
/// the client asked ([`CLIENT_CAP_CURSOR`](punktfunk_core::quic::CLIENT_CAP_CURSOR)) AND the
|
/// the client asked ([`CLIENT_CAP_CURSOR`](punktfunk_core::quic::CLIENT_CAP_CURSOR)) AND the
|
||||||
/// capture path can deliver cursor metadata separately from the frame — today that is the
|
/// capture path can deliver cursor metadata separately from the frame — the Linux portal
|
||||||
/// Linux portal `SPA_META_Cursor` path only: not gamescope (its capture paints no cursor at
|
/// `SPA_META_Cursor` path (not gamescope, whose capture paints no cursor at all), or Windows
|
||||||
/// all), not Windows (DWM composites into the IDD frame — M2c). THE single predicate: the
|
/// with a proto-v5 pf-vdisplay driver (the IddCx hardware-cursor channel, M2c). THE single
|
||||||
/// Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off wiring both read it,
|
/// predicate: the Welcome's `HOST_CAP_CURSOR` bit and the session's forwarding/blend-off
|
||||||
/// so they can never disagree.
|
/// wiring both read it, so they can never disagree.
|
||||||
pub(super) fn cursor_forward(
|
pub(super) fn cursor_forward(
|
||||||
client_caps: u8,
|
client_caps: u8,
|
||||||
compositor: Option<crate::vdisplay::Compositor>,
|
compositor: Option<crate::vdisplay::Compositor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
cfg!(target_os = "linux")
|
if client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR == 0 {
|
||||||
&& client_caps & punktfunk_core::quic::CLIENT_CAP_CURSOR != 0
|
return false;
|
||||||
&& compositor.is_some_and(|c| c != crate::vdisplay::Compositor::Gamescope)
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
compositor.is_some_and(|c| c != crate::vdisplay::Compositor::Gamescope)
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Windows (M2c): the pf-vdisplay driver must speak the v5 hardware-cursor channel —
|
||||||
|
// DWM composites the pointer into the IDD frame otherwise, and forwarding a second
|
||||||
|
// copy would double it. The probe latches by opening the control device once.
|
||||||
|
let _ = compositor;
|
||||||
|
crate::vdisplay::manager::hw_cursor_capable()
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
let _ = compositor;
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the Hello→Welcome→Start negotiation. Borrows the control streams (the caller keeps them for
|
/// Run the Hello→Welcome→Start negotiation. Borrows the control streams (the caller keeps them for
|
||||||
@@ -510,6 +527,9 @@ pub(super) async fn negotiate(
|
|||||||
let (ctx_tx, ctx_rx) = std::sync::mpsc::sync_channel::<SessionContext>(1);
|
let (ctx_tx, ctx_rx) = std::sync::mpsc::sync_channel::<SessionContext>(1);
|
||||||
let client_identity = endpoint::peer_fingerprint(conn);
|
let client_identity = endpoint::peer_fingerprint(conn);
|
||||||
let client_hdr = hello.display_hdr;
|
let client_hdr = hello.display_hdr;
|
||||||
|
// Same predicate the Welcome's HOST_CAP_CURSOR bit used — the prepared display and
|
||||||
|
// the session wiring must agree with what we just advertised.
|
||||||
|
let cursor_fw = cursor_forward(hello.client_caps, Some(comp));
|
||||||
let (mode, shard_payload) = (hello.mode, welcome.shard_payload);
|
let (mode, shard_payload) = (hello.mode, welcome.shard_payload);
|
||||||
let trace = bringup.clone();
|
let trace = bringup.clone();
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
@@ -520,6 +540,7 @@ pub(super) async fn negotiate(
|
|||||||
mode,
|
mode,
|
||||||
client_identity,
|
client_identity,
|
||||||
client_hdr,
|
client_hdr,
|
||||||
|
cursor_fw,
|
||||||
bitrate_kbps,
|
bitrate_kbps,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
|
|||||||
@@ -999,6 +999,7 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
// the client is not drawing it locally (the M2 cursor channel — blending too would
|
// the client is not drawing it locally (the M2 cursor channel — blending too would
|
||||||
// show it twice).
|
// show it twice).
|
||||||
ctx.compositor != pf_vdisplay::Compositor::Gamescope && !ctx.cursor_forward,
|
ctx.compositor != pf_vdisplay::Compositor::Gamescope && !ctx.cursor_forward,
|
||||||
|
ctx.cursor_forward,
|
||||||
);
|
);
|
||||||
// PyroWave rides the datagram-aligned wire mode (§4.4): every encoder this session opens
|
// PyroWave rides the datagram-aligned wire mode (§4.4): every encoder this session opens
|
||||||
// packetizes at the negotiated shard payload, so a lost datagram costs blocks, not frames.
|
// packetizes at the negotiated shard payload, so a lost datagram costs blocks, not frames.
|
||||||
@@ -1089,6 +1090,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
// panel instead of the driver's built-in ~1000-nit placeholder. No-op on Linux
|
// panel instead of the driver's built-in ~1000-nit placeholder. No-op on Linux
|
||||||
// backends and for older/SDR clients.
|
// backends and for older/SDR clients.
|
||||||
vd.set_client_hdr(client_hdr);
|
vd.set_client_hdr(client_hdr);
|
||||||
|
// Cursor-forward sessions ask the backend for an out-of-band hardware cursor
|
||||||
|
// (Windows pf-vdisplay / IddCx; no-op on Linux — the portal already separates it).
|
||||||
|
vd.set_hw_cursor(cursor_forward);
|
||||||
// Deliberate-quit wiring (Windows pf-vdisplay; no-op elsewhere): every lease the
|
// Deliberate-quit wiring (Windows pf-vdisplay; no-op elsewhere): every lease the
|
||||||
// backend mints — the retry-hold below AND the capturer's — carries the session's quit
|
// backend mints — the retry-hold below AND the capturer's — carries the session's quit
|
||||||
// flag, so a user "stop" (⌘D → the QUIT close code) tears the virtual monitor down the
|
// flag, so a user "stop" (⌘D → the QUIT close code) tears the virtual monitor down the
|
||||||
@@ -2007,10 +2011,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
}
|
}
|
||||||
// Cursor channel (M2): every iteration — new frame OR repeat — states the pointer
|
// Cursor channel (M2): every iteration — new frame OR repeat — states the pointer
|
||||||
// (self-healing under datagram loss) and forwards a changed shape via the control
|
// (self-healing under datagram loss) and forwards a changed shape via the control
|
||||||
// bridge. `frame` is the newest bound frame either way. A hidden-but-known pointer
|
// bridge. A hidden-but-known pointer (overlay with `visible: false`) is the M3
|
||||||
// (overlay with `visible: false`) is the M3 relative-mode hint.
|
// relative-mode hint. The capturer's LIVE cursor (the Windows hardware-cursor channel,
|
||||||
|
// where pointer-only moves produce no frame) outranks the frame-attached overlay
|
||||||
|
// (the Linux portal path); `frame` is the newest bound frame either way.
|
||||||
if let Some(fwd) = cursor_fwd.as_mut() {
|
if let Some(fwd) = cursor_fwd.as_mut() {
|
||||||
fwd.tick(frame.cursor.as_ref(), &conn, &cursor_shape_tx);
|
let live = capturer.cursor();
|
||||||
|
fwd.tick(
|
||||||
|
live.as_ref().or(frame.cursor.as_ref()),
|
||||||
|
&conn,
|
||||||
|
&cursor_shape_tx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// The overlay now surfaces hidden pointers too (for the hint above) — strip them
|
// The overlay now surfaces hidden pointers too (for the hint above) — strip them
|
||||||
// HERE, after forwarding, so no blend path ever draws an invisible cursor.
|
// HERE, after forwarding, so no blend path ever draws an invisible cursor.
|
||||||
@@ -2675,6 +2686,7 @@ pub(super) fn prepare_display(
|
|||||||
mode: punktfunk_core::Mode,
|
mode: punktfunk_core::Mode,
|
||||||
client_identity: Option<[u8; 32]>,
|
client_identity: Option<[u8; 32]>,
|
||||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
|
cursor_forward: bool,
|
||||||
bitrate_kbps: u32,
|
bitrate_kbps: u32,
|
||||||
bit_depth: u8,
|
bit_depth: u8,
|
||||||
chroma: crate::encode::ChromaFormat,
|
chroma: crate::encode::ChromaFormat,
|
||||||
@@ -2691,7 +2703,8 @@ pub(super) fn prepare_display(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
codec,
|
codec,
|
||||||
compositor != pf_vdisplay::Compositor::Gamescope,
|
compositor != pf_vdisplay::Compositor::Gamescope && !cursor_forward,
|
||||||
|
cursor_forward,
|
||||||
);
|
);
|
||||||
if codec == crate::encode::Codec::PyroWave {
|
if codec == crate::encode::Codec::PyroWave {
|
||||||
plan.wire_chunk = Some(shard_payload as usize);
|
plan.wire_chunk = Some(shard_payload as usize);
|
||||||
@@ -2699,6 +2712,7 @@ pub(super) fn prepare_display(
|
|||||||
let mut vd = crate::vdisplay::open(compositor)?;
|
let mut vd = crate::vdisplay::open(compositor)?;
|
||||||
vd.set_client_identity(client_identity);
|
vd.set_client_identity(client_identity);
|
||||||
vd.set_client_hdr(client_hdr);
|
vd.set_client_hdr(client_hdr);
|
||||||
|
vd.set_hw_cursor(cursor_forward);
|
||||||
vd.set_quit_flag(quit.clone());
|
vd.set_quit_flag(quit.clone());
|
||||||
// Slot-scoped setup serialization + reconnect preempt — see the inline arm in
|
// Slot-scoped setup serialization + reconnect preempt — see the inline arm in
|
||||||
// `virtual_stream` for the full rationale; released when this fn returns.
|
// `virtual_stream` for the full rationale; released when this fn returns.
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ pub struct SessionPlan {
|
|||||||
/// Encoders whose fast path cannot blend (the Vulkan EFC RGB-direct source) stay on their
|
/// Encoders whose fast path cannot blend (the Vulkan EFC RGB-direct source) stay on their
|
||||||
/// blending path when this is set, so the pointer never silently vanishes from the stream.
|
/// blending path when this is set, so the pointer never silently vanishes from the stream.
|
||||||
pub cursor_blend: bool,
|
pub cursor_blend: bool,
|
||||||
|
/// The session negotiated the cursor-forward channel (M2/M2c): the client draws the pointer
|
||||||
|
/// locally, so `cursor_blend` is off AND (on Windows) the capturer sets the driver's
|
||||||
|
/// hardware cursor up via [`OutputFormat::hw_cursor`](pf_frame::OutputFormat).
|
||||||
|
pub cursor_forward: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionPlan {
|
impl SessionPlan {
|
||||||
@@ -118,6 +122,7 @@ impl SessionPlan {
|
|||||||
chroma: crate::encode::ChromaFormat,
|
chroma: crate::encode::ChromaFormat,
|
||||||
codec: crate::encode::Codec,
|
codec: crate::encode::Codec,
|
||||||
cursor_blend: bool,
|
cursor_blend: bool,
|
||||||
|
cursor_forward: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
SessionPlan {
|
SessionPlan {
|
||||||
capture: CaptureBackend::resolve(),
|
capture: CaptureBackend::resolve(),
|
||||||
@@ -129,6 +134,7 @@ impl SessionPlan {
|
|||||||
codec,
|
codec,
|
||||||
wire_chunk: None,
|
wire_chunk: None,
|
||||||
cursor_blend,
|
cursor_blend,
|
||||||
|
cursor_forward,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +177,7 @@ impl SessionPlan {
|
|||||||
crate::capture::OutputFormat {
|
crate::capture::OutputFormat {
|
||||||
gpu,
|
gpu,
|
||||||
hdr: self.hdr,
|
hdr: self.hdr,
|
||||||
|
hw_cursor: self.cursor_forward,
|
||||||
// 4:4:4 needs a full-chroma source: on Windows this keeps the capturer on RGB (not the
|
// 4:4:4 needs a full-chroma source: on Windows this keeps the capturer on RGB (not the
|
||||||
// default NV12/P010 video-engine output) so NVENC can CSC to 4:4:4.
|
// default NV12/P010 video-engine output) so NVENC can CSC to 4:4:4.
|
||||||
chroma_444: self.chroma.is_444(),
|
chroma_444: self.chroma.is_444(),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ thiserror = "2.0"
|
|||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
features = [
|
features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_Graphics_Direct3D",
|
"Win32_Graphics_Direct3D",
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
|||||||
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
|
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
|
||||||
// SAFETY: `request` is the framework WDFREQUEST.
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) },
|
control::IOCTL_UPDATE_MODES => unsafe { update_modes(request) },
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
control::IOCTL_SET_CURSOR_CHANNEL => unsafe { set_cursor_channel(request) },
|
||||||
_ => complete(request, STATUS_NOT_FOUND),
|
_ => complete(request, STATUS_NOT_FOUND),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,6 +150,7 @@ unsafe fn add(request: WDFREQUEST) {
|
|||||||
max_frame_avg_nits: req.max_frame_avg_nits,
|
max_frame_avg_nits: req.max_frame_avg_nits,
|
||||||
min_millinits: req.min_luminance_millinits,
|
min_millinits: req.min_luminance_millinits,
|
||||||
},
|
},
|
||||||
|
req.hw_cursor != 0,
|
||||||
) else {
|
) else {
|
||||||
complete(request, STATUS_NOT_FOUND);
|
complete(request, STATUS_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
@@ -174,6 +177,35 @@ unsafe fn add(request: WDFREQUEST) {
|
|||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// `request` is the framework `WDFREQUEST`.
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
/// `IOCTL_SET_CURSOR_CHANNEL` (v5): adopt a monitor's hardware-cursor section, declare the
|
||||||
|
/// hardware cursor to the OS, start the query→publish worker.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `request` is the framework `WDFREQUEST`.
|
||||||
|
unsafe fn set_cursor_channel(request: WDFREQUEST) {
|
||||||
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
|
let Some(req) = (unsafe { read_input::<control::SetCursorChannelRequest>(request) }) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ch) = crate::cursor_worker::CursorChannel::from_request(&req) else {
|
||||||
|
complete(request, STATUS_INVALID_PARAMETER);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match crate::monitor::set_cursor_channel(req.target_id, ch) {
|
||||||
|
Ok(()) => complete(request, STATUS_SUCCESS),
|
||||||
|
Err(ch) => {
|
||||||
|
dbglog!(
|
||||||
|
"[pf-vd] SET_CURSOR_CHANNEL: no hw-cursor monitor with target_id {} — rejecting",
|
||||||
|
req.target_id
|
||||||
|
);
|
||||||
|
// NOT adopted: the host's error path reaps the duplicated handle remotely.
|
||||||
|
ch.into_unowned();
|
||||||
|
complete(request, STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn set_frame_channel(request: WDFREQUEST) {
|
unsafe fn set_frame_channel(request: WDFREQUEST) {
|
||||||
// SAFETY: `request` is the framework WDFREQUEST.
|
// SAFETY: `request` is the framework WDFREQUEST.
|
||||||
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
|
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
//! IddCx hardware-cursor worker (proto v5, remote-desktop-sweep M2c).
|
||||||
|
//!
|
||||||
|
//! When the host ADDs a monitor with `hw_cursor` set and delivers a [`CursorShm`] section
|
||||||
|
//! (`IOCTL_SET_CURSOR_CHANNEL`), we declare a hardware cursor to the OS
|
||||||
|
//! (`IddCxMonitorSetupHardwareCursor`) — DWM then EXCLUDES the pointer from the desktop image
|
||||||
|
//! it renders into our swap-chain and instead signals our event on every cursor change. This
|
||||||
|
//! worker thread drains those signals (`IddCxMonitorQueryHardwareCursor`) and seqlock-publishes
|
||||||
|
//! shape + position + visibility into the host-created section; the host polls it at its
|
||||||
|
//! encode-tick pace (no event crosses the process boundary).
|
||||||
|
//!
|
||||||
|
//! Coordinates are published VERBATIM in the OS's desktop space (`IDARG_OUT_QUERY_HWCURSOR::X/Y`
|
||||||
|
//! = the shape's top-left, can be negative); the host subtracts its monitor's desktop origin.
|
||||||
|
//! Shape pixels are the OS's 32-bpp rows at `Pitch` — BGRA for ALPHA cursors, color+mask for
|
||||||
|
//! MASKED_COLOR — copied raw; the host converts (keeping this thread dumb and allocation-free
|
||||||
|
//! after startup).
|
||||||
|
|
||||||
|
use core::sync::atomic::{AtomicU32, Ordering, fence};
|
||||||
|
|
||||||
|
use pf_driver_proto::cursor::{
|
||||||
|
CURSOR_MAGIC, CURSOR_SHAPE_BYTES, CURSOR_SHAPE_MAX, CURSOR_SHAPE_OFFSET, CURSOR_SHM_SIZE,
|
||||||
|
CursorShm,
|
||||||
|
};
|
||||||
|
use wdk_iddcx::nt_success;
|
||||||
|
use wdk_sys::iddcx;
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0};
|
||||||
|
use windows::Win32::System::Memory::{
|
||||||
|
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::Threading::{CreateEventW, INFINITE, SetEvent, WaitForMultipleObjects};
|
||||||
|
|
||||||
|
/// The host's `IOCTL_SET_CURSOR_CHANNEL` delivery: the [`CursorShm`] mapping handle VALUE,
|
||||||
|
/// already duplicated into this WUDFHost process. Owning a `CursorChannel` means owning the
|
||||||
|
/// handle; `Drop` closes it unless [`into_unowned`](Self::into_unowned) disarmed that (the
|
||||||
|
/// not-adopted reject path, where the host reaps remotely) or the worker consumed it.
|
||||||
|
pub struct CursorChannel {
|
||||||
|
handle: u64,
|
||||||
|
owned: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorChannel {
|
||||||
|
pub fn from_request(req: &pf_driver_proto::control::SetCursorChannelRequest) -> Option<Self> {
|
||||||
|
if req.header_handle == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(CursorChannel {
|
||||||
|
handle: req.header_handle,
|
||||||
|
owned: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disarm the Drop (delivery rejected — the handle stays for the host to reap remotely).
|
||||||
|
pub fn into_unowned(mut self) {
|
||||||
|
self.owned = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for CursorChannel {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.owned && self.handle != 0 {
|
||||||
|
// SAFETY: we own this duplicated handle value; closing at most once (owned is our flag).
|
||||||
|
unsafe {
|
||||||
|
let _ = CloseHandle(HANDLE(self.handle as *mut core::ffi::c_void));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The live worker: stops + joins on drop (monitor departure / replacement).
|
||||||
|
pub struct CursorWorker {
|
||||||
|
stop: isize,
|
||||||
|
join: Option<std::thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: `stop` is an event handle value; the worker owns every other resource.
|
||||||
|
unsafe impl Send for CursorWorker {}
|
||||||
|
|
||||||
|
impl Drop for CursorWorker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: `stop` is our owned manual-reset event; signal + join, then close.
|
||||||
|
unsafe {
|
||||||
|
let _ = SetEvent(HANDLE(self.stop as *mut core::ffi::c_void));
|
||||||
|
}
|
||||||
|
if let Some(j) = self.join.take() {
|
||||||
|
let _ = j.join();
|
||||||
|
}
|
||||||
|
// SAFETY: the worker has exited; nothing else references the handle.
|
||||||
|
unsafe {
|
||||||
|
let _ = CloseHandle(HANDLE(self.stop as *mut core::ffi::c_void));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Declare the hardware cursor for `monitor` and start the query→publish worker over the
|
||||||
|
/// 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> {
|
||||||
|
// 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.
|
||||||
|
let view = unsafe {
|
||||||
|
MapViewOfFile(
|
||||||
|
mapping,
|
||||||
|
FILE_MAP_READ | FILE_MAP_WRITE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CURSOR_SHM_SIZE,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if view.Value.is_null() {
|
||||||
|
dbglog!("[pf-vd] cursor: MapViewOfFile failed — keeping composited cursor");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let shm = view.Value.cast::<CursorShm>();
|
||||||
|
// SAFETY: the view spans CURSOR_SHM_SIZE >= size_of::<CursorShm>(); reading the host stamp.
|
||||||
|
if unsafe { core::ptr::addr_of!((*shm).magic).read_volatile() } != CURSOR_MAGIC {
|
||||||
|
dbglog!("[pf-vd] cursor: section magic mismatch — rejecting");
|
||||||
|
// SAFETY: unmapping the view we just mapped.
|
||||||
|
unsafe {
|
||||||
|
let _ = UnmapViewOfFile(view);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-reset data event (the OS signals it per cursor update) + manual-reset stop event.
|
||||||
|
// SAFETY: plain event creation, no names, no security descriptor.
|
||||||
|
let (data_evt, stop_evt) = unsafe {
|
||||||
|
match (
|
||||||
|
CreateEventW(None, false, false, None),
|
||||||
|
CreateEventW(None, true, false, None),
|
||||||
|
) {
|
||||||
|
(Ok(d), Ok(s)) => (d, s),
|
||||||
|
_ => {
|
||||||
|
let _ = UnmapViewOfFile(view);
|
||||||
|
dbglog!("[pf-vd] cursor: event creation failed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let caps = iddcx::IDDCX_CURSOR_CAPS {
|
||||||
|
Size: core::mem::size_of::<iddcx::IDDCX_CURSOR_CAPS>() as u32,
|
||||||
|
// Alpha covers every modern cursor; XOR/monochrome shapes arrive converted to masked
|
||||||
|
// color, which the host approximates. No XOR plane emulation on our side.
|
||||||
|
ColorXorCursorSupport: iddcx::IDDCX_XOR_CURSOR_SUPPORT::IDDCX_XOR_CURSOR_SUPPORT_NONE,
|
||||||
|
MaxX: CURSOR_SHAPE_MAX,
|
||||||
|
MaxY: CURSOR_SHAPE_MAX,
|
||||||
|
AlphaCursorSupport: 1,
|
||||||
|
};
|
||||||
|
let setup = iddcx::IDARG_IN_SETUP_HWCURSOR {
|
||||||
|
CursorInfo: caps,
|
||||||
|
hNewCursorDataAvailable: data_evt.0.cast(),
|
||||||
|
};
|
||||||
|
// SAFETY: `monitor` is a live IddCx monitor (post-create); `setup` outlives the call; the
|
||||||
|
// OS duplicates the event handle for its own signaling.
|
||||||
|
let st = unsafe { wdk_iddcx::IddCxMonitorSetupHardwareCursor(monitor, &setup) };
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
let view_v = view.Value as usize;
|
||||||
|
let data_v = data_evt.0 as isize;
|
||||||
|
let stop_v = stop_evt.0 as isize;
|
||||||
|
let mapping_v = ch.handle;
|
||||||
|
ch.into_unowned(); // the worker owns the mapping handle from here (closed on exit below)
|
||||||
|
|
||||||
|
let join = std::thread::Builder::new()
|
||||||
|
.name("pf-vd-cursor".into())
|
||||||
|
.spawn(move || {
|
||||||
|
run_worker(monitor_v, view_v, data_v, stop_v);
|
||||||
|
// SAFETY: the worker is the sole owner of these at exit; close/unmap exactly once.
|
||||||
|
unsafe {
|
||||||
|
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||||
|
Value: view_v as *mut core::ffi::c_void,
|
||||||
|
});
|
||||||
|
let _ = CloseHandle(HANDLE(data_v as *mut core::ffi::c_void));
|
||||||
|
let _ = CloseHandle(HANDLE(mapping_v as *mut core::ffi::c_void));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(CursorWorker {
|
||||||
|
stop: stop_v,
|
||||||
|
join: Some(join),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The wait→query→publish loop. Exits when the stop event signals.
|
||||||
|
fn run_worker(monitor_v: usize, view_v: usize, data_v: isize, stop_v: isize) {
|
||||||
|
let monitor = monitor_v as iddcx::IDDCX_MONITOR;
|
||||||
|
let shm = view_v as *mut CursorShm;
|
||||||
|
let shape_dst = (view_v + CURSOR_SHAPE_OFFSET) as *mut u8;
|
||||||
|
let mut shape_buf = vec![0u8; CURSOR_SHAPE_BYTES];
|
||||||
|
let mut last_shape_id: u32 = 0;
|
||||||
|
let mut query_warned = false;
|
||||||
|
let handles = [
|
||||||
|
HANDLE(stop_v as *mut core::ffi::c_void),
|
||||||
|
HANDLE(data_v as *mut core::ffi::c_void),
|
||||||
|
];
|
||||||
|
loop {
|
||||||
|
// SAFETY: both handles are live for the worker's lifetime (owner drops after join).
|
||||||
|
let w = unsafe { WaitForMultipleObjects(&handles, false, INFINITE) };
|
||||||
|
if w == WAIT_OBJECT_0 {
|
||||||
|
return; // stop
|
||||||
|
}
|
||||||
|
if w.0 != WAIT_OBJECT_0.0 + 1 {
|
||||||
|
return; // wait failed — owner is tearing down
|
||||||
|
}
|
||||||
|
let in_args = iddcx::IDARG_IN_QUERY_HWCURSOR {
|
||||||
|
LastShapeId: last_shape_id,
|
||||||
|
ShapeBufferSizeInBytes: CURSOR_SHAPE_BYTES as u32,
|
||||||
|
pShapeBuffer: shape_buf.as_mut_ptr(),
|
||||||
|
};
|
||||||
|
// SAFETY: zero-init is a valid OUT arg (the OS writes every field it reports).
|
||||||
|
let mut out: iddcx::IDARG_OUT_QUERY_HWCURSOR = unsafe { core::mem::zeroed() };
|
||||||
|
// SAFETY: `monitor` is live (departure drops this worker FIRST), args outlive the call.
|
||||||
|
let st = unsafe { wdk_iddcx::IddCxMonitorQueryHardwareCursor(monitor, &in_args, &mut out) };
|
||||||
|
if !nt_success(st) {
|
||||||
|
if !query_warned {
|
||||||
|
query_warned = true;
|
||||||
|
dbglog!(
|
||||||
|
"[pf-vd] cursor: query failed 0x{:08x} (logged once)",
|
||||||
|
st as u32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Seqlock publish: odd → write → even. The header alone changes on position moves;
|
||||||
|
// shape bytes are only rewritten when the OS says the image changed, so a reader that
|
||||||
|
// skips unchanged shape_ids never observes torn pixels.
|
||||||
|
// SAFETY: `shm` points at the mapped CursorShm for the worker's lifetime.
|
||||||
|
let seq = unsafe { &*core::ptr::addr_of!((*shm).seq).cast::<AtomicU32>() };
|
||||||
|
let s = seq.load(Ordering::Relaxed);
|
||||||
|
seq.store(s.wrapping_add(1), Ordering::Relaxed); // odd = mid-update
|
||||||
|
fence(Ordering::Release);
|
||||||
|
// SAFETY: exclusive writer (single worker per section); plain volatile field writes.
|
||||||
|
unsafe {
|
||||||
|
core::ptr::addr_of_mut!((*shm).visible).write_volatile(if out.IsCursorVisible != 0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
});
|
||||||
|
core::ptr::addr_of_mut!((*shm).x).write_volatile(out.X);
|
||||||
|
core::ptr::addr_of_mut!((*shm).y).write_volatile(out.Y);
|
||||||
|
if out.IsCursorShapeUpdated != 0 && out.IsCursorVisible != 0 {
|
||||||
|
let info = &out.CursorShapeInfo;
|
||||||
|
let rows = info.Height.min(CURSOR_SHAPE_MAX);
|
||||||
|
let bytes = (rows as usize * info.Pitch as usize).min(CURSOR_SHAPE_BYTES);
|
||||||
|
core::ptr::copy_nonoverlapping(shape_buf.as_ptr(), shape_dst, bytes);
|
||||||
|
core::ptr::addr_of_mut!((*shm).cursor_type).write_volatile(info.CursorType as u32);
|
||||||
|
core::ptr::addr_of_mut!((*shm).width)
|
||||||
|
.write_volatile(info.Width.min(CURSOR_SHAPE_MAX));
|
||||||
|
core::ptr::addr_of_mut!((*shm).height).write_volatile(rows);
|
||||||
|
core::ptr::addr_of_mut!((*shm).pitch).write_volatile(info.Pitch);
|
||||||
|
core::ptr::addr_of_mut!((*shm).hot_x).write_volatile(info.XHot);
|
||||||
|
core::ptr::addr_of_mut!((*shm).hot_y).write_volatile(info.YHot);
|
||||||
|
core::ptr::addr_of_mut!((*shm).shape_id).write_volatile(info.ShapeId);
|
||||||
|
last_shape_id = info.ShapeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fence(Ordering::Release);
|
||||||
|
seq.store(s.wrapping_add(2), Ordering::Release); // even = consistent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ mod log;
|
|||||||
mod adapter;
|
mod adapter;
|
||||||
mod callbacks;
|
mod callbacks;
|
||||||
mod control;
|
mod control;
|
||||||
|
mod cursor_worker;
|
||||||
mod direct_3d_device;
|
mod direct_3d_device;
|
||||||
mod edid;
|
mod edid;
|
||||||
mod entry;
|
mod entry;
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ pub struct MonitorObject {
|
|||||||
/// on the SAME adapter (same pooled device — a cross-device texture would be unusable). Dropped
|
/// on the SAME adapter (same pooled device — a cross-device texture would be unusable). Dropped
|
||||||
/// with the `MonitorObject` on teardown (it holds only a driver-private texture, no handles).
|
/// with the `MonitorObject` on teardown (it holds only a driver-private texture, no handles).
|
||||||
pub preserved_stash: Option<(crate::frame_transport::FrameStash, u32, i32)>,
|
pub preserved_stash: Option<(crate::frame_transport::FrameStash, u32, i32)>,
|
||||||
|
/// The host asked for an IddCx hardware cursor (`AddRequest::hw_cursor`, proto v5): declared
|
||||||
|
/// once the cursor channel arrives (`IOCTL_SET_CURSOR_CHANNEL`) — see [`set_cursor_channel`].
|
||||||
|
pub hw_cursor: bool,
|
||||||
|
/// The live cursor query→publish worker (drops = stop+join) — set by [`set_cursor_channel`].
|
||||||
|
pub cursor_worker: Option<crate::cursor_worker::CursorWorker>,
|
||||||
/// When the entry was created — the watchdog skips still-initializing monitors.
|
/// When the entry was created — the watchdog skips still-initializing monitors.
|
||||||
pub created_at: Instant,
|
pub created_at: Instant,
|
||||||
}
|
}
|
||||||
@@ -370,6 +375,45 @@ pub fn has_frame_channel(target_id: u32) -> bool {
|
|||||||
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
|
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adopt a hardware-cursor channel delivery (`IOCTL_SET_CURSOR_CHANNEL`, proto v5): declare the
|
||||||
|
/// hardware cursor to the OS and start the worker. Rejected (`Err(ch)`) when the monitor is
|
||||||
|
/// unknown, didn't ask for a cursor at ADD, or isn't created yet. A RE-delivery replaces the
|
||||||
|
/// worker (the old one stops+joins on drop) — the host only re-sends after recreating the
|
||||||
|
/// section. The IddCx setup runs OUTSIDE the monitors lock (it can call back into mode DDIs).
|
||||||
|
pub fn set_cursor_channel(
|
||||||
|
target_id: u32,
|
||||||
|
ch: crate::cursor_worker::CursorChannel,
|
||||||
|
) -> Result<(), crate::cursor_worker::CursorChannel> {
|
||||||
|
if target_id == 0 {
|
||||||
|
return Err(ch);
|
||||||
|
}
|
||||||
|
let (monitor_obj, old_worker) = {
|
||||||
|
let mut lock = lock_monitors();
|
||||||
|
let Some(m) = lock
|
||||||
|
.iter_mut()
|
||||||
|
.find(|m| m.target_id == target_id && m.hw_cursor)
|
||||||
|
else {
|
||||||
|
return Err(ch);
|
||||||
|
};
|
||||||
|
let Some(obj) = m.object else {
|
||||||
|
return Err(ch);
|
||||||
|
};
|
||||||
|
(obj, 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 {
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
let mut lock = lock_monitors();
|
||||||
|
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
|
||||||
|
m.cursor_worker = Some(worker);
|
||||||
|
}
|
||||||
|
// A monitor departed in the window: dropping `worker` here stops it cleanly.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Stash a swap-chain worker's still-live [`FramePublisher`](crate::frame_transport::FramePublisher) on
|
/// Stash a swap-chain worker's still-live [`FramePublisher`](crate::frame_transport::FramePublisher) on
|
||||||
/// its monitor across a swap-chain unassign→reassign flap (STEP 6 sibling-join fix; see the field docs
|
/// its monitor across a swap-chain unassign→reassign flap (STEP 6 sibling-join fix; see the field docs
|
||||||
/// on [`MonitorObject::preserved_publisher`]). Called from the EXITING worker thread — the caller must
|
/// on [`MonitorObject::preserved_publisher`]). Called from the EXITING worker thread — the caller must
|
||||||
@@ -497,6 +541,7 @@ pub fn create_monitor(
|
|||||||
refresh: u32,
|
refresh: u32,
|
||||||
preferred_id: u32,
|
preferred_id: u32,
|
||||||
client_lum: crate::edid::ClientLuminance,
|
client_lum: crate::edid::ClientLuminance,
|
||||||
|
hw_cursor: bool,
|
||||||
) -> Option<(u32, u32, u32, i32)> {
|
) -> Option<(u32, u32, u32, i32)> {
|
||||||
let adapter = crate::adapter::adapter()?;
|
let adapter = crate::adapter::adapter()?;
|
||||||
// Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart
|
// Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart
|
||||||
@@ -550,6 +595,8 @@ pub fn create_monitor(
|
|||||||
frame_channel: None,
|
frame_channel: None,
|
||||||
preserved_publisher: None,
|
preserved_publisher: None,
|
||||||
preserved_stash: None,
|
preserved_stash: None,
|
||||||
|
hw_cursor,
|
||||||
|
cursor_worker: None,
|
||||||
created_at: Instant::now(),
|
created_at: Instant::now(),
|
||||||
});
|
});
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -133,6 +133,24 @@ iddcx_ddi!(
|
|||||||
IddCxMonitorDeparture(monitor: iddcx::IDDCX_MONITOR)
|
IddCxMonitorDeparture(monitor: iddcx::IDDCX_MONITOR)
|
||||||
@ IddCxMonitorDepartureTableIndex as PFN_IDDCXMONITORDEPARTURE
|
@ IddCxMonitorDepartureTableIndex as PFN_IDDCXMONITORDEPARTURE
|
||||||
);
|
);
|
||||||
|
iddcx_ddi!(
|
||||||
|
/// Declare hardware-cursor support for a monitor (proto v5 cursor channel): the OS stops
|
||||||
|
/// compositing the pointer into the desktop image and signals `hNewCursorDataAvailable`
|
||||||
|
/// on every cursor change instead (drained via [`IddCxMonitorQueryHardwareCursor`]).
|
||||||
|
IddCxMonitorSetupHardwareCursor(
|
||||||
|
monitor: iddcx::IDDCX_MONITOR,
|
||||||
|
in_args: *const iddcx::IDARG_IN_SETUP_HWCURSOR,
|
||||||
|
) @ IddCxMonitorSetupHardwareCursorTableIndex as PFN_IDDCXMONITORSETUPHARDWARECURSOR
|
||||||
|
);
|
||||||
|
iddcx_ddi!(
|
||||||
|
/// Drain one hardware-cursor update: position/visibility always; shape bytes are copied
|
||||||
|
/// into `in_args.pShapeBuffer` only when the OS's shape id moved past `LastShapeId`.
|
||||||
|
IddCxMonitorQueryHardwareCursor(
|
||||||
|
monitor: iddcx::IDDCX_MONITOR,
|
||||||
|
in_args: *const iddcx::IDARG_IN_QUERY_HWCURSOR,
|
||||||
|
out_args: *mut iddcx::IDARG_OUT_QUERY_HWCURSOR,
|
||||||
|
) @ IddCxMonitorQueryHardwareCursorTableIndex as PFN_IDDCXMONITORQUERYHARDWARECURSOR
|
||||||
|
);
|
||||||
iddcx_ddi!(
|
iddcx_ddi!(
|
||||||
/// Set the preferred render adapter (LUID) for the virtual adapter.
|
/// Set the preferred render adapter (LUID) for the virtual adapter.
|
||||||
IddCxAdapterSetRenderAdapter(
|
IddCxAdapterSetRenderAdapter(
|
||||||
|
|||||||
@@ -84,5 +84,5 @@ const _: fn() = || {
|
|||||||
/// The FP16/HDR adapter flag + high-color-space target cap the driver MUST set (these gate the whole
|
/// The FP16/HDR adapter flag + high-color-space target cap the driver MUST set (these gate the whole
|
||||||
/// `*2` callback requirement). Confirms the ModuleConsts paths the port uses — note these enums have NO
|
/// `*2` callback requirement). Confirms the ModuleConsts paths the port uses — note these enums have NO
|
||||||
/// `_`-prefixed module (unlike `_IDDFUNCENUM`/`_IDDSTRUCTENUM`).
|
/// `_`-prefixed module (unlike `_IDDFUNCENUM`/`_IDDSTRUCTENUM`).
|
||||||
const _: u32 = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16 as u32;
|
const _: u32 = iddcx::IDDCX_ADAPTER_FLAGS::IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16;
|
||||||
const _: u32 = iddcx::IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE as u32;
|
const _: u32 = iddcx::IDDCX_TARGET_CAPS::IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE;
|
||||||
|
|||||||
Reference in New Issue
Block a user