feat(vdisplay): complete Stage 5 §6A group semantics — per-group restore, Mutter group-aware, gamescope groups
Host-side completion of Stage 5 (§6A many-clients-as-monitors), all unit-tested; two-session on-glass validation still pending (no GPU on the dev VM): - Per-group topology restore (§6.1): the KWin `exclusive` restore no longer rides the per-session StopGuard (which re-enabled the physical the moment the FIRST of several exclusive sessions dropped, under a live sibling). KWin hands its restore to the registry as a closure (new trait `take_topology_restore`); the registry keeps it in the display group (`Entry.topology_restore`) and, on teardown, floats it to a surviving same-group sibling (`hand_off_restore`) or runs it when the group empties — outside the lock, before the last output's keepalive drops, so the compositor never sees zero outputs. All three teardown paths (lease drop / linger expiry / mgmt release) honor it. Single-display path byte-for-byte unchanged. Unit-tested: float / run-on-last / non-carrier-first / never-cross-backend. - Mutter group-aware (new trait `set_first_in_group`): the registry tells each backend whether it's the first display of its group; a non-first Mutter session EXTENDS into the already-exclusive desktop instead of re-applying a sole-monitor ApplyMonitorsConfig that would disable the first session's virtual. (Mutter connectors are un-nameable, so it can't build a keep-all-virtuals config; skipping is the safe equivalent.) Single-session unchanged. Residual APPLY_TEMPORARY revert documented. - gamescope groups (§6.1): `registry::group_key` makes each gamescope spawn its own group (independent nested session, no shared desktop) — never auto-rowed against or restore-/topology-grouped with another gamescope. Applied in both the /display/state assembly and the acquire-time position computation. Unit-tested. Remaining Stage 5: the web console arrangement table, on-glass validation, and the documented residuals (wlroots exclusive, Mutter APPLY_TEMPORARY). design doc updated. cargo build/test (214)/clippy --all-targets/fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
# Virtual-display management & lifecycle policy — design
|
||||
|
||||
> **Status (2026-07-05):** **Stages 0–4 DONE + on-glass validated; Stage 5 IN PROGRESS** (branch
|
||||
> `display-mgmt-stage0`, not yet merged). Stage 5 so far: group-aware KWin `exclusive` (§6.1) + the
|
||||
> **layout foundation** — a pure arrangement engine (`vdisplay/layout.rs`, auto-row + manual), the
|
||||
> `PUT /display/layout` mgmt endpoint, group/position/index surfaced in `/display/state`, and KWin
|
||||
> manual-position apply. See the **Status — handoff** block under §11 for the per-stage state, the key
|
||||
> decisions (notably the Windows `reject` default), and what's left.
|
||||
> **Status (2026-07-05):** **Stages 0–4 DONE + on-glass validated; Stage 5 HOST-SIDE DONE** (branch
|
||||
> `display-mgmt-stage0`, not yet merged). Stage 5 §6A host-side complete: display **groups**
|
||||
> (`registry::group_key` — one per desktop backend, each gamescope spawn its own), group-aware
|
||||
> `exclusive`/`primary` (KWin name-filter + first-slot-wins; Mutter `set_first_in_group`), **per-group
|
||||
> topology restore** (KWin restore floats through the group, runs on the last member's teardown), the
|
||||
> **layout engine** (`vdisplay/layout.rs`, auto-row + manual) + registry-driven `apply_position`, and the
|
||||
> `PUT /display/layout` endpoint with group/position/index in `/display/state`. **Remaining Stage 5:** the
|
||||
> web console arrangement table + on-glass validation (2 clients on a GPU box) + a couple of documented
|
||||
> residuals (wlroots `exclusive`, Mutter `APPLY_TEMPORARY` revert). See the **Status — handoff** block
|
||||
> under §11 for the per-stage state and the key decisions (notably the Windows `reject` default).
|
||||
> 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
|
||||
@@ -670,17 +674,20 @@ GNOME/Mutter, RTX 5070 Ti), **`.116`** (Bazzite KDE/KWin, AMD — build via a `f
|
||||
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: IN PROGRESS.** Landed: (a) the §6.1 **group-aware exclusive** fix for KWin
|
||||
(`kwin.rs` `MANAGED_PREFIX` + first-slot-wins), unit-tested but NOT yet driven by two concurrent
|
||||
sessions on-glass; (b) the **layout foundation** — a pure arrangement engine
|
||||
(`vdisplay/layout.rs::arrange`, auto-row + manual, unit-tested), a group model in the Linux registry
|
||||
(group = backend; `/display/state` now carries `group`/`display_index`/`position`/`identity_slot`/
|
||||
`topology`, positions computed via the engine), the `PUT /api/v1/display/layout` endpoint (persists a
|
||||
manual arrangement via the pure `EffectivePolicy::with_manual_layout` transform), and KWin
|
||||
**manual-position apply** at create (`apply_manual_position`, guarded + best-effort — a no-op under
|
||||
auto-row / an unpinned slot, so the default path is untouched). The registry reads the backend's
|
||||
resolved slot via a new `VirtualDisplay::last_identity_slot` (only KWin reports one), so the
|
||||
arrangement + state honestly key on per-client identity. Still TODO in Stage 5 (below).
|
||||
- **Stage 5: HOST-SIDE DONE (web table + on-glass pending).** All §6A group semantics landed + unit-tested
|
||||
(no two-session on-glass possible on the GPU-less dev VM): **display groups** (`registry::group_key` — one
|
||||
per desktop backend, each gamescope spawn its own group), **group-aware exclusive/primary** (KWin
|
||||
`MANAGED_PREFIX` + first-slot-wins; Mutter `set_first_in_group` → a non-first session extends rather than
|
||||
re-clobbering), **per-group topology restore** (KWin hands its restore to the registry via
|
||||
`take_topology_restore`; `Entry::topology_restore` + `hand_off_restore` float it to a surviving sibling
|
||||
and run it only when the group empties, before the last output drops — all 3 teardown paths), the pure
|
||||
**layout engine** (`vdisplay/layout.rs::arrange`, auto-row + manual) + **registry-driven `apply_position`**
|
||||
(`position_for_new` over the whole group; skips the origin so the single-display path is unchanged), the
|
||||
`PUT /api/v1/display/layout` endpoint (`EffectivePolicy::with_manual_layout`), and `/display/state` now
|
||||
carrying `group`/`display_index`/`position`/`identity_slot`/`topology`. The registry keys the arrangement
|
||||
on per-client identity via `VirtualDisplay::last_identity_slot` (KWin). **Remaining:** the web arrangement
|
||||
table + on-glass validation + the documented residuals (wlroots `exclusive`, Mutter `APPLY_TEMPORARY`
|
||||
revert) — see the Stage 5 entry below.
|
||||
|
||||
**Decisions / deltas from this plan as written — read before continuing:**
|
||||
- **Windows admission default is `reject`, NOT `join`** (supersedes the Stage-4 line below). Two
|
||||
@@ -734,10 +741,10 @@ Stage-5 group-aware exclusive.
|
||||
`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. [IN PROGRESS]** 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:**
|
||||
- **Stage 5 — §6A multi-client monitors. [HOST-SIDE DONE ✓ — web table + on-glass pending]** 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:**
|
||||
- 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.
|
||||
@@ -763,20 +770,39 @@ Stage-5 group-aware exclusive.
|
||||
first-of-group session (and every non-KWin backend, which no-ops `apply_position`) issues no
|
||||
positioning at all — the historical single-display path is byte-for-byte unchanged. `position_for_new`
|
||||
is unit-tested. *On-glass-validation-pending (kscreen positioning of a live virtual output).*
|
||||
- **Per-group topology restore** (design §6.1 — restore the physical only when the group's LAST member
|
||||
drops): the KWin `exclusive` restore no longer rides the per-session `StopGuard` (which would re-enable
|
||||
the physical the moment the FIRST of several exclusive sessions dropped, under a live sibling). KWin
|
||||
now hands the restore to the registry as a closure (`take_topology_restore`); the registry keeps it in
|
||||
the display **group** (`Entry::topology_restore`) and, on teardown, **floats** it to a surviving
|
||||
same-group sibling (`hand_off_restore`) or, when the group empties, runs it — outside the lock, BEFORE
|
||||
the last output's keepalive drops, so the compositor never sees zero outputs. All three teardown paths
|
||||
(lease drop / linger expiry / mgmt release) honor it. The single-display path is byte-for-byte
|
||||
unchanged (one member → run on its teardown). `hand_off_restore` is unit-tested (float / run-on-last /
|
||||
non-carrier-first / never-cross-backend). *Residual concurrent-connect race + two-session on-glass
|
||||
validation pending.*
|
||||
- **Mutter group-aware** (`set_first_in_group`): the registry tells each backend whether it is the
|
||||
FIRST display of its group; a non-first Mutter session **extends** into the already-exclusive desktop
|
||||
instead of re-applying a sole-monitor `ApplyMonitorsConfig` that would disable the first session's
|
||||
virtual. (Simpler than the originally-planned "include all group virtuals," which Mutter can't do —
|
||||
its connectors are un-nameable — and achieves the same connect-time outcome.) Single-session unchanged
|
||||
(`first == true`). *Residual: Mutter `APPLY_TEMPORARY` reverts the topology when the FIRST session
|
||||
leaves under a live sibling (§7) — a full fix needs a group-owned `DisplayConfig` connection; deferred.
|
||||
Concurrent-Mutter on-glass validation pending (even ≥2 `RecordVirtual` monitors is unproven).*
|
||||
- **gamescope groups** (design §6.1): a gamescope **spawn** is an independent nested session per client
|
||||
(no shared desktop), so `registry::group_key` makes each gamescope display its OWN group — never
|
||||
auto-rowed against, topology-grouped with, or restore-grouped with another gamescope. Unit-tested.
|
||||
(§6B single-output "decline extras" is Stage 6.)
|
||||
**TODO (still Stage 5):**
|
||||
- **Mutter + wlroots group-aware analogues** (Mutter is more involved — its sole-monitor
|
||||
`ApplyMonitorsConfig` must include ALL group virtuals, not just its own; it can't name-filter like
|
||||
KWin — the registry must tell it which pre-existing connectors are managed siblings).
|
||||
- **Per-group topology restore** (restore the physical only when the group's LAST member drops): KWin's
|
||||
`exclusive` acquire is group-aware, but its RESTORE is still per-display (the `StopGuard` re-enables
|
||||
the physical on its own teardown), so the first sibling out re-enables the physical while a sibling is
|
||||
still exclusive. The clean fix moves the restore into a registry group record (run once, when the
|
||||
group empties, ordered BEFORE the last member's output is reclaimed so KWin never sees zero outputs).
|
||||
Needs two-session on-glass to validate — deferred alongside the group-aware-exclusive on-glass item.
|
||||
- Console arrangement table (web, x/y first); gamescope groups (single-output → decline extras, §6B).
|
||||
*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.
|
||||
- **Console arrangement table (web)** — an x/y editor in the `Virtual displays` card reading
|
||||
`/display/state` and writing `PUT /display/layout` (x/y table first; drag mini-map is the stretch).
|
||||
The host API + persistence are done; this is the remaining web-only piece.
|
||||
- **wlroots group-aware exclusive** stays deferred: wlroots `exclusive` is not implemented at all (needs
|
||||
a Sway box), so there is no topology to make group-aware yet. §6A multi-view on wlroots already works
|
||||
(independent `HEADLESS-N` outputs).
|
||||
*Validate (all on-glass, needs a GPU box + 2 clients — not the dev VM):* 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.
|
||||
- **Stage 6 — §6B protocol + Linux host + GTK client.** `VIDEO_CAP_MULTI_DISPLAY`, control-
|
||||
stream Add/Remove/DisplayAdded, per-flow nonce-salt derivation, per-display pipelines on
|
||||
KWin/wlroots, input display-index routing, C ABI additions, GTK client multi-window
|
||||
|
||||
Reference in New Issue
Block a user