windows-drivers / probe-and-proto (pull_request) Successful in 30s
ci / bun-nix (pull_request) Successful in 56s
ci / docs-site (pull_request) Successful in 1m16s
ci / web (pull_request) Successful in 1m17s
ci / rust-arm64 (pull_request) Successful in 1m31s
windows-drivers / driver-build (pull_request) Successful in 1m47s
android / android (pull_request) Successful in 4m40s
ci / rust (pull_request) Successful in 9m54s
apple / swift (pull_request) Failing after 13m27s
apple / screenshots (pull_request) Skipped
The RX 9070 XT field A/B (2026-08-11/12 logs) convicted BOTH of our REALTIME GPU-scheduling levers of generating the metronomic capture-stall class the stall program has chased for weeks — compose-silence holes of 150-800 ms in which ETW shows NO process presenting while the GPU stays responsive: - the vdisplay driver's IddCxSetRealtimeGPUPriority raise beat at ~1.75-1.78 s (PFVD_NO_RT_GPU=1 alone removed that metronome: ~0.35 stalls/s metronomic -> 10 sparse aperiodic over 3.9 min); - the host auto-gate's HIGH->REALTIME upgrade (pf-frame dxgi.rs, T2.3) beat at ~3.58 s in the AV1 sessions where it promoted (vram_pct=1, 12:59:26); pinning PUNKTFUNK_GPU_PRIORITY_CLASS=high removed that residual too (13:45 session: zero metronomic, stall rate at the clean-run baseline). Neither period matches any punktfunk clock: the full periodic-actor census (driver: event-paced drain + 16 ms E_PENDING wait, 33 ms cursor poll, 3 s watchdog reap; host: 250 ms descriptor poll, 5/50/100 ms probes + ~2 s scanline retarget, 2 s VRAM gate, 2 s exclusive re-assert, 3.33 s pinger, 1 s stats, ~1 Hz phase-lock, fps/2 LTR marks) has nothing in the 1.69-2.29 s band, and every host-side actor ran unchanged in the A/B that killed the fast metronome. The periodicity is emergent from holding an unreachable-priority queue against the WDDM scheduler on this AMD family (the period even differs by which of our processes holds REALTIME); it is not a punktfunk cadence being amplified, so there is nothing punktfunk-periodic to fix - the fix is to stop holding REALTIME by default, which is also canonical parity (no shipping IDD raises it, and HIGH was the class that delivered the original Sunshine-parity encode win). - Driver: PFVD_NO_RT_GPU (default-ON, opt-OUT) becomes the PFVD_RT_GPU ladder, default OFF on every vendor: unset = no raise (canonical IDD behavior); =thread = SetGPUThreadPriority(+7), a graduated in-band middle rung for field A/B (not default: unmeasured here, and the host measured the same call as "no help" for its own starvation case); anything else = the old REALTIME DDI. PFVD_NO_RT_GPU stays recognized and WINS over the opt-in, so the field boxes that carry it through the default-ON era keep meaning OFF. Both directions remain A/B-able without a rebuild (machine env + device restart). The CPU half of the original branch-2 hardening (MMCSS / TIME_CRITICAL) is untouched - it addressed the delivery holes that were actually observed. - Host: PUNKTFUNK_GPU_PRIORITY_CLASS default auto -> high. `auto` (the gated REALTIME upgrade) stays available as an explicit opt-in, `realtime` still pins; unrecognized values now land on the HIGH default instead of silently opting into the gate - a typo must not buy the hazard. The VRAM/HAGS gate machinery is unchanged for `auto`; it guards the NVENC-hang hazard but cannot see this one. - stall.rs: the no-OS-event METRONOMIC warning now carries rt_gpu_driver / rt_gpu_host fields (the machine-env state of both levers) and names clearing them as the FIRST cure, ahead of the display-hardware suspects - a field log self-answers the triage question this program just spent a week on. No console policy axis for the driver knob: the lever is default-safe now, the driver reads config at WUDFHost scope where machine env already matches the device-restart lifecycle, and a policy axis would need pf-driver-proto churn (or a device-key registry write) for an experimental lever that only exists to be A/B-ed. If the `thread` rung ever proves out as a default-worthy raise, that is the moment to revisit.
622 lines
35 KiB
Rust
622 lines
35 KiB
Rust
//! The swap-chain processor (STEP 5 + STEP 6): a worker thread that DRAINS the IddCx swap-chain (so the
|
||
//! virtual monitor stays a usable display) and PUBLISHES each acquired surface into the host-created
|
||
//! shared ring (the IDD-push path).
|
||
//!
|
||
//! The OS presents the composited desktop to the driver through a swap-chain; the driver MUST consume it
|
||
//! (acquire → finished-processing) or the monitor stalls. STEP 5 binds our render device to the swap-chain
|
||
//! (`IddCxSwapChainSetDevice`) and loops acquire/finish. STEP 6 lazily attaches a [`FramePublisher`] to
|
||
//! the host's shared ring and, on each acquired frame, `CopyResource`s `out.MetaData.pSurface` into the
|
||
//! next ring slot before finishing the frame (a non-IDD-push session simply never attaches and keeps
|
||
//! draining). Frames the ring can NOT take feed the [`FrameStash`] instead, which every fresh attach
|
||
//! republishes as its instant first frame — the first-frame guarantee that makes a session opened onto
|
||
//! an IDLE desktop show a picture without waiting for anything to dirty the display.
|
||
//!
|
||
//! Ported from the proven oracle (`packaging/windows/vdisplay-driver/pf-vdisplay/src/
|
||
//! swap_chain_processor.rs`) onto wdk-sys + wdk-iddcx. The oracle's `wdf_umdf`/`wdf_umdf_sys` are
|
||
//! replaced by `wdk_sys::iddcx::*` + the `wdk_iddcx` DDI wrappers. Those wrappers return a RAW
|
||
//! `NTSTATUS` (`i32`) that is HRESULT-shaped for the swap-chain DDIs, so we classify it by hand
|
||
//! (`hr >= 0` = success; `0x8000_000A` = E_PENDING; `hr < 0 && != E_PENDING` = error) rather than with
|
||
//! `nt_success`.
|
||
|
||
use std::{
|
||
mem::size_of,
|
||
sync::{
|
||
Arc,
|
||
atomic::{AtomicBool, Ordering},
|
||
},
|
||
thread::{self, JoinHandle},
|
||
time::{Duration, Instant},
|
||
};
|
||
|
||
use wdk_sys::iddcx::{
|
||
IDARG_IN_RELEASEANDACQUIREBUFFER2, IDARG_IN_SETREALTIMEGPUPRIORITY,
|
||
IDARG_IN_SWAPCHAINSETDEVICE, IDARG_OUT_RELEASEANDACQUIREBUFFER2, IDDCX_SWAPCHAIN,
|
||
};
|
||
// `HANDLE` is the shared wdk-sys typedef (`crate::types`) re-used by the iddcx bindings — take it from
|
||
// the crate root, which is guaranteed to export it (the iddcx module only re-exports it if bindgen
|
||
// re-declared it there). It is the same type as `IDARG_IN_SETSWAPCHAIN.hNextSurfaceAvailable`.
|
||
use wdk_sys::{HANDLE, NTSTATUS, WDFOBJECT, call_unsafe_wdf_function_binding};
|
||
use windows::{
|
||
Win32::{
|
||
Foundation::HANDLE as WHANDLE,
|
||
Graphics::{
|
||
Direct3D11::ID3D11Texture2D,
|
||
Dxgi::{IDXGIDevice, IDXGIResource},
|
||
},
|
||
System::Threading::{
|
||
AvRevertMmThreadCharacteristics, AvSetMmThreadCharacteristicsW, GetCurrentThread,
|
||
SetThreadPriority, THREAD_PRIORITY_TIME_CRITICAL, WaitForSingleObject,
|
||
},
|
||
},
|
||
core::{Interface, w},
|
||
};
|
||
|
||
use crate::{
|
||
direct_3d_device::Direct3DDevice,
|
||
frame_transport::{FramePublisher, FrameStash, PublishOutcome},
|
||
};
|
||
|
||
/// E_PENDING — `ReleaseAndAcquireBuffer2` returns this (HRESULT-shaped) when the swap-chain is valid but
|
||
/// DWM has composed no new frame yet; wait on the surface-available event and retry.
|
||
const E_PENDING: u32 = 0x8000_000A;
|
||
/// `WAIT_TIMEOUT` from `WaitForSingleObject` (defined locally to avoid pulling a windows-crate constant
|
||
/// type into the comparison — the raw `WAIT_EVENT.0` is just a `u32`).
|
||
const WAIT_TIMEOUT_U32: u32 = 0x0000_0102;
|
||
|
||
/// HRESULT-shaped success test for the swap-chain DDIs (raw `NTSTATUS`/HRESULT: success iff non-negative).
|
||
#[inline]
|
||
fn hr_success(hr: NTSTATUS) -> bool {
|
||
hr >= 0
|
||
}
|
||
|
||
/// How (whether) the swap-chain processing device's GPU scheduling is raised — the
|
||
/// interval-stutter program's A/B ladder, resolved once per WUDFHost process from the MACHINE
|
||
/// environment (the driver runs as LocalService: `setx /M PFVD_RT_GPU 1` + a device restart
|
||
/// applies it; the [`crate::log`] `OnceLock` pattern).
|
||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||
enum RtGpuMode {
|
||
/// No raise at all — canonical-IDD scheduling, and the DEFAULT since the 2026-08 field
|
||
/// conviction (see [`rt_gpu_mode`]).
|
||
Off,
|
||
/// `PFVD_RT_GPU=thread`: `IDXGIDevice::SetGPUThreadPriority(7)` — the graduated middle rung.
|
||
/// A per-device GPU *thread* priority inside the band ordinary applications can also reach,
|
||
/// so it biases the scheduler without the REALTIME rung's unreachable-preemption hazard. Not
|
||
/// the default because it is unmeasured here — and the host process measured the same call as
|
||
/// "no help" for its encode-starvation case (`pf-frame/src/dxgi.rs`) — so it exists purely as
|
||
/// the field-A/B rung between OFF and REALTIME.
|
||
GpuThread,
|
||
/// `PFVD_RT_GPU=<anything else>`: the IddCx 1.9 `IddCxSetRealtimeGPUPriority` DDI — the old
|
||
/// default-ON behavior, "higher priority than any regular application can set".
|
||
Realtime,
|
||
}
|
||
|
||
/// Resolve the [`RtGpuMode`] ladder. Default **OFF**: no canonical IDD driver raises its
|
||
/// swap-chain device's GPU priority, and a 2026-08 field A/B on an RX 9070 XT convicted our
|
||
/// REALTIME raise as the amplifier of a metronomic ~1.8 s capture-stall class — every ~1.8 s
|
||
/// EVERY process's presents stopped for 150–800 ms while the GPU stayed responsive (a starved
|
||
/// present path, not a stalled engine); clearing the raise removed the metronome entirely.
|
||
/// The raise was added as speculative "outranks GPU contention" hardening (branch-2 of the
|
||
/// disturbance-immunity program) whose CPU half — MMCSS / TIME_CRITICAL on this thread — is the
|
||
/// part that addressed the observed delivery holes and REMAINS in force; the GPU half never had
|
||
/// a measured win and now has a measured loss, so it is opt-in on every vendor (NVIDIA is
|
||
/// untested in either direction, and a vendor-split default would double the support matrix on
|
||
/// no evidence).
|
||
///
|
||
/// Precedence: the old opt-OUT (`PFVD_NO_RT_GPU`, any value) wins over the new opt-IN — a field
|
||
/// box that carried it through the default-ON era must keep meaning OFF no matter what is set
|
||
/// beside it. Both directions stay A/B-able without a rebuild.
|
||
fn rt_gpu_mode() -> RtGpuMode {
|
||
use std::sync::OnceLock;
|
||
static MODE: OnceLock<RtGpuMode> = OnceLock::new();
|
||
*MODE.get_or_init(|| {
|
||
if std::env::var_os("PFVD_NO_RT_GPU").is_some() {
|
||
return RtGpuMode::Off;
|
||
}
|
||
match std::env::var_os("PFVD_RT_GPU") {
|
||
None => RtGpuMode::Off,
|
||
Some(v) if v.eq_ignore_ascii_case("thread") => RtGpuMode::GpuThread,
|
||
Some(_) => RtGpuMode::Realtime,
|
||
}
|
||
})
|
||
}
|
||
|
||
/// A minimal newtype to move a raw pointer / handle across the thread boundary. The wrapped value is a
|
||
/// raw IddCx swap-chain handle or an event HANDLE (both raw pointers, framework-managed) — sending them
|
||
/// to the worker is sound because only this thread touches them and the framework synchronises lifetime.
|
||
struct Sendable<T>(T);
|
||
// SAFETY: see the type doc — the wrapped raw handle is owned by the worker for its lifetime.
|
||
unsafe impl<T> Send for Sendable<T> {}
|
||
|
||
pub struct SwapChainProcessor {
|
||
terminate: Arc<AtomicBool>,
|
||
thread: Option<JoinHandle<()>>,
|
||
}
|
||
|
||
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
|
||
// terminate flag.
|
||
unsafe impl Send for SwapChainProcessor {}
|
||
// SAFETY: as above — the raw pointer is only touched by the serialised worker, so a shared
|
||
// `&SwapChainProcessor` reference exposes no unsynchronised access.
|
||
unsafe impl Sync for SwapChainProcessor {}
|
||
|
||
impl SwapChainProcessor {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
terminate: Arc::new(AtomicBool::new(false)),
|
||
thread: None,
|
||
}
|
||
}
|
||
|
||
pub fn run(
|
||
&mut self,
|
||
swap_chain: IDDCX_SWAPCHAIN,
|
||
device: Arc<Direct3DDevice>,
|
||
available_buffer_event: HANDLE,
|
||
target_id: u32,
|
||
render_luid_low: u32,
|
||
render_luid_high: i32,
|
||
) {
|
||
let available_buffer_event = Sendable(available_buffer_event);
|
||
let swap_chain = Sendable(swap_chain);
|
||
let terminate = self.terminate.clone();
|
||
|
||
let join_handle = thread::spawn(move || {
|
||
// Rust 2021 disjoint closure captures would otherwise grab the raw `swap_chain.0` /
|
||
// `available_buffer_event.0` FIELDS directly (defeating the `Sendable` Send wrapper, since the
|
||
// inner `*mut IDDCX_SWAPCHAIN__` / `HANDLE` are `!Send`). Rebind the WHOLE wrappers here so the
|
||
// closure captures them as `Sendable<_>` (which IS `Send`), then unwrap from the locals.
|
||
let swap_chain = swap_chain;
|
||
let available_buffer_event = available_buffer_event;
|
||
// It is very important to prioritize this thread by making use of the Multimedia Scheduler
|
||
// Service. It will intelligently prioritize the thread for improved throughput in high
|
||
// CPU-load scenarios.
|
||
let mut av_task = 0u32;
|
||
// SAFETY: `w!("Distribution")` is a 'static null-terminated UTF-16 task name; `av_task` is a
|
||
// valid local out-param. The returned handle is reverted with AvRevertMmThreadCharacteristics.
|
||
let res = unsafe { AvSetMmThreadCharacteristicsW(w!("Distribution"), &mut av_task) };
|
||
// MMCSS can fail under the restricted WUDFHost token ('Distribution' task unregistered /
|
||
// service unavailable). The MS sample CONTINUES unprioritized — never abort: returning
|
||
// here would leave the assigned swap-chain undrained (the monitor stalls, DWM blocks on
|
||
// it) and leak the WDF swap-chain object until device teardown. But "unprioritized" is
|
||
// not acceptable either: this thread is the whole display's frame pump, and at normal
|
||
// priority a display-stack disturbance (DDC/HPD servicing DPC pressure, poller-software
|
||
// storms) can starve it into multi-hundred-ms delivery holes. Fall back to
|
||
// TIME_CRITICAL — the highest band available without the realtime priority class, and
|
||
// the closest to what MMCSS 'Distribution' would have granted. The thread spends its
|
||
// life blocked on the surface-available event / keyed mutex, so it cannot starve others.
|
||
let av_handle = match res {
|
||
Ok(h) => Some(h),
|
||
Err(e) => {
|
||
// SAFETY: plain FFI; GetCurrentThread returns a pseudo-handle (never fails,
|
||
// nothing to close), SetThreadPriority on it affects only this thread.
|
||
let fallback = unsafe {
|
||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)
|
||
};
|
||
dbglog!(
|
||
"[pf-vd] swap-chain: MMCSS prioritization failed ({e:?}) — fell back to \
|
||
TIME_CRITICAL thread priority (ok={})",
|
||
fallback.is_ok()
|
||
);
|
||
None
|
||
}
|
||
};
|
||
|
||
Self::run_core(
|
||
swap_chain.0,
|
||
&device,
|
||
available_buffer_event.0,
|
||
&terminate,
|
||
target_id,
|
||
render_luid_low,
|
||
render_luid_high,
|
||
);
|
||
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core RETURNED (target={target_id}) — deleting swap-chain, device drops next"
|
||
);
|
||
|
||
// Delete the swap-chain WDF object BEFORE the `Arc<Direct3DDevice>` drops (the swap-chain
|
||
// referenced our device). `WdfObjectDelete` takes a WDFOBJECT.
|
||
// SAFETY: `swap_chain` is a live IddCx swap-chain handle; we own the sole reference here and
|
||
// the drain loop has exited.
|
||
unsafe {
|
||
call_unsafe_wdf_function_binding!(WdfObjectDelete, swap_chain.0 as WDFOBJECT);
|
||
}
|
||
|
||
// Revert the thread to normal once it's done (only if MMCSS was actually engaged).
|
||
if let Some(h) = av_handle {
|
||
// SAFETY: `h` is the live characteristics handle returned by
|
||
// AvSetMmThreadCharacteristicsW above, reverted exactly once here at thread exit.
|
||
let res = unsafe { AvRevertMmThreadCharacteristics(h) };
|
||
if let Err(e) = res {
|
||
dbglog!("[pf-vd] swap-chain: failed to revert prioritized thread: {e:?}");
|
||
}
|
||
}
|
||
});
|
||
|
||
self.thread = Some(join_handle);
|
||
}
|
||
|
||
fn run_core(
|
||
swap_chain: IDDCX_SWAPCHAIN,
|
||
device: &Direct3DDevice,
|
||
available_buffer_event: HANDLE,
|
||
terminate: &AtomicBool,
|
||
target_id: u32,
|
||
render_luid_low: u32,
|
||
render_luid_high: i32,
|
||
) {
|
||
// SetDevice fails (0x887A0026, FACILITY_DXGI) when the monitor briefly flaps INACTIVE during
|
||
// topology activation — the OS unassigns + re-assigns the swap-chain, and a fresh run_core thread
|
||
// can lose the race to the unassign. Retry briefly so a stable re-assign binds the device instead
|
||
// of giving up on the first transient failure. `terminate` (set when the OS unassigns + drops the
|
||
// processor) breaks us out promptly.
|
||
//
|
||
// Cast to IDXGIDevice ONCE and BORROW it to the swap-chain across all retries. Re-casting +
|
||
// `into_raw()`'ing on EVERY attempt — and a flapping monitor fails several attempts per session —
|
||
// orphans an IDXGIDevice reference per failure, pinning the D3D device (and its ~dozen worker
|
||
// threads + tens of MB of VRAM) so it is NEVER freed when the processor drops. `as_raw()` keeps
|
||
// our single reference (released right after the loop); IddCx AddRefs its own on success, and
|
||
// `device` keeps the object alive for the drain loop regardless.
|
||
let dxgi_device = match device.device.cast::<IDXGIDevice>() {
|
||
Ok(d) => d,
|
||
Err(e) => {
|
||
dbglog!("[pf-vd] swap-chain: failed to cast ID3D11Device to IDXGIDevice: {e:?}");
|
||
return;
|
||
}
|
||
};
|
||
// Built zeroed + field-assigned (driver style) — robust against a bindgen field-set difference.
|
||
let mut set_device = pod_init!(IDARG_IN_SWAPCHAINSETDEVICE);
|
||
set_device.pDevice = dxgi_device.as_raw().cast();
|
||
let mut set_ok = false;
|
||
let mut terminated = false;
|
||
for attempt in 0..60u32 {
|
||
if terminate.load(Ordering::Relaxed) {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: terminated during SetDevice (attempt {attempt}, target={target_id})"
|
||
);
|
||
terminated = true;
|
||
break;
|
||
}
|
||
// SAFETY: driver is loaded; `swap_chain` is valid; `set_device` points to valid local storage.
|
||
let hr = unsafe { wdk_iddcx::IddCxSwapChainSetDevice(swap_chain, &set_device) };
|
||
if hr_success(hr) {
|
||
set_ok = true;
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: SetDevice OK (target={target_id}, attempt={attempt}) — entering drain loop"
|
||
);
|
||
break;
|
||
}
|
||
if attempt == 0 {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: SetDevice attempt 0 failed ({hr:#x}) — retrying up to 60x@50ms (monitor may be flapping)"
|
||
);
|
||
}
|
||
thread::sleep(Duration::from_millis(50));
|
||
}
|
||
// GPU-scheduling raise for the swap-chain processing device — OPT-IN, default none (see
|
||
// [`rt_gpu_mode`] for the field conviction that inverted the old default-ON). What used
|
||
// to be sold as stall immunity ("swap-chain buffer processing outruns ordinary GPU
|
||
// contention") preempts the game's and DWM's own queues at a level apps can't reach, and
|
||
// on an AMD field box that manifested as the metronomic content-starving stall class the
|
||
// stall program spent weeks attributing. The CPU-side half of that hardening (MMCSS /
|
||
// TIME_CRITICAL, above) is untouched — it addressed the delivery holes actually observed.
|
||
//
|
||
// Both raises are best-effort, never fatal, and issued while our borrowed device
|
||
// reference is still alive (IddCx uses it synchronously; the DXGI call is direct). The
|
||
// REALTIME slot is guaranteed populated (`IddMinimumVersionRequired = 10`, lib.rs), but
|
||
// the DDI may still decline (e.g. E_NOTIMPL on pre-WDDM-3.0 hardware).
|
||
if set_ok {
|
||
match rt_gpu_mode() {
|
||
RtGpuMode::Off => {}
|
||
RtGpuMode::GpuThread => {
|
||
// SAFETY: `dxgi_device` is the live device just bound to the swap-chain; the
|
||
// call takes a scalar in the documented −7..=7 band and retains nothing.
|
||
let res = unsafe { dxgi_device.SetGPUThreadPriority(7) };
|
||
dbglog!(
|
||
"[pf-vd] swap-chain: GPU thread priority +7 (PFVD_RT_GPU=thread) — ok={} (target={target_id})",
|
||
res.is_ok()
|
||
);
|
||
}
|
||
RtGpuMode::Realtime => {
|
||
let mut rt = pod_init!(IDARG_IN_SETREALTIMEGPUPRIORITY);
|
||
rt.pDevice = dxgi_device.as_raw().cast();
|
||
// SAFETY: driver is loaded; `swap_chain` is the live assigned swap-chain whose
|
||
// device bind just succeeded; `rt.pDevice` is that same bound DXGI device,
|
||
// alive across the synchronous call; `rt` points to valid local storage.
|
||
let hr = unsafe { wdk_iddcx::IddCxSetRealtimeGPUPriority(swap_chain, &rt) };
|
||
if hr_success(hr) {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain: processing device raised to REALTIME GPU priority (PFVD_RT_GPU) (target={target_id})"
|
||
);
|
||
} else {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain: realtime GPU priority declined ({hr:#x}) — normal scheduling (target={target_id})"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Release our borrowed device reference — IddCx holds its own now, or we gave up. (Explicit drop
|
||
// so NLL can't release it mid-loop while the swap-chain still references the raw ptr.)
|
||
drop(dxgi_device);
|
||
if !set_ok {
|
||
if !terminated {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: SetDevice never succeeded after retries (target={target_id}) — giving up"
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring over the SEALED channel. The
|
||
// frame objects are unnamed — the host duplicates their handles into this process and delivers
|
||
// the values via IOCTL_SET_FRAME_CHANNEL, which the control plane stashes on our monitor
|
||
// (`monitor::take_frame_channel`). Until a delivery lands we just drain — exactly the STEP-5
|
||
// behaviour — so a non-IDD-push session never stalls. The frame-channel stash is polled every
|
||
// iteration (attach latency = first-frame latency, since attach republishes the FrameStash).
|
||
// STEP 6 sibling-join fix: re-adopt a FramePublisher PRESERVED across a swap-chain
|
||
// unassign→reassign flap. When a SIBLING display churns the desktop topology (a second client
|
||
// joining / leaving / resizing), the OS reassigns THIS monitor's swap-chain and the previous
|
||
// worker exited — but the host-owned ring it published into is still live, and the host only
|
||
// re-delivers the frame channel on a ring RECREATE (a descriptor change). Without this the
|
||
// fresh worker has nothing to attach to and the first client's stream freezes (repeat frames
|
||
// forever). Re-adopt ONLY when the freshly-assigned swap-chain renders on the SAME adapter as
|
||
// the preserved publisher (same pooled Direct3DDevice → its immediate context + opened ring
|
||
// textures are valid); a mismatch drops it and falls back to a fresh channel delivery. A
|
||
// preserved publisher that the host superseded meanwhile (ring recreate → is_stale, or a
|
||
// pending delivery) is dropped + replaced by the existing re-attach logic at the loop top.
|
||
let mut publisher: Option<FramePublisher> =
|
||
crate::monitor::take_preserved_publisher(target_id).and_then(|p| {
|
||
if p.render_adapter() == (render_luid_low, render_luid_high) {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: re-adopted preserved publisher (target={target_id}) — resuming the host ring across the swap-chain flap"
|
||
);
|
||
Some(p)
|
||
} else {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: preserved publisher's render adapter changed (target={target_id}) — dropping it, will re-attach from a fresh channel"
|
||
);
|
||
None
|
||
}
|
||
});
|
||
// The FIRST-FRAME stash (see `FrameStash`): the retained last composed frame, republished
|
||
// into every fresh ring at attach so a session opening onto an idle desktop is never black.
|
||
// Re-adopted across a swap-chain flap on the same adapter (`take_preserved_stash` drops a
|
||
// cross-adapter one — its texture lives on the other adapter's pooled device).
|
||
let mut stash: FrameStash =
|
||
crate::monitor::take_preserved_stash(target_id, render_luid_low, render_luid_high)
|
||
.unwrap_or_default();
|
||
|
||
let mut logged_pending = false;
|
||
let mut logged_frame = false;
|
||
// The frame-channel delivery gate (see `monitor::frame_channel_gen`): the loop only takes
|
||
// the monitors mutex when a delivery LANDED since it last looked. Seeded one behind the
|
||
// current generation so a delivery that arrived before this worker started (host delivered
|
||
// ahead of the swap-chain assign) is checked on the very first pass.
|
||
let mut seen_chan_gen = crate::monitor::frame_channel_gen().wrapping_sub(1);
|
||
loop {
|
||
// Check terminate at the TOP, every iteration. The success branch below does NOT re-check it,
|
||
// so during a CONTINUOUS frame burst (DWM rendering the freshly-activated desktop) a thread the
|
||
// OS unassigns — or that the processor is dropping — never sees the flag and loops on, pinning
|
||
// its D3D device (and ~36 NVIDIA worker threads). That is THE reconnect leak; it only
|
||
// reproduced at full speed (E_PENDING gaps DO check terminate and masked it under a debugger).
|
||
// Without this, `SwapChainProcessor::drop`'s join can also block until the burst ends.
|
||
if terminate.load(Ordering::Relaxed) {
|
||
break;
|
||
}
|
||
|
||
// The lock-free delivery gate: `chan_pending` is true only when a `set_frame_channel`
|
||
// landed since the last pass — the two mutex-taking checks below (`has_frame_channel`,
|
||
// `take_frame_channel`) used to run EVERY pass (≥60 locks/s per worker on a mutex
|
||
// contended by the whole control plane, the mode DDIs and the watchdog); now the
|
||
// steady state takes no lock at all.
|
||
let chan_gen = crate::monitor::frame_channel_gen();
|
||
let chan_pending = chan_gen != seen_chan_gen;
|
||
// Re-attach triggers, either of:
|
||
// * `is_stale` — the host recreated the ring mid-session (HDR flip): it bumps OUR header's
|
||
// generation and re-delivers; without dropping here we'd keep CopyResource'ing into the
|
||
// stale ring, whose format now mismatches the surface → the publish() format-guard drops
|
||
// every frame and the stream freezes until the next swap-chain recreate.
|
||
// * a PENDING delivery (newest-wins) — a host build-retry creates a whole NEW ring with a
|
||
// DIFFERENT header mapping; the old publisher's header never changes, so `is_stale` can't
|
||
// fire. The host only delivers after fully (re)creating a ring, so a pending delivery
|
||
// always supersedes whatever we're attached to.
|
||
if publisher.as_ref().is_some_and(FramePublisher::is_stale)
|
||
|| (publisher.is_some()
|
||
&& chan_pending
|
||
&& crate::monitor::has_frame_channel(target_id))
|
||
{
|
||
// Harvest the superseded ring's last-published frame into the stash BEFORE dropping
|
||
// the publisher: between sessions the driver keeps publishing into the (host-side
|
||
// dead) previous ring, so that slot holds the CURRENT desktop image — exactly what
|
||
// the new ring's attach below republishes as its instant first frame.
|
||
if let Some(p) = publisher.take() {
|
||
p.harvest_into(&device.device, &mut stash);
|
||
}
|
||
}
|
||
// Lazy-attach at the loop TOP so we keep trying even while the display is idle (E_PENDING /
|
||
// no frames presented yet), not only when a frame is acquired — gated by `chan_pending`
|
||
// (a delivery can only appear via `set_frame_channel`, which bumps the generation):
|
||
// attach latency is still first-frame latency, since the attach itself republishes the
|
||
// stash. A taken delivery is consumed whether the attach succeeds or not (on failure its
|
||
// handles are closed inside from_channel, the host's wait-for-attach reads the status
|
||
// code, and any retry is a NEW delivery — with its own bump). `target_id` binds the
|
||
// attach: the mapped ring must name THIS monitor (proto v3 validation inside
|
||
// from_channel — a cross-delivered ring is refused, never published into).
|
||
if publisher.is_none()
|
||
&& chan_pending
|
||
&& let Some(channel) = crate::monitor::take_frame_channel(target_id)
|
||
&& let Ok(mut p) = FramePublisher::from_channel(
|
||
channel,
|
||
target_id,
|
||
render_luid_low,
|
||
render_luid_high,
|
||
&device.device,
|
||
&device.device_context,
|
||
)
|
||
{
|
||
// FIRST-FRAME GUARANTEE: republish the retained desktop image into the fresh ring
|
||
// immediately. On an idle desktop DWM composes nothing, so without this the host
|
||
// would wait (and kick synthetic input) for a frame that may never come. A
|
||
// stale-descriptor stash (e.g. pre-HDR-flip) is rejected by publish()'s guard —
|
||
// at worst the old wait-for-compose path.
|
||
if let Some(t) = stash.texture()
|
||
&& p.publish(t) == PublishOutcome::Published
|
||
{
|
||
dbglog!(
|
||
"[pf-vd] frame-push(driver): republished the retained frame into the fresh ring (target={target_id}) — instant first frame, no compose needed"
|
||
);
|
||
}
|
||
publisher = Some(p);
|
||
}
|
||
// The pending generation was serviced above — whichever branch ran, a lock-taking
|
||
// check happened (`has_frame_channel` and/or `take_frame_channel`), so this pass has
|
||
// seen everything up to `chan_gen`. A delivery racing in between bumps past it and
|
||
// re-arms the gate on the next pass.
|
||
if chan_pending {
|
||
seen_chan_gen = chan_gen;
|
||
}
|
||
|
||
// ...Buffer2 is required once CAN_PROCESS_FP16 is set. AcquireSystemMemoryBuffer=FALSE keeps
|
||
// the GPU surface (out.MetaData.pSurface) — STEP 6 publishes it into the shared ring in the
|
||
// success branch below. Built zeroed + field-assigned (driver style) so a bindgen field-set
|
||
// difference can't break a positional struct literal.
|
||
let mut in_args = pod_init!(IDARG_IN_RELEASEANDACQUIREBUFFER2);
|
||
#[allow(clippy::cast_possible_truncation)]
|
||
{
|
||
in_args.Size = size_of::<IDARG_IN_RELEASEANDACQUIREBUFFER2>() as u32;
|
||
}
|
||
in_args.AcquireSystemMemoryBuffer = 0;
|
||
// `pod_init!` (zeroed, not `::default()`) — consistent with every other IddCx out-struct
|
||
// in this driver, and robust whether or not bindgen derives `Default` for this type (its
|
||
// `MetaData` field carries a raw `pSurface` pointer + union which can suppress the derive).
|
||
let mut buffer = pod_init!(IDARG_OUT_RELEASEANDACQUIREBUFFER2);
|
||
// SAFETY: driver is loaded; `swap_chain` is valid; in/out point to valid local storage.
|
||
let hr: NTSTATUS = unsafe {
|
||
wdk_iddcx::IddCxSwapChainReleaseAndAcquireBuffer2(
|
||
swap_chain,
|
||
&mut in_args,
|
||
&mut buffer,
|
||
)
|
||
};
|
||
|
||
// v2 telemetry (stall attribution): stamp the drain heartbeat — plus the last-acquire
|
||
// on a pass that got a composed frame — into the shared header EVERY pass, E_PENDING
|
||
// included (the wait below is ≤16 ms, so the heartbeat cadence bounds how stale it can
|
||
// read while this thread is scheduled). What lets the host split a capture stall into
|
||
// worker-starved / DWM-composed-nothing / our-delivery-leg.
|
||
if let Some(p) = publisher.as_ref() {
|
||
p.note_drain(hr_success(hr));
|
||
}
|
||
|
||
if (hr as u32) == E_PENDING {
|
||
if !logged_pending {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: E_PENDING (target={target_id}) — swap-chain valid but DWM has composed NO frame yet"
|
||
);
|
||
logged_pending = true;
|
||
}
|
||
// SAFETY: `available_buffer_event` is the framework-provided surface-available event.
|
||
let wait_result =
|
||
unsafe { WaitForSingleObject(WHANDLE(available_buffer_event.cast()), 16).0 };
|
||
|
||
// thread requested an end
|
||
if terminate.load(Ordering::Relaxed) {
|
||
break;
|
||
}
|
||
|
||
// WAIT_OBJECT_0 | WAIT_TIMEOUT
|
||
if matches!(wait_result, 0 | WAIT_TIMEOUT_U32) {
|
||
// We have a new buffer (or timed out), so try the AcquireBuffer again.
|
||
continue;
|
||
}
|
||
|
||
// The wait was cancelled or something unexpected happened.
|
||
break;
|
||
} else if hr_success(hr) {
|
||
if !logged_frame {
|
||
dbglog!(
|
||
"[pf-vd] swap-chain run_core: FIRST FRAME acquired (target={target_id}) — DWM IS compositing the virtual display!"
|
||
);
|
||
logged_frame = true;
|
||
}
|
||
// STEP 6: copy the acquired surface into the shared ring BEFORE FinishedProcessingFrame
|
||
// (the surface is valid until the next ReleaseAndAcquire). Every successful acquire
|
||
// TRANSFERS one surface reference to the driver — the MS sample `Attach`es it into a
|
||
// ComPtr and `Reset`s BEFORE FinishedProcessingFrame, warning that a driver which
|
||
// "forgets to release the reference" leaves the surfaces alive after the swap-chain is
|
||
// destroyed. Holding it (the old `from_raw_borrowed`) leaked the swap-chain's whole
|
||
// surface set per assign/unassign cycle (reconnect, mode change, HDR flip) — so adopt
|
||
// the reference UNCONDITIONALLY (publisher or not); it is released when `res` drops at
|
||
// the end of this block. (Publisher attach happens at the loop top.)
|
||
{
|
||
let raw = buffer.MetaData.pSurface as *mut core::ffi::c_void;
|
||
if !raw.is_null() {
|
||
// SAFETY: `raw` is the live surface IddCx just handed us, carrying the acquire's
|
||
// transferred reference; `from_raw` adopts exactly that reference (released on
|
||
// drop, below — the queued GPU copy is unaffected: D3D defers destruction, and
|
||
// the copy is ordered before the consumer via the slot keyed mutex).
|
||
let res = unsafe { IDXGIResource::from_raw(raw) };
|
||
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
|
||
match publisher.as_mut().map(|p| p.publish(&tex)) {
|
||
// Ring took it (or the host is alive and busy) — nothing to retain.
|
||
Some(PublishOutcome::Published | PublishOutcome::Dropped) => {}
|
||
// No ring, or the surface's descriptor doesn't match it (a mode-set /
|
||
// HDR flip racing the host's ring recreate): RETAIN the frame — it is
|
||
// the desktop image the next attach republishes as its first frame.
|
||
// Unattached/mismatched composes are damage-driven and transient, so
|
||
// the extra copy costs nothing at steady state.
|
||
Some(PublishOutcome::DescMismatch) | None => {
|
||
stash.store(
|
||
&device.device,
|
||
&device.device_context,
|
||
&tex,
|
||
Instant::now(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// `res` drops here → the acquire's surface reference is released, pre-Finished.
|
||
}
|
||
}
|
||
|
||
// SAFETY: driver is loaded; `swap_chain` is valid.
|
||
let hr = unsafe { wdk_iddcx::IddCxSwapChainFinishedProcessingFrame(swap_chain) };
|
||
if !hr_success(hr) {
|
||
break;
|
||
}
|
||
} else {
|
||
// The swap-chain was likely abandoned (e.g. DXGI_ERROR_ACCESS_LOST) — exit the loop.
|
||
break;
|
||
}
|
||
}
|
||
|
||
// STEP 6 sibling-join fix: the drain loop exited (the OS unassigned this swap-chain — typically
|
||
// because a SIBLING display churned the desktop topology — or it errored), but the host-owned
|
||
// ring the publisher holds is still live. Hand it to the monitor so the NEXT worker assigned to
|
||
// this monitor resumes publishing into the same ring instead of freezing (the host re-delivers
|
||
// the channel only on a ring recreate). If the monitor is GONE (a genuine teardown, not a flap),
|
||
// `preserve_publisher` hands the publisher back inside the `Err` and dropping the returned
|
||
// `Result` closes the ring handles here — no leak, no stale ring left behind.
|
||
if let Some(p) = publisher.take() {
|
||
let _ = crate::monitor::preserve_publisher(target_id, p);
|
||
}
|
||
// Preserve the first-frame stash alongside (dropped inside if empty or the monitor is gone
|
||
// — it owns no handles, just a driver-private texture). The next worker on this adapter
|
||
// re-adopts it, so the retained frame survives the flap too.
|
||
crate::monitor::preserve_stash(target_id, render_luid_low, render_luid_high, stash);
|
||
}
|
||
}
|
||
|
||
impl Drop for SwapChainProcessor {
|
||
fn drop(&mut self) {
|
||
if let Some(handle) = self.thread.take() {
|
||
// signal the worker to end
|
||
self.terminate.store(true, Ordering::Relaxed);
|
||
// wait until the worker is finished (it deletes the swap-chain object before returning)
|
||
let _ = handle.join();
|
||
}
|
||
}
|
||
}
|