Files
punktfunk/scripts/steamdeck
enricobuehlerandClaude Fable 5 19392918ff feat(gamescope/hdr): HDR is attempted by default and every install path ships the patched gamescope
Field report (RX 5700 / SteamOS): 10-bit HEVC "unsupported" — but the GPU was
never the blocker (RADV probed Main10 fine on that box). The session stayed SDR
because stock gamescope offers no 10-bit PQ capture formats and nothing shipped
our pipewire-hdr build outside the Bazzite sysext, while PUNKTFUNK_GAMESCOPE_HDR
additionally defaulted off everywhere. Close all three gaps:

- PUNKTFUNK_GAMESCOPE_HDR now defaults ON (explicit-off grammar, the flip the
  config comment planned post-canary). Safe by construction: the +pfhdr banner
  probe, managed-spawn term, and client 10-bit cap keep stock-gamescope boxes on
  the exact 8-bit path. The nix module's gamescopeHdr (default true now) only
  controls whether the patched binary is on PATH; it no longer sets the env.
- SteamOS: new scripts/steamdeck/build-gamescope.sh builds the HDR gamescope in
  the same pf2 trixie distrobox (apt dep list validated by a full build in a
  debian:trixie container), installs ~/.local/bin/punktfunk-gamescope, and
  maintains PUNKTFUNK_GAMESCOPE_BIN in host.env ONLY while the binary passes its
  on-glass --version/+pfhdr check — a stale absolute override would break
  session spawning, not just HDR. Wired into install.sh (§3b), update.sh
  (retrofit), and rebuild-check.sh (ldd-probes it like the host binary).
- Arch: build-sysext.sh gains a --gamescope fold-in (banner-verified by
  execution, host-image-only), and punktfunk-host optdepends the companion.
  Deliberately NO CI-published host sysext: per the 2026-07-22 packaging
  verdict a prebuilt SteamOS host breaks on A/B soname bumps (and /var is
  per-partition-set), so arch.yml records that instead — SteamOS hosts use the
  distrobox flow above.

Also fixes a latent bug that made build-sysext.sh fail every invocation as
documented: a literal `}` inside `${1:?usage …{host,client}…}` terminates the
expansion early and corrupts $PKG (caught by the new synthetic-package test,
which also covers the fold-in, the unmarked-gamescope refusal, and the
client-image guard).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:50:31 +02:00
..

punktfunk host on a Steam Deck

Run a punktfunk host on a Steam Deck — stream its Game Mode (or KDE desktop) to other devices. (Streaming to a Deck is the client; use the Flatpak + Decky plugin instead.)

User-facing guide: docs-site → "SteamOS (Host)" (docs-site/content/docs/steamos-host.md). This README is the deep reference for what the scripts do and how to operate them by hand.

Why build on-device (not a package or prebuilt binary)

SteamOS 3 is an immutable, read-only Arch base:

  • No pacman -S for system libs; /usr is read-only and reset on A/B updates.
  • A prebuilt binary is fragile — it links the system FFmpeg/glibc, and a SteamOS update can bump those sonames out from under it (the same class of breakage as the NVIDIA-driver-after-update issue).
  • The host needs unsandboxed /dev/uinput + /dev/uhid, PipeWire, the compositor, and VAAPI — so Flatpak (the normal Deck app channel) doesn't fit. Flatpak/Decky are for the client.

So the host is built natively inside a Debian-trixie distrobox (pf2), chosen because its FFmpeg/glibc ABI matches SteamOS's — the resulting binary runs natively on SteamOS (the container is only the build environment; punktfunk-host is launched directly, not via distrobox enter). A rebuild always matches the running OS. Encode is VAAPI on the Deck's AMD GPU (NVENC on NVIDIA), auto-selected by PUNKTFUNK_ENCODER=auto.

The honest trade-off: on-device building costs a slow first install (~1015 min, ~1 GB of image + toolchain) and adds moving parts (apt mirrors, rustup, bun) — the price of an install that can always chase the OS. Both failure modes of that chase are automated away now: punktfunk-rebuild-check rebuilds when an OS update breaks the binary's links, and the atomic-update keep list preserves the /etc tuning. The eventual lighter-weight alternative is a CI-prebuilt bundle with the volatile libraries (FFmpeg et al.) vendored under an $ORIGIN rpath — OS-update-proof without a toolchain on the device — worth it once SteamOS host volume justifies per-release artifact signing/hosting; the from-source path would stay as the dev/fallback route.

The web console is the one part that stays in the container at runtime: it's a Nitro bun build (bun both builds and runs it — the bun-preset output uses Bun.serve with TLS, serving HTTPS (HTTP/1.1 over TLS) with the host's identity cert), so its service does distrobox enter pf2 -- … bun .output/server/index.mjs. bun is provisioned in the container.

Scripts

Script What it does
install.sh Idempotent installer: ensure the pf2 distrobox + toolchain → build host + web + plugin runner → write config → build the HDR gamescope below → tune sysctl + udev + vhci-hcd + input group and register it on SteamOS's atomic-update keep list (sudo) → install + start punktfunk-host / punktfunk-web systemd user services with linger, plus the rebuild check below.
update.sh Rebuild everything from the current source and restart the services (config + pairings persist). --pull does git pull first. Also retrofits anything a newer install.sh writes (runner, HDR gamescope, keep-list registration, rebuild check) onto older installs.
build-gamescope.sh Build gamescope + the pipewire-hdr patches (packaging/gamescope) in the same distrobox and install it as ~/.local/bin/punktfunk-gamescope, wiring PUNKTFUNK_GAMESCOPE_BIN into host.env — what lets Game Mode stream 10-bit BT.2020 PQ (HDR) instead of 8-bit SDR. Best-effort: a failure warns and the host streams SDR. Content-stamped — a no-op unless packaging/gamescope/ changed or the binary broke.
rebuild-check.sh The post-OS-update self-heal (run by punktfunk-rebuild-check.service before the host at session start): ldd-probes the host binary and the HDR gamescope — milliseconds when healthy, a full update.sh rebuild only when a SteamOS update actually broke library links.
git clone https://git.unom.io/unom/punktfunk ~/punktfunk
bash ~/punktfunk/scripts/steamdeck/install.sh            # PIN pairing required (secure default)
bash ~/punktfunk/scripts/steamdeck/install.sh --open     # trusted LAN: accept unpaired clients
bash ~/punktfunk/scripts/steamdeck/install.sh --no-web   # host only, no web console
bash ~/punktfunk/scripts/steamdeck/install.sh --no-gamestream  # native punktfunk/1 only, no Moonlight surface
bash ~/punktfunk/scripts/steamdeck/update.sh             # after pulling new source

Note: unlike a bare serve (native-only by default), the Deck install enables --gamestream by default so stock Moonlight clients work out of the box; --no-gamestream turns that surface off.

Env overrides: PUNKTFUNK_SRC (source dir, default ~/punktfunk), PUNKTFUNK_BOX (container name, default pf2), PUNKTFUNK_MGMT_PORT (47990), PUNKTFUNK_WEB_PORT (47992).

What gets installed

  • Binary: ~/punktfunk/target-steamos/release/punktfunk-host (built in pf2, run natively).
  • Config: ~/.config/punktfunk/host.env (encoder/compositor) and web.env (generated web login password + session secret). Trust material (cert.pem, mgmt-token, punktfunk1-paired.json) lives here too and persists across updates.
  • Services: ~/.config/systemd/user/punktfunk-host.service (runs serve --gamestream --mgmt-bind 0.0.0.0:47990, + --open if chosen — --gamestream adds the Moonlight-compat planes so the Deck's Game Mode also streams to stock Moonlight; the native punktfunk/1 plane is always on), punktfunk-web.service, punktfunk-rebuild-check.service (post-OS-update self-heal, enabled), and punktfunk-scripting.service (plugin runner, opt-in — enable it once you use plugins/scripts). Linger is enabled so they run without a login session.
  • Plugin runner: the deb's payload laid out user-scoped (read-only /usr can't take the package): wrapper ~/.local/bin/punktfunk-scripting, pinned bun in ~/.local/lib/punktfunk-scripting/, bundle in ~/.local/share/punktfunk-scripting/.
  • HDR gamescope: ~/.local/bin/punktfunk-gamescope (gamescope + the pipewire-hdr patches, built in pf2, run natively — it does not replace the system gamescope; only the sessions the host spawns use it). host.env gains PUNKTFUNK_GAMESCOPE_BIN= pointing at it — a line build-gamescope.sh maintains and removes again if the binary ever stops working, because a stale absolute override would break session spawning, not just HDR. HDR is attempted by default when present; PUNKTFUNK_GAMESCOPE_HDR=0 in host.env forces SDR. Verify with punktfunk-host hdr-probe.
  • System tuning (sudo): /etc/sysctl.d/99-punktfunk-net.conf (32 MB UDP buffers — the #1 high-bitrate lever), /etc/udev/rules.d/60-punktfunk.rules (uinput/uhid access), /etc/modules-load.d/punktfunk.conf (vhci-hcd for the native Deck pad), $USER in the input group — and /etc/atomic-update.conf.d/punktfunk.conf, which registers the three files on SteamOS's atomic-update keep list so A/B OS updates carry them over (verified: without it an update silently strips them — pads degrade to Xbox 360, buffers drop to 208 KB).

Operating

systemctl --user status  punktfunk-host punktfunk-web
journalctl --user -u punktfunk-host -f          # watch sessions / pairing PIN
systemctl --user restart punktfunk-host         # after editing host.env

Pair from the web console (Devices → arm pairing) or directly from a client with the host's PIN. The host advertises over mDNS as _punktfunk._udp, so clients discover it automatically.

Gotchas

  • distrobox required. If missing: curl -sfL https://raw.githubusercontent.com/89luca89/distrobox/main/install | sh -s -- --prefix ~/.local (then ensure ~/.local/bin is on PATH).
  • First build is slow (~1015 min + ~1 GB toolchain/image). Incremental afterwards.
  • No passwordless sudo → the installer skips the sysctl/udev/input steps with a warning; high bitrates will drop packets until you apply 99-punktfunk-net.conf and join input yourself.
  • Game Mode auto-suspend drops the host off the network on idle — disable it (Settings → Power) for a headless host.
  • WiFi tx ceiling ≈ 250 Mbps goodput (a Deck hardware/driver packet-rate limit, band-independent); fine for 1080p/1440p60. A wired dock lifts it.
  • After a SteamOS update nothing should be needed: the /etc tuning survives via the atomic-update keep list, and punktfunk-rebuild-check rebuilds the binary automatically if the new base actually broke its library links (first session start after the update takes the build's few minutes in that case). A manual update.sh remains harmless.