diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc8bcf1..80666cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,25 @@ with the version table of the release you are moving to, then read **Breaking ch ## v0.28.1 β€” in development +### punktfunk-gamescope `+pfhdr7` β€” a lingered session no longer dies of its own capture teardown + +πŸ›‘ **On client disconnect the host keeps the headless gamescope alive so a reconnect resumes the +same session β€” and gamescope could SIGSEGV in exactly that window, so the kept display was dead and +reconnect silently got a fresh compositor with the game lost.** When the capture consumer leaves, +PipeWire's `remove_buffer` (and the stale-push path in `dispatch_nudge`) destroyed idle buffers on +the **PipeWire thread**; dropping the last `CVulkanTexture` reference there calls into the Vulkan +driver (`vkDestroyImage`/`FreeMemory`/dmabuf fds) while steamcompmgr can still be inside +`vulkan_screenshot` on another buffer of the same 4-buffer pool. On NVIDIA that races to a SIGSEGV +in `CVulkanCmdBuffer::insertBarrier` β€” timed at stream end, which is why it selectively killed +linger. The journal signature: linger line β†’ coredump β†’ `kept display was dead β€” recreating`. + +Patch 0009 queues those corpses on the PipeWire thread and has steamcompmgr reap them on every +vblank β€” including while the stream is paused, which is precisely the linger state. Found, fixed +and proven live by **luxus** ([punktfunk-overlay#9](https://github.com/luxus/punktfunk-overlay/issues/9)): +four coredumps on 4K60 HDR + composited cursor, zero after; disconnect/reconnect now reuses the +lingered session. Banner `+pfhdr6` β†’ `+pfhdr7` (no new capability β€” but "reconnect lost my game" +triage must be able to read a box's exposure off its banner, the same rule as `+pfhdr5`/`6`). + ### NixOS β€” the plugin runner was installed, running, and reported missing πŸ›‘ **On NixOS every plugin *package* op failed with "the plugin runner isn't installed", on a box diff --git a/packaging/gamescope/PKGBUILD b/packaging/gamescope/PKGBUILD index 54694f87..18472c65 100644 --- a/packaging/gamescope/PKGBUILD +++ b/packaging/gamescope/PKGBUILD @@ -19,7 +19,7 @@ pkgname=punktfunk-gamescope # bump it with the marker so pacman sees a new version when only our patches moved. _gsver=3.16.25 _gsrev=5fb8dce4a09d0a68d097b9faf9513782106bc843 -pkgver="${_gsver}.pfhdr6" +pkgver="${_gsver}.pfhdr7" # 2: patch 0006 (never destroy the Vulkan device/output at exit). No capability moved, so the # `.pfhdrN` level deliberately stays put β€” see README.md. # 3: pin moved 8c676c39 -> 5fb8dce4 (3.16.25-1 -> 3.16.25-11), which brings upstream's own @@ -39,6 +39,12 @@ pkgver="${_gsver}.pfhdr6" # the composite (and the stream) black while every health signal stayed green. No capability the # host probes for, but a field box's banner has to distinguish a build that can lose its composite # this way from one that cannot. +# +# pfhdr7 / rel 1: patch 0009 (reported + written by luxus, punktfunk-overlay#9) moves capture-buffer +# destruction off the PipeWire thread: remove_buffer used to drop the last CVulkanTexture ref there, +# racing steamcompmgr's vulkan_screenshot on the same device β€” a SIGSEGV precisely in the linger +# window, so a kept display was dead and reconnect lost the game session. No capability the host +# probes for, but "reconnect lost my game" triage has to read the difference off the banner. pkgrel=1 pkgdesc="gamescope with 10-bit BT.2020/PQ PipeWire capture, for punktfunk HDR streaming" arch=('x86_64' 'aarch64') diff --git a/packaging/gamescope/README.md b/packaging/gamescope/README.md index 426a70a0..fbc18796 100644 --- a/packaging/gamescope/README.md +++ b/packaging/gamescope/README.md @@ -19,6 +19,7 @@ The patches here add the missing half, and nothing else. See | `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 | | `0007-pipewire-never-leave-pw_buffer-user_data-pointing-at.patch` | Associate `pw_buffer->user_data` with its `pipewire_buffer` for every path out of `add_buffer`, clear it in `remove_buffer` (the last point both halves are known), and null-check the consumers β€” killing the use-after-free that aborted the session on every capture renegotiation | **Yes** β€” a plain use-after-free in the PipeWire buffer lifecycle | | `0008-steamcompmgr-honor-GAMESCOPE_NO_FOCUS-never-a-focus-.patch` | Honor `GAMESCOPE_NO_FOCUS` (set by hhd-ui and MangoHud, consumed by nobody): such windows are skipped by both focus-candidate collectors, so a mapped-but-unpainted overlay app can no longer win focus and turn the composite black. Compositing is untouched β€” only focus SELECTION is barred | **Yes** β€” the atom's setters already exist in the wild; some compositor has to keep the promise | +| `0009-pipewire-destroy-capture-textures-on-the-compositor-.patch` | Move capture-buffer destruction off the PipeWire thread: `remove_buffer`/stale-push queue the corpse (`bury_buffer`), steamcompmgr reaps on every vblank β€” including while the stream is paused, which is exactly the linger window. Without it, dropping the last `CVulkanTexture` ref on the PW thread races `vulkan_screenshot` on the same device and SIGSEGVs (NVIDIA `insertBarrier`), so a lingered display is dead and reconnect loses the session. Reported + written by luxus (punktfunk-overlay#9) | **Yes** β€” the race is upstream's `paint_pipewire` vs `destroy_buffer`; our patches only make the paint path heavier | ### Why the headless patch matters @@ -68,6 +69,20 @@ variant of the same fault instead. Two traps when triaging it: lands in a working-looking game mode at the wrong resolution and without any of these patches. Read the banner in `~/.gamescope-stdout.log`, not the fact that a session exists. +### Why the teardown patch is what makes linger real + +Patch 0007 keeps a session alive across renegotiations; patch 0009 keeps it alive across +*disconnects*. When the capture consumer leaves, `stream_handle_remove_buffer` used to destroy +idle buffers on the PipeWire thread β€” and `~CVulkanTexture` talks to the Vulkan device +(`vkDestroyImage`/`FreeMemory`/dmabuf fds) while steamcompmgr can still be inside +`vulkan_screenshot` on another buffer of the same 4-buffer pool. On NVIDIA that races to a SIGSEGV +in `CVulkanCmdBuffer::insertBarrier`, timed precisely at stream end β€” so the display the host +keeps lingering for a reconnect is already dead, and the "resumed" session silently becomes a +fresh compositor with the game lost. The journal signature: a linger line, then a coredump, then +`kept display was dead β€” recreating`. Found, fixed and proven live by luxus +([punktfunk-overlay#9](https://github.com/luxus/punktfunk-overlay/issues/9)) on 4K60 HDR + composited +cursor, the heaviest paint path we ship. + ## Why the marker exists punktfunk decides a session's shape **before** the virtual display exists: the bit depth at @@ -87,6 +102,7 @@ The number is a **monotonic patch-set revision**, so one probe answers every cap | `+pfhdr4` | …and `--pipewire-composite-external-overlay` | | `+pfhdr5` | …and the PipeWire buffer use-after-free is fixed (no new capability) | | `+pfhdr6` | …and `GAMESCOPE_NO_FOCUS` windows are never focus candidates (no new capability) | +| `+pfhdr7` | …and PipeWire teardown cannot SIGSEGV a lingering compositor (no new capability) | Bump it whenever a patch adds or changes something the host must know about before it spawns. diff --git a/packaging/gamescope/patches/0009-pipewire-destroy-capture-textures-on-the-compositor-.patch b/packaging/gamescope/patches/0009-pipewire-destroy-capture-textures-on-the-compositor-.patch new file mode 100644 index 00000000..da784b82 --- /dev/null +++ b/packaging/gamescope/patches/0009-pipewire-destroy-capture-textures-on-the-compositor-.patch @@ -0,0 +1,159 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: luxus +Date: Thu, 13 Aug 2026 23:15:48 +0200 +Subject: [PATCH] pipewire: destroy capture textures on the compositor thread +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When the capture consumer leaves, PipeWire remove_buffer destroys idle +buffers on the PW thread. CVulkanTexture's destructor talks to the Vulkan +device (DestroyImage / FreeMemory / close dmabuf fds). steamcompmgr may +still be inside vulkan_screenshot on another buffer of the same device. + +That race SIGSEGVs in CVulkanCmdBuffer::insertBarrier / libnvidia-eglcore +and kills the compositor. A lingering gamescope session then cannot be +reused β€” the host has to spawn a new one. + +Queue those corpses and delete them on the steamcompmgr vblank, including +when the stream is only paused (linger: no consumer, compositor stays up). + +The same race exists on the stale-push path (dispatch_nudge destroying a +buffer whose pw_buffer vanished while it was copying), so that call is +buried too. Corpses queued when the compositor exits are reclaimed by the +kernel, same as patch 0006's deliberate leak. + +Reported, written and proven live by luxus (punktfunk-overlay#9): four +coredumps on an NVIDIA host, all at stream end / linger, three in +paint_pipewire β†’ vulkan_screenshot β†’ insertBarrier with a concurrent +destroy_buffer β†’ ~CVulkanTexture on the PipeWire thread; after this patch +a disconnect/reconnect reuses the lingered session. + +Not addressed here: stream_handle_add_buffer's `error:` path still deletes +on the PW thread. By the later `goto error`s a texture may be attached, so +the same race is reachable in theory β€” but only when an add FAILS mid- +renegotiation, which none of the field coredumps show. Left as-is to stay +byte-identical with the change that was proven on-glass. + +punktfunk: the banner moves to +pfhdr7. No new capability β€” but a build +whose linger can die of its own capture teardown is indistinguishable from +one that cannot except by this marker, and "reconnect lost my game" is +exactly the field report that needs that read (same rule as pfhdr5/6). + +Upstream: yes β€” the race is upstream's paint_pipewire vs destroy_buffer; +our HDR/cursor patches only make the paint path heavier. +--- + src/meson.build | 3 ++- + src/pipewire.cpp | 28 ++++++++++++++++++++++++++-- + src/pipewire.hpp | 3 +++ + src/steamcompmgr.cpp | 10 ++++++++-- + 4 files changed, 39 insertions(+), 5 deletions(-) + +diff --git a/src/meson.build b/src/meson.build +index acfcaea..fe854af 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -186,7 +186,8 @@ vcs_tag = run_command(vcs_tag_cmd, check: false).stdout().strip() + # +pfhdr4 β€” …and `--pipewire-composite-external-overlay` + # +pfhdr5 β€” …and the PipeWire buffer use-after-free is fixed (no new capability) + # +pfhdr6 β€” …and GAMESCOPE_NO_FOCUS windows are never focus candidates (no new capability) +-version_tag = vcs_tag + '+pfhdr6' + ' (' + compiler_name + ' ' + compiler_version + ')' ++# +pfhdr7 β€” …and PipeWire teardown cannot SIGSEGV a lingering compositor (no new capability) ++version_tag = vcs_tag + '+pfhdr7' + ' (' + compiler_name + ' ' + compiler_version + ')' + + gamescope_version_conf = configuration_data() + gamescope_version_conf.set('VCS_TAG', version_tag) +diff --git a/src/pipewire.cpp b/src/pipewire.cpp +index c683b3a..3c727e9 100644 +--- a/src/pipewire.cpp ++++ b/src/pipewire.cpp +@@ -7,6 +7,7 @@ + #include + + #include ++#include + #include + #include + +@@ -103,6 +104,29 @@ static void destroy_buffer(struct pipewire_buffer *buffer) { + delete buffer; + } + ++// The PipeWire thread must not destroy CVulkanTextures: steamcompmgr may be ++// inside vulkan_screenshot on another buffer of the same device. Queue the ++// corpse and let steamcompmgr delete it on the next vblank. ++static std::mutex s_deadBuffersMutex; ++static std::vector s_deadBuffers; ++ ++static void bury_buffer(struct pipewire_buffer *buffer) ++{ ++ std::lock_guard lock(s_deadBuffersMutex); ++ s_deadBuffers.push_back(buffer); ++} ++ ++void pipewire_reap_dead_buffers(void) ++{ ++ std::vector dead; ++ { ++ std::lock_guard lock(s_deadBuffersMutex); ++ dead.swap(s_deadBuffers); ++ } ++ for (struct pipewire_buffer *buffer : dead) ++ destroy_buffer(buffer); ++} ++ + void pipewire_destroy_buffer(struct pipewire_buffer *buffer) + { + destroy_buffer(buffer); +@@ -384,7 +408,7 @@ static void dispatch_nudge(struct pipewire_state *state, int fd) + pwr_log.errorf("pw_stream_queue_buffer failed"); + } + } else { +- destroy_buffer(buffer); ++ bury_buffer(buffer); + } + } + } +@@ -694,7 +718,7 @@ static void stream_handle_remove_buffer(void *data, struct pw_buffer *pw_buffer) + buffer->buffer = nullptr; + + if (!buffer->copying) { +- destroy_buffer(buffer); ++ bury_buffer(buffer); + } + } + +diff --git a/src/pipewire.hpp b/src/pipewire.hpp +index b4d7e29..d0b510c 100644 +--- a/src/pipewire.hpp ++++ b/src/pipewire.hpp +@@ -60,5 +60,8 @@ uint32_t get_pipewire_stream_node_id(void); + struct pipewire_buffer *dequeue_pipewire_buffer(void); + bool pipewire_is_streaming(); + void pipewire_destroy_buffer(struct pipewire_buffer *buffer); ++// Destroy textures queued by the PipeWire thread. Must run on steamcompmgr β€” ++// CVulkanTexture's dtor talks to the Vulkan device. ++void pipewire_reap_dead_buffers(void); + void push_pipewire_buffer(struct pipewire_buffer *buffer); + void nudge_pipewire(void); +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 14596ae..932316f 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -9321,8 +9321,14 @@ steamcompmgr_main(int argc, char **argv) + + #if HAVE_PIPEWIRE + // Drive on vblank, not the timer: under VRR the timer starves (page flips re-arm it). +- if ( vblank && pipewire_is_streaming() ) +- paint_pipewire(); ++ // Reap even when the consumer is gone: linger keeps us alive, and textures ++ // queued by remove_buffer must die on this thread, not PipeWire's. ++ if ( vblank ) ++ { ++ pipewire_reap_dead_buffers(); ++ if ( pipewire_is_streaming() ) ++ paint_pipewire(); ++ } + #endif + + update_vrr_atoms(root_ctx, false, &flush_root);