diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4f7f5298..39f652f6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0c3d914..73cbb429 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`). diff --git a/README.md b/README.md index 7a70385e..398016dc 100644 --- a/README.md +++ b/README.md @@ -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://: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)**. diff --git a/data/platforms.json b/data/platforms.json new file mode 100644 index 00000000..d4cc8ac5 --- /dev/null +++ b/data/platforms.json @@ -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" + ] + } + ] + +} diff --git a/docs-site/README.md b/docs-site/README.md index 7e57d041..8cfbdffb 100644 --- a/docs-site/README.md +++ b/docs-site/README.md @@ -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 diff --git a/packaging/README.md b/packaging/README.md index ddb3c7de..a0d79d6f 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -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/ diff --git a/packaging/arch/README.md b/packaging/arch/README.md index 22f12966..6f365bce 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -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 `</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`. diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index 7570a411..a0e1abea 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -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 3–6). +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[-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[-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 `.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. + -**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 4–5). -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://: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 ` (or `PUNKTFUNK_DATA_PORT`) and open exactly that one port with - `firewall-cmd --add-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//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. diff --git a/packaging/debian/README.md b/packaging/debian/README.md index e83c5599..f334c776 100644 --- a/packaging/debian/README.md +++ b/packaging/debian/README.md @@ -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://: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 `** (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 | -| 47998–48010 | 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 ` 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 diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md index d8ab421e..7d8762cb 100644 --- a/packaging/flatpak/README.md +++ b/packaging/flatpak/README.md @@ -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) diff --git a/packaging/rpm/README.md b/packaging/rpm/README.md index eed9bca2..e32e5f9e 100644 --- a/packaging/rpm/README.md +++ b/packaging/rpm/README.md @@ -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). diff --git a/scripts/ci/check-docs-drift.sh b/scripts/ci/check-docs-drift.sh new file mode 100755 index 00000000..27beb360 --- /dev/null +++ b/scripts/ci/check-docs-drift.sh @@ -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" diff --git a/scripts/ci/check-docs-links.sh b/scripts/ci/check-docs-links.sh new file mode 100755 index 00000000..a97d8c85 --- /dev/null +++ b/scripts/ci/check-docs-links.sh @@ -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/…)` markdown link and +# `href="/docs/…"` 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" diff --git a/scripts/ci/docs-undocumented-env-baseline.txt b/scripts/ci/docs-undocumented-env-baseline.txt new file mode 100644 index 00000000..330e3546 --- /dev/null +++ b/scripts/ci/docs-undocumented-env-baseline.txt @@ -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