Two bodies of work in one commit (the rename moved files the fixes also touched). Naming/structure cleanup (pre-launch): - Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host, m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source-> Punktfunk1Options/Punktfunk1Source. - Clients consolidated out of crates/ into clients/: punktfunk-client-rs-> clients/probe (crate punktfunk-probe), client-linux->clients/linux, client-windows->clients/windows, punktfunk-android->clients/android/native (crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI contract is unchanged). crates/ now holds only core + host. - Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site, kept only in docs/implementation-plan.md. docs/m2-plan.md-> docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated. Client loss-recovery (video froze and never recovered after a brief drop): - Export punktfunk_connection_frames_dropped through the C ABI (the core already tracked it for the client keyframe-recovery loop; it was never reachable from the ABI clients). Regenerated punktfunk_core.h. - Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll frames_dropped and request a keyframe when it climbs -- the same loss-driven recovery Linux/Windows already had. Under infinite GOP the decoder silently conceals reference-missing frames, so the decode-error trigger rarely fires. Apple rumble robustness (worked then went spotty -- DualSense + Xbox): - Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio interruption / server reset) and drop the permanent `broken` latch on a transient drive failure; latch only when the controller truly has no haptics. - Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging. Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift. Not runnable on this box (verify in CI): Gitea workflows, gradle/Android, flatpak, Swift/decky. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
title, description
| title | description |
|---|---|
| gamescope Multi-User Isolation (deferred) | 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). 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_SOCKETfor 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
!Sendservice.punktfunk1.rs::InjectorServiceopens oneinject::open(backend)for the whole run and forwards events over an mpsc channel. It was made shared deliberately (the portalCreateSessionchurn 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()setsSTREAM_CAPTURE_SINKand 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.MicServicefeeds one PipeWire source namedpunktfunk-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
- Per-instance EIS socket — give each gamescope a unique relay file
(
/tmp/punktfunk-gamescope-{id}-ei) and carry the path onVirtualOutput(new field) so the session can find its own socket. - Per-session injector — for gamescope sessions, create a per-session injector bound to that
socket (its own thread, since
InputInjectoris!Send), instead of the sharedInjectorService. 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 afterbuild_pipeline. - 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. - Per-session mic — a virtual
Audio/Sourceper 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.