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.
This commit is contained in:
2026-07-28 18:03:30 +02:00
parent fd648aa776
commit 9eb9f74893
3 changed files with 67 additions and 20 deletions
+19
View File
@@ -81,6 +81,25 @@ ninja -C build/
install -Dm755 build/src/gamescope /usr/bin/punktfunk-gamescope
```
### Build dependencies
They are gamescope's, not ours, and they vary by distro. Two shortcuts that work:
```sh
# Fedora / Bazzite (inside a toolbox/distrobox — the host is immutable)
sudo dnf install -y dnf-plugins-core meson ninja-build glslc
sudo dnf builddep -y gamescope
sudo dnf install -y xorg-x11-server-Xwayland-devel # NOT pulled by builddep; wlroots needs it
# Arch / SteamOS — see the makedepends in ./PKGBUILD
```
⚠️ `dnf builddep gamescope` resolves Fedora's *packaged* gamescope, which is older than the master
we pin, so it can come up short. `xorg-x11-server-Xwayland-devel` is the one that actually bit
(2026-07-28, Fedora 43): without it wlroots' configure fails with `Neither a subproject directory
nor a xserver.wrap file was found`, several minutes into an otherwise clean run. If a different
one surfaces, meson names it — install and re-run with `--srcdir` so the clone is not repeated.
`gamescope` needs `CAP_SYS_NICE` for its realtime priority; the distro packages set it on their
own binary. Mirror it if you install ours system-wide:
@@ -75,10 +75,23 @@ 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 the WSI layer is a SEPARATE artifact the distro's gamescope
# package already installs; ours must not collide with it.
# (The layer the nested games load is that one — it is version-
# independent of the compositor binary.)
meson setup "$BUILD" "$SRCDIR" \
--prefix="$PREFIX" \
--buildtype=release \
-Dpipewire=enabled
-Dpipewire=enabled \
-Denable_tests=false \
-Denable_openvr_support=false \
-Denable_gamescope_wsi_layer=false
echo "==> building"
ninja -C "$BUILD" ${JOBS:+-j "$JOBS"}
@@ -32,24 +32,23 @@ follow what the app happens to render.
Works on the headless backend as well as a real connector: no HDR display is
involved anywhere in the LUT set.
---
src/pipewire.cpp | 99 ++++++++++++++++++++++++++++++++------------
src/steamcompmgr.cpp | 21 ++++++++--
2 files changed, 91 insertions(+), 29 deletions(-)
src/pipewire.cpp | 114 +++++++++++++++++++++++++++++++++----------
src/steamcompmgr.cpp | 21 ++++++--
2 files changed, 106 insertions(+), 29 deletions(-)
diff --git a/src/pipewire.cpp b/src/pipewire.cpp
index 76b3ea8..0cf71d2 100644
index 76b3ea8..6b56b01 100644
--- a/src/pipewire.cpp
+++ b/src/pipewire.cpp
@@ -18,6 +18,25 @@
@@ -18,6 +18,40 @@
static LogScope pwr_log("pipewire");
+// 10-bit HDR capture.
+//
+// The packed 10-bit RGB SPA formats (xRGB_210LE / xBGR_210LE) and the SMPTE ST.2084 (PQ)
+// transfer function all predate PipeWire 1.0 by a wide margin; the guard only exists so the
+// file still compiles against an ancient libspa, in which case the stream offers exactly the
+// 8-bit formats it always did.
+// The packed 10-bit RGB SPA formats (xRGB_210LE / xBGR_210LE) predate PipeWire 1.0; the guard
+// only exists so this file still compiles against an ancient libspa, in which case the stream
+// offers exactly the 8-bit formats it always did.
+#if PW_CHECK_VERSION(1, 0, 0)
+#define GAMESCOPE_PIPEWIRE_HDR 1
+#else
@@ -57,6 +56,22 @@ index 76b3ea8..0cf71d2 100644
+#endif
+
+#if GAMESCOPE_PIPEWIRE_HDR
+// The colorimetry VALUES, spelled out rather than taken from the enums, because the headers that
+// name them are much newer than the ones that name the formats above: SPA grew
+// BT2020_10 / SMPTE2084 / ARIB_STD_B67 as one block, and a libspa without it stops at ADOBERGB —
+// PipeWire 1.4.11 (Fedora 43 / Bazzite) does exactly that, so `SPA_VIDEO_TRANSFER_SMPTE2084`
+// fails to compile there even though the library parses the value perfectly well.
+//
+// Spelling them out is safe because the enum is WIRE ABI, not a private detail: SPA mirrors
+// GStreamer's GstVideoTransferFunction / GstVideoColorPrimaries, where these landed together, so
+// the numbers are identical on every implementation that has them at all. On one that does not,
+// a consumer simply fails to intersect this pod and negotiates 8-bit — the same outcome as not
+// offering HDR. (punktfunk's own consumer spells out the same 14 for the same reason.)
+static constexpr uint32_t kSpaVideoTransferSmpte2084 = 14;
+static constexpr uint32_t kSpaVideoColorPrimariesBt2020 = 7;
+#endif
+
+#if GAMESCOPE_PIPEWIRE_HDR
+static bool is_hdr_video_format(uint32_t format)
+{
+ return format == SPA_VIDEO_FORMAT_xRGB_210LE || format == SPA_VIDEO_FORMAT_xBGR_210LE;
@@ -66,7 +81,7 @@ index 76b3ea8..0cf71d2 100644
static struct pipewire_state pipewire_state = { .stream_node_id = SPA_ID_INVALID };
static int nudgePipe[2] = { -1, -1 };
@@ -94,6 +113,37 @@ static void calculate_capture_size()
@@ -94,6 +128,37 @@ static void calculate_capture_size()
s_nCaptureHeight = SPA_ROUND_UP_N(s_nCaptureHeight, kCaptureAlign);
}
@@ -94,9 +109,9 @@ index 76b3ea8..0cf71d2 100644
+#if GAMESCOPE_PIPEWIRE_HDR
+ else if (is_hdr_video_format(format)) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_transferFunction, SPA_POD_PROP_FLAG_MANDATORY);
+ spa_pod_builder_id(builder, SPA_VIDEO_TRANSFER_SMPTE2084);
+ spa_pod_builder_id(builder, kSpaVideoTransferSmpte2084);
+ spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_colorPrimaries, SPA_POD_PROP_FLAG_MANDATORY);
+ spa_pod_builder_id(builder, SPA_VIDEO_COLOR_PRIMARIES_BT2020);
+ spa_pod_builder_id(builder, kSpaVideoColorPrimariesBt2020);
+ }
+#endif
+}
@@ -104,7 +119,7 @@ index 76b3ea8..0cf71d2 100644
static void build_format_params(struct spa_pod_builder *builder, spa_video_format format, std::vector<const struct spa_pod *> &params) {
struct spa_rectangle size = SPA_RECTANGLE(s_nCaptureWidth, s_nCaptureHeight);
struct spa_rectangle min_requested_size = { 0, 0 };
@@ -112,18 +162,7 @@ static void build_format_params(struct spa_pod_builder *builder, spa_video_forma
@@ -112,18 +177,7 @@ static void build_format_params(struct spa_pod_builder *builder, spa_video_forma
SPA_FORMAT_VIDEO_requested_size, SPA_POD_CHOICE_RANGE_Rectangle( &min_requested_size, &min_requested_size, &max_requested_size ),
SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_CHOICE_RANGE_Long( 0ll, INT64_MIN, INT64_MAX ),
0);
@@ -124,7 +139,7 @@ index 76b3ea8..0cf71d2 100644
spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY);
spa_pod_builder_push_choice(builder, &choice_frame, SPA_CHOICE_Enum, 0);
spa_pod_builder_long(builder, modifier); // default
@@ -141,18 +180,7 @@ static void build_format_params(struct spa_pod_builder *builder, spa_video_forma
@@ -141,18 +195,7 @@ static void build_format_params(struct spa_pod_builder *builder, spa_video_forma
SPA_FORMAT_VIDEO_requested_size, SPA_POD_CHOICE_RANGE_Rectangle( &min_requested_size, &min_requested_size, &max_requested_size ),
SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_CHOICE_RANGE_Long( 0ll, INT64_MIN, INT64_MAX ),
0);
@@ -144,7 +159,7 @@ index 76b3ea8..0cf71d2 100644
params.push_back((const struct spa_pod *) spa_pod_builder_pop(builder, &obj_frame));
// for (auto& param : params)
@@ -166,6 +194,14 @@ static std::vector<const struct spa_pod *> build_format_params(struct spa_pod_bu
@@ -166,6 +209,14 @@ static std::vector<const struct spa_pod *> build_format_params(struct spa_pod_bu
build_format_params(builder, SPA_VIDEO_FORMAT_BGRx, params);
build_format_params(builder, SPA_VIDEO_FORMAT_NV12, params);
@@ -159,7 +174,7 @@ index 76b3ea8..0cf71d2 100644
return params;
}
@@ -288,7 +324,7 @@ static void dispatch_nudge(struct pipewire_state *state, int fd)
@@ -288,7 +339,7 @@ static void dispatch_nudge(struct pipewire_state *state, int fd)
if (s_nCaptureWidth != state->video_info.size.width || s_nCaptureHeight != state->video_info.size.height) {
pwr_log.debugf("renegotiating stream params (size: %dx%d)", s_nCaptureWidth, s_nCaptureHeight);
@@ -168,7 +183,7 @@ index 76b3ea8..0cf71d2 100644
struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
std::vector<const struct spa_pod *> format_params = build_format_params(&builder);
int ret = pw_stream_update_params(state->stream, format_params.data(), format_params.size());
@@ -412,6 +448,12 @@ static void stream_handle_param_changed(void *data, uint32_t id, const struct sp
@@ -412,6 +463,12 @@ static void stream_handle_param_changed(void *data, uint32_t id, const struct sp
state->video_info.size.width, state->video_info.size.height,
s_nRequestedWidth, s_nRequestedHeight,
state->video_info.format, state->shm_stride, shm_size, state->dmabuf);
@@ -181,7 +196,7 @@ index 76b3ea8..0cf71d2 100644
}
static void randname(char *buf)
@@ -450,6 +492,11 @@ uint32_t spa_format_to_drm(uint32_t spa_format)
@@ -450,6 +507,11 @@ uint32_t spa_format_to_drm(uint32_t spa_format)
switch (spa_format)
{
case SPA_VIDEO_FORMAT_NV12: return DRM_FORMAT_NV12;
@@ -193,7 +208,7 @@ index 76b3ea8..0cf71d2 100644
default:
case SPA_VIDEO_FORMAT_BGR: return DRM_FORMAT_XRGB8888;
}
@@ -715,7 +762,7 @@ bool init_pipewire(void)
@@ -715,7 +777,7 @@ bool init_pipewire(void)
s_nOutputHeight = g_nOutputHeight;
calculate_capture_size();