Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
712ee935d6 | ||
|
|
a2aa0a5f97 | ||
|
|
d6b9862f1e |
@@ -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
|
/// 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.
|
/// 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")
|
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
|
/// 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
|
/// 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
|
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 }
|
guard let f = format, !token.isStopped else { return true }
|
||||||
if decoder.decode(au: au, format: f) {
|
if decoder.decode(au: au, format: f) {
|
||||||
decodeFailRun = 0
|
decodeFailRun = 0
|
||||||
|
|||||||
@@ -116,6 +116,21 @@ final class StreamPump {
|
|||||||
}
|
}
|
||||||
awaitingIDR = false // a fresh IDR re-anchored decode — recovery complete
|
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
|
let failed = layer.status == .failed
|
||||||
if failed {
|
if failed {
|
||||||
// Decode wedged hard (the cold-first-connect case — a lost/corrupt opening
|
// Decode wedged hard (the cold-first-connect case — a lost/corrupt opening
|
||||||
|
|||||||
@@ -533,47 +533,14 @@ impl StallWatch {
|
|||||||
suspects)"
|
suspects)"
|
||||||
);
|
);
|
||||||
} else {
|
} 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!(
|
tracing::warn!(
|
||||||
period_s = format!("{:.2}", period.as_secs_f64()),
|
period_s = format!("{:.2}", period.as_secs_f64()),
|
||||||
os_correlated = correlated,
|
os_correlated = correlated,
|
||||||
connected_inactive = %suspects,
|
connected_inactive = %suspects,
|
||||||
rt_gpu_driver,
|
|
||||||
rt_gpu_host,
|
|
||||||
verdicts = %verdict_tally,
|
verdicts = %verdict_tally,
|
||||||
classes = %class_tally,
|
classes = %class_tally,
|
||||||
"capture stalls are METRONOMIC with NO coinciding OS display event — \
|
"capture stalls are METRONOMIC with NO coinciding OS display event — \
|
||||||
the disturbance is BELOW Windows. FIRST: if rt_gpu_driver or \
|
the disturbance is BELOW Windows: the GPU driver servicing a \
|
||||||
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), \
|
connected-but-asleep sink (standby HPD/DDC/link probing), \
|
||||||
display-poller software (the SteelSeries-GG/SignalRGB class — \
|
display-poller software (the SteelSeries-GG/SignalRGB class — \
|
||||||
correlate 'slow display-descriptor poll' lines), or the DWM present \
|
correlate 'slow display-descriptor poll' lines), or the DWM present \
|
||||||
|
|||||||
+24
-42
@@ -155,26 +155,18 @@ enum PrioMode {
|
|||||||
Off,
|
Off,
|
||||||
/// A fixed class the operator pinned (`normal`=2 / `high`=4 / `realtime`=5).
|
/// A fixed class the operator pinned (`normal`=2 / `high`=4 / `realtime`=5).
|
||||||
Static(i32),
|
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
|
/// 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
|
/// tightens). REALTIME is the proven ceiling-raiser (it is how our brief encode preempts a
|
||||||
/// pixel granularity), but it carries TWO field-proven hazards: REALTIME + NVIDIA + HAGS +
|
/// saturating game), but REALTIME + NVIDIA + HAGS + near-full VRAM is a documented NVENC
|
||||||
/// near-full VRAM is a documented NVENC hang (the VRAM gate covers that one), and on AMD the
|
/// hang — the gate takes the win everywhere it cannot hit the hazard.
|
||||||
/// 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,
|
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,
|
/// 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);
|
/// 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
|
/// `high` restores the pre-T2.3 static default.
|
||||||
/// 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 {
|
fn configured_gpu_priority_mode() -> PrioMode {
|
||||||
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
|
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -182,10 +174,9 @@ fn configured_gpu_priority_mode() -> PrioMode {
|
|||||||
{
|
{
|
||||||
Some("off") => PrioMode::Off,
|
Some("off") => PrioMode::Off,
|
||||||
Some("normal") => PrioMode::Static(2),
|
Some("normal") => PrioMode::Static(2),
|
||||||
|
Some("high") => PrioMode::Static(4),
|
||||||
Some("realtime") => PrioMode::Static(5),
|
Some("realtime") => PrioMode::Static(5),
|
||||||
Some("auto") => PrioMode::Auto,
|
_ => PrioMode::Auto,
|
||||||
// `high`, unset, and anything unrecognized all land on the HIGH default.
|
|
||||||
_ => PrioMode::Static(4),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
/// 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
|
/// `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`
|
/// 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
|
/// alone, which we measured as no help) lets our brief encode preempt the game. Default is the
|
||||||
/// static HIGH — the class that delivered that win. The T2.3 `auto` mode (HIGH here, then
|
/// T2.3 `auto` mode: HIGH immediately here, then [`auto_priority_gate`] upgrades to REALTIME
|
||||||
/// [`auto_priority_gate`] upgrades to REALTIME behind the NVENC-hang VRAM gate) is opt-in since
|
/// where the NVIDIA+HAGS+full-VRAM NVENC-hang hazard cannot bite (and a monitor downgrades when
|
||||||
/// the 2026-08-12 field A/B: on AMD the REALTIME upgrade generated its own metronomic
|
/// it could). Runs once per process; best-effort.
|
||||||
/// content-starving stall class (~3.6 s period) that the VRAM gate cannot see, and pinning HIGH
|
/// `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime|auto` (default auto; `high` = the
|
||||||
/// removed it. Runs once per process; best-effort.
|
/// pre-gate static behavior; `realtime` = pinned, operator owns the hazard). Best-effort:
|
||||||
/// `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime|auto` (default high; `auto` = the
|
/// silently no-ops under a UAC-filtered token (the process will not hold SE_INC_BASE_PRIORITY,
|
||||||
/// gated-REALTIME upgrade, operator opts into the AMD stall hazard for the extra ceiling;
|
/// so the D3DKMT call is a no-op).
|
||||||
/// `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() {
|
fn elevate_process_gpu_priority() {
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
static ONCE: Once = Once::new();
|
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
|
// 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),
|
// 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
|
// and our SYSTEM service uniquely holds the SE_INC_BASE_PRIORITY it needs. The one documented
|
||||||
// hazards bound it. (1) REALTIME + NVIDIA + HAGS-on + near-full VRAM can hang NVENC — the VRAM
|
// hazard: REALTIME + NVIDIA + HAGS-on + near-full VRAM can hang NVENC. So: probe HAGS once via
|
||||||
// gate below exists for that one: probe HAGS once via D3DKMT; HAGS off ⇒ REALTIME
|
// D3DKMT; HAGS off ⇒ REALTIME unconditionally; HAGS on ⇒ REALTIME gated on LOCAL-segment VRAM
|
||||||
// unconditionally; HAGS on ⇒ REALTIME gated on LOCAL-segment VRAM headroom, with a monitor
|
// headroom, with a monitor thread that downgrades to HIGH the moment usage crosses
|
||||||
// thread that downgrades to HIGH the moment usage crosses [`VRAM_DOWNGRADE_PCT`] of the OS
|
// [`VRAM_DOWNGRADE_PCT`] of the OS budget and restores REALTIME after it has stayed under
|
||||||
// budget and restores REALTIME after it has stayed under [`VRAM_RESTORE_PCT`] for
|
// [`VRAM_RESTORE_PCT`] for [`VRAM_RESTORE_TICKS`] consecutive polls (hysteresis against flapping
|
||||||
// [`VRAM_RESTORE_TICKS`] consecutive polls (hysteresis against flapping on the boundary of the
|
// on the boundary of the hazard window).
|
||||||
// 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.
|
/// Downgrade REALTIME→HIGH when local VRAM usage exceeds this share of the OS budget.
|
||||||
const VRAM_DOWNGRADE_PCT: u64 = 92;
|
const VRAM_DOWNGRADE_PCT: u64 = 92;
|
||||||
|
|||||||
@@ -217,18 +217,32 @@ fn journal_and_disable(targets: Vec<(String, String)>) -> Vec<String> {
|
|||||||
disabled
|
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 {
|
pub fn enable_instances(ids: &[String]) -> u32 {
|
||||||
let mut ok = 0u32;
|
let mut ok = 0u32;
|
||||||
|
let mut reenabled: Vec<&String> = Vec::with_capacity(ids.len());
|
||||||
for id in ids {
|
for id in ids {
|
||||||
if set_devnode(id, false) {
|
if set_devnode(id, false) {
|
||||||
tracing::info!(id, "PnP-disable: monitor devnode re-enabled");
|
tracing::info!(id, "PnP-disable: monitor devnode re-enabled");
|
||||||
|
reenabled.push(id);
|
||||||
ok += 1;
|
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()
|
let journal: Vec<String> = read_journal()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|j| !ids.contains(j))
|
.filter(|j| !reenabled.contains(&j))
|
||||||
.collect();
|
.collect();
|
||||||
write_journal(&journal);
|
write_journal(&journal);
|
||||||
ok
|
ok
|
||||||
|
|||||||
@@ -1962,29 +1962,173 @@ pub fn restore_displays_ccd(saved: &SavedConfig) {
|
|||||||
isolate_journal::clear();
|
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) {
|
fn restore_displays_ccd_inner(saved: &SavedConfig) {
|
||||||
let (paths, modes) = saved;
|
let (saved_paths, saved_modes) = saved;
|
||||||
if paths.is_empty() {
|
if saved_paths.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// SAFETY: the CCD contract at the top of this file — the path/mode arrays go over as
|
// Prune the snapshot against what is STILL ATTACHED before replaying it. A monitor unplugged
|
||||||
// slices, so pointer and length cannot disagree, and both outlive this synchronous
|
// mid-session leaves the snapshot referencing an absent target, and SetDisplayConfig rejects
|
||||||
// call. `retry_set_display_config` binds it to the input desktop, which is the one
|
// the WHOLE array with 0x57 ERROR_INVALID_PARAMETER — nothing restores, the desk stays dark,
|
||||||
// precondition a caller of this global-state write could otherwise get wrong.
|
// and the next session snapshots that wreckage (the poisoned-snapshot chain's first link;
|
||||||
let rc = crate::input_desktop::retry_set_display_config(|| unsafe {
|
// field 2026-08-12: rc=0x57 across a mid-session unplug, then sessions flipping between
|
||||||
SetDisplayConfig(
|
// black/working at random). Dropping the stale paths lets the surviving displays restore
|
||||||
Some(paths.as_slice()),
|
// normally; when NOTHING survives there is nothing to replay and the dark-desk backstop
|
||||||
Some(modes.as_slice()),
|
// below is the whole answer.
|
||||||
SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES,
|
let (kept, pruned_modes, dropped);
|
||||||
)
|
let (paths, modes): (&Vec<_>, &Vec<_>) = match available_target_keys() {
|
||||||
});
|
Some(avail) => {
|
||||||
if rc == 0 {
|
(kept, pruned_modes, dropped) =
|
||||||
tracing::info!("display isolate (CCD): restored original topology");
|
prune_saved_config_for_targets(saved_paths, saved_modes, &avail);
|
||||||
} else {
|
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!(
|
tracing::warn!(
|
||||||
"display isolate (CCD): topology restore failed rc={rc:#x}{} — physical displays may be left deactivated",
|
"display isolate (CCD): nothing from the topology snapshot is still attached — \
|
||||||
sdc_access_denied_hint(rc)
|
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
|
// 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
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
tracing::warn!(
|
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();
|
force_extend_topology();
|
||||||
// Measure what the force achieved: a sink still dark AFTER the EXTEND preset can never
|
// 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)");
|
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
|
hr >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How (whether) the swap-chain processing device's GPU scheduling is raised — the
|
/// The `IddCxSetRealtimeGPUPriority` A/B knob: `PFVD_NO_RT_GPU` (any value, MACHINE env — the
|
||||||
/// interval-stutter program's A/B ladder, resolved once per WUDFHost process from the MACHINE
|
/// driver runs in WUDFHost as LocalService, so `setx /M PFVD_NO_RT_GPU 1` + a device restart)
|
||||||
/// environment (the driver runs as LocalService: `setx /M PFVD_RT_GPU 1` + a device restart
|
/// turns the priority raise OFF. Read once per process, the [`crate::log`] `OnceLock` pattern.
|
||||||
/// applies it; the [`crate::log`] `OnceLock` pattern).
|
fn realtime_gpu_priority_enabled() -> bool {
|
||||||
#[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;
|
use std::sync::OnceLock;
|
||||||
static MODE: OnceLock<RtGpuMode> = OnceLock::new();
|
static ON: OnceLock<bool> = OnceLock::new();
|
||||||
*MODE.get_or_init(|| {
|
*ON.get_or_init(|| std::env::var_os("PFVD_NO_RT_GPU").is_none())
|
||||||
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
|
/// 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));
|
thread::sleep(Duration::from_millis(50));
|
||||||
}
|
}
|
||||||
// GPU-scheduling raise for the swap-chain processing device — OPT-IN, default none (see
|
// IddCx 1.9 realtime GPU scheduling priority for the processing device (stall-immunity
|
||||||
// [`rt_gpu_mode`] for the field conviction that inverted the old default-ON). What used
|
// program, branch-2 hardening): swap-chain buffer processing outruns ordinary GPU
|
||||||
// to be sold as stall immunity ("swap-chain buffer processing outruns ordinary GPU
|
// contention — "higher priority than any regular application can set". The slot is
|
||||||
// contention") preempts the game's and DWM's own queues at a level apps can't reach, and
|
// guaranteed populated (`IddMinimumVersionRequired = 10`, lib.rs); the DDI itself may
|
||||||
// on an AMD field box that manifested as the metronomic content-starving stall class the
|
// still decline (e.g. E_NOTIMPL on pre-WDDM-3.0 hardware) — best-effort, never fatal.
|
||||||
// stall program spent weeks attributing. The CPU-side half of that hardening (MMCSS /
|
// Called while our borrowed device reference is still alive; IddCx uses it synchronously.
|
||||||
// TIME_CRITICAL, above) is untouched — it addressed the delivery holes actually observed.
|
|
||||||
//
|
//
|
||||||
// Both raises are best-effort, never fatal, and issued while our borrowed device
|
// Knobbed (PFVD_NO_RT_GPU, machine env, read in the WUDFHost process): no canonical IDD
|
||||||
// reference is still alive (IddCx uses it synchronously; the DXGI call is direct). The
|
// driver raises this priority, and it preempts the game's and DWM's own queues at a level
|
||||||
// REALTIME slot is guaranteed populated (`IddMinimumVersionRequired = 10`, lib.rs), but
|
// apps can't reach — a candidate aggravator in the interval-stutter program that must
|
||||||
// the DDI may still decline (e.g. E_NOTIMPL on pre-WDDM-3.0 hardware).
|
// stay A/B-able on a field box without a rebuild. Default ON (today's behavior).
|
||||||
if set_ok {
|
if set_ok && realtime_gpu_priority_enabled() {
|
||||||
match rt_gpu_mode() {
|
let mut rt = pod_init!(IDARG_IN_SETREALTIMEGPUPRIORITY);
|
||||||
RtGpuMode::Off => {}
|
rt.pDevice = dxgi_device.as_raw().cast();
|
||||||
RtGpuMode::GpuThread => {
|
// SAFETY: driver is loaded; `swap_chain` is the live assigned swap-chain whose device
|
||||||
// SAFETY: `dxgi_device` is the live device just bound to the swap-chain; the
|
// bind just succeeded; `rt.pDevice` is that same bound DXGI device, alive across the
|
||||||
// call takes a scalar in the documented −7..=7 band and retains nothing.
|
// synchronous call; `rt` points to valid local storage.
|
||||||
let res = unsafe { dxgi_device.SetGPUThreadPriority(7) };
|
let hr = unsafe { wdk_iddcx::IddCxSetRealtimeGPUPriority(swap_chain, &rt) };
|
||||||
dbglog!(
|
if hr_success(hr) {
|
||||||
"[pf-vd] swap-chain: GPU thread priority +7 (PFVD_RT_GPU=thread) — ok={} (target={target_id})",
|
dbglog!(
|
||||||
res.is_ok()
|
"[pf-vd] swap-chain: processing device raised to REALTIME GPU priority (target={target_id})"
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
RtGpuMode::Realtime => {
|
dbglog!(
|
||||||
let mut rt = pod_init!(IDARG_IN_SETREALTIMEGPUPRIORITY);
|
"[pf-vd] swap-chain: realtime GPU priority declined ({hr:#x}) — normal scheduling (target={target_id})"
|
||||||
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
|
// Release our borrowed device reference — IddCx holds its own now, or we gave up. (Explicit drop
|
||||||
|
|||||||
Reference in New Issue
Block a user