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..17f1490516e 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -5,8 +5,11 @@ #pragma once // standard includes +#include +#include #include #include +#include // lib includes #include @@ -602,14 +605,16 @@ 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) { if (sd.fds[x] >= 0) { close(sd.fds[x]); @@ -619,6 +624,23 @@ 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() 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"; + } + } + } + surface_descriptor_t sd; ///< DMA-BUF surface descriptor for the captured image. // Increment sequence when new rgb_t needs to be created @@ -632,6 +654,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..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 @@ -87,6 +88,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 +196,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 +342,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 +358,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 +485,10 @@ 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_) { + retain_current_buffer_until_conversion(img_descriptor); + } } else { img->data = stream_data.front_buffer->data(); img->row_pitch = stream_data.local_stride; @@ -453,15 +508,35 @@ 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; 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..c53c44515c1 --- /dev/null +++ b/tests/unit/platform/linux/test_graphics.cpp @@ -0,0 +1,61 @@ +/** + * @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 + +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); + + 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::ranges::fill(descriptor.sd.fds, -1); + + bool released = false; + descriptor.capture_buffer_consumed_cb = [&released]() { + released = true; + }; + + descriptor.reset(); + + EXPECT_TRUE(released); +} + +TEST(EglImageDescriptorTest, ContainsCaptureBufferReleaseExceptions) { + egl::img_descriptor_t descriptor; + std::ranges::fill(descriptor.sd.fds, -1); + + descriptor.capture_buffer_consumed_cb = []() { + throw capture_buffer_release_error {}; + }; + + EXPECT_NO_THROW(descriptor.mark_capture_buffer_consumed()); + EXPECT_FALSE(descriptor.capture_buffer_consumed_cb); +} +#endif