From 5f71aeb02440050a474b92fc3092f7fdf8866263 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 00:15:16 +0200 Subject: [PATCH] feat(flatpak): vendor the gamescope WSI layer so Deck HDR works on a plain install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packaging/flatpak/io.unom.Punktfunk.yml | 97 ++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml index 4aafa891..6f27c165 100644 --- a/packaging/flatpak/io.unom.Punktfunk.yml +++ b/packaging/flatpak/io.unom.Punktfunk.yml @@ -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 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