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
+5
View File
@@ -108,6 +108,11 @@
"display_ddc_disabled": "Aus",
"display_ddc_enabled": "Ein",
"display_ddc_badge": "Monitore aus via DDC/CI",
"display_pnp": "Monitor-Geräte beim Streamen deaktivieren (PnP)",
"display_pnp_help": "Nur Windows, wirkt bei Topologie Exklusiv. Zusätzlich zum Entfernen der physischen Monitore vom Desktop deaktiviert der Host deren Windows-Geräteeinträge für die Dauer des Streams und aktiviert sie danach wieder. Ein Monitor oder Fernseher im Standby, der seine Verbindung immer wieder aufweckt (automatische Eingangssuche, Instant-On), kann den Stream so nicht mehr stören — Windows ignoriert seine Aufwach-Ereignisse vollständig, solange das Gerät deaktiviert ist. Stürzt der Host mitten im Stream ab, werden die Monitore beim nächsten Start wieder aktiviert; bis dahin lassen sie sich manuell im Geräte-Manager aktivieren.",
"display_pnp_disabled": "Aus",
"display_pnp_enabled": "Ein",
"display_pnp_badge": "Monitor-Geräte deaktiviert (PnP)",
"display_identity_shared": "Geteilt",
"display_identity_per_client": "Pro Client",
"display_identity_per_client_mode": "Pro Client + Auflösung",
+5
View File
@@ -108,6 +108,11 @@
"display_ddc_disabled": "Off",
"display_ddc_enabled": "On",
"display_ddc_badge": "Monitors off via DDC/CI",
"display_pnp": "Disable monitor devices while streaming (PnP)",
"display_pnp_help": "Windows only, takes effect with Exclusive topology. On top of removing your physical monitors from the desktop, the host disables their Windows device entries for the duration of the stream and re-enables them afterwards. A standby monitor or TV that keeps waking its connection (auto input scan, instant-on) can no longer interrupt the stream — Windows ignores its wake events entirely while the device is disabled. If the host crashes mid-stream, the monitors are re-enabled the next time it starts; until then you can re-enable them manually in Device Manager.",
"display_pnp_disabled": "Off",
"display_pnp_enabled": "On",
"display_pnp_badge": "Monitor devices disabled (PnP)",
"display_identity_shared": "Shared",
"display_identity_per_client": "Per client",
"display_identity_per_client_mode": "Per client + resolution",
+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;