From 6a506a8fa9e91534a12e071140a55a12ed30c4cd Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 12 Aug 2026 13:52:19 +0200 Subject: [PATCH] fix(vdisplay/driver,pf-frame): no punktfunk process holds REALTIME GPU priority by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../pf-capture/src/windows/idd_push/stall.rs | 35 +++++- crates/pf-frame/src/dxgi.rs | 66 ++++++---- .../pf-vdisplay/src/swap_chain_processor.rs | 119 +++++++++++++----- 3 files changed, 164 insertions(+), 56 deletions(-) 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