diff --git a/packaging/gamescope/PKGBUILD b/packaging/gamescope/PKGBUILD index c4aadf0a..4d847e18 100644 --- a/packaging/gamescope/PKGBUILD +++ b/packaging/gamescope/PKGBUILD @@ -20,7 +20,9 @@ pkgname=punktfunk-gamescope _gsver=3.16.25 _gsrev=8c676c399c761e4540587f61004c957993d12fea pkgver="${_gsver}.pfhdr4" -pkgrel=1 +# 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 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 d1b9bd84..056ca35d 100644 --- a/packaging/gamescope/README.md +++ b/packaging/gamescope/README.md @@ -16,6 +16,7 @@ The patches here add the missing half, and nothing else. See | `0003-headless-advertise-the-virtual-display-s-mode-and-re.patch` | Give `CHeadlessConnector` a real `GetModes()` + `GetValidDynamicRefreshRates()` from the resolved `-W`/`-H`/`-r`, report `GAMESCOPE_SCREEN_TYPE_EXTERNAL` so `update_mode_atoms` publishes the list, and add `--custom-refresh-rates` | **Yes** — a headless session that cannot report its own mode is a plain bug | | `0004-pipewire-optionally-composite-the-external-overlay-i.patch` | `--pipewire-composite-external-overlay` (off by default): paint the external overlay layer (mangoapp — the fps/stats readout) into the capture stream | **Yes** — same shape as the cursor patch, same argument | | `0005-punktfunk-stamp-the-version-banner-with-pfhdrN.patch` | Append `+pfhdr` to the `--version` banner | **No** — ours only, retired when the functional patches above land upstream | +| `0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch` | Give `g_device` and `g_output` storage that is never destroyed, so their destructors cannot call a Vulkan driver glibc has already unloaded at `exit()` | **Yes** — a plain static-destruction-order bug, not punktfunk-specific | ### Why the headless patch matters @@ -59,6 +60,12 @@ The number is a **monotonic patch-set revision**, so one probe answers every cap Bump it whenever a patch adds or changes something the host must know about before it spawns. +A patch that only fixes a crash does **not** bump it: `0006` (the exit-time Vulkan teardown fix) +changes nothing the host probes for, so the level stays `+pfhdr4` and the rebuild ships as a +`pkgrel` bump instead — exactly the split the PKGBUILD's own comment describes. Bumping the level +for a bugfix would be worse than useless: it would advertise a capability tier that does not exist +and strand hosts that gate on it. + ⚠️ The two indirect spawn modes (the `GAMESCOPE_BIN` wrapper for gamescope-session-plus, and the SteamOS PATH shim) pass these flags through `PF_HDR_ARGS`, so they share one dependency: if the session ignores `GAMESCOPE_BIN`/`PATH` and execs the distro's gamescope, it gets neither the HDR 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 new file mode 100644 index 00000000..c8b449b9 --- /dev/null +++ b/packaging/gamescope/patches/0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch @@ -0,0 +1,125 @@ +From 509fb928c7dc3307372629ca692f4c895c4fe984 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 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Every gamescope session punktfunk spawns ended in SIGSEGV. It happened after +the compositor had already done its work — "Primary child shut down!", then a +coredump — so the stream itself looked fine and the crash only showed up as a +steady drip of coredumps and a non-zero exit from the spawn. + +The cause is static destruction order, not anything gamescope does wrong at +runtime. `g_device` (CVulkanDevice) and `g_output` (VulkanOutput_t) were plain +globals, so glibc ran their destructors from `__run_exit_handlers` once main() +returned. Those destructors call back into the driver: + + ~CVulkanCmdBuffer -> m_device->vk.FreeCommandBuffers(...) + ~CVulkanTexture -> vk.Destroy*(...) + +but the Vulkan ICD has already been torn down and unloaded by that point, so +each call jumps through a function pointer into an unmapped page. The faulting +address equals the instruction pointer, which is the signature of exactly that: + + #0 0x00007fe8fd1d1070 in ?? () + #1 CVulkanCmdBuffer::~CVulkanCmdBuffer at rendervulkan.cpp:1543 + #9 std::vector>::~vector (g_device+1792) + #10 CVulkanDevice::~CVulkanDevice at rendervulkan.hpp:768 + #11 __run_exit_handlers / exit() + +On NVIDIA it is 100% reproducible: +`gamescope --backend headless -W 1280 -H 720 -r 60 --xwayland-count 1 -- true` +exits 139 every time, and cleanly with this patch (5/5, plus 2/2 at the real +session's 2752x2064@120 --steam). + +Nothing needs freeing at that point. The process is exiting; the kernel +reclaims the device, its command buffers and every GPU allocation. So give both +objects storage that is constructed exactly as before but never destroyed — a +union member is destroyed only if the union's destructor says so, and ours +deliberately does not. `g_device` and `g_output` keep their names and types +(now references bound at constant-initialisation time), so no use site changes. + +Both are needed: pinning only the device relocated the fault into +~VulkanOutput_t, which is why this is a shared helper and not a one-off. +--- + src/rendervulkan.cpp | 35 +++++++++++++++++++++++++++++++++-- + src/rendervulkan.hpp | 4 ++-- + 2 files changed, 35 insertions(+), 4 deletions(-) + +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index 5c2dd11..8cd5ca2 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -118,7 +118,37 @@ static VkResult vulkan_load_module() + return s_result; + } + +-VulkanOutput_t g_output; ++// punktfunk: globals that own GPU objects must OUTLIVE static destruction. ++// ++// gamescope kept its Vulkan device and output as plain globals, so glibc ran their destructors ++// from `__run_exit_handlers` after main() returned. Those destructors call back into the driver ++// (~CVulkanCmdBuffer -> vk.FreeCommandBuffers, ~CVulkanTexture -> vk.Destroy*), but by then the ++// Vulkan ICD has already been torn down and unloaded, so the call jumps through a function ++// pointer into an unmapped page: SIGSEGV at exactly the address it tried to execute. On NVIDIA it ++// is 100% reproducible -- `gamescope --backend headless ... -- true` dies with exit 139 EVERY ++// time -- so every punktfunk gamescope session ended in a coredump. ++// ++// Nothing needs freeing at that point: the process is exiting and the kernel reclaims the device, ++// its command buffers and every GPU allocation. So give these objects storage that is constructed ++// exactly as before but NEVER destroyed. A union member is only destroyed if the union says so, ++// and ours deliberately does not. ++// ++// Fixing only one of them just moves the crash to the next global (verified: pinning the device ++// relocated the fault into ~VulkanOutput_t), which is why this is a shared helper rather than a ++// one-off. ++namespace ++{ ++ template ++ union CNoDestroy ++ { ++ T value; ++ CNoDestroy() : value() {} ++ ~CNoDestroy() {} // deliberately does NOT destroy `value` ++ }; ++} ++ ++namespace { CNoDestroy g_outputHolder; } ++VulkanOutput_t &g_output = g_outputHolder.value; + + uint32_t g_uCompositeDebug = 0u; + gamescope::ConVar cv_composite_debug{ "composite_debug", 0, "Debug composition flags" }; +@@ -1943,7 +1973,8 @@ void CVulkanCmdBuffer::insertBarrier(bool flush) + 0, 0, nullptr, 0, nullptr, barriers.size(), barriers.data()); + } + +-CVulkanDevice g_device; ++namespace { CNoDestroy g_deviceHolder; } ++CVulkanDevice &g_device = g_deviceHolder.value; + + static bool allDMABUFsEqual( wlr_dmabuf_attributes *pDMA ) + { +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +index b6749d4..a9335c4 100644 +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -564,7 +564,7 @@ enum ShaderType { + SHADER_TYPE_COUNT + }; + +-extern VulkanOutput_t g_output; ++extern VulkanOutput_t &g_output; + + struct SamplerState + { +@@ -1007,4 +1007,4 @@ void vulkan_wait_idle(); + // Whether the driver implements VK_EXT_physical_device_drm + bool vulkan_has_drm_props(); + +-extern CVulkanDevice g_device; ++extern CVulkanDevice &g_device; +-- +2.55.0 +