fix(flatpak): Deck HDR works on a plain install #64

Merged
enricobuehler merged 2 commits from worktree-deck-hdr-wsi-env into main 2026-08-05 22:21:53 +00:00
Owner

Field report on 0.23.0: HDR not working, streaming from a SteamOS host to a Deck OLED. Confirmed on-glass as HDR→SDR in the stats overlay.

The host was clean — the host log proves PQ capture (transfer_function=14, color_primaries=7), HEVC Main10, SPS VUI CICP 9/16/9, HDR10 metadata sent, and the client advertising video_caps=59. The break was entirely in client-side presentation.

Root cause

gamescope's WSI layer decides whether to engage from one signal: isRunningUnderGamescope() reads GAMESCOPE_WAYLAND_DISPLAY and nothing else. flatpak does not forward host env into the sandbox, so it arrived unset, the layer's CreateInstance early-returned before creating a GamescopeInstance, no gamescope surface was ever made, and the HDR10/ST.2084 formats were never appended.

The trap: the layer still loads and still logs its generic bits in that state (Forcing on VK_EXT_swapchain_maintenance1, swapchain destroys), so it reads as working. The discriminator is the [Gamescope WSI] Surface state: block — absent means the bypass never engaged.

The three settings already in the manifest all sit downstream of that gate and bought nothing without it. The old comment asserted "THREE things are needed"; that was wrong, and is why this survived a previous on-glass session.

Measured on a Deck OLED (Galileo, SteamOS 3.8.16), client --browse

Reading pf_presenter::vk::setup "swapchain config":

GAMESCOPE_WAYLAND_DISPLAY hdr_enabled layer ext result
unset 0 installed no Surface state block at all → hdr10_format=None
set 0 installed server hdr output enabled: falseNone
set 1 installed hdr formats exposed to client: trueSome(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT)
set 1 removed zero [Gamescope WSI] lines → None

DXVK_HDR was ruled out by measurement — the layer only force-sets it to 0 for clients it has already denied.

Second half: the layer is now vendored

That last row is the other half of the problem. The layer came from the flathub runtime extension org.freedesktop.Platform.VulkanLayer.gamescope, which every user had to install by hand — documented only in a comment in this file, so in practice nobody did.

The layer is genuinely required, not legacy. On this Deck the gamescope-0 socket advertises gamescope_swapchain_factory_v2 but not wp_color_manager_v1, with HDR both off and on — so Mesa's Wayland WSI has no colour-management protocol to negotiate HDR10 through. (The gamescope binary contains wp_color_manager_v1 strings but never advertises it; strings misleads here.)

Vendored rather than declared via add-extensions autodownload, deliberately:

  • the extension is 94 MB of whole-gamescope to deliver one 4 MB .so
  • its layer JSON hardcodes a /usr library_path that an app-scoped extension (mounted under /app) would not satisfy
  • it would make flathub a hard install-time dependency of an app we self-host on flatpak.unom.io

Build notes:

  • -Denable_gamescope=false skips subdir('src') and every compositor dependency (wlroots, SDL2, libliftoff, …) — only protocol/ and layer/ build
  • buildsystem: simple rather than meson, because glm and stb ship no meson.build of their own; the wraps' patch_directory supplies it, and without that copy configure dies with "Subproject exists but has no meson.build file"
  • meson generates the layer JSON from prefix+libdir, so it self-writes library_path: /app/lib/… into /app/share/vulkan/implicit_layer.d, already covered by XDG_DATA_DIRS. VK_ADD_IMPLICIT_LAYER_PATH is therefore dropped — keeping it would risk double-loading two same-named layers for anyone who still has the extension installed
  • pinned to the same gamescope rev as packaging/gamescope/PKGBUILD (8c676c39), so the client's layer and the host's punktfunk-gamescope come from one tree — bump both together

Verification

  • builds fully offline (--wrap-mode=nodownload) in org.gnome.Sdk//50 on the Deck
  • the resulting .so, staged against the Deck's system gamescope 3.16.23.4, drives it to hdr formats exposed to client: true with hdr10_format=Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT) — so our build works, not just flathub's

Not verified: the module has never been through a real flatpak-builder run — only meson-in-SDK. flatpak.yml publishes canary on manifest push, so that is the gate this needs.

Known remaining gate, not fixable here

gamescope's hdr_enabled convar (Steam's HDR display setting) drives the GAMESCOPE_HDR_OUTPUT_FEEDBACK X property the layer reads. With it off, no app on the Deck gets HDR, ours included. So this makes HDR work on a plain install provided HDR is on in Steam's Display settings. Flipping a user's display setting is not something a packaging change should do.

Still undocumented in docs-site (hdr.md, steam-deck.md, troubleshooting) — worth a follow-up.

Manifest-only; no client code changes.

Field report on 0.23.0: HDR not working, streaming from a SteamOS host to a Deck OLED. Confirmed on-glass as `HDR→SDR` in the stats overlay. The host was clean — the host log proves PQ capture (`transfer_function=14`, `color_primaries=7`), HEVC Main10, SPS VUI CICP 9/16/9, HDR10 metadata sent, and the client advertising `video_caps=59`. The break was entirely in client-side presentation. ## Root cause gamescope's WSI layer decides whether to engage from one signal: `isRunningUnderGamescope()` reads `GAMESCOPE_WAYLAND_DISPLAY` and nothing else. flatpak does not forward host env into the sandbox, so it arrived unset, the layer's `CreateInstance` early-returned before creating a `GamescopeInstance`, no gamescope surface was ever made, and the HDR10/ST.2084 formats were never appended. **The trap:** the layer still loads and still logs its generic bits in that state (`Forcing on VK_EXT_swapchain_maintenance1`, swapchain destroys), so it reads as working. The discriminator is the `[Gamescope WSI] Surface state:` block — absent means the bypass never engaged. The three settings already in the manifest all sit downstream of that gate and bought nothing without it. The old comment asserted "THREE things are needed"; that was wrong, and is why this survived a previous on-glass session. ## Measured on a Deck OLED (Galileo, SteamOS 3.8.16), client `--browse` Reading `pf_presenter::vk::setup` "swapchain config": | `GAMESCOPE_WAYLAND_DISPLAY` | `hdr_enabled` | layer ext | result | |---|---|---|---| | unset | 0 | installed | no `Surface state` block at all → `hdr10_format=None` | | set | 0 | installed | `server hdr output enabled: false` → `None` | | **set** | **1** | installed | `hdr formats exposed to client: true` → **`Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT)`** | | set | 1 | **removed** | zero `[Gamescope WSI]` lines → `None` | `DXVK_HDR` was ruled out by measurement — the layer only force-sets it to `0` for clients it has already denied. ## Second half: the layer is now vendored That last row is the other half of the problem. The layer came from the flathub runtime extension `org.freedesktop.Platform.VulkanLayer.gamescope`, which every user had to install **by hand** — documented only in a comment in this file, so in practice nobody did. The layer is genuinely required, not legacy. On this Deck the `gamescope-0` socket advertises `gamescope_swapchain_factory_v2` but **not** `wp_color_manager_v1`, with HDR both off and on — so Mesa's Wayland WSI has no colour-management protocol to negotiate HDR10 through. (The gamescope binary contains `wp_color_manager_v1` strings but never advertises it; `strings` misleads here.) Vendored rather than declared via `add-extensions` autodownload, deliberately: - the extension is 94 MB of whole-gamescope to deliver one 4 MB `.so` - its layer JSON hardcodes a `/usr` `library_path` that an app-scoped extension (mounted under `/app`) would not satisfy - it would make flathub a hard install-time dependency of an app we self-host on flatpak.unom.io Build notes: - `-Denable_gamescope=false` skips `subdir('src')` and every compositor dependency (wlroots, SDL2, libliftoff, …) — only `protocol/` and `layer/` build - `buildsystem: simple` rather than `meson`, because glm and stb ship no `meson.build` of their own; the wraps' `patch_directory` supplies it, and without that copy configure dies with *"Subproject exists but has no meson.build file"* - meson generates the layer JSON from prefix+libdir, so it self-writes `library_path: /app/lib/…` into `/app/share/vulkan/implicit_layer.d`, already covered by `XDG_DATA_DIRS`. `VK_ADD_IMPLICIT_LAYER_PATH` is therefore dropped — keeping it would risk double-loading two same-named layers for anyone who still has the extension installed - pinned to the same gamescope rev as `packaging/gamescope/PKGBUILD` (`8c676c39`), so the client's layer and the host's `punktfunk-gamescope` come from one tree — bump both together ## Verification - builds fully offline (`--wrap-mode=nodownload`) in `org.gnome.Sdk//50` on the Deck - the resulting `.so`, staged against the Deck's **system** gamescope 3.16.23.4, drives it to `hdr formats exposed to client: true` with `hdr10_format=Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT)` — so our build works, not just flathub's **Not verified:** the module has never been through a real `flatpak-builder` run — only meson-in-SDK. `flatpak.yml` publishes canary on manifest push, so that is the gate this needs. ## Known remaining gate, not fixable here gamescope's `hdr_enabled` convar (Steam's HDR display setting) drives the `GAMESCOPE_HDR_OUTPUT_FEEDBACK` X property the layer reads. With it off, no app on the Deck gets HDR, ours included. So this makes HDR work on a plain install **provided HDR is on in Steam's Display settings**. Flipping a user's display setting is not something a packaging change should do. Still undocumented in docs-site (`hdr.md`, `steam-deck.md`, troubleshooting) — worth a follow-up. Manifest-only; no client code changes.
enricobuehler added 2 commits 2026-08-05 22:18:42 +00:00
The gamescope WSI layer decides whether to engage from one signal:
isRunningUnderGamescope() reads $GAMESCOPE_WAYLAND_DISPLAY and nothing
else. flatpak does not forward host env into the sandbox, so it arrived
unset and the layer's CreateInstance early-returned before creating a
GamescopeInstance — no gamescope surface, so the HDR10/ST.2084 formats
were never appended and the surface stayed SDR.

The layer still loads and still logs its generic bits in that state, so
it reads as working. It is not: the three settings already here (layer
search path, ENABLE_GAMESCOPE_WSI, the socket bind) all sit downstream
of this gate and buy nothing without it.

Measured on a Deck OLED (Galileo, SteamOS 3.8.16), client --browse,
reading "swapchain config":
  unset              -> no [Gamescope WSI] Surface state block, None
  set, hdr_enabled=0 -> server hdr output enabled: false, None
  set, hdr_enabled=1 -> hdr formats exposed to client: true,
                        Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT)

Matches the field report of "HDR->SDR" in the stats overlay on a
correct HDR host. DXVK_HDR was ruled out by measurement. The remaining
gate (gamescope's hdr_enabled convar = Steam's HDR display setting) is
a user-side step, not a packaging one.
feat(flatpak): vendor the gamescope WSI layer so Deck HDR works on a plain install
ci / web (pull_request) Successful in 56s
ci / docs-site (pull_request) Successful in 1m6s
ci / rust-arm64 (pull_request) Successful in 2m10s
ci / rust (pull_request) Successful in 7m59s
5f71aeb024
HDR on a Deck needed a manual second step nobody took:
  flatpak install --user flathub org.freedesktop.Platform.VulkanLayer.gamescope//25.08
documented only in a comment in this file. Build the layer ourselves
instead, so a plain `flatpak install` is all it takes.

The layer is genuinely required, not legacy. Measured on SteamOS 3.8.16
(gamescope 3.16.23.4): the gamescope-0 socket advertises
gamescope_swapchain_factory_v2 but NOT wp_color_manager_v1, with HDR both
off and on — so Mesa's Wayland WSI has no colour-management protocol to
negotiate HDR10 through, and this layer is the only thing that can append
the ST.2084 surface formats. Removing the extension gives zero
[Gamescope WSI] lines and hdr10_format=None.

Vendored rather than declared via add-extensions autodownload: the
extension is 94 MB of whole-gamescope for one 4 MB .so, its layer JSON
hardcodes a /usr library_path that an app-scoped extension mounted under
/app would not satisfy, and it would make flathub a hard install-time
dependency of an app we self-host on flatpak.unom.io.

enable_gamescope=false skips subdir('src') and every compositor
dependency, so only protocol/ and layer/ build. buildsystem is simple
rather than meson because glm and stb ship no meson.build of their own -
the wraps' patch_directory supplies it, and without that copy configure
dies with "Subproject exists but has no meson.build file".

meson generates the layer JSON from prefix+libdir, so it self-writes
library_path=/app/lib/... into /app/share/vulkan/implicit_layer.d, which
XDG_DATA_DIRS already covers. VK_ADD_IMPLICIT_LAYER_PATH is therefore
dropped - keeping it would also risk double-loading two same-named layers
for anyone who still has the flathub extension installed.

Pinned to the same gamescope rev as packaging/gamescope/PKGBUILD so the
client's layer and the host's punktfunk-gamescope come from one tree.

Verified on a Deck OLED: builds offline (--wrap-mode=nodownload) in
org.gnome.Sdk//50, and the resulting .so drives the Deck's system
gamescope to "hdr formats exposed to client: true" with
hdr10_format=Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT).

Still user-side, and not fixable in packaging: gamescope's hdr_enabled
convar (Steam's HDR display setting) must be on.
enricobuehler merged commit 35ba64ca0f into main 2026-08-05 22:21:53 +00:00
enricobuehler deleted branch worktree-deck-hdr-wsi-env 2026-08-05 22:21:53 +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#64