feat(host,web): experimental DDC/CI monitor power-off for Exclusive sessions
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 19s
apple / swift (push) Successful in 1m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
apple / screenshots (push) Successful in 5m28s
ci / bench (push) Successful in 6m40s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 8m45s
android / android (push) Successful in 12m43s
deb / build-publish (push) Successful in 13m1s
arch / build-publish (push) Successful in 14m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m20s
ci / rust (push) Successful in 22m12s

The sole-virtual-display stutter investigation's active experiment: when the
Exclusive isolate deactivates a physical monitor, the dark-but-connected head
keeps getting serviced (monitor standby auto-input-scan / DP link churn) at a
seconds-scale cadence — the leading suspect for the periodic double-jolt. A
panel commanded off over DDC/CI (the VESA monitor-control channel in the video
cable) believes it has an owner and, on cooperating firmware, stops probing.

- New `ddc_power_off` display-policy axis (default off): orthogonal to presets
  like game_session, stored in display-settings.json, surfaced in GET/PUT
  /display/settings + the enforced list, carried through the layout transform.
- windows/ddc.rs: VCP 0xD6 power-mode control via the dxva2 Physical Monitor
  API. Deliberately DPMS-off (0x04, DDC stays responsive, signal return wakes)
  and never power-button-off (0x05, bricks-until-button on many monitors).
  Probe-before-write; every failure is skip-and-log — monitors without DDC/CI,
  OSD-disabled, or behind docks/KVMs degrade to a logged no-op.
- Manager wiring: panels commanded off immediately BEFORE the Exclusive CCD
  isolate (an HMONITOR — and with it the DDC channel — only exists while the
  display is active); teardown wakes them right after the CCD restore, where
  returning signal alone already wakes most firmware.
- Web console: an Experimental-badged on/off control on the display card,
  applied immediately like the game-session axis and preserved across preset
  switches; EN/DE strings incl. the wake-failure escape hatch (press the
  monitor's power button once, turn the option off).

Diagnostic value on top of the fix: if this kills a reporter's stutter, the
churn is monitor-firmware-initiated; if only topology=primary/extend does, the
driver services dark heads regardless — the two remaining root-cause classes.

Verified: Linux 258 tests + clippy + fmt clean; Windows (RTX box) 220/220 +
clippy clean; web tsc + production build clean; openapi.json regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 12:32:36 +02:00
parent f68f6bc590
commit cca5008805
9 changed files with 322 additions and 8 deletions
+26 -3
View File
@@ -224,6 +224,16 @@ pub struct DisplayPolicy {
/// so existing `display-settings.json` files are untouched.
#[serde(default)]
pub game_session: GameSession,
/// EXPERIMENTAL (Windows): command physical monitors' panels off over DDC/CI (VCP 0xD6 →
/// DPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.
/// Targets the "connected-but-dark head" periodic-stutter class (monitor standby
/// auto-input-scan / DP link churn while the virtual display is the sole active display) at
/// the monitor-firmware level. Best-effort — monitors without DDC/CI (or with it disabled in
/// the OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across
/// preset changes; `#[serde(default)]` = off so existing `display-settings.json` files are
/// untouched.
#[serde(default)]
pub ddc_power_off: bool,
}
fn one() -> u32 {
@@ -247,6 +257,7 @@ impl Default for DisplayPolicy {
layout: Layout::default(),
max_displays: 4,
game_session: GameSession::default(),
ddc_power_off: false,
}
}
}
@@ -306,6 +317,7 @@ impl EffectivePolicy {
&self,
positions: BTreeMap<String, Position>,
game_session: GameSession,
ddc_power_off: bool,
) -> DisplayPolicy {
DisplayPolicy {
version: 1,
@@ -319,8 +331,9 @@ impl EffectivePolicy {
positions,
},
max_displays: self.max_displays,
// Preserve the orthogonal game-session axis (EffectivePolicy doesn't carry it).
// Preserve the orthogonal axes (EffectivePolicy doesn't carry them).
game_session,
ddc_power_off,
}
}
}
@@ -434,6 +447,13 @@ impl DisplayPolicyStore {
self.get().game_session
}
/// The experimental DDC/CI panel-off axis — orthogonal to the preset (like
/// [`Self::game_session`]), read directly off the stored policy (default off when
/// unconfigured).
pub fn ddc_power_off(&self) -> bool {
self.get().ddc_power_off
}
/// Persist + adopt a new policy (sanitized first). The in-memory value changes only if the disk
/// write succeeds, so a full disk can't leave memory and file disagreeing.
pub fn set(&self, policy: DisplayPolicy) -> Result<()> {
@@ -749,9 +769,10 @@ mod tests {
let mut positions = BTreeMap::new();
positions.insert("1".to_string(), Position { x: 0, y: 0 });
positions.insert("7".to_string(), Position { x: 2560, y: 0 });
let p = eff.with_manual_layout(positions, GameSession::Dedicated);
// The orthogonal game-session axis is preserved through the layout transform.
let p = eff.with_manual_layout(positions, GameSession::Dedicated, true);
// The orthogonal axes (game-session, DDC power-off) are preserved through the transform.
assert_eq!(p.game_session, GameSession::Dedicated);
assert!(p.ddc_power_off);
// Preset drops to Custom so the explicit fields (incl. the layout) rule…
assert_eq!(p.preset, Preset::Custom);
// …every other behavior axis is preserved verbatim…
@@ -776,6 +797,8 @@ mod tests {
assert_eq!(p.keep_alive, KeepAlive::default());
assert_eq!(p.topology, Topology::Auto);
assert_eq!(p.version, 1);
// A file written before the experimental DDC axis existed defaults it OFF.
assert!(!p.ddc_power_off);
}
#[test]
@@ -118,6 +118,9 @@ struct Monitor {
stop: Arc<AtomicBool>,
pinger: Option<JoinHandle<()>>,
ccd_saved: Option<SavedConfig>,
/// How many physical panels acknowledged the EXPERIMENTAL DDC/CI off command at this monitor's
/// isolate (`ddc_power_off` policy axis) — teardown wakes them after the CCD restore iff > 0.
ddc_panels_off: u32,
/// Generation stamp; a [`MonitorLease`] only releases if its gen still matches (stale-lease no-op).
gen: u64,
}
@@ -668,6 +671,7 @@ impl VirtualDisplayManager {
}
}
let mut ccd_saved: Option<SavedConfig> = None;
let mut ddc_panels_off = 0u32;
match &gdi_name {
Some(n) => {
tracing::info!(backend = self.driver.name(), "target {} -> {n}", added.target_id);
@@ -682,6 +686,15 @@ impl VirtualDisplayManager {
use crate::vdisplay::policy::Topology;
match topology_action() {
Topology::Exclusive => {
// EXPERIMENTAL `ddc_power_off` policy axis: command the physical panels
// dark over DDC/CI BEFORE the isolate — an HMONITOR (and with it the DDC
// channel) only exists while the display is still active. A panel that
// believes it has an owner skips its no-signal standby probing — the
// suspected source of the periodic sole-virtual-display stutter (the
// rationale + evidence live in `windows/ddc.rs`).
if crate::vdisplay::policy::prefs().ddc_power_off() {
ddc_panels_off = crate::ddc::panel_off_except(n);
}
// SAFETY: `isolate_displays_ccd` is `unsafe` for its CCD topology FFI; it takes the
// `Copy` target id by value and returns an owned `SavedConfig` (no borrowed memory
// crosses), under the `state` lock — the sole topology mutator.
@@ -743,6 +756,7 @@ impl VirtualDisplayManager {
stop,
pinger: Some(pinger),
ccd_saved,
ddc_panels_off,
gen: self.gen.fetch_add(1, Ordering::Relaxed),
})
}
@@ -805,6 +819,20 @@ impl VirtualDisplayManager {
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero displays.
if let Some(saved) = &mon.ccd_saved {
restore_displays_ccd(saved);
// EXPERIMENTAL `ddc_power_off` wake: the restore re-activated the physical paths, and
// returning signal alone wakes DPMS-off panels on most firmware — the explicit ON is
// belt-and-braces for the rest. The brief settle wait lets the re-activated paths show
// up in EnumDisplayMonitors (no HMONITOR, no DDC channel); teardown is already
// seconds-scale and watched by the 10 s wedge logger above.
if mon.ddc_panels_off > 0 {
thread::sleep(Duration::from_millis(300));
let woken = crate::ddc::panel_on_all();
tracing::info!(
commanded_off = mon.ddc_panels_off,
woken,
"DDC/CI: panel wake commands sent after topology restore"
);
}
}
// SAFETY: `teardown`'s own `# Safety` contract guarantees `dev` is the live control handle, and
// `remove_monitor` requires exactly that. `&mon.key` borrows the `MonitorKey` inside the