docs: user-facing docs revamp — structured product docs + per-platform setup
ci / web (push) Failing after 47s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
ci / docs-site (push) Failing after 37s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m19s

Replace the dev/agent-log pages with a proper user-facing doc set:

- Getting Started: Introduction (rewritten), How It Works, Quick Start.
- Host Setup: Requirements, then clean per-platform guides — Ubuntu GNOME,
  Ubuntu KDE, Fedora KDE (new), Bazzite (rewritten) — plus Running as a Service
  (desktop / headless GNOME / headless KDE).
- Connecting: Clients overview, Moonlight, Pairing & Trust.
- Configuration: host.env reference, Host CLI, Troubleshooting.
- The dev/design notes (architecture, roadmap, the deferred design specs, CI)
  move to a clearly-separated "Project & Internals" nav section.

Removes the superseded box-specific pages (gnome-box, headless-box, linux-setup,
overview). status.md (the internal progress tracker, with box IPs) is kept as a
file but dropped from the public nav. Site builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 14:01:19 +00:00
parent 015f2ee47b
commit 91d5874e94
22 changed files with 944 additions and 453 deletions
@@ -103,7 +103,7 @@ same-host-only, as today.
`VideoToolboxRoundTripTests` → assert a `CVPixelBuffer` of the right dimensions + the
decode callback fires). Present is display-bound — validate it **live** via the HUD number.
- Live: connect to a Linux host (`m3-host --source virtual` on the GNOME box; see
[GNOME Box Setup](/docs/gnome-box)), confirm `capture→present` is a few ms over `capture→client`
[Ubuntu — GNOME](/docs/ubuntu-gnome)), confirm `capture→present` is a few ms over `capture→client`
and that `decode→present` shrank vs. an `AVSampleBufferDisplayLayer` baseline.
- Compare against the headless reference number: `punktfunk-client-rs` reports skew-corrected
capture→reassembled (~1.3 ms p50 GNOME box → dev box); capture→present should be that **+ decode +
+42 -28
View File
@@ -1,49 +1,63 @@
---
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."
title: Bazzite — gamescope
description: Set up a punktfunk host on Bazzite, streaming a Steam/gamescope session at your client's mode.
---
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.
[Bazzite](https://bazzite.gg/) already ships everything a punktfunk host needs — the NVIDIA driver,
NVENC, PipeWire, and **gamescope**. So a Bazzite host is the most "appliance-like" setup: the host
launches its own gamescope session at the **client's** resolution and refresh, so your games run at
the mode of the device you're streaming to, not the TV the box is plugged into.
## Input permissions — use the ujust command
> This is ideal for a dedicated game-streaming box. For a general desktop, prefer
> [Ubuntu/Fedora KDE](/docs/ubuntu-kde) or [GNOME](/docs/ubuntu-gnome).
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:
## Install
The host installs from the punktfunk COPR repository (see `packaging/bazzite/` in the repo for the
exact COPR/RPM/bootc options). You can also build from source as on
[Fedora KDE](/docs/fedora-kde) — Bazzite is Fedora Atomic underneath, and its FFmpeg builds the host
fine.
## Allow controller input
Gamepad and DualSense input needs your user in the `input` group. On Bazzite, don't use
`usermod` — the base is immutable and the group is managed by a recipe. Use:
```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.
Then **log out and back in**. (A controller that's "detected but does nothing" is almost always this
permission, not a client problem.)
## Headless Steam session at the client's mode
## Configure
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`:
Point the host at the gamescope backend in `~/.config/punktfunk/host.env`:
```sh
PUNKTFUNK_COMPOSITOR=gamescope
PUNKTFUNK_GAMESCOPE_SESSION=steam # host owns the session at the client mode
PUNKTFUNK_GAMESCOPE_SESSION=steam # the host owns a Steam session at the client's mode
PUNKTFUNK_INPUT_BACKEND=gamescope
PUNKTFUNK_ZEROCOPY=1
```
## Gotchas
With this, when a client connects the host starts a `gamescope-session-plus` (Steam) session at the
client's exact resolution and refresh, and relaunches it if the client changes mode. There should be
**no physical gaming session already running** on the box.
- **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.
## Run as an always-on host
## FFmpeg
Bazzite hosts are typically headless. Enable the host service and linger so it starts at boot — see
[Running as a Service](/docs/running-as-a-service). Because the host launches its own gamescope
session per client, you don't need a separate desktop-session unit.
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.
## Good to know
- **gamescope 3.16.22 or newer is required.** Older versions can deadlock during capture. Bazzite's
current gamescope is fine; this only bites if you've pinned an old one.
- **The mouse cursor isn't included in the captured image** — a gamescope limitation for now.
- **HDR isn't supported yet** on the gamescope path — gamescope's capture output is 8-bit. SDR streams
normally.
Then [connect a client](/docs/clients) — Moonlight works great for couch gaming, and the Apple app for
Apple TV / iPad.
+51
View File
@@ -0,0 +1,51 @@
---
title: Clients
description: The ways to connect to a punktfunk host — the Apple app, Moonlight, or the Linux client.
---
A punktfunk host accepts two kinds of client. Pick whichever fits the device you're streaming *to*.
## Apple app (Mac, iPhone, iPad, Apple TV)
The native app for Apple devices speaks punktfunk's own [`punktfunk/1`](/docs/how-it-works#two-protocols)
protocol — the lowest-latency, most resilient path, with the full feature set:
- **Automatic host discovery** — hosts on your network appear under *On this network*; no IP typing.
- **PIN pairing** built in, and pinned reconnects after that.
- **Controllers**, including DualSense — rumble, adaptive triggers, lightbar, motion, and touchpad.
- A live **stats overlay** (resolution, fps, bitrate, latency) and a built-in **network speed test**
to pick a bitrate for your link.
Open the app, pick your host, [pair](/docs/pairing) once, and stream. It builds from the
`clients/apple` directory in the repo (Swift / VideoToolbox / Metal).
## Moonlight (anything else)
punktfunk also speaks the **GameStream** protocol, so any [Moonlight](https://moonlight-stream.org/)
client — Windows, Android, Steam Deck, a browser, an old phone — connects with no punktfunk-specific
software. See [Connect with Moonlight](/docs/moonlight).
This is the broadest-compatibility option and great for couch gaming. It doesn't use the native
protocol's FEC/encryption extensions, but for a healthy LAN that rarely matters.
## Linux reference client
`punktfunk-client-rs` (in the repo) is a command-line client for the native protocol, mainly for
testing and development. It connects, streams to a file, runs the speed test, and can discover hosts:
```sh
punktfunk-client-rs --discover # list hosts on the network
punktfunk-client-rs --connect <host>:9777 --pin <fp> # connect to one
```
A full graphical Linux client (hardware decode + present) is on the [roadmap](/docs/roadmap).
## Which should I use?
| You're streaming to… | Use |
|---|---|
| A Mac, iPhone, iPad, or Apple TV | The **Apple app** |
| Windows, Android, Steam Deck, a browser, a TV | **Moonlight** |
| Another Linux box (testing) | **`punktfunk-client-rs`** |
Whichever you choose, the first connection needs a one-time [pairing](/docs/pairing).
+62
View File
@@ -0,0 +1,62 @@
---
title: Configuration
description: The host.env settings — compositor, resolution, bitrate, input — and how to tune them.
---
The host reads its settings from **`~/.config/punktfunk/host.env`** (a simple `KEY=value` file). Your
[setup guide](/docs/requirements) gives you a starting `host.env` for your desktop; this page is the
reference.
## Session settings
These tell the host which desktop session to attach to. Your setup guide sets them for you.
| Setting | What it does |
|---|---|
| `WAYLAND_DISPLAY` | The Wayland socket of your session (`wayland-0` for a normal desktop). |
| `XDG_CURRENT_DESKTOP` | Your desktop (`GNOME`, `KDE`). |
| `XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS` | Needed when the host runs outside your interactive session (e.g. as a service). |
## Core settings
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_COMPOSITOR` | `mutter` · `kwin` · `gamescope` · `wlroots` | Which backend creates the virtual display. Match your desktop. |
| `PUNKTFUNK_VIDEO_SOURCE` | `virtual` · `portal` | `virtual` creates a per-client display at its exact mode (the normal choice). `portal` captures an existing monitor instead. |
| `PUNKTFUNK_ZEROCOPY` | `1` · `0` | GPU zero-copy capture→encode. Leave on; it falls back to a CPU path automatically. |
| `PUNKTFUNK_INPUT_BACKEND` | `libei` · `gamescope` · `wlr` · `uinput` | How input is injected. `libei` for GNOME/KDE, `gamescope` for Bazzite. |
## Resolution and refresh rate
You don't set these on the host — **the client chooses them**. When a device connects, the host
creates a virtual display at that device's resolution and refresh rate. A 1080p60 laptop and a
1440p120 desktop each get their own. (With Moonlight, set the mode in Moonlight's settings; with the
Apple app, it uses the device's display.)
## Bitrate
The client requests a bitrate; the host encodes to it. To find a good value for your link:
- **Apple app:** use the built-in **speed test** (a host card's menu → *Test Network Speed*). It
measures your link and suggests a bitrate, then applies it.
- **Moonlight:** set the bitrate in Moonlight's settings. Start moderate and raise it.
## Multiple devices at once
A host can stream to several clients simultaneously — each gets its own virtual display at its own
resolution. This is the natural way to put your desktop on a laptop *and* a TV at the same time (both
see and control the same desktop).
The number of simultaneous streams is bounded by your GPU's encoder. Cap it with
`--max-concurrent N` on the host command line (default 4); extra clients wait until a slot frees.
## Codec and FEC
- The host encodes **HEVC (H.265)** by default; **AV1** is available for clients that support it.
- The native protocol adds forward error correction for lossy links. `PUNKTFUNK_FEC_PCT=N` sets the
redundancy percentage (the default is sensible for a normal LAN).
## Diagnostics
- `PUNKTFUNK_PERF=1` logs per-stage timing (capture, encode, send) — handy when tuning latency.
- `RUST_LOG=info` (or `debug`) controls log verbosity.
+80
View File
@@ -0,0 +1,80 @@
---
title: Fedora — KDE Plasma
description: Set up a punktfunk host on Fedora with KDE Plasma (KWin).
---
Set up a punktfunk host on **Fedora KDE** (the KDE Plasma spin). Like the Ubuntu KDE setup, the host
uses KWin to create per-client virtual displays — the difference is the package manager and the NVIDIA
driver source.
> Fedora KDE is the newest supported setup. The flow mirrors [Ubuntu — KDE](/docs/ubuntu-kde); this
> page covers the Fedora-specific bits.
## 1. NVIDIA driver
The cleanest source on Fedora is **RPM Fusion**:
```sh
sudo dnf install \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda
```
Let the `akmod` build finish (a few minutes), then reboot. Verify:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y (RPM Fusion enables it by default)
```
> With **Secure Boot** enabled, RPM Fusion's `akmods` need their key enrolled — follow the
> [RPM Fusion Secure Boot guide](https://rpmfusion.org/Howto/Secure%20Boot), or disable Secure Boot.
## 2. Dependencies
```sh
sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git \
pipewire pipewire-pulseaudio wireplumber pipewire-devel \
wayland-devel wayland-protocols-devel libxkbcommon-devel opus-devel \
libdrm-devel mesa-libgbm-devel mesa-libEGL-devel mesa-libGLES-devel libva-devel \
ffmpeg-free-devel libei-devel
```
> Fedora ships **FFmpeg** through RPM Fusion (`ffmpeg` + `ffmpeg-devel`) or the `-free` packages
> shown above. Either works; the host builds against the system FFmpeg.
Install Rust:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
## 3. Build
```sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release -p punktfunk-host
```
## 4. Configure and run
Same as Ubuntu KDE — write `~/.config/punktfunk/host.env` for KWin and run `serve --native`:
```sh
mkdir -p ~/.config/punktfunk
cat > ~/.config/punktfunk/host.env <<'ENV'
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=KDE
PUNKTFUNK_COMPOSITOR=kwin
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
ENV
cargo run --release -p punktfunk-host -- serve --native
```
Make sure you're on a **KDE Wayland** session with **KWin ≥ 6.5.6**. Then
[connect a client](/docs/clients). For boot-time startup, see
[Running as a Service](/docs/running-as-a-service).
-99
View File
@@ -1,99 +0,0 @@
---
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
@@ -1,62 +0,0 @@
---
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.
+54
View File
@@ -0,0 +1,54 @@
---
title: Host CLI
description: The punktfunk-host commands and the flags you'll actually use.
---
The host is one binary, `punktfunk-host`. Most of the time you'll run a single command; the rest reads
its settings from [`host.env`](/docs/configuration).
## `serve --native`
The normal way to run a host. Starts the unified host: the GameStream server (for Moonlight) **and**
the native `punktfunk/1` server, plus the management API/web console — all in one process.
```sh
punktfunk-host serve --native
```
| Flag | Meaning |
|---|---|
| `--native` | Also run the native `punktfunk/1` server (recommended; enables the Apple app and discovery). |
| `--native-port <PORT>` | Native QUIC port (default `9777`). |
| `--open` | Don't require pairing — serve any device on the network. Off by default; only for trusted single-user setups. |
| `--mgmt-bind <IP:PORT>` | Management API address (default loopback `127.0.0.1:47990`). |
| `--mgmt-token <TOKEN>` | Bearer token for the management API; required when `--mgmt-bind` isn't loopback. |
By default the host **requires pairing** — see [Pairing & Trust](/docs/pairing). Arm pairing from the
web console (or the `m3-host` flags below for a quick test).
## `m3-host`
A standalone native-only host, mainly for testing the `punktfunk/1` path without the GameStream server
or web console.
```sh
punktfunk-host m3-host --source virtual
```
| Flag | Meaning |
|---|---|
| `--port <N>` | QUIC listen port (default `9777`). |
| `--source virtual` | Use a real virtual display + NVENC (vs. `synthetic` test frames). |
| `--max-concurrent <N>` | Stream at most N sessions at once (default 4); overflow waits in the queue. |
| `--max-sessions <N>` | Exit after N sessions (0 = serve forever). |
| `--allow-pairing` | Accept PIN pairing; the host prints a PIN when a client pairs. |
| `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
Both `serve --native` and `m3-host` advertise the host on the network so clients can discover it. List
hosts from another machine with `punktfunk-client-rs --discover`.
## Environment
Most behaviour (compositor, video source, input backend, zero-copy) is set in
[`host.env`](/docs/configuration), not on the command line. When running as a
[service](/docs/running-as-a-service), the unit loads `host.env` for you.
+61
View File
@@ -0,0 +1,61 @@
---
title: How It Works
description: The ideas behind punktfunk — per-client virtual displays, the two protocols, and trust.
---
You don't need to know any of this to use punktfunk, but it helps to understand what's happening
when you connect.
## A virtual display, sized to your device
When a client connects, the host asks your desktop to create a **new virtual display** at exactly the
client's resolution and refresh rate, captures that display, and streams it. The virtual display is
real to your desktop — apps can be moved onto it, games open on it — but it isn't tied to any physical
monitor. When the client disconnects, the virtual display goes away.
That's why a 1080p60 laptop and a 1440p120 desktop can stream from the same host **at the same time**,
each at its own mode — they each get their own virtual display.
How the virtual display is created depends on your desktop:
| Desktop | How |
|---|---|
| **GNOME** (Mutter) | A virtual monitor via the screen-cast API |
| **KDE Plasma** (KWin) | A virtual output via KWin's screencast |
| **Bazzite / Steam** (gamescope) | A nested gamescope session launched at the client's mode |
| **Sway** (wlroots) | A headless output added to the running session |
## From screen to GPU to wire
Captured frames never touch the CPU on their way to the encoder. They go straight from the
compositor to the GPU's NVENC hardware encoder (HEVC/H.264/AV1) and out to the network — a **zero-copy
GPU path** that keeps latency low even at high resolutions and frame rates.
## Two protocols
punktfunk speaks two protocols over the same host:
- **GameStream** — the protocol Moonlight uses. Any [Moonlight](/docs/moonlight) client connects with
no special software. This is the most compatible way in.
- **punktfunk/1 (native)** — a purpose-built protocol with a QUIC control channel and a UDP data
channel hardened with forward error correction and encryption. It's lower-latency and more resilient
on imperfect networks, and it's what the [Apple app](/docs/clients) uses.
Both run from a single host process, so you don't choose up front — Moonlight clients use GameStream,
the native clients use punktfunk/1.
## Pairing and trust
The first time a device connects, you pair it: the host shows a short **PIN**, you type it into the
client, and the two remember each other. After that the device reconnects automatically on a pinned
cryptographic identity — no PIN, no account, no cloud. See [Pairing & Trust](/docs/pairing).
## Finding hosts
Hosts advertise themselves on your local network, so clients can **discover** them automatically
instead of needing an IP address. The Apple app and Moonlight both list hosts they find on the LAN.
## Multiple devices at once
A host can stream to several clients simultaneously — your laptop and your TV both viewing (and
controlling) the desktop, each at its own resolution. See [Multiple devices](/docs/configuration#multiple-devices).
+32 -11
View File
@@ -1,21 +1,42 @@
---
title: Introduction
description: Low-latency desktop and game streaming, Linux-first.
description: Low-latency desktop and game streaming from a Linux host to any of your devices.
---
import { Cards, Card } from 'fumadocs-ui/components/card'
**punktfunk** is a ground-up low-latency desktop and game streaming stack, built Linux-first,
with a shared Rust protocol core (`punktfunk-core`) exposed over a C ABI and native clients per
platform. It speaks two protocols: GameStream (so a stock Moonlight client just works) and the
native `punktfunk/1` (QUIC control plane + a hardened UDP data plane with GF(2¹⁶) Leopard FEC and
AES-GCM).
**punktfunk** streams your Linux desktop or games to your other devices — a laptop, a Mac, a tablet,
a TV — at low latency and at **each device's own resolution and refresh rate**. Run the host on a
Linux machine with an NVIDIA GPU, connect a client, and you're streaming.
## Start here
It's built for the things that make streaming feel native:
- **Your device's exact mode.** The host spins up a virtual display sized to the client that's
connecting — 1080p60 to your laptop, 1440p120 to your desktop, 4K to your TV — at the same time.
No letterboxing, no scaling, no juggling your real monitors.
- **Low latency, GPU end to end.** Frames go straight from the compositor to the GPU encoder
(NVENC) with zero CPU copies, and over a transport tuned for responsiveness rather than throughput.
- **Works with the apps you already have.** punktfunk speaks the GameStream protocol, so any
**Moonlight** client connects out of the box — and a faster **native protocol** with a dedicated
app for Apple devices.
- **Secure by default.** Hosts require a one-time PIN pairing; after that, devices reconnect on a
pinned identity. No accounts, no cloud.
## Pick your path
<Cards>
<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="Host Setup" href="/docs/linux-setup" description="Build env + bring-up: Linux, headless KDE, GNOME/Mutter, Bazzite." />
<Card title="How It Works" href="/docs/how-it-works" description="The ideas behind punktfunk in a few minutes — virtual displays, the two protocols, pairing." />
<Card title="Quick Start" href="/docs/quickstart" description="From nothing to streaming: set up a host and connect your first client." />
<Card title="Host Setup" href="/docs/requirements" description="Install the host on Ubuntu (GNOME or KDE), Fedora (KDE), or Bazzite." />
<Card title="Connect a Client" href="/docs/clients" description="Stream with the Apple app, Moonlight, or the Linux client." />
</Cards>
## What you need
- A **Linux host** with an **NVIDIA GPU** (for the NVENC hardware encoder) running one of the
[supported setups](/docs/requirements): **Ubuntu** (GNOME or KDE), **Fedora** (KDE), or **Bazzite**.
- A **client device** to stream to — a Mac/iPhone/iPad/Apple TV (native app), or anything that runs
**Moonlight**.
- Both on the **same network** (LAN or VPN). punktfunk is designed for a trusted local network.
Ready? Head to the [Quick Start](/docs/quickstart).
-163
View File
@@ -1,163 +0,0 @@
---
title: "Linux Host Setup"
description: "Bring up the build environment on an NVIDIA-GPU Ubuntu VM."
---
How to bring up the build environment for the punktfunk Linux host on an NVIDIA-GPU Ubuntu VM
and run the **M0** capture→encode spike. `punktfunk-core` already builds and is tested
cross-platform; this is about the platform backends in `crates/punktfunk-host`.
> Target **Ubuntu 24.04 (noble)**: Sway 1.9, FFmpeg 6.1.1, xdg-desktop-portal 1.18.
> 22.04 (jammy) ships Sway 1.7 / FFmpeg 4.4 — too old for this path; build from source or
> upgrade. Package names/versions below were verified against the live Ubuntu archive.
## 1. Bootstrap
```sh
git clone git@git.unom.io:unom/punktfunk.git && cd punktfunk && git checkout m1-punktfunk-core
bash scripts/bootstrap-ubuntu.sh
```
It **verifies** the (already-installed) NVIDIA + NVENC stack, installs the Rust toolchain
(rustup) and the build/runtime deps (PipeWire, xdg-desktop-portal + the wlroots backend,
Sway, Wayland/DRM/EGL/GBM/VA dev libs, capture tools), **gates** the FFmpeg `-dev`
headers so it can't clobber your custom NVENC FFmpeg, and drops headless-Sway + portal
config templates into `~/.config` (only if absent). It does **not** reboot or edit GRUB.
After it runs, sanity-check the core on Linux:
```sh
cargo test --workspace # 21 tests; same suite that's green on macOS
```
## 2. NVIDIA prerequisites (one-time, may need a reboot)
Wayland on NVIDIA requires KMS modeset. The bootstrap checks it; if it isn't `Y`:
```sh
echo 'options nvidia-drm modeset=1 fbdev=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
cat /sys/module/nvidia_drm/parameters/modeset # must print Y after reboot
```
- Driver **≥ 535** is the floor for headless wlroots (EGL/dmabuf); 550+ recommended.
- **Install the NVIDIA GL/EGL userspace, not just `nvidia-utils`:**
`sudo apt install libnvidia-gl-<NNN>` (matching the driver, e.g. `libnvidia-gl-595`).
`nvidia-utils-NNN` ships nvidia-smi + NVENC but **not** `libEGL_nvidia.so.0` or the GLVND
vendor JSON (`/usr/share/glvnd/egl_vendor.d/10_nvidia.json`). Without them libglvnd falls
back to Mesa, wlroots can't init EGL on the GPU and drops to the **pixman** software
renderer — and the ScreenCast portal then fails to negotiate a buffer format
(`unable to receive a valid format from wlr_screencopy`). Verify after install:
`ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json && ldconfig -p | grep libEGL_nvidia`.
A correct GPU Sway logs `EGL vendor: NVIDIA` and a list of DMA-BUF formats.
- **Join the `render` + `video` groups:** `sudo usermod -aG render,video $USER`, then
**re-login** (group changes only apply to new logins). wlroots opens
`/dev/dri/renderD128` (group `render`) and `/dev/dri/card*` (group `video`), both 0660;
without membership Sway aborts with `Permission denied`. (`scripts/headless/*.sh` bridge a
not-yet-re-logged-in shell with `sg render`, but re-login is the clean fix.)
- A **headless VM GPU exposes no DRM connectors** — that's expected. We don't use the DRM
backend; `WLR_BACKENDS=headless` renders to an offscreen GBM/EGL surface and creates a
virtual `HEADLESS-1` output. Use the render node `/dev/dri/renderD128`.
- **NVENC in a VM:** full PCI **passthrough** = bare-metal NVENC, no license. **vGPU**
needs a valid license (vWS) or NVENC runs degraded — the bootstrap's smoke-encode tells
you if it actually works. Consumer GeForce cards also cap concurrent NVENC sessions
(~8); datacenter/RTX-pro are effectively unlimited — relevant once we serve many clients.
## 3. Bring up the headless compositor + prove capture→NVENC
```sh
# shell 1 — start headless GPU Sway on the shared user bus (blocks; -d for debug log)
bash scripts/headless/run-headless-sway.sh # success logs "EGL vendor: NVIDIA"
# shell 2 — same user: set the client mode, import the portal env, write the env file
bash scripts/headless/prepare-session.sh 2560x1440@60Hz
source /tmp/punktfunk-sway-env.sh
swaymsg -t get_outputs # confirm HEADLESS-1 active
swaymsg exec foot # optional: animated content to capture
bash scripts/headless/capture-smoke-test.sh # wf-recorder (wlr-screencopy) -> hevc_nvenc
ffprobe /tmp/punktfunk-headless-test.mkv # confirm a real H.265 stream
```
`wf-recorder` uses `wlr-screencopy` directly (no portal/D-Bus) — the fastest way to
de-risk the GPU encode path. **Note:** screencopy encodes straight to a file and *cannot*
feed PipeWire; the real integration uses the ScreenCast portal (see M0). If shell 1 logged
a Mesa/EGL fallback (or Sway dropped to pixman) instead of `EGL vendor: NVIDIA`, install the
NVIDIA GL userspace (§2) — the portal cannot capture a pixman output.
**An idle headless output produces no frames** (its frame clock is driven by damage); give
it a real refresh mode (`prepare-session.sh` does) *and* run something animated
(`swaymsg exec foot`) or the capture will be ~1 frame.
The wlroots-on-NVIDIA env workarounds (`WLR_RENDERER=gles2`, `WLR_NO_HARDWARE_CURSORS=1`,
`GBM_BACKEND=nvidia-drm`, `sway --unsupported-gpu`, …) live in
`scripts/headless/env.sh``source` it before launching anything Wayland.
## 4. M0 proper — wire it into `punktfunk-core`
Goal (plan §8): headless output → PipeWire ScreenCast → NVENC → a playable file, then feed
the encoded access units into a `punktfunk_core::Session` (host role). The module seams exist
in `crates/punktfunk-host/src/{vdisplay,capture,encode,inject,pipeline}.rs`.
**Status: implemented and verified end-to-end** in `crates/punktfunk-host` (`m0.rs`,
`capture/linux.rs`, `encode/linux.rs`). After the §3 bring-up:
```sh
source /tmp/punktfunk-sway-env.sh
swaymsg exec foot # animated content
# Live portal capture → NVENC HEVC → playable file, with each AU also round-tripped
# through a punktfunk_core host→client Session (FEC + packetize + reassemble) and verified:
cargo run -p punktfunk-host -- m0 --source portal --seconds 5 --out /tmp/punktfunk-m0.h265
ffprobe /tmp/punktfunk-m0.h265
# No capture session needed (encode + core only): --source synthetic
```
Verified result: `1920x1080` HEVC, ~300 frames in 5s, `punktfunk-core loopback … 0 mismatches`.
The portal negotiates packed **`RGB` (24-bit, 3 bpp)** on wlroots; the encoder expands it to
`rgb0` (one pad byte/pixel, no colour math) since NVENC accepts `rgb0`/`bgr0` but not
`rgb24`. dmabuf zero-copy import is still deferred (plan §9) — this is the CPU-copy path.
Crate choices, verified current:
- **Capture (portal path):** [`ashpd`](https://docs.rs/ashpd) **0.13** with the
`screencast` feature (the `pipewire` feature is *not* needed — `open_pipe_wire_remote`
is unconditional). Flow (0.13 API, verified against the vendored source): `Screencast::new`
`create_session(Default)``select_sources(&session, SelectSourcesOptions::default()
.set_sources(BitFlags::from_flag(SourceType::Monitor))…)``start(&session, None,
Default)``.response()?``Stream::pipe_wire_node_id()` + `open_pipe_wire_remote()`.
Note 0.13 takes **options structs**, not the old positional args, and defaults to the
**tokio** runtime — drive the handshake on a *multi-thread* tokio runtime (a
current-thread one starves zbus's reader and the portal reports "Invalid session").
Pull frames with [`pipewire`](https://docs.rs/pipewire) **0.9** — it must match the
pipewire crate ashpd 0.13 links (the `pipewire-sys` `links` key is unique per build, so
`0.10` fails to resolve). 0.9 uses `MainLoopRc`/`ContextRc::connect_fd_rc(OwnedFd)`/
`StreamBox`. Only request `SourceType::Monitor` — the wlr backend's
`AvailableSourceTypes` is `1` (Monitor only); asking for `Window`/`Virtual` invalidates
the session. Set `XDG_CURRENT_DESKTOP=sway` so the wlr portal backend is chosen, and
import it into the portal's environment (see "Portal bring-up" below).
- **Encode:** [`ffmpeg-next`](https://crates.io/crates/ffmpeg-next) **8.x** (binds the
system FFmpeg 8.x via pkg-config; needs `clang`/`libclang`). Select the encoder by
name — `encoder::find_by_name("hevc_nvenc")`, *not* by codec id (that's the SW encoder).
Low-latency opts: `preset=p1`, `tune=ull`, `rc=cbr`, `bf=0`, `delay=0`, large `g`.
If your FFmpeg is in a non-standard prefix, `export FFMPEG_DIR=/that/prefix`.
- **Zero-copy is the hard part.** There's no direct dmabuf→CUDA import in FFmpeg.
**Start with the CPU-copy fallback** (download frame → `hwupload_cuda``hevc_nvenc`)
to get an end-to-end stream, then chase true dmabuf zero-copy. The plan flags this
(§9) and the `capture` module already has a `cpu_bytes` fallback field.
- **Input (M2):** [`reis`](https://crates.io/crates/reis) (pure-Rust libei — no native
`libei` needed) with `input-linux`/uinput as the universal fallback.
Then continue toward **M2**: `serverinfo`/RTSP/pairing enough for a stock Moonlight client
to connect, a KWin virtual output created on connect, input via reis/uinput — the
shippable milestone.
## Troubleshooting
| Symptom | Fix |
|---|---|
| Sway aborts on NVIDIA | add `--unsupported-gpu` (the helper scripts do) |
| `not a KMS device` / no connectors | expected on a headless VM GPU — use `WLR_BACKENDS=headless`, not the DRM backend |
| Sway won't start at all | `WLR_RENDERER_ALLOW_SOFTWARE=1 WLR_RENDERER=pixman` to prove the pipeline, then fix EGL |
| ScreenCast portal finds no output | ensure `xdg-desktop-portal-wlr` is running in the same session, `XDG_CURRENT_DESKTOP=sway`, and `~/.config/xdg-desktop-portal-wlr/config` has `output_name=HEADLESS-1` |
| `Cannot load libnvidia-encode.so.1` | NVENC runtime lib missing (driver) or unlicensed vGPU |
| `cargo build` can't find FFmpeg | `export FFMPEG_DIR=$(pkg-config --variable=prefix libavcodec)` or point `PKG_CONFIG_PATH` at the custom build |
| bindgen: libclang not found | `export LIBCLANG_PATH=$(llvm-config --libdir)` |
+20 -10
View File
@@ -2,19 +2,29 @@
"title": "Documentation",
"pages": [
"index",
"overview",
"status",
"implementation-plan",
"how-it-works",
"quickstart",
"---Host Setup---",
"requirements",
"ubuntu-gnome",
"ubuntu-kde",
"fedora-kde",
"bazzite",
"running-as-a-service",
"---Connecting---",
"clients",
"moonlight",
"pairing",
"---Configuration---",
"configuration",
"host-cli",
"troubleshooting",
"---Project & Internals---",
"roadmap",
"m2-plan",
"implementation-plan",
"windows-host",
"apple-stage2-presenter",
"gamescope-multiuser",
"---Setup---",
"linux-setup",
"headless-box",
"gnome-box",
"bazzite",
"windows-host",
"dualsense-haptics",
"ci"
]
+40
View File
@@ -0,0 +1,40 @@
---
title: Connect with Moonlight
description: Stream from a punktfunk host using any Moonlight client.
---
punktfunk speaks the **GameStream** protocol, so [Moonlight](https://moonlight-stream.org/) connects
to it like it would to any GameStream host — no punktfunk-specific app needed. This is the easiest way
to stream to Windows, Android, the Steam Deck, a browser, or a TV.
## 1. Make sure the host is running
On the host machine, `serve --native` (or your [service](/docs/running-as-a-service)) should be up.
The host advertises itself on the network, so Moonlight usually finds it on its own.
## 2. Add the host in Moonlight
Open Moonlight. Your host should appear automatically on the same network. If it doesn't, use **Add
Host manually** and enter the host machine's IP address.
## 3. Pair
Select the host and choose **Pair**. Moonlight shows a 4-digit PIN. On the host, you confirm pairing
(from the web console, or it accepts the ceremony when armed) — see [Pairing & Trust](/docs/pairing).
Once paired, Moonlight remembers the host.
## 4. Stream
Pick an app/desktop and start streaming. The host creates a virtual display at the resolution and
frame rate Moonlight requests (set these in Moonlight's settings), encodes it on the GPU, and streams
it. Mouse, keyboard, and controllers flow back to the host.
## Tips
- **Set your resolution and frame rate in Moonlight's settings** before connecting — the host matches
whatever Moonlight asks for, creating the virtual display at that exact mode.
- **Codec:** HEVC (H.265) is a good default; AV1 is available if your client supports it.
- **Bitrate:** start moderate and raise it. For very high bitrates, the native [Apple
app](/docs/clients) has a built-in speed test; with Moonlight, set the bitrate manually.
- Moonlight uses the GameStream protocol, not punktfunk's native FEC/encryption extensions. On a
solid LAN this is fine; on a lossy link the [Apple app](/docs/clients) holds up better.
-77
View File
@@ -1,77 +0,0 @@
---
title: "Project Overview"
description: "Status, architecture, and milestones at a glance."
---
*A ground-up low-latency desktop streaming stack, built Linux-first, with a shared Rust
protocol core and native clients per platform.*
`punktfunk` is a placeholder codename. The bet: ship a **Linux virtual-display streaming
host** that speaks the existing Moonlight protocol (every Moonlight/Artemis client works
day one), then break the ~1 Gbps FEC wall with a **GF(2¹⁶) Leopard-RS** transport as a
negotiated extension. See [`docs/implementation-plan.md`](docs/implementation-plan.md).
## Status
| Milestone | State |
|-----------|-------|
| **M1 — `punktfunk-core` + C ABI** | ✅ done & hardened (FEC, packetization, AES-GCM, session, adversarial-review fixes, `punktfunk_core.h`) |
| **M2 — GameStream host → stock Moonlight** | ✅ live end-to-end: pairing, RTSP, audio, per-client virtual output at native res, GPU zero-copy NVENC, gamepads |
| **M3 — `punktfunk/1` native protocol** | ✅ validated live: QUIC control + GF(2¹⁶) FEC/AES data plane, SPAKE2 PIN pairing, mid-stream mode renegotiation |
| **M4 — client decode + present (Apple)** | 🟡 macOS first light: AnnexB→VideoToolbox HEVC on glass + input/pairing over `punktfunk/1` (`clients/apple`); iOS + presenter next |
| **Web console + management API** | ✅ TanStack web console (`web/`) over the OpenAPI mgmt API: host status, paired devices, on-demand native pairing (arm → show PIN) |
The **GameStream host works with a stock Moonlight client** — validated live on NVIDIA
(RTX 5070 Ti & RTX 4090, driver 595): trust-on-first-use pairing that persists, an app
catalog, RTSP/ENet/audio, and **video at the client's exact resolution and refresh** via a
per-session virtual output (KWin, gamescope, Mutter, Sway backends), encoded with GPU
**zero-copy** (dmabuf → CUDA/Vulkan → NVENC) at up to 5120×1440@240. The native
**`punktfunk/1`** protocol adds a QUIC control plane and a GF(2¹⁶) Leopard-FEC + AES-GCM data
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).
## Layout
```
crates/
punktfunk-core/ protocol · FEC · pacing · crypto · quic — the C ABI (lib + cdylib + staticlib)
punktfunk-host/ Linux host: vdisplay · capture · encode · inject · gamestream · m3 · mgmt · native_pairing
punktfunk-client-rs/ punktfunk/1 reference client (M3 headless; M4 adds decode+present)
clients/{apple,android}/ native client scaffolds (import punktfunk_core.h); apple = macOS first light
web/ TanStack web console (host status · paired devices · pairing) over the mgmt API
packaging/ Fedora/Bazzite RPM · bootc image · COPR (see packaging/bazzite/README.md)
include/punktfunk_core.h cbindgen-generated C header (checked in)
tools/{latency-probe,loss-harness}/ measurement (plan §10)
docs/{implementation-plan,roadmap,windows-host,dualsense-haptics}.md
```
## Build & test
```sh
cargo build --workspace # green on Linux and macOS
cargo test --workspace # unit + loopback + proptest + C ABI harness
cargo clippy --workspace --all-targets
cargo run -p loss-harness # FEC loss-resilience sweep (no network needed)
bash crates/punktfunk-core/tests/c/run.sh # standalone C-ABI link+round-trip proof
```
The C header regenerates from `crates/punktfunk-core/src/abi.rs` on every build (cbindgen via
`build.rs`) into `include/punktfunk_core.h`.
## Design invariants
- **One core, linked everywhere.** Protocol/FEC/crypto/pacing live in `punktfunk-core` exactly
once, exposed over a stable, versioned C ABI (`punktfunk_abi_version()`, `PunktfunkConfig`
carries its own `struct_size`).
- **No async on the hot path.** The per-frame pipeline uses native threads only;
`tokio`/`quinn` are gated behind the off-by-default `quic` feature (control plane only).
- **FEC is the wall-breaker.** GF(2⁸) (≤255 shards/block) for Moonlight compat;
GF(2¹⁶) (≤65535 shards/block, SIMD, O(n log n)) to push past ~1 Gbps.
## License
MIT OR Apache-2.0.
+54
View File
@@ -0,0 +1,54 @@
---
title: Pairing & Trust
description: How a client and host establish trust — PIN pairing once, pinned reconnects after.
---
punktfunk has no accounts and no cloud. Trust is established directly between a client and a host, on
your network, with a one-time **PIN pairing**. After that, the device reconnects automatically on a
pinned cryptographic identity.
## How it works
- Each host has a stable **identity** (a certificate). Clients remember its fingerprint, so they know
they're talking to the same host next time.
- The first time a client connects, you **pair** it: the host shows a short **4-digit PIN**, you type
it into the client, and a secure exchange (SPAKE2) binds the two identities. An attacker who doesn't
know the PIN gets a single online guess — no offline cracking.
- After pairing, the host stores the client's identity in its allow-list, and the client stores the
host's fingerprint. Reconnects are automatic — no PIN.
## Arming pairing on the host
Pairing has to be **armed** on the host before a client can pair (so a random device can't pair
itself). Two ways:
- **Web console** *(recommended)* — open the host's management console, click to arm pairing, and it
shows the PIN and the list of paired devices. This is the easiest way and works on a headless host
over the network.
- **Command line** — start the host with `--allow-pairing` (or `--require-pairing`); it prints a PIN
in its log when a client begins pairing.
Then, on the client:
- **Apple app:** select the host (or use *Pair with PIN…* from its menu) and enter the PIN.
- **Moonlight:** choose **Pair**; Moonlight shows the PIN to confirm on the host side.
## Requiring pairing (the default)
By default, the native host **requires** pairing — only devices that have paired can stream. This is
the right setting on a shared network: a device has to complete the PIN ceremony once before it can
connect.
If you're on a fully trusted single-user network and want to skip pairing, the host can be run open —
but requiring pairing is strongly recommended.
## Trust-on-first-use
If a host *isn't* requiring pairing, a client connecting for the first time will show the host's
fingerprint and ask you to confirm it (trust-on-first-use), then pin it. Pairing is the stronger path
and the default; trust-on-first-use is a convenience for trusted setups.
## Managing paired devices
The web console lists every paired device and lets you remove one (revoking its access). Re-pairing is
just the PIN ceremony again.
+54
View File
@@ -0,0 +1,54 @@
---
title: Quick Start
description: From nothing to streaming — set up a host and connect your first client.
---
This is the shortest path to a working stream. Each step links to the details.
## 1. Set up the host
On your Linux + NVIDIA machine, follow the guide for your system:
- [Ubuntu — GNOME](/docs/ubuntu-gnome)
- [Ubuntu — KDE Plasma](/docs/ubuntu-kde)
- [Fedora — KDE Plasma](/docs/fedora-kde)
- [Bazzite — gamescope / Steam](/docs/bazzite)
Each one covers the NVIDIA driver, the dependencies, and how to build and run the host. Check the
[Requirements](/docs/requirements) first if you're not sure your machine is a fit.
## 2. Start the host
From a terminal **inside your desktop session** (so the host can reach your compositor):
```sh
punktfunk-host serve --native
```
The host starts listening and prints its identity fingerprint. It advertises itself on your local
network, so clients can find it by name. Leave it running. (To start it automatically at boot, see
[Running as a Service](/docs/running-as-a-service).)
## 3. Connect and pair a client
On the device you want to stream to:
- **Apple (Mac, iPhone, iPad, Apple TV):** open the punktfunk app — your host appears under *On this
network*. Tap it, and when prompted, **pair**.
- **Anything with Moonlight:** add the host (it should be discovered automatically), then pair.
To pair, the host needs to show a PIN. Arm pairing from the host's web console (or with
`--allow-pairing` on the command line) — the host displays a 4-digit PIN, you type it into the client,
and they trust each other from then on. Full details: [Pairing & Trust](/docs/pairing).
## 4. Stream
Once paired, select the host and start streaming. The host creates a virtual display at your device's
resolution and refresh, and the picture comes up. Mouse, keyboard, and controllers flow back to the
host.
## Next steps
- Tune [resolution, refresh, and bitrate](/docs/configuration).
- Run the host [as a background service](/docs/running-as-a-service) so it's always available.
- Hit a snag? See [Troubleshooting](/docs/troubleshooting).
+58
View File
@@ -0,0 +1,58 @@
---
title: Requirements
description: What you need to run a punktfunk host — GPU, driver, desktop, and network.
---
## Supported setups
A punktfunk host runs on a Linux machine with an NVIDIA GPU. These are the desktop environments it
supports today, each with its own guide:
| Setup | Desktop / compositor | Guide |
|---|---|---|
| **Ubuntu** (Desktop or Server) | GNOME (Mutter) | [Ubuntu — GNOME](/docs/ubuntu-gnome) |
| **Ubuntu** (Desktop or Server) | KDE Plasma (KWin) | [Ubuntu — KDE](/docs/ubuntu-kde) |
| **Fedora** | KDE Plasma (KWin) | [Fedora — KDE](/docs/fedora-kde) |
| **Bazzite** | gamescope (Steam) | [Bazzite](/docs/bazzite) |
Other wlroots compositors (Sway/Hyprland) also work but aren't a primary target. If your desktop isn't
listed, the host still needs one of these compositor backends to create a virtual display.
## GPU and driver
- **An NVIDIA GPU** with NVENC — effectively any GeForce RTX or workstation card. NVENC is what
encodes the video in hardware.
- **NVIDIA driver 535 or newer** (550+ recommended). The driver must include the **GL/EGL userspace**,
not just `nvidia-utils` — without it the compositor can't initialise the GPU and capture fails. Each
setup guide installs the right package (e.g. `libnvidia-gl-<version>` on Ubuntu).
- **`nvidia-drm modeset=1`** must be enabled (Wayland on NVIDIA needs it). The setup guides cover this.
> Consumer GeForce cards historically cap the number of **concurrent** NVENC sessions (a few at once);
> workstation cards don't. This only matters if you stream to many devices simultaneously.
## Desktop session
The host attaches to a **Wayland** desktop session and creates virtual displays in it, so a session
needs to be running for the user the host runs as. This can be:
- a **normal logged-in desktop** (you're sitting at the machine, or it auto-logs-in), or
- a **headless session** that comes up at boot with no monitor or login — see
[Running as a Service](/docs/running-as-a-service).
Minimum compositor versions (newer is fine):
- **KWin ≥ 6.5.6** (KDE Plasma) — headless virtual outputs.
- **GNOME ≥ 48** (Mutter) — virtual-monitor screen-cast.
- **gamescope ≥ 3.16.22** (Bazzite/Steam) — older versions deadlock during capture.
## Network
- Host and client on the **same network** — a LAN, or a VPN that puts them on one subnet. punktfunk
assumes a trusted local network; it's not built to be exposed to the public internet.
- For best results, a wired or fast Wi-Fi link. The host can run a built-in **speed test** to pick a
bitrate for your link (see [Configuration](/docs/configuration)).
## A client
You also need something to stream *to* — see [Connect a Client](/docs/clients). The Apple app and any
Moonlight client both work; both can discover the host on your network automatically.
@@ -0,0 +1,88 @@
---
title: Running as a Service
description: Start the host at boot — for a desktop you log into, or a fully headless always-on machine.
---
Running `serve --native` in a terminal is fine for trying punktfunk out. To make a machine an
always-available host, run it as a service. There are two cases.
## A. A desktop you log into
If you sit at the machine (or it auto-logs-in to a desktop), run the host as a **systemd user
service** that starts with your session:
```sh
mkdir -p ~/.config/systemd/user
cp scripts/punktfunk-host.service ~/.config/systemd/user/
# Put your host.env in place first — see the setup guide for your desktop.
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host
```
The host now starts whenever you log in. Check it with `systemctl --user status punktfunk-host`.
## B. A headless, always-on host
To run with **no monitor and no login** — a machine in a closet that's always ready — you need two
things: a desktop session that comes up at boot, and the host service started without a login.
Start by making the host service start at boot even when nobody logs in:
```sh
sudo loginctl enable-linger "$USER"
```
Then bring up a session automatically, depending on your desktop:
### Headless GNOME
Have GDM auto-login your user, so a GNOME Wayland session is always running:
```ini
# /etc/gdm3/custom.conf (Ubuntu) · /etc/gdm/custom.conf (Fedora)
[daemon]
AutomaticLoginEnable = true
AutomaticLogin = your-user
```
Then **disable the screen lock** — a locked GNOME session blocks screen capture, and there's no one to
unlock a headless box:
```sh
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
```
Enable the host user service (section A) and reboot. The host comes up on the auto-login session.
### Headless KDE
punktfunk ships a unit that brings up a headless KWin/Plasma session with no display manager, so the
host has a desktop to stream even with no monitor attached:
```sh
cp scripts/punktfunk-kde-session.service scripts/punktfunk-host.service ~/.config/systemd/user/
# host.env: PUNKTFUNK_COMPOSITOR=kwin, WAYLAND_DISPLAY=wayland-kde
systemctl --user daemon-reload
systemctl --user enable punktfunk-kde-session punktfunk-host
sudo loginctl enable-linger "$USER"
reboot
```
The session unit starts headless KWin; the host unit follows it and starts listening. (KWin only needs
to be up by the time a client connects, so the ordering is soft.)
### Headless Bazzite
On Bazzite, the host launches its own gamescope/Steam session per client, so you don't need a separate
session unit — see [Bazzite](/docs/bazzite).
## Verifying
After a reboot, from another machine on the network:
```sh
punktfunk-client-rs --discover # or just look for the host in the Apple app / Moonlight
```
If the host is listed, it's up. If not, check `journalctl --user -u punktfunk-host` on the host.
+2 -2
View File
@@ -21,7 +21,7 @@ and the design in the [Implementation Plan](/docs/implementation-plan); this pag
| 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-worker-3** (GNOME) | GNOME/Mutter appliance | mutter (RecordVirtual) | RTX 4090; autologin GNOME Wayland; `serve --native` user unit. See [Ubuntu — GNOME](/docs/ubuntu-gnome) |
| **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.
@@ -62,7 +62,7 @@ All three appliances advertise over mDNS (`_punktfunk._udp`) and require PIN pai
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).
Two physical-NVIDIA gotchas documented in [Ubuntu — GNOME](/docs/ubuntu-gnome).
- **Encode|send thread split** validated on real NIC (`send_dropped=0` at 720p60 / 1080p120). (`b295a5b`)
### Earlier (see roadmap + git log)
+74
View File
@@ -0,0 +1,74 @@
---
title: Troubleshooting
description: Common problems setting up or using a punktfunk host, and how to fix them.
---
## The host isn't found on the network
- Make sure the host is actually running (`systemctl --user status punktfunk-host`, or you see it
listening in the terminal).
- Host and client must be on the **same network/subnet**. Discovery uses mDNS, which doesn't cross
routed subnets or most VPNs-without-multicast. As a fallback, add the host by **IP address** in your
client.
- A firewall on the host can block it. The native protocol uses UDP port **9777** (plus the data
port); GameStream/Moonlight uses its standard ports. Allow them on the host's firewall.
## `nvidia-smi` says it can't communicate with the driver
- The NVIDIA kernel module didn't load. With **Secure Boot** enabled, enrol the module's signing key:
`sudo mokutil --import /var/lib/shim-signed/mok/MOK.der`, reboot, **Enrol MOK** at the blue screen
(or disable Secure Boot). On Fedora, follow RPM Fusion's Secure Boot steps.
- After a kernel update the module may need a rebuild — reinstall the driver package.
## The desktop won't start, or "GPU … not supported by EGL"
The NVIDIA **GL/EGL userspace** is missing — the base driver package doesn't always include it.
- **Ubuntu:** `sudo apt install libnvidia-gl-<version>` (matching your driver).
- Confirm `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists and `nvidia-drm modeset` is `Y`.
## Black screen / no picture, but the client connects
- You must be on a **Wayland** session, not X11 (check the login-screen session picker).
- KWin must be **≥ 6.5.6** (`kwin_wayland --version`); GNOME **≥ 48**; gamescope **≥ 3.16.22**.
- Confirm `PUNKTFUNK_COMPOSITOR` in [`host.env`](/docs/configuration) matches your desktop.
## Capture fails: "Session creation inhibited" (GNOME)
A **locked** GNOME session blocks screen capture. On an always-on/headless host, disable the lock:
```sh
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
```
See [Running as a Service](/docs/running-as-a-service).
## A controller is detected but does nothing (Bazzite)
The host user needs to be in the `input` group. On Bazzite:
```sh
ujust add-user-to-input-group
```
Then log out and back in. On other distros this is `sudo usermod -aG input $USER` + re-login.
## Pairing is rejected / the client can't connect
- The host **requires pairing** by default. Arm pairing (web console, or `--allow-pairing`), then
enter the PIN on the client. See [Pairing & Trust](/docs/pairing).
- If you re-installed the host, its identity changed — re-pair the client.
## Stutter, drops, or high latency
- Lower the **bitrate**. On a busy or Wi-Fi link, the requested bitrate may be too high — the Apple
app's [speed test](/docs/configuration#bitrate) picks a safe value; with Moonlight, set it manually.
- Prefer a **wired** connection or 5 GHz Wi-Fi between host and client.
- Streaming to **many devices at once** shares the GPU encoder; cap concurrency with
`--max-concurrent`.
## Still stuck?
Run the host with `RUST_LOG=info` (or `debug`) and check `journalctl --user -u punktfunk-host` for the
error around the failed connect or capture.
+113
View File
@@ -0,0 +1,113 @@
---
title: Ubuntu — GNOME
description: Set up a punktfunk host on Ubuntu with the GNOME desktop (Mutter).
---
Set up a punktfunk host on **Ubuntu** (Desktop or Server) running **GNOME**. The host uses GNOME's
Mutter compositor to create a per-client virtual display. Tested on Ubuntu 24.04+ and GNOME 48+.
> New to this? Skim [Requirements](/docs/requirements) first.
## 1. NVIDIA driver
Install the recommended NVIDIA driver:
```sh
sudo ubuntu-drivers install # or: sudo apt install nvidia-driver-<version>
```
Then make sure the **GL/EGL userspace** is present — GNOME on NVIDIA needs it, and the base driver
package doesn't always pull it in. Install the `libnvidia-gl` package matching your driver version:
```sh
sudo apt install libnvidia-gl-<version> # e.g. libnvidia-gl-550
```
Reboot, then confirm the driver and KMS modeset:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** on a machine with Secure Boot **enabled**, the NVIDIA kernel module won't load
> until you enrol its signing key. If `nvidia-smi` reports it can't talk to the driver, run
> `sudo mokutil --import /var/lib/shim-signed/mok/MOK.der` (set a one-time password), reboot, and
> choose **Enrol MOK** at the blue screen. Or disable Secure Boot in firmware.
## 2. Dependencies
Install the build toolchain and runtime libraries:
```sh
sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \
pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \
libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \
libdrm-dev libgbm-dev libegl-dev libgles-dev mesa-common-dev libva-dev \
ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev
```
Install Rust if you don't have it:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
## 3. Build
```sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release -p punktfunk-host
```
The host binary is at `target/release/punktfunk-host`.
## 4. Configure
The host reads its settings from `~/.config/punktfunk/host.env`. For GNOME:
```sh
mkdir -p ~/.config/punktfunk
cat > ~/.config/punktfunk/host.env <<'ENV'
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=GNOME
PUNKTFUNK_COMPOSITOR=mutter
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
ENV
```
See the [Configuration reference](/docs/configuration) for every option.
## 5. Run
From a terminal **inside your GNOME session** (so the host can reach Mutter):
```sh
cargo run --release -p punktfunk-host -- serve --native
```
The host starts listening, prints its fingerprint, and advertises itself on the network. Now
[connect a client](/docs/clients).
To run it automatically at boot — including on a **headless** machine with no monitor — see
[Running as a Service](/docs/running-as-a-service).
## Troubleshooting
- **gnome-shell fails to start / "GPU … not supported by EGL":** the NVIDIA GL/EGL userspace is
missing. Install `libnvidia-gl-<version>` (step 1) and confirm
`/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists.
- **Capture fails with "Session creation inhibited":** a **locked** GNOME session blocks screen
capture. On a headless/always-on host, disable the lock — see
[Running as a Service](/docs/running-as-a-service).
- More in [Troubleshooting](/docs/troubleshooting).
+58
View File
@@ -0,0 +1,58 @@
---
title: Ubuntu — KDE Plasma
description: Set up a punktfunk host on Ubuntu with KDE Plasma (KWin).
---
Set up a punktfunk host on **Ubuntu** running **KDE Plasma**. The host uses KDE's KWin compositor to
create a per-client virtual display. Needs **KWin 6.5.6 or newer**.
> New to this? Skim [Requirements](/docs/requirements) first.
## NVIDIA driver, dependencies, and build
These steps are identical to the GNOME guide — follow **steps 13** of
[Ubuntu — GNOME](/docs/ubuntu-gnome#1-nvidia-driver):
1. Install the NVIDIA driver **and** the `libnvidia-gl-<version>` userspace; enable `nvidia-drm
modeset=1`; reboot and verify with `nvidia-smi`.
2. Install the build toolchain and runtime libraries (the same `apt` line).
3. Clone and `cargo build --release -p punktfunk-host`.
## Configure
The host reads `~/.config/punktfunk/host.env`. For KDE Plasma:
```sh
mkdir -p ~/.config/punktfunk
cat > ~/.config/punktfunk/host.env <<'ENV'
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=KDE
PUNKTFUNK_COMPOSITOR=kwin
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
ENV
```
> Make sure you're on a **KDE Wayland** session (not X11) — the picker on the login screen. The
> virtual-display path is Wayland-only. See the [Configuration reference](/docs/configuration) for
> every option.
## Run
From a terminal **inside your Plasma session**:
```sh
cargo run --release -p punktfunk-host -- serve --native
```
The host starts listening and advertises itself on the network. Now [connect a client](/docs/clients).
To run it at boot — including fully **headless**, with KWin brought up automatically and no login —
see [Running as a Service](/docs/running-as-a-service); the headless appliance is built around KDE.
## Troubleshooting
- **KWin too old:** virtual outputs need KWin **≥ 6.5.6**. Check with `kwin_wayland --version`.
- **No picture / capture fails:** confirm you're on a Wayland session and the NVIDIA GL userspace is
installed (`libnvidia-gl-<version>`). More in [Troubleshooting](/docs/troubleshooting).