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
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:
@@ -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 +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
|
||||
@@ -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)` |
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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 1–3** 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).
|
||||
Reference in New Issue
Block a user