One home per fact: IA rules, docs drift gates in CI, and the READMEs stop restating the docs #337

Merged
enricobuehler merged 1 commits from worktree-docs-wp0-wp1-anti-drift into main 2026-08-19 16:22:07 +00:00
14 changed files with 792 additions and 612 deletions
+38
View File
@@ -175,6 +175,19 @@ jobs:
- name: Test (unit + loopback + proptest + C ABI harness)
run: cargo test --workspace --locked
# The deep half of the docs-drift gates (the `docs-drift` job checks the docs-site copy
# and the textual rest): the committed spec must match what the binary actually serves.
# Build already compiled punktfunk-host with default features, so this re-links at worst.
# Byte diff on purpose — the generator is deterministic, and if that ever stops being
# true it deserves to surface here.
- name: OpenAPI spec drift gate
run: |
cargo run -p punktfunk-host --locked -- openapi > /tmp/openapi.regen.json
diff -u api/openapi.json /tmp/openapi.regen.json >/dev/null || {
echo "::error::api/openapi.json is stale — regenerate: cargo run -p punktfunk-host -- openapi > api/openapi.json && cp api/openapi.json docs-site/public/openapi.json"
exit 1
}
# The GPU encode backends are OFF by default, so every step above compiles ~none of them:
# `nvenc` gates enc/linux/nvenc_cuda.rs (+ nvenc_core/nvenc_status) and `vulkan-encode` gates
# enc/linux/vulkan_video.rs (+ the vendored vk_av1_encode/vk_valve_rgb bindings) — ~8,150
@@ -390,3 +403,28 @@ jobs:
# schema stability across bun2nix releases). Fix with: scripts/ci/check-bun-nix.sh --fix
- name: bun.nix drift gate
run: sh scripts/ci/check-bun-nix.sh
# Docs drift gates — pure git-grep textual checks, no cargo, no bun install (the deep half,
# regenerating the OpenAPI spec from the built host, rides in the `rust` job above). Same
# reasoning as bun-nix for being UNFILTERED: docs drift arrives through commits that look
# unrelated to docs — a renamed env var, a removed subcommand, a moved page.
docs-drift:
runs-on: ubuntu-24.04
container:
image: oven/bun:1
timeout-minutes: 15
steps:
# oven/bun ships neither git nor a real node, and the slim base has no CA bundle —
# actions/checkout needs all three (see the web job).
- name: Install git + node + CA certs
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates git nodejs
- uses: actions/checkout@v4
# OpenAPI snapshot in sync, PUNKTFUNK_* vars in docs still exist, undocumented-var
# ratchet (baseline: scripts/ci/docs-undocumented-env-baseline.txt), host-cli.md commands
# still exist, data/platforms.json parses.
- name: Docs drift gates
run: sh scripts/ci/check-docs-drift.sh
# Internal links only: /docs/* page links in docs-site content, relative file links in
# the repo's markdown. External URLs and #anchors are deliberately not checked.
- name: Docs link check
run: sh scripts/ci/check-docs-links.sh
+28 -3
View File
@@ -83,15 +83,40 @@ Two more gates that only apply to some changes:
instead of waiting for the CI job that compiles it.
Generated artifacts are checked in. `include/punktfunk_core.h` (cbindgen) is regenerated by the build
and CI fails if the committed copy drifts. `api/openapi.json` is **not** gated — nothing in CI
regenerates or diffs it, so regenerate and commit it yourself whenever you touch the management API,
and copy the snapshot the docs site serves:
and CI fails if the committed copy drifts. `api/openapi.json` is gated the same way: the `rust` job
regenerates the spec and diffs it against the committed file, and the `docs-drift` job checks that
`docs-site/public/openapi.json` — the snapshot the docs site serves — is a byte-for-byte copy of it.
Touch the management API and CI stays red until you regenerate and re-copy:
```sh
cargo run -p punktfunk-host -- openapi > api/openapi.json
cp api/openapi.json docs-site/public/openapi.json
```
## Where facts live (docs vs READMEs vs website)
Every user-facing fact has exactly one canonical home; everything else links to it. Duplicated
walkthroughs are how the docs drifted before — don't add new ones.
| Surface | Owns | Never contains |
|---|---|---|
| [docs-site](https://docs.punktfunk.unom.io) (`docs-site/content/`) | All user-facing facts: install, config, features, troubleshooting | Design rationale |
| READMEs (root, `packaging/*`, `scripts/*`) | Dev/packager rationale and pointers into the docs | User walkthroughs duplicated from docs-site |
| [punktfunk.unom.io](https://punktfunk.unom.io) (separate repo) | Marketing, downloads, blog | Instructions — it deep-links the docs instead |
| punktfunk-planning (private) | Design rationale, RFCs, plans | Anything user-facing |
Docs pages are written for one of two audiences, not both at once: the **get-started track**
(quickstart, install, pairing — short, one task per page, happy path only) assumes no Linux
expertise; the **reference track** (configuration, CLI, API, per-compositor pages) is allowed to be
dense. When a change touches a user-facing fact, update the docs-site page that owns it in the same
PR.
CI enforces the cheap half of this (`scripts/ci/check-docs-drift.sh` and `check-docs-links.sh`):
the OpenAPI snapshot must match `api/openapi.json`, every `PUNKTFUNK_*` variable the docs mention
must still exist in the tree, the counts of undocumented `PUNKTFUNK_*` variables and undocumented
`punktfunk-host` subcommands may never grow (document the new knob, or consciously raise the
baseline in the script), and internal docs links must resolve.
Match the surrounding code's comment density and naming. Commit messages end with the
`Co-Authored-By` trailer (see `git log`).
+4 -29
View File
@@ -109,36 +109,11 @@ installer (all-vendor: NVIDIA, AMD, Intel).
`punktfunk-host` is the streaming host; `punktfunk-web` is the browser console (pairing + status).
**Linux:** every package ships systemd **user** units, so you don't launch the host by hand. The
host unit won't start until `~/.config/punktfunk/host.env` exists, so copy the template your package
installed first:
```sh
mkdir -p ~/.config/punktfunk
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Debian/Ubuntu
# (on Bazzite take host.env.bazzite instead)
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
systemctl --user enable --now punktfunk-host # the streaming host
systemctl --user enable --now punktfunk-web # the web console (Arch: install punktfunk-web first)
```
The shipped host unit runs `serve --gamestream` — the native `punktfunk/1` plane **plus** the
GameStream/Moonlight-compat planes, which belong on a trusted LAN only; for a native-only host drop
the flag with a `systemctl --user edit punktfunk-host` drop-in (which needs an empty `ExecStart=`
line before the replacement — the install guide has the snippet). Then open
`https://<host-ip>:47992` and pair.
How the virtual display and input are wired up depends on your desktop — see
[KDE](https://docs.punktfunk.unom.io/docs/kde) · [GNOME](https://docs.punktfunk.unom.io/docs/gnome) ·
The per-platform guide walks you through the rest — first run, the web console, pairing, and the
desktop-specific wiring ([KDE](https://docs.punktfunk.unom.io/docs/kde) ·
[GNOME](https://docs.punktfunk.unom.io/docs/gnome) ·
[Steam / gamescope](https://docs.punktfunk.unom.io/docs/gamescope) ·
[Sway](https://docs.punktfunk.unom.io/docs/sway).
**Windows:** the installer registers and starts the host as a `LocalSystem` service, so there is
nothing to run by hand — open the web console and pair. Use
`punktfunk-host service start|stop|restart|status` if you need to control it. Upgrades happen in
place — the console's **Updates** card, `winget upgrade unom.PunktfunkHost`, or the newer
`setup.exe` over the old install; uninstall from Add/Remove Programs.
[Sway](https://docs.punktfunk.unom.io/docs/sway)).
Full instructions: **[docs.punktfunk.unom.io/docs/install](https://docs.punktfunk.unom.io/docs/install)**.
+126
View File
@@ -0,0 +1,126 @@
{
"$comment": "Single source for the install/port facts that used to drift across four surfaces (docs-and-onboarding-overhaul WP1). Consumers: docs-site install pages (WP2), the website download page (WP3), the guided install script (WP4). A port number, repo URL or install command lives HERE and nowhere else — pages quote it, they don't restate it. Validated by scripts/ci/check-docs-drift.sh (parse only; ponytail: cross-check against code literals when a consumer exists).",
"ports": {
"mgmt": {
"port": 47990,
"proto": "tcp",
"what": "management REST API + web console endpoint",
"env": "PUNKTFUNK_MGMT_BIND",
"conflict": "Sunshine / Apollo / Vibeshine serve their web UI on 47990 too — the one port still shared with them once GameStream compat is off. Move it via PUNKTFUNK_MGMT_BIND; clients relearn it from discovery."
},
"native": {
"port": 9777,
"proto": "udp",
"what": "punktfunk/1 QUIC control port",
"env": "PUNKTFUNK_NATIVE_PORT"
},
"data": {
"port": null,
"proto": "udp",
"what": "per-session video data plane — random port + hole-punch by default, pinnable",
"env": "PUNKTFUNK_DATA_PORT"
},
"mdns": {
"port": 5353,
"proto": "udp",
"what": "mDNS discovery"
},
"gamestream": {
"tcp": [47984, 47989, 48010],
"udp": [47998, 47999, 48000],
"what": "GameStream/Moonlight-compat planes (opt-in, `serve --gamestream`)",
"conflict": "Sunshine / Apollo / Vibeshine bind these same fixed ports and advertise the same mDNS name — run only one GameStream host at a time, or keep punktfunk native-only."
}
},
"conflicts": {
"hosts": ["Sunshine", "Apollo", "Vibeshine"],
"detect": "punktfunk-host detect-conflicts",
"detectExit": "1 only when a conflicting host runs or will start on its own; dormant leftovers print but exit 0",
"docs": "/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed"
},
"platforms": [
{
"id": "debian",
"name": "Debian 13 / Ubuntu 24.04+",
"installs": "host",
"packageManager": "apt",
"docs": "/docs/debian",
"install": [
"echo \"deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main\" | sudo tee /etc/apt/sources.list.d/punktfunk.list",
"sudo apt update",
"sudo apt install punktfunk-host"
]
},
{
"id": "arch",
"name": "Arch Linux / CachyOS",
"installs": "host",
"packageManager": "pacman",
"docs": "/docs/arch",
"install": [
"curl -fsS https://git.unom.io/api/packages/unom/arch/repository.key | sudo pacman-key --add -",
"sudo pacman-key --lsign-key E0CA04465C99C936E0B0C6510A317015A34DDD69",
"printf '\\n[punktfunk]\\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\\n' | sudo tee -a /etc/pacman.conf >/dev/null",
"sudo pacman -Sy punktfunk-host"
]
},
{
"id": "fedora",
"name": "Fedora 43+",
"installs": "host",
"packageManager": "dnf",
"docs": "/docs/fedora",
"repo": "https://git.unom.io/api/packages/unom/rpm/fedora-44",
"install": [
"sudo dnf install punktfunk",
"sudo usermod -aG input \"$USER\""
]
},
{
"id": "bazzite",
"name": "Bazzite / Fedora Atomic",
"installs": "host",
"packageManager": "sysext",
"docs": "/docs/bazzite",
"install": [
"curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh",
"sudo bash punktfunk-sysext.sh install"
]
},
{
"id": "nixos",
"name": "NixOS",
"installs": "host",
"packageManager": "nix",
"docs": "/docs/install",
"install": [
"# flake input: inputs.punktfunk.url = \"git+https://git.unom.io/unom/punktfunk\";",
"# then: imports = [ punktfunk.nixosModules.default ]; services.punktfunk.host.enable = true;"
]
},
{
"id": "windows",
"name": "Windows 10/11",
"installs": "host",
"packageManager": "winget",
"docs": "/docs/windows-host",
"install": [
"winget install unom.PunktfunkHost --interactive"
]
},
{
"id": "linux-client",
"name": "Linux client (any distro)",
"installs": "client",
"packageManager": "flatpak",
"docs": "/docs/install-client",
"install": [
"flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref"
]
}
]
}
+10 -11
View File
@@ -4,7 +4,12 @@ The Punktfunk documentation site: [Fumadocs](https://fumadocs.dev) on
[TanStack Start](https://tanstack.com/start) (Vite + Nitro/bun preset).
Content lives in [`content/docs/`](content/docs) as `.md`/`.mdx`. This site is the source of truth
for the **user-facing** guides; design rationale lives in the internal punktfunk-planning repo.
for the **user-facing** guides; design rationale lives in the internal punktfunk-planning repo, and
READMEs and the marketing site link here instead of restating anything — see "Where facts live" in
[CONTRIBUTING.md](../CONTRIBUTING.md). Pages serve one of two audiences, not both at once: the
**get-started track** (quickstart, install, pairing) assumes no Linux expertise — short pages, one
task each, happy path only; the **reference track** (configuration, CLI, API, per-compositor
pages) is allowed to be dense.
## API reference
@@ -19,16 +24,10 @@ cargo run -p punktfunk-host -- openapi > api/openapi.json
cp api/openapi.json docs-site/public/openapi.json
```
Nothing in CI diffs the two, so the snapshot goes stale silently — that manual `cp` is the only
thing keeping them in sync. Before publishing docs, check that they match:
```bash
diff <(jq -S . api/openapi.json) <(jq -S . docs-site/public/openapi.json)
```
That should print nothing. Right now it doesn't: the committed snapshot predates the
`/api/v1/update/check`, `/api/v1/update/apply` and `/api/v1/update/status` endpoints, so the
published `/api` reference is missing the host self-update surface — re-copy it.
CI keeps the pair honest: the `docs-drift` job fails unless the snapshot is a byte-for-byte copy
of `api/openapi.json`, and the `rust` job regenerates the spec and diffs it against the committed
one — so a management-API change can't publish stale API docs any more, it fails CI until you run
the two commands above.
## Develop
+3 -2
View File
@@ -9,8 +9,9 @@ present; the NVIDIA-specific notes below apply to the NVENC path.
> [`debian/README.md`](debian/README.md) (`apt update && apt upgrade` for new builds).
> 👉 **End-to-end Bazzite setup walkthrough** (install → udev/group → `host.env` → service →
> firewall → verify → troubleshooting): [`bazzite/README.md`](bazzite/README.md). This file is the
> higher-level packaging rationale.
> firewall → verify): [docs.punktfunk.unom.io/docs/bazzite](https://docs.punktfunk.unom.io/docs/bazzite).
> [`bazzite/README.md`](bazzite/README.md) keeps the install-path/packaging view (sysext vs bootc
> vs layering); this file is the higher-level packaging rationale.
```
packaging/
+10 -22
View File
@@ -35,29 +35,17 @@ them). Enable exactly one.
The registry **signs the repo database and every package**, so first import its key into
pacman's keyring (a one-time step — after this, packages install signature-verified):
```sh
# 1. Trust the registry signing key.
curl -fsS https://git.unom.io/api/packages/unom/arch/repository.key \
| sudo pacman-key --add -
sudo pacman-key --lsign-key E0CA04465C99C936E0B0C6510A317015A34DDD69
The user-facing walkthrough — key import, repo add, `pacman -Syu` install — lives on the
[Arch docs page](https://docs.punktfunk.unom.io/docs/arch), stated once so it can't drift (see
"Where facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)). Packager notes: no `SigLevel`
line is needed (pacman's default `Required DatabaseOptional` verifies the signed packages against
the imported key), the repo-add uses `printf` rather than a heredoc so it works in fish (CachyOS's
default shell has no `<<EOF`), and Arch is rolling — packages are built against current Arch
sonames, so the box itself must stay updated.
# 2. Add the repo (pick ONE channel — punktfunk for releases, punktfunk-canary for main builds).
# printf, not a heredoc, so this works in fish too (CachyOS's default shell has no `<<EOF`).
printf '\n[punktfunk]\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\n' \
| sudo tee -a /etc/pacman.conf >/dev/null
# 3. Sync + install.
sudo pacman -Sy punktfunk-host # gaming rig
sudo pacman -Sy punktfunk-client # the native GTK4 Linux client
sudo pacman -Sy punktfunk-web # optional browser management console
```
(No `SigLevel` line needed — pacman's default `Required DatabaseOptional` verifies the signed
packages against the key you just trusted. Arch is rolling, so the packages are built against
current Arch sonames — keep the box itself updated too.)
Step 2 **appends**, so running it twice leaves two `[punktfunk]` blocks and every later pacman
run opens with `error: could not register 'punktfunk' database (database already registered)`.
The repo-add **appends** to `/etc/pacman.conf`, so running it twice leaves two `[punktfunk]`
blocks and every later pacman run opens with
`error: could not register 'punktfunk' database (database already registered)`.
It is harmless — pacman ignores the duplicate and carries on — but to silence it, delete the
extra block from `/etc/pacman.conf`.
+42 -402
View File
@@ -1,16 +1,12 @@
# Setting up punktfunk on Bazzite
# punktfunk on Bazzite — packaging and install paths
A step-by-step setup guide for running the **punktfunk** low-latency streaming host on
**Bazzite** (the immutable, Fedora-Atomic gaming distro). Everything below is grounded in this
repo's packaging and ops files; where something is **not yet published or not in the repo**, it's
flagged explicitly. For the higher-level packaging rationale ("why not Flatpak", the build), see
[`../README.md`](../README.md).
> **What you get on Bazzite:** it already ships the three things punktfunk normally has to fight
> for — **gamescope**, **PipeWire/WirePlumber**, and (on the `-nvidia` images) the **NVIDIA driver
> with NVENC/EGL**. The only genuinely new runtime bits punktfunk adds are `ffmpeg-libs` (with
> NVENC, from RPM Fusion **nonfree**), `opus`, and `libei`.
> Source: `packaging/README.md`, `packaging/rpm/punktfunk.spec`.
The packager/ops view of the Bazzite (Fedora Atomic) target: which install path to ship, how each
one is built and updated, and the gotchas of layered/bootc hosts. **Setting a host up** — udev and
the `input` group, `host.env`, the service, the firewall, streaming the KDE desktop, verifying —
is the docs' job: follow **[docs.punktfunk.unom.io/docs/bazzite](https://docs.punktfunk.unom.io/docs/bazzite)**
(firewall specifics are on [Fedora §4](https://docs.punktfunk.unom.io/docs/fedora), same firewalld
service definitions). Nothing here duplicates those pages. For the higher-level packaging
rationale ("why not Flatpak", the build), see [`../README.md`](../README.md).
> ⚠️ **COPR note (Path C only).** The legacy layering path's commands reference a COPR project
> named `enricobuehler/punktfunk` that is operator-run and may not be published (see
@@ -18,8 +14,6 @@ flagged explicitly. For the higher-level packaging rationale ("why not Flatpak",
> the repo file `https://git.unom.io/api/packages/unom/rpm/bazzite.repo`) — it's what CI
> actually publishes to. Paths A (sysext) and B (bootc) don't involve the COPR at all.
---
## 1. Choose an install path
There are three paths on Bazzite, driven by different files in `packaging/`:
@@ -34,53 +28,28 @@ There are three paths on Bazzite, driven by different files in `packaging/`:
every OS update slower and can **block upgrades entirely** until removed. A sysext never enters an
rpm-ostree transaction: it merges/unmerges at runtime, survives OS updates, and updating punktfunk
is one command with **no reboot** (layering needs one per update). It's the mechanism the Fedora
Atomic maintainers ship via [fedora-sysexts](https://fedora-sysexts.github.io/). All paths require
the **same first-run setup** (sections 36).
Atomic maintainers ship via [fedora-sysexts](https://fedora-sysexts.github.io/).
### Path A — systemd-sysext (recommended)
Run on the Bazzite host:
Install, day-2 commands, channel switching, rollback, feed-signature refusals and the
major-rebase behavior are all on the [docs page](https://docs.punktfunk.unom.io/docs/bazzite) —
that's the walkthrough to hand a user. Packager-side facts:
```sh
# One-time bootstrap; afterwards the tool is on PATH as `punktfunk-sysext` (it ships inside
# the image). `--channel canary` for rolling main-branch builds instead of releases.
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
sudo bash punktfunk-sysext.sh install
```
This downloads the newest image for your Fedora base (host + tray + **web console**,
SHA-256-verified against a signed manifest from the feed
`…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/`),
installs it as `/var/lib/extensions/punktfunk.raw`, merges it, and immediately applies what the
RPM scriptlets would have (udev reload, sysctl) plus the two `/etc` files a sysext can't carry
(the gamescope-session drop-in and the tray autostart entry, staged under
`/usr/share/punktfunk/etc/`). No reboot at any point. Day-2:
```sh
sudo punktfunk-sysext update # fetch + merge the newest build (then restart the user service)
sudo punktfunk-sysext status # merged?, installed vs latest, channel/feed
sudo punktfunk-sysext remove # unmerge + delete; ~/.config/punktfunk is left alone
```
Details worth knowing:
- **The feed is signed.** Each feed carries `SHA256SUMS` plus a detached OpenPGP signature
`SHA256SUMS.asc` from `packages@unom.io` (`AF245C506F4E4763`) — the same key that signs our RPMs.
`punktfunk-sysext` verifies that signature, with the public key baked into the script, *before*
it believes the manifest, and refuses a feed it can't verify. The checksums alone never proved
authorship: they sit on the same registry as the images they describe, so whatever could replace
an image could replace its checksum in the same breath. If you are on a feed published before
signing existed, it is sealed on the next publish to that Fedora major; to install from it
meanwhile — accepting that the image is unauthenticated — set `PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1`.
- CI (`.gitea/workflows/rpm.yml`) builds the image per Fedora major and publishes it to the feed
`…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/``SHA256SUMS` plus a detached
OpenPGP signature from `packages@unom.io` (`AF245C506F4E4763`, the RPM signing key). The public
key is baked into `punktfunk-sysext.sh`; the script refuses a feed it can't verify
(`PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1` is the documented escape hatch for pre-signing feeds).
- The image embeds `ID=fedora` + `VERSION_ID` (matched through Bazzite's `ID_LIKE`), so after a
**major Bazzite rebase** (F43 → F44) the old image is **refused** instead of merging
soname-broken binaries — `punktfunk-sysext update` then fetches the image built for the new
base (feeds exist per Fedora major, from the same CI matrix as the RPM groups).
- SELinux labels are baked into the image at build time (squashfs pseudo-xattrs computed from
the targeted policy) — without them udev couldn't read the gamepad rule under enforcing.
Validated live on Bazzite 43.
- **Migrating from layering (path C):** install the sysext (it shadows the layered copy at
once), then `sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot`.
major rebase the old image is refused instead of merging soname-broken binaries; feeds exist
per Fedora major, from the same CI matrix as the RPM groups.
- SELinux labels are baked into the image at build time (squashfs pseudo-xattrs computed from the
targeted policy) — without them udev couldn't read the gamepad rule under enforcing. Validated
live on Bazzite 43.
- Install also applies what the RPM scriptlets would have (udev reload, sysctl) and seeds the two
`/etc` files a sysext can't carry (the gamescope-session drop-in, the tray autostart entry),
staged under `/usr/share/punktfunk/etc/`.
### Path B — bootc image (`FROM bazzite-nvidia`)
@@ -169,340 +138,29 @@ sudo bash packaging/bazzite/update-punktfunk.sh --reboot
> `punktfunk.repo`, canary's `<next-minor>.0-0.ciN` **outranks** the stable `X.Y.Z-1` and the box
> silently tracks canary. Enable exactly one channel — set `enabled=0` in the other repo file.
---
## 2. First-run setup
## 2. Prerequisites — what Bazzite gives you vs. what you must still do
All of it — `input`/`punktfunk` groups, `host.env`, enabling the service, the firewall, KDE
desktop mode, verifying the first stream — lives on the
[docs page](https://docs.punktfunk.unom.io/docs/bazzite) and applies identically to all three
install paths. Don't restate it here; a fact stated twice is a fact that drifts (see
"Where facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)).
**Already satisfied on Bazzite (`-nvidia` images):**
## 3. Gotchas the docs don't carry (yet)
- NVIDIA driver: `libnvidia-encode` (NVENC) + `libEGL_nvidia` for the zero-copy path.
- `gamescope` — the default compositor backend punktfunk uses on Bazzite.
- PipeWire + WirePlumber — the capture/audio graph.
<!-- WP2 of the docs overhaul moves these into /docs/troubleshooting; until then this is their
only home, so they stay. -->
**You must still do (covered below):**
1. **Reboot** after layering / rebasing (section 1).
2. **Join the `input` group** and ensure the **udev rule** is installed (section 3) — required for
virtual gamepads / DualSense.
3. **Place `host.env`** and **enable the systemd user service** (sections 45).
4. **Open firewall ports** (section 6).
RPM Fusion's `ffmpeg-libs` is a **weak dependency** (`Recommends:` in the spec) — the package
installs without it, but **NVENC encoding will fail at runtime** if it's missing. The RPM Fusion
step in section 1 covers this.
---
## 3. udev rule + the `input` group
punktfunk creates **virtual X-Box-360 gamepads** via `/dev/uinput` and **virtual DualSense** pads
via `/dev/uhid` (kernel `hid-playstation` driver — LEDs, adaptive triggers, touchpad, gyro). The
udev rule grants the `input` group access to both nodes.
The RPM **already installs** the rule to `/usr/lib/udev/rules.d/60-punktfunk.rules` and its `%post`
reloads udev. So on a packaged install (Path A or B) **you only need to join the `input` group**:
```sh
ujust add-user-to-input-group # then LOG OUT and back in (or reboot)
```
> ⚠️ **On Bazzite use `ujust add-user-to-input-group`, NOT `sudo usermod -aG input $USER`.** Bazzite
> is an atomic (rpm-ostree) OS where `/etc/group` is managed declaratively — a plain `usermod` either
> doesn't stick or gets reverted on the next update. The `ujust` recipe edits the group the
> immutable-OS-correct way (and reloads udev). (`ujust` ships with Bazzite; `ujust --list` shows all
> recipes.)
> 🔁 **The group change does not apply to your current login session** — you must re-login (or
> reboot). Until then, gamepad creation fails with a permission error on `/dev/uinput`. This is the
> single most common "why don't my gamepads work" gotcha.
If you installed from a tarball/source instead of the RPM (so the rule isn't in place), install it
manually — the exact commands from the rule file's header (`scripts/60-punktfunk.rules`):
```sh
sudo cp scripts/60-punktfunk.rules /etc/udev/rules.d/
ujust add-user-to-input-group # NOT `usermod` on Bazzite (see the note above); then re-login
sudo udevadm control --reload-rules && sudo udevadm trigger
```
The core rule contents, for reference (the full file additionally grants the `input` group access
to the vhci attach files and to the hidraw nodes of the virtual pads the host creates — Steam/SDL
need hidraw to send a DualSense's adaptive-trigger/lightbar feedback and reliable rumble):
```
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess"
KERNEL=="uhid", SUBSYSTEM=="misc", OPTIONS+="static_node=uhid", GROUP="input", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", GROUP="input", MODE="0660", TAG+="uaccess" # + 0DF2/09CC/2009/1205/1102
```
---
## 4. Configure `host.env`
The systemd user unit reads its environment from **`~/.config/punktfunk/host.env`**
(`EnvironmentFile=-%h/.config/punktfunk/host.env` in `scripts/punktfunk-host.service` — the `-`
makes it optional, since no package creates the file; absent just means every knob at its default).
The RPM ships a Bazzite-tuned template at `/usr/share/punktfunk/host.env.bazzite`. Copy it into
place:
```sh
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
# then edit ~/.config/punktfunk/host.env
```
The Bazzite template (`packaging/bazzite/host.env`) contains:
```sh
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy capture (dmabuf -> CUDA -> NVENC) is ON by default and auto-falls back to CPU if
# unavailable. No need to set it. Set to 0 only to force the CPU path.
# PUNKTFUNK_ZEROCOPY=0
#RUST_LOG=info
```
**What each knob means and why these are the Bazzite defaults:**
| Knob | Value | Meaning |
|---|---|---|
| *(no compositor / no anchors)* | — | The host **auto-detects** the live session per connect (Gaming Mode gamescope vs the KDE desktop) and follows switches mid-stream; a `systemctl --user` service inherits the right `XDG_RUNTIME_DIR` and the host derives the bus itself. Pinning `PUNKTFUNK_COMPOSITOR` or hardcoding uid-1000 anchors only breaks this — leave them out. |
| `PUNKTFUNK_VIDEO_SOURCE` | `virtual` | Create a per-client virtual output at the client's exact WxH@Hz (the flagship "native resolution, no scaling" mode), vs. `portal` which captures an existing monitor. |
| `PUNKTFUNK_GAMESCOPE_ATTACH` | *(unset)* | Gaming Mode model. Left unset the host picks one per connect, which on Bazzite is **managed**: it relaunches the gaming session headless at the **client's** exact mode, so the client gets a display of its own. Set `=1` to force **attach** instead — the box keeps its session and the client is served a mirror of the box's own screen at the box's mode (see the template's comments). |
| `PUNKTFUNK_ZEROCOPY` | `on` *(default)* | GPU zero-copy capture (dmabuf → CUDA → NVENC), on by default. Falls back to CPU automatically if unavailable; set `0` to force the CPU path. |
| `RUST_LOG` | (commented) | Uncomment `RUST_LOG=info` for verbose logs while debugging. |
**Optional — a real DualSense for clients holding one:** add `PUNKTFUNK_GAMEPAD=dualsense` to present
games a virtual Sony DualSense (lightbar, adaptive triggers, touchpad, motion) instead of the
default X-Box-360 pad. The feedback flows back to a real DualSense on the client.
**The Plasma desktop needs no extra config:** the same auto-detection streams the KDE Desktop
session whenever that's what's live — no compositor pin, no `WAYLAND_DISPLAY` /
`XDG_CURRENT_DESKTOP` (the host retargets those per connect). The full knob list (FEC %, per-stage
timing, etc.) is in `scripts/host.env.example` / `/usr/share/punktfunk/host.env.example`.
> Auto-detection is what makes Bazzite the easy path: the host follows the box between Gaming Mode
> and the Desktop — even mid-stream — with a one-line config. You don't need any of the
> headless-KDE bring-up scripts (`scripts/headless/run-headless-kde.sh`) on Bazzite.
---
## 5. Enable and start the service
punktfunk runs as a **systemd `--user` service** (not root) — it needs your graphical/user
session's PipeWire and D-Bus. The unit (`scripts/punktfunk-host.service`) is installed by the RPM
into the user unit directory.
```sh
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host
# Management web console (pairing + status), if you installed punktfunk-web (it ships in the Gitea
# RPM registry / bootc image — COPR can't build it; see ../rpm/README.md). Read the login password:
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' # then open https://<host-ip>:47992
```
Check health and logs:
```sh
systemctl --user status punktfunk-host
journalctl --user -u punktfunk-host -f
```
> **What `serve` actually starts.** The bundled unit's `ExecStart` runs `punktfunk-host serve
> --gamestream`, so out of the box you get the **unified host**: the native `punktfunk/1` (QUIC) plane
> — always on in `serve` — **plus** the GameStream/Moonlight-compat planes (mDNS discovery, pairing,
> RTSP, the fixed GameStream ports) and the management REST API on 47990. The `--gamestream` flag is
> what adds the Moonlight surface; GameStream pairs over plain HTTP and its legacy encryption is weaker
> than the native plane's (security-review #5/#9), so it's **opt-in and trusted-LAN only**. For a
> **secure native-only host**, drop `--gamestream` from the unit's `ExecStart` (bare `serve`) — native
> clients still work; only stock Moonlight stops.
> (Source: `crates/punktfunk-host/src/main.rs` — `serve` runs the native plane + mgmt; `--gamestream`
> adds `gamestream::serve`.)
> **Unit caveat:** `scripts/punktfunk-host.service` declares only `After=pipewire.service` and (in
> the upstream/dev layout) assumes the binary at `%h/punktfunk/target/release/punktfunk-host`. The
> **RPM-installed** binary lives at `/usr/bin/punktfunk-host`. If `systemctl --user cat
> punktfunk-host` shows `ExecStart` pointing at a missing path in your home dir, drop an override
> (`systemctl --user edit punktfunk-host`) setting `ExecStart=/usr/bin/punktfunk-host serve
> --gamestream` (or bare `serve` for a secure native-only host).
---
## 6. Firewall
Bazzite runs **firewalld**, so the ports must be opened. The `punktfunk-host` package installs
firewalld **service definitions** (`/usr/lib/firewalld/services/punktfunk-gamestream.xml` and
`punktfunk-native.xml`), so enabling is one command — reload first so firewalld picks up the
definition, add the service, reload to apply:
```sh
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --add-service=punktfunk-gamestream # Moonlight/GameStream host
# --add-service=punktfunk-native # …or the native-only host
sudo firewall-cmd --reload
```
`punktfunk-gamestream` opens the fixed Moonlight ports + mDNS; `punktfunk-native` opens the QUIC
control port (UDP 9777) + mDNS + the mgmt/library API (TCP 47990, HTTPS + mTLS). Enable both if the
host runs `serve --gamestream` (both planes). The
per-port breakdown below is for reference (or for opening ports by hand); the ports are the code
constants (`crates/punktfunk-host/src/gamestream/mod.rs`, `mgmt.rs`) and the GameStream-host port-map
(punktfunk-planning: `gamestream-host-plan.md`).
**GameStream / Moonlight ports** (fixed; Moonlight derives them from the HTTP base). These only apply
when the host runs `serve --gamestream` (the bundled unit's default); on a bare-`serve` native-only
host you don't open them:
| Port | Proto | Purpose |
|---|---|---|
| 47984 | TCP | HTTPS nvhttp (paired, mutual-TLS) |
| 47989 | TCP | HTTP nvhttp (`/serverinfo`, `/pair` PIN flow) |
| 48010 | TCP | RTSP handshake |
| 47998 | UDP | Video RTP (+ FEC) |
| 47999 | UDP | ENet control stream + remote input |
| 48000 | UDP | Audio (Opus) |
| 5353 | UDP | mDNS — so Moonlight auto-discovers the host (`_nvstream._tcp.local.`) |
**Management REST API:** **TCP 47990** (HTTPS + mTLS) — `serve` **binds it to all interfaces by
default** so paired clients can browse the game library over the LAN; the `punktfunk-native` profile
(above) opens it, or open it by hand with `firewall-cmd --add-port=47990/tcp`. Off-loopback it serves
only read-only status/library to a paired client cert; every admin action stays loopback-only. Pass
`--mgmt-bind 127.0.0.1:47990` to keep it loopback-only instead.
To open the GameStream ports by hand instead of the service (equivalent):
```sh
sudo firewall-cmd --permanent --add-port=47984/tcp \
--add-port=47989/tcp \
--add-port=48010/tcp
sudo firewall-cmd --permanent --add-port=47998/udp \
--add-port=47999/udp \
--add-port=48000/udp \
--add-port=5353/udp
sudo firewall-cmd --reload
```
**If you also run the native `punktfunk/1` host** (`punktfunk-host punktfunk1-host`, not started by the
default unit):
- **QUIC control plane: UDP 9777** (default `--port`; change with `--port N`).
- **Data plane: a separate UDP port** — by default *random* (`0.0.0.0:0`), so there is **no fixed
port to open**. Video flows host → client, but the client sends the first packet (a hole-punch): if
firewalld drops it, the host waits ~2.5 s and falls back to the client-reported address and streams
anyway, so you normally **leave the data port closed**. To skip that ~2.5 s fallback, pin it with
`serve --data-port <PORT>` (or `PUNKTFUNK_DATA_PORT`) and open exactly that one port with
`firewall-cmd --add-port=<PORT>/udp`. A fixed port serves one session at a time (concurrent ones
fall back to random + hole-punch) and streams to the client's reported address (flat LAN /
non-remapping forward only).
```sh
# Only if you run `punktfunk1-host`:
sudo firewall-cmd --permanent --add-port=9777/udp && sudo firewall-cmd --reload
```
---
## 6.5 Desktop (KDE) mode — stream the desktop at the client's resolution (optional)
The host **auto-detects** the live session per connect: in **Steam Gaming Mode** it attaches to the
running gamescope (no setup); switch the box to the **KDE Desktop** and it drives a KWin *virtual
output at the connecting client's exact resolution* (no TV-stretch, churn-free). The Desktop path
needs one one-shot setup the first time, because a normal KDE login withholds two things the
headless host needs — the privileged `zkde_screencast` virtual-output protocol, and an
auto-approved RemoteDesktop input grant:
```sh
bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
# then log out + back into the KDE Desktop session once (or reboot) so KWin restarts with the flag
```
That seeds the `kde-authorized` RemoteDesktop grant into `~/.local/share/flatpak/db/` — the input
half. The **video** half needs no session-wide override: the image ships
`io.unom.Punktfunk.Host.desktop`, whose `X-KDE-Wayland-Interfaces` grants the host KWin's
`zkde_screencast` protocol on a normal Plasma login (least-privilege — only this binary, only that
interface). Older versions of the script wrote a session-wide
`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` into `~/.config/environment.d/10-punktfunk-kwin.conf`; it now
*removes* that file as an over-broad leftover. Gaming Mode is unaffected. To connect from Desktop
Mode, switch to it (Steam → Power → Switch to Desktop), then connect the client; switching
**mid-stream** requires a reconnect (the host resolves the backend per connect).
> **On 0.26.0-1 specifically, Desktop mode is broken and no amount of this setup fixes it.** That
> image shipped `cap_sys_nice=ep` on `/usr/bin/punktfunk-host`, and a capability-carrying process is
> one KWin cannot identify (it resolves `/proc/<pid>/exe` to match the `.desktop`, and the kernel
> refuses that read), so the session dies with `KWin does not expose zkde_screencast_unstable_v1 to
> this client`. A merged sysext's `/usr` is read-only, so it cannot be repaired in place — take the
> next image (`sudo punktfunk-sysext update`). `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` works around it
> meanwhile by disabling the check that needs the identification.
---
## 7. Verify it's working
**1. Watch the startup log:**
```sh
journalctl --user -u punktfunk-host -f
```
A healthy `serve` startup logs the `punktfunk-host (punktfunk_core ABI v…)` banner, then `mDNS
advertising`, and an RTSP listening line on port 48010. No NVENC/EGL errors on the first connection.
**2. Pair a stock Moonlight client (recommended first test):**
- Open Moonlight on your phone/PC on the **same LAN** — the host should appear automatically (mDNS).
- Select it; Moonlight shows a 4-digit PIN. The host completes the GameStream pairing handshake (it
persists across restarts).
- Launch the app — you should get video at your client's native resolution/refresh, with the nested
`steam -gamepadui` (or whatever `PUNKTFUNK_GAMESCOPE_APP` you set) running inside gamescope.
**3. (Optional) native punktfunk/1 client** — only if you're running the separate `punktfunk1-host`. The
repo's reference client is `punktfunk-probe`, e.g. `punktfunk-probe --mode 1280x720x120 --out
/tmp/a.h265` (add `--pin HEX` for PIN pairing). This is a headless/decode-to-file reference, not a
desktop viewer.
---
## 8. Troubleshooting (grounded in the repo's real gotchas)
- **Gamepads don't appear / permission denied on `/dev/uinput` or `/dev/uhid`.** Either you haven't
joined the `input` group, or you haven't re-logged-in since. On Bazzite join it with
`ujust add-user-to-input-group` (a plain `sudo usermod -aG input` doesn't stick on an atomic OS —
see section 3), then log out and back in (or reboot): group membership only takes effect on a new
session. The host log makes this unambiguous — it prints `virtual gamepad created` /
`virtual DualSense created` on success, or `… creation failed — controller input disabled` when
the device node isn't writable. (`scripts/60-punktfunk.rules`, `packaging/README.md`.)
- **No video / NVENC fails to encode.** RPM Fusion's `ffmpeg-libs` (with NVENC) is missing — it's a
weak dependency, so the package installed without it. Re-run the RPM Fusion step in section 1.
- **No video / NVENC fails to encode.** RPM Fusion's `ffmpeg-libs` (with NVENC) is a **weak
dependency** (`Recommends:` in the spec) — the package installs without it, and NVENC then
fails at runtime. Re-run the RPM Fusion step in Path C step 1.
(`packaging/rpm/punktfunk.spec`: `Recommends: ffmpeg-libs`.)
- **gamescope session won't come up / capture deadlocks.** punktfunk needs **gamescope ≥ 3.16.22**
older versions (e.g. the broken 3.16.20 some bases shipped) **deadlock on PipeWire ≥ 1.6**, and a
wedged capture link can head-block the whole PipeWire daemon system-wide. Check with `gamescope
--version`. Bazzite tracks recent gamescope, but verify if you hit hangs. (Project notes.)
- **NVENC/EGL silently stops working after a system update.** punktfunk's reference box uses the
NVIDIA **open** kernel module, and a kernel update can silently drop it. On Bazzite the NVIDIA
stack is image-managed (`bazzite-nvidia`), so this is **far less likely** — but if NVENC dies right
after an `rpm-ostree`/`bootc` update, confirm the NVIDIA driver still loads (`nvidia-smi`) before
blaming punktfunk.
- **Zero-copy falls back to CPU.** The zero-copy path (on by default) needs working EGL/CUDA from the
NVIDIA driver. The code falls back to CPU automatically; check the log for the fallback line and
verify the `-nvidia` image / driver is healthy.
- **Session anchors in `host.env`.** The template no longer sets `XDG_RUNTIME_DIR` /
`DBUS_SESSION_BUS_ADDRESS` — a `systemctl --user` service inherits the right values. If an older
config hardcodes them with the wrong uid (`/run/user/1000` when `id -u` isn't 1000), the host
points at another user's PipeWire/D-Bus and everything fails (`pw audio connect … Creation
failed`, no capture). Delete both lines, or fix the uid.
- **Service `ExecStart` points at a missing path in `$HOME`.** The dev unit references
`%h/punktfunk/target/release/...`. The RPM binary is `/usr/bin/punktfunk-host`. Override
`ExecStart=/usr/bin/punktfunk-host serve --gamestream` (or bare `serve` for native-only) if needed
(section 5).
- **Dev-layout unit.** `scripts/punktfunk-host.service` (the upstream/dev unit) assumes the binary
at `%h/punktfunk/target/release/punktfunk-host`; the packaged binary is `/usr/bin/punktfunk-host`.
If `systemctl --user cat punktfunk-host` shows `ExecStart` pointing into a home dir, drop an
override (`systemctl --user edit punktfunk-host`) setting
`ExecStart=/usr/bin/punktfunk-host serve --gamestream` (or bare `serve` for native-only).
- **Stream lags, then freezes, with a DualSense-type client pad (SELinux enforcing).** The virtual
DualSense / DualShock 4 binds the kernel's `hid-playstation` driver, and Valve's `ds_inhibit`
@@ -527,11 +185,6 @@ desktop viewer.
no `hid-playstation`) — costs adaptive triggers, lightbar and touchpad. The host-side
`PUNKTFUNK_GAMEPAD` knob does **not** help: an explicit client choice outranks it.
- **Moonlight can't see the host.** Ensure UDP 5353 (mDNS) and the GameStream ports are open
(section 6) and client + host are on the same L2 LAN segment.
---
## Appendix — if the COPR isn't published yet
The COPR (`enricobuehler/punktfunk`) is **operator-run and may not be live**. If `rpm-ostree install
@@ -549,16 +202,3 @@ To publish the COPR for others (so `rpm-ostree install punktfunk` / the bootc im
`packaging/copr/README.md` — create the project, point build-from-SCM at the repo with spec path
`packaging/rpm/punktfunk.spec`, add RPM Fusion nonfree as an external repo, and select chroots
matching your Bazzite Fedora base (`rpm -E %fedora`).
---
### Accuracy flags
1. The COPR is **operator-run / not assumed published** — both install paths depend on it.
2. There is **no firewall script/doc in the repo** — the ports above are derived from the code.
3. The bundled systemd unit runs `serve --gamestream` — the native `punktfunk/1` QUIC plane (always
on) **plus** the GameStream/Moonlight planes. Drop `--gamestream` for a secure native-only host;
`punktfunk1-host` is a separate standalone native host, unmanaged by the unit.
4. The mgmt port (47990) **binds all interfaces by default** and is opened for paired clients by the
`punktfunk-native` profile (game-library browsing over mTLS); off-loopback it serves only read-only
status/library and keeps admin loopback-only. `--mgmt-bind 127.0.0.1:47990` restores loopback-only.
+16 -102
View File
@@ -80,112 +80,26 @@ Two things make the one package serve both Debian 13 and Ubuntu 26.04:
**Ubuntu 24.04 gets no gamescope package** and cannot: the wayland floor is a runtime one too.
## Install on a host (one-time)
## Install on a host
The registry is public, so no apt auth is needed — just trust the repo's signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
Then, as the desktop user:
```sh
sudo usermod -aG input "$USER" # virtual gamepads (re-login to take effect)
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk-host/host.env.example ~/.config/punktfunk/host.env # then edit
systemctl --user enable --now punktfunk-host
# Web console — enable it and read the auto-generated login password (then open https://<host-ip>:47992):
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
```
The user-facing walkthrough — repo key, sources line, `apt install`, first-run setup — lives on
the docs pages ([Debian](https://docs.punktfunk.unom.io/docs/debian) /
[Ubuntu](https://docs.punktfunk.unom.io/docs/ubuntu)), stated once so it can't drift (see "Where
facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)). Packager-relevant: the registry is
public (no apt auth — only the repo's signing key), and `stable` / `canary` are separate apt
distributions, so a stable box never jumps to a canary build.
## Firewall
**Debian ships no firewall and Ubuntu's `ufw` is installed-but-inactive by default**, so out of the
box there is nothing to open. If you turn one on, the `punktfunk-host` package ships a one-liner
opener for both **ufw** and **firewalld** (neither auto-enabled):
```sh
# ufw (Ubuntu) — profile at /etc/ufw/applications.d/punktfunk, read at once (no reload):
sudo ufw allow punktfunk-native # the default native host
sudo ufw allow punktfunk-gamestream # …add for Moonlight compat
# firewalld — service definitions at /usr/lib/firewalld/services/:
sudo firewall-cmd --reload # load the installed definition
sudo firewall-cmd --permanent --add-service=punktfunk-native
# --add-service=punktfunk-gamestream # …add for Moonlight compat
sudo firewall-cmd --reload
```
If you installed the **web console** (`punktfunk-web`) and want it reachable from another device,
open its port with the matching one-liner — `sudo ufw allow punktfunk-web` or `sudo firewall-cmd
--permanent --add-service=punktfunk-web && sudo firewall-cmd --reload` — which opens **TCP 47992**
(HTTPS, login-gated). The mgmt API (47990) is opened for paired clients by the `punktfunk-native`
profile (game-library browsing over mTLS); off-loopback it serves only read-only status/library and
keeps admin loopback-only.
Prefer explicit rules? Open the ports directly. The **native `punktfunk/1`** plane:
- **QUIC control plane: UDP 9777** (`serve --native-port N` to change).
- **Data plane: a separate UDP port.** By default it's *random* — the host binds `0.0.0.0:0` and
tells the client which port it got. Video flows host → client, but the **client sends the first
packet** (a hole-punch), so the host learns the client's real source and streams back — this
traverses NAT / inter-VLAN with no forwarded port. **You normally don't open it:** if a deny-inbound
firewall drops the punch, the host waits ~2.5 s and falls back to the client-reported address, and a
stateful firewall then admits the return (it just adds ~2.5 s to session start). To skip that delay,
pin it with **`serve --data-port <PORT>`** (or `PUNKTFUNK_DATA_PORT`): the host binds that fixed
port and streams direct (no punch-wait) — open exactly that one port. A fixed port serves one
session at a time (concurrent ones fall back to random + hole-punch), and direct mode needs the
client's reported address to be reachable (flat LAN / a non-remapping port-forward).
And the **GameStream / Moonlight** ports (fixed) — only needed if you run the host with
`serve --gamestream` (opt-in, trusted LAN only); bare `serve` is native-only and doesn't open these:
| Port | Proto | Purpose |
|---|---|---|
| 47984 | TCP | HTTPS nvhttp (paired, mutual-TLS) |
| 47989 | TCP | HTTP nvhttp (`/serverinfo`, `/pair` PIN flow) |
| 48010 | TCP | RTSP handshake |
| 4799848010 | UDP | Video RTP (+ FEC), ENet control (47999), audio (48000) |
| 5353 | UDP | mDNS auto-discovery |
The mgmt API (TCP 47990, HTTPS + mTLS) binds all interfaces by default so paired clients can browse the
game library — the `punktfunk-native` profile opens it. Off-loopback it serves only read-only
status/library to a paired client cert; the admin surface stays loopback-only. Pass
`--mgmt-bind 127.0.0.1:47990` to keep it loopback-only (then leave 47990 closed).
With `ufw` (explicit ports, instead of the shipped profile):
```sh
sudo ufw allow 9777/udp # punktfunk/1 control plane
sudo ufw allow 47990/tcp # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired)
sudo ufw allow 47984/tcp && sudo ufw allow 47989/tcp && sudo ufw allow 48010/tcp
sudo ufw allow 47998,47999,48000/udp # GameStream video/control/audio
sudo ufw allow 5353/udp # mDNS discovery
# The punktfunk/1 data plane uses a random UDP port; leave it closed on a LAN — the host hole-punches
# and falls back (~2.5s at session start if firewalled). To skip that, pin it: `serve --data-port
# 9778` and `ufw allow 9778/udp`.
```
With raw `nftables` (add to your `inet filter input` chain):
```
udp dport 9777 accept # punktfunk/1 control plane
tcp dport 47990 accept # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired)
tcp dport { 47984, 47989, 48010 } accept
udp dport { 47998-48010, 5353 } accept
# The punktfunk/1 data plane is a random UDP port — normally left closed (hole-punch + ~2.5s
# fallback). Pin it with `serve --data-port <PORT>` to open exactly one instead.
```
Debian ships no firewall and Ubuntu's `ufw` is installed-but-inactive by default, so out of the
box there is nothing to open. The package ships openers for both — ufw profiles at
`/etc/ufw/applications.d/punktfunk` and firewalld service definitions at
`/usr/lib/firewalld/services/` (neither auto-enabled). The commands, per-port breakdown and the
data-plane hole-punch behavior are documented once:
[Debian](https://docs.punktfunk.unom.io/docs/debian) for the openers, port facts in
[`data/platforms.json`](../../data/platforms.json) and on
[Moonlight](https://docs.punktfunk.unom.io/docs/moonlight), hole-punch mechanics in
[Troubleshooting](https://docs.punktfunk.unom.io/docs/troubleshooting).
## Updates
+6 -20
View File
@@ -33,26 +33,12 @@ flatpak fallback).
## Install (recommended): the hosted repo
One command adds the signed `unom` remote and installs the client; it auto-adds Flathub for the
GNOME runtime, and `flatpak update` tracks new builds from then on:
```sh
flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref
flatpak run io.unom.Punktfunk
```
Equivalent two-step (add the whole remote, then install by app id):
```sh
flatpak remote-add --user --if-not-exists unom https://flatpak.unom.io/unom.flatpakrepo
flatpak install --user unom io.unom.Punktfunk
```
Updates — the whole point of the hosted repo:
```sh
flatpak update # or: flatpak update io.unom.Punktfunk
```
The user-facing walkthrough (the `.flatpakref` one-liner, updates) lives on the docs pages —
[Install a client](https://docs.punktfunk.unom.io/docs/install-client) /
[Steam Deck](https://docs.punktfunk.unom.io/docs/steam-deck) — stated once so it can't drift (see
"Where facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)). Mechanically: the flatpakref
adds the signed `unom` remote, auto-adds Flathub for the GNOME runtime, and `flatpak update`
tracks new builds from then on.
## Install on the Deck via the bundle (no-remote fallback)
+11 -21
View File
@@ -17,28 +17,18 @@ This is the same package as the [COPR](../copr/README.md) / [bootc](../bootc/Con
paths — same spec (`punktfunk.spec`) — just self-hosted in Gitea instead of COPR, mirroring the
[Debian/apt](../debian/README.md) setup.
## Install on a Bazzite host (one-time)
## Install on a host (one-time)
```sh
# Add the repo. Packages are GPG-signed (gpgcheck=1, the packages@unom.io key) AND the repo
# metadata is Gitea-signed (repo_gpgcheck=1); gpgkey lists both so dnf/rpm-ostree imports each.
sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO'
[gitea-unom-bazzite]
name=punktfunk (unom, Bazzite)
baseurl=https://git.unom.io/api/packages/unom/rpm/bazzite
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key
https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk
REPO
# Layer the host + the web console (pairing/status), then reboot into the new deployment.
# (punktfunk Recommends punktfunk-web; list it explicitly so it's pulled regardless of weak-dep
# settings. The registry carries punktfunk-web because CI builds the spec --with web; COPR can't.)
rpm-ostree install punktfunk punktfunk-web
systemctl reboot
```
The user-facing walkthrough — the `.repo` file (same shape for the `fedora-NN` and `bazzite`
baseurl groups) and the install command — lives on the docs pages
([Fedora](https://docs.punktfunk.unom.io/docs/fedora) /
[Bazzite](https://docs.punktfunk.unom.io/docs/bazzite), where the sysext, not layering, is the
supported default), stated once so it can't drift (see "Where facts live" in
[`CONTRIBUTING.md`](../../CONTRIBUTING.md)). Packager notes: packages are GPG-signed
(`gpgcheck=1`, the packages@unom.io key) AND the repo metadata is Gitea-signed
(`repo_gpgcheck=1`) — the `gpgkey` line lists both so dnf/rpm-ostree imports each; on a layered
box list `punktfunk-web` explicitly (weak-dep settings vary), and the registry — not COPR —
carries it because CI builds the spec `--with web` (COPR's chroot has no `bun`).
> If `rpm-ostree` can't complete the metadata GPG check non-interactively, set `repo_gpgcheck=0`
> (TLS-only trust to the self-hosted registry).
+102
View File
@@ -0,0 +1,102 @@
#!/bin/sh
# Docs drift gates (docs-and-onboarding-overhaul WP1). The docs rotted through four unchecked
# duplication channels; these are the cheap textual halves of closing them. No cargo, no bun
# install — pure git grep, so the gate runs on every push in seconds (the deep half, regenerating
# the OpenAPI spec from the host binary, lives in ci.yml's `rust` job where the build already
# exists).
#
# 1. docs-site/public/openapi.json must be a byte-for-byte copy of api/openapi.json. The
# snapshot is a manual `cp` (docs-site/README.md); it sat stale on main for weeks once,
# publishing an /api reference missing the whole self-update surface.
# 2. Every PUNKTFUNK_* variable the docs mention must still exist somewhere in the tree
# (docs describing a removed knob is drift a reader pays for). Historical records —
# docs/releases/, CHANGELOG.md — don't count as existence: a knob that lives only in old
# release notes is gone.
# 3. Ratchet: the count of PUNKTFUNK_* env vars in code that the docs never mention must not
# grow. 300+ internal/debug knobs are deliberately undocumented today, so this can't be a
# hard list — but a NEW knob must either be documented in docs-site (configuration.md or
# the page owning its feature) or the baseline below raised in the same commit, making
# "undocumented" a decision instead of an accident. Shrink the gap? Lower the baseline.
# 4. Every command the host-cli.md tables list must still exist as a string literal in
# crates/punktfunk-host (same "docs describing removed things" class as gate 2).
# 5. data/platforms.json (the single source for install/port facts that docs, the website
# download page and the guided installer consume) must parse.
#
# Textual gates, so textual limits: gate 2/3 match token spelling, not env reads — a var name in
# a code comment counts as "exists", and a quoted constant that isn't an env var counts toward
# the ratchet. Both err toward false calm on removal and a one-line baseline bump on addition,
# which is the cheap side to be wrong on.
set -u
LC_ALL=C
export LC_ALL
cd "$(dirname "$0")/../.." || exit 2
fail=0
tmp="${TMPDIR:-/tmp}/docs-drift.$$"
mkdir -p "$tmp"
trap 'rm -rf "$tmp"' EXIT
# ---------------------------------------------------------------- gate 1: openapi snapshot
if [ "$(cksum < api/openapi.json)" != "$(cksum < docs-site/public/openapi.json)" ]; then
echo "::error::docs-site/public/openapi.json is not a copy of api/openapi.json — re-sync it:"
echo " cp api/openapi.json docs-site/public/openapi.json"
fail=1
fi
# ---------------------------------------------------------------- gate 2: docs env vars exist
git grep -ohE 'PUNKTFUNK_[A-Z0-9_]+' -- docs-site/content | sort -u > "$tmp/docs-vars"
while IFS= read -r var; do
if ! git grep -qF "$var" -- ':!docs-site' ':!docs/releases' ':!CHANGELOG.md'; then
echo "::error::docs-site documents $var but nothing outside the docs mentions it — the knob was removed or renamed; fix the docs page"
fail=1
fi
done < "$tmp/docs-vars"
# ---------------------------------------------------------------- gate 3: undocumented ratchet
# Quoted occurrences only: env reads are string literals ("PUNKTFUNK_X"); bare identifiers are
# Rust constants / C symbols, not knobs. The committed baseline enumerates today's undocumented
# set so a violation names exactly the new knob.
baseline=scripts/ci/docs-undocumented-env-baseline.txt
sort -u "$baseline" > "$tmp/baseline" # re-sort under our LC_ALL=C, whatever locale wrote it
git grep -ohE '"PUNKTFUNK_[A-Z0-9_]+"' -- ':!docs-site' ':!*.md' | tr -d '"' | sort -u > "$tmp/code-vars"
comm -23 "$tmp/code-vars" "$tmp/docs-vars" > "$tmp/undocumented"
comm -23 "$tmp/undocumented" "$tmp/baseline" > "$tmp/new-undocumented"
if [ -s "$tmp/new-undocumented" ]; then
echo "::error::new PUNKTFUNK_* vars are neither documented in docs-site nor in the baseline:"
sed 's/^/ /' "$tmp/new-undocumented"
echo "Document each in docs-site (configuration.md or the page owning the feature), or — for a deliberately internal knob — add it to $baseline in the same commit."
fail=1
fi
comm -13 "$tmp/undocumented" "$tmp/baseline" > "$tmp/stale-baseline"
if [ -s "$tmp/stale-baseline" ]; then
echo "baseline entries no longer undocumented (removed or now documented) — prune them from $baseline:"
sed 's/^/ /' "$tmp/stale-baseline"
fi
# ---------------------------------------------------------------- gate 4: documented CLI exists
# First table cell of every row in host-cli.md: subcommands, sub-actions and flags. Multi-word
# cells (flag + argument) are skipped — they don't map to one string literal.
grep -E '^\|' docs-site/content/docs/host-cli.md | awk -F'|' '{print $2}' \
| grep -oE '`[a-z0-9-]+`|`--[a-z-]+`' | tr -d '`' | sort -u > "$tmp/cli-cmds"
while IFS= read -r cmd; do
if ! git grep -qF "\"$cmd\"" -- crates/punktfunk-host; then
echo "::error::host-cli.md documents \`$cmd\` but crates/punktfunk-host has no \"$cmd\" literal — removed or renamed; fix the docs page"
fail=1
fi
done < "$tmp/cli-cmds"
# ---------------------------------------------------------------- gate 5: platforms.json parses
# Explicit try/exit: `bun -e` (1.3.x) exits 0 on an uncaught JSON.parse throw, so relying on
# the default uncaught-exception exit code silently disarms the gate.
json_check='try{JSON.parse(require("fs").readFileSync("data/platforms.json","utf8"))}catch(e){console.error(e.message);process.exit(1)}'
if [ ! -f data/platforms.json ]; then
echo "::error::data/platforms.json is missing"
fail=1
elif command -v bun >/dev/null 2>&1; then
bun -e "$json_check" || { echo "::error::data/platforms.json is not valid JSON"; fail=1; }
elif command -v node >/dev/null 2>&1; then
node -e "$json_check" || { echo "::error::data/platforms.json is not valid JSON"; fail=1; }
fi
exit "$fail"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# Internal-link checker (docs-and-onboarding-overhaul WP1). Two classes, both cheap and exact:
#
# 1. Docs-site pages linking other docs-site pages: every `](/docs/<slug>…)` markdown link and
# `href="/docs/<slug>…"` attribute in docs-site/content must resolve to a content page.
# Fumadocs 404s these at runtime only — a renamed page leaves silent dead links behind.
# 2. Relative file links in the repo's markdown (READMEs, CONTRIBUTING, docs/): the target file
# must exist in the tree. Same failure mode: a moved file, a dead link, no CI signal.
#
# Deliberately NOT checked: external URLs (flaky third-party servers must not gate pushes),
# `#anchor` fragments (heading anchors need a markdown renderer to verify; ponytail: add if dead
# anchors actually bite), and site-absolute non-/docs paths like /api (three of them, all routes
# in docs-site/src). Historical records — docs/releases/, CHANGELOG.md — are exempt from class 2:
# they describe the tree as it was.
set -u
LC_ALL=C
export LC_ALL
cd "$(dirname "$0")/../.." || exit 2
fail=0
tmp="${TMPDIR:-/tmp}/docs-links.$$"
mkdir -p "$tmp"
trap 'rm -rf "$tmp"' EXIT
# ---------------------------------------------------------------- class 1: /docs/* page links
git ls-files 'docs-site/content/docs' > "$tmp/pages"
git grep -ohE '\]\(/docs[^)]*\)|href="/docs[^"]*"' -- docs-site/content \
| sed -e 's/^](\(.*\))$/\1/' -e 's/^href="\(.*\)"$/\1/' \
| sed -e 's/[#?].*$//' | sort -u > "$tmp/links"
while IFS= read -r link; do
slug=${link#/docs}
slug=${slug#/}
slug=${slug%/}
if [ -z "$slug" ]; then
target="docs-site/content/docs/index"
else
target="docs-site/content/docs/$slug"
fi
if ! grep -qxE "$target\.(md|mdx)|$target/index\.(md|mdx)" "$tmp/pages"; then
echo "::error::dead docs link: $link (no $target.md/.mdx) — referenced from:"
git grep -lF "$link" -- docs-site/content | sed 's/^/ /'
fail=1
fi
done < "$tmp/links"
# ---------------------------------------------------------------- class 2: relative file links
# Vendored trees are third-party docs describing their upstream repo, not ours (same exclusion
# as check-unsafe-hygiene.sh).
git ls-files '*.md' ':!docs-site/content' ':!docs/releases' ':!CHANGELOG.md' \
':!clients' ':!*/vendor/*' > "$tmp/mdfiles"
while IFS= read -r f; do
dir=$(dirname "$f")
# Markdown links whose target is a plain relative path: skip URLs (://), mailto:, pure
# anchors, site-absolute paths, and anything with spaces or template syntax.
grep -oE '\]\([^)]+\)' "$f" 2>/dev/null | sed 's/^](\(.*\))$/\1/' | sed 's/#.*$//' \
| grep -vE '^$|://|^mailto:|^/|[ $]' | sort -u > "$tmp/rels" || true
while IFS= read -r rel; do
if [ ! -e "$dir/$rel" ]; then
echo "::error::$f links $rel — no such file"
fail=1
fi
done < "$tmp/rels"
done < "$tmp/mdfiles"
exit "$fail"
@@ -0,0 +1,330 @@
PUNKTFUNK_444_FULLRANGE
PUNKTFUNK_ABI_VERSION
PUNKTFUNK_ACCESS_EXPIRED_CLOSE_CODE
PUNKTFUNK_AMF_BENCH
PUNKTFUNK_AMF_USAGE
PUNKTFUNK_APP_EXITED_CLOSE_CODE
PUNKTFUNK_AUDIO_BITS_16
PUNKTFUNK_AUDIO_BITS_24
PUNKTFUNK_AUDIO_CODEC_FLAC_RESERVED
PUNKTFUNK_AUDIO_CODEC_OPUS
PUNKTFUNK_AUDIO_CODEC_PCM
PUNKTFUNK_AUDIO_FRAME_MS
PUNKTFUNK_AUDIO_FRAME_US_LADDER
PUNKTFUNK_AUDIO_MAGIC
PUNKTFUNK_AUDIO_PCM_HEADER
PUNKTFUNK_AUDIO_PCM_HEADER_LEN
PUNKTFUNK_AUDIO_PCM_MAGIC
PUNKTFUNK_AUDIO_RED_HEADER
PUNKTFUNK_AUDIO_RED_MAGIC
PUNKTFUNK_AUDIO_SAMPLE_RATE_HZ
PUNKTFUNK_AUDIO_SINK
PUNKTFUNK_AUDIO_SOURCE
PUNKTFUNK_AUTOCONNECT
PUNKTFUNK_AU_FAULT
PUNKTFUNK_AXIS_LS_X
PUNKTFUNK_AXIS_LS_Y
PUNKTFUNK_AXIS_LT
PUNKTFUNK_AXIS_RS_X
PUNKTFUNK_AXIS_RS_Y
PUNKTFUNK_AXIS_RT
PUNKTFUNK_BILINEAR_LUMA
PUNKTFUNK_BITRATE_KBPS
PUNKTFUNK_BTN_A
PUNKTFUNK_BTN_B
PUNKTFUNK_BTN_BACK
PUNKTFUNK_BTN_DPAD_DOWN
PUNKTFUNK_BTN_DPAD_LEFT
PUNKTFUNK_BTN_DPAD_RIGHT
PUNKTFUNK_BTN_DPAD_UP
PUNKTFUNK_BTN_GUIDE
PUNKTFUNK_BTN_LB
PUNKTFUNK_BTN_LS_CLICK
PUNKTFUNK_BTN_MISC1
PUNKTFUNK_BTN_PADDLE1
PUNKTFUNK_BTN_PADDLE2
PUNKTFUNK_BTN_PADDLE3
PUNKTFUNK_BTN_PADDLE4
PUNKTFUNK_BTN_RB
PUNKTFUNK_BTN_RS_CLICK
PUNKTFUNK_BTN_START
PUNKTFUNK_BTN_TOUCHPAD
PUNKTFUNK_BTN_X
PUNKTFUNK_BTN_Y
PUNKTFUNK_CAPTURE_HDR_PTS
PUNKTFUNK_CHROMA_IDC_420
PUNKTFUNK_CHROMA_IDC_444
PUNKTFUNK_CIPHER_AES_128_GCM
PUNKTFUNK_CIPHER_CHACHA20_POLY1305
PUNKTFUNK_CLIENT_10BIT
PUNKTFUNK_CLIENT_444
PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES
PUNKTFUNK_CLIENT_CAP_AUDIO_RED
PUNKTFUNK_CLIENT_CAP_CURSOR
PUNKTFUNK_CLIENT_CAP_PHASE_LOCK
PUNKTFUNK_CLIENT_CHACHA20
PUNKTFUNK_CLIENT_PEAK_NITS
PUNKTFUNK_CLIP_CANCELLED_CODE
PUNKTFUNK_CLIP_CHUNK
PUNKTFUNK_CLIP_FETCH_CAP
PUNKTFUNK_CLIP_FETCH_DENIED
PUNKTFUNK_CLIP_FETCH_OK
PUNKTFUNK_CLIP_FETCH_STALE
PUNKTFUNK_CLIP_FETCH_UNAVAILABLE
PUNKTFUNK_CLIP_FILE_INDEX_NONE
PUNKTFUNK_CLIP_FLAG_FILES
PUNKTFUNK_CLIP_MAX_KINDS
PUNKTFUNK_CLIP_MAX_MIME
PUNKTFUNK_CLIP_POLICY_FILES
PUNKTFUNK_CLIP_POLICY_TEXT
PUNKTFUNK_CLIP_REASON_BACKEND_UNAVAILABLE
PUNKTFUNK_CLIP_REASON_NOT_PERMITTED
PUNKTFUNK_CLIP_REASON_NO_FILES
PUNKTFUNK_CLIP_REASON_OK
PUNKTFUNK_CLIP_REASON_POLICY_DISABLED
PUNKTFUNK_CLIP_REASON_TAKEN_OVER
PUNKTFUNK_CLIP_STREAM_KIND_FETCH
PUNKTFUNK_CODEC_AV1
PUNKTFUNK_CODEC_H264
PUNKTFUNK_CODEC_HEVC
PUNKTFUNK_CODEC_PYROWAVE
PUNKTFUNK_CORE_H
PUNKTFUNK_CURSOR_RELATIVE_HINT
PUNKTFUNK_CURSOR_SHAPE_MAX_SIDE
PUNKTFUNK_CURSOR_STATE_MAGIC
PUNKTFUNK_CURSOR_VISIBLE
PUNKTFUNK_D3D11_NO_MUTEX
PUNKTFUNK_DEBUG_RECONFIGURE
PUNKTFUNK_DRAWABLE_COUNT
PUNKTFUNK_DRM_CARD
PUNKTFUNK_DUALSENSE_USBIP_GRACE_MS
PUNKTFUNK_DUMP_VIDEO
PUNKTFUNK_FAKE_LIBRARY
PUNKTFUNK_FEATURE_QUIC
PUNKTFUNK_FFWIN_POLL_MS
PUNKTFUNK_FLAG_EOF
PUNKTFUNK_FLAG_PIC
PUNKTFUNK_FLAG_PROBE
PUNKTFUNK_FLAG_SOF
PUNKTFUNK_FORCE_GAMEPAD_UI
PUNKTFUNK_FORCE_SHM
PUNKTFUNK_FRAME_DRIVEN
PUNKTFUNK_FRAME_LATENCY
PUNKTFUNK_GAMESCOPE_BIND
PUNKTFUNK_GAMESCOPE_WSI
PUNKTFUNK_GAMESCOPE_WSI_DISABLE
PUNKTFUNK_GATE_DEPTH
PUNKTFUNK_GRANT_ALL
PUNKTFUNK_GRANT_CLIPBOARD
PUNKTFUNK_GRANT_GAMEPAD
PUNKTFUNK_GRANT_KEYBOARD
PUNKTFUNK_GRANT_LAUNCH
PUNKTFUNK_GRANT_MIC
PUNKTFUNK_GRANT_POINTER
PUNKTFUNK_GRANT_PRESET_CONTROLLER_ONLY
PUNKTFUNK_GRANT_PRESET_FULL
PUNKTFUNK_GRANT_PRESET_VIEW_ONLY
PUNKTFUNK_GRANT_RESERVED
PUNKTFUNK_GUESS_PIN
PUNKTFUNK_GUESS_PORT
PUNKTFUNK_HDR10
PUNKTFUNK_HDR_META_BODY_LEN
PUNKTFUNK_HDR_META_MAGIC
PUNKTFUNK_HELLO_LAUNCH_MAX
PUNKTFUNK_HELLO_NAME_MAX
PUNKTFUNK_HIDOUT_MAGIC
PUNKTFUNK_HID_RAW_FEATURE
PUNKTFUNK_HID_RAW_OUTPUT
PUNKTFUNK_HID_REPORT_MAX
PUNKTFUNK_HOST_CAP_AUDIO_HIRES
PUNKTFUNK_HOST_CAP_AUDIO_RED
PUNKTFUNK_HOST_CAP_CLIPBOARD
PUNKTFUNK_HOST_CAP_CURSOR
PUNKTFUNK_HOST_CAP_GAMEPAD_STATE
PUNKTFUNK_HOST_CAP_PEN
PUNKTFUNK_HOST_CAP_TEXT_INPUT
PUNKTFUNK_HOST_TIMING_MAGIC
PUNKTFUNK_HW_FAULT
PUNKTFUNK_IDD_ADAPTIVE
PUNKTFUNK_INBOUND_REQ_FLAG
PUNKTFUNK_INPUT_DEBUG
PUNKTFUNK_INPUT_MAGIC
PUNKTFUNK_INPUT_TEST_ABS
PUNKTFUNK_INPUT_WIRE_LEN
PUNKTFUNK_INTRA_REFRESH
PUNKTFUNK_IR_PERIOD_FRAMES
PUNKTFUNK_LAUNCH_NOT_PERMITTED_CLOSE_CODE
PUNKTFUNK_LEGACY_STALE_MS
PUNKTFUNK_LOOPBACK_PORT
PUNKTFUNK_LTR_FORCE_AT
PUNKTFUNK_LTR_INTERVAL_FRAMES
PUNKTFUNK_MAX_DATAGRAM_BYTES
PUNKTFUNK_MAX_PADS
PUNKTFUNK_MAX_SCALE
PUNKTFUNK_MGMT_URL
PUNKTFUNK_MIC_ALWAYS_ON
PUNKTFUNK_MIC_MAGIC
PUNKTFUNK_MIN_SCALE
PUNKTFUNK_MIN_SHARD_PAYLOAD
PUNKTFUNK_MIN_STREAM_BLOCK_SHARDS
PUNKTFUNK_MODE
PUNKTFUNK_MSG_ACCESS_UPDATE
PUNKTFUNK_MSG_BITRATE_CHANGED
PUNKTFUNK_MSG_CLIP_CONTROL
PUNKTFUNK_MSG_CLIP_FETCH
PUNKTFUNK_MSG_CLIP_FETCH_HDR
PUNKTFUNK_MSG_CLIP_OFFER
PUNKTFUNK_MSG_CLIP_STATE
PUNKTFUNK_MSG_CLOCK_ECHO
PUNKTFUNK_MSG_CLOCK_PROBE
PUNKTFUNK_MSG_CURSOR_RENDER
PUNKTFUNK_MSG_CURSOR_SHAPE
PUNKTFUNK_MSG_LOSS_REPORT
PUNKTFUNK_MSG_PAIR_CHALLENGE
PUNKTFUNK_MSG_PAIR_PROOF
PUNKTFUNK_MSG_PAIR_REQUEST
PUNKTFUNK_MSG_PAIR_RESULT
PUNKTFUNK_MSG_PHASE_REPORT
PUNKTFUNK_MSG_PIPELINE_GAP
PUNKTFUNK_MSG_PROBE_REQUEST
PUNKTFUNK_MSG_PROBE_RESULT
PUNKTFUNK_MSG_RECONFIGURE
PUNKTFUNK_MSG_RECONFIGURED
PUNKTFUNK_MSG_REQUEST_KEYFRAME
PUNKTFUNK_MSG_RFI_REQUEST
PUNKTFUNK_MSG_SET_BITRATE
PUNKTFUNK_MSG_SHARD_PAYLOAD_ACK
PUNKTFUNK_MSG_SHARD_PAYLOAD_CHANGED
PUNKTFUNK_NO_AMF_LTR
PUNKTFUNK_NO_AV_SYNC
PUNKTFUNK_NO_OUTPUT_KEYFRAME_STREAK
PUNKTFUNK_NO_QSV_LTR
PUNKTFUNK_NO_VIRTUAL_MOUSE
PUNKTFUNK_NV12
PUNKTFUNK_NVENC_ASYNC
PUNKTFUNK_NVENC_ASYNC_DEPTH
PUNKTFUNK_NVENC_DIRECT
PUNKTFUNK_NVENC_MAX_SESSIONS
PUNKTFUNK_NVENC_SLICES
PUNKTFUNK_NVENC_STREAM_ORDERED
PUNKTFUNK_NV_PROFILE
PUNKTFUNK_OVERLAY_MASK
PUNKTFUNK_PACE_BURST_KB
PUNKTFUNK_PACE_FACTOR
PUNKTFUNK_PAD_AUDIO_STAMPS
PUNKTFUNK_PAD_CHANNEL_TRUST_MAILBOX
PUNKTFUNK_PAD_SINK_PARENT_CLASS
PUNKTFUNK_PAD_SINK_VOLUME
PUNKTFUNK_PAIRING_PIN
PUNKTFUNK_PAIRING_PORT
PUNKTFUNK_PAIR_APPROVAL_TIMEOUT_CLOSE_CODE
PUNKTFUNK_PAIR_BOUND_OTHER_CLOSE_CODE
PUNKTFUNK_PAIR_DENIED_CLOSE_CODE
PUNKTFUNK_PAIR_NOT_ARMED_CLOSE_CODE
PUNKTFUNK_PAIR_NO_IDENTITY_CLOSE_CODE
PUNKTFUNK_PAIR_RATE_LIMITED_CLOSE_CODE
PUNKTFUNK_PAIR_SUPERSEDED_CLOSE_CODE
PUNKTFUNK_PEN_ANGLE_UNKNOWN
PUNKTFUNK_PEN_BARREL1
PUNKTFUNK_PEN_BARREL2
PUNKTFUNK_PEN_BATCH_MAX
PUNKTFUNK_PEN_DISTANCE_UNKNOWN
PUNKTFUNK_PEN_IN_RANGE
PUNKTFUNK_PEN_PREDICTED
PUNKTFUNK_PEN_SAMPLE_WIRE_LEN
PUNKTFUNK_PEN_TILT_UNKNOWN
PUNKTFUNK_PEN_TOUCHING
PUNKTFUNK_PEN_TOUCH_TIMEOUT_MS
PUNKTFUNK_PF_VDISPLAY_LIVE
PUNKTFUNK_PHASE_LOCK
PUNKTFUNK_PIN_CLOCKS
PUNKTFUNK_PIPEWIRE_NV12
PUNKTFUNK_PRESETS
PUNKTFUNK_PW_FIXED_POD
PUNKTFUNK_PYROWAVE_CHUNK_KIB
PUNKTFUNK_PYROWAVE_STREAMED_AU
PUNKTFUNK_QUIT_CLOSE_CODE
PUNKTFUNK_REANCHOR_MARKS_TO_LIFT
PUNKTFUNK_RECVMSG_X
PUNKTFUNK_REJECT_BUSY_CLOSE_CODE
PUNKTFUNK_REMOTE_COMPOSITOR
PUNKTFUNK_REMOTE_GAMEPAD
PUNKTFUNK_REMOTE_HOST
PUNKTFUNK_REMOTE_PIN
PUNKTFUNK_REMOTE_PORT
PUNKTFUNK_RFI_MAX_RANGE
PUNKTFUNK_RICH_INPUT_MAGIC
PUNKTFUNK_RUMBLE_ENVELOPE
PUNKTFUNK_RUMBLE_IDLE_MS
PUNKTFUNK_RUMBLE_MAGIC
PUNKTFUNK_RUMBLE_TTL_MS
PUNKTFUNK_RUMBLE_V1_LEN
PUNKTFUNK_RUMBLE_V2_LEN
PUNKTFUNK_RUMBLE_V3_LEN
PUNKTFUNK_SDR_COLORSPACE
PUNKTFUNK_SEAL_LANES
PUNKTFUNK_SESSION_LAYOUT
PUNKTFUNK_SETUP_FAILED_CLOSE_CODE
PUNKTFUNK_SHOT_EDITING
PUNKTFUNK_SHOT_HERO
PUNKTFUNK_SHOT_INTERACTIVE
PUNKTFUNK_SHOT_OUT
PUNKTFUNK_SHOT_PALETTE
PUNKTFUNK_SHOT_SCENE
PUNKTFUNK_SHOT_SELFCAPTURE
PUNKTFUNK_SHOT_SETTINGS_PAGE
PUNKTFUNK_SHOT_SETTINGS_SCOPE
PUNKTFUNK_SHOT_SETTLE_MS
PUNKTFUNK_SLICE_STREAM
PUNKTFUNK_SPIKE_HDR
PUNKTFUNK_STALL_PROBES
PUNKTFUNK_STATS_STDOUT
PUNKTFUNK_STEAM_FORCE
PUNKTFUNK_STEAM_REMAP
PUNKTFUNK_STEAM_USBIP
PUNKTFUNK_STREAMED_AU
PUNKTFUNK_SYNTH_NOISE
PUNKTFUNK_TAG_LEN
PUNKTFUNK_TEST_FEEDBACK
PUNKTFUNK_TONEMAP_PEAK
PUNKTFUNK_TRIGGER_EFFECT_MAX
PUNKTFUNK_TXN_PRESENT
PUNKTFUNK_UI_SECURE
PUNKTFUNK_UI_TLS_CERT
PUNKTFUNK_UI_TLS_KEY
PUNKTFUNK_UPDATE_FEED
PUNKTFUNK_USBIP_ATTACH
PUNKTFUNK_USBIP_TRACE
PUNKTFUNK_USER_FLAG_CHUNK_ALIGNED
PUNKTFUNK_USER_FLAG_RECOVERY_ANCHOR
PUNKTFUNK_USER_FLAG_RECOVERY_POINT
PUNKTFUNK_USER_FLAG_SLICE_STREAM
PUNKTFUNK_VAAPI_ASYNC_DEPTH
PUNKTFUNK_VBV_FRAMES
PUNKTFUNK_VERSION
PUNKTFUNK_VIDEO_CAP_10BIT
PUNKTFUNK_VIDEO_CAP_444
PUNKTFUNK_VIDEO_CAP_CHACHA20
PUNKTFUNK_VIDEO_CAP_HDR
PUNKTFUNK_VIDEO_CAP_HOST_TIMING
PUNKTFUNK_VIDEO_CAP_MULTI_SLICE
PUNKTFUNK_VIDEO_CAP_PROBE_SEQ
PUNKTFUNK_VIDEO_CAP_STREAMED_AU
PUNKTFUNK_VK_ADAPTER
PUNKTFUNK_VK_DEVICE
PUNKTFUNK_VK_QUEUE_PRIORITY
PUNKTFUNK_VULKAN_INFLIGHT
PUNKTFUNK_VULKAN_QUALITY
PUNKTFUNK_VULKAN_RC
PUNKTFUNK_VULKAN_RGB_DIRECT
PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT
PUNKTFUNK_WASAPI_LIVE
PUNKTFUNK_WEB_CONSOLE
PUNKTFUNK_WINDOWED_PRESENT
PUNKTFUNK_WIN_SEPARATE
PUNKTFUNK_WIRE_VERSION
PUNKTFUNK_WIRE_VERSION_CLOSE_CODE
PUNKTFUNK_XBOX_BACKEND
PUNKTFUNK_ZEROCOPY_HOLD
PUNKTFUNK_ZEROCOPY_INPROC
PUNKTFUNK_ZEROCOPY_RENDER_NODE