Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a506a8fa9 |
@@ -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 \
|
||||
|
||||
+42
-24
@@ -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;
|
||||
|
||||
@@ -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=<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 ON: OnceLock<bool> = OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var_os("PFVD_NO_RT_GPU").is_none())
|
||||
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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user