diff --git a/crates/pf-capture/src/windows/idd_push/stall.rs b/crates/pf-capture/src/windows/idd_push/stall.rs index 27d729fc..832c611a 100644 --- a/crates/pf-capture/src/windows/idd_push/stall.rs +++ b/crates/pf-capture/src/windows/idd_push/stall.rs @@ -533,14 +533,47 @@ impl StallWatch { suspects)" ); } else { + // The two REALTIME GPU-priority opt-ins, as configured in THIS process's + // environment (machine env; the WUDFHost driver process resolves the PFVD pair + // the same way, so this read mirrors what the driver decided — modulo a machine + // env edited after either process started, which a restart heals). The RX 9070 + // XT field A/B (2026-08-12) convicted EXACTLY this warning's signature twice + // over: the driver's swap-chain REALTIME raise beat at ~1.8 s, the host + // auto-gate's REALTIME upgrade at ~3.6 s — so a log carrying this warning must + // say whether either lever is engaged before anyone chases display hardware. + let rt_gpu_driver = if std::env::var_os("PFVD_NO_RT_GPU").is_some() { + "off (PFVD_NO_RT_GPU)" + } else { + match std::env::var_os("PFVD_RT_GPU") { + None => "off (default)", + Some(v) if v.eq_ignore_ascii_case("thread") => "gpu-thread (+7)", + Some(_) => "REALTIME (PFVD_RT_GPU)", + } + }; + let rt_gpu_host = match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS") + .ok() + .as_deref() + { + Some("off") => "off", + Some("normal") => "normal", + Some("realtime") => "REALTIME (pinned)", + Some("auto") => "auto (gated REALTIME upgrade)", + _ => "high (default)", + }; tracing::warn!( period_s = format!("{:.2}", period.as_secs_f64()), os_correlated = correlated, connected_inactive = %suspects, + rt_gpu_driver, + rt_gpu_host, verdicts = %verdict_tally, classes = %class_tally, "capture stalls are METRONOMIC with NO coinciding OS display event — \ - the disturbance is BELOW Windows: the GPU driver servicing a \ + the disturbance is BELOW Windows. FIRST: if rt_gpu_driver or \ + rt_gpu_host shows a REALTIME opt-in, clear it (unset PFVD_RT_GPU / \ + set PUNKTFUNK_GPU_PRIORITY_CLASS=high) — a punktfunk process holding \ + REALTIME GPU priority is the field-proven amplifier of exactly this \ + signature on AMD. Otherwise: the GPU driver servicing a \ connected-but-asleep sink (standby HPD/DDC/link probing), \ display-poller software (the SteelSeries-GG/SignalRGB class — \ correlate 'slow display-descriptor poll' lines), or the DWM present \ diff --git a/crates/pf-frame/src/dxgi.rs b/crates/pf-frame/src/dxgi.rs index e846e3c0..0189c1b4 100644 --- a/crates/pf-frame/src/dxgi.rs +++ b/crates/pf-frame/src/dxgi.rs @@ -155,18 +155,26 @@ enum PrioMode { Off, /// A fixed class the operator pinned (`normal`=2 / `high`=4 / `realtime`=5). Static(i32), - /// The default: HIGH immediately, then upgrade to REALTIME when it is safe — HAGS off, or + /// Opt-in (`auto`): HIGH immediately, then upgrade to REALTIME when it is safe — HAGS off, or /// HAGS on with comfortable VRAM headroom (with a monitor that downgrades the moment VRAM - /// tightens). REALTIME is the proven ceiling-raiser (it is how our brief encode preempts a - /// saturating game), but REALTIME + NVIDIA + HAGS + near-full VRAM is a documented NVENC - /// hang — the gate takes the win everywhere it cannot hit the hazard. + /// tightens). REALTIME is the T2.3 ceiling-raiser (a higher-priority context preempts at + /// pixel granularity), but it carries TWO field-proven hazards: REALTIME + NVIDIA + HAGS + + /// near-full VRAM is a documented NVENC hang (the VRAM gate covers that one), and on AMD the + /// upgrade itself produced a metronomic content-starving stall class (~3.6 s period, RX 9070 + /// XT, 2026-08-12 A/B: pinning `high` removed it) that no VRAM gate can see — which is why + /// `auto` is no longer the default. Auto, } -/// Resolve `PUNKTFUNK_GPU_PRIORITY_CLASS` (`off|normal|high|realtime|auto`, default **auto**). +/// Resolve `PUNKTFUNK_GPU_PRIORITY_CLASS` (`off|normal|high|realtime|auto`, default **high**). /// D3DKMT_SCHEDULINGPRIORITYCLASS: IDLE 0, BELOW_NORMAL 1, NORMAL 2, ABOVE_NORMAL 3, HIGH 4, /// REALTIME 5. `realtime` pins REALTIME statically (no gate — the operator owns the hazard); -/// `high` restores the pre-T2.3 static default. +/// `auto` is the T2.3 gated-REALTIME mode, opt-in since the 2026-08-12 field A/B convicted the +/// REALTIME upgrade of its own metronomic stall class on AMD (see [`PrioMode::Auto`]) — HIGH is +/// the Sunshine/Apollo-parity lever that delivered the original decisive win, and the default +/// must not hold REALTIME anywhere (the same inversion as the vdisplay driver's `PFVD_RT_GPU` +/// ladder, which fixed the faster ~1.8 s metronome the same day). Unrecognized values read as +/// the default, not as `auto` — a typo must not opt a box into the hazard. fn configured_gpu_priority_mode() -> PrioMode { match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS") .ok() @@ -174,9 +182,10 @@ fn configured_gpu_priority_mode() -> PrioMode { { Some("off") => PrioMode::Off, Some("normal") => PrioMode::Static(2), - Some("high") => PrioMode::Static(4), Some("realtime") => PrioMode::Static(5), - _ => PrioMode::Auto, + Some("auto") => PrioMode::Auto, + // `high`, unset, and anything unrecognized all land on the HIGH default. + _ => PrioMode::Static(4), } } @@ -275,14 +284,17 @@ unsafe fn d3dkmt_set_scheduling_priority_class( /// GPU-saturated game our capture+encode process is starved of GPU time slices — NVENC sits ~idle but /// `lock_bitstream` waits ~20 ms for our context to be scheduled. Elevating the PROCESS GPU scheduling /// priority class (the strong cross-process lever — far more effective than `SetGPUThreadPriority` -/// alone, which we measured as no help) lets our brief encode preempt the game. Default is the -/// T2.3 `auto` mode: HIGH immediately here, then [`auto_priority_gate`] upgrades to REALTIME -/// where the NVIDIA+HAGS+full-VRAM NVENC-hang hazard cannot bite (and a monitor downgrades when -/// it could). Runs once per process; best-effort. -/// `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime|auto` (default auto; `high` = the -/// pre-gate static behavior; `realtime` = pinned, operator owns the hazard). Best-effort: -/// silently no-ops under a UAC-filtered token (the process will not hold SE_INC_BASE_PRIORITY, -/// so the D3DKMT call is a no-op). +/// alone, which we measured as no help) lets our brief encode preempt the game. Default is a +/// static HIGH — the class that delivered that win. The T2.3 `auto` mode (HIGH here, then +/// [`auto_priority_gate`] upgrades to REALTIME behind the NVENC-hang VRAM gate) is opt-in since +/// the 2026-08-12 field A/B: on AMD the REALTIME upgrade generated its own metronomic +/// content-starving stall class (~3.6 s period) that the VRAM gate cannot see, and pinning HIGH +/// removed it. Runs once per process; best-effort. +/// `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime|auto` (default high; `auto` = the +/// gated-REALTIME upgrade, operator opts into the AMD stall hazard for the extra ceiling; +/// `realtime` = pinned, operator owns every hazard). Best-effort: silently no-ops under a +/// UAC-filtered token (the process will not hold SE_INC_BASE_PRIORITY, so the D3DKMT call is a +/// no-op). fn elevate_process_gpu_priority() { use std::sync::Once; static ONCE: Once = Once::new(); @@ -316,17 +328,23 @@ fn elevate_process_gpu_priority() { }); } -// --- REALTIME auto-gate (gpu-contention §5.C / latency plan T2.3) -------------------------------- +// --- REALTIME auto-gate (gpu-contention §5.C / latency plan T2.3) — OPT-IN since 2026-08-12 ------ // // REALTIME GPU scheduling priority is the genuine cross-process ceiling-raiser under a saturating // game (a higher-priority context preempts at pixel granularity — the Async-TimeWarp mechanism), -// and our SYSTEM service uniquely holds the SE_INC_BASE_PRIORITY it needs. The one documented -// hazard: REALTIME + NVIDIA + HAGS-on + near-full VRAM can hang NVENC. So: probe HAGS once via -// D3DKMT; HAGS off ⇒ REALTIME unconditionally; HAGS on ⇒ REALTIME gated on LOCAL-segment VRAM -// headroom, with a monitor thread that downgrades to HIGH the moment usage crosses -// [`VRAM_DOWNGRADE_PCT`] of the OS budget and restores REALTIME after it has stayed under -// [`VRAM_RESTORE_PCT`] for [`VRAM_RESTORE_TICKS`] consecutive polls (hysteresis against flapping -// on the boundary of the hazard window). +// and our SYSTEM service uniquely holds the SE_INC_BASE_PRIORITY it needs. Two field-proven +// hazards bound it. (1) REALTIME + NVIDIA + HAGS-on + near-full VRAM can hang NVENC — the VRAM +// gate below exists for that one: probe HAGS once via D3DKMT; HAGS off ⇒ REALTIME +// unconditionally; HAGS on ⇒ REALTIME gated on LOCAL-segment VRAM headroom, with a monitor +// thread that downgrades to HIGH the moment usage crosses [`VRAM_DOWNGRADE_PCT`] of the OS +// budget and restores REALTIME after it has stayed under [`VRAM_RESTORE_PCT`] for +// [`VRAM_RESTORE_TICKS`] consecutive polls (hysteresis against flapping on the boundary of the +// hazard window). (2) On AMD (RX 9070 XT A/B), a punktfunk process holding REALTIME generated a +// metronomic content-starving stall class — every ~3.6 s ALL processes' presents paused +// 150–800 ms with the GPU responsive — that no VRAM gate can see, and the vdisplay driver's +// REALTIME swap-chain raise produced the same pathology on its own ~1.8 s beat. That second +// hazard is why the whole gate now runs only under an explicit `auto`, and the default stays a +// static HIGH. /// Downgrade REALTIME→HIGH when local VRAM usage exceeds this share of the OS budget. const VRAM_DOWNGRADE_PCT: u64 = 92; diff --git a/packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs b/packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs index 09e2ee21..90ef4807 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs @@ -69,13 +69,55 @@ fn hr_success(hr: NTSTATUS) -> bool { hr >= 0 } -/// The `IddCxSetRealtimeGPUPriority` A/B knob: `PFVD_NO_RT_GPU` (any value, MACHINE env — the -/// driver runs in WUDFHost as LocalService, so `setx /M PFVD_NO_RT_GPU 1` + a device restart) -/// turns the priority raise OFF. Read once per process, the [`crate::log`] `OnceLock` pattern. -fn realtime_gpu_priority_enabled() -> bool { +/// 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=`: 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 ON: OnceLock = OnceLock::new(); - *ON.get_or_init(|| std::env::var_os("PFVD_NO_RT_GPU").is_none()) + static MODE: OnceLock = 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 @@ -252,32 +294,47 @@ impl SwapChainProcessor { } thread::sleep(Duration::from_millis(50)); } - // IddCx 1.9 realtime GPU scheduling priority for the processing device (stall-immunity - // program, branch-2 hardening): swap-chain buffer processing outruns ordinary GPU - // contention — "higher priority than any regular application can set". The slot is - // guaranteed populated (`IddMinimumVersionRequired = 10`, lib.rs); the DDI itself may - // still decline (e.g. E_NOTIMPL on pre-WDDM-3.0 hardware) — best-effort, never fatal. - // Called while our borrowed device reference is still alive; IddCx uses it synchronously. + // 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. // - // Knobbed (PFVD_NO_RT_GPU, machine env, read in the WUDFHost process): no canonical IDD - // driver raises this priority, and it preempts the game's and DWM's own queues at a level - // apps can't reach — a candidate aggravator in the interval-stutter program that must - // stay A/B-able on a field box without a rebuild. Default ON (today's behavior). - if set_ok && realtime_gpu_priority_enabled() { - 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 (target={target_id})" - ); - } else { - dbglog!( - "[pf-vd] swap-chain: realtime GPU priority declined ({hr:#x}) — normal scheduling (target={target_id})" - ); + // 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