feat(flatpak): vendor the gamescope WSI layer so Deck HDR works on a plain install

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.
This commit is contained in:
2026-08-06 00:15:16 +02:00
parent e1adc5d6d7
commit 5f71aeb024
+86 -11
View File
@@ -85,19 +85,17 @@ finish-args:
# --- persistent client identity / pairing store (shared with punktfunk-probe) ---
- --filesystem=~/.config/punktfunk:create # client-{cert,key}.pem, known-hosts, settings
# --- HDR under gamescope (Steam Deck Game Mode) ---
# A flatpak's Vulkan loader can't see the host's gamescope WSI layer, so the SDL3 surface never
# offers the HDR10 (ST.2084) colorspace and the presenter silently tone-maps PQ->SDR — the
# Game-Mode HDR indicator stays dark (verified on a Deck OLED: the sandbox loader found NO
# frog/gamescope layer). The layer ships as the runtime extension
# `org.freedesktop.Platform.VulkanLayer.gamescope`, which org.gnome.Platform//50 auto-mounts at
# /usr/lib/extensions/vulkan/gamescope once installed (a one-time, per-Deck step; keep it in the
# Decky plugin's setup / docs):
# flatpak install --user -y flathub org.freedesktop.Platform.VulkanLayer.gamescope//25.08
# A flatpak's Vulkan loader can't see the host's gamescope WSI layer, so without help the SDL3
# surface never offers the HDR10 (ST.2084) colorspace and the presenter silently tone-maps
# PQ->SDR — the field-reported "HDR->SDR" badge. The layer is now VENDORED (see the
# gamescope-wsi-layer module below), so it is always present and there is no longer any
# manual `flatpak install ... VulkanLayer.gamescope` step for the user.
# FOUR things are needed. An earlier revision of this block claimed three and was WRONG: the
# fourth is the gate that makes the other three moot, so the Deck sat at hdr10_format=None with
# all of (1)-(3) in place, which is exactly the field report ("HDR->SDR" in the stats overlay).
# (1) VK_ADD_IMPLICIT_LAYER_PATH puts the layer's implicit-layer JSON on the Vulkan loader's
# search path (the runtime point mounts the files but not onto the path). (2) ENABLE_GAMESCOPE_WSI
# (1) the layer's implicit-layer JSON must be on the Vulkan loader's search path — now
# automatic, the vendored module installs it to /app/share/vulkan/implicit_layer.d which
# XDG_DATA_DIRS already covers. (2) ENABLE_GAMESCOPE_WSI
# flips the layer's own `enable_environment` gate. (3) --filesystem=xdg-run/gamescope-0 binds
# gamescope's private Wayland socket: the layer must reach the compositor over it to negotiate
# HDR10, and the Deck runs games as X11 clients (DISPLAY=:1, no WAYLAND_DISPLAY exported) so
@@ -122,7 +120,6 @@ finish-args:
# The REMAINING gate is not ours: gamescope's `hdr_enabled` convar (Steam's HDR display setting)
# drives the GAMESCOPE_HDR_OUTPUT_FEEDBACK X property the layer reads, and with it off no app on
# the Deck gets HDR. See docs — that one is a user/Decky-side step, not a packaging one.
- --env=VK_ADD_IMPLICIT_LAYER_PATH=/usr/lib/extensions/vulkan/gamescope/share/vulkan/implicit_layer.d
- --env=ENABLE_GAMESCOPE_WSI=1
- --env=GAMESCOPE_WAYLAND_DISPLAY=gamescope-0 # the layer's ONLY "am I under gamescope?" signal
- --filesystem=xdg-run/gamescope-0 # gamescope's private Wayland socket (HDR negotiation)
@@ -198,6 +195,84 @@ modules:
cleanup:
- '*'
# ---------------------------------------------------------------------------------------
# gamescope WSI layer — VENDORED, so HDR works from a plain `flatpak install` with no
# second step. This is the ONLY route to HDR on a Deck: 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` (checked with HDR both off and on), so Mesa's Wayland WSI
# has no colour-management protocol to negotiate HDR10 through and only this layer can add
# the ST.2084 surface formats. Without it: zero `[Gamescope WSI]` lines, hdr10_format=None.
#
# It used to come from the flathub runtime extension
# `org.freedesktop.Platform.VulkanLayer.gamescope`, which every user had to install BY HAND
# (documented only in a comment here — so in practice nobody did, and the field report was
# "HDR->SDR" in the stats overlay). Vendoring instead of `add-extensions` autodownload,
# deliberately: that 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, and it would make flathub a hard install-time dependency of an
# app we self-host on flatpak.unom.io.
#
# Pinned to the SAME gamescope rev as packaging/gamescope/PKGBUILD (`_gsrev`) so the
# client's layer and the host's punktfunk-gamescope always come from one tree — bump both
# together. `enable_gamescope=false` skips subdir('src') and every compositor dependency
# (wlroots, SDL2, libliftoff, ...); only protocol/ and layer/ are built.
#
# `buildsystem: simple` rather than `meson` because two subprojects need their wrap
# `patch_directory` applied by hand: glm and stb ship NO meson.build of their own, and the
# one meson would normally inject lives in subprojects/packagefiles/. Cloning them as plain
# sources without that copy fails at configure with "Subproject exists but has no
# meson.build file". `--wrap-mode=nodownload` then proves the build is genuinely offline.
#
# The layer JSON is generated by meson from prefix+libdir, so it self-writes
# `library_path: /app/lib/libVkLayer_FROG_gamescope_wsi_x86_64.so` and lands in
# /app/share/vulkan/implicit_layer.d — already on the loader's search path via
# XDG_DATA_DIRS, which is why no VK_ADD_IMPLICIT_LAYER_PATH is needed (and why it was
# dropped from finish-args: pointing at the old /usr extension path too would risk
# double-loading two layers of the same name).
#
# Verified on a Deck OLED 2026-08-05: builds offline in org.gnome.Sdk//50, and the
# resulting .so drives the Deck's system gamescope to
# "hdr formats exposed to client: true" + hdr10_format=Some(...).
# ---------------------------------------------------------------------------------------
- name: gamescope-wsi-layer
buildsystem: simple
build-commands:
# Apply the wraps' patch_directory by hand (see above) — these supply the meson.build
# that glm and stb do not ship themselves.
- cp -r subprojects/packagefiles/glm/. subprojects/glm/
- cp -r subprojects/packagefiles/stb/. subprojects/stb/
- meson setup _build --prefix=/app --libdir=lib --wrap-mode=nodownload
-Denable_gamescope=false -Denable_gamescope_wsi_layer=true
-Denable_tests=false -Denable_openvr_support=false
- ninja -C _build
- ninja -C _build install
sources:
- type: git
url: https://github.com/ValveSoftware/gamescope.git
# KEEP IN SYNC with `_gsrev` in packaging/gamescope/PKGBUILD.
commit: 8c676c399c761e4540587f61004c957993d12fea
# Submodule + wrap pins as of that rev. `git ls-tree <rev> subprojects/` for the
# submodules; subprojects/*.wrap for the rest.
- type: git
url: https://github.com/Joshua-Ashton/vkroots.git
commit: 5106d8a0df95de66cc58dc1ea37e69c99afc9540
dest: subprojects/vkroots
- type: git
url: https://github.com/g-truc/glm.git
commit: 0af55ccecd98d4e5a8d1fad7de25ba429d60e863
dest: subprojects/glm
- type: git
url: https://github.com/nothings/stb.git
commit: 5736b15f7ea0ffb08dd38af21067c314d6a3aae9
dest: subprojects/stb
cleanup:
# Only the .so and its implicit-layer JSON are runtime. vkroots installs its dev files
# from the subproject, and the layer-only install still drops gamescope's display .lua
# scripts + LUT .cube files, none of which a client uses.
- /include
- /lib/pkgconfig
- /share/gamescope
# ---------------------------------------------------------------------------------------
# The client. cargo-sources.json is the GENERATED offline crate cache:
# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/flatpak/cargo-sources.json