Commit Graph
6 Commits
Author SHA1 Message Date
enricobuehlerandClaude Opus 5 53ff313046 fix(packaging/gamescope): the shipped compositor starts on SteamOS — a rolling-distro libstdc++ never followed it there
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
ci / web (push) Successful in 52s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 1m8s
ci / docs-site (push) Successful in 2m13s
docker / builders-arm64cross (push) Successful in 10s
docker / deploy-docs (push) Successful in 40s
ci / rust (push) Canceled after 5m49s
ci / rust-arm64 (push) Canceled after 5m47s
arch / build-publish (push) Successful in 15m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m46s
`punktfunk-gamescope-3.16.25.pfhdr2-1` off the pacman repo cannot start on SteamOS 3.8.16:
`/usr/lib/libstdc++.so.6: version 'GLIBCXX_3.4.35' not found`. The Arch container CI builds it in
is on gcc 16.1.1; SteamOS ships libstdc++ 3.4.34 and moves when Valve says so. Nothing else about
the binary was wrong — every other soname resolved on the box, and glibc was never close (it asks
for 2.38 at most against SteamOS's 2.41) — so the one dynamic C++ runtime was the whole reason the
gamescope backend's own most important platform got a package that dies at `--version`.

The C++ runtime therefore goes static, for the same reason wlroots already does: this binary is
built on a rolling distro and has to start on a frozen one. It is safe here because gamescope
links no shared C++ library at all — its NEEDED list is all C, and glslang/SPIRV are build-time
only — so no C++ ABI crosses a shared boundary. Cost is ~1 MB (5.9 → 7.1). The flags are appended
to LDFLAGS rather than passed as `-Dcpp_link_args`, which would replace the value meson derives
from the environment and silently drop makepkg's `-z relro`/`-z now`/`--as-needed`.

A static runtime is invisible in a passing build and only surfaces as a binary that will not start
somewhere else, so the build now asserts it: no `libstdc++` in NEEDED, which needs no version
threshold to check and turns the regression back into a build failure.

Verified by building in `archlinux:base-devel` — the environment arch.yml uses, gcc 16.1.1 and
glibc 2.44, both far newer than the target — and running the result on SteamOS 3.8.16 with nothing
supplied: banner `punktfunk-gamescope version 3.16.25-4-g6bbe157+pfhdr2`, no libstdc++ in NEEDED,
max GLIBC_2.38, every soname resolving. The host's whole HDR gate chain then answers on SteamOS
for the first time (780M / RADV PHOENIX): 10-bit PQ capture offered, cursor painted in-node,
native-plane HDR and GameStream HDR capable both true once `PUNKTFUNK_GAMESCOPE_HDR` is on, false
with the distro's gamescope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:23:35 +02:00
enricobuehlerandClaude Opus 5 c801465469 fix(packaging): every channel that ships punktfunk-gamescope now builds it
The docs already told users where to get it — the Bazzite sysext, an Arch
package, a NixOS option — and none of the three were true. `rpm.yml` built the
sysext without ever passing `--gamescope`, `arch.yml` did not know the PKGBUILD
existed, and the nix derivation had never been evaluated once.

Evaluating it found its central assumption wrong: `gamescope.unwrapped` does
not exist on current nixpkgs, where `gamescope` IS the buildable derivation, so
the override threw on every build. It now prefers `.unwrapped` where a future
nixpkgs wraps it and checks the RESULT is patchable — `overrideAttrs` on a
symlinkJoin succeeds and does nothing, which would install an UNPATCHED
gamescope under a name the host reads as a promise of HDR.

Both it and the PKGBUILD had also drifted to a stale patch list: two patches
named where three exist, one of them under the level-1 filename retired when
the cursor patch landed. Both read the patch directory now, so the list cannot
go stale again. The PKGBUILD additionally delegates the whole build to
`build-punktfunk-gamescope.sh` rather than re-deriving the meson invocation —
its copy had already lost `force_fallback_for=wlroots`, and unlike Fedora 43,
Arch ships wlroots, so that package would have linked it shared and shipped a
binary that starts only on machines carrying the dev library. It asserts its
own pinned rev matches the script's, the one thing makepkg needs statically.

Both CI builds are cached on `packaging/gamescope/**`, which is the only reason
this is affordable: that tree depends on nothing else in the repo, so a normal
push restores a binary instead of spending ten minutes on someone else's C++.
And both are best-effort. punktfunk works without this binary — SDR on the
gamescope backend, which is what every release before this one did — so a
hiccup building gamescope must not cost the packages those workflows exist to
publish. A failure warns and is never cached, so the next run retries.

`rpm.yml` also installs Fedora's own gamescope for its runtime libraries: the
sysext verifies our binary by executing `--version`, and on a cache hit nothing
else in the job would have pulled libavif/luajit/seatd in.

Verified by evaluating and patch-phase-building the nix derivation against
nixos-unstable: all three patches apply to nixpkgs' already-patched 3.16.25
tree, and the banner stamps +pfhdr2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:03:30 +02:00
enricobuehler 0d2cc65f17 fix(gamescope): force the VENDORED wlroots — the binary's portability was luck
Built the patches on a second distro (Nobara 44 / Fedora 44, for the NVIDIA
leg) and the resulting binary would not start on its own host:

    libwlroots-0.19.so: cannot open shared object file

gamescope vendors wlroots as a submodule, but meson prefers a SYSTEM one when
the build host has `wlroots-devel` — and links it shared. Fedora 43 has no such
package, so the `.116` build fell back to the submodule and linked it
statically; Fedora 44's `dnf builddep gamescope` pulls one in, so the same
script produced a binary bound to a library that exists only inside the build
container.

Confirmed rather than assumed: `ldd` on the f43 binary lists no wlroots at all
and its log says `Subproject wlroots finished`, while the f44 log says
`Dependency wlroots-0.19 found` from the system.

The consequence, had Fedora 43 ever shipped `wlroots-devel`: the sysext payload
would have carried a gamescope that cannot start, and it would have surfaced to
users as "HDR just doesn't work" rather than as a build failure. `-Dforce_
fallback_for=libliftoff,vkroots,wlroots` makes every distro produce the same
self-contained binary. All three entries go together — gamescope's own
meson.build hard-errors if the first two are dropped from that list.

Verified after the fix: `ldd` shows no wlroots/liftoff/vkroots and no missing
deps, the binary starts on the Nobara host, and its node offers the same four
formats with the colorimetry props — same result as Bazzite f43.
2026-07-28 18:03:30 +02:00
enricobuehler 9eb9f74893 fix(gamescope): the patch did not compile on Fedora/Bazzite — three real defects
Built it for the first time, on the AMD Bazzite box (`.116`, RADV 780M,
Fedora 43 toolbox). The patches applied cleanly to the pinned rev and the
configure got all the way through wlroots — then found three things no amount
of reading would have:

**1. `SPA_VIDEO_TRANSFER_SMPTE2084` does not exist on Fedora 43.** PipeWire
1.4.11's transfer-function enum stops at `ADOBERGB`: SPA grew
BT2020_10 / SMPTE2084 / ARIB_STD_B67 as one later block. So the patch simply
did not compile on the distro it is primarily FOR. This is the same trap
punktfunk's own consumer documents and works around — `pf-capture`'s
`pw_pods.rs` spells the value out as `14` for exactly this reason — and the
fix is the same: spell out both colorimetry values, because the enum is wire
ABI mirroring GStreamer's, not a private detail. `SPA_VIDEO_COLOR_PRIMARIES_BT2020`
is present here but gets the same treatment, since its own header comment says
`\since 1.6` and there is no reason to depend on that.

The `PW_CHECK_VERSION(1, 0, 0)` guard was also just wrong: it tests the library
version, which says nothing about which enum members a header names. It now
guards only the FORMAT constants, which are what it was actually right about.

**2. The build pulled in three subprojects we never ship.** gamescope's own
unit tests want Catch2 **v3** (`catch2-with-main`) and Fedora ships v2, so the
configure died on a test suite that is not ours to build. Also disabled: the
OpenVR integration (a code path a headless capture session never enters) and
the WSI layer — which would have been WORSE than wasted work, since the distro
gamescope package installs that same layer and ours would have collided with
it file-for-file.

**3. `dnf builddep gamescope` is not sufficient.** It resolves Fedora's
*packaged* gamescope, which is older than the master we pin, and misses
`xorg-x11-server-Xwayland-devel` — without which wlroots fails several minutes
in with a `xserver.wrap` error that names nothing useful. Documented with the
symptom, so the next person recognises it in one line instead of ten minutes.
2026-07-28 18:03:30 +02:00
enricobuehler fd648aa776 feat(gamescope): put the cursor in the node, so a session can be zero-copy
gamescope keeps the pointer out of its PipeWire node — it lives on a hardware
plane for scanout, and `paint_pipewire()` composites a separate, reduced frame
that never includes it. So punktfunk has always reconstructed it from XFixes
and blended it in host-side.

That blend is what has been blocking the zero-CSC encode path, and the cost is
larger than it sounds: `VulkanVideoEncoder::open` refuses the RGB-direct (EFC)
source for any session with `cursor_blend`, because that front end is
fixed-function and has no blend stage. `cursor_blend_for` sets it
unconditionally for gamescope. Net effect: a gamescope session paid a
full-frame colour-conversion pass per frame, forever, for a pointer.

So put the cursor where it belongs. A second carried gamescope patch adds
`--pipewire-composite-cursor` (off by default — the node has never carried it,
and a consumer that draws its own would get two), painting it with the same
`MouseCursor::paint` call the scanout composite uses. It scales for free:
paint_pipewire has already set `currentOutputWidth/Height` to the capture size,
which is what that function scales against. The repaint test grows the cursor's
state beside the commit ids — a pointer-only move produces no commit, so
without it the composited cursor would freeze on a static screen while the real
one moved, and a cursor that became hidden would never be erased.

Host side, the `+pfhdr` marker becomes a monotonic PATCH LEVEL, so one probe
answers every capability the session must know before it is planned (level 1 =
HDR formats, level 2 = the cursor flag). `cursor_blend_for` and the
`gamescope_cursor` resolver both consult it through one helper, because they
have to agree: the reader without the blend is a wasted X11 connection, the
blend without the reader is a stream with no pointer, and both together with a
gamescope that paints its own would draw two.

⚠ The two indirect spawn modes carry the flag through `PF_HDR_ARGS`, so this
shares a dependency with the HDR flags: a session that ignores
`GAMESCOPE_BIN`/`PATH` and execs the distro's gamescope gets neither. HDR fails
loudly there (negotiation timeout + SDR latch); a missing cursor would be
silent. Noted in packaging/gamescope/README.md as worth a post-spawn
`/proc/<pid>/cmdline` check if it ever bites.
2026-07-28 18:03:30 +02:00
enricobuehler d6818263ce feat(gamescope): a build of gamescope whose capture output can be HDR
gamescope's built-in PipeWire node has always been SDR-only: it offers BGRx
and NV12, and `paint_pipewire()` hardcodes a Gamma-2.2 composite with the SDR
screenshot LUT set. So an HDR game reaches punktfunk already tone-mapped down,
and the whole gamescope backend streams 8-bit no matter what the client can
decode. Games CAN render HDR on a headless gamescope today — only the capture
half is missing.

Carry the two patches that close it (packaging/gamescope/patches): the node
additionally offers `xRGB_210LE`/`xBGR_210LE` with MANDATORY SMPTE ST.2084 +
BT.2020 props, mapped to the same 10-bit capture texture the HDR AVIF
screenshot path already allocates, and `paint_pipewire()` composites into them
with the HDR screenshot LUTs + `EOTF_PQ` — which is exactly the in-tree
`bHDRScreenshot` branch. The new formats are listed LAST, so every existing
consumer keeps negotiating the 8-bit stream bit-for-bit. Offered upstream
against ValveSoftware/gamescope#2126.

Ship it as `punktfunk-gamescope`, beside the distro's own binary rather than
replacing it: a Bazzite sysext payload (`build-sysext.sh --gamescope`, which
refuses a binary without the marker), an Arch package, a nix derivation +
`services.punktfunk.host.gamescopeHdr`, and one distro-agnostic build script
the rest call.

The second patch stamps `+pfhdr1` into the `--version` banner. That is not
cosmetic: punktfunk fixes a session's bit depth in the Welcome, before the
display exists and with no way to take it back, so its capability answer has to
be a static property of the binary it will spawn.
2026-07-28 18:01:20 +02:00