Commit Graph
8 Commits
Author SHA1 Message Date
enricobuehler eb8c943572 feat(packaging): ship punktfunk-gamescope on RPM and apt too
ci / web (pull_request) Successful in 1m4s
ci / bun-nix (pull_request) Successful in 26s
apple / swift (pull_request) Successful in 1m40s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 2m3s
ci / docs-site (pull_request) Successful in 1m41s
ci / rust-arm64 (pull_request) Successful in 2m44s
android / android (pull_request) Successful in 4m50s
nix / flake (pull_request) Failing after 16m12s
Until now the patched gamescope reached exactly four kinds of box: the
Bazzite/Fedora-Atomic sysext, the Arch package, the SteamOS installer and a NixOS
option. Everyone else was told to build gamescope from source. A traditional
Fedora-family box — Nobara, plain Fedora, the HTPCs people actually stream from —
therefore ran stock gamescope by default, which streams SDR, cursorless, and
tells every game its display is 60 Hz. That is not a user error; there was no
package to install.

Both new packages REPACK the binary CI already builds rather than building
gamescope again: it is a ~10-minute meson compile of an unrelated tree, cached
per distro base because the binary is soname-coupled to it. The Arch PKGBUILD
stays the one recipe that builds from source, because that is what makepkg is
for.

- packaging/gamescope/punktfunk-gamescope.spec + build-gamescope-rpm.sh. Version
  is derived from the binary's own banner (3.16.25.pfhdr4) — the only source that
  cannot drift from what is in the package. rpmbuild's automatic ELF Requires are
  what stop an f43 build installing on f44.
- packaging/debian/build-gamescope-deb.sh, same shape, with dpkg-shlibdeps for
  Depends.
- rpm.yml packages and publishes it beside the host RPMs; deb.yml gains a cached
  gamescope build (keyed on packaging/gamescope/** alone) and packages it into the
  existing publish loop. Both legs are best-effort, matching the sysext's existing
  rule: no binary, no package, and the host stays on its current SDR path.

Neither package Provides or Conflicts with gamescope — it installs as
/usr/bin/punktfunk-gamescope and only the sessions the host starts itself resolve
it, so a box's own Game Mode keeps using the distro binary.

Both refuse to package a binary without the +pfhdr marker. That marker is the
host's entire capability probe, so a build that lost the patches would install
fine and then silently stream SDR with no cursor.

Verified: build-gamescope-deb.sh produces an installable .deb from a stand-in
binary (correct version derived from the banner, 0755 tree, control fields) and
exits 1 on an unmarked one. The .spec is not yet exercised — no rpm tooling on
the box I had; CI's Fedora leg is its first run.
2026-08-08 15:36:19 +02:00
enricobuehler 818531a26e feat(gamescope): a headless session now reports its own mode, and the perf overlay reaches the stream
Two new patches on the pinned upstream, and the marker patch moves last so the
banner is stamped after the capabilities it advertises.

0003 — headless: advertise the virtual display's mode and refresh rates.
A headless gamescope is how we give a game a display: we pass the client's exact
mode and the session runs at it. It never told anyone. CHeadlessConnector
returned empty spans from GetModes() and GetValidDynamicRefreshRates() and
reported GAMESCOPE_SCREEN_TYPE_INTERNAL, so update_mode_atoms DELETED the
mode-list atom (no resolution list) and wlserver fell through to a one-entry
refresh list built from g_nOutputRefresh (no refresh list). With --nested-refresh
absent that entry is Init()'s 60 Hz default — which is why a field report on a
1920x1080@120 client saw "gamescope only shows 60hz, and there's no other
option", and why Overwatch capped itself to 60 while the stream ran at 120.
Populate both from the resolved mode, report EXTERNAL, and add
--custom-refresh-rates so the offered set can be widened. gamescope-session-plus
has probed for that flag for years; upstream never had it, so the
CUSTOM_REFRESH_RATES env it plumbs was a no-op everywhere.

0004 — pipewire: optionally composite the external overlay into the capture
stream. That layer is mangoapp: the fps/frametime readout the Deck UI turns on.
paint_pipewire has never referenced it on any version, so a consumer whose only
view of the session is the node sees the overlay it just enabled not appear, with
nothing to configure. Behind --pipewire-composite-external-overlay, off by
default, same argument as the cursor flag. Its commit id joins the repaint test —
the numbers change while the picture behind them is static, exactly the case the
existing test skips.

Verified: the series git-am's cleanly onto the pinned 8c676c39, and both new
functions were extracted verbatim and compiled with -Wall -Wextra under C++23
against stubs, with unit assertions for the parser and the mode/rate publication
(sorting, dedup, the running rate always present, re-entrancy, zero rejected).
A full gamescope build was not run — no box here has its dependency set; CI's
per-Fedora-major leg is the first real compile.
2026-08-08 15:35:37 +02:00
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