gamescope +pfhdr7 — linger no longer dies of its own capture teardown (luxus's fix, overlay#9) #212
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: luxus <luxus@users.noreply.github.com>
|
||||
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 <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
+#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<struct pipewire_buffer *> s_deadBuffers;
|
||||
+
|
||||
+static void bury_buffer(struct pipewire_buffer *buffer)
|
||||
+{
|
||||
+ std::lock_guard<std::mutex> lock(s_deadBuffersMutex);
|
||||
+ s_deadBuffers.push_back(buffer);
|
||||
+}
|
||||
+
|
||||
+void pipewire_reap_dead_buffers(void)
|
||||
+{
|
||||
+ std::vector<struct pipewire_buffer *> dead;
|
||||
+ {
|
||||
+ std::lock_guard<std::mutex> 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);
|
||||
Reference in New Issue
Block a user