Files
punktfunk/packaging/windows/drivers/pf-vdisplay/src/monitor.rs
T
enricobuehler 45c29a99d5 perf(host+driver): in-place resize = advertised-mode fast path + mode-history union
On-glass round 2 settled the mechanism: after UpdateModes2 the OS re-parses
our description AND re-queries target modes (driver log — both callbacks
served the fresh list) yet the SETTABLE set stays pruned to the modes known
at monitor ARRIVAL; the monitor source-mode set is pinned then, below
anything the driver can refresh. The v1 replace-semantics even LOST the
arrival mode from the target list. Consequences:

- driver: UPDATE_MODES now UNIONs (new mode first, previous list kept,
  deduped by resolution, cap 12), and a re-created same-id monitor inherits
  its departed predecessor's list (MODE_HISTORY) — every size an identity
  ever served is settable at the next arrival, so returning to a
  previously-used size (windowed<->fullscreen, drag back) is IN-PLACE.
- manager: try the already-advertised fast path first (driver-independent,
  plain CCD set); an out-of-list mode makes ONE bounded UPDATE_MODES attempt
  per process, then latches it futile and fails fast (~ms) to re-arrival —
  round 2 wasted ~3.1 s per arbitrary resize on the doomed wait. Fallback
  log demoted warn->info (expected-normal for first-seen sizes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:01:36 +02:00

834 lines
39 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Virtual-monitor model + lifecycle (STEP 4). Monitors are created on demand by the control plane
//! ([`crate::control`], `IOCTL_ADD`): each carries the requested mode (advertised as preferred) plus the
//! `session_id` the host keys it by and the OS target id + render-adapter LUID captured at arrival. Ported
//! from the working upstream virtual-display-rs (`monitor.rs` + `context.rs::create_monitor`), with
//! `guid: u128` → `session_id: u64` for the owned `pf_driver_proto` control plane.
use std::sync::Mutex;
use std::time::{Duration, Instant};
use wdk_sys::{NTSTATUS, WDFOBJECT, call_unsafe_wdf_function_binding, iddcx};
/// One resolution with the refresh rates it supports.
#[derive(Clone)]
pub struct Mode {
pub width: u32,
pub height: u32,
pub refresh_rates: Vec<u32>,
}
/// A single (width, height, refresh) tuple — modes flattened across their refresh rates.
#[derive(Copy, Clone)]
pub struct ModeItem {
pub width: u32,
pub height: u32,
pub refresh_rate: u32,
}
/// Flatten a mode list into per-refresh-rate tuples (the order the mode DDIs emit).
pub fn flatten(modes: &[Mode]) -> impl Iterator<Item = ModeItem> + '_ {
modes.iter().flat_map(|m| {
m.refresh_rates.iter().map(|&rr| ModeItem {
width: m.width,
height: m.height,
refresh_rate: rr,
})
})
}
/// A live (or pending) virtual monitor.
pub struct MonitorObject {
/// The IddCx monitor handle, set once `IddCxMonitorCreate` returns (None while pending).
pub object: Option<iddcx::IDDCX_MONITOR>,
/// EDID serial / connector index — the key the mode DDIs match on.
pub id: u32,
/// Advertised modes (requested mode first, then [`default_modes`]).
pub modes: Vec<Mode>,
/// The host's monotonic key (ADD/REMOVE).
pub session_id: u64,
/// OS target id + render-adapter LUID from `IDARG_OUT_MONITORARRIVAL` (the ADD reply).
pub target_id: u32,
pub adapter_luid_low: u32,
pub adapter_luid_high: i32,
/// The live swap-chain drain worker, set by `assign_swap_chain` and dropped (RAII-joins the worker
/// thread) by `unassign_swap_chain` / departure (STEP 5).
pub swap_chain_processor: Option<crate::swap_chain_processor::SwapChainProcessor>,
/// The host's sealed-channel delivery (`IOCTL_SET_FRAME_CHANNEL`) awaiting pickup by the swap-chain
/// worker ([`take_frame_channel`]). Exactly one owner per delivery: replacing or dropping the entry
/// closes an unconsumed channel's handles via [`FrameChannel`]'s `Drop`, so no delivery can leak
/// handles in the WUDFHost table whatever the monitor's fate.
pub frame_channel: Option<crate::frame_transport::FrameChannel>,
/// A live [`FramePublisher`](crate::frame_transport::FramePublisher) preserved across a swap-chain
/// unassign→reassign flap (STEP 6 sibling-join fix). The OS unassigns a monitor's swap-chain
/// whenever a SIBLING display churns the desktop topology (a second client joining / leaving /
/// resizing), which drops the swap-chain worker — but the HOST-owned ring (header / event /
/// textures) the publisher holds stays valid, and the host only re-delivers the frame channel on a
/// ring RECREATE (a descriptor change), so a fresh worker had nothing to re-attach from and the
/// first client's stream froze (repeat frames forever). The exiting worker stashes its still-live
/// publisher here ([`preserve_publisher`]); the next worker on the SAME render adapter takes it back
/// ([`take_preserved_publisher`]) and resumes publishing into the same ring. Dropped with the
/// `MonitorObject` on teardown (closing its ring handles) if no worker ever reclaims it.
pub preserved_publisher: Option<crate::frame_transport::FramePublisher>,
/// The worker's [`FrameStash`](crate::frame_transport::FrameStash) (the retained last composed
/// frame — the first-frame guarantee) preserved across a swap-chain unassign→reassign flap,
/// tagged with the render-adapter LUID its texture lives on: the next worker re-adopts it only
/// 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)>,
/// When the entry was created — the watchdog skips still-initializing monitors.
pub created_at: Instant,
}
// SAFETY: the raw IddCx monitor handle is framework-managed; access is serialized by MONITOR_MODES.
unsafe impl Send for MonitorObject {}
/// All live monitors. A process-`static` (not a WDFDEVICE-context-owned allocation) BY NECESSITY: the IddCx
/// monitor/mode DDIs receive only an IddCx handle — never the WDFDEVICE or its context — so this state must
/// be reachable without one (the upstream virtual-display-rs is a process-`static` for the same reason).
/// With a single `pf_vdisplay` devnode + `UmdfHostProcessSharing=ProcessSharingDisabled` the host process
/// (and this state) die WITH the device, so it is effectively device-scoped already; a `Box` + `AtomicPtr`
/// "device-owned" variant (audit §2.5) would only add a use-after-free window — the host-gone watchdog
/// thread ([`crate::control::start_watchdog`]) races device cleanup — for no real gain. Cleanup of the
/// heavy per-monitor resources on device removal is instead done explicitly ([`cleanup_for_device_removal`]).
pub static MONITOR_MODES: Mutex<Vec<MonitorObject>> = Mutex::new(Vec::new());
/// Lock [`MONITOR_MODES`], recovering the guard on poison instead of failing. DEFENSIVE ONLY: this driver
/// workspace builds with `panic = "abort"` (packaging/windows/drivers/Cargo.toml), so a panic while the
/// lock is held aborts the process WITHOUT unwinding — `MutexGuard::drop` never runs, the poison flag is
/// never set, and `.lock()` can never return `Err`. The `into_inner()` arm is therefore currently
/// unreachable; it is retained to consolidate the lock pattern and to stay correct if the panic strategy
/// ever becomes `unwind` (the guarded data is a plain `Vec` with no cross-field invariant a half-completed
/// panic could corrupt, so recovering the guard is sound). NOTE: this does NOT explain the observed ADD
/// 0x80070490 wedge — that is ghost-monitor slot-budget exhaustion (the arrival-failure `WdfObjectDelete`
/// teardown above + the host-side reap), not lock poisoning.
fn lock_monitors() -> std::sync::MutexGuard<'static, Vec<MonitorObject>> {
MONITOR_MODES.lock().unwrap_or_else(|e| e.into_inner())
}
/// True if any virtual monitor currently exists — the host-gone watchdog only reaps when there's
/// something to reap (see [`crate::control::start_watchdog`]).
pub fn has_monitors() -> bool {
!lock_monitors().is_empty()
}
/// Depart every monitor that has existed at least `grace` — the host-gone watchdog reap
/// ([`crate::control::start_watchdog`]). The grace skips a just-created monitor (the host adds it, then
/// starts pinging) so a momentarily-stale ping timer can't nuke a brand-new monitor. Returns the count
/// departed. Same lock discipline as [`remove_monitor`]: drop each worker (which RAII-joins its thread)
/// OUTSIDE the `MONITOR_MODES` lock, then depart.
pub fn reap_orphaned(grace: Duration) -> usize {
let mut drained: Vec<(
Option<iddcx::IDDCX_MONITOR>,
Option<crate::swap_chain_processor::SwapChainProcessor>,
)> = {
let mut lock = lock_monitors();
let mut taken = Vec::new();
let mut i = 0;
while i < lock.len() {
if lock[i].created_at.elapsed() >= grace {
let mut m = lock.remove(i);
taken.push((m.object, m.swap_chain_processor.take()));
} else {
i += 1;
}
}
taken
};
let n = drained.len();
for (_, processor) in &mut drained {
drop(processor.take());
}
for (object, _) in drained {
if let Some(m) = object {
// SAFETY: `m` is a live IddCx monitor handle; departure tears it down.
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
}
}
n
}
/// Append `from`'s modes to `into`, skipping resolutions already present, capped at
/// [`MODE_LIST_CAP`] — the accumulate half of the union semantics (see [`update_monitor_modes`]).
fn union_modes(into: &mut Vec<Mode>, from: &[Mode]) {
for m in from {
if into.len() >= MODE_LIST_CAP {
break;
}
if !into
.iter()
.any(|e| (e.width, e.height) == (m.width, m.height))
{
into.push(m.clone());
}
}
}
/// The last advertised mode list of a DEPARTED monitor, per monitor id — consumed by the next
/// same-id [`create_monitor`] so a re-arrived monitor's ARRIVAL list already contains every mode
/// its predecessor ever served. The OS pins a monitor's settable set at arrival (see
/// [`update_monitor_modes`]), so this is what makes a windowed↔fullscreen cycle (or any return to
/// a previously-used size) an IN-PLACE mode set instead of another hotplug. In-process only (a
/// WUDFHost restart forgets it — harmless, the next resizes re-teach it); bounded: ≤ 16 ids ×
/// [`MODE_LIST_CAP`] modes.
static MODE_HISTORY: Mutex<Vec<(u32, Vec<Mode>)>> = Mutex::new(Vec::new());
/// Record a departing monitor's advertised list for its id ([`MODE_HISTORY`]).
fn remember_modes(id: u32, modes: &[Mode]) {
let mut hist = MODE_HISTORY.lock().unwrap_or_else(|e| e.into_inner());
if let Some(slot) = hist.iter_mut().find(|(i, _)| *i == id) {
slot.1 = modes.to_vec();
} else {
hist.push((id, modes.to_vec()));
}
}
/// Fallback modes appended after the requested mode, so a topology change still has options.
fn default_modes() -> Vec<Mode> {
vec![
Mode {
width: 1920,
height: 1080,
refresh_rates: vec![60, 120],
},
Mode {
width: 1280,
height: 720,
refresh_rates: vec![60],
},
]
}
/// `DISPLAYCONFIG_VIDEO_SIGNAL_INFO` for a monitor mode (vSyncFreqDivider = 0, per the DDI contract).
pub fn display_info(
width: u32,
height: u32,
refresh_rate: u32,
) -> wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO {
// Compute in u64 then saturate the u32 rational numerators: the old u32 `refresh*(h+4)^2` overflows
// for a large mode (e.g. 8K@240), which panics→aborts the extern-"C" mode DDI in a debug build.
// Identical for every real mode; only an absurd (also now bounds-rejected) mode saturates.
let clock_rate: u64 =
u64::from(refresh_rate) * u64::from(height + 4) * u64::from(height + 4) + 1000;
let clock_rate_u32 = u32::try_from(clock_rate).unwrap_or(u32::MAX);
let mut si = pod_init!(wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO);
si.pixelRate = clock_rate;
si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate_u32,
Denominator: height + 4,
};
si.vSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: clock_rate_u32,
Denominator: (height + 4) * (height + 4),
};
si.activeSize = wdk_sys::DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
};
si.totalSize = wdk_sys::DISPLAYCONFIG_2DREGION {
cx: width + 4,
cy: height + 4,
};
// union { AdditionalSignalInfo bitfield | videoStandard:u32 }: videoStandard=255, vSyncFreqDivider=0.
si.__bindgen_anon_1.videoStandard = 255;
si.scanLineOrdering =
wdk_sys::DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;
si
}
/// `IDDCX_TARGET_MODE` for a scan-out mode (vSyncFreqDivider = 1, per the DDI contract).
pub fn target_mode(width: u32, height: u32, refresh_rate: u32) -> iddcx::IDDCX_TARGET_MODE {
let region = wdk_sys::DISPLAYCONFIG_2DREGION {
cx: width,
cy: height,
};
let mut si = pod_init!(wdk_sys::DISPLAYCONFIG_VIDEO_SIGNAL_INFO);
si.pixelRate = u64::from(refresh_rate) * u64::from(width) * u64::from(height);
si.hSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate * height,
Denominator: 1,
};
si.vSyncFreq = wdk_sys::DISPLAYCONFIG_RATIONAL {
Numerator: refresh_rate,
Denominator: 1,
};
si.totalSize = region;
si.activeSize = region;
si.scanLineOrdering =
wdk_sys::DISPLAYCONFIG_SCANLINE_ORDERING::DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;
// videoStandard=255, vSyncFreqDivider=1 (bits 16..21) => 255 | (1<<16).
si.__bindgen_anon_1.videoStandard = 255 | (1 << 16);
let mut tm = pod_init!(iddcx::IDDCX_TARGET_MODE);
tm.Size = core::mem::size_of::<iddcx::IDDCX_TARGET_MODE>() as u32;
tm.TargetVideoSignalInfo = wdk_sys::DISPLAYCONFIG_TARGET_MODE {
targetVideoSignalInfo: si,
};
tm
}
/// Wire bit-depth advertised per mode in the `*2` (HDR) mode DDIs. STEP 7: advertise BOTH 8 and 10 bpc
/// RGB (so the OS offers HDR10 modes), no YCbCr. The wdk-sys bindgen enum is `ModuleConsts`, so each
/// `IDDCX_BITS_PER_COMPONENT_*` is a plain-int const and the `IDDCX_WIRE_BITS_PER_COMPONENT` fields are
/// plain ints — OR the constants directly (NO newtype `.0` like the oracle's wdf-umdf-sys binding). Field
/// names (Rgb/YCbCr444/YCbCr422/YCbCr420, IDDCX_BITS_PER_COMPONENT_8/_10/_NONE) are the verbatim C header
/// names, identical across both bindings.
pub fn wire_bits() -> iddcx::IDDCX_WIRE_BITS_PER_COMPONENT {
let rgb = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_8
| iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_10;
let mut w = pod_init!(iddcx::IDDCX_WIRE_BITS_PER_COMPONENT);
w.Rgb = rgb;
w.YCbCr444 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE;
w.YCbCr422 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE;
w.YCbCr420 = iddcx::IDDCX_BITS_PER_COMPONENT::IDDCX_BITS_PER_COMPONENT_NONE;
w
}
/// `IDDCX_TARGET_MODE2` for a scan-out mode (HDR `*2` path): builds the v1 [`target_mode`] and copies its
/// `TargetVideoSignalInfo`, then stamps the `*2` Size + per-mode wire bit-depth ([`wire_bits`]). Rest
/// zeroed.
pub fn target_mode2(width: u32, height: u32, refresh_rate: u32) -> iddcx::IDDCX_TARGET_MODE2 {
let m1 = target_mode(width, height, refresh_rate);
let mut tm = pod_init!(iddcx::IDDCX_TARGET_MODE2);
tm.Size = core::mem::size_of::<iddcx::IDDCX_TARGET_MODE2>() as u32;
tm.TargetVideoSignalInfo = m1.TargetVideoSignalInfo;
tm.BitsPerComponent = wire_bits();
tm
}
/// A monitor's advertised modes (the looked-up entry returns a clone for lock-free mode-DDI fill).
pub fn modes_for_id(id: u32) -> Option<Vec<Mode>> {
MONITOR_MODES
.lock()
.ok()?
.iter()
.find(|m| m.id == id)
.map(|m| m.modes.clone())
}
/// Modes for the monitor whose handle matches (used by `monitor_query_modes`).
pub fn modes_for_object(object: iddcx::IDDCX_MONITOR) -> Option<Vec<Mode>> {
MONITOR_MODES
.lock()
.ok()?
.iter()
.find(|m| m.object == Some(object))
.map(|m| m.modes.clone())
}
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to key the
/// frame-channel stash for its worker). `None` if the monitor isn't found.
pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
MONITOR_MODES
.lock()
.ok()?
.iter()
.find(|m| m.object == Some(object))
.map(|m| m.target_id)
}
/// Stash a host frame-channel delivery on the monitor with `target_id` (an ARRIVED monitor — a pending
/// entry's `target_id` is still 0, which the host can never send since OS target ids are non-zero).
/// Replacing an unconsumed delivery drops it → its handles close (it WAS adopted by a prior success).
/// `Err(ch)` if no such monitor exists — the caller must NOT close those handles (the host only sees
/// the error status and reaps its remote duplicates itself; closing here too would double-close values
/// the OS may have reused).
pub fn set_frame_channel(
target_id: u32,
ch: crate::frame_transport::FrameChannel,
) -> Result<(), crate::frame_transport::FrameChannel> {
if target_id == 0 {
return Err(ch);
}
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.frame_channel = Some(ch);
Ok(())
} else {
Err(ch)
}
}
/// Take (remove) the pending frame-channel delivery for `target_id`, transferring handle ownership to
/// the caller (the swap-chain worker's attach). `None` until the host delivers one.
pub fn take_frame_channel(target_id: u32) -> Option<crate::frame_transport::FrameChannel> {
if target_id == 0 {
return None;
}
lock_monitors()
.iter_mut()
.find(|m| m.target_id == target_id)?
.frame_channel
.take()
}
/// Is a frame-channel delivery pending for `target_id`? The swap-chain worker treats a pending
/// delivery as NEWEST-WINS: it supersedes an attached publisher, because the host only re-delivers
/// after (re)creating the ring — and a retry-created ring is a DIFFERENT header mapping, whose
/// generation bump an old publisher (mapped to the previous header) can never observe.
pub fn has_frame_channel(target_id: u32) -> bool {
target_id != 0
&& lock_monitors()
.iter()
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
}
/// 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
/// NOT hold `MONITOR_MODES` (this locks it), matching the same drop-outside-the-lock discipline the
/// processor teardown paths use. Returns `Err(publisher)` when no monitor with `target_id` exists (a
/// genuine teardown, not a flap: the entry was already removed) so the caller drops it, closing the ring
/// handles. Replacing an already-stashed publisher (should not happen — one worker exits at a time)
/// drops the old one, so it can never accumulate. Returning the publisher in the `Err` makes the
/// `Result` itself `#[must_use]`, so a caller can't silently drop the not-preserved publisher.
pub fn preserve_publisher(
target_id: u32,
publisher: crate::frame_transport::FramePublisher,
) -> Result<(), crate::frame_transport::FramePublisher> {
if target_id == 0 {
return Err(publisher);
}
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.preserved_publisher = Some(publisher);
Ok(())
} else {
Err(publisher)
}
}
/// Take (remove) a preserved [`FramePublisher`](crate::frame_transport::FramePublisher) for a freshly-
/// (re)assigned swap-chain worker (STEP 6 sibling-join fix). The caller re-adopts it ONLY when the new
/// swap-chain's render adapter matches the publisher's ([`FramePublisher::render_adapter`]) — same
/// pooled device, so its context + opened ring textures are still valid; on a mismatch the caller drops
/// it and waits for a fresh channel delivery instead. `None` until a worker has stashed one.
pub fn take_preserved_publisher(target_id: u32) -> Option<crate::frame_transport::FramePublisher> {
if target_id == 0 {
return None;
}
lock_monitors()
.iter_mut()
.find(|m| m.target_id == target_id)?
.preserved_publisher
.take()
}
/// Preserve an EXITING worker's [`FrameStash`](crate::frame_transport::FrameStash) on its monitor
/// across a swap-chain unassign→reassign flap, tagged with the render-adapter LUID it lives on
/// (see [`MonitorObject::preserved_stash`]). An empty stash, or one for a monitor that no longer
/// exists (genuine teardown), is simply dropped — unlike a publisher it owns no handles, so there
/// is nothing to hand back.
pub fn preserve_stash(
target_id: u32,
luid_low: u32,
luid_high: i32,
stash: crate::frame_transport::FrameStash,
) {
if target_id == 0 || stash.texture().is_none() {
return;
}
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
m.preserved_stash = Some((stash, luid_low, luid_high));
}
}
/// Take (remove) the preserved [`FrameStash`](crate::frame_transport::FrameStash) for a freshly-
/// (re)assigned swap-chain worker — returned only when the worker's render adapter matches the one
/// the stash was preserved on (same pooled device); a mismatched stash is dropped (its texture
/// would be cross-device).
pub fn take_preserved_stash(
target_id: u32,
luid_low: u32,
luid_high: i32,
) -> Option<crate::frame_transport::FrameStash> {
if target_id == 0 {
return None;
}
let (stash, low, high) = lock_monitors()
.iter_mut()
.find(|m| m.target_id == target_id)?
.preserved_stash
.take()?;
((low, high) == (luid_low, luid_high)).then_some(stash)
}
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
/// self-deadlock). `None` returned if the monitor isn't found (the caller should drop `proc` itself).
#[must_use]
pub fn set_swap_chain_processor(
object: iddcx::IDDCX_MONITOR,
proc: crate::swap_chain_processor::SwapChainProcessor,
) -> Option<crate::swap_chain_processor::SwapChainProcessor> {
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.object == Some(object)) {
m.swap_chain_processor.replace(proc)
} else {
// No such monitor — hand `proc` back so the caller drops it (joins the worker) outside the lock.
Some(proc)
}
}
/// Take (remove) the swap-chain processor from the monitor whose handle matches, returning it for the
/// caller to drop OUTSIDE the lock (see `set_swap_chain_processor`). `None` if none was installed.
#[must_use]
pub fn take_swap_chain_processor(
object: iddcx::IDDCX_MONITOR,
) -> Option<crate::swap_chain_processor::SwapChainProcessor> {
MONITOR_MODES
.lock()
.ok()?
.iter_mut()
.find(|m| m.object == Some(object))?
.swap_chain_processor
.take()
}
/// `IOCTL_ADD`: create + arrive a virtual monitor at `width`x`height`@`refresh` for `session_id`, naming it
/// by `preferred_id` (the host's per-client stable id; `0` = auto-allocate) and advertising the
/// CLIENT display's luminance volume in its EDID's CTA HDR block (`client_lum`; all-zero = the
/// built-in defaults). Returns the resolved `(monitor_id, target_id, adapter_luid_low,
/// adapter_luid_high)` for the [`AddReply`](pf_driver_proto::control::AddReply), or `None` on
/// failure (no adapter yet / IddCx error).
pub fn create_monitor(
session_id: u64,
width: u32,
height: u32,
refresh: u32,
preferred_id: u32,
client_lum: crate::edid::ClientLuminance,
) -> 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
// the stale monitor first, so one session maps to exactly one monitor (no duplicate EDID/target lingers).
if MONITOR_MODES
.lock()
.map(|l| l.iter().any(|m| m.session_id == session_id))
.unwrap_or(false)
{
dbglog!(
"[pf-vd] create_monitor: session {session_id} already live — departing the stale monitor"
);
remove_monitor(session_id);
}
let mut modes = vec![Mode {
width,
height,
refresh_rates: vec![refresh],
}];
modes.extend(default_modes());
// Register the (pending) monitor so the mode DDIs can find it by EDID-serial id before arrival. The id
// seeds the EDID serial + IddCx ConnectorIndex + ContainerId — i.e. the monitor's OS IDENTITY. Honor the
// host's per-client `preferred_id` when it is valid + not currently live, so a given client gets a
// STABLE identity across reconnects (→ Windows reapplies its saved per-monitor DPI scaling); else fall
// back to the lowest-free id (auto — the original slot-based behavior). A bounded reused id (vs a
// monotonic counter) keeps IddCx reusing the same OS target slot rather than leaving a ghost monitor
// node behind (the slot-exhaustion wedge). Allocated under the lock with the push so two concurrent ADDs
// can't pick the same id.
let id = {
let mut lock = lock_monitors();
let id = resolve_id(&lock, preferred_id);
// Same-id mode history (P2 union semantics): a RE-ARRIVED monitor advertises every mode
// its departed predecessor served, so the OS's arrival-pinned settable set already
// contains them — a return to any previously-used size is then an IN-PLACE mode set.
{
let hist = MODE_HISTORY.lock().unwrap_or_else(|e| e.into_inner());
if let Some((_, prev)) = hist.iter().find(|(i, _)| *i == id) {
union_modes(&mut modes, prev);
}
}
lock.push(MonitorObject {
object: None,
id,
modes,
session_id,
target_id: 0,
adapter_luid_low: 0,
adapter_luid_high: 0,
swap_chain_processor: None,
frame_channel: None,
preserved_publisher: None,
preserved_stash: None,
created_at: Instant::now(),
});
id
};
// EDID (serial = id) describes the monitor; the OS calls back into parse_monitor_description.
let mut edid = crate::edid::Edid::generate_with(id, client_lum);
let mut desc = pod_init!(iddcx::IDDCX_MONITOR_DESCRIPTION);
desc.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_DESCRIPTION>() as u32;
desc.Type = iddcx::IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID;
desc.DataSize = edid.len() as u32;
// SAFETY: `edid` is a local Vec that outlives this `create_monitor` call; IddCxMonitorCreate (below)
// reads through `pData` SYNCHRONOUSLY, before `edid` drops — the pointer never escapes the call.
desc.pData = edid.as_mut_ptr().cast();
let mut info = pod_init!(iddcx::IDDCX_MONITOR_INFO);
info.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_INFO>() as u32;
info.MonitorContainerId = container_guid(id);
info.MonitorType =
wdk_sys::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY::DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
info.ConnectorIndex = id;
info.MonitorDescription = desc;
let mut attr = pod_init!(wdk_sys::WDF_OBJECT_ATTRIBUTES);
attr.Size = core::mem::size_of::<wdk_sys::WDF_OBJECT_ATTRIBUTES>() as u32;
attr.ExecutionLevel = wdk_sys::_WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent;
attr.SynchronizationScope =
wdk_sys::_WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent;
let create_in = iddcx::IDARG_IN_MONITORCREATE {
ObjectAttributes: &raw mut attr,
pMonitorInfo: &raw mut info,
};
let mut create_out = pod_init!(iddcx::IDARG_OUT_MONITORCREATE);
// SAFETY: adapter is a valid IddCx adapter; create_in points to valid local storage read synchronously.
let st = unsafe { wdk_iddcx::IddCxMonitorCreate(adapter, &create_in, &mut create_out) };
dbglog!("[pf-vd] IddCxMonitorCreate(id={id}) -> {st:#x}");
if !wdk_iddcx::nt_success(st) {
remove_by_id(id);
return None;
}
let monitor = create_out.MonitorObject;
{
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.id == id) {
m.object = Some(monitor);
}
}
// Tell the OS the monitor is plugged in.
let mut arrival_out = pod_init!(iddcx::IDARG_OUT_MONITORARRIVAL);
// SAFETY: `monitor` is the just-created IddCx monitor handle.
let st = unsafe { wdk_iddcx::IddCxMonitorArrival(monitor, &mut arrival_out) };
dbglog!("[pf-vd] IddCxMonitorArrival(id={id}) -> {st:#x}");
if !wdk_iddcx::nt_success(st) {
// Arrival failed on a monitor we already CREATED. It must be torn down with `WdfObjectDelete`:
// `IddCxMonitorDeparture` is only valid for an ARRIVED monitor, so departing here would be a
// no-op that LEAKS the IddCx monitor object and permanently pins its slot against the adapter's
// `MaxMonitorsSupported` budget — the leak that, asymmetric with the create-failure path just
// above (which only reclaims the id, having no object to delete), accelerates the ADD 0x80070490
// wedge. Reclaim the id FIRST (drop the `MONITOR_MODES` entry that still holds this handle) so a
// concurrent `clear_all`/`reap_orphaned` can't grab + depart the handle we're about to delete,
// THEN delete the object — `monitor` is a local copy of the handle, valid across both.
dbglog!(
"[pf-vd] IddCxMonitorArrival(id={id}) FAILED — reclaiming the id + deleting the created monitor"
);
remove_by_id(id);
// SAFETY: `monitor` is the just-created (not-yet-arrived) IddCx monitor handle, now owned solely
// here (its `MONITOR_MODES` entry was just removed); `WdfObjectDelete` takes a `WDFOBJECT` (a raw
// handle cast, as in the swap-chain / device-cleanup teardowns).
unsafe {
call_unsafe_wdf_function_binding!(WdfObjectDelete, monitor as WDFOBJECT);
}
return None;
}
let (target_id, luid_low, luid_high) = (
arrival_out.OsTargetId,
arrival_out.OsAdapterLuid.LowPart,
arrival_out.OsAdapterLuid.HighPart,
);
{
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.id == id) {
m.target_id = target_id;
m.adapter_luid_low = luid_low;
m.adapter_luid_high = luid_high;
}
}
Some((id, target_id, luid_low, luid_high))
}
/// How many distinct resolutions a monitor's advertised list may accumulate (the requested head +
/// history + the built-in fallbacks). Bounds the union growth across many resizes; the OLDEST
/// history entries fall off first.
const MODE_LIST_CAP: usize = 12;
/// `IOCTL_UPDATE_MODES` (v4): refresh the LIVE monitor's advertised mode list to lead with a new
/// preferred mode and push the new TARGET mode list to the OS via `IddCxMonitorUpdateModes2` —
/// the in-place mid-stream resize (`design/first-frame-and-resize-latency.md` P2). No departure:
/// the monitor's OS identity, its swap-chain worker and the retained frame stash all survive.
/// The `*2` (HDR) DDI matches the `*2` mode/buffer family this driver already requires
/// (IddCx 1.10), so it adds no new OS floor.
///
/// UNION semantics (on-glass finding, build 26200): the OS re-parses the description AND
/// re-queries target modes after `UpdateModes2` — our callbacks served the fresh list — yet the
/// SETTABLE set stays pruned to the modes known at monitor ARRIVAL (the monitor source-mode set
/// is pinned then). So replacing the list can only ever LOSE settable modes (v1 of this op
/// dropped the arrival mode from the target list, breaking even a resize BACK to it); the update
/// therefore accumulates — new mode first, every previously-advertised mode kept (deduped by
/// resolution, capped at [`MODE_LIST_CAP`]) — and the real payoff is at the NEXT re-arrival,
/// where [`create_monitor`]'s same-id history union makes every previously-used mode settable.
///
/// The stored list is updated FIRST (under the lock) so any OS re-query through the mode DDIs
/// ([`modes_for_object`]/[`modes_for_id`]) sees the new list, and REVERTED if the DDI fails — the
/// OS then still holds the old list and the two stay coherent. The DDI itself is called OUTSIDE
/// the lock (it may re-enter the mode-query callbacks, which lock [`MONITOR_MODES`]).
pub fn update_monitor_modes(session_id: u64, width: u32, height: u32, refresh: u32) -> NTSTATUS {
// Swap the stored list (union — see above) + grab the live handle under the lock.
let (object, old_modes, new_modes) = {
let mut lock = lock_monitors();
let Some(m) = lock.iter_mut().find(|m| m.session_id == session_id) else {
return crate::STATUS_NOT_FOUND;
};
let Some(object) = m.object else {
return crate::STATUS_NOT_FOUND; // created but not yet arrived — nothing to update
};
let mut new_modes = vec![Mode {
width,
height,
refresh_rates: vec![refresh],
}];
union_modes(&mut new_modes, &m.modes);
let old = core::mem::replace(&mut m.modes, new_modes.clone());
(object, old, new_modes)
};
// The OS's target-mode list for this monitor (the `*2`/HDR shape, like `monitor_query_modes2`).
let mut targets: Vec<iddcx::IDDCX_TARGET_MODE2> = flatten(&new_modes)
.map(|item| target_mode2(item.width, item.height, item.refresh_rate))
.collect();
let mut in_args = pod_init!(iddcx::IDARG_IN_UPDATEMODES2);
in_args.Reason = iddcx::IDDCX_UPDATE_REASON::IDDCX_UPDATE_REASON_OTHER;
in_args.TargetModeCount = targets.len() as u32;
in_args.pTargetModes = targets.as_mut_ptr();
// SAFETY: `object` is a live IddCx monitor handle (arrived — checked above; a concurrent REMOVE
// is serialized by the host, which only ever resizes a monitor its own session holds a lease
// on). `in_args` points at valid local storage (`targets` outlives the synchronous DDI call).
let st = unsafe { wdk_iddcx::IddCxMonitorUpdateModes2(object, &in_args) };
dbglog!(
"[pf-vd] IddCxMonitorUpdateModes2(session={session_id}, {width}x{height}@{refresh}) -> {st:#x}"
);
if !wdk_iddcx::nt_success(st) {
// Keep the stored list coherent with what the OS actually holds (the old one).
let mut lock = lock_monitors();
if let Some(m) = lock.iter_mut().find(|m| m.session_id == session_id) {
m.modes = old_modes;
}
return st;
}
crate::STATUS_SUCCESS
}
/// `IOCTL_REMOVE`: depart + drop the monitor for `session_id`. Returns true if one was removed.
pub fn remove_monitor(session_id: u64) -> bool {
// Pull out the IddCx handle AND the swap-chain processor under the lock, but drop the processor
// (which RAII-joins its worker thread) only AFTER the lock guard is released — joining a worker
// while holding `MONITOR_MODES` would head-block the whole control plane / risk a self-deadlock.
let (monitor, processor) = {
let mut lock = lock_monitors();
let Some(pos) = lock.iter().position(|m| m.session_id == session_id) else {
return false;
};
let mut entry = lock.remove(pos);
// Keep the departing monitor's advertised list for its id — the next same-id create
// unions it back in (P2 mode history; see MODE_HISTORY).
remember_modes(entry.id, &entry.modes);
(entry.object, entry.swap_chain_processor.take())
};
// Drop the worker FIRST (it joins + deletes the swap-chain), THEN depart the monitor.
drop(processor);
if let Some(m) = monitor {
// SAFETY: `m` is a live IddCx monitor handle; departure tears it down.
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
}
true
}
/// `IOCTL_CLEAR_ALL`: depart + drop every monitor (host-startup orphan reap).
pub fn clear_all() {
// Drain every entry under the lock, keeping each (handle, processor); drop the processors (RAII-join
// their workers) only AFTER releasing the lock, then depart the monitors. See `remove_monitor`.
let mut drained: Vec<(
Option<iddcx::IDDCX_MONITOR>,
Option<crate::swap_chain_processor::SwapChainProcessor>,
)> = {
let mut lock = lock_monitors();
lock.drain(..)
.map(|mut m| (m.object, m.swap_chain_processor.take()))
.collect()
};
// Drop all workers FIRST (join + delete their swap-chains), THEN depart the monitors.
for (_, processor) in &mut drained {
drop(processor.take());
}
for (object, _) in drained {
if let Some(m) = object {
// SAFETY: `m` is a live IddCx monitor handle.
unsafe { wdk_iddcx::IddCxMonitorDeparture(m) };
}
}
}
/// `EvtCleanupCallback` (device removal, [`crate::callbacks::device_cleanup`]): drop every monitor's heavy
/// resources — the swap-chain processor workers (each RAII-joins its thread + deletes its swap-chain) — and
/// clear the list, WITHOUT `IddCxMonitorDeparture` (the framework tears the IddCx monitors down together
/// with the departing device; departing here would double-tear). Frees our worker threads promptly even
/// though the per-devnode WUDFHost (`ProcessSharingDisabled`) would also reap them when it exits.
pub fn cleanup_for_device_removal() {
let mut drained: Vec<Option<crate::swap_chain_processor::SwapChainProcessor>> = {
let mut lock = lock_monitors();
lock.drain(..)
.map(|mut m| m.swap_chain_processor.take())
.collect()
};
// Drop the workers (join their threads) AFTER releasing the lock — joining under MONITOR_MODES would
// head-block the control plane (same discipline as remove_monitor / clear_all).
for processor in &mut drained {
drop(processor.take());
}
}
/// Drop a pending entry by id (create failed before arrival).
fn remove_by_id(id: u32) {
lock_monitors().retain(|m| m.id != id);
}
/// Resolve the id to name a new monitor by: honor the host's `preferred` per-client id when it is in the
/// valid range (`1..=15`, so the IddCx `ConnectorIndex` = id stays `< MaxMonitorsSupported` = 16) AND not
/// currently live (two live monitors MUST have distinct ids/connectors); otherwise fall back to
/// [`alloc_monitor_id`] (auto, lowest-free). NEVER auto-departs a colliding live monitor — that would tear
/// down an unrelated concurrent client — so the live-uniqueness invariant is preserved even against a host
/// bug. `preferred == 0` (anonymous/TOFU/GameStream) always falls through to auto. Caller holds `MONITOR_MODES`.
fn resolve_id(modes: &[MonitorObject], preferred: u32) -> u32 {
if (1..=15).contains(&preferred) && !modes.iter().any(|m| m.id == preferred) {
preferred
} else {
alloc_monitor_id(modes)
}
}
/// The lowest monitor id (≥1) not currently live. Reusing freed ids (instead of a monotonic counter) keeps
/// the connector index / EDID serial / container GUID bounded to the number of concurrent monitors, so a
/// fresh ADD reuses a departed monitor's OS target slot rather than allocating a new one and orphaning the
/// old (the ghost-monitor accumulation that wedges ADD at 0x80070490 ERROR_NOT_FOUND). Caller holds
/// `MONITOR_MODES`. With ≤ N live ids, a free one always exists in `1..=N+1` (pigeonhole).
fn alloc_monitor_id(modes: &[MonitorObject]) -> u32 {
(1u32..=modes.len() as u32 + 1)
.find(|id| !modes.iter().any(|m| m.id == *id))
.unwrap_or(1)
}
/// A deterministic, monitor-unique container GUID (groups targets into a physical device). Derived from
/// `id` so it is stable + collision-free without a random source.
fn container_guid(id: u32) -> wdk_sys::GUID {
wdk_sys::GUID {
Data1: 0x7066_7664u32.wrapping_add(id),
Data2: 0x7044,
Data3: 0x5350,
Data4: [
0xa1,
0xb2,
0xc3,
0xd4,
0xe5,
0xf6,
(id >> 8) as u8,
id as u8,
],
}
}