HDR was a stop bright, in-game HDR was unreachable on every distro, and SDR shipped untagged #210
Merged
enricobuehler
merged 5 commits from 2026-08-13 21:50:13 +00:00
worktree-hdr-colour-anchor-fixes into main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
082c65755f |
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
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.
|
||
|
|
3ac4548cf8 |
fix(gamescope): ship the WSI layer built beside our compositor, instead of guessing at the distro's
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. |
||
|
|
a190fdb1ae |
fix(apple/tvos): an SDR stream forced the TV into HDR10 output
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. |
||
|
|
94f049ba70 |
fix(apple): the SDR layer shipped untagged, because an SDR session never configured colour
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. |
||
|
|
c29e720324 |
fix(gamescope): HDR sessions anchored SDR white a stop bright, and never said game HDR was unreachable
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. |