diff --git a/.gitea/workflows/arch.yml b/.gitea/workflows/arch.yml index 51fec22f..f46c1f59 100644 --- a/.gitea/workflows/arch.yml +++ b/.gitea/workflows/arch.yml @@ -210,6 +210,13 @@ jobs: rm -rf dist-gamescope # never cache a failed build (an empty path is not saved) fi + # NOTE deliberately NO sysext image is built or published here: a prebuilt HOST binary on + # SteamOS breaks on the next A/B soname bump (and /var — where sysexts live — is + # per-partition-set), which is the standing packaging verdict behind the on-device + # distrobox build (scripts/steamdeck/, see scripts/steamdeck/README.md). That flow builds + # its own HDR gamescope too. packaging/arch/build-sysext.sh remains a by-hand tool for the + # Deck CLIENT image and for operators who accept the prebuilt-host trade-off. + - name: Publish to the Gitea Arch registry env: TOKEN: ${{ secrets.REGISTRY_TOKEN }} diff --git a/crates/pf-host-config/src/lib.rs b/crates/pf-host-config/src/lib.rs index 11690892..5652add0 100644 --- a/crates/pf-host-config/src/lib.rs +++ b/crates/pf-host-config/src/lib.rs @@ -151,10 +151,12 @@ pub struct HostConfig { /// gamescope's PipeWire node the 10-bit PQ capture formats; the host probes for it and stays /// SDR when it isn't installed, so this knob only decides whether HDR is *attempted*. /// - /// Default OFF for the canary release, then default-on (matching `PUNKTFUNK_10BIT`'s - /// explicit-off grammar). It gates the whole feature — spawn flags included — so an operator - /// who hits a bad interaction can turn the gamescope backend back into exactly today's 8-bit - /// path with one env var and no downgrade path to trip over. + /// Default ON (explicit-off grammar, matching `PUNKTFUNK_10BIT`) since the post-0.22.3 flip: + /// the capability chain behind it (the `+pfhdr` banner probe, managed spawn, the client's + /// 10-bit cap, the per-source downgrade latch) keeps a stock-gamescope box on today's 8-bit + /// path, so the knob's remaining job is the emergency escape hatch — an operator who hits a + /// bad interaction sets `=0` and the gamescope backend is exactly the old SDR path again, + /// spawn flags included. pub gamescope_hdr: bool, /// `PUNKTFUNK_GAMESCOPE_SDR_NITS` — the luminance SDR content is mapped to inside the PQ /// container of an HDR gamescope session (gamescope's `--hdr-sdr-content-nits`, default 400). @@ -270,7 +272,7 @@ impl HostConfig { // its first frames at all; `=0` is the A/B + escape hatch. gamescope_splash: env_on("PUNKTFUNK_GAMESCOPE_SPLASH").unwrap_or(true), // Default OFF for one canary release (design §4 rollout), then flip the `unwrap_or`. - gamescope_hdr: env_on("PUNKTFUNK_GAMESCOPE_HDR").unwrap_or(false), + gamescope_hdr: env_on("PUNKTFUNK_GAMESCOPE_HDR").unwrap_or(true), gamescope_sdr_nits: val("PUNKTFUNK_GAMESCOPE_SDR_NITS") .and_then(|s| s.trim().parse::().ok()) .filter(|n| (1..=10_000).contains(n)), diff --git a/docs-site/content/docs/arch.md b/docs-site/content/docs/arch.md index afec61ae..902324b4 100644 --- a/docs-site/content/docs/arch.md +++ b/docs-site/content/docs/arch.md @@ -49,6 +49,7 @@ printf '\n[punktfunk]\nServer = https://git.unom.io/api/packages/unom/arch/$repo ```sh sudo pacman -Sy punktfunk-host # the streaming host sudo pacman -S punktfunk-web # optional: the browser management console (pairing + status) +sudo pacman -S punktfunk-gamescope # optional: HDR (10-bit BT.2020 PQ) off gamescope sessions sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply) ``` @@ -155,5 +156,7 @@ sudo pacman -U punktfunk-host-*.pkg.tar.zst NVENC/EGL come from the NVIDIA driver (`nvidia-utils`); on a GPU-less builder, symlink the CUDA stub into the link path first (the `PKGBUILD` header documents this). Full details, the -Fedora→Arch dependency map, and the SteamOS systemd-sysext path are in +Fedora→Arch dependency map, and the systemd-sysext mechanism are in [`packaging/arch/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md). +(For a **SteamOS host**, use the [on-device installer](/docs/steamos-host) instead — it builds +the host and the HDR gamescope against the running OS.) diff --git a/docs-site/content/docs/bazzite.md b/docs-site/content/docs/bazzite.md index 3ae4dc44..2b223000 100644 --- a/docs-site/content/docs/bazzite.md +++ b/docs-site/content/docs/bazzite.md @@ -152,9 +152,9 @@ These apply to the **Gaming Mode (gamescope)** path; the KDE Desktop path is una current gamescope is fine; this only bites if you've pinned an old one. - **The mouse cursor isn't included in the captured image** — a gamescope limitation for now. (The KDE Desktop path renders the cursor normally.) -- **HDR needs the punktfunk gamescope build** (`punktfunk-gamescope`, shipped in the sysext) plus - `PUNKTFUNK_GAMESCOPE_HDR=1`. Without it sessions stream SDR — correctly, including SDR versions - of HDR games. +- **HDR needs the punktfunk gamescope build** (`punktfunk-gamescope`, shipped in the sysext) and + is attempted by default when it is present (`PUNKTFUNK_GAMESCOPE_HDR=0` forces SDR). Without + the build sessions stream SDR — correctly, including SDR versions of HDR games. Canonical list: [gamescope → Known limits](/docs/gamescope#known-limits). diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index e1be10c0..afd17b1b 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -59,7 +59,7 @@ the full picture (and [Bazzite](/docs/bazzite) for that distro's specifics). | `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode (headless appliance; no physical session running). | | `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover + capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. | | `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). | -| `PUNKTFUNK_GAMESCOPE_HDR` | `1` · `0` *(default off)* | Allow HDR (10-bit BT.2020 PQ) sessions on the gamescope backend. Needs the `punktfunk-gamescope` build — see [HDR on gamescope](/docs/gamescope#hdr-on-gamescope). Without it, or without the build, sessions stream SDR. | +| `PUNKTFUNK_GAMESCOPE_HDR` | `1` · `0` *(default on)* | Allow HDR (10-bit BT.2020 PQ) sessions on the gamescope backend. Needs the `punktfunk-gamescope` build — see [HDR on gamescope](/docs/gamescope#hdr-on-gamescope); without the build, sessions stream SDR. Set `0` to force SDR. | | `PUNKTFUNK_GAMESCOPE_SDR_NITS` | e.g. `400` | On an HDR gamescope session, the luminance SDR content (desktop, Steam overlay, SDR games) is mapped to inside the PQ container. Unset = gamescope's own default of 400. | | `PUNKTFUNK_GAMESCOPE_BIN` | path | Force a specific gamescope binary for the sessions the host spawns. Unset = prefer `punktfunk-gamescope` on `PATH`, then `gamescope`. | | `PUNKTFUNK_SESSION_WATCH` | `1` · `0` | Follow a Gaming ↔ Desktop switch **mid-stream** (rebuild the backend in place, no reconnect). **On by default** on Bazzite/SteamOS; set `0` to disable. | @@ -92,7 +92,7 @@ See your desktop page ([KDE](/docs/kde), [GNOME](/docs/gnome)) for when to set t | Setting | Values | Meaning | |---|---|---| | `PUNKTFUNK_FEC_PCT` | `N` (percent) | Forward-error-correction redundancy for lossy links (the default is sensible for a normal LAN). Higher = more loss-resilient, more bandwidth. | -| `PUNKTFUNK_10BIT` | `1` · `0` *(default on)* | HEVC Main10 / HDR. **On by default** — the host permits 10-bit; a session goes 10-bit only when the client advertises it (behind the client's HDR setting). Set `0` to force 8-bit. Windows host, plus the Linux **GNOME 50+ GameStream desktop mirror** (`PUNKTFUNK_VIDEO_SOURCE=portal`, mirrored monitor in HDR mode — check with `punktfunk-host hdr-probe`). Also the Linux **gamescope** backend with the `punktfunk-gamescope` build + `PUNKTFUNK_GAMESCOPE_HDR=1`. The other Linux virtual displays (Mutter, KWin, wlroots) stay 8-bit: their virtual-monitor screencasts are SDR-only upstream. | +| `PUNKTFUNK_10BIT` | `1` · `0` *(default on)* | HEVC Main10 / HDR. **On by default** — the host permits 10-bit; a session goes 10-bit only when the client advertises it (behind the client's HDR setting). Set `0` to force 8-bit. Windows host, plus the Linux **GNOME 50+ GameStream desktop mirror** (`PUNKTFUNK_VIDEO_SOURCE=portal`, mirrored monitor in HDR mode — check with `punktfunk-host hdr-probe`). Also the Linux **gamescope** backend with the `punktfunk-gamescope` build (on by default too). The other Linux virtual displays (Mutter, KWin, wlroots) stay 8-bit: their virtual-monitor screencasts are SDR-only upstream. | | `PUNKTFUNK_444` | `1` · `0` *(default on)* | Full-chroma HEVC 4:4:4 (Range Extensions) — sharper text/desktop, no chroma loss. **On by default** on the host; the client's own 4:4:4 setting (default off) is the real switch. Set `0` to force 4:2:0. **punktfunk/1 native only** (Moonlight stays 4:2:0), HEVC-only, honored only when the client advertises 4:4:4 **and** the GPU supports it (probed; NVENC is the validated path — VAAPI/AMF/QSV decline). Independent of 10-bit. | | `PUNKTFUNK_CHACHA20` | `1` · `0` *(default on)* | ChaCha20-Poly1305 session encryption for clients without hardware AES (old ARM TVs, e.g. webOS), lifting their ~100 Mbps software-AES decrypt ceiling. **On by default** on the host; a session uses it only when the client requests it — everyone else stays on AES-GCM. Purely a performance choice (both ciphers are full-strength); set `0` to force AES-GCM for all sessions. | | `PUNKTFUNK_PYROWAVE_MAX_MBPS` | `N` (Mbps) | Cap the [PyroWave](/docs/pyrowave) Automatic bitrate pin, for a host on a link that the open-loop pin can outrun (e.g. 4:4:4 + HDR at 5120×1440@240 pins ~5.3 Gbps, over a 5GbE link). Unset = no cap. Only affects Automatic (bitrate `0`) PyroWave sessions; an explicit client bitrate bypasses it. | diff --git a/docs-site/content/docs/gamescope.md b/docs-site/content/docs/gamescope.md index b93a0d21..e1579b01 100644 --- a/docs-site/content/docs/gamescope.md +++ b/docs-site/content/docs/gamescope.md @@ -113,7 +113,7 @@ a model. See the full [Configuration reference](/docs/configuration) for every o | `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover and capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. | | `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). | | `PUNKTFUNK_SESSION_WATCH` | `1` · `0` | Follow a Gaming ↔ Desktop switch mid-stream (rebuild in place, no reconnect). On by default on Bazzite/SteamOS; set `0` to disable. | -| `PUNKTFUNK_GAMESCOPE_HDR` | `1` · `0` *(default off)* | Allow HDR (10-bit BT.2020 PQ) sessions. Needs `punktfunk-gamescope` — see below. | +| `PUNKTFUNK_GAMESCOPE_HDR` | `1` · `0` *(default on)* | Allow HDR (10-bit BT.2020 PQ) sessions. Needs `punktfunk-gamescope` (SDR otherwise) — see below. Set `0` to force SDR. | | `PUNKTFUNK_GAMESCOPE_SDR_NITS` | e.g. `400` | On an HDR session, how bright SDR content (the desktop, the Steam overlay, an SDR game) is inside the PQ container. gamescope's default is 400. | | `PUNKTFUNK_GAMESCOPE_BIN` | path | Force a specific gamescope binary. Otherwise the host prefers `punktfunk-gamescope` on `PATH`, then `gamescope`. | @@ -130,17 +130,14 @@ To stream real HDR you need `punktfunk-gamescope`: gamescope plus a small patch own name and does **not** replace your system gamescope — your Gaming Mode keeps using that one. - **Bazzite / Fedora Atomic** — included in the punktfunk sysext; `punktfunk-sysext update` gets it. -- **Arch / SteamOS** — the `punktfunk-gamescope` package. -- **NixOS** — `services.punktfunk.host.gamescopeHdr = true;` (it also sets the env var below). +- **Arch** — the `punktfunk-gamescope` package. +- **SteamOS (Steam Deck installer)** — built and wired automatically by + `scripts/steamdeck/install.sh` / `update.sh`. +- **NixOS** — `services.punktfunk.host.gamescopeHdr` (default `true`). - **Anything else** — `bash packaging/gamescope/build-punktfunk-gamescope.sh` from the source tree. -Then turn it on: - -```sh -PUNKTFUNK_GAMESCOPE_HDR=1 -``` - -and check what the host thinks it can do: +HDR is attempted by default once the build is present (`PUNKTFUNK_GAMESCOPE_HDR=0` forces SDR). +Check what the host thinks it can do: ```sh punktfunk-host hdr-probe @@ -186,8 +183,8 @@ These apply to the **Gaming Mode (gamescope)** path only; the desktop path is un - **HDR needs the punktfunk gamescope build.** A stock gamescope's capture output is 8-bit SDR, so sessions stream SDR — correctly, including SDR versions of HDR games. Install `punktfunk-gamescope` (gamescope plus a small patch that teaches its capture node the 10-bit - BT.2020 PQ formats) and set `PUNKTFUNK_GAMESCOPE_HDR=1`, and a 10-bit-capable client streams - true HDR10. See [HDR on gamescope](#hdr-on-gamescope) below. + BT.2020 PQ formats), and a 10-bit-capable client streams true HDR10 — no knob needed. + See [HDR on gamescope](#hdr-on-gamescope) below. To stream the KDE Plasma desktop of a Steam box instead, see [KDE Plasma](/docs/kde). To bring up the web console and pair a client, see [The Web Console](/docs/web-console). diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index dfbc475b..715c120a 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -105,7 +105,7 @@ see [Status & Progress](/docs/status). (`PUNKTFUNK_VIDEO_SOURCE=portal`) negotiates the 10-bit PQ formats and encodes HEVC Main10 BT.2020 PQ. The **gamescope** virtual output is no longer blocked either — the `punktfunk-gamescope` build carries the small patch its capture node was missing, so - `PUNKTFUNK_GAMESCOPE_HDR=1` streams true HDR10 (on-glass validation pending; the patch is + installing it streams true HDR10 by default (on-glass validation pending; the patch is offered upstream as [gamescope#2126](https://github.com/ValveSoftware/gamescope/issues/2126)). What stays blocked upstream is HDR on **Mutter's virtual monitors** — `RecordVirtual` streams are still SDR-only diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md index 93daa2c2..57ca1597 100644 --- a/docs-site/content/docs/status.md +++ b/docs-site/content/docs/status.md @@ -40,7 +40,7 @@ host is newer than the Linux host.) mirror an HDR monitor over the GameStream desktop-capture source (`PUNKTFUNK_VIDEO_SOURCE=portal`): the portal negotiates the 10-bit PQ screencast formats GNOME 50 added and encodes Main10 PQ. And a **gamescope** host can stream HDR off its virtual output - with the `punktfunk-gamescope` build plus `PUNKTFUNK_GAMESCOPE_HDR=1` — games get real HDR + with the `punktfunk-gamescope` build (on by default when installed) — games get real HDR surfaces and the composite is captured as 10-bit BT.2020 PQ. (Run `punktfunk-host hdr-probe` to check either; both are pending on-glass validation.) The **other** Linux virtual displays — Mutter, KWin, wlroots — still stream 8-bit: their virtual-monitor screencasts are SDR-only diff --git a/docs-site/content/docs/steamos-host.md b/docs-site/content/docs/steamos-host.md index 58083657..93137c6e 100644 --- a/docs-site/content/docs/steamos-host.md +++ b/docs-site/content/docs/steamos-host.md @@ -126,6 +126,22 @@ streams it at your client's resolution; in Desktop Mode it streams the KDE deskt auto-detects which session is live per connection. See [Steam / gamescope](/docs/gamescope) for the attach-vs-managed detail and known limits. +## HDR (10-bit BT.2020 PQ) + +The installer also builds `punktfunk-gamescope` — gamescope plus the small patch that adds the +10-bit PQ formats to its capture node (see [HDR on gamescope](/docs/gamescope#hdr-on-gamescope)) — +and points the host at it. Nothing to configure: with it in place, a 10-bit-capable client with +HDR enabled streams true HDR10 from Game Mode; anything missing and the session streams 8-bit SDR +instead (never a mislabelled picture). Check what the host resolved with: + +```sh +~/punktfunk/target-steamos/release/punktfunk-host hdr-probe +``` + +The build is best-effort: if it fails, the installer says so loudly and everything else keeps +working in SDR — re-run `update.sh` to retry. `PUNKTFUNK_GAMESCOPE_HDR=0` in +`~/.config/punktfunk/host.env` forces SDR deliberately. + ## Updating After pulling new source, rebuild and restart in one step (config + pairings persist): diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 706d2c19..92a59534 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -138,6 +138,7 @@ package_punktfunk-host() { optdepends=('pipewire-pulse: PulseAudio-API audio from games/apps (real `pulseaudio` also works)' 'nvidia-utils: NVENC hardware encode + GPU EGL/CUDA zero-copy (REQUIRED to encode on NVIDIA)' 'gamescope: per-session nested compositor backend (no desktop login needed) — needs >=3.16.22' + 'punktfunk-gamescope: HDR (10-bit BT.2020 PQ) streaming on the gamescope backend — attempted by default when installed' 'kwin: stream a KDE Plasma desktop (kwin VirtualDisplay backend)' 'mutter: stream a GNOME desktop (Mutter RecordVirtual backend)' 'sway: stream a wlroots desktop (Sway VirtualDisplay backend)' diff --git a/packaging/arch/README.md b/packaging/arch/README.md index 0c1b4d3b..ebf2cb3a 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -114,28 +114,37 @@ NVENC/EGL come from the NVIDIA driver: `sudo pacman -S --needed nvidia-utils`. A | NVIDIA driver (NVENC/EGL/CUDA) | `nvidia-utils` *(optdepend — never a hard dep)* | | Compositor backends | `gamescope` (≥3.16.22) / `kwin` / `mutter` / `sway` *(optdepends)* | -## SteamOS 3 (immutable) — use a systemd-sysext +## Immutable Arch (SteamOS 3) — the systemd-sysext mechanism SteamOS has a **read-only `/usr` on A/B partitions**, and every OS update reimages the rootfs — -so `steamos-readonly disable` + `pacman` (and flatpak/distrobox) are fragile or unusable for a -host that needs `/dev/uinput`, `/dev/uhid`, the host PipeWire socket, the GPU render node, and the -right to spawn a compositor. The update-survivable, SteamOS-blessed mechanism is a -**systemd-sysext**: an overlay image merged read-only over `/usr` at boot, living in the writable -`/var/lib/extensions/` (so it persists across A/B updates, no readonly-disable). +so `steamos-readonly disable` + `pacman` is fragile for anything that must survive updates. The +SteamOS-blessed overlay mechanism is a **systemd-sysext**: an image merged read-only over `/usr` +at boot, living in the writable `/var/lib/extensions/`. + +> **For a SteamOS HOST this is NOT the supported path** — that is +> [`scripts/steamdeck/install.sh`](../../scripts/steamdeck/) (the on-device distrobox build, +> which also builds the HDR gamescope). A host sysext carries a prebuilt binary that breaks on +> the next SteamOS soname bump, and `/var` — where sysexts live — is per-A/B-partition-set. +> The mechanism below is what the Deck **client** image uses (next section), and an option for +> operators on other immutable Arch derivatives who accept the prebuilt trade-off. Build the package, then wrap its `/usr` payload into a sysext image: ```sh -# 1. build the pacman package (needs an Arch environment / container) +# 1. build the pacman packages (needs an Arch environment / container) cd packaging/arch && PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver -# 2. turn it into a sysext .raw (extracts the package's /usr into an image + extension-release) -bash build-sysext.sh punktfunk-host-*.pkg.tar.zst -# 3. on the SteamOS box: +( cd ../gamescope && makepkg -f -d --holdver ) # optional: the HDR gamescope companion +# 2. turn it into a sysext .raw (extracts the packages' /usr into an image + extension-release); +# --gamescope folds the HDR build into a HOST image (verified by its +pfhdr banner) +bash build-sysext.sh --gamescope ../gamescope/punktfunk-gamescope-*.pkg.tar.zst punktfunk-host-*.pkg.tar.zst +# 3. on the box: sudo cp punktfunk-host.raw /var/lib/extensions/ -sudo systemctl enable --now systemd-sysext # merges it; survives OS updates +sudo systemctl enable --now systemd-sysext # merges it systemctl --user enable --now punktfunk-host # the user unit is now under /usr/lib ``` The udev rule, sysctl, and systemd **user** unit all live under `/usr/lib`, so the merged sysext -exposes them. `systemd-sysext refresh` re-merges after a reboot. +exposes them. `systemd-sysext refresh` re-merges after a reboot. (One HDR nuance of the sysext +path: file capabilities don't survive it, so gamescope runs without `CAP_SYS_NICE` — everything +works, frame pacing is marginally worse than the pacman install, whose `.install` sets the cap.) ## Steam Deck — the client (what the Decky plugin launches) diff --git a/packaging/arch/build-sysext.sh b/packaging/arch/build-sysext.sh index 37884ace..ba63c344 100755 --- a/packaging/arch/build-sysext.sh +++ b/packaging/arch/build-sysext.sh @@ -5,15 +5,34 @@ # disable`. Works for either split package — on a Steam Deck you'd wrap the CLIENT. Needs # `bsdtar`/`tar`, `squashfs-tools` (mksquashfs). # -# Usage: bash build-sysext.sh +# Usage: bash build-sysext.sh [--gamescope ] \ +# # Output: .raw (e.g. punktfunk-client.raw) +# +# --gamescope folds the HDR-capable gamescope companion package (packaging/gamescope) into a HOST +# image as /usr/bin/punktfunk-gamescope — what lets the gamescope backend stream 10-bit BT.2020 PQ +# instead of 8-bit SDR (the host prefers that name on PATH and attempts HDR by default). Mirrors +# the Bazzite image's fold-in, including the honesty check: the binary is verified by executing +# its `+pfhdr` banner, never trusted by filename. Omit it and the image is exactly what it was — +# the host then stays SDR on that backend, by design. (No CAP_SYS_NICE inside the image: file +# capabilities don't survive this squashfs path — gamescope runs without it, pacing slightly +# worse, same as the Bazzite sysext.) set -euo pipefail -PKG="${1:?usage: build-sysext.sh }" +GAMESCOPE="" +if [ "${1:-}" = "--gamescope" ]; then + GAMESCOPE="${2:?--gamescope needs a punktfunk-gamescope package}"; shift 2 +fi +# No braces in the message: a literal `}` inside ${1:?...} terminates the expansion early and +# corrupts $PKG (the tail of the message gets appended to the value — a real field bug). +PKG="${1:?usage: build-sysext.sh [--gamescope ] }" [ -f "$PKG" ] || { echo "no such package: $PKG" >&2; exit 1; } # Derive the package name from the file (pkgname is everything before the -). NAME="$(basename "$PKG" | sed -E 's/-[0-9].*//')" [ -n "$NAME" ] || { echo "could not derive package name from $PKG" >&2; exit 1; } +if [ -n "$GAMESCOPE" ] && [ "$NAME" != "punktfunk-host" ]; then + echo "--gamescope only makes sense for a punktfunk-host image (got: $NAME)" >&2; exit 1 +fi STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT @@ -26,6 +45,25 @@ else tar -C "$STAGE" -xf "$PKG" usr fi +# The HDR gamescope companion (see --gamescope in the header). Verified by its banner marker +# rather than trusted by filename: an unpatched gamescope shipped under this name would make the +# host promise HDR it cannot deliver, and the punktfunk/1 Welcome cannot take that back +# mid-session. Executing the staged binary needs a build box the binary runs on (the Arch CI +# container qualifies; it built it). +if [ -n "$GAMESCOPE" ]; then + [ -f "$GAMESCOPE" ] || { echo "no such package: $GAMESCOPE" >&2; exit 1; } + if command -v bsdtar >/dev/null 2>&1; then + bsdtar -C "$STAGE" -xf "$GAMESCOPE" usr + else + tar -C "$STAGE" -xf "$GAMESCOPE" usr + fi + GS_BIN="$STAGE/usr/bin/punktfunk-gamescope" + [ -x "$GS_BIN" ] || { echo "$GAMESCOPE did not provide usr/bin/punktfunk-gamescope" >&2; exit 1; } + "$GS_BIN" --version 2>&1 | grep -q '+pfhdr' || { + echo "$GAMESCOPE's binary has no +pfhdr marker — it is not a punktfunk HDR build" >&2; exit 1; } + echo "folded in $("$GS_BIN" --version 2>&1 | head -1)" +fi + # The marker systemd-sysext requires to merge the image. ID=_any merges onto ANY host os-release # (SteamOS, Arch, Bazzite); ARCHITECTURE pins it to x86-64 so it's never merged on the wrong arch. install -d "$STAGE/usr/lib/extension-release.d" diff --git a/packaging/bazzite/host.env b/packaging/bazzite/host.env index 53165239..eb58e3c8 100644 --- a/packaging/bazzite/host.env +++ b/packaging/bazzite/host.env @@ -40,9 +40,10 @@ PUNKTFUNK_GAMESCOPE_ATTACH=1 # # HDR (10-bit BT.2020 PQ) on the gamescope path. Needs `punktfunk-gamescope` — gamescope plus the # patch that teaches its capture node the 10-bit formats — which the sysext ships alongside the -# host; it does NOT replace the system gamescope. Off by default while the feature soaks; a host -# without the patched binary stays SDR either way. `punktfunk-host hdr-probe` says what it found. -# PUNKTFUNK_GAMESCOPE_HDR=1 +# host; it does NOT replace the system gamescope. ON by default when that binary is present; a +# host without it stays SDR either way. `punktfunk-host hdr-probe` says what it found. To force +# SDR on the gamescope backend: +# PUNKTFUNK_GAMESCOPE_HDR=0 # # On an HDR session, how bright SDR content (desktop, Steam overlay, SDR games) is inside the PQ # container. gamescope's default is 400 nits; raise it if whites look dim on your TV. diff --git a/packaging/nix/nixos-module.nix b/packaging/nix/nixos-module.nix index 55efeb62..11c12b24 100644 --- a/packaging/nix/nixos-module.nix +++ b/packaging/nix/nixos-module.nix @@ -166,19 +166,16 @@ in gamescopeHdr = mkOption { type = types.bool; - default = false; + default = true; description = '' Put `punktfunk-gamescope` — gamescope carrying punktfunk's `pipewire-hdr` patches — on - the host service's PATH and turn HDR on for the gamescope backend - (`PUNKTFUNK_GAMESCOPE_HDR=1`), so a 10-bit-capable client can stream true HDR10 - (BT.2020 PQ) off a gamescope virtual output. + the host service's PATH, so a 10-bit-capable client can stream true HDR10 (BT.2020 PQ) + off a gamescope virtual output. HDR is attempted by default once the binary is present + (`PUNKTFUNK_GAMESCOPE_HDR=0` in `settings`/`environmentFile` forces SDR). It does NOT replace the system's `gamescope`: the binary has its own name and the host - prefers it only for the sessions it spawns itself. Costs a gamescope build from source. - - Off by default while the feature soaks (design §4 rollout); the host also stays SDR on - its own if the patched binary is somehow absent, so this is a policy switch, not a - promise. + prefers it only for the sessions it spawns itself. Costs a gamescope build from source + — set `false` to skip that build; the host then stays SDR on the gamescope backend. ''; }; @@ -365,9 +362,9 @@ in (optional (cfg.host.settings != { }) "${hostSettingsFile}") ++ (optional (cfg.host.environmentFile != null) "-${toString cfg.host.environmentFile}"); }; - # Layered UNDER `settings`/`environmentFile` (systemd's Environment= loses to a later - # EnvironmentFile), so an explicit `PUNKTFUNK_GAMESCOPE_HDR` in either still wins. - environment = mkIf cfg.host.gamescopeHdr { PUNKTFUNK_GAMESCOPE_HDR = "1"; }; + # No PUNKTFUNK_GAMESCOPE_HDR here: the host defaults it on, and the capability probe on + # the resolved binary keeps a build-less box SDR. `gamescopeHdr` only controls whether + # the patched binary is on PATH; `settings`/`environmentFile` can still set =0 to force SDR. }; }) diff --git a/scripts/steamdeck/README.md b/scripts/steamdeck/README.md index c251f79d..c606d869 100644 --- a/scripts/steamdeck/README.md +++ b/scripts/steamdeck/README.md @@ -40,9 +40,10 @@ serving HTTPS (HTTP/1.1 over TLS) with the host's identity cert), so its service | Script | What it does | |--------|--------------| -| `install.sh` | Idempotent installer: ensure the `pf2` distrobox + toolchain → build host + web + **plugin runner** → write config → 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, keep-list registration, rebuild check) onto older installs. | -| `rebuild-check.sh` | The post-OS-update self-heal (run by `punktfunk-rebuild-check.service` before the host at session start): `ldd`-probes the binary — milliseconds when healthy, a full `update.sh` rebuild only when a SteamOS update actually broke its library links. | +| `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. | ```sh git clone https://git.unom.io/unom/punktfunk ~/punktfunk @@ -74,6 +75,13 @@ default `pf2`), `PUNKTFUNK_MGMT_PORT` (47990), `PUNKTFUNK_WEB_PORT` (47992). - **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` diff --git a/scripts/steamdeck/build-gamescope.sh b/scripts/steamdeck/build-gamescope.sh new file mode 100755 index 00000000..19a71362 --- /dev/null +++ b/scripts/steamdeck/build-gamescope.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# punktfunk — build the HDR-capable `punktfunk-gamescope` for a SteamOS host (called by +# install.sh and update.sh; safe to run by hand). +# +# Stock gamescope offers no 10-bit PQ formats on its PipeWire capture node, so every session on +# the gamescope backend stays 8-bit SDR. This builds gamescope + punktfunk's `pipewire-hdr` +# patches (the ONE canonical recipe, packaging/gamescope/build-punktfunk-gamescope.sh) inside the +# same Debian-trixie distrobox the host is built in, installs it as +# ~/.local/bin/punktfunk-gamescope, and pins the host to it via PUNKTFUNK_GAMESCOPE_BIN in +# host.env. HDR is on by default in the host; with this binary present a 10-bit-capable client +# streams true HDR10 from Game Mode. +# +# BEST-EFFORT BY DESIGN, mirroring the CI packaging paths: streaming works without it (sessions +# stay SDR), so a failure here warns loudly and exits 0 — it must never cost the host install or +# update it runs inside. The one hard rule is honesty at the seams: host.env points at the binary +# ONLY while `--version` proves it runs on SteamOS glass and carries the `+pfhdr` marker; on any +# failure both the binary and the host.env line are removed (a stale absolute override would +# otherwise break gamescope session spawning entirely, which is worse than SDR). +# +# The build clones gamescope fresh each run (submodules included). That costs bandwidth, but it +# only runs when the packaging/gamescope tree changed (pin bump, patch change — tracked by a +# content stamp) or the installed binary stopped working; a no-op re-run is milliseconds. +set -euo pipefail + +log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } +ok() { printf '\033[1;32m ok\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m !!\033[0m %s\n' "$*" >&2; } + +SRC="${PUNKTFUNK_SRC:-$HOME/punktfunk}" +BOX="${PUNKTFUNK_BOX:-pf2}" +GS_BIN="$HOME/.local/bin/punktfunk-gamescope" +STAMP="$HOME/.local/share/punktfunk/gamescope.stamp" +HOST_ENV="$HOME/.config/punktfunk/host.env" +PKGDIR="$SRC/packaging/gamescope" + +# host.env may only name the binary while it verifiably works (see header). sed -i is fine: the +# file is ours (install.sh §3) and the line is one this script wrote. +unwire() { + [ -f "$HOST_ENV" ] || return 0 + if grep -q '^PUNKTFUNK_GAMESCOPE_BIN=' "$HOST_ENV"; then + sed -i '/^# HDR gamescope built by scripts\/steamdeck/d;/^PUNKTFUNK_GAMESCOPE_BIN=/d' "$HOST_ENV" + warn "host.env: removed PUNKTFUNK_GAMESCOPE_BIN (no working punktfunk-gamescope) — sessions stream SDR" + fi +} +wire() { + [ -f "$HOST_ENV" ] || return 0 # install.sh runs this after §3 wrote host.env; by-hand runs may predate it + grep -q '^PUNKTFUNK_GAMESCOPE_BIN=' "$HOST_ENV" && return 0 + printf '\n# HDR gamescope built by scripts/steamdeck/build-gamescope.sh (10-bit BT.2020 PQ capture;\n# rebuilt by update.sh; PUNKTFUNK_GAMESCOPE_HDR=0 forces SDR).\nPUNKTFUNK_GAMESCOPE_BIN=%s\n' "$GS_BIN" >> "$HOST_ENV" + ok "host.env: PUNKTFUNK_GAMESCOPE_BIN=$GS_BIN" +} +# The on-glass truth: the binary must RUN ON STEAMOS (not merely in the build container — this is +# the ABI seam the whole distrobox approach exists for) and carry the +pfhdr banner marker. +verifies() { [ -x "$GS_BIN" ] && "$GS_BIN" --version 2>&1 | head -1 | grep -q '+pfhdr'; } + +[ -d "$PKGDIR/patches" ] || { warn "no $PKGDIR — skipping the HDR gamescope build"; exit 0; } + +# Content stamp over the packaging tree: a pin bump or patch edit re-triggers the build; anything +# else makes this a fast no-op. +want_stamp="$(find "$PKGDIR" -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -d' ' -f1)" +if [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$want_stamp" ] && verifies; then + ok "punktfunk-gamescope up to date ($("$GS_BIN" --version 2>&1 | head -1))" + wire + exit 0 +fi + +log "Building punktfunk-gamescope (HDR 10-bit capture; ~5-10 min, best-effort)" +# gamescope's build deps in the box (trixie names, mirroring packaging/gamescope/PKGBUILD — keep +# the two lists in step). Provisioned here, not in install.sh's main pass, so a dep problem can +# only ever cost this feature. glm/stb come in as meson wraps; wlroots/libliftoff/vkroots/ +# libdisplay-info are vendored submodules — none of those need packages. +if ! distrobox enter "$BOX" -- bash -lc ' +set -e +export DEBIAN_FRONTEND=noninteractive +sudo apt-get update -qq +sudo apt-get install -y -qq --no-install-recommends \ + meson ninja-build cmake glslang-tools \ + libwayland-dev wayland-protocols libxkbcommon-dev \ + libx11-dev libxcomposite-dev libxdamage-dev libxext-dev libxmu-dev \ + libxrender-dev libxres-dev libxtst-dev libxxf86vm-dev libxcursor-dev \ + libxcb-errors-dev libxcb-icccm4-dev libxcb-ewmh-dev \ + libxcb1-dev libxcb-composite0-dev libxcb-render0-dev libxcb-xfixes0-dev \ + libxcb-shm0-dev libxcb-res0-dev libxcb-present-dev libxcb-xinput-dev \ + libxcb-randr0-dev libxcb-dri3-dev libxcb-shape0-dev \ + libcap-dev libdrm-dev libinput-dev libudev-dev libseat-dev \ + libvulkan-dev libglm-dev libpixman-1-dev libeis-dev \ + libavif-dev libdecor-0-dev hwdata libluajit-5.1-dev \ + libpipewire-0.3-dev libspa-0.2-dev libsdl2-dev \ + xwayland liblcms2-dev >/dev/null +' ; then + warn "could not provision gamescope build deps in '$BOX' — sessions stay SDR (re-run update.sh to retry)" + exit 0 +fi +if ! distrobox enter "$BOX" -- bash -lc " +set -e +bash '$PKGDIR/build-punktfunk-gamescope.sh' --prefix \"\$HOME/.local\" --no-setcap +"; then + warn "punktfunk-gamescope failed to build — sessions stay SDR (re-run update.sh to retry)" + unwire + exit 0 +fi + +if verifies; then + # CAP_SYS_NICE is gamescope's scheduling boost — worth it, never required. Filesystem caps + # can't be set from the rootless box, so it happens here on the SteamOS side. + if sudo -n true 2>/dev/null; then + sudo setcap 'CAP_SYS_NICE=eip' "$GS_BIN" 2>/dev/null \ + && ok "setcap CAP_SYS_NICE (frame-pacing boost)" \ + || warn "could not setcap CAP_SYS_NICE (gamescope still works, pacing slightly worse)" + fi + mkdir -p "$(dirname "$STAMP")" + printf '%s' "$want_stamp" > "$STAMP" + wire + ok "punktfunk-gamescope ready: $("$GS_BIN" --version 2>&1 | head -1)" +else + # Built in the box but does not run on SteamOS (an ABI/soname seam) or lacks the marker. A + # broken binary must not linger where PATH or host.env could resolve it. + warn "built binary failed its on-glass check on SteamOS — removing it; sessions stay SDR" + "$GS_BIN" --version 2>&1 | head -3 >&2 || true + rm -f "$GS_BIN" "$STAMP" + unwire +fi +exit 0 diff --git a/scripts/steamdeck/install.sh b/scripts/steamdeck/install.sh index 48539141..11e6295b 100755 --- a/scripts/steamdeck/install.sh +++ b/scripts/steamdeck/install.sh @@ -245,6 +245,14 @@ else [ "$WITH_WEB" = 1 ] && ok "web.env exists (login password unchanged)" fi +# --- 3b. HDR gamescope (punktfunk-gamescope, best-effort) ------------------ +# Stock gamescope offers only 8-bit capture, so Game Mode streams SDR. The punktfunk build adds +# the 10-bit BT.2020 PQ formats, and the host attempts HDR by default the moment it is present +# (PUNKTFUNK_GAMESCOPE_HDR=0 forces SDR). Best-effort by design: on any failure this warns and +# the host streams SDR — see build-gamescope.sh, which also wires PUNKTFUNK_GAMESCOPE_BIN into +# host.env only while the binary provably runs on SteamOS. +PUNKTFUNK_SRC="$SRC" PUNKTFUNK_BOX="$BOX" bash "$SRC/scripts/steamdeck/build-gamescope.sh" + # --- 4. system tuning (needs sudo: UDP buffers + gamepad udev rule + vhci-hcd + input group) -------- log "System tuning (UDP buffers + gamepad rules + vhci-hcd + input group)" # sudo was acquired up front in preflight (SUDO_OK) so this never stalls behind the long build; a diff --git a/scripts/steamdeck/rebuild-check.sh b/scripts/steamdeck/rebuild-check.sh index 3f064eb4..c0f4fb31 100755 --- a/scripts/steamdeck/rebuild-check.sh +++ b/scripts/steamdeck/rebuild-check.sh @@ -18,14 +18,29 @@ export PATH="$HOME/.local/bin:$PATH" SRC="${PUNKTFUNK_SRC:-$HOME/punktfunk}" BIN="$SRC/target-steamos/release/punktfunk-host" +NEED=0 if [ ! -x "$BIN" ]; then echo "punktfunk-host binary missing at $BIN — running a full rebuild" >&2 -elif ! ldd "$BIN" 2>/dev/null | grep -q "not found"; then - exit 0 # every library resolves — nothing to do -else + NEED=1 +elif ldd "$BIN" 2>/dev/null | grep -q "not found"; then echo "punktfunk-host no longer loads after a SteamOS update — its missing libraries:" >&2 ldd "$BIN" 2>/dev/null | grep "not found" >&2 || true echo "rebuilding against the new OS tree (this takes a few minutes; streaming resumes after)" >&2 + NEED=1 fi +# The HDR gamescope companion chases OS libraries the same way. Probe it only when host.env pins +# it (build-gamescope.sh wires that line only while the binary works) — a break here would not +# just lose HDR, it would break gamescope session SPAWNING via the stale absolute override, so it +# rebuilds with the same urgency as the host binary. +GS_BIN="$(sed -n 's/^PUNKTFUNK_GAMESCOPE_BIN=//p' "$HOME/.config/punktfunk/host.env" 2>/dev/null | head -1)" +if [ -n "$GS_BIN" ]; then + if [ ! -x "$GS_BIN" ] || ldd "$GS_BIN" 2>/dev/null | grep -q "not found"; then + echo "punktfunk-gamescope no longer loads after a SteamOS update — rebuilding it" >&2 + NEED=1 + fi +fi + +[ "$NEED" = 0 ] && exit 0 # everything resolves — nothing to do (every normal boot) + exec bash "$SRC/scripts/steamdeck/update.sh" diff --git a/scripts/steamdeck/update.sh b/scripts/steamdeck/update.sh index 3247594f..ac163598 100755 --- a/scripts/steamdeck/update.sh +++ b/scripts/steamdeck/update.sh @@ -52,6 +52,12 @@ sed 's|^ExecStart=.*|ExecStart=%h/.local/bin/punktfunk-scripting|' \ systemctl --user daemon-reload ok "plugin runner rebuilt (opt-in service: systemctl --user enable --now punktfunk-scripting)" +# HDR gamescope (punktfunk-gamescope): rebuild when the packaging tree changed or the installed +# binary stopped working — also RETROFITS it onto older installs that predate it (fast no-op +# otherwise). Best-effort; on failure the host streams SDR (see build-gamescope.sh). +log "HDR gamescope (punktfunk-gamescope)" +PUNKTFUNK_SRC="$SRC" PUNKTFUNK_BOX="$BOX" bash "$SRC/scripts/steamdeck/build-gamescope.sh" + # Retrofit the post-OS-update rebuild check (install.sh §5) onto older installs: probes the host # binary with ldd at session start and re-runs this script when a SteamOS update broke its links. if [ ! -f "$HOME/.config/systemd/user/punktfunk-rebuild-check.service" ]; then