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:
@@ -1,6 +1,9 @@
|
|||||||
# Virtual-display management & lifecycle policy — design
|
# 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
|
> existing per-compositor `VirtualDisplay` backends** — user-configurable lifecycle (keep-alive
|
||||||
> after disconnect), topology (primary / exclusive), conflict handling (what happens when a second
|
> after disconnect), topology (primary / exclusive), conflict handling (what happens when a second
|
||||||
> client wants a different mode), stable display identity (so desktop environments remember
|
> 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
|
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
|
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
|
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 <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
|
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
|
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
|
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.*
|
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
|
(+proptests: no lost teardowns, no double-frees across arbitrary acquire/release/expiry
|
||||||
interleavings), `registry.rs`, the ownership split (`DisplayLease`/`CaptureSource` — the one
|
interleavings), `registry.rs`, the ownership split (`DisplayLease`/`CaptureSource` — the one
|
||||||
cross-cutting refactor, touches `capture_virtual_output` signatures on both OSes), keep-alive
|
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).
|
linger/pinned decisions to `lifecycle.rs` (its driver specifics untouched).
|
||||||
*Validate:* sway on this box (headless), gamescope spawn: connect → disconnect → verify
|
*Validate:* sway on this box (headless), gamescope spawn: connect → disconnect → verify
|
||||||
vkcube/game still runs → reconnect → same session, no relaunch.
|
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)
|
KWin and Mutter (each behind its validation; fallback recreate), `primary` (without disable)
|
||||||
on KWin/Mutter/Windows, `exclusive` on wlroots, restore paths regression-tested.
|
on KWin/Mutter/Windows, `exclusive` on wlroots, restore paths regression-tested.
|
||||||
*Validate:* headless KDE session (the `run-headless-kde.sh` rig), GNOME box .248.
|
*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,
|
naming (+ the concurrent-session name-clash fix riding along), GameStream identity wiring,
|
||||||
optional `per-client-mode` keying, per-client `default_scale` on KWin.
|
optional `per-client-mode` keying, per-client `default_scale` on KWin.
|
||||||
*Validate on KDE:* connect client A → set 150 % scaling → disconnect → reconnect → scaling
|
*Validate on KDE:* connect client A → set 150 % scaling → disconnect → reconnect → scaling
|
||||||
reapplied; client B unaffected; `kwinoutputconfig.json` inspected for the named entries.
|
reapplied; client B unaffected; `kwinoutputconfig.json` inspected for the named entries.
|
||||||
- **Stage 4 — mode-conflict admission.** Decision function wired into both handshakes, the
|
- **Stage 4 — mode-conflict admission. [DONE ✓]** Decision function (`vdisplay/admission.rs`,
|
||||||
typed punktfunk/1 `busy` refusal, GameStream 503 path, the Windows silent-reconfigure →
|
`decide`/`admit`/`effective_conflict`) wired into the punktfunk/1 handshake + GameStream `h_launch`,
|
||||||
`join`-default change (call it out in release notes — it's a behavior fix), `steal` victim
|
the typed punktfunk/1 `busy` refusal (QUIC close `0x42` + reason), GameStream 503 path, `steal`
|
||||||
signaling reusing the stop-flag plumbing.
|
victim signaling reusing the stop-flag plumbing. **The Windows default is `reject`, NOT the
|
||||||
*Validate:* two probe clients loopback (`--mode` differing) under each policy value.
|
`join`/silent-reconfigure originally planned** — see the handoff Decisions above (single-capturer
|
||||||
- **Stage 5 — §6A multi-client monitors.** Display groups, group-aware exclusive/primary/
|
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
|
restore (incl. the name-filter fix), layout auto-row + manual, `/display/layout`, console
|
||||||
arrangement table. Cheap: rides Stages 1–3 infrastructure, no protocol change.
|
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;
|
*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,
|
drag a window across; disconnect one → its slot lingers per policy, sibling unaffected,
|
||||||
restore only after both drop.
|
restore only after both drop.
|
||||||
|
|||||||
Reference in New Issue
Block a user