Compare commits

..
Author SHA1 Message Date
enricobuehler 712ee935d6 fix(win-display): a failed devnode re-enable must keep its crash-journal entry
ci / rust-arm64 (pull_request) Successful in 1m36s
ci / docs-site (pull_request) Successful in 1m22s
apple / swift (pull_request) Successful in 1m43s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 3m34s
ci / web (pull_request) Successful in 4m2s
android / android (pull_request) Successful in 4m13s
ci / rust (pull_request) Successful in 5m56s
enable_instances cleared every requested id from the pnp-disabled-monitors
journal unconditionally — a mid-life re-enable failure erased its own
crash-recovery entry, so neither the running host nor the next start would
ever retry, leaving the operator's monitor disabled (invisible to Windows and
every display listing) until a manual Device Manager fix. Field 2026-08-12
(Android-client host running the pnp_disable_monitors axis): displays gone
from the client after 2-3 sessions, only a full host restart bringing them
back — the restart path only works while the journal still has the entry.

Only successfully re-enabled ids clear now; failures stay journaled (and are
logged with the consequence) so startup_recover retries them.
2026-08-12 13:30:04 +02:00
enricobuehler a2aa0a5f97 fix(apple): never sit black AND silent while the video format is unseeded
Both pumps (Stage2Pipeline + the stage-1 StreamPump) dropped every AU in a
silent guard while 'format' was nil — the opening IDR's parameter sets never
arrived or never parsed, and under the host's infinite GOP nothing re-delivers
them unless the client asks. Field 2026-08-12 (iOS, H265): sessions decoding
nothing, requesting nothing, host streaming perfectly — a black stream with
zero recovery requests, randomly self-healing only when some later event
produced a fresh IDR.

While format is nil after the first received AU, the pumps now set awaitingIDR
(routing through the existing 100 ms-throttled recovery.request()) and log the
state once, so a lost/unparsed opening IDR converts to a normal recovery
round-trip instead of a permanent silent black.
2026-08-12 13:16:58 +02:00
enricobuehler d6b9862f1e fix(win-display): prune unplugged targets from the CCD restore snapshot
A monitor unplugged mid-session left the teardown replaying a snapshot whose
paths reference an absent target — SetDisplayConfig rejects the WHOLE array
with 0x57 ERROR_INVALID_PARAMETER, nothing restores, the desk stays dark, and
the next session snapshots the wreckage (the poisoned-snapshot chain; field
2026-08-12: rc=0x57 across a mid-session unplug, then sessions flipping
between black and working at random, incl. one that genuinely encoded black).

The restore now prunes the snapshot against a live QDC_ALL_PATHS availability
sweep before replaying: stale paths drop, the mode table is rebuilt with only
the entries the survivors reference (an orphaned mode entry is itself an
0x57), and an empty survivor set skips the apply so the dark-desk backstop
decides. Verbatim behavior is unchanged when everything is still attached or
when the availability query itself fails.
2026-08-12 13:15:02 +02:00
7 changed files with 388 additions and 185 deletions
@@ -57,6 +57,8 @@ let presentDebug = ProcessInfo.processInfo.environment["PUNKTFUNK_PRESENT_DEBUG"
/// to Console.app wirelessly with no env var / Xcode attach. Always on for deadline pacing (the
/// stats are a few arrays + one log line per second); other pacings keep the env-gated print.
private let presentLog = Logger(subsystem: "io.unom.punktfunk", category: "present")
/// Pump-side events (loss recovery, format seeding) the stage-2 sibling of StreamPump's log.
private let pumpLog = Logger(subsystem: "io.unom.punktfunk", category: "pump")
/// Decoded-frame hand-off between the decode half and the render thread. The POLICY is the
/// user's presentation intent (design/apple-presentation-rebuild.md the 2026-07 rebuild that
@@ -932,6 +934,21 @@ public final class Stage2Pipeline {
}
awaitingIDR = false // a fresh IDR re-anchored decode recovery complete
}
if format == nil {
// No decodable format yet: the opening IDR's parameter sets never
// arrived (or never parsed), and under the host's infinite GOP nothing
// re-delivers them unless we ASK. Without this the guard below drops
// every AU silently, forever the field "black stream, zero recovery
// requests" state (2026-08-12): the host streams perfectly, the client
// shows nothing and says nothing. awaitingIDR routes through the same
// 100 ms-throttled recovery.request() at the top of the loop.
if !awaitingIDR {
pumpLog.warning(
"video: received AUs but no decodable format (missing/unparsed parameter sets) — requesting an IDR until one seeds it"
)
}
awaitingIDR = true
}
guard let f = format, !token.isStopped else { return true }
if decoder.decode(au: au, format: f) {
decodeFailRun = 0
@@ -116,6 +116,21 @@ final class StreamPump {
}
awaitingIDR = false // a fresh IDR re-anchored decode recovery complete
}
if format == nil {
// No decodable format yet: the opening IDR's parameter sets never
// arrived (or never parsed), and under the host's infinite GOP nothing
// re-delivers them unless we ASK. Without this the format guard below
// drops every AU silently, forever the field "black stream, zero
// recovery requests" state (2026-08-12). awaitingIDR routes through the
// same 100 ms-throttled recovery.request() at the top of the loop.
if !awaitingIDR {
awaitingSince = Date()
pumpLog.warning(
"video: received AUs but no decodable format (missing/unparsed parameter sets) — requesting an IDR until one seeds it"
)
}
awaitingIDR = true
}
let failed = layer.status == .failed
if failed {
// Decode wedged hard (the cold-first-connect case a lost/corrupt opening
@@ -533,47 +533,14 @@ 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. 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 \
the disturbance is BELOW Windows: 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 \
+24 -42
View File
@@ -155,26 +155,18 @@ enum PrioMode {
Off,
/// A fixed class the operator pinned (`normal`=2 / `high`=4 / `realtime`=5).
Static(i32),
/// Opt-in (`auto`): HIGH immediately, then upgrade to REALTIME when it is safe — HAGS off, or
/// The default: 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 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.
/// 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.
Auto,
}
/// Resolve `PUNKTFUNK_GPU_PRIORITY_CLASS` (`off|normal|high|realtime|auto`, default **high**).
/// Resolve `PUNKTFUNK_GPU_PRIORITY_CLASS` (`off|normal|high|realtime|auto`, default **auto**).
/// 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);
/// `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.
/// `high` restores the pre-T2.3 static default.
fn configured_gpu_priority_mode() -> PrioMode {
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
.ok()
@@ -182,10 +174,9 @@ 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),
Some("auto") => PrioMode::Auto,
// `high`, unset, and anything unrecognized all land on the HIGH default.
_ => PrioMode::Static(4),
_ => PrioMode::Auto,
}
}
@@ -284,17 +275,14 @@ 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 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).
/// 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).
fn elevate_process_gpu_priority() {
use std::sync::Once;
static ONCE: Once = Once::new();
@@ -328,23 +316,17 @@ fn elevate_process_gpu_priority() {
});
}
// --- REALTIME auto-gate (gpu-contention §5.C / latency plan T2.3) — OPT-IN since 2026-08-12 ------
// --- REALTIME auto-gate (gpu-contention §5.C / latency plan T2.3) --------------------------------
//
// 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. 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
// 150800 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.
// 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).
/// Downgrade REALTIME→HIGH when local VRAM usage exceeds this share of the OS budget.
const VRAM_DOWNGRADE_PCT: u64 = 92;
+16 -2
View File
@@ -217,18 +217,32 @@ fn journal_and_disable(targets: Vec<(String, String)>) -> Vec<String> {
disabled
}
/// Re-enable `ids` (teardown / recovery) and clear them from the journal.
/// Re-enable `ids` (teardown / recovery) and clear the ones that actually re-enabled from the
/// journal. A FAILED re-enable must keep its journal entry: it is the only record that the
/// devnode is still disabled, and the next host start's [`startup_recover`] is the only thing
/// left that will retry it. (The old behavior cleared every requested id unconditionally — a
/// mid-life re-enable failure erased its own crash-recovery entry, leaving the operator's
/// monitor invisible to Windows AND to every display listing until they re-enabled it by hand
/// in Device Manager: the "my displays are gone until I restart everything" field class.)
pub fn enable_instances(ids: &[String]) -> u32 {
let mut ok = 0u32;
let mut reenabled: Vec<&String> = Vec::with_capacity(ids.len());
for id in ids {
if set_devnode(id, false) {
tracing::info!(id, "PnP-disable: monitor devnode re-enabled");
reenabled.push(id);
ok += 1;
} else {
tracing::warn!(
id,
"PnP-disable: monitor devnode re-enable FAILED — keeping its crash-journal \
entry so the next host start retries (until then this monitor stays disabled)"
);
}
}
let journal: Vec<String> = read_journal()
.into_iter()
.filter(|j| !ids.contains(j))
.filter(|j| !reenabled.contains(&j))
.collect();
write_journal(&journal);
ok
+284 -19
View File
@@ -1962,29 +1962,173 @@ pub fn restore_displays_ccd(saved: &SavedConfig) {
isolate_journal::clear();
}
/// Every display target that still EXISTS right now — `(adapter LUID low, high, target id)` keys
/// from a full `QDC_ALL_PATHS` sweep, counting a target present when the OS says a monitor is
/// attached (`targetAvailable`) OR an active path drives it (the flag reads FALSE transiently
/// right after a removal — same rule as [`target_inventory`]). `None` when the CCD query itself
/// fails, so the caller can fall back to trusting its snapshot verbatim.
fn available_target_keys() -> Option<Vec<(u32, i32, u32)>> {
let mut np = 0u32;
let mut nm = 0u32;
// SAFETY: the CCD contract at the top of this file — `&mut np`/`&mut nm` are live locals the
// OS fills with the counts it wants for these flags.
if unsafe { GetDisplayConfigBufferSizes(QDC_ALL_PATHS, &mut np, &mut nm) }.is_err() {
return None;
}
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
// SAFETY: the CCD contract — `paths`/`modes` were just allocated with exactly `np`/`nm`
// elements from the sizing call above, and are handed over with those same counts.
if unsafe {
QueryDisplayConfig(
QDC_ALL_PATHS,
&mut np,
paths.as_mut_ptr(),
&mut nm,
modes.as_mut_ptr(),
None,
)
}
.is_err()
{
return None;
}
paths.truncate(np as usize);
let mut keys: Vec<(u32, i32, u32)> = Vec::new();
for p in &paths {
let t = &p.targetInfo;
let key = (t.adapterId.LowPart, t.adapterId.HighPart, t.id);
let present = t.targetAvailable.as_bool() || p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0;
if present && !keys.contains(&key) {
keys.push(key);
}
}
Some(keys)
}
/// Drop every snapshot path whose TARGET no longer exists (`avail` — the live
/// [`available_target_keys`] sweep) and rebuild the mode table with only the entries the
/// survivors reference, remapping their `modeInfoIdx` slots. Both halves matter:
/// `SetDisplayConfig(SDC_USE_SUPPLIED_DISPLAY_CONFIG)` validates the WHOLE submission, so one
/// stale path — or one orphaned mode entry left behind by a dropped path — fails the entire
/// restore with 0x57 ERROR_INVALID_PARAMETER. Returns `(paths, modes, dropped_path_count)`;
/// pure over its inputs so the remap arithmetic is unit-testable without a live CCD.
fn prune_saved_config_for_targets(
paths: &[DISPLAYCONFIG_PATH_INFO],
modes: &[DISPLAYCONFIG_MODE_INFO],
avail: &[(u32, i32, u32)],
) -> (
Vec<DISPLAYCONFIG_PATH_INFO>,
Vec<DISPLAYCONFIG_MODE_INFO>,
usize,
) {
let mut kept: Vec<DISPLAYCONFIG_PATH_INFO> = Vec::with_capacity(paths.len());
let mut new_modes: Vec<DISPLAYCONFIG_MODE_INFO> = Vec::with_capacity(modes.len());
// old mode index → new mode index, memoized: clone configs legitimately share a source mode
// entry between paths, and it must land in the rebuilt table exactly once.
let mut remap: Vec<Option<u32>> = vec![None; modes.len()];
let take =
|idx: u32, new_modes: &mut Vec<DISPLAYCONFIG_MODE_INFO>, remap: &mut Vec<Option<u32>>| {
if idx == DISPLAYCONFIG_PATH_MODE_IDX_INVALID {
return DISPLAYCONFIG_PATH_MODE_IDX_INVALID;
}
match modes.get(idx as usize) {
// An out-of-range index could never have applied — un-pin the mode rather than
// shipping a table the whole submission fails on.
None => DISPLAYCONFIG_PATH_MODE_IDX_INVALID,
Some(m) => match remap[idx as usize] {
Some(n) => n,
None => {
let n = new_modes.len() as u32;
new_modes.push(*m);
remap[idx as usize] = Some(n);
n
}
},
}
};
let mut dropped = 0usize;
for p in paths {
let t = &p.targetInfo;
if !avail.contains(&(t.adapterId.LowPart, t.adapterId.HighPart, t.id)) {
dropped += 1;
continue;
}
let mut p = *p;
// SAFETY: POD union reads (CCD header contract) — `modeInfoIdx` overlays a same-sized
// bitfield struct, both valid for every bit pattern; used only as bounds-checked indices.
let (src_idx, tgt_idx) = unsafe {
(
p.sourceInfo.Anonymous.modeInfoIdx,
p.targetInfo.Anonymous.modeInfoIdx,
)
};
p.sourceInfo.Anonymous.modeInfoIdx = take(src_idx, &mut new_modes, &mut remap);
p.targetInfo.Anonymous.modeInfoIdx = take(tgt_idx, &mut new_modes, &mut remap);
kept.push(p);
}
(kept, new_modes, dropped)
}
fn restore_displays_ccd_inner(saved: &SavedConfig) {
let (paths, modes) = saved;
if paths.is_empty() {
let (saved_paths, saved_modes) = saved;
if saved_paths.is_empty() {
return;
}
// SAFETY: the CCD contract at the top of this file — the path/mode arrays go over as
// slices, so pointer and length cannot disagree, and both outlive this synchronous
// call. `retry_set_display_config` binds it to the input desktop, which is the one
// precondition a caller of this global-state write could otherwise get wrong.
let rc = crate::input_desktop::retry_set_display_config(|| unsafe {
SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
)
});
if rc == 0 {
tracing::info!("display isolate (CCD): restored original topology");
} else {
// Prune the snapshot against what is STILL ATTACHED before replaying it. A monitor unplugged
// mid-session leaves the snapshot referencing an absent target, and SetDisplayConfig rejects
// the WHOLE array with 0x57 ERROR_INVALID_PARAMETER — nothing restores, the desk stays dark,
// and the next session snapshots that wreckage (the poisoned-snapshot chain's first link;
// field 2026-08-12: rc=0x57 across a mid-session unplug, then sessions flipping between
// black/working at random). Dropping the stale paths lets the surviving displays restore
// normally; when NOTHING survives there is nothing to replay and the dark-desk backstop
// below is the whole answer.
let (kept, pruned_modes, dropped);
let (paths, modes): (&Vec<_>, &Vec<_>) = match available_target_keys() {
Some(avail) => {
(kept, pruned_modes, dropped) =
prune_saved_config_for_targets(saved_paths, saved_modes, &avail);
if dropped > 0 {
tracing::warn!(
dropped,
kept = kept.len(),
"display isolate (CCD): snapshot references target(s) that are no longer \
attached (unplugged mid-session?) pruned them so the survivors can restore \
(a verbatim replay fails whole with rc=0x57)"
);
}
(&kept, &pruned_modes)
}
// The availability query itself failed — replay verbatim, exactly the old behavior.
None => (saved_paths, saved_modes),
};
let mut apply_rc = 0i32; // 0 also when the replay was skipped (nothing left to apply)
if paths.is_empty() {
tracing::warn!(
"display isolate (CCD): topology restore failed rc={rc:#x}{} — physical displays may be left deactivated",
sdc_access_denied_hint(rc)
"display isolate (CCD): nothing from the topology snapshot is still attached — \
skipping the replay (the dark-desk backstop decides what lights up)"
);
} else {
// SAFETY: the CCD contract at the top of this file — the path/mode arrays go over as
// slices, so pointer and length cannot disagree, and both outlive this synchronous
// call. `retry_set_display_config` binds it to the input desktop, which is the one
// precondition a caller of this global-state write could otherwise get wrong.
let rc = crate::input_desktop::retry_set_display_config(|| unsafe {
SetDisplayConfig(
Some(paths.as_slice()),
Some(modes.as_slice()),
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
)
});
apply_rc = rc;
if rc == 0 {
tracing::info!("display isolate (CCD): restored original topology");
} else {
tracing::warn!(
"display isolate (CCD): topology restore failed rc={rc:#x}{} — physical displays may be left deactivated",
sdc_access_denied_hint(rc)
);
}
}
// GUARANTEE the desk is never left all-dark. The saved config can be unappliable (field
// rc=0x64a ERROR_BAD_CONFIGURATION: it pinned a virtual target incarnation that was since
@@ -2020,7 +2164,7 @@ fn restore_displays_ccd_inner(saved: &SavedConfig) {
return;
}
tracing::warn!(
"display isolate (CCD): no external physical display active after the restore (rc={rc:#x}, connected={connected}) — forcing the EXTEND preset so the desk is not left dark"
"display isolate (CCD): no external physical display active after the restore (rc={apply_rc:#x}, connected={connected}) — forcing the EXTEND preset so the desk is not left dark"
);
force_extend_topology();
// Measure what the force achieved: a sink still dark AFTER the EXTEND preset can never
@@ -2128,3 +2272,124 @@ mod live_tests {
tracing::info!("live CCD query: {n} active display path(s)");
}
}
#[cfg(test)]
mod prune_saved_config_tests {
//! The snapshot-prune remap arithmetic (`prune_saved_config_for_targets`) — pure over its
//! inputs, so the 0x57-poisoned-restore fix is testable without a live CCD: a stale target's
//! path must vanish, its modes must not orphan (an orphaned entry fails the whole
//! SetDisplayConfig exactly like the stale path did), and clone-shared modes must land once.
use super::*;
fn path(
luid_low: u32,
target_id: u32,
src_mode: u32,
tgt_mode: u32,
) -> DISPLAYCONFIG_PATH_INFO {
let mut p = DISPLAYCONFIG_PATH_INFO::default();
p.targetInfo.adapterId.LowPart = luid_low;
p.targetInfo.id = target_id;
p.sourceInfo.adapterId.LowPart = luid_low;
p.sourceInfo.Anonymous.modeInfoIdx = src_mode;
p.targetInfo.Anonymous.modeInfoIdx = tgt_mode;
p
}
fn mode(marker: u32) -> DISPLAYCONFIG_MODE_INFO {
DISPLAYCONFIG_MODE_INFO {
id: marker,
..Default::default()
}
}
fn indices(p: &DISPLAYCONFIG_PATH_INFO) -> (u32, u32) {
// SAFETY: POD union reads — `modeInfoIdx` overlays a same-sized bitfield struct, both
// valid for every bit pattern (the same contract the production reads rely on).
unsafe {
(
p.sourceInfo.Anonymous.modeInfoIdx,
p.targetInfo.Anonymous.modeInfoIdx,
)
}
}
#[test]
fn everything_attached_survives_with_dense_indices() {
let paths = vec![path(1, 100, 0, 1), path(1, 200, 2, 3)];
let modes = vec![mode(10), mode(11), mode(12), mode(13)];
let avail = vec![(1, 0, 100), (1, 0, 200)];
let (kept, new_modes, dropped) = prune_saved_config_for_targets(&paths, &modes, &avail);
assert_eq!(dropped, 0);
assert_eq!(kept.len(), 2);
assert_eq!(new_modes.len(), 4);
assert_eq!(indices(&kept[0]), (0, 1));
assert_eq!(indices(&kept[1]), (2, 3));
assert_eq!(new_modes[3].id, 13, "mode entries follow their paths");
}
#[test]
fn a_gone_target_drops_its_path_and_modes() {
// Target 200 was unplugged mid-session (the field rc=0x57 case): its path AND its two
// mode entries must vanish, and the survivor's indices must be remapped dense.
let paths = vec![path(1, 100, 0, 1), path(1, 200, 2, 3)];
let modes = vec![mode(10), mode(11), mode(12), mode(13)];
let avail = vec![(1, 0, 100)];
let (kept, new_modes, dropped) = prune_saved_config_for_targets(&paths, &modes, &avail);
assert_eq!(dropped, 1);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].targetInfo.id, 100);
assert_eq!(
new_modes.len(),
2,
"the dropped path's modes must not orphan"
);
assert_eq!((new_modes[0].id, new_modes[1].id), (10, 11));
assert_eq!(indices(&kept[0]), (0, 1));
}
#[test]
fn a_clone_shared_source_mode_lands_exactly_once() {
// Clone configs share one source mode entry between paths — the rebuilt table must
// contain it once, referenced by both survivors.
let paths = vec![path(1, 100, 0, 1), path(1, 200, 0, 2)];
let modes = vec![mode(10), mode(11), mode(12)];
let avail = vec![(1, 0, 100), (1, 0, 200)];
let (kept, new_modes, dropped) = prune_saved_config_for_targets(&paths, &modes, &avail);
assert_eq!(dropped, 0);
assert_eq!(new_modes.len(), 3);
let (a_src, _) = indices(&kept[0]);
let (b_src, _) = indices(&kept[1]);
assert_eq!(a_src, b_src, "shared source mode keeps one table entry");
}
#[test]
fn unpinned_and_corrupt_indices_stay_unpinned() {
// The INVALID sentinel must pass through, and an out-of-range index (a corrupt snapshot)
// must degrade to unpinned rather than shipping a table the whole apply fails on.
let paths = vec![path(1, 100, DISPLAYCONFIG_PATH_MODE_IDX_INVALID, 99)];
let modes = vec![mode(10)];
let avail = vec![(1, 0, 100)];
let (kept, new_modes, dropped) = prune_saved_config_for_targets(&paths, &modes, &avail);
assert_eq!(dropped, 0);
assert!(new_modes.is_empty());
assert_eq!(
indices(&kept[0]),
(
DISPLAYCONFIG_PATH_MODE_IDX_INVALID,
DISPLAYCONFIG_PATH_MODE_IDX_INVALID
)
);
}
#[test]
fn different_adapters_do_not_alias_the_same_target_id() {
// Target ids are only unique per adapter LUID — a survivor on adapter 2 must not keep a
// stale path alive on adapter 1 just because the ids match.
let paths = vec![path(1, 100, 0, 1)];
let modes = vec![mode(10), mode(11)];
let avail = vec![(2, 0, 100)];
let (kept, _, dropped) = prune_saved_config_for_targets(&paths, &modes, &avail);
assert_eq!((kept.len(), dropped), (0, 1));
}
}
@@ -69,55 +69,13 @@ 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 {
/// 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 {
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,
}
})
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var_os("PFVD_NO_RT_GPU").is_none())
}
/// A minimal newtype to move a raw pointer / handle across the thread boundary. The wrapped value is a
@@ -294,47 +252,32 @@ impl SwapChainProcessor {
}
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.
// 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.
//
// 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})"
);
}
}
// 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})"
);
}
}
// Release our borrowed device reference — IddCx holds its own now, or we gave up. (Explicit drop