diff --git a/design/display-management.md b/design/display-management.md index b0d0540..06818f7 100644 --- a/design/display-management.md +++ b/design/display-management.md @@ -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 0–4 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 0–4: 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 "`, 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-` 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 1–3 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.