diff --git a/.gitea/workflows/rpm.yml b/.gitea/workflows/rpm.yml index b39c6040..004bc787 100644 --- a/.gitea/workflows/rpm.yml +++ b/.gitea/workflows/rpm.yml @@ -247,6 +247,31 @@ jobs: echo "::warning::no usable punktfunk-gamescope for f${{ matrix.fedver }} — skipping its RPM" fi + # A SECOND signing pass, for this package only. The main "Sign RPMs" step ran back at build + # time, long before this RPM existed — the gamescope build sits behind its own ~10-minute + # cache and deliberately runs after the host RPMs are already published. So every + # punktfunk-gamescope RPM went to the registry UNSIGNED, and the repo file we tell users to + # install carries gpgcheck=1: `dnf install punktfunk-gamescope` failed with "The package is + # not signed" on every Fedora and Nobara box. The package was in the channel the whole time + # and could not be installed from it — which is worse than absent, because the release notes + # and the docs-site both say it is there. + # + # Same fail-closed rule as the first pass: sign-rpms.sh hard-fails on refs/tags/v* if the org + # secret is missing, rather than republishing something a user's dnf will reject. + - name: Sign punktfunk-gamescope + env: + RPM_GPG_PRIVATE_KEY: ${{ secrets.RPM_GPG_PRIVATE_KEY }} + RPM_GPG_PASSPHRASE: ${{ secrets.RPM_GPG_PASSPHRASE }} + run: | + shopt -s nullglob + rpms=(dist/punktfunk-gamescope-*.rpm) + # No RPM here is the best-effort skip above, already warned about — not a signing failure. + if [ "${#rpms[@]}" -eq 0 ]; then + echo "no punktfunk-gamescope RPM to sign (see the packaging step above)" + exit 0 + fi + bash packaging/rpm/sign-rpms.sh "${rpms[@]}" + - name: Publish punktfunk-gamescope to the Gitea RPM registry env: TOKEN: ${{ secrets.REGISTRY_TOKEN }} diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml index 67cfac28..816f7a94 100644 --- a/packaging/flatpak/io.unom.Punktfunk.yml +++ b/packaging/flatpak/io.unom.Punktfunk.yml @@ -272,7 +272,7 @@ modules: - type: git url: https://github.com/ValveSoftware/gamescope.git # KEEP IN SYNC with `_gsrev` in packaging/gamescope/PKGBUILD. - commit: 8c676c399c761e4540587f61004c957993d12fea + commit: 5fb8dce4a09d0a68d097b9faf9513782106bc843 # Wrap pins as of that rev (`subprojects/*.wrap`). These are meson WRAPS, not gamescope # submodules, so nothing else populates them and they need explicit sources. # diff --git a/packaging/gamescope/PKGBUILD b/packaging/gamescope/PKGBUILD index 4d847e18..c86767e1 100644 --- a/packaging/gamescope/PKGBUILD +++ b/packaging/gamescope/PKGBUILD @@ -18,11 +18,17 @@ pkgname=punktfunk-gamescope # The `.pfhdrN` suffix is the patch-set revision the binary stamps into its banner (see README.md); # bump it with the marker so pacman sees a new version when only our patches moved. _gsver=3.16.25 -_gsrev=8c676c399c761e4540587f61004c957993d12fea +_gsrev=5fb8dce4a09d0a68d097b9faf9513782106bc843 pkgver="${_gsver}.pfhdr4" # 2: patch 0006 (never destroy the Vulkan device/output at exit). No capability moved, so the # `.pfhdrN` level deliberately stays put — see README.md. -pkgrel=2 +# 3: pin moved 8c676c39 -> 5fb8dce4 (3.16.25-1 -> 3.16.25-11), which brings upstream's own +# `vulkan_get_rgb10_capture_format()` — the XBGR2101010 fallback for devices with no linear-tiled +# A2R10G10B10 storage (every NVIDIA). That fixes the NV12/P010 capture intermediate and AVIF +# screenshots, which are upstream's paths, not ours. Patch 0001 additionally now offers +# `xBGR_210LE` BEFORE `xRGB_210LE` so a third-party consumer cannot pick the one NVIDIA fills +# byte-reversed. Still no capability the host probes for, so `.pfhdrN` stays at 4. +pkgrel=3 pkgdesc="gamescope with 10-bit BT.2020/PQ PipeWire capture, for punktfunk HDR streaming" arch=('x86_64' 'aarch64') url="https://git.unom.io/unom/punktfunk" diff --git a/packaging/gamescope/README.md b/packaging/gamescope/README.md index 056ca35d..3901074b 100644 --- a/packaging/gamescope/README.md +++ b/packaging/gamescope/README.md @@ -97,14 +97,21 @@ distro's `gamescope`. ## Building -Pinned upstream: `8c676c39` (master, 2026-07-27 — tags through 3.16.25). The patches apply -cleanly to that commit; they touch `src/pipewire.cpp`, `src/steamcompmgr.cpp` and -`src/meson.build` only. +Pinned upstream: `5fb8dce4` (master, 2026-08-03 — `3.16.25-11-g5fb8dce`). The patches apply +cleanly to that commit; they touch `src/pipewire.cpp`, `src/steamcompmgr.cpp`, +`src/rendervulkan.cpp`, `src/rendervulkan.hpp` and `src/meson.build` only. + +The bump from `8c676c39` is deliberate: it brings upstream's `vulkan_get_rgb10_capture_format()` +(`ff6b924`), which probes `linearTilingFeatures` for STORAGE+SAMPLED and falls back to +`DRM_FORMAT_XBGR2101010` on devices that cannot do linear-tiled `A2R10G10B10` — i.e. every +NVIDIA. That covers the paths that are upstream's rather than ours: the RGB intermediate +`paint_pipewire()` acquires when the stream is YCbCr, and AVIF screenshots. Our own 10-bit RGB +node is covered by patch `0001`, which offers `xBGR_210LE` first for the same reason. ```sh git clone https://github.com/ValveSoftware/gamescope.git cd gamescope -git checkout 8c676c39 +git checkout 5fb8dce4 git submodule update --init --recursive # or let meson fetch the subprojects git am /path/to/punktfunk/packaging/gamescope/patches/*.patch diff --git a/packaging/gamescope/build-punktfunk-gamescope.sh b/packaging/gamescope/build-punktfunk-gamescope.sh index 2f15545d..f2ed3075 100755 --- a/packaging/gamescope/build-punktfunk-gamescope.sh +++ b/packaging/gamescope/build-punktfunk-gamescope.sh @@ -25,7 +25,7 @@ set -euo pipefail # The pinned upstream. Bump together with the patches (they are `git am`-able and rebase cheaply — # two files, mirroring code that already exists in-tree; see README.md). -GAMESCOPE_REV="8c676c399c761e4540587f61004c957993d12fea" +GAMESCOPE_REV="5fb8dce4a09d0a68d097b9faf9513782106bc843" GAMESCOPE_REPO="https://github.com/ValveSoftware/gamescope.git" REV="$GAMESCOPE_REV" PREFIX=/usr DESTDIR="" SRCDIR="" JOBS="" SETCAP=1 diff --git a/packaging/gamescope/patches/0001-pipewire-offer-10-bit-BT.2020-PQ-capture-formats-HDR.patch b/packaging/gamescope/patches/0001-pipewire-offer-10-bit-BT.2020-PQ-capture-formats-HDR.patch index 81708ccc..aae821fe 100644 --- a/packaging/gamescope/patches/0001-pipewire-offer-10-bit-BT.2020-PQ-capture-formats-HDR.patch +++ b/packaging/gamescope/patches/0001-pipewire-offer-10-bit-BT.2020-PQ-capture-formats-HDR.patch @@ -32,12 +32,12 @@ follow what the app happens to render. Works on the headless backend as well as a real connector: no HDR display is involved anywhere in the LUT set. --- - src/pipewire.cpp | 114 +++++++++++++++++++++++++++++++++---------- + src/pipewire.cpp | 123 ++++++++++++++++++++++++++++++++++--------- src/steamcompmgr.cpp | 21 ++++++-- - 2 files changed, 106 insertions(+), 29 deletions(-) + 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/src/pipewire.cpp b/src/pipewire.cpp -index 76b3ea8..6b56b01 100644 +index 76b3ea8..c84b19c 100644 --- a/src/pipewire.cpp +++ b/src/pipewire.cpp @@ -18,6 +18,40 @@ @@ -159,7 +159,7 @@ index 76b3ea8..6b56b01 100644 params.push_back((const struct spa_pod *) spa_pod_builder_pop(builder, &obj_frame)); // for (auto& param : params) -@@ -166,6 +209,14 @@ static std::vector build_format_params(struct spa_pod_bu +@@ -166,6 +209,23 @@ static std::vector build_format_params(struct spa_pod_bu build_format_params(builder, SPA_VIDEO_FORMAT_BGRx, params); build_format_params(builder, SPA_VIDEO_FORMAT_NV12, params); @@ -168,13 +168,22 @@ index 76b3ea8..6b56b01 100644 + // negotiates today's 8-bit stream keeps negotiating it bit-for-bit. Only a consumer that + // asks for a 10-bit format by name — and accepts the MANDATORY BT.2020 + PQ colorimetry + // above — ever reaches these. -+ build_format_params(builder, SPA_VIDEO_FORMAT_xRGB_210LE, params); ++ // ++ // xBGR_210LE FIRST, and that order is correctness, not style. A consumer takes the first pod ++ // it can use, and xBGR is the only one every vendor fills correctly: capture textures are ++ // mappable, hence linear-tiled, and linear STORAGE for A2R10G10B10 is an optional Vulkan ++ // feature NVIDIA does not implement — there the composite's `imageStore` lands in XBGR order, ++ // so a consumer that took xRGB_210LE gets a buffer LABELLED XRGB2101010 and FILLED as XBGR, ++ // i.e. red and blue swapped, with every format mapping on both ends individually correct. ++ // A2B10G10R10 is the universally supported packed-10 format, so leading with it costs nothing ++ // on AMD or Intel. xRGB_210LE stays as the second pod for a consumer that only speaks it. + build_format_params(builder, SPA_VIDEO_FORMAT_xBGR_210LE, params); ++ build_format_params(builder, SPA_VIDEO_FORMAT_xRGB_210LE, params); +#endif return params; } -@@ -288,7 +339,7 @@ static void dispatch_nudge(struct pipewire_state *state, int fd) +@@ -288,7 +348,7 @@ static void dispatch_nudge(struct pipewire_state *state, int fd) if (s_nCaptureWidth != state->video_info.size.width || s_nCaptureHeight != state->video_info.size.height) { pwr_log.debugf("renegotiating stream params (size: %dx%d)", s_nCaptureWidth, s_nCaptureHeight); @@ -183,7 +192,7 @@ index 76b3ea8..6b56b01 100644 struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); std::vector format_params = build_format_params(&builder); int ret = pw_stream_update_params(state->stream, format_params.data(), format_params.size()); -@@ -412,6 +463,12 @@ static void stream_handle_param_changed(void *data, uint32_t id, const struct sp +@@ -412,6 +472,12 @@ static void stream_handle_param_changed(void *data, uint32_t id, const struct sp state->video_info.size.width, state->video_info.size.height, s_nRequestedWidth, s_nRequestedHeight, state->video_info.format, state->shm_stride, shm_size, state->dmabuf); @@ -196,7 +205,7 @@ index 76b3ea8..6b56b01 100644 } static void randname(char *buf) -@@ -450,6 +507,11 @@ uint32_t spa_format_to_drm(uint32_t spa_format) +@@ -450,6 +516,11 @@ uint32_t spa_format_to_drm(uint32_t spa_format) switch (spa_format) { case SPA_VIDEO_FORMAT_NV12: return DRM_FORMAT_NV12; @@ -208,7 +217,7 @@ index 76b3ea8..6b56b01 100644 default: case SPA_VIDEO_FORMAT_BGR: return DRM_FORMAT_XRGB8888; } -@@ -715,7 +777,7 @@ bool init_pipewire(void) +@@ -715,7 +786,7 @@ bool init_pipewire(void) s_nOutputHeight = g_nOutputHeight; calculate_capture_size(); @@ -218,7 +227,7 @@ index 76b3ea8..6b56b01 100644 std::vector format_params = build_format_params(&builder); diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp -index ff9ae1f..01b2abf 100644 +index ecb3808..83c751b 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -2335,17 +2335,32 @@ static void paint_pipewire() diff --git a/packaging/gamescope/patches/0002-pipewire-optionally-composite-the-cursor-into-the-ca.patch b/packaging/gamescope/patches/0002-pipewire-optionally-composite-the-cursor-into-the-ca.patch index 32f2b1ce..d80b3096 100644 --- a/packaging/gamescope/patches/0002-pipewire-optionally-composite-the-cursor-into-the-ca.patch +++ b/packaging/gamescope/patches/0002-pipewire-optionally-composite-the-cursor-into-the-ca.patch @@ -58,7 +58,7 @@ index 9fc54f0..1eb35b3 100644 " If this is not set, and there is a HDR client, it will be tonemapped SDR.\n" " --sdr-gamut-wideness Set the 'wideness' of the gamut for SDR comment. 0 - 1.\n" diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp -index 01b2abf..5c65420 100644 +index 83c751b..a8a816f 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -2316,6 +2316,13 @@ static void update_touch_scaling( const struct FrameInfo_t *frameInfo ) @@ -146,9 +146,9 @@ index 01b2abf..5c65420 100644 + } + gamescope::Rc pRGBTexture = s_pPipewireBuffer->texture->isYcbcr() - ? vulkan_acquire_capture_texture( uWidth, uHeight, false, DRM_FORMAT_XRGB2101010 ) + ? vulkan_acquire_capture_texture( uWidth, uHeight, false, vulkan_get_rgb10_capture_format() ) : gamescope::Rc{ s_pPipewireBuffer->texture }; -@@ -8397,6 +8452,12 @@ steamcompmgr_main(int argc, char **argv) +@@ -8404,6 +8459,12 @@ steamcompmgr_main(int argc, char **argv) g_FadeOutDuration = atoi(optarg); } else if (strcmp(opt_name, "force-windows-fullscreen") == 0) { bForceWindowsFullscreen = true; diff --git a/packaging/gamescope/patches/0004-pipewire-optionally-composite-the-external-overlay-i.patch b/packaging/gamescope/patches/0004-pipewire-optionally-composite-the-external-overlay-i.patch index 924f5455..d26a3048 100644 --- a/packaging/gamescope/patches/0004-pipewire-optionally-composite-the-external-overlay-i.patch +++ b/packaging/gamescope/patches/0004-pipewire-optionally-composite-the-external-overlay-i.patch @@ -62,7 +62,7 @@ index 2c4fb50..b406caf 100644 " If this is not set, and there is a HDR client, it will be tonemapped SDR.\n" " --sdr-gamut-wideness Set the 'wideness' of the gamut for SDR comment. 0 - 1.\n" diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp -index 5c65420..0d293c6 100644 +index a8a816f..64e1a8c 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -2323,6 +2323,12 @@ gamescope::ConVar cv_pipewire_composite_cursor{ "pipewire_composite_cursor @@ -127,7 +127,7 @@ index 5c65420..0d293c6 100644 // The cursor, when this stream was asked for it. gamescope keeps the pointer OUT of the // PipeWire node by default — it lives on a hardware plane for scanout, and a remote-play // consumer that draws its own would end up with two — so a consumer that has no cursor of -@@ -8457,6 +8490,12 @@ steamcompmgr_main(int argc, char **argv) +@@ -8464,6 +8497,12 @@ steamcompmgr_main(int argc, char **argv) cv_pipewire_composite_cursor = true; #else fprintf( stderr, "gamescope: --pipewire-composite-cursor ignored (built without PipeWire)\n" ); diff --git a/packaging/gamescope/patches/0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch b/packaging/gamescope/patches/0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch index c8b449b9..5e9be9c3 100644 --- a/packaging/gamescope/patches/0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch +++ b/packaging/gamescope/patches/0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch @@ -1,4 +1,4 @@ -From 509fb928c7dc3307372629ca692f4c895c4fe984 Mon Sep 17 00:00:00 2001 +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrico=20B=C3=BChler?= Date: Sat, 8 Aug 2026 19:17:25 +0200 Subject: [PATCH] punktfunk: never destroy the Vulkan device or output at exit @@ -49,7 +49,7 @@ Both are needed: pinning only the device relocated the fault into 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp -index 5c2dd11..8cd5ca2 100644 +index 3f7ea90..0382a4b 100644 --- a/src/rendervulkan.cpp +++ b/src/rendervulkan.cpp @@ -118,7 +118,37 @@ static VkResult vulkan_load_module() @@ -102,10 +102,10 @@ index 5c2dd11..8cd5ca2 100644 static bool allDMABUFsEqual( wlr_dmabuf_attributes *pDMA ) { diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp -index b6749d4..a9335c4 100644 +index c49b95f..ee98b38 100644 --- a/src/rendervulkan.hpp +++ b/src/rendervulkan.hpp -@@ -564,7 +564,7 @@ enum ShaderType { +@@ -565,7 +565,7 @@ enum ShaderType { SHADER_TYPE_COUNT }; @@ -114,12 +114,9 @@ index b6749d4..a9335c4 100644 struct SamplerState { -@@ -1007,4 +1007,4 @@ void vulkan_wait_idle(); - // Whether the driver implements VK_EXT_physical_device_drm - bool vulkan_has_drm_props(); +@@ -1010,4 +1010,4 @@ bool vulkan_has_drm_props(); + + bool vulkan_has_drm_modifiers_for_features(VkFormat format, VkFormatFeatureFlags features); -extern CVulkanDevice g_device; +extern CVulkanDevice &g_device; --- -2.55.0 - diff --git a/packaging/nix/gamescope.nix b/packaging/nix/gamescope.nix index 55078b5f..f13bd953 100644 --- a/packaging/nix/gamescope.nix +++ b/packaging/nix/gamescope.nix @@ -20,6 +20,14 @@ # been stable across the 3.16 series (`src/pipewire.cpp`'s format builders, `paint_pipewire()` in # `src/steamcompmgr.cpp`), so this normally just works — and when it does not, the build fails # loudly at `patchPhase` rather than producing a gamescope that quietly cannot do HDR. +# +# ⚠️ Kept deliberately free of any dependency on the pinned rev. The pin moved past upstream's +# `vulkan_get_rgb10_capture_format()` (`ff6b924`, after 3.16.25) to fix red/blue on NVIDIA, and it +# would have been natural to have patch `0001` call it — that is what the host-side note in +# `crates/pf-capture/src/linux/pw_pods.rs` proposes. It does NOT, precisely so this derivation +# keeps building against a nixpkgs that still pins 3.16.25, where that symbol does not exist and +# the failure would be an opaque C++ error rather than a patch conflict. Patch `0001` gets the +# same outcome version-independently by offering `xBGR_210LE` ahead of `xRGB_210LE`. { lib, gamescope, diff --git a/packaging/rpm/sign-rpms.sh b/packaging/rpm/sign-rpms.sh index 7c577e30..51a13548 100755 --- a/packaging/rpm/sign-rpms.sh +++ b/packaging/rpm/sign-rpms.sh @@ -17,8 +17,24 @@ # can't supply a passphrase non-interactively here. # # Usage (in rpm.yml, after build-rpm.sh): RPM_GPG_PRIVATE_KEY=... bash packaging/rpm/sign-rpms.sh +# +# Takes the RPMs to sign as arguments, defaulting to `dist/*.rpm`. The argument form exists because +# punktfunk-gamescope is built LATER in the job than the host RPMs — it is a ~10-minute C++ build +# behind its own cache — so it misses the main signing pass entirely and needs a second one just +# for itself. It shipped unsigned into a `gpgcheck=1` repo that way, which made +# `dnf install punktfunk-gamescope` fail with "The package is not signed" for every Fedora/Nobara +# user: the package was in the channel and still uninstallable. set -euo pipefail +# Default target, and a real glob rather than a literal when nothing matched. +if [ "$#" -gt 0 ]; then + RPMS=("$@") +else + shopt -s nullglob + RPMS=(dist/*.rpm) +fi +[ "${#RPMS[@]}" -gt 0 ] || { echo "no RPMs to sign" >&2; exit 1; } + if [ -z "${RPM_GPG_PRIVATE_KEY:-}" ]; then case "${GITHUB_REF:-}" in refs/tags/v*) @@ -47,11 +63,11 @@ KEYID="$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit} # correctly. (A custom __gpg_sign_cmd passed via --define reached gpg with those filename macros # UNEXPANDED -> "No such file or directory".) Just point rpm at our key; the GNUPGHOME above # (passphrase-less key + loopback) lets gpg sign headless. -for rpm in dist/*.rpm; do +for rpm in "${RPMS[@]}"; do rpmsign --define "_gpg_name $KEYID" --addsign "$rpm" done # Verify locally so a bad signature fails the build before publishing. rpm --import <(gpg --export --armor "$KEYID") -rpmkeys --checksig dist/*.rpm -echo "signed + verified $(find dist -name '*.rpm' | wc -l) RPM(s) with key $KEYID" +rpmkeys --checksig "${RPMS[@]}" +echo "signed + verified ${#RPMS[@]} RPM(s) with key $KEYID"