diff --git a/docs-site/content/docs/apple-stage2-presenter.md b/docs-site/content/docs/apple-stage2-presenter.md index 78843be..250e0d1 100644 --- a/docs-site/content/docs/apple-stage2-presenter.md +++ b/docs-site/content/docs/apple-stage2-presenter.md @@ -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 + diff --git a/docs-site/content/docs/bazzite.md b/docs-site/content/docs/bazzite.md index b09fcd9..d4eb806 100644 --- a/docs-site/content/docs/bazzite.md +++ b/docs-site/content/docs/bazzite.md @@ -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. diff --git a/docs-site/content/docs/clients.md b/docs-site/content/docs/clients.md new file mode 100644 index 0000000..0b82395 --- /dev/null +++ b/docs-site/content/docs/clients.md @@ -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 :9777 --pin # 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). diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md new file mode 100644 index 0000000..50d0efc --- /dev/null +++ b/docs-site/content/docs/configuration.md @@ -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. diff --git a/docs-site/content/docs/fedora-kde.md b/docs-site/content/docs/fedora-kde.md new file mode 100644 index 0000000..0e43f27 --- /dev/null +++ b/docs-site/content/docs/fedora-kde.md @@ -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). diff --git a/docs-site/content/docs/gnome-box.md b/docs-site/content/docs/gnome-box.md deleted file mode 100644 index 3421fdd..0000000 --- a/docs-site/content/docs/gnome-box.md +++ /dev/null @@ -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 -> . -# Needs console access (the screen is firmware-level, not reachable over SSH). -``` - -After reboot, `nvidia-smi` should show the GPU. (Alternatively, disable Secure Boot in firmware.) - -## 2. GNOME Wayland needs the GLVND NVIDIA EGL vendor - -If gnome-shell logs **`GPU /dev/dri/cardN ... not supported by EGL`** / `No EGL display` and -`libEGL warning: ... driver (null)`, GLVND has no NVIDIA EGL vendor and falls back to Mesa for the -NVIDIA card. The missing file is `/usr/share/glvnd/egl_vendor.d/10_nvidia.json`, shipped by -`libnvidia-gl-` — which `nvidia-driver-NNN-open` does **not** always pull in: - -```sh -sudo apt-get install -y libnvidia-gl-595 # provides 10_nvidia.json -``` - -(The EGL external-platform JSONs `10_nvidia_wayland` / `15_nvidia_gbm` are usually already present.) -`nvidia-drm modeset=1` must also be set (it's in `/etc/modprobe.d/nvidia-graphics-drivers-kms.conf` -on a normal install) for Wayland on NVIDIA. - -## 3. A GNOME Wayland session for Mutter to attach to - -The host attaches to a running GNOME **Wayland** session (`wayland-0`). On a headless box, autologin -provides it (a connected monitor also lets the session boot; a truly headless box would need -`gnome-shell --headless --virtual-monitor`). Enable GDM autologin: - -```ini -# /etc/gdm3/custom.conf -[daemon] -AutomaticLoginEnable = true -AutomaticLogin = -``` - -### Disable the screen lock (important for a headless appliance) - -A **locked** GNOME session makes Mutter reject RemoteDesktop/ScreenCast with -`RemoteDesktop.CreateSession: ... Session creation inhibited` — so capture fails after the session -auto-locks on idle. There's no human to unlock a headless box, so disable it (in the user session): - -```sh -gsettings set org.gnome.desktop.screensaver lock-enabled false -gsettings set org.gnome.desktop.screensaver idle-activation-enabled false -gsettings set org.gnome.desktop.session idle-delay 0 -``` - -## 4. Host config + appliance unit - -`~/.config/punktfunk/host.env` for the Mutter backend: - -```sh -XDG_RUNTIME_DIR=/run/user/1000 -DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus -WAYLAND_DISPLAY=wayland-0 -XDG_CURRENT_DESKTOP=GNOME -PUNKTFUNK_COMPOSITOR=mutter -PUNKTFUNK_VIDEO_SOURCE=virtual -PUNKTFUNK_ZEROCOPY=1 -PUNKTFUNK_INPUT_BACKEND=libei -``` - -Run it as a persistent appliance — the standard user unit + linger (no kde-session unit needed here, -autologin provides the session): - -```sh -mkdir -p ~/.config/systemd/user -cp scripts/punktfunk-host.service ~/.config/systemd/user/ -systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host -sudo loginctl enable-linger "$USER" # start the user unit at boot without an interactive login -``` - -`serve --native` then listens on GameStream + native QUIC (9777), creates a per-client Mutter virtual -output at the client's exact mode, and streams it zero-copy. Confirm it's up: -`punktfunk-client-rs --discover` from another box should list it. diff --git a/docs-site/content/docs/headless-box.md b/docs-site/content/docs/headless-box.md deleted file mode 100644 index 5efcccb..0000000 --- a/docs-site/content/docs/headless-box.md +++ /dev/null @@ -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. diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md new file mode 100644 index 0000000..bba2e55 --- /dev/null +++ b/docs-site/content/docs/host-cli.md @@ -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 ` | 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 ` | Management API address (default loopback `127.0.0.1:47990`). | +| `--mgmt-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 ` | QUIC listen port (default `9777`). | +| `--source virtual` | Use a real virtual display + NVENC (vs. `synthetic` test frames). | +| `--max-concurrent ` | Stream at most N sessions at once (default 4); overflow waits in the queue. | +| `--max-sessions ` | 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. diff --git a/docs-site/content/docs/how-it-works.md b/docs-site/content/docs/how-it-works.md new file mode 100644 index 0000000..304c0cf --- /dev/null +++ b/docs-site/content/docs/how-it-works.md @@ -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). diff --git a/docs-site/content/docs/index.mdx b/docs-site/content/docs/index.mdx index f4efe00..51f66a3 100644 --- a/docs-site/content/docs/index.mdx +++ b/docs-site/content/docs/index.mdx @@ -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 - - - - + + + + + +## 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). diff --git a/docs-site/content/docs/linux-setup.md b/docs-site/content/docs/linux-setup.md deleted file mode 100644 index e48ed64..0000000 --- a/docs-site/content/docs/linux-setup.md +++ /dev/null @@ -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-` (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)` | diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 6a5a389..cad4758 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -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" ] diff --git a/docs-site/content/docs/moonlight.md b/docs-site/content/docs/moonlight.md new file mode 100644 index 0000000..433c320 --- /dev/null +++ b/docs-site/content/docs/moonlight.md @@ -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. diff --git a/docs-site/content/docs/overview.md b/docs-site/content/docs/overview.md deleted file mode 100644 index 832ce1a..0000000 --- a/docs-site/content/docs/overview.md +++ /dev/null @@ -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. diff --git a/docs-site/content/docs/pairing.md b/docs-site/content/docs/pairing.md new file mode 100644 index 0000000..f8159de --- /dev/null +++ b/docs-site/content/docs/pairing.md @@ -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. diff --git a/docs-site/content/docs/quickstart.md b/docs-site/content/docs/quickstart.md new file mode 100644 index 0000000..8b96052 --- /dev/null +++ b/docs-site/content/docs/quickstart.md @@ -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). diff --git a/docs-site/content/docs/requirements.md b/docs-site/content/docs/requirements.md new file mode 100644 index 0000000..4fd7b54 --- /dev/null +++ b/docs-site/content/docs/requirements.md @@ -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-` 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. diff --git a/docs-site/content/docs/running-as-a-service.md b/docs-site/content/docs/running-as-a-service.md new file mode 100644 index 0000000..48a69bf --- /dev/null +++ b/docs-site/content/docs/running-as-a-service.md @@ -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. diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md index 7322b08..42a7889 100644 --- a/docs-site/content/docs/status.md +++ b/docs-site/content/docs/status.md @@ -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) diff --git a/docs-site/content/docs/troubleshooting.md b/docs-site/content/docs/troubleshooting.md new file mode 100644 index 0000000..423f415 --- /dev/null +++ b/docs-site/content/docs/troubleshooting.md @@ -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-` (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. diff --git a/docs-site/content/docs/ubuntu-gnome.md b/docs-site/content/docs/ubuntu-gnome.md new file mode 100644 index 0000000..786d049 --- /dev/null +++ b/docs-site/content/docs/ubuntu-gnome.md @@ -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- +``` + +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- # 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-` (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). diff --git a/docs-site/content/docs/ubuntu-kde.md b/docs-site/content/docs/ubuntu-kde.md new file mode 100644 index 0000000..ad80bed --- /dev/null +++ b/docs-site/content/docs/ubuntu-kde.md @@ -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-` 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-`). More in [Troubleshooting](/docs/troubleshooting).