docs(site): make docs-site the knowledge base — status tracker + setup guides
ci / rust (push) Has been cancelled
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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user