feat(windows): pf-vdisplay IDD-push — HDR + pipelined zero-copy capture

HDR (display-driven, matching the WGC path):
- CTA-861.3 HDR EDID (BT.2020 primaries + HDR Static Metadata block) so Windows
  offers "Use HDR" on the virtual display. The host FOLLOWS the display's live
  advanced-color state, recreating the shared ring at the matching format
  (FP16 in HDR / BGRA in SDR) on a toggle — no freeze.
- Always emit Main10/BT.2020-PQ Rgb10a2 while the display is HDR; the client
  auto-detects PQ from the HEVC VUI (clients under-report VIDEO_CAP_10BIT).
  Generic HDR10 mastering SEI on every IDR.
- Generation-tagged `latest` (gen<<40|seq<<8|slot) + driver `is_stale` re-attach
  kill the toggle-time garbage frame and any stale-ring read.

Perf:
- Pipeline the encode loop (Capturer::pipeline_depth; IDD-push = 2): submit N+1
  before polling N so the convert/copy on the 3D engine overlaps the NVENC encode
  of N on the ASIC. PUNKTFUNK_IDD_DEPTH overrides (1 = synchronous).
- Rotating host output ring (OUT_RING) so the in-flight encode and the next
  convert never touch the same texture.
- HDR converts directly from the keyed-mutex slot's SRV into the output ring
  (drops the redundant slot->fp16 scratch copy); SDR copies the BGRA slot in.
  The slot mutex is held only across the convert/copy, not the encode.
  RING_LEN 3->6 for publish headroom.
- Capture-health diagnostic: new_fps vs repeat_fps under PUNKTFUNK_PERF (a low
  new_fps at a high send rate means the source isn't compositing, not an encode
  stall).

Validated live on the RTX box: 5120x1440@240 HDR streams; driver composes
~180 new fps, encode 240 fps @ ~4.3 ms p50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 00:35:52 +02:00
parent 994cc071d7
commit c7ef0e411a
25 changed files with 2743 additions and 313 deletions
@@ -6,8 +6,9 @@
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
use log::{error, info};
use wdf_umdf::{
@@ -16,7 +17,7 @@ use wdf_umdf::{
};
use wdf_umdf_sys::{IDARG_IN_ADAPTERSETRENDERADAPTER, LUID, NTSTATUS, WDFDEVICE, WDFREQUEST};
use crate::context::DeviceContext;
use crate::context::{DeviceContext, MonitorContext};
use crate::monitor::{
default_modes, Mode, MonitorData, MonitorObject, ADAPTER, MONITOR_MODES, NEXT_ID,
PREFERRED_RENDER_ADAPTER, PROTOCOL_VERSION, WATCHDOG_COUNTDOWN, WATCHDOG_TIMEOUT,
@@ -37,6 +38,16 @@ const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
/// Serializes monitor lifecycle ops — ADD / REMOVE / watchdog-teardown — against each other. Without
/// it, a watchdog expiry can drain an entry out from under an in-flight `do_add` (which releases the
/// `MONITOR_MODES` lock before the slow `create_monitor`), leaving `do_add` to return
/// `STATUS_UNSUCCESSFUL` → the host sees `ERROR_GEN_FAILURE`. This was the reconnect-churn fault.
static MONITOR_OP_LOCK: Mutex<()> = Mutex::new(());
/// A monitor created less than this ago is still in its host-side setup window (CCD commit + GDI-name
/// resolve + topology settle, ~5 s) and is never reaped by the watchdog — only by an explicit
/// CLEAR_ALL. Protects a freshly-born monitor from a transient PING gap during reconnect churn.
const MONITOR_GRACE: Duration = Duration::from_secs(6);
#[repr(C)]
struct AddParams {
width: u32,
@@ -117,7 +128,7 @@ pub extern "C-unwind" fn device_io_control(
IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes),
IOCTL_PING => NTSTATUS::STATUS_SUCCESS,
IOCTL_CLEAR_ALL => {
disconnect_all_monitors();
disconnect_all_monitors(true);
NTSTATUS::STATUS_SUCCESS
}
IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes),
@@ -136,6 +147,11 @@ unsafe fn do_add(
output_len: usize,
bytes: &mut usize,
) -> NTSTATUS {
// Serialize the whole ADD (push entry → create_monitor → verify) against the watchdog teardown +
// REMOVE, so an expiry can never drain this entry mid-flight. `create_monitor` is fast (the slow
// CCD/GDI work is host-side, after this returns), and PING/GET_WATCHDOG don't take this lock, so
// the host keeps the watchdog reset while we hold it.
let _op = MONITOR_OP_LOCK.lock().unwrap();
if input_len < size_of::<AddParams>() || output_len < size_of::<AddOut>() {
return NTSTATUS::STATUS_BUFFER_TOO_SMALL;
}
@@ -182,6 +198,7 @@ unsafe fn do_add(
target_id: 0,
adapter_luid_low: 0,
adapter_luid_high: 0,
created_at: Instant::now(),
});
// Create the IddCx monitor via the device context (captures target id + LUID into the entry).
@@ -226,18 +243,37 @@ unsafe fn do_remove(request: WDFREQUEST, input_len: usize) -> NTSTATUS {
let params = unsafe { &*pin.cast::<RemoveParams>() };
let guid = guid_key(&params.guid);
let mut lock = MONITOR_MODES.lock().unwrap();
if let Some(pos) = lock.iter().position(|m| m.guid == guid) {
let mon = lock.remove(pos);
if let Some(obj) = mon.object {
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
// Serialize against ADD + watchdog teardown (lock order: OP_LOCK → MONITOR_MODES).
let _op = MONITOR_OP_LOCK.lock().unwrap();
let mon = {
let mut lock = MONITOR_MODES.lock().unwrap();
match lock.iter().position(|m| m.guid == guid) {
Some(pos) => lock.remove(pos),
None => return NTSTATUS::STATUS_NOT_FOUND,
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
} else {
NTSTATUS::STATUS_NOT_FOUND
// MONITOR_MODES released here — the processor-join + departure below must not hold it.
};
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("REMOVE: departure failed: {e:?}");
}
}
info!("REMOVE target_id={}", mon.target_id);
NTSTATUS::STATUS_SUCCESS
}
/// Drop a monitor's live swap-chain processor BEFORE departure. The WDF context is an
/// `Arc<RwLock<MonitorContext>>` that WDF frees WITHOUT running Rust `Drop` (no `EvtCleanupCallback`
/// is wired), and the OS does not reliably call UNASSIGN on a host-initiated departure — so the
/// streaming `Direct3DDevice` (its ~dozens of D3D worker threads + tens of MB of VRAM) was orphaned
/// once per session, the dominant reconnect-churn leak. `get_mut` takes the context `RwLock`, so this
/// is safe against a concurrent OS unassign callback (whichever runs second sees `None`).
fn free_swap_chain_processor(monitor: *mut wdf_umdf_sys::IDDCX_MONITOR__) {
// SAFETY: `monitor` is a live IddCx monitor object whose context was init'd at creation.
let r = unsafe { MonitorContext::get_mut(monitor.cast(), |ctx| ctx.unassign_swap_chain()) };
if let Err(e) = r {
error!("free_swap_chain_processor: get_mut FAILED: {e:?}");
}
}
@@ -295,22 +331,46 @@ unsafe fn do_get_version(request: WDFREQUEST, output_len: usize, bytes: &mut usi
NTSTATUS::STATUS_SUCCESS
}
/// Tear down every monitor (watchdog expiry — the host is gone). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors() {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
for mon in lock.drain(..) {
/// Tear down monitors. `force` (CLEAR_ALL) reaps EVERYTHING — orphans from a crashed previous host;
/// the watchdog passes `false`, which spares any monitor still inside its creation grace
/// (`MONITOR_GRACE`) so a freshly-born monitor is never reaped mid-setup. Caller MUST hold
/// `MONITOR_OP_LOCK` (lock order: OP_LOCK → MONITOR_MODES). Mirrors SudoVDA's DisconnectAllMonitors.
fn disconnect_all_monitors_locked(force: bool) {
// Drain under the lock (fast); free processors + depart OUTSIDE it (the processor-join blocks).
let to_depart: Vec<MonitorObject> = {
let mut lock = MONITOR_MODES.lock().unwrap();
if lock.is_empty() {
return;
}
let mut keep: Vec<MonitorObject> = Vec::new();
let mut depart: Vec<MonitorObject> = Vec::new();
for mon in lock.drain(..) {
if !force && mon.created_at.elapsed() < MONITOR_GRACE {
keep.push(mon); // still in its host-side setup window — leave it alone
} else {
depart.push(mon);
}
}
*lock = keep;
depart
};
for mon in to_depart {
if let Some(obj) = mon.object {
free_swap_chain_processor(obj.as_ptr());
// SAFETY: `obj` is a live IddCx monitor object.
if let Err(e) = unsafe { IddCxMonitorDeparture(obj.as_ptr()) } {
error!("watchdog: monitor departure failed: {e:?}");
error!("teardown: monitor departure failed: {e:?}");
}
}
}
}
/// Public entry: takes `MONITOR_OP_LOCK`, then tears down. Used by CLEAR_ALL (`force = true`).
fn disconnect_all_monitors(force: bool) {
let _op = MONITOR_OP_LOCK.lock().unwrap();
disconnect_all_monitors_locked(force);
}
/// Start the watchdog thread (once). The host reads the timeout via GET_WATCHDOG and PINGs every
/// timeout/3; if it stops, the countdown reaches 0 and every monitor is torn down — so a crashed/gone
/// host never leaves a phantom display. Mirrors SudoVDA's RunWatchdog.
@@ -340,8 +400,14 @@ pub fn start_watchdog() {
.is_ok()
&& prev - 1 == 0
{
error!("watchdog expired (host stopped pinging) — tearing down all monitors");
disconnect_all_monitors();
// About to fire. Serialize against do_add/do_remove (so we never tear an entry out from
// under an in-flight ADD), then RE-CHECK the countdown under the lock: if a concurrent
// IOCTL (PING/ADD) reset it while we were acquiring the lock, the host is alive — abort.
let _op = MONITOR_OP_LOCK.lock().unwrap();
if WATCHDOG_COUNTDOWN.load(Ordering::Relaxed) == 0 {
error!("watchdog expired (host stopped pinging) — tearing down stale monitors");
disconnect_all_monitors_locked(false);
}
}
});
}