docs(display-management): handoff — mark Stages 0-4 done, Stage 5 started

Update the design doc for handoff: top-of-doc status, a Status/handoff block in §11
(per-stage state, validation boxes, key decisions), and per-stage [DONE]/[STARTED]
markers. Records the decisions that diverged from the plan as written — the Windows
admission default is reject (single-capturer IDD-push), reject is typed (QUIC 0x42),
Stage 5's group-aware exclusive fixes a Stage-3 latent bug — and what's left in
Stage 5 (Mutter/wlroots analogues, layout, /display/layout, per-group restore).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:50:50 +00:00
parent eddcd91f48
commit a5dc3134de
+62 -12
View File
@@ -1,6 +1,9 @@
# Virtual-display management & lifecycle policy — design
> **Status:** PLANNED (nothing implemented). This doc designs a **policy layer on top of the
> **Status (2026-07-05):** **Stages 04 DONE + on-glass validated; Stage 5 STARTED** (branch
> `display-mgmt-stage0`, not yet merged). See the **Status — handoff** block under §11 for the
> per-stage state, the key decisions (notably the Windows `reject` default), and what's left.
> This doc designs a **policy layer on top of the
> existing per-compositor `VirtualDisplay` backends** — user-configurable lifecycle (keep-alive
> after disconnect), topology (primary / exclusive), conflict handling (what happens when a second
> client wants a different mode), stable display identity (so desktop environments remember
@@ -649,14 +652,53 @@ display is one data-plane instance — multi-display never muxes into the core p
Each stage lands green (`cargo test/clippy/fmt`, OpenAPI drift check) and is independently
shippable; on-glass validation notes inline. **Heads-up for this box:** the dev VM currently has
no GPU passthrough (RTX 5070 Ti detached at the Proxmox level, 2026-07-01) — KWin-path live
validation needs the GPU back or one of the LAN hosts (.248 GNOME / .48 Fedora KDE).
validation needs the GPU back or one of the LAN hosts.
- **Stage 0 — policy + plumbing-lite.** `policy.rs` (schema/presets/persist/env-compat, fully
### Status — 2026-07-05 handoff
Branch **`display-mgmt-stage0`** (NOT merged; merge when the whole feature is polished/complete).
On-glass validation boxes: **`.173`** (Windows, pf-vdisplay + a physical monitor), **`.21`** (CachyOS
GNOME/Mutter, RTX 5070 Ti), **`.116`** (Bazzite KDE/KWin, AMD — build via a `fedora:43` distrobox;
`.48` Fedora KDE is DOWN). Every commit is `cargo test/clippy/fmt`-green.
- **Stages 04: DONE + on-glass validated.** 0 (policy surface + `/display/settings` + console card),
1 (pure `lifecycle.rs` + `registry.rs` Linux keep-alive pool + ownership split via `DisplayLease` +
`/display/state`/`/display/release`), 2 (topology decoupling — distinct `extend`/`primary`/`exclusive`
via `effective_topology()`), 3 (platform-neutral `identity.rs` map + `per-client-mode` + KWin per-slot
output naming → **KWin persists per-output scale by name**, proven via `kwinoutputconfig.json` on `.116`),
4 (mode-conflict admission — `vdisplay/admission.rs`, loopback-validated for all four policies).
- **Stage 5: STARTED** — only the critical §6.1 **group-aware exclusive** fix for KWin has landed
(`kwin.rs` `MANAGED_PREFIX` + first-slot-wins), unit-tested but NOT yet driven by two concurrent
sessions on-glass. Everything else in Stage 5 is TODO.
**Decisions / deltas from this plan as written — read before continuing:**
- **Windows admission default is `reject`, NOT `join`** (supersedes the Stage-4 line below). Two
concurrent Windows sessions both drive one pf-vdisplay monitor's **single-capturer** IDD-push channel
(newest-delivery-wins) → the 2nd freezes + can WEDGE the 1st (observed live: it wedged `.173`, needed a
reboot — surfaced as Moonlight "no video"). True multi-session Windows capture is §6.6/Stage 7. So on
Windows `separate` (incl. the unconfigured default) resolves to `reject` — a 2nd client gets a clean
503, the live session is protected; `join`/`steal` are explicit opt-ins. Centralised in
`admission::effective_conflict()`, shared by the native handshake + GameStream `h_launch`.
- **Reject IS typed:** punktfunk/1 closes the QUIC connection with app code `0x42` + the reason
`"host busy: streaming WxH@Hz to <client>"`, which the client reads from `ApplicationClosed`.
- **Stage 5's group-aware exclusive fixes a bug Stage 3 introduced:** per-slot names meant a 2nd
`exclusive` session's disable-filter would black out the 1st session's `Virtual-punktfunk-<id>` output.
Fixed on KWin by recognising the whole managed group via the shared `Virtual-punktfunk` prefix.
- **GameStream 503** is implemented (owner-fp on `LaunchSession`, `gamestream_admission()` unit-tested,
shares `effective_conflict()`) but NOT Moonlight-validated (can't drive `/launch` autonomously).
**Deferred (need a display-attached box / a specific compositor / a real client):** the `primary`
physical-keep EFFECT on Linux + a Windows primary-only CCD variant; **wlroots `exclusive`**; the KWin
set-150 %-scaling ROUND-TRIP (SSH can't drive `kscreen-doctor` into the live session — the persist
mechanism itself is already proven); GameStream 503 on-glass; two-concurrent-session validation of the
Stage-5 group-aware exclusive.
- **Stage 0 — policy + plumbing-lite. [DONE ✓]** `policy.rs` (schema/presets/persist/env-compat, fully
unit-tested), mgmt GET/PUT `/display/settings`, console card (settings only), docs page
skeleton with the presets/options tables. Behavior deltas limited to what existing knobs can
express: Windows linger reads the policy; Linux topology auto/extend/exclusive routes through
the existing primary code. *No lifecycle change yet — zero-risk adoption of the surface.*
- **Stage 1 — lifecycle core + Linux keep-alive (easy backends).** `lifecycle.rs` pure machine
- **Stage 1 — lifecycle core + Linux keep-alive. [DONE ✓]** `lifecycle.rs` pure machine
(+proptests: no lost teardowns, no double-frees across arbitrary acquire/release/expiry
interleavings), `registry.rs`, the ownership split (`DisplayLease`/`CaptureSource` — the one
cross-cutting refactor, touches `capture_virtual_output` signatures on both OSes), keep-alive
@@ -665,23 +707,31 @@ validation needs the GPU back or one of the LAN hosts (.248 GNOME / .48 Fedora K
linger/pinned decisions to `lifecycle.rs` (its driver specifics untouched).
*Validate:* sway on this box (headless), gamescope spawn: connect → disconnect → verify
vkcube/game still runs → reconnect → same session, no relaunch.
- **Stage 2 — KWin/Mutter keep-alive + topology decoupling.** Kept-node PipeWire re-attach on
- **Stage 2 — topology decoupling. [DONE ✓]** Kept-node PipeWire re-attach on
KWin and Mutter (each behind its validation; fallback recreate), `primary` (without disable)
on KWin/Mutter/Windows, `exclusive` on wlroots, restore paths regression-tested.
*Validate:* headless KDE session (the `run-headless-kde.sh` rig), GNOME box .248.
- **Stage 3 — identity.** Platform-neutral identity map + migration, per-slot KWin output
- **Stage 3 — identity. [DONE ✓]** Platform-neutral identity map + migration, per-slot KWin output
naming (+ the concurrent-session name-clash fix riding along), GameStream identity wiring,
optional `per-client-mode` keying, per-client `default_scale` on KWin.
*Validate on KDE:* connect client A → set 150 % scaling → disconnect → reconnect → scaling
reapplied; client B unaffected; `kwinoutputconfig.json` inspected for the named entries.
- **Stage 4 — mode-conflict admission.** Decision function wired into both handshakes, the
typed punktfunk/1 `busy` refusal, GameStream 503 path, the Windows silent-reconfigure →
`join`-default change (call it out in release notes — it's a behavior fix), `steal` victim
signaling reusing the stop-flag plumbing.
*Validate:* two probe clients loopback (`--mode` differing) under each policy value.
- **Stage 5 — §6A multi-client monitors.** Display groups, group-aware exclusive/primary/
- **Stage 4 — mode-conflict admission. [DONE ✓]** Decision function (`vdisplay/admission.rs`,
`decide`/`admit`/`effective_conflict`) wired into the punktfunk/1 handshake + GameStream `h_launch`,
the typed punktfunk/1 `busy` refusal (QUIC close `0x42` + reason), GameStream 503 path, `steal`
victim signaling reusing the stop-flag plumbing. **The Windows default is `reject`, NOT the
`join`/silent-reconfigure originally planned** — see the handoff Decisions above (single-capturer
IDD-push). Loopback-validated (all four policies) + `.173` reject-default validated; GameStream 503
unit-tested, Moonlight-pending.
- **Stage 5 — §6A multi-client monitors. [STARTED]** Display groups, group-aware exclusive/primary/
restore (incl. the name-filter fix), layout auto-row + manual, `/display/layout`, console
arrangement table. Cheap: rides Stages 13 infrastructure, no protocol change.
**Done so far:** KWin group-aware `exclusive` (the name-filter fix — recognise the managed group by
the `Virtual-punktfunk` prefix instead of one hardcoded name) + first-slot-wins for the group
primary, unit-tested. **TODO:** Mutter + wlroots group-aware analogues (Mutter is more involved — its
sole-monitor `ApplyMonitorsConfig` must include ALL group virtuals, not just its own); layout
auto-row + manual + `/display/layout` + console table; per-group topology restore (restore the
physical only when the group's LAST member drops); gamescope groups (single-output → decline extras).
*Validate:* two clients (probe + GTK) on the headless KDE box forming a 2-output desktop;
drag a window across; disconnect one → its slot lingers per policy, sibling unaffected,
restore only after both drop.