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.
128 lines
6.5 KiB
Markdown
128 lines
6.5 KiB
Markdown
# `punktfunk-gamescope` — gamescope with 10-bit HDR PipeWire capture
|
|
|
|
Upstream gamescope's built-in PipeWire node is SDR-only: `build_format_params()` offers `BGRx`
|
|
and `NV12`, and `paint_pipewire()` hardcodes a Gamma-2.2 composite with the SDR screenshot LUT
|
|
set. An HDR game therefore reaches every capture consumer already tone-mapped down — which is
|
|
why the punktfunk gamescope backend has always streamed 8-bit, even though games *can* render
|
|
HDR on a headless gamescope today (`--hdr-enabled --hdr-debug-force-support`).
|
|
|
|
The patches here add the missing half, and nothing else. See
|
|
`punktfunk-planning/design/gamescope-hdr-virtual-output.md` for the full design.
|
|
|
|
| Patch | What | Upstream? |
|
|
|---|---|---|
|
|
| `0001-pipewire-offer-10-bit-BT.2020-PQ-capture-formats-HDR.patch` | Offer SPA `xRGB_210LE`/`xBGR_210LE` with MANDATORY SMPTE ST.2084 + BT.2020 props, map them to `DRM_FORMAT_XRGB2101010`/`XBGR2101010`, and composite them with `g_ScreenshotColorMgmtLutsHDR` + `EOTF_PQ` | **Yes** — offered against [gamescope#2126](https://github.com/ValveSoftware/gamescope/issues/2126) |
|
|
| `0002-pipewire-optionally-composite-the-cursor-into-the-ca.patch` | `--pipewire-composite-cursor` (off by default): paint the pointer into the capture stream, using the same `MouseCursor::paint` call the scanout composite uses | **Yes** — independently useful to any consumer with no cursor of its own |
|
|
| `0003-punktfunk-stamp-the-version-banner-with-pfhdrN.patch` | Append `+pfhdr<N>` to the `--version` banner | **No** — ours only, retired when the two above land upstream |
|
|
|
|
### Why the cursor patch matters more than it looks
|
|
|
|
gamescope keeps the pointer out of its PipeWire node — it lives on a hardware plane for scanout —
|
|
so punktfunk has always reconstructed it from XFixes and blended it into every frame host-side.
|
|
That blend is what forces the encode path onto its compute colour-conversion arm: the zero-copy
|
|
RGB-direct encode source (`VK_VALVE_video_encode_rgb_conversion`) hands the captured buffer to a
|
|
fixed-function front end with no blend stage. Painting the cursor into the node removes the reason
|
|
for the blend, and with it a full-frame pass per frame — a gamescope session becomes the first one
|
|
that can be genuinely zero-copy end to end.
|
|
|
|
## Why the marker exists
|
|
|
|
punktfunk decides a session's shape **before** the virtual display exists: the bit depth at
|
|
handshake time (irrevocable — a PQ stream handed to an 8-bit encoder is a deliberate hard error),
|
|
and whether the host must composite the cursor before the encoder is even opened. Both answers
|
|
must therefore be static properties of the resolved binary, not optimistic negotiations. The host
|
|
runs `<gamescope> --version` once per boot and reads the revision — see `gamescope_patch_level()`
|
|
in `crates/pf-vdisplay/src/vdisplay/linux/gamescope/discovery.rs`.
|
|
|
|
The number is a **monotonic patch-set revision**, so one probe answers every capability:
|
|
|
|
| Level | Adds |
|
|
|---|---|
|
|
| `+pfhdr1` | 10-bit BT.2020/PQ capture formats |
|
|
| `+pfhdr2` | …and `--pipewire-composite-cursor` |
|
|
|
|
Bump it whenever a patch adds or changes something the host must know about before it spawns.
|
|
|
|
⚠️ The two indirect spawn modes (the `GAMESCOPE_BIN` wrapper for gamescope-session-plus, and the
|
|
SteamOS PATH shim) pass these flags through `PF_HDR_ARGS`, so they share one dependency: if the
|
|
session ignores `GAMESCOPE_BIN`/`PATH` and execs the distro's gamescope, it gets neither the HDR
|
|
formats nor the cursor flag. HDR fails loudly there (the capture negotiation times out and latches
|
|
an SDR downgrade); a missing cursor would be silent. Worth a post-spawn `/proc/<pid>/cmdline` check
|
|
if that ever bites.
|
|
|
|
## Which binary the host runs
|
|
|
|
Resolution order, applied identically by the bare spawn, the `GAMESCOPE_BIN` wrapper
|
|
(gamescope-session-plus) and the SteamOS PATH shim:
|
|
|
|
1. `PUNKTFUNK_GAMESCOPE_BIN` — absolute path override
|
|
2. `punktfunk-gamescope` on `PATH`
|
|
3. `gamescope`
|
|
|
|
So installing this build under the name `punktfunk-gamescope` is enough; nothing replaces the
|
|
distro's `gamescope`.
|
|
|
|
## Building
|
|
|
|
Pinned upstream: `8c676c39` (master, 2026-07-27 — tags through 3.16.25). The patches apply
|
|
cleanly to that commit; they touch `src/pipewire.cpp`, `src/steamcompmgr.cpp` and
|
|
`src/meson.build` only.
|
|
|
|
```sh
|
|
git clone https://github.com/ValveSoftware/gamescope.git
|
|
cd gamescope
|
|
git checkout 8c676c39
|
|
git submodule update --init --recursive # or let meson fetch the subprojects
|
|
git am /path/to/punktfunk/packaging/gamescope/patches/*.patch
|
|
|
|
meson setup build/ --prefix=/usr -Dpipewire=enabled
|
|
ninja -C build/
|
|
# install as punktfunk-gamescope, NOT as gamescope
|
|
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:
|
|
|
|
```sh
|
|
setcap 'CAP_SYS_NICE=eip' /usr/bin/punktfunk-gamescope
|
|
```
|
|
|
|
## Verifying the patch on a box (P0 exit)
|
|
|
|
```sh
|
|
punktfunk-gamescope --version # must contain +pfhdr2
|
|
punktfunk-gamescope --backend headless -W 1920 -H 1080 -r 60 \
|
|
--hdr-enabled --hdr-debug-force-support --pipewire-composite-cursor -- vkcube &
|
|
pw-dump | grep -A40 '"gamescope"' # node offers xRGB_210LE / xBGR_210LE
|
|
```
|
|
|
|
The stream is only 10-bit once a **consumer** asks for it: the formats are listed last, so any
|
|
consumer that negotiates the 8-bit stream today keeps negotiating it bit-for-bit.
|
|
|
|
## Rebase policy
|
|
|
|
The functional patch is two files and mirrors code that already exists in-tree (the HDR AVIF
|
|
screenshot path), so it rebases cheaply. We pin the gamescope commit we ship; when upstream
|
|
takes it, both patches are dropped and the host's capability probe becomes a plain version
|
|
floor.
|