diff --git a/docs-site/content/docs/gamescope-multiuser.md b/docs-site/content/docs/gamescope-multiuser.md new file mode 100644 index 0000000..23bdd5b --- /dev/null +++ b/docs-site/content/docs/gamescope-multiuser.md @@ -0,0 +1,73 @@ +--- +title: "gamescope Multi-User Isolation (deferred)" +description: "Research + design for concurrent INDEPENDENT gamescope desktops (multi-user), and why it's deferred. The shared-desktop multi-view case already landed." +--- + +**Status: deferred (2026-06-12).** Concurrent sessions landed for the **shared-desktop multi-view** +case — multiple devices viewing/controlling the *same* KWin/Mutter/wlroots desktop ([Status](/docs/status)). +This page captures the research for the *other* model — **independent desktops** (each client its own +gamescope instance: the multi-user / cloud-gaming-on-one-box case) — and why it's parked. Pick this +up from here if the use case becomes a priority. + +## What landed vs what this is + +| Model | Backends | Input | Audio | Status | +|---|---|---|---|---| +| **Shared-desktop multi-view** | kwin / mutter / wlroots | shared (all drive one desktop) | shared (all hear one desktop) | ✅ **landed** — correct semantics: stream *your* desktop to laptop + TV at once | +| **Independent desktops (multi-user)** | gamescope | **per-session** (each drives its own game) | **per-session** | ⏸ **deferred** — this page | + +For independent desktops, shared input/audio is *wrong* — each user must drive and hear only their own +session. gamescope is the natural fit: each `create()` spawns a fresh nested compositor (own +rendering, own EIS input socket). The blocker is that the host's input/audio/mic are host-lifetime +**shared** services, and the gamescope EIS socket is relayed through a single global file. + +## Current architecture (the research) + +Each gamescope **process is per-session** (`vdisplay/gamescope.rs::create()` spawns one; the +`VirtualOutput.keepalive` owns it). But: + +- **EIS input socket — single global file.** gamescope exports `LIBEI_SOCKET` for its children; a + shell wrapper relays it to the fixed path `/tmp/punktfunk-gamescope-ei` (`EI_SOCKET_FILE`). + **Two concurrent instances overwrite each other's socket name** in that one file. +- **Injector — one host-lifetime `!Send` service.** `m3.rs::InjectorService` opens **one** + `inject::open(backend)` for the whole run and forwards events over an mpsc channel. It was made + shared deliberately (the portal `CreateSession` churn wedged KWin's EIS — "EIS setup timed out"). + For gamescope it reads the one global socket file, so all sessions' input lands in whichever + instance wrote last. +- **Audio — global default-sink monitor.** `audio::open_audio_capture()` sets + `STREAM_CAPTURE_SINK` and autoconnects to the host's **default sink monitor** (PW_ID_ANY) — the + whole system's output, not a per-gamescope node. gamescope exposes **no per-instance audio node**. +- **Mic — one global `Audio/Source`.** `MicService` feeds one PipeWire source named `punktfunk-mic`; + all clients' mic uplinks mix into it. +- Per-session already (no work): the gamescope process, the PipeWire video node, and the uinput + gamepads. + +## What it would take + +1. **Per-instance EIS socket** — give each gamescope a unique relay file + (`/tmp/punktfunk-gamescope-{id}-ei`) and carry the path on `VirtualOutput` (new field) so the + session can find its own socket. +2. **Per-session injector** — for gamescope sessions, create a **per-session** injector bound to that + socket (its own thread, since `InputInjector` is `!Send`), instead of the shared `InjectorService`. + Keep the shared service for the portal backends (kwin/mutter) where shared input is correct. + Ordering nuance: the input thread is wired before the gamescope socket exists, so the per-session + injector must open **lazily** (on first event, by which time gamescope is up) or be created after + `build_pipeline`. +3. **Per-session audio (the bigger piece).** gamescope has no per-instance audio node, but audio + *is* isolatable: create a **per-session PipeWire null-sink**, route that gamescope's apps to it + (`PULSE_SINK` / a target node on the spawn env), and capture **that sink's monitor** per session. + This is the largest addition — null-sink create/teardown + routing + per-session capture. +4. **Per-session mic** — a virtual `Audio/Source` per session (`punktfunk-mic-{id}`), routed into + that gamescope, instead of the one global source. + +## Why deferred + +- It's a **large multi-file refactor** — the whole input path (per-instance sockets + per-session + injector + the lazy-open ordering), **plus** per-session null-sink audio routing, **plus** per-session + mic — for a **niche** use case (multiple independent users gaming on one box). +- The **common** concurrency case — stream one desktop to several of *your own* devices — is the + shared-desktop multi-view model, which **already landed and is the correct semantics** for it. +- No correctness gap in what shipped: concurrent sessions work today; this is purely the *additional* + independent-desktops model. + +Revisit when there's a real multi-user requirement. The plumbing list above is the whole job. diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index e63b9bd..6a5a389 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -8,6 +8,7 @@ "roadmap", "m2-plan", "apple-stage2-presenter", + "gamescope-multiuser", "---Setup---", "linux-setup", "headless-box", diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index 8ec5f29..2b72e5b 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -325,7 +325,13 @@ buffer; `sendmmsg`/`recvmmsg` batching; the capture-timestamp anchor placement. compressed samples internally with no per-frame callback, so it needs the **stage-2 presenter** (`VTDecompressionSession` decode-completion timestamp + `CAMetalLayer`/display-link present) to stamp on-glass present time; (2) the host **render→capture** term (PipeWire buffer presentation - timestamp vs our capture stamp). `tools/latency-probe` is still the cross-machine orchestrator. + timestamp vs our capture stamp). **render→capture is parked (low priority):** pipewire-rs 0.9.2 + exposes no per-buffer meta accessor, no raw buffer pointer (`pub(crate)`), and no stream-timing + API, so reading `SPA_META_Header.pts` would require introducing raw `spa_sys`/`pw_sys` FFI into the + working, perf-critical capture buffer-acquisition — a risky rewrite for the *smallest* g2g term, + with KWin/Mutter `Header.pts` support unconfirmed. Glass-to-glass is effectively complete as + **capture→present** (the stage-2 presenter measures it). `tools/latency-probe` is still the + cross-machine orchestrator. - **Bigger bets (ordered, deferred — need real-NIC/GPU/Mac validation):** 1. **CUDA stream+event** to drop one of two redundant `cuCtxSynchronize` in `submit_cuda` (keep the copy) — ~0.1–0.4 ms@720p, ~1 ms@5K; only if per-stage timing proves the sync is on the path. @@ -356,3 +362,23 @@ GameStream already auto-discovered via mDNS (`_nvstream._tcp`). Now both the uni (`pair=optional`); fingerprint + pairing state correct in both. - **Next** (not done): wire NWBrowser discovery into the Apple client UI (host picker); the host-side contract above is all it needs. + +## 14. Concurrent sessions *(shared-desktop multi-view ✅ done; multi-user deferred)* + +The host no longer serves one client at a time. The accept loop spawns each session (`JoinSet`), +bounded by `--max-concurrent` (default 4 — a NVENC bound; overflow waits in the accept queue). Each +session keeps its own virtual output + NVENC encoder; the host-lifetime input/audio/mic services stay +shared. + +- **Done & live (shared-desktop multi-view):** multiple devices viewing/controlling the **same** + desktop on the shared-desktop backends (kwin/mutter/wlroots) — e.g. stream *your* desktop to a + laptop + a TV at once; shared input/audio is the correct semantics there. Validated live on the + GNOME box: two clients → **two independent Mutter virtual outputs (1280×720 + 1920×1080) streaming + simultaneously**. The QUIC handshake stays in the accept loop so a failed handshake doesn't consume + a slot or block the next client. +- **Deferred — gamescope multi-user (independent desktops):** the *other* model — each client its own + gamescope instance, with **per-session input + audio + mic** (the multi-user / cloud-gaming case). + Researched 2026-06-12 and **parked**: it's a large multi-file refactor (per-instance EIS sockets + + a per-session injector + per-session **null-sink audio routing** + per-session mic) for a niche + use case, while the common multi-device case is already covered by the multi-view model above. Full + research + the plumbing list: [gamescope Multi-User Isolation](/docs/gamescope-multiuser).