docs(site): make docs-site the knowledge base — status tracker + setup guides
ci / rust (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 11:33:39 +00:00
parent 05bc9ab22c
commit e586961e0b
9 changed files with 288 additions and 346 deletions
+49
View File
@@ -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.
+99
View File
@@ -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 -> <password>.
# 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-<DRV>` — 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 = <your-user>
```
### 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.
+62
View File
@@ -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.
+2 -2
View File
@@ -14,8 +14,8 @@ AES-GCM).
## Start here
<Cards>
<Card title="Project Overview" href="/docs/overview" description="Status, architecture, and milestones at a glance." />
<Card title="Status & Progress" href="/docs/status" description="Where the work stands, what's live on each box, and a dated progress log." />
<Card title="Implementation Plan" href="/docs/implementation-plan" description="The full design: protocol core, milestones, and architecture." />
<Card title="Roadmap" href="/docs/roadmap" description="Decided next goals and the longer-term bets." />
<Card title="Linux Host Setup" href="/docs/linux-setup" description="Bring up the build environment on an NVIDIA-GPU Ubuntu VM." />
<Card title="Host Setup" href="/docs/linux-setup" description="Build env + bring-up: Linux, headless KDE, GNOME/Mutter, Bazzite." />
</Cards>
+4
View File
@@ -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"
]
+15 -6
View File
@@ -299,15 +299,24 @@ buffer; `sendmmsg`/`recvmmsg` batching; the capture-timestamp anchor placement.
`sync_channel(3)` with backpressure. Removes the serialization (~28 ms @60120 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 →
hostclient 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.10.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 (~36 ms at 4K/5K/IDR); large + driver-ABI-fragile, on top of the
thread split, only after measurement justifies it.
+54
View File
@@ -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).