HDR was a stop bright, in-game HDR was unreachable on every distro, and SDR shipped untagged #210

Merged
enricobuehler merged 5 commits from worktree-hdr-colour-anchor-fixes into main 2026-08-13 21:50:13 +00:00
Owner

From a field report on 2026-08-13: streaming from a Bazzite host in gaming mode to an iPad, "with HDR enabled the Steam menu looks very saturated and when HDR is enabled in game it looks washed out", plus Steam's own "enable HDR" greyed out as "display is not compatible with HDR". A second report on the same day: blacks reading as grey in SDR on an Apple TV, with the client's HDR switch already off.

Four distinct defects, two of them long-standing and silent. The host's colour signalling was audited and exonerated — capture negotiates xBGR_210LE with transfer_function=14 (PQ) and color_primaries=7 (BT.2020), and the SPS VUI, the AV1 sequence header and the Welcome's ColorInfo all agree on BT.2020 PQ limited. Nothing here is the old red/blue swap.

1. Both iPad symptoms were one bug (c29e7203)

gamescope maps everything that is not an HDR game — the desktop, the Steam overlay, an SDR title — into the session's PQ container at --hdr-sdr-content-nits, and we only passed that flag when an operator had set PUNKTFUNK_GAMESCOPE_SDR_NITS. Unset, gamescope used its own default of 400 nits, while every first-party client anchors diffuse white at 203 (BT.2408 reference white; the Apple presenter hands exactly that to CAEDRMetadata.hdr10's opticalOutputScale).

Nearly a stop apart. The SDR UI lands above SDR white, which reads as glaring and over-saturated, and the client's tone-mapper then works from a reference point the host never used, which flattens real HDR content into "washed out". Same error, opposite-looking results, because the two contents sit on opposite sides of the anchor.

The flag is now always passed, defaulting to 203, and both ends carry a comment naming the other. The client constant's comment claimed it "matches the host's standard PQ reference white" — it did not.

2. In-game HDR was unreachable on essentially every distro (3ac4548c, 082c6575)

A game nested under gamescope gets its HDR10 swapchain from the FROG WSI layer and from nothing else. That layer speaks gamescope_swapchain to the compositor, and a mismatched one makes the compositor reject swapchain_feedback and kill every Vulkan client — a black screen with sound and input and no error.

We ship our own compositor and shipped no layer, on the recorded grounds that the layer is "version-independent of the compositor binary". It is not, and wsi_layer_matches_our_gamescope() exists because it is not. So the host was left guessing from version triples — and that guess is wrong in both directions:

  • Same upstream tag, distro patched the protocol → compares equal, layer kept, every game black-screens. That is the Nobara case the check was written for, so it does not even prevent its own motivating failure.
  • Different tag, byte-identical protocol → compares unequal, HDR lost for nothing. Since we pin a rev, this is the normal case.

So the layer was disabled on essentially every box with a distro gamescope, and no game could render HDR. Our six patches touch only src/, never protocol/, so this was never about our changes.

Fixed by shipping the layer instead of guessing at someone else's. It is built from the same tree at the same rev as the compositor, so the two cannot drift. It installs under our own name (VK_LAYER_PUNKTFUNK_gamescope_wsi) at our own path with our own enable/disable variables, so it coexists with the distro's rather than colliding — the Vulkan loader deduplicates implicit layers by name — and the host switches the two independently in one session.

WsiPlan makes the three states explicit and resolves them once per launch:

Ours our layer is installed: enable it, force the distro's off
DistroKept no layer of ours, distro's looks compatible: touch nothing
DistroDisabled no layer of ours, distro's untrusted: today's behaviour

That last arm is a fail-safe: a host newer than its gamescope package behaves exactly as it does today rather than enabling a layer that is not there. So this rolls out one packaging channel at a time with no flag day.

All channels now carry it — rpm (spec Source1/Source2), deb, Arch, both sysexts, and nix. The packaging scripts take --stage, the DESTDIR the build script wrote, rather than a path to one binary, so the next file this package needs will not require a new flag in four scripts and two workflows. A missing layer is fatal in every channel, never best-effort: a package carrying the compositor without it looks completely healthy and then silently denies every game an HDR swapchain, which is precisely the failure being fixed.

Two things needed care. The manifest carries an absolute library_path baked in at build time, so every channel installs the .so at exactly that path — literal /usr/lib/punktfunk, not %{_libdir} (/usr/lib64 on Fedora) and not a Debian multiarch triplet. Nothing links it by soname; the loader dlopens it by that path. The rpm and nix install checks 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. And NixOS has no /usr, so the layer lives inside the derivation and the path is overridable via PUNKTFUNK_GAMESCOPE_WSI_LAYER_DIR, which the module sets — same posture as PUNKTFUNK_GAMESCOPE_BIN.

The manifest rewrite lives in packaging/gamescope/rewrite-wsi-layer-manifest.py rather than a heredoc 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.

3. The Apple SDR layer shipped untagged (94f049ba)

configure(hdr:) guards on hdr != hdrActive, and hdrActive starts false. A session that is SDR from its first frame matched the initial state and fell straight through, so configureColor never ran even once and the layer kept make()'s bare config, which never assigns a colour space.

An untagged CAMetalLayer gets no colour matching — the BT.709 stream is drawn in the display's native space. That is mild oversaturation on a P3 panel, and on a tvOS display composited for HDR it also lifts the black floor, which matches the grey-blacks report. It also meant PUNKTFUNK_SDR_COLORSPACE was dead code on exactly the sessions it existed to fix, so anyone who tried that lever in the field saw nothing change.

configureColor now runs once regardless, and tagging is the default; PUNKTFUNK_SDR_COLORSPACE=none restores the untagged look.

4. An SDR stream forced the Apple TV into HDR10 output (a190fdb1)

applyDisplayCriteriaIfNeeded hardcodes BT.2020 + ST.2084 PQ into the format description it hands AVDisplayCriteria, but guarded only on the user's HDR setting — never on the stream being HDR. Since that setting defaults to true, an ordinary SDR session drove an HDR-capable TV into PQ output, which is a standard way to raise the black floor. Now gated on connection.isHDR.

This was not the cause of the grey-blacks report — that had the client's HDR setting off, so this path never ran. Found while investigating it.

Verification

  • 214 pf-vdisplay tests pass in a Linux container, including a new one pinning that the Ours arm enables ours and forces the distro's off together (either half alone is a bug)
  • scripts/xcheck.sh linux clippy clean at -D warnings; rustfmt clean
  • Full tvOS compile of every PunktfunkKit source clean against the AppleTVOS SDK. The link step fails on three ABI symbols missing from the checked-in PunktfunkCore.xcframework, which predates them — pre-existing, main fails identically
  • bash -n on all five changed shell scripts; both workflow YAMLs parse
  • The rewrite script run against a synthetic FROG manifest, confirming it renames, repoints and re-gates while preserving the functions block — the field that decides whether the layer loads at all

Not verified: no nix on the machine this was written on, 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. The CI gamescope cache key already hashes packaging/gamescope/**, which this branch changes, so stale caches in the old single-file shape cannot be restored into the new layout.

Still open, deliberately

Steam's "Enable HDR" toggle stays greyed out. That is a separate cause: we always run --backend headless, whose connector reports no HDR support, and patch 0003 — which does patch wlserver_send_gamescope_control(), the function feeding Steam its display info — deliberately touches only screen type, modes and refresh. --hdr-debug-force-support reaches only the gamescopeHDROutputFeedback root atom the WSI layer reads, not the capability Steam queries. The real fix is a +pfhdr7 patch populating CHeadlessConnector::m_HDRInfo, which needs gamescope source and a box. In-session discriminator: xprop -root | grep -i HDR — feedback 1 with supports-HDR 0 or absent confirms it.

The version-triple check is kept as the fallback arm only, with its doc rewritten to say it is a guess wrong in both directions and that nothing new should be built on it. Its behaviour is unchanged on purpose: re-enabling a genuinely mismatched layer black-screens every Vulkan client, which is worse than losing HDR, and retesting that trade needs a real box.

Field test that needs no rebuild: PUNKTFUNK_GAMESCOPE_SDR_NITS=203 on an existing host should fix both iPad symptoms if the anchor arithmetic is right.

From a field report on 2026-08-13: streaming from a Bazzite host in gaming mode to an iPad, "with HDR enabled the Steam menu looks very saturated and when HDR is enabled in game it looks washed out", plus Steam's own "enable HDR" greyed out as "display is not compatible with HDR". A second report on the same day: blacks reading as grey in **SDR** on an Apple TV, with the client's HDR switch already off. Four distinct defects, two of them long-standing and silent. The host's colour signalling was audited and **exonerated** — capture negotiates `xBGR_210LE` with `transfer_function=14` (PQ) and `color_primaries=7` (BT.2020), and the SPS VUI, the AV1 sequence header and the Welcome's `ColorInfo` all agree on BT.2020 PQ limited. Nothing here is the old red/blue swap. ### 1. Both iPad symptoms were one bug (`c29e7203`) gamescope maps everything that is not an HDR game — the desktop, the Steam overlay, an SDR title — into the session's PQ container at `--hdr-sdr-content-nits`, and we only passed that flag when an operator had set `PUNKTFUNK_GAMESCOPE_SDR_NITS`. Unset, gamescope used its own default of **400 nits**, while every first-party client anchors diffuse white at **203** (BT.2408 reference white; the Apple presenter hands exactly that to `CAEDRMetadata.hdr10`'s `opticalOutputScale`). Nearly a stop apart. The SDR UI lands above SDR white, which reads as glaring and over-saturated, and the client's tone-mapper then works from a reference point the host never used, which flattens real HDR content into "washed out". Same error, opposite-looking results, because the two contents sit on opposite sides of the anchor. The flag is now always passed, defaulting to 203, and both ends carry a comment naming the other. The client constant's comment claimed it "matches the host's standard PQ reference white" — it did not. ### 2. In-game HDR was unreachable on essentially every distro (`3ac4548c`, `082c6575`) A game nested under gamescope gets its HDR10 swapchain from the FROG WSI layer and from nothing else. That layer speaks `gamescope_swapchain` to the compositor, and a mismatched one makes the compositor reject `swapchain_feedback` and kill every Vulkan client — a black screen with sound and input and no error. We ship our own compositor and shipped **no layer**, on the recorded grounds that the layer is "version-independent of the compositor binary". It is not, and `wsi_layer_matches_our_gamescope()` exists because it is not. So the host was left guessing from version triples — and that guess is wrong in **both** directions: - Same upstream tag, distro patched the protocol → compares **equal**, layer kept, every game black-screens. That is the Nobara case the check was written for, so it does not even prevent its own motivating failure. - Different tag, byte-identical protocol → compares unequal, HDR lost for nothing. Since we pin a rev, **this is the normal case**. So the layer was disabled on essentially every box with a distro gamescope, and no game could render HDR. Our six patches touch only `src/`, never `protocol/`, so this was never about our changes. Fixed by shipping the layer instead of guessing at someone else's. It is built from the same tree at the same rev as the compositor, so the two cannot drift. It installs under our own name (`VK_LAYER_PUNKTFUNK_gamescope_wsi`) at our own path with our own enable/disable variables, so it **coexists** with the distro's rather than colliding — the Vulkan loader deduplicates implicit layers by name — and the host switches the two independently in one session. `WsiPlan` makes the three states explicit and resolves them once per launch: | | | |---|---| | `Ours` | our layer is installed: enable it, force the distro's off | | `DistroKept` | no layer of ours, distro's looks compatible: touch nothing | | `DistroDisabled` | no layer of ours, distro's untrusted: today's behaviour | That last arm is a **fail-safe**: a host newer than its gamescope package behaves exactly as it does today rather than enabling a layer that is not there. So this rolls out one packaging channel at a time with no flag day. All channels now carry it — rpm (spec `Source1`/`Source2`), deb, Arch, both sysexts, and nix. The packaging scripts take `--stage`, the DESTDIR the build script wrote, rather than a path to one binary, so the next file this package needs will not require a new flag in four scripts and two workflows. A missing layer is **fatal** in every channel, never best-effort: a package carrying the compositor without it looks completely healthy and then silently denies every game an HDR swapchain, which is precisely the failure being fixed. Two things needed care. The manifest carries an **absolute** `library_path` baked in at build time, so every channel installs the `.so` at exactly that path — literal `/usr/lib/punktfunk`, not `%{_libdir}` (`/usr/lib64` on Fedora) and not a Debian multiarch triplet. Nothing links it by soname; the loader dlopens it by that path. The rpm and nix install checks 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. And NixOS has no `/usr`, so the layer lives inside the derivation and the path is overridable via `PUNKTFUNK_GAMESCOPE_WSI_LAYER_DIR`, which the module sets — same posture as `PUNKTFUNK_GAMESCOPE_BIN`. The manifest rewrite lives in `packaging/gamescope/rewrite-wsi-layer-manifest.py` rather than a heredoc 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. ### 3. The Apple SDR layer shipped untagged (`94f049ba`) `configure(hdr:)` guards on `hdr != hdrActive`, and `hdrActive` starts `false`. A session that is SDR from its first frame matched the initial state and fell straight through, so `configureColor` never ran **even once** and the layer kept `make()`'s bare config, which never assigns a colour space. An untagged `CAMetalLayer` gets no colour matching — the BT.709 stream is drawn in the display's native space. That is mild oversaturation on a P3 panel, and on a tvOS display composited for HDR it also lifts the black floor, which matches the grey-blacks report. It also meant `PUNKTFUNK_SDR_COLORSPACE` was dead code on exactly the sessions it existed to fix, so anyone who tried that lever in the field saw nothing change. `configureColor` now runs once regardless, and tagging is the default; `PUNKTFUNK_SDR_COLORSPACE=none` restores the untagged look. ### 4. An SDR stream forced the Apple TV into HDR10 output (`a190fdb1`) `applyDisplayCriteriaIfNeeded` hardcodes BT.2020 + ST.2084 PQ into the format description it hands `AVDisplayCriteria`, but guarded only on the user's HDR setting — never on the stream being HDR. Since that setting defaults to true, an ordinary SDR session drove an HDR-capable TV into PQ output, which is a standard way to raise the black floor. Now gated on `connection.isHDR`. This was **not** the cause of the grey-blacks report — that had the client's HDR setting off, so this path never ran. Found while investigating it. ## Verification - 214 `pf-vdisplay` tests pass in a Linux container, including a new one pinning that the `Ours` arm enables ours **and** forces the distro's off together (either half alone is a bug) - `scripts/xcheck.sh linux clippy` clean at `-D warnings`; rustfmt clean - Full tvOS compile of every `PunktfunkKit` source clean against the AppleTVOS SDK. The link step fails on three ABI symbols missing from the checked-in `PunktfunkCore.xcframework`, which predates them — pre-existing, `main` fails identically - `bash -n` on all five changed shell scripts; both workflow YAMLs parse - The rewrite script run against a synthetic FROG manifest, confirming it renames, repoints and re-gates while preserving the `functions` block — the field that decides whether the layer loads at all **Not verified:** no nix on the machine this was written on, 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. The CI gamescope cache key already hashes `packaging/gamescope/**`, which this branch changes, so stale caches in the old single-file shape cannot be restored into the new layout. ## Still open, deliberately Steam's "Enable HDR" toggle stays greyed out. That is a separate cause: we always run `--backend headless`, whose connector reports no HDR support, and patch 0003 — which does patch `wlserver_send_gamescope_control()`, the function feeding Steam its display info — deliberately touches only screen type, modes and refresh. `--hdr-debug-force-support` reaches only the `gamescopeHDROutputFeedback` root atom the WSI layer reads, not the capability Steam queries. The real fix is a `+pfhdr7` patch populating `CHeadlessConnector::m_HDRInfo`, which needs gamescope source and a box. In-session discriminator: `xprop -root | grep -i HDR` — feedback 1 with supports-HDR 0 or absent confirms it. The version-triple check is kept as the fallback arm only, with its doc rewritten to say it is a guess wrong in both directions and that nothing new should be built on it. Its behaviour is unchanged on purpose: re-enabling a genuinely mismatched layer black-screens every Vulkan client, which is worse than losing HDR, and retesting that trade needs a real box. Field test that needs no rebuild: `PUNKTFUNK_GAMESCOPE_SDR_NITS=203` on an existing host should fix both iPad symptoms if the anchor arithmetic is right.
enricobuehler added 5 commits 2026-08-13 21:38:52 +00:00
Field report 2026-08-13, Bazzite host in gaming mode to an iPad: Steam's Big
Picture UI looked glaring and over-saturated while HDR game content looked
washed out, both on the same stream.

Those are one error. gamescope maps everything that is not an HDR game -- the
desktop, the Steam overlay, an SDR title -- into the session's PQ container at
--hdr-sdr-content-nits, and we only passed that flag when an operator had set
PUNKTFUNK_GAMESCOPE_SDR_NITS. Unset, gamescope used its own default of 400,
while every first-party client anchors diffuse white at 203 (BT.2408 reference
white; the Apple presenter hands exactly that to CAEDRMetadata.hdr10's
opticalOutputScale). The two ends were nearly a stop apart, so the UI landed
above SDR white and the client's tone-mapper worked from a reference point the
host had never used, flattening the content around it.

The flag is now always passed, defaulting to 203. The knob still overrides it
for anyone who wants a brighter or dimmer desktop.

Separately, and visible in the same log: the two HDR decisions in a gamescope
session are made independently. hdr_args() never consults
wsi_layer_matches_our_gamescope(), so when the WSI-layer version check fires --
which it does on essentially every Bazzite box, since we pin our own gamescope
rev and the check compares version triples -- the session launches advertising
HDR while having made an HDR10 swapchain unreachable for every game in it. That
layer is the only route to one, so a title told to render HDR renders it into an
SDR swapchain and looks washed out, with nothing anywhere saying why. It now
warns. The behaviour of the check itself is deliberately left alone: re-enabling
a genuinely mismatched layer black-screens every Vulkan client, which is worse
than losing HDR, and that trade needs a real box to retest.

Verified: scripts/xcheck.sh linux clippy clean on pf-vdisplay (-D warnings),
rustfmt clean. Not yet verified on glass.
configure(hdr:) guards on hdr != hdrActive, and hdrActive starts false. A
session that is SDR from its first frame therefore matched the initial state and
fell straight through the guard, so configureColor never ran even once and the
layer kept make()'s bare configuration -- which never assigns a colour space.

An untagged CAMetalLayer gets no colour matching: the BT.709-encoded stream is
drawn in the display's native space. That is mild oversaturation on a P3 Mac or
iPad, and on a tvOS display composited for HDR it also lifts the black floor.
It matches a field report of greys where blacks should be, which arrived with
the client's own HDR switch already OFF -- so nothing else in the pipeline had
tagged those pixels either.

It also meant PUNKTFUNK_SDR_COLORSPACE was dead code on exactly the sessions it
existed to fix: an operator A/B-ing it in the field would have seen no change at
all, because the assignment it feeds was never executed.

So: configureColor now runs once regardless (same-state calls after the first
are still no-ops), and tagging is the default rather than opt-in, since drawing
a BT.709 stream in the panel's native space is not a rendering anyone asked for.
PUNKTFUNK_SDR_COLORSPACE=none restores the untagged look as the A/B lever.

The tvOS HDR tone-map branch gets the same tag -- pf_frag_hdr_tv outputs BT.709,
so it is an SDR layer by the time it is presented.

Verified: full tvOS compile of every PunktfunkKit source clean against the
AppleTVOS SDK. (The build's link step fails on three ABI symbols missing from
the checked-in PunktfunkCore.xcframework, which predates them -- pre-existing,
main fails identically, unrelated to these files.) Not yet verified on glass.
applyDisplayCriteriaIfNeeded builds a synthetic format description hardcoding
BT.2020 primaries, ST.2084 PQ and the BT.2020 matrix, then hands it to
AVDisplayManager to pick a display mode. Its guard checked only that no criteria
had been set yet and that the user's HDR setting was on -- never that the stream
itself was HDR. Since that setting defaults to true, an ordinary SDR session
drove an HDR-capable TV into PQ output.

That is a standard way to raise the black floor: the Apple TV switches HDMI to
limited-range levels in its HDR modes, and a set configured for full range then
renders code 16 as grey rather than black.

Now gated on connection.isHDR as well. Layout re-runs this, so a session that
flips to HDR mid-stream still picks the mode up on the next pass.

This was NOT the cause of the 2026-08-13 grey-blacks report -- that one had the
client's HDR setting off, so this path never ran (see the SDR layer tagging in
the previous commit for the mechanism that did apply). It is a real bug on its
own, found while investigating it.

Verified: full tvOS compile clean against the AppleTVOS SDK. Not yet verified on
glass.
A game nested under gamescope gets an HDR10 swapchain from the FROG WSI layer and
from nothing else -- gamescope advertises no runtime colour-management protocol a
Mesa/NVIDIA WSI could negotiate through. That layer talks `gamescope_swapchain`
to the compositor, and when the two disagree the compositor rejects the client's
swapchain_feedback and every Vulkan client dies on a black screen with sound and
input and no error anywhere.

We ship our own compositor and did NOT ship a layer, on the recorded grounds that
the layer is "version-independent of the compositor binary". It is not, and
wsi_layer_matches_our_gamescope() exists because it is not. So the host was left
guessing from version triples, and that guess is wrong in both directions: a
distro at the same upstream tag that patched the protocol compares EQUAL and
keeps a layer that will kill every game, while a distro at a different tag with a
byte-identical protocol compares unequal and loses HDR for nothing. Since we pin
a rev, the second case is the normal one -- on essentially every box with a
distro gamescope, the layer was disabled and no game could render HDR.

Ship the layer instead. It is built from the same tree at the same rev as the
compositor, so the two cannot drift, and the guess stops being load-bearing. It
is installed under our own name (VK_LAYER_PUNKTFUNK_gamescope_wsi) at our own
path with our own enable/disable variables, so it coexists with the distro's
rather than colliding -- the Vulkan loader keys implicit layers on that name --
and the host switches the two independently in one session.

WsiPlan makes the three states explicit and resolves them once per launch, since
the fallback spawns `--version` probes:
  Ours            our layer is installed: enable it, force the distro's off
  DistroKept      no layer of ours, distro's looks compatible: touch nothing
  DistroDisabled  no layer of ours, distro's untrusted: today's behaviour

That last arm is the fail-safe. A host newer than its gamescope package behaves
exactly as it does today rather than enabling a layer that is not there, so this
can roll out one packaging surface at a time without a flag day.

Only the Arch PKGBUILD carries the new files so far. The rpm path takes a
CI-cached binary rather than the build script's stage dir, so it needs the cache,
build-gamescope-rpm.sh and the spec moved together; the deb, both sysexts and
gamescope.nix need the same two files added. Until each lands, those boxes take
the DistroDisabled arm and are no worse off than before.

Verified: 214 pf-vdisplay tests pass in a linux container (including a new one
pinning that the Ours arm enables ours AND forces the distro's off together --
either half alone is a bug), clippy -D warnings and rustfmt clean, both shell
files pass bash -n, and the manifest rewrite was run against a synthetic FROG
manifest to confirm it renames/repoints/regates while preserving the `functions`
block. NOT verified: an actual gamescope build, any package build, or a game
taking an HDR swapchain on glass.
fix(packaging): every channel ships the WSI layer, so in-game HDR works off a stock install
ci / web (pull_request) Successful in 1m0s
ci / bun-nix (pull_request) Successful in 2m6s
ci / rust-arm64 (pull_request) Successful in 3m29s
ci / docs-site (pull_request) Successful in 3m53s
android / android (pull_request) Successful in 5m4s
ci / rust (pull_request) Successful in 5m9s
apple / swift (pull_request) Successful in 2m0s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
nix / flake (pull_request) Failing after 6m17s
082c65755f
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.
enricobuehler merged commit fb33555059 into main 2026-08-13 21:50:13 +00:00
enricobuehler deleted branch worktree-hdr-colour-anchor-fixes 2026-08-13 21:50:14 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#210