From 5bd3776190968c259c10c54f0b97acddce867800 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 12 Jun 2026 11:33:39 +0000 Subject: [PATCH] =?UTF-8?q?docs(site):=20make=20docs-site=20the=20knowledg?= =?UTF-8?q?e=20base=20=E2=80=94=20status=20tracker=20+=20setup=20guides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the new docs workflow (docs-site = KB layer; repo docs/ keeps design notes): - Add a canonical Status & Progress tracker (status.md): milestones, per-box live state, and a dated progress log — the go-forward place to track progress. - Add setup guides: GNOME/Mutter host (gnome-box — Secure Boot MOK enroll, the libnvidia-gl EGL fix, autologin, screen-lock disable, appliance unit), headless KDE box, and Bazzite host (ujust input group, gamescope session, gotchas). - Roadmap is now canonical in docs-site (synced the skew-handshake section 12 update); removed the repo docs/roadmap.md copy and repointed README to docs-site. - Nav (meta.json) + landing cards updated; site builds (bun run build). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 +- docs-site/content/docs/bazzite.md | 49 +++++++++++++ docs-site/content/docs/gnome-box.md | 99 ++++++++++++++++++++++++++ docs-site/content/docs/headless-box.md | 62 ++++++++++++++++ docs-site/content/docs/index.mdx | 4 +- docs-site/content/docs/meta.json | 4 ++ docs-site/content/docs/roadmap.md | 21 ++++-- docs-site/content/docs/status.md | 54 ++++++++++++++ 8 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 docs-site/content/docs/bazzite.md create mode 100644 docs-site/content/docs/gnome-box.md create mode 100644 docs-site/content/docs/headless-box.md create mode 100644 docs-site/content/docs/status.md diff --git a/README.md b/README.md index a852434b..2b514ec6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ per-session virtual output (KWin, gamescope, Mutter, Sway backends), encoded wit plane (p50 ~0.8 ms capture→reassembled at 720p120), with a SPAKE2 PIN pairing ceremony. Both run from **one process** (`serve --native`), managed through a REST API + web console. Builds against FFmpeg 7 or 8; deployed live on Bazzite. Full status: [`CLAUDE.md`](CLAUDE.md); -roadmap: [`docs/roadmap.md`](docs/roadmap.md). +roadmap, setup guides & progress: the docs site ([`docs-site/`](docs-site) — Fumadocs; +`bun run dev`), with the canonical [roadmap](docs-site/content/docs/roadmap.md) and +[status](docs-site/content/docs/status.md) there. Design notes stay in [`docs/`](docs). ## Layout diff --git a/docs-site/content/docs/bazzite.md b/docs-site/content/docs/bazzite.md new file mode 100644 index 00000000..b09fcd9c --- /dev/null +++ b/docs-site/content/docs/bazzite.md @@ -0,0 +1,49 @@ +--- +title: "Bazzite / SteamOS-like Host" +description: "Run punktfunk on Bazzite as a headless Steam host — gamescope session at the client's mode, input perms, and the gotchas." +--- + +Running punktfunk on **Bazzite** (Fedora Atomic / SteamOS-like) as a headless game-streaming host. +The host launches a **gamescope** session at the *client's* exact resolution + refresh, so games see +the client mode, not the box's TV. Full packaging (COPR / RPM / bootc) is in +[`packaging/bazzite/README.md`](https://github.com); this page is the operational quick-reference. + +## Input permissions — use the ujust command + +Gamepad + DualSense injection needs the user in the `input` group. On Bazzite you **can't** just +`usermod -aG input` (the immutable base + how the group is managed) — use the provided recipe: + +```sh +ujust add-user-to-input-group +``` + +The udev rule (`60-punktfunk.rules`) grants access to `/dev/uinput` and `/dev/uhid`. A DualSense that +shows "detected but no input" is almost always this **host-side** `/dev/uhid`/`/dev/uinput` +permission, not a client bug — confirm the input group + the udev rule, then re-login. + +## Headless Steam session at the client's mode + +The host owns a `gamescope-session-plus` session and relaunches it when the client mode changes, so +games run at the client's resolution + refresh (`--nested-refresh` + a generated CVT mode). Requires +the headless-appliance prerequisites (linger + `multi-user.target`) and **no** physical gaming +session running. Configure via `host.env`: + +```sh +PUNKTFUNK_COMPOSITOR=gamescope +PUNKTFUNK_GAMESCOPE_SESSION=steam # host owns the session at the client mode +PUNKTFUNK_INPUT_BACKEND=gamescope +``` + +## Gotchas + +- **gamescope ≥ 3.16.22 required.** Older versions deadlock capturing on PipeWire ≥ 1.6 (a loop-lock + bug), and a wedged capture link head-blocks the whole PipeWire daemon. Never `pkill -x gamescope-wl` + on a box where it's the live session compositor — it kills the user's session. +- **The hardware cursor isn't in the capture** (gamescope limitation; won't-fix for now). +- **HDR is blocked upstream**: gamescope's capture node is 8-bit only (PipeWire HDR export + unimplemented), so HDR/10-bit is deferred even though the downstream encode path is ready. + +## FFmpeg + +Bazzite ships FFmpeg 7.x / libavcodec 61 — `ffmpeg-sys-next` auto-detects it and the host builds +against it (validated live). NVENC (`hevc_nvenc` / `av1_nvenc`) works through the system FFmpeg. diff --git a/docs-site/content/docs/gnome-box.md b/docs-site/content/docs/gnome-box.md new file mode 100644 index 00000000..3421fddb --- /dev/null +++ b/docs-site/content/docs/gnome-box.md @@ -0,0 +1,99 @@ +--- +title: "GNOME / Mutter Host Setup" +description: "Bring up an Ubuntu GNOME desktop as a headless punktfunk appliance — the physical-NVIDIA gotchas, autologin, and the Mutter virtual-output path." +--- + +How to bring up an **Ubuntu GNOME** box as a punktfunk host using the **Mutter** backend (per-client +virtual output via the `RecordVirtual` D-Bus API, full zero-copy). Validated live on home-worker-3 +(Ubuntu 26.04, RTX 4090, GNOME Shell 50). Two gotchas here that a QEMU VM never hits — a **physical** +NVIDIA box has Secure Boot and needs the GLVND EGL vendor — so they're called out explicitly. + +## 1. NVIDIA driver under Secure Boot + +Install the driver (`ubuntu-drivers` recommends the right `-open` build): + +```sh +sudo apt-get install -y nvidia-driver-595-open # match what `ubuntu-drivers devices` recommends +``` + +On a physical box with **Secure Boot enabled**, the DKMS module is signed with a local MOK that +isn't enrolled, so `modprobe nvidia` fails with **`Key was rejected by service`** and `nvidia-smi` +reports it can't talk to the driver. Enroll the MOK (no BIOS change, survives kernel/driver updates): + +```sh +sudo mokutil --import /var/lib/shim-signed/mok/MOK.der # set a throwaway one-time password +sudo reboot +# At the blue "Shim UEFI key management" screen on boot: Enroll MOK -> Continue -> Yes -> . +# Needs console access (the screen is firmware-level, not reachable over SSH). +``` + +After reboot, `nvidia-smi` should show the GPU. (Alternatively, disable Secure Boot in firmware.) + +## 2. GNOME Wayland needs the GLVND NVIDIA EGL vendor + +If gnome-shell logs **`GPU /dev/dri/cardN ... not supported by EGL`** / `No EGL display` and +`libEGL warning: ... driver (null)`, GLVND has no NVIDIA EGL vendor and falls back to Mesa for the +NVIDIA card. The missing file is `/usr/share/glvnd/egl_vendor.d/10_nvidia.json`, shipped by +`libnvidia-gl-` — which `nvidia-driver-NNN-open` does **not** always pull in: + +```sh +sudo apt-get install -y libnvidia-gl-595 # provides 10_nvidia.json +``` + +(The EGL external-platform JSONs `10_nvidia_wayland` / `15_nvidia_gbm` are usually already present.) +`nvidia-drm modeset=1` must also be set (it's in `/etc/modprobe.d/nvidia-graphics-drivers-kms.conf` +on a normal install) for Wayland on NVIDIA. + +## 3. A GNOME Wayland session for Mutter to attach to + +The host attaches to a running GNOME **Wayland** session (`wayland-0`). On a headless box, autologin +provides it (a connected monitor also lets the session boot; a truly headless box would need +`gnome-shell --headless --virtual-monitor`). Enable GDM autologin: + +```ini +# /etc/gdm3/custom.conf +[daemon] +AutomaticLoginEnable = true +AutomaticLogin = +``` + +### Disable the screen lock (important for a headless appliance) + +A **locked** GNOME session makes Mutter reject RemoteDesktop/ScreenCast with +`RemoteDesktop.CreateSession: ... Session creation inhibited` — so capture fails after the session +auto-locks on idle. There's no human to unlock a headless box, so disable it (in the user session): + +```sh +gsettings set org.gnome.desktop.screensaver lock-enabled false +gsettings set org.gnome.desktop.screensaver idle-activation-enabled false +gsettings set org.gnome.desktop.session idle-delay 0 +``` + +## 4. Host config + appliance unit + +`~/.config/punktfunk/host.env` for the Mutter backend: + +```sh +XDG_RUNTIME_DIR=/run/user/1000 +DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus +WAYLAND_DISPLAY=wayland-0 +XDG_CURRENT_DESKTOP=GNOME +PUNKTFUNK_COMPOSITOR=mutter +PUNKTFUNK_VIDEO_SOURCE=virtual +PUNKTFUNK_ZEROCOPY=1 +PUNKTFUNK_INPUT_BACKEND=libei +``` + +Run it as a persistent appliance — the standard user unit + linger (no kde-session unit needed here, +autologin provides the session): + +```sh +mkdir -p ~/.config/systemd/user +cp scripts/punktfunk-host.service ~/.config/systemd/user/ +systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host +sudo loginctl enable-linger "$USER" # start the user unit at boot without an interactive login +``` + +`serve --native` then listens on GameStream + native QUIC (9777), creates a per-client Mutter virtual +output at the client's exact mode, and streams it zero-copy. Confirm it's up: +`punktfunk-client-rs --discover` from another box should list it. diff --git a/docs-site/content/docs/headless-box.md b/docs-site/content/docs/headless-box.md new file mode 100644 index 00000000..5efcccb4 --- /dev/null +++ b/docs-site/content/docs/headless-box.md @@ -0,0 +1,62 @@ +--- +title: "Headless KDE Box Setup" +description: "Run punktfunk on a headless box with a nested KWin/Plasma session — the boot-appliance pattern." +--- + +How to run a punktfunk host on a **headless** box (no physical display / KMS scanout) using the +**KWin** backend: a nested headless Plasma session on `WAYLAND_DISPLAY=wayland-kde`, captured into a +per-client virtual output. This is the dev-box pattern (a QEMU VM with a passthrough NVIDIA GPU, no +KMS scanout → everything renders offscreen via `renderD128`). + +## Requirements + +- **KWin ≥ 6.5.6** (headless `--virtual` gained `createVirtualOutput`), or a DRM backend. On a box + with no KMS scanout, `kwin --drm` is impossible — use the headless/virtual path below. +- NVIDIA driver with GL/EGL userspace (see [Linux Host Setup](/docs/linux-setup) for the build deps). + +## Bring up the session + +The headless Plasma session is launched by [`scripts/headless/run-headless-kde.sh`](https://github.com), +which starts `kwin --virtual` on `wayland-kde` plus the full Plasma desktop (portals, polkit agent, a +supervised plasmashell). It sets the env Plasma needs — notably `XDG_MENU_PREFIX=plasma-`, without +which plasmashell runs but the launcher menu is empty: + +```sh +# shell 1 — the compositor session +bash scripts/headless/run-headless-kde.sh 1920x1080 + +# shell 2 — the host +WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE PUNKTFUNK_VIDEO_SOURCE=virtual \ + PUNKTFUNK_ZEROCOPY=1 cargo run -rp punktfunk-host -- serve --native +``` + +## Boot appliance (no login, comes up at boot) + +Two user systemd units bring the whole thing up at boot with no interaction: + +```sh +cp scripts/punktfunk-kde-session.service scripts/punktfunk-host.service ~/.config/systemd/user/ +cp scripts/host.env.example ~/.config/punktfunk/host.env # edit for the kwin backend +systemctl --user daemon-reload +systemctl --user enable punktfunk-kde-session punktfunk-host +sudo loginctl enable-linger "$USER" # start user units at boot WITHOUT a login +reboot +``` + +`punktfunk-kde-session.service` runs the headless KWin/Plasma session; `punktfunk-host.service` +(`serve --native`) `After=`s it and starts listening immediately (it only touches the compositor +per session, so the ordering is soft). `host.env` for this backend: + +```sh +WAYLAND_DISPLAY=wayland-kde +XDG_CURRENT_DESKTOP=KDE +PUNKTFUNK_COMPOSITOR=kwin +PUNKTFUNK_VIDEO_SOURCE=virtual +PUNKTFUNK_ZEROCOPY=1 +``` + +## Other backends + +The same box can stream a **nested app** (no desktop) via the gamescope backend, or attach to GNOME +([GNOME Box Setup](/docs/gnome-box)) or Sway/wlroots. Each compositor keeps its own +`VirtualDisplay` backend — there's no cross-compositor protocol for client-sized outputs. diff --git a/docs-site/content/docs/index.mdx b/docs-site/content/docs/index.mdx index 19e0a3cb..f4efe005 100644 --- a/docs-site/content/docs/index.mdx +++ b/docs-site/content/docs/index.mdx @@ -14,8 +14,8 @@ AES-GCM). ## Start here - + - + diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 3232070c..a8245d42 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -3,11 +3,15 @@ "pages": [ "index", "overview", + "status", "implementation-plan", "roadmap", "m2-plan", "---Setup---", "linux-setup", + "headless-box", + "gnome-box", + "bazzite", "windows-host", "dualsense-haptics" ] diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index 80621eb7..42c1bb09 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -299,15 +299,24 @@ buffer; `sendmmsg`/`recvmmsg` batching; the capture-timestamp anchor placement. `sync_channel(3)` with backpressure. Removes the serialization (~2–8 ms @60–120 fps) and is the substrate the slice wrapper needs. Real-NIC soak (host on the Ubuntu/GNOME box, client over the LAN): `send_dropped=0` at 720p60 / 1080p120, and a 1 Gbps probe pushed 625 MB in 5 s clean. +- **Done & live (skew handshake landed 2026-06-12):** **wall-clock skew handshake** — `ClockProbe`/ + `ClockEcho` on the control stream (8 NTP-style rounds right after `Start`; min-RTT sample → + host−client offset; `clock_offset_ns`). The client adds the offset to its receive instant before + differencing against the AU `pts_ns`, so the `capture→reassembled` percentiles are now valid + **across machines** (reported `skew_corrected=true`), not just same-host. Back-compat: an old host + that doesn't answer times out → `skew_corrected=false` (shared-clock assumption, as before). + Validated cross-LAN (GNOME box → dev box): offset ≈ −1.57 ms (reproducible), rtt ~140 µs, **p50 + 1.30 ms** skew-corrected capture→reassembled. **Remaining for true glass-to-glass**: the **client + present-stamp** (decode→present term) — only the Apple client presents today, so it needs the + connector to expose the offset + an Apple present-time probe; and the **render→capture** term + (PipeWire buffer presentation timestamp vs our capture stamp). `tools/latency-probe` is still the + cross-machine orchestrator. - **Bigger bets (ordered, deferred — need real-NIC/GPU/Mac validation):** - 1. **Wall-clock skew handshake + glass-to-glass probe** (`tools/latency-probe`) — measures the two - biggest unmeasured terms (render→capture, decode→present); client present-stamp vs the AU's - `pts_ns` (already attached). - 2. **CUDA stream+event** to drop one of two redundant `cuCtxSynchronize` in `submit_cuda` (keep the + 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. - 3. **Stage-2 Apple presenter** (`VTDecompressionSession` → `CAMetalLayer`, hand-paced) — ~0.5 refresh + 2. **Stage-2 Apple presenter** (`VTDecompressionSession` → `CAMetalLayer`, hand-paced) — ~0.5 refresh off the present tail (biggest client win at 60 Hz); gate on the probe proving present is real. - 4. **NVENC slice-mode wrapper** (roadmap §2 sub-frame pipelining) — per-slice transmit overlaps + 3. **NVENC slice-mode wrapper** (roadmap §2 sub-frame pipelining) — per-slice transmit overlaps encode+send within a frame (~3–6 ms at 4K/5K/IDR); large + driver-ABI-fragile, on top of the thread split, only after measurement justifies it. diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md new file mode 100644 index 00000000..bee2ada0 --- /dev/null +++ b/docs-site/content/docs/status.md @@ -0,0 +1,54 @@ +--- +title: "Status & Progress" +description: "Where the work stands, what's live on each box, and a running progress log." +--- + +The living progress tracker. Milestone-level status lives in [`CLAUDE.md`](https://github.com) +and the design in the [Implementation Plan](/docs/implementation-plan); this page is the +**current state + a dated log** of what landed, kept up to date as work happens. Newest first. + +## Milestones at a glance + +| Milestone | State | +|---|---| +| **M1** — `punktfunk-core` + C ABI (protocol · FEC · crypto) | ✅ complete & hardened | +| **M2** — GameStream host (Moonlight-compatible) | ✅ working end-to-end; HDR/surround-audio polish open | +| **M3** — `punktfunk/1` native protocol (QUIC control + UDP data) | ✅ full session planes, validated live | +| **M4** — native client decode + present (Apple first) | 🟡 stage 1 done (first light); stage-2 presenter next | + +## Live on the boxes + +| Box | Role | Compositor | Notes | +|---|---|---|---| +| **home-worker-2** (dev) | KDE/KWin appliance | kwin (headless Plasma) | QEMU VM, passthrough RTX 5070 Ti; `serve --native` user unit | +| **home-worker-3** (GNOME) | GNOME/Mutter appliance | mutter (RecordVirtual) | RTX 4090; autologin GNOME Wayland; `serve --native` user unit. See [GNOME Box Setup](/docs/gnome-box) | +| **home-bazzite-1** | SteamOS-like host | gamescope | host-managed Steam session at client mode. See [Bazzite Setup](/docs/bazzite) | + +All three appliances advertise over mDNS (`_punktfunk._udp`) and require PIN pairing by default. + +## Progress log + +### 2026-06-12 +- **Wall-clock skew handshake** (`ClockProbe`/`ClockEcho`, 8 NTP rounds after `Start`) — makes the + client's capture→reassembled latency valid **cross-machine**. Validated GNOME box → dev box: + offset −1.57 ms removed, **p50 1.30 ms** skew-corrected. (`05bc9ab`) +- **Native LAN auto-discovery** — host advertises `_punktfunk._udp` (TXT: fingerprint, pairing, + proto); `punktfunk-client-rs --discover` lists hosts. Validated cross-LAN. (`4fff464`) +- **Third test box stood up** — home-worker-3 (Ubuntu 26.04, RTX 4090, GNOME 50): first GNOME/Mutter + zero-copy streaming on a real desktop; **1 Gbps probe clean** (625 MB/5 s, `send_dropped=0`). + Two physical-NVIDIA gotchas documented in [GNOME Box Setup](/docs/gnome-box). +- **Encode|send thread split** validated on real NIC (`send_dropped=0` at 720p60 / 1080p120). (`b295a5b`) + +### Earlier (see roadmap + git log) +- **1 Gbps data plane**: batched `sendmmsg`/`recvmmsg` + microburst-cap paced send thread. +- **Boot appliance**: headless KDE session + host systemd units (no login). +- **Speed test + settable bitrate**: negotiation + bandwidth probe (host side). +- **DualSense** UHID + haptics; gamepads live; mic uplink; AV1 + surround (unit/live-capture tested). + +## In flight / next + +See the [Roadmap](/docs/roadmap) for the ordered list. Near-term: +- **True glass-to-glass**: Apple client present-stamp (decode→present) + host render→capture term. +- **Apple stage-2 presenter** (`VTDecompressionSession` → `CAMetalLayer`). +- **Mandatory PIN pairing + delegated pairing approval**; concurrent sessions. +- **bazzite** kept up to date (currently offline; one rebuild behind).