Files
punktfunk/packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs
T
enricobuehler 6a506a8fa9
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
fix(vdisplay/driver,pf-frame): no punktfunk process holds REALTIME GPU priority by default
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.
2026-08-12 13:57:20 +02:00

622 lines
35 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.
//! 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 150800 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();
}
}
}