From 38d7977534083a06b3c98d05bbe9d76e8d13a38e Mon Sep 17 00:00:00 2001 From: hallo1 <2302004040@qq.com> Date: Tue, 11 Aug 2026 21:37:17 +0800 Subject: [PATCH 1/3] fix(linux): retain PipeWire DMA-BUFs through CUDA conversion --- src/platform/linux/cuda.cpp | 5 ++ src/platform/linux/graphics.h | 15 +++++ src/platform/linux/pipewire.cpp | 66 +++++++++++++++++++++ tests/unit/platform/linux/test_graphics.cpp | 42 +++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 tests/unit/platform/linux/test_graphics.cpp diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 6623c9e95a0..a0512b74ebb 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -604,6 +604,11 @@ namespace cuda { CU_CHECK(cdf->cuGraphicsUnmapResources(resources.size(), resources.data(), stream.get()), "Couldn't unmap GL textures from CUDA"); } + // Mapping the GL conversion targets into CUDA synchronizes the preceding + // GL draw that consumed the source DMA-BUF. It is now safe for PipeWire + // to return that producer-owned buffer to KWin for reuse. + descriptor.mark_capture_buffer_consumed(); + return 0; } diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index 2a2a06429c1..178d8a5df70 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -5,6 +5,7 @@ #pragma once // standard includes +#include #include #include @@ -610,6 +611,8 @@ namespace egl { * @brief Reset the object to its initial empty state. */ void reset() { + mark_capture_buffer_consumed(); + for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); @@ -619,6 +622,17 @@ namespace egl { } } + /** + * @brief Notify the capture backend that the imported source buffer is no + * longer needed by conversion and can be returned to its producer. + */ + void mark_capture_buffer_consumed() { + if (capture_buffer_consumed_cb) { + auto callback = std::move(capture_buffer_consumed_cb); + callback(); + } + } + surface_descriptor_t sd; ///< DMA-BUF surface descriptor for the captured image. // Increment sequence when new rgb_t needs to be created @@ -632,6 +646,7 @@ namespace egl { std::optional seq; ///< PipeWire frame sequence number. std::optional pw_damage; ///< Whether PipeWire damage tracking should be used. std::optional pw_flags; ///< PipeWire frame flags reported with the buffer. + std::function capture_buffer_consumed_cb; ///< Releases a producer-owned capture buffer after import/conversion. }; /** diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 1048b8399ca..9432f72176f 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -87,6 +87,53 @@ namespace pipewire { std::string err_msg; ///< Last PipeWire error message reported by the stream. }; + /** + * @brief Safely returns retained PipeWire buffers from Sunshine's conversion + * thread while preventing use after stream teardown. + */ + struct buffer_release_state_t { + /** + * @brief Make buffer releases target the active PipeWire stream. + * + * @param new_loop PipeWire thread loop that owns the stream. + * @param new_stream PipeWire stream that owns captured buffers. + */ + void activate(struct pw_thread_loop *new_loop, struct pw_stream *new_stream) { + std::scoped_lock lock(mutex); + loop = new_loop; + stream = new_stream; + } + + /** + * @brief Ignore future buffer releases before stream teardown. + */ + void deactivate() { + std::scoped_lock lock(mutex); + loop = nullptr; + stream = nullptr; + } + + /** + * @brief Return a retained buffer to the active PipeWire stream. + * + * @param buffer PipeWire buffer whose capture contents are no longer used. + */ + void release(struct pw_buffer *buffer) { + std::scoped_lock lock(mutex); + if (!loop || !stream || !buffer) { + return; + } + + pw_thread_loop_lock(loop); + pw_stream_queue_buffer(stream, buffer); + pw_thread_loop_unlock(loop); + } + + std::mutex mutex; ///< Protects stream lifetime and serialized buffer release. + struct pw_thread_loop *loop = nullptr; ///< Thread loop that owns `stream`. + struct pw_stream *stream = nullptr; ///< Active stream that owns retained buffers. + }; + /** * @brief PipeWire stream handle, format, and shared state pointer. */ @@ -148,6 +195,7 @@ namespace pipewire { ~pipewire_t() { BOOST_LOG(debug) << "[pipewire] Destroying pipewire_t"sv; + buffer_release_state->deactivate(); pw_thread_loop_lock(loop); // Lock the frame mutex to stop fill_img @@ -293,6 +341,7 @@ namespace pipewire { BOOST_LOG(debug) << "[pipewire] Create PW stream"sv; stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props); + buffer_release_state->activate(loop, stream_data.stream); pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data); std::array buffer; @@ -308,6 +357,7 @@ namespace pipewire { bool use_dmabuf = n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi || mem_type == platf::mem_type_e::vulkan || (mem_type == platf::mem_type_e::cuda && display_is_nvidia)); + retain_dmabuf_for_cuda_ = use_dmabuf && mem_type == platf::mem_type_e::cuda; if (use_dmabuf) { for (int i = 0; i < n_dmabuf_infos; i++) { auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers); @@ -434,6 +484,20 @@ namespace pipewire { fill_img_metadata(img_descriptor, buf); if (buf->datas[0].type == SPA_DATA_DmaBuf) { fill_img_dmabuf(img_descriptor, buf, stream_data); + + if (retain_dmabuf_for_cuda_) { + // Transfer ownership of this PipeWire buffer to the captured image. + // The GL/CUDA conversion path returns it only after it has finished + // reading the imported DMA-BUF. + const auto retained_buffer = stream_data.current_buffer; + std::weak_ptr weak_release_state = buffer_release_state; + img_descriptor->capture_buffer_consumed_cb = [weak_release_state, retained_buffer]() { + if (auto release_state = weak_release_state.lock()) { + release_state->release(retained_buffer); + } + }; + stream_data.current_buffer = nullptr; + } } else { img->data = stream_data.front_buffer->data(); img->row_pitch = stream_data.local_stride; @@ -458,10 +522,12 @@ namespace pipewire { struct pw_core *core; struct spa_hook core_listener; struct stream_data_t stream_data; + std::shared_ptr buffer_release_state = std::make_shared(); int fd; uint32_t node; uint64_t object_serial; bool negotiate_maxframerate_ = true; + bool retain_dmabuf_for_cuda_ = false; ///< Retain producer buffers until GL/CUDA conversion consumes them. struct spa_pod *build_format_parameter(struct spa_pod_builder *b, uint32_t width, uint32_t height, uint32_t refresh_rate, int32_t format, uint64_t *modifiers, int n_modifiers) { struct spa_pod_frame object_frame; diff --git a/tests/unit/platform/linux/test_graphics.cpp b/tests/unit/platform/linux/test_graphics.cpp new file mode 100644 index 00000000000..f61b99c36d1 --- /dev/null +++ b/tests/unit/platform/linux/test_graphics.cpp @@ -0,0 +1,42 @@ +/** + * @file tests/unit/platform/linux/test_graphics.cpp + * @brief Test src/platform/linux/graphics.h image descriptor behavior. + */ +#include "../../../tests_common.h" + +#if defined(__linux__) + #include + #include + + #include + +TEST(EglImageDescriptorTest, ReleasesCaptureBufferOnlyOnce) { + egl::img_descriptor_t descriptor; + std::fill(std::begin(descriptor.sd.fds), std::end(descriptor.sd.fds), -1); + + int release_count = 0; + descriptor.capture_buffer_consumed_cb = [&release_count]() { + ++release_count; + }; + + descriptor.mark_capture_buffer_consumed(); + descriptor.mark_capture_buffer_consumed(); + descriptor.reset(); + + EXPECT_EQ(release_count, 1); +} + +TEST(EglImageDescriptorTest, ResetReleasesCaptureBuffer) { + egl::img_descriptor_t descriptor; + std::fill(std::begin(descriptor.sd.fds), std::end(descriptor.sd.fds), -1); + + bool released = false; + descriptor.capture_buffer_consumed_cb = [&released]() { + released = true; + }; + + descriptor.reset(); + + EXPECT_TRUE(released); +} +#endif From f85b335933c2e5404babeaac96b674048808df9f Mon Sep 17 00:00:00 2001 From: hallo1 <2302004040@qq.com> Date: Tue, 11 Aug 2026 22:10:25 +0800 Subject: [PATCH 2/3] fix(linux): make capture buffer release exception-safe --- src/platform/linux/graphics.h | 20 +++++++++---- src/platform/linux/pipewire.cpp | 31 +++++++++++++-------- tests/unit/platform/linux/test_graphics.cpp | 18 ++++++++++-- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index 178d8a5df70..17f1490516e 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -5,9 +5,11 @@ #pragma once // standard includes +#include #include #include #include +#include // lib includes #include @@ -603,14 +605,14 @@ namespace egl { */ class img_descriptor_t: public cursor_t { public: - ~img_descriptor_t() { + ~img_descriptor_t() noexcept { reset(); } /** * @brief Reset the object to its initial empty state. */ - void reset() { + void reset() noexcept { mark_capture_buffer_consumed(); for (auto x = 0; x < 4; ++x) { @@ -626,10 +628,16 @@ namespace egl { * @brief Notify the capture backend that the imported source buffer is no * longer needed by conversion and can be returned to its producer. */ - void mark_capture_buffer_consumed() { - if (capture_buffer_consumed_cb) { - auto callback = std::move(capture_buffer_consumed_cb); - callback(); + void mark_capture_buffer_consumed() noexcept { + auto callback = std::exchange(capture_buffer_consumed_cb, {}); + if (callback) { + try { + callback(); + } catch (const std::exception &e) { + BOOST_LOG(error) << "Failed to release capture buffer: " << e.what(); + } catch (...) { + BOOST_LOG(error) << "Failed to release capture buffer: unknown exception"; + } } } diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 9432f72176f..36cc854b2b3 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -4,6 +4,7 @@ */ // standard includes #include +#include // lib includes #include @@ -486,17 +487,7 @@ namespace pipewire { fill_img_dmabuf(img_descriptor, buf, stream_data); if (retain_dmabuf_for_cuda_) { - // Transfer ownership of this PipeWire buffer to the captured image. - // The GL/CUDA conversion path returns it only after it has finished - // reading the imported DMA-BUF. - const auto retained_buffer = stream_data.current_buffer; - std::weak_ptr weak_release_state = buffer_release_state; - img_descriptor->capture_buffer_consumed_cb = [weak_release_state, retained_buffer]() { - if (auto release_state = weak_release_state.lock()) { - release_state->release(retained_buffer); - } - }; - stream_data.current_buffer = nullptr; + retain_current_buffer_until_conversion(img_descriptor); } } else { img->data = stream_data.front_buffer->data(); @@ -517,6 +508,24 @@ namespace pipewire { } private: + /** + * @brief Transfer the current PipeWire buffer to an image until conversion completes. + * + * @param img_descriptor Captured image that will release the buffer after conversion. + */ + void retain_current_buffer_until_conversion(egl::img_descriptor_t *img_descriptor) { + const auto retained_buffer = std::exchange(stream_data.current_buffer, nullptr); + const std::weak_ptr weak_release_state = buffer_release_state; + img_descriptor->capture_buffer_consumed_cb = [weak_release_state, retained_buffer]() { + const auto release_state = weak_release_state.lock(); + if (!release_state) { + return; + } + + release_state->release(retained_buffer); + }; + } + struct pw_thread_loop *loop; struct pw_context *context; struct pw_core *core; diff --git a/tests/unit/platform/linux/test_graphics.cpp b/tests/unit/platform/linux/test_graphics.cpp index f61b99c36d1..76457036796 100644 --- a/tests/unit/platform/linux/test_graphics.cpp +++ b/tests/unit/platform/linux/test_graphics.cpp @@ -6,13 +6,13 @@ #if defined(__linux__) #include - #include + #include #include TEST(EglImageDescriptorTest, ReleasesCaptureBufferOnlyOnce) { egl::img_descriptor_t descriptor; - std::fill(std::begin(descriptor.sd.fds), std::end(descriptor.sd.fds), -1); + std::ranges::fill(descriptor.sd.fds, -1); int release_count = 0; descriptor.capture_buffer_consumed_cb = [&release_count]() { @@ -28,7 +28,7 @@ TEST(EglImageDescriptorTest, ReleasesCaptureBufferOnlyOnce) { TEST(EglImageDescriptorTest, ResetReleasesCaptureBuffer) { egl::img_descriptor_t descriptor; - std::fill(std::begin(descriptor.sd.fds), std::end(descriptor.sd.fds), -1); + std::ranges::fill(descriptor.sd.fds, -1); bool released = false; descriptor.capture_buffer_consumed_cb = [&released]() { @@ -39,4 +39,16 @@ TEST(EglImageDescriptorTest, ResetReleasesCaptureBuffer) { EXPECT_TRUE(released); } + +TEST(EglImageDescriptorTest, ContainsCaptureBufferReleaseExceptions) { + egl::img_descriptor_t descriptor; + std::ranges::fill(descriptor.sd.fds, -1); + + descriptor.capture_buffer_consumed_cb = []() { + throw std::runtime_error("release failed"); + }; + + EXPECT_NO_THROW(descriptor.mark_capture_buffer_consumed()); + EXPECT_FALSE(descriptor.capture_buffer_consumed_cb); +} #endif From 1219f6aa1df94bdb77ad17ec69c88e1999179c45 Mon Sep 17 00:00:00 2001 From: hallo1 <2302004040@qq.com> Date: Tue, 11 Aug 2026 22:18:08 +0800 Subject: [PATCH 3/3] test(linux): use a dedicated release exception --- tests/unit/platform/linux/test_graphics.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/platform/linux/test_graphics.cpp b/tests/unit/platform/linux/test_graphics.cpp index 76457036796..c53c44515c1 100644 --- a/tests/unit/platform/linux/test_graphics.cpp +++ b/tests/unit/platform/linux/test_graphics.cpp @@ -6,10 +6,17 @@ #if defined(__linux__) #include - #include + #include #include +namespace { + /** + * @brief Test-only failure raised by a capture-buffer release callback. + */ + class capture_buffer_release_error: public std::exception {}; +} // namespace + TEST(EglImageDescriptorTest, ReleasesCaptureBufferOnlyOnce) { egl::img_descriptor_t descriptor; std::ranges::fill(descriptor.sd.fds, -1); @@ -45,7 +52,7 @@ TEST(EglImageDescriptorTest, ContainsCaptureBufferReleaseExceptions) { std::ranges::fill(descriptor.sd.fds, -1); descriptor.capture_buffer_consumed_cb = []() { - throw std::runtime_error("release failed"); + throw capture_buffer_release_error {}; }; EXPECT_NO_THROW(descriptor.mark_capture_buffer_consumed());