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:
@@ -97,6 +97,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
||||
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -148,6 +150,7 @@ unsafe fn add(request: WDFREQUEST) {
|
||||
max_frame_avg_nits: req.max_frame_avg_nits,
|
||||
min_millinits: req.min_luminance_millinits,
|
||||
},
|
||||
req.hw_cursor != 0,
|
||||
) else {
|
||||
complete(request, STATUS_NOT_FOUND);
|
||||
return;
|
||||
@@ -174,6 +177,35 @@ unsafe fn add(request: WDFREQUEST) {
|
||||
///
|
||||
/// # Safety
|
||||
/// `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) {
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
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 callbacks;
|
||||
mod control;
|
||||
mod cursor_worker;
|
||||
mod direct_3d_device;
|
||||
mod edid;
|
||||
mod entry;
|
||||
|
||||
@@ -75,6 +75,11 @@ pub struct MonitorObject {
|
||||
/// 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).
|
||||
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.
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -497,6 +541,7 @@ pub fn create_monitor(
|
||||
refresh: u32,
|
||||
preferred_id: u32,
|
||||
client_lum: crate::edid::ClientLuminance,
|
||||
hw_cursor: bool,
|
||||
) -> Option<(u32, u32, u32, i32)> {
|
||||
let adapter = crate::adapter::adapter()?;
|
||||
// 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,
|
||||
preserved_publisher: None,
|
||||
preserved_stash: None,
|
||||
hw_cursor,
|
||||
cursor_worker: None,
|
||||
created_at: Instant::now(),
|
||||
});
|
||||
id
|
||||
|
||||
Reference in New Issue
Block a user