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
+4
View File
@@ -2517,6 +2517,10 @@
"type": "object",
"description": "The user-facing display-management policy — what `display-settings.json` holds and what the mgmt\nAPI GETs/PUTs. When [`preset`](Self::preset) is not [`Preset::Custom`] the explicit fields are\nignored (the console writes one or the other); [`effective`](Self::effective) resolves both to a\nsingle [`EffectivePolicy`].",
"properties": {
"ddc_power_off": {
"type": "boolean",
"description": "EXPERIMENTAL (Windows): command physical monitors' panels off over DDC/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan / DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched."
},
"game_session": {
"$ref": "#/components/schemas/GameSession",
"description": "How a game-launching session is served (`design/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched."
+3
View File
@@ -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;
+10 -4
View File
@@ -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
+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
+201
View File
@@ -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
}
+6
View File
@@ -102,6 +102,12 @@
"display_game_session_help": "Wie eine Sitzung bedient wird, die ein Spiel aus der Bibliothek startet. „Dediziert“ gibt dem Start immer ein eigenes headless-gamescope in genau deiner Auflösung — das Spiel startet direkt, ohne Steam Big Picture, ohne Game-Mode. „Auto“ nutzt die aktuelle Sitzung der Box. gamescope muss installiert sein; sonst fällt Dediziert auf Auto zurück.",
"display_game_session_auto": "Auto",
"display_game_session_dedicated": "Dediziert",
"display_experimental": "Experimentell",
"display_ddc": "Monitore beim Streamen ausschalten (DDC/CI)",
"display_ddc_help": "Nur Windows, wirkt bei Topologie Exklusiv. Bevor die physischen Monitore deaktiviert werden, weist der Host sie zusätzlich über den DDC/CI-Steuerkanal an, ihr Panel auszuschalten — und weckt sie am Ende des Streams wieder. Das kann ein periodisches Ruckeln beheben, das manche Setups zeigen, wenn die gestreamte Anzeige die einzige aktive ist (der dunkle Monitor sucht sonst weiter seine Eingänge ab; ein schlafen gelegtes Panel nicht). Monitore ohne DDC/CI werden übersprungen — falls ein Monitor danach nicht aufwacht, einmal seinen Power-Knopf drücken und die Option ausschalten.",
"display_ddc_disabled": "Aus",
"display_ddc_enabled": "Ein",
"display_ddc_badge": "Monitore aus via DDC/CI",
"display_identity_shared": "Geteilt",
"display_identity_per_client": "Pro Client",
"display_identity_per_client_mode": "Pro Client + Auflösung",
+6
View File
@@ -102,6 +102,12 @@
"display_game_session_help": "How a session that launches a game from the library is served. “Dedicated” always gives the launch its own headless gamescope at your exact resolution — the game boots straight in, no Steam Big Picture, no game mode. “Auto” uses whatever session the box is in. gamescope must be installed; otherwise Dedicated falls back to Auto.",
"display_game_session_auto": "Auto",
"display_game_session_dedicated": "Dedicated",
"display_experimental": "Experimental",
"display_ddc": "Turn monitors off while streaming (DDC/CI)",
"display_ddc_help": "Windows only, takes effect with Exclusive topology. Before disabling your physical monitors, the host also tells them to power their panel off over the DDC/CI monitor-control channel, and wakes them again when the stream ends. This can eliminate a periodic stutter some setups see when the streamed display is the only active one (the dark monitor keeps probing its inputs; a panel that was told to sleep doesn't). Monitors without DDC/CI support are skipped — if your monitor doesn't wake up afterwards, press its power button once and turn this off.",
"display_ddc_disabled": "Off",
"display_ddc_enabled": "On",
"display_ddc_badge": "Monitors off via DDC/CI",
"display_identity_shared": "Shared",
"display_identity_per_client": "Per client",
"display_identity_per_client_mode": "Per client + resolution",
+38 -1
View File
@@ -165,8 +165,9 @@ const DisplayForm: FC<{
identity: effective.identity,
layout: effective.layout,
max_displays: effective.max_displays,
// Game-session is orthogonal to the preset — carry it through the Custom switch.
// Game-session + DDC are orthogonal to the preset — carry them through the Custom switch.
game_session: draft.game_session ?? "auto",
ddc_power_off: draft.ddc_power_off ?? false,
});
} else {
apply({ ...draft, preset: id as Preset });
@@ -181,6 +182,8 @@ const DisplayForm: FC<{
preset: "custom",
...p.fields,
game_session: p.game_session ?? "auto",
// The experimental DDC axis isn't part of a preset — keep the current setting.
ddc_power_off: draft.ddc_power_off ?? false,
});
// A custom card is "current" when the in-force policy is a Custom one whose fields + game-session
@@ -464,6 +467,37 @@ const DisplayForm: FC<{
/>
</div>
{/* EXPERIMENTAL: DDC/CI panel power-off — orthogonal like game-session (survives preset
switches, applies immediately). Windows-only in effect, acted on at the Exclusive isolate. */}
<div className="border-t pt-4">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label className="block">{m.display_ddc()}</Label>
<Badge variant="outline" className="text-amber-600 dark:text-amber-500">
{m.display_experimental()}
</Badge>
</div>
<div className="flex flex-wrap gap-2">
{([false, true] as const).map((on) => (
<Button
key={String(on)}
size="sm"
variant={(draft.ddc_power_off ?? false) === on ? "default" : "outline"}
disabled={busy}
onClick={() => {
const next = { ...draft, ddc_power_off: on };
setDraft(next);
apply(next);
}}
>
{on ? m.display_ddc_enabled() : m.display_ddc_disabled()}
</Button>
))}
</div>
<p className="max-w-prose text-xs text-muted-foreground">{m.display_ddc_help()}</p>
</div>
</div>
{/* What's in force right now */}
<div className="flex flex-wrap items-center gap-2 border-t pt-3">
<span className="text-sm text-muted-foreground">{m.display_effective()}:</span>
@@ -476,6 +510,9 @@ const DisplayForm: FC<{
{(draft.game_session ?? "auto") === "dedicated" && (
<Badge variant="secondary">{m.display_game_session_dedicated()}</Badge>
)}
{(draft.ddc_power_off ?? false) && (
<Badge variant="outline">{m.display_ddc_badge()}</Badge>
)}
</div>
<p className="max-w-prose text-xs text-muted-foreground">{m.display_pending_note()}</p>