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
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:
@@ -28,6 +28,9 @@ mod wol;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/crash.rs"]
|
||||
mod crash;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/ddc.rs"]
|
||||
mod ddc;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "linux/dmabuf_fence.rs"]
|
||||
mod dmabuf_fence;
|
||||
|
||||
@@ -1051,6 +1051,9 @@ fn display_settings_state() -> DisplaySettingsState {
|
||||
"identity".into(),
|
||||
"layout".into(),
|
||||
"game_session".into(),
|
||||
// EXPERIMENTAL, Windows-only in effect: acted on at the `exclusive` isolate
|
||||
// (`vdisplay/windows/manager.rs`); stored-but-inert elsewhere.
|
||||
"ddc_power_off".into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1256,10 +1259,11 @@ async fn set_display_layout(ApiJson(req): ApiJson<DisplayLayoutRequest>) -> Resp
|
||||
// Lock the current effective behavior into explicit fields + set the manual arrangement (pure
|
||||
// transform, unit-tested in `policy.rs`) — so arranging displays is orthogonal to the other policy
|
||||
// axes. (`effective` keep_alive is never `Forever` via the API — the settings PUT rejects it.)
|
||||
let policy = store
|
||||
.get()
|
||||
.effective()
|
||||
.with_manual_layout(req.positions, store.game_session());
|
||||
let policy = store.get().effective().with_manual_layout(
|
||||
req.positions,
|
||||
store.game_session(),
|
||||
store.ddc_power_off(),
|
||||
);
|
||||
if let Err(e) = store.set(policy) {
|
||||
return api_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -2944,6 +2948,8 @@ mod tests {
|
||||
assert!(enforced.contains(&"mode_conflict"));
|
||||
assert!(enforced.contains(&"identity"));
|
||||
assert!(enforced.contains(&"layout"));
|
||||
// The experimental DDC/CI panel-off axis is acted on (Windows exclusive-isolate path).
|
||||
assert!(enforced.contains(&"ddc_power_off"));
|
||||
}
|
||||
|
||||
/// The display state/release endpoints are wired + auth-gated. On the test host no backend has
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//! DDC/CI monitor panel power control — the EXPERIMENTAL `ddc_power_off` display-policy axis.
|
||||
//!
|
||||
//! DDC/CI is the VESA command channel to the monitor itself: an I²C bus inside the video cable
|
||||
//! (dedicated pins on VGA/DVI/HDMI, tunneled over the AUX channel on DisplayPort) whose MCCS
|
||||
//! "VCP codes" expose the monitor's OSD knobs to software. VCP 0xD6 is the power mode; we command
|
||||
//! `0x04` (DPMS off — panel + backlight dark, firmware still listening) and never `0x05`
|
||||
//! (power-button off — many monitors kill their DDC controller in that state and need a physical
|
||||
//! button press to come back).
|
||||
//!
|
||||
//! Why: the "periodic double-jolt while the virtual display is the SOLE active display" stutter
|
||||
//! class (Apollo #179/#358/#368/#563/#776 and our own field report). When an `Exclusive` isolate
|
||||
//! deactivates the physical monitor, its link drops and the monitor falls into its no-signal flow:
|
||||
//! standby with periodic auto-input-scan / link probing that the GPU driver services with
|
||||
//! display-subsystem stalls at a seconds-scale cadence. A panel commanded off over DDC/CI believes
|
||||
//! it has an owner and (on cooperating firmware) stops probing. This is deliberately shipped as an
|
||||
//! experiment: whether it helps discriminates *who initiates* the churn — monitor firmware (DDC-off
|
||||
//! fixes it) vs. the driver servicing a dark head regardless (only a driven link fixes it, i.e.
|
||||
//! topology `primary`/`extend`).
|
||||
//!
|
||||
//! Everything here is best-effort and warn-and-continue: monitors without DDC/CI support (or with
|
||||
//! it disabled in the OSD), docks/KVMs that don't pass the channel through, and laptop-internal
|
||||
//! panels (ACPI backlight, no DDC) all simply probe as unsupported and are skipped. Each DDC
|
||||
//! transaction can block for tens of ms — callers run at session acquire/teardown, never on the
|
||||
//! frame path.
|
||||
|
||||
use windows::Win32::Devices::Display::{
|
||||
DestroyPhysicalMonitors, GetNumberOfPhysicalMonitorsFromHMONITOR,
|
||||
GetPhysicalMonitorsFromHMONITOR, GetVCPFeatureAndVCPFeatureReply, SetVCPFeature,
|
||||
PHYSICAL_MONITOR,
|
||||
};
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFOEXW,
|
||||
};
|
||||
|
||||
/// MCCS VCP code 0xD6 — display power mode.
|
||||
const VCP_POWER_MODE: u8 = 0xD6;
|
||||
/// VCP 0xD6 value: on.
|
||||
const POWER_ON: u32 = 0x01;
|
||||
/// VCP 0xD6 value: DPMS off (dark panel, DDC controller stays responsive). Deliberately NOT 0x05.
|
||||
const POWER_OFF: u32 = 0x04;
|
||||
|
||||
/// One active display: its HMONITOR and GDI device name (`\\.\DISPLAYn`).
|
||||
struct ActiveMonitor {
|
||||
hmon: HMONITOR,
|
||||
device: String,
|
||||
}
|
||||
|
||||
/// Enumerate the active displays (HMONITOR + GDI name). HMONITORs are only valid while a display
|
||||
/// is part of the desktop — which is exactly why the off-command must run BEFORE a CCD isolate
|
||||
/// and the on-command AFTER the restore.
|
||||
fn active_monitors() -> Vec<ActiveMonitor> {
|
||||
unsafe extern "system" fn collect(
|
||||
hmon: HMONITOR,
|
||||
_hdc: HDC,
|
||||
_rect: *mut windows::Win32::Foundation::RECT,
|
||||
data: LPARAM,
|
||||
) -> windows::core::BOOL {
|
||||
// SAFETY: `data` is the `&mut Vec<ActiveMonitor>` passed by `active_monitors` below,
|
||||
// valid for the duration of the synchronous EnumDisplayMonitors call that invokes us.
|
||||
let out = unsafe { &mut *(data.0 as *mut Vec<ActiveMonitor>) };
|
||||
let mut info = MONITORINFOEXW::default();
|
||||
info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
|
||||
// SAFETY: `hmon` is the live monitor handle the enumeration just handed us; `info` is a
|
||||
// properly-sized MONITORINFOEXW local whose cbSize is set, which GetMonitorInfoW requires
|
||||
// to safely write the extended (szDevice) variant.
|
||||
if unsafe { GetMonitorInfoW(hmon, &mut info.monitorInfo) }.as_bool() {
|
||||
let len = info
|
||||
.szDevice
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(info.szDevice.len());
|
||||
out.push(ActiveMonitor {
|
||||
hmon,
|
||||
device: String::from_utf16_lossy(&info.szDevice[..len]),
|
||||
});
|
||||
}
|
||||
true.into() // keep enumerating
|
||||
}
|
||||
|
||||
let mut out: Vec<ActiveMonitor> = Vec::new();
|
||||
// SAFETY: `collect` matches MONITORENUMPROC; `&mut out` outlives the synchronous enumeration
|
||||
// and is only dereferenced inside the callback (single-threaded — user32 invokes it inline).
|
||||
let _ = unsafe {
|
||||
EnumDisplayMonitors(
|
||||
None,
|
||||
None,
|
||||
Some(collect),
|
||||
LPARAM(&mut out as *mut Vec<ActiveMonitor> as isize),
|
||||
)
|
||||
};
|
||||
out
|
||||
}
|
||||
|
||||
/// Apply `value` to VCP 0xD6 on every physical monitor behind `hmon` that answers a 0xD6 probe.
|
||||
/// Returns how many panels acknowledged the set. `device` is for the log lines only.
|
||||
fn set_power(hmon: HMONITOR, device: &str, value: u32) -> u32 {
|
||||
let mut n = 0u32;
|
||||
// SAFETY: `hmon` is a live monitor handle from the enumeration; `n` is a valid out-param.
|
||||
if unsafe { GetNumberOfPhysicalMonitorsFromHMONITOR(hmon, &mut n) }.is_err() || n == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut phys = vec![PHYSICAL_MONITOR::default(); n as usize];
|
||||
// SAFETY: `phys` is sized to exactly the count the API just reported for this handle.
|
||||
if unsafe { GetPhysicalMonitorsFromHMONITOR(hmon, &mut phys) }.is_err() {
|
||||
return 0;
|
||||
}
|
||||
let mut acked = 0u32;
|
||||
for p in &phys {
|
||||
// PHYSICAL_MONITOR is `packed(1)` (dxva2 header pragma) — copy the fields OUT by value
|
||||
// before touching them; a reference into a packed field is rejected (E0793, UB).
|
||||
let handle = p.hPhysicalMonitor;
|
||||
let desc_raw = p.szPhysicalMonitorDescription;
|
||||
let len = desc_raw
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(desc_raw.len());
|
||||
let desc = String::from_utf16_lossy(&desc_raw[..len]);
|
||||
// Probe first: a monitor without DDC/CI (or with it disabled in the OSD, or behind a
|
||||
// dock/KVM that drops the channel) fails here and is skipped — never blind-write to a
|
||||
// bus we can't read.
|
||||
let (mut current, mut max) = (0u32, 0u32);
|
||||
// SAFETY: `handle` is the live physical-monitor handle (valid until
|
||||
// DestroyPhysicalMonitors below); the value pointers are valid locals ('None' for the
|
||||
// code-type out-param we don't need).
|
||||
let probe = unsafe {
|
||||
GetVCPFeatureAndVCPFeatureReply(
|
||||
handle,
|
||||
VCP_POWER_MODE,
|
||||
None,
|
||||
&mut current,
|
||||
Some(&mut max),
|
||||
)
|
||||
};
|
||||
if probe == 0 {
|
||||
tracing::debug!(
|
||||
device,
|
||||
monitor = desc,
|
||||
"DDC/CI: no reply to the power-mode (0xD6) probe — skipping (no DDC/CI, \
|
||||
disabled in the OSD, or not passed through)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// SAFETY: as the probe above — same live physical-monitor handle, plain value args.
|
||||
let set = unsafe { SetVCPFeature(handle, VCP_POWER_MODE, value) };
|
||||
if set == 0 {
|
||||
tracing::warn!(
|
||||
device,
|
||||
monitor = desc,
|
||||
value,
|
||||
"DDC/CI: power-mode set failed after a successful probe"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
device,
|
||||
monitor = desc,
|
||||
from = current,
|
||||
to = value,
|
||||
"DDC/CI: panel power mode commanded"
|
||||
);
|
||||
acked += 1;
|
||||
}
|
||||
}
|
||||
// SAFETY: `phys` holds exactly the handles GetPhysicalMonitorsFromHMONITOR opened for us;
|
||||
// each is destroyed once, here.
|
||||
if let Err(e) = unsafe { DestroyPhysicalMonitors(&phys) } {
|
||||
tracing::debug!(device, "DDC/CI: DestroyPhysicalMonitors failed: {e}");
|
||||
}
|
||||
acked
|
||||
}
|
||||
|
||||
/// Command every physical panel EXCEPT `exclude_gdi` (the virtual display) off via DDC/CI
|
||||
/// (VCP 0xD6 → DPMS off). Call while the physical displays are still ACTIVE — i.e. immediately
|
||||
/// before the `Exclusive` CCD isolate. Returns how many panels acknowledged.
|
||||
pub fn panel_off_except(exclude_gdi: &str) -> u32 {
|
||||
let mut acked = 0;
|
||||
for m in active_monitors() {
|
||||
if m.device.eq_ignore_ascii_case(exclude_gdi) {
|
||||
continue;
|
||||
}
|
||||
acked += set_power(m.hmon, &m.device, POWER_OFF);
|
||||
}
|
||||
if acked == 0 {
|
||||
tracing::info!(
|
||||
"DDC/CI: no panel accepted the off command — the experiment is a no-op on this box \
|
||||
(monitors without DDC/CI, or none besides the virtual display)"
|
||||
);
|
||||
}
|
||||
acked
|
||||
}
|
||||
|
||||
/// Best-effort wake: command ON to every physical panel that answers. Call AFTER the CCD restore
|
||||
/// has re-activated the physical paths — the returning signal alone wakes DPMS-off panels on most
|
||||
/// firmware; this is the belt-and-braces for the rest.
|
||||
pub fn panel_on_all() -> u32 {
|
||||
let mut acked = 0;
|
||||
for m in active_monitors() {
|
||||
acked += set_power(m.hmon, &m.device, POWER_ON);
|
||||
}
|
||||
acked
|
||||
}
|
||||
Reference in New Issue
Block a user