forked from unom/punktfunk
The previous commit built the layer and taught the host to use it, but only the
Arch PKGBUILD carried the files, so every other channel still landed on the
no-game-HDR fallback. This finishes the job.
The packaging scripts now take `--stage`, the DESTDIR the gamescope build script
wrote, instead of a path to one binary. That is the part worth keeping: the next
file this package needs will not require a new flag in four scripts and two
workflows. CI caches the whole staged tree for the same reason. The gs-cache key
already hashes packaging/gamescope/**, which this commit changes, so stale caches
in the old single-file shape cannot be restored into the new layout.
Channels, all of them:
rpm spec gains Source1/Source2 and %files entries
deb build-gamescope-deb.sh copies the layer into the package root
Arch PKGBUILD (previous commit); the sysext extracts the whole usr tree
sysext bazzite takes --gamescope-stage; arch asserts the layer arrived
nix the derivation keeps, renames and rewrites the layer rather than
deleting it with everything else
A missing layer is fatal in every one of them, not best-effort. A package that
carries the compositor without it looks completely healthy and then silently
denies every game an HDR10 swapchain -- the exact failure this whole change
exists to end, so it must not be possible to ship it again by accident.
Two things needed care:
The layer manifest carries an ABSOLUTE library_path baked in at build time, so
every channel has to install the .so at exactly that path. That means literal
/usr/lib/punktfunk, not %{_libdir} (which is /usr/lib64 on Fedora) and not a
Debian multiarch triplet. Nothing links the .so by soname -- the loader dlopens
it by that path -- so multilib has no claim here. The rpm and nix install checks
now read the path back out of the manifest and fail if it names a file the
package does not install, because a manifest pointing at nothing is the silent
shape of this bug.
NixOS has no /usr, so the layer lives inside the gamescope derivation and the
host's path is overridable via PUNKTFUNK_GAMESCOPE_WSI_LAYER_DIR, which the
module sets -- the same posture as PUNKTFUNK_GAMESCOPE_BIN, and documented.
The manifest rewrite moved out of a heredoc into
packaging/gamescope/rewrite-wsi-layer-manifest.py because the FHS builds and the
Nix store both need it and must rename the layer identically; two copies would
drift into a host looking for a name only one of them produces.
Verified: 214 pf-vdisplay tests pass in a linux container, clippy -D warnings and
rustfmt clean, bash -n on all five changed shell scripts, both workflow YAMLs
parse, and the rewrite script was run against a synthetic FROG manifest to
confirm it renames/repoints/regates while preserving the `functions` block --
which is the field that decides whether the layer loads at all.
NOT verified: no nix on this machine, so gamescope.nix, flake.nix and the module
are unevaluated; no gamescope build, no package build of any kind, and no game
has taken an HDR swapchain on glass.
195 lines
11 KiB
Bash
Executable File
195 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build gamescope + punktfunk's `pipewire-hdr` patches and install it as `punktfunk-gamescope`.
|
|
#
|
|
# The ONE build recipe every packaging path calls (Arch PKGBUILD, the Bazzite sysext image, an
|
|
# operator building by hand); the nix derivation expresses the same thing declaratively. It never
|
|
# touches the distro's own `gamescope` — the binary lands under a different name, and the host's
|
|
# resolution order (PUNKTFUNK_GAMESCOPE_BIN > punktfunk-gamescope > gamescope) picks it up.
|
|
#
|
|
# Usage:
|
|
# bash build-punktfunk-gamescope.sh [--rev <git-rev>] [--prefix /usr] [--destdir DIR]
|
|
# [--srcdir DIR] [--jobs N] [--no-setcap]
|
|
#
|
|
# --rev gamescope commit/tag to build (default: the pin below)
|
|
# --prefix install prefix (default /usr)
|
|
# --destdir staging root for packaging (default: install straight into --prefix)
|
|
# --srcdir reuse an existing gamescope checkout instead of cloning
|
|
# --no-setcap skip CAP_SYS_NICE (always skipped when --destdir is set — a package sets it)
|
|
#
|
|
# Needs: git, meson >= 0.58, ninja, a C++20 compiler, and gamescope's own build deps. Those vary
|
|
# by distro; the packaging files list them (packaging/gamescope/PKGBUILD for Arch). gamescope
|
|
# vendors wlroots / libliftoff / vkroots / libdisplay-info / SPIRV-Headers / reshade as git
|
|
# SUBMODULES (plus two meson wraps for glm + stb), so both the clone and the build want network
|
|
# access unless the checkout already carries them — hence `--recurse-submodules` below.
|
|
set -euo pipefail
|
|
|
|
# The pinned upstream. Bump together with the patches (they are `git am`-able and rebase cheaply —
|
|
# two files, mirroring code that already exists in-tree; see README.md).
|
|
GAMESCOPE_REV="5fb8dce4a09d0a68d097b9faf9513782106bc843"
|
|
GAMESCOPE_REPO="https://github.com/ValveSoftware/gamescope.git"
|
|
|
|
REV="$GAMESCOPE_REV" PREFIX=/usr DESTDIR="" SRCDIR="" JOBS="" SETCAP=1 EXTRA_FALLBACK=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--rev) REV="${2:?}"; shift 2 ;;
|
|
--prefix) PREFIX="${2:?}"; shift 2 ;;
|
|
--destdir) DESTDIR="${2:?}"; shift 2 ;;
|
|
--srcdir) SRCDIR="${2:?}"; shift 2 ;;
|
|
--jobs) JOBS="${2:?}"; shift 2 ;;
|
|
--no-setcap) SETCAP=0; shift ;;
|
|
# Extra `force_fallback_for` entries, comma-separated, appended to the mandatory three below.
|
|
# Exists for ONE package family: the .deb has to install on both Debian 13 and Ubuntu 26.04,
|
|
# and those two disagree on the libdisplay-info SONAME (0.2.0 -> libdisplay-info2 vs 0.3.0 ->
|
|
# libdisplay-info3), so a package built against either one is uninstallable on the other.
|
|
# Vendoring it makes ONE .deb serve both. Opt-in rather than baked in, so the Arch/Fedora/nix
|
|
# packages — which have no such split and are shipping fine — keep producing exactly the binary
|
|
# they produce today. Its only caller is the `build-publish-gamescope` job in
|
|
# .gitea/workflows/deb.yml, which passes `--extra-fallback libdisplay-info`.
|
|
--extra-fallback) EXTRA_FALLBACK="${2:?}"; shift 2 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
# A staged install is a package build: the package manager owns file capabilities.
|
|
[ -n "$DESTDIR" ] && SETCAP=0
|
|
|
|
for tool in git meson ninja; do
|
|
command -v "$tool" >/dev/null || { echo "missing tool: $tool" >&2; exit 1; }
|
|
done
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
PATCHES=("$HERE"/patches/*.patch)
|
|
[ -e "${PATCHES[0]}" ] || { echo "no patches found in $HERE/patches" >&2; exit 1; }
|
|
|
|
WORK=""
|
|
if [ -z "$SRCDIR" ]; then
|
|
WORK="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
SRCDIR="$WORK/gamescope"
|
|
echo "==> cloning gamescope @ ${REV:0:12}"
|
|
git clone --recurse-submodules "$GAMESCOPE_REPO" "$SRCDIR"
|
|
git -C "$SRCDIR" checkout --recurse-submodules "$REV"
|
|
fi
|
|
|
|
# `git am` needs an identity and a clean tree; both are ours to provide in a throwaway checkout.
|
|
# Idempotent: a checkout that already carries the marker is left alone, so a re-run of this script
|
|
# against --srcdir does not fail on an already-applied patch.
|
|
if grep -q '+pfhdr' "$SRCDIR/src/meson.build"; then
|
|
echo "==> patches already applied in $SRCDIR"
|
|
else
|
|
echo "==> applying punktfunk patches"
|
|
git -C "$SRCDIR" \
|
|
-c user.name=punktfunk -c user.email=packages@unom.io \
|
|
am "${PATCHES[@]}"
|
|
fi
|
|
|
|
BUILD="$SRCDIR/build-punktfunk"
|
|
echo "==> configuring"
|
|
# We ship ONE binary out of this tree, so build only what leads to it:
|
|
# -Denable_tests=false gamescope's own unit tests want Catch2 **v3**
|
|
# (`catch2-with-main`); Fedora ships v2, and building someone else's
|
|
# test suite is not our job either way.
|
|
# -Denable_openvr_support the VR integration pulls the openvr submodule + its build for a
|
|
# code path a headless capture session never enters.
|
|
# -Denable_gamescope_wsi_layer ON, and installed under our own name below. This used to be off,
|
|
# on the grounds that the distro's gamescope package already ships a
|
|
# layer and that the layer is "version-independent of the compositor
|
|
# binary". That second half is FALSE: the layer and the compositor
|
|
# speak `gamescope_swapchain` to each other, and when they disagree
|
|
# the compositor rejects the client's `swapchain_feedback` and every
|
|
# Vulkan client dies on a black screen. A compositor we ship needs
|
|
# the layer we built beside it.
|
|
#
|
|
# `force_fallback_for` includes **wlroots** on purpose, and it is load-bearing for a binary we
|
|
# SHIP: gamescope vendors a wlroots submodule, but meson prefers a system one when the build host
|
|
# happens to have `wlroots-devel` — and then links it SHARED. The result starts fine on the build
|
|
# host and dies with `libwlroots-0.19.so: cannot open shared object file` anywhere else, which is
|
|
# every machine that installs our package. Fedora 43 has no wlroots-devel so it fell back and
|
|
# linked static by luck; Fedora 44's `dnf builddep gamescope` pulls one in, and the binary built
|
|
# there would not start on the host. Pinning the fallback makes the outcome the same everywhere.
|
|
# (gamescope's own meson.build hard-errors if libliftoff/vkroots are missing from this list, so
|
|
# all three go together.)
|
|
#
|
|
# The C++ runtime goes STATIC for the same reason wlroots does: this binary is built on a ROLLING
|
|
# distro and has to start on a FROZEN one. Arch's gcc (16.1.1 when this was written) makes the
|
|
# compositor require `GLIBCXX_3.4.35`, and SteamOS 3.8.16 ships libstdc++ 3.4.34 — so the published
|
|
# Arch package died on the very platform the gamescope backend matters most on ("version
|
|
# GLIBCXX_3.4.35 not found"), while every other soname resolved and glibc was never close to the
|
|
# limit (the binary asks for 2.38 at most; SteamOS has 2.41). Safe because
|
|
# gamescope links NO shared C++ library (its `NEEDED` list is all C — glslang/SPIRV are build-time
|
|
# only), so no C++ ABI ever crosses a shared boundary; it costs ~1 MB.
|
|
# Appended to LDFLAGS rather than passed as `-Dcpp_link_args`, because that option would REPLACE
|
|
# the value meson derives from the environment and silently drop makepkg's hardening flags
|
|
# (`-z relro`, `-z now`, `--as-needed`).
|
|
export LDFLAGS="${LDFLAGS:-} -static-libstdc++ -static-libgcc"
|
|
meson setup "$BUILD" "$SRCDIR" \
|
|
--prefix="$PREFIX" \
|
|
--buildtype=release \
|
|
-Dforce_fallback_for="libliftoff,vkroots,wlroots${EXTRA_FALLBACK:+,$EXTRA_FALLBACK}" \
|
|
-Dpipewire=enabled \
|
|
-Denable_tests=false \
|
|
-Denable_openvr_support=false \
|
|
-Denable_gamescope_wsi_layer=true
|
|
|
|
echo "==> building"
|
|
ninja -C "$BUILD" ${JOBS:+-j "$JOBS"}
|
|
|
|
# Install ONLY the compositor, under our own name. gamescope's `ninja install` would also drop
|
|
# gamescopectl/gamescopereaper/gamescopestream + the WSI layer into the prefix, colliding with the
|
|
# distro's gamescope package — and we need none of them: the host only ever execs the compositor.
|
|
BIN="$BUILD/src/gamescope"
|
|
[ -x "$BIN" ] || { echo "build produced no $BIN" >&2; exit 1; }
|
|
# The static C++ runtime above is invisible in a successful build and only shows up as a binary
|
|
# that will not start on an older distro — so assert it here, where a mistake is a build failure
|
|
# instead of a package that dies at `--version` on SteamOS. No `libstdc++.so.6` in NEEDED is the
|
|
# whole invariant (and it needs no version threshold to check).
|
|
if command -v objdump >/dev/null; then
|
|
objdump -p "$BIN" 2>/dev/null | grep -q 'NEEDED.*libstdc++' && {
|
|
echo "built binary links libstdc++ dynamically — the static C++ runtime did not take, and this" >&2
|
|
echo "package would not start on a distro older than the build host" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
DEST="${DESTDIR}${PREFIX}/bin/punktfunk-gamescope"
|
|
echo "==> installing $DEST"
|
|
install -Dm755 "$BIN" "$DEST"
|
|
|
|
# The WSI layer, under OUR name, at OUR path.
|
|
#
|
|
# A game nested under gamescope gets an HDR10 swapchain from this layer and from nothing else —
|
|
# gamescope advertises no runtime colour-management protocol a Mesa/NVIDIA WSI could negotiate
|
|
# through — so a compositor shipped WITHOUT a matching layer simply cannot do HDR for games. Built
|
|
# from this same tree at this same rev, so the two can never drift apart; that is the whole point,
|
|
# and it is what makes the host's old "compare version triples and hope" check unnecessary.
|
|
#
|
|
# It must not collide with the distro's layer and must be switchable independently of it, so the
|
|
# generated manifest is rewritten to carry our layer name, our library path and our own
|
|
# enable/disable variables. The Vulkan loader keys implicit layers on that NAME, so with a distinct
|
|
# one both layers can sit installed side by side and the host picks per session.
|
|
#
|
|
# python3 rather than sed because meson is itself a Python program — it is guaranteed present on any
|
|
# host that got this far — and a JSON edit belongs in a JSON parser.
|
|
LAYER_SO=$(find "$BUILD" -type f -name 'libVkLayer_*gamescope_wsi*.so' | head -1)
|
|
LAYER_SRC_JSON=$(find "$BUILD" -type f -name '*gamescope_wsi*.json' | head -1)
|
|
[ -n "$LAYER_SO" ] && [ -n "$LAYER_SRC_JSON" ] || {
|
|
echo "the WSI layer did not build (.so=${LAYER_SO:-none} .json=${LAYER_SRC_JSON:-none}) — without" >&2
|
|
echo "it no game in a punktfunk gamescope session can get an HDR10 swapchain" >&2
|
|
exit 1
|
|
}
|
|
LAYER_LIB_PATH="${PREFIX}/lib/punktfunk/libVkLayer_PUNKTFUNK_gamescope_wsi.so"
|
|
LAYER_DEST_JSON="${DESTDIR}${PREFIX}/lib/punktfunk/vulkan/implicit_layer.d/punktfunk_gamescope_wsi.json"
|
|
echo "==> installing ${DESTDIR}${LAYER_LIB_PATH}"
|
|
install -Dm755 "$LAYER_SO" "${DESTDIR}${LAYER_LIB_PATH}"
|
|
install -d "$(dirname "$LAYER_DEST_JSON")"
|
|
python3 "$(dirname "$0")/rewrite-wsi-layer-manifest.py" \
|
|
"$LAYER_SRC_JSON" "$LAYER_DEST_JSON" "$LAYER_LIB_PATH"
|
|
|
|
if [ "$SETCAP" = 1 ] && command -v setcap >/dev/null; then
|
|
# gamescope raises its own scheduling priority; without CAP_SYS_NICE it still runs, just noisier
|
|
# and with worse frame pacing. Best-effort — needs root, and a package sets it declaratively.
|
|
setcap 'CAP_SYS_NICE=eip' "$DEST" 2>/dev/null \
|
|
|| echo "note: could not setcap CAP_SYS_NICE on $DEST (run as root, or let the package do it)"
|
|
fi
|
|
|
|
echo "==> done: $("$DEST" --version 2>&1 | head -1)"
|
|
echo " the banner above must contain +pfhdr — that marker is how the host detects HDR support"
|