From e1adc5d6d767d777db00c8dd82a17b679ab44f70 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 5 Aug 2026 23:59:45 +0200 Subject: [PATCH 1/2] fix(flatpak): export GAMESCOPE_WAYLAND_DISPLAY so the Deck actually gets HDR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packaging/flatpak/io.unom.Punktfunk.yml | 42 ++++++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml index 3d5b75ed..4aafa891 100644 --- a/packaging/flatpak/io.unom.Punktfunk.yml +++ b/packaging/flatpak/io.unom.Punktfunk.yml @@ -93,20 +93,38 @@ finish-args: # /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 - # THREE things are needed, not two (verified live on a Deck OLED — the env vars alone left - # hdr10_format=None). (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 flips the layer's own `enable_environment` gate. (3) The layer, once - # loaded, must open a *Wayland* connection to gamescope's private socket ($GAMESCOPE_WAYLAND_DISPLAY - # = gamescope-0) to negotiate the HDR10 colorspace via the gamescope_swapchain protocol — but the - # Deck runs games as X11 clients (DISPLAY=:1, no WAYLAND_DISPLAY exported), so --socket=wayland - # binds nothing and that socket never enters the sandbox. Without it the layer loads, maps, and - # silently can't reach the compositor → no HDR10 offered → PQ tone-mapped to SDR, badge dark. - # Binding xdg-run/gamescope-0 is the missing half (chiaki-ng does the same). With all three the - # surface offers HDR10 and the presenter's existing HDR10 swapchain path engages — no client code - # change. Harmless off-Deck: the layer no-ops when there's no gamescope socket to bind. + # 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 + # 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 + # --socket=wayland binds nothing (chiaki-ng does the same). (4) GAMESCOPE_WAYLAND_DISPLAY must be + # set INSIDE the sandbox. The layer's `isRunningUnderGamescope()` reads that env var and nothing + # else; flatpak does not forward host env, so it arrives unset and the layer's CreateInstance + # early-returns before it ever creates a GamescopeInstance. The layer still LOADS and still logs + # its generic bits ("Forcing on VK_EXT_swapchain_maintenance1", swapchain destroys), which is why + # this reads as working — but no gamescope surface is made, so no HDR10 format is ever appended + # and (1)-(3) buy nothing. Measured on a Deck OLED (Galileo, SteamOS 3.8.16) 2026-08-05, client + # `--browse`, reading `pf_presenter::vk::setup` "swapchain config": + # unset -> no "[Gamescope WSI] Surface state" block at all, hdr10_format=None + # set, hdr_enabled=0 -> "server hdr output enabled: false", hdr10_format=None + # set, hdr_enabled=1 -> "hdr formats exposed to client: true", + # hdr10_format=Some(A2B10G10R10_UNORM_PACK32, HDR10_ST2084_EXT) + # DXVK_HDR is NOT the gate for us and was ruled out by measurement: the layer forces it OFF for + # clients it has already decided to deny, it does not turn HDR on. + # Hardcoding `gamescope-0` matches the socket bound just below, and is safe off-Deck both ways: + # on a normal Wayland desktop --socket=wayland sets WAYLAND_DISPLAY=wayland-0 inside the sandbox + # and the layer bails on the mismatch; on X11-only there is no gamescope socket to connect to, so + # it prints one "Bypass layer will be unavailable" line and passes through. + # 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) build-options: From 5f71aeb02440050a474b92fc3092f7fdf8866263 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 00:15:16 +0200 Subject: [PATCH 2/2] 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