feat(host,web): experimental PnP monitor-devnode disable for Exclusive sessions
ci / web (push) Successful in 1m0s
ci / docs-site (push) Successful in 1m6s
apple / swift (push) Successful in 1m9s
decky / build-publish (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 47s
windows-host / package (push) Failing after 3m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m54s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m21s
flatpak / build-publish (push) Failing after 2m26s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m2s
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
release / apple (push) Failing after 19m24s

Second experiment against the connected-but-dark-head stutter (field-proven
on the reporter's box: unplugging his standby HDMI TV removes a metronomic
~4 s double-jolt; DDC/CI is a dead end for TVs — measured on the lab LG, VCP
0xD6 gets no I2C ACK). An Exclusive isolate only removes physical monitors
from the CCD topology; their PnP devnodes stay live, so every standby wake
(auto input scan, Instant-On HPD cycling) still triggers the full Windows
reaction: PnP arrival/removal, CCD re-evaluation, DWM invalidation — the
suspected hiccup mechanism (Apollo #368's Device-Manager-refresh signature).

- New `pnp_disable_monitors` display-policy axis (default off): orthogonal
  to presets like game_session/ddc_power_off, surfaced in GET/PUT
  /display/settings + the enforced list, carried through the layout
  transform.
- windows/monitor_devnode.rs: after the isolate takes, disable exactly the
  deactivated monitors' devnodes — CCD target → monitor device path
  (DISPLAYCONFIG_TARGET_DEVICE_NAME) → PnP instance id → CM_Disable_DevNode
  with CM_DISABLE_PERSIST, so a hot-plug RE-ARRIVAL stays disabled (that
  persistence is the whole point). Teardown re-enables BEFORE the CCD
  restore (+300 ms re-arrival settle) so restored paths have their monitors
  back. Precise selection — co-installed third-party virtual displays are
  never touched.
- Crash safety: instance ids journal to <config>/pnp-disabled-monitors.json
  before disabling; serve startup re-enables leftovers from a crashed host.
  Worst case is documented in the console help (Device Manager re-enable).
- Web console: second Experimental-badged toggle (the DDC block refactored
  into a shared ExperimentalToggle), EN/DE strings, preset-switch carry.

Verified: Linux 263 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 16:47:40 +02:00
parent d1770c3476
commit a011aebef5
9 changed files with 362 additions and 36 deletions
+75 -32
View File
@@ -165,9 +165,11 @@ const DisplayForm: FC<{
identity: effective.identity,
layout: effective.layout,
max_displays: effective.max_displays,
// Game-session + DDC are orthogonal to the preset — carry them through the Custom switch.
// Game-session + the experimental axes 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,
pnp_disable_monitors: draft.pnp_disable_monitors ?? false,
});
} else {
apply({ ...draft, preset: id as Preset });
@@ -182,8 +184,9 @@ 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.
// The experimental axes aren't part of a preset — keep the current settings.
ddc_power_off: draft.ddc_power_off ?? false,
pnp_disable_monitors: draft.pnp_disable_monitors ?? false,
});
// A custom card is "current" when the in-force policy is a Custom one whose fields + game-session
@@ -467,36 +470,34 @@ 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>
{/* EXPERIMENTAL toggles — orthogonal like game-session (survive preset switches, apply
immediately). Windows-only in effect, acted on at the Exclusive isolate. */}
<ExperimentalToggle
label={m.display_ddc()}
help={m.display_ddc_help()}
value={draft.ddc_power_off ?? false}
offLabel={m.display_ddc_disabled()}
onLabel={m.display_ddc_enabled()}
busy={busy}
onSet={(on) => {
const next = { ...draft, ddc_power_off: on };
setDraft(next);
apply(next);
}}
/>
<ExperimentalToggle
label={m.display_pnp()}
help={m.display_pnp_help()}
value={draft.pnp_disable_monitors ?? false}
offLabel={m.display_pnp_disabled()}
onLabel={m.display_pnp_enabled()}
busy={busy}
onSet={(on) => {
const next = { ...draft, pnp_disable_monitors: on };
setDraft(next);
apply(next);
}}
/>
{/* What's in force right now */}
<div className="flex flex-wrap items-center gap-2 border-t pt-3">
@@ -513,6 +514,9 @@ const DisplayForm: FC<{
{(draft.ddc_power_off ?? false) && (
<Badge variant="outline">{m.display_ddc_badge()}</Badge>
)}
{(draft.pnp_disable_monitors ?? false) && (
<Badge variant="outline">{m.display_pnp_badge()}</Badge>
)}
</div>
<p className="max-w-prose text-xs text-muted-foreground">{m.display_pending_note()}</p>
@@ -535,6 +539,45 @@ const Field: FC<{ label: string; help?: string; children: ReactNode }> = ({
</div>
);
/**
* An Experimental-badged on/off policy toggle (the DDC/CI and PnP monitor axes) — rendered outside
* the Custom block like the game-session axis: survives preset switches and applies immediately.
*/
const ExperimentalToggle: FC<{
label: string;
help: string;
value: boolean;
offLabel: string;
onLabel: string;
busy: boolean;
onSet: (v: boolean) => void;
}> = ({ label, help, value, offLabel, onLabel, busy, onSet }) => (
<div className="border-t pt-4">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label className="block">{label}</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={value === on ? "default" : "outline"}
disabled={busy}
onClick={() => onSet(on)}
>
{on ? onLabel : offLabel}
</Button>
))}
</div>
<p className="max-w-prose text-xs text-muted-foreground">{help}</p>
</div>
</div>
);
/** A [`Field`] whose control is a row of mutually-exclusive option buttons (topology / conflict / …). */
const Choice: FC<{
label: string;