Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/platform/linux/cuda.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
15 changes: 15 additions & 0 deletions src/platform/linux/graphics.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

// standard includes
#include <functional>
#include <optional>
#include <string_view>

Expand Down Expand Up @@ -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]);
Expand All @@ -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
Expand All @@ -632,6 +646,7 @@ namespace egl {
std::optional<uint64_t> seq; ///< PipeWire frame sequence number.
std::optional<bool> pw_damage; ///< Whether PipeWire damage tracking should be used.
std::optional<uint32_t> pw_flags; ///< PipeWire frame flags reported with the buffer.
std::function<void()> capture_buffer_consumed_cb; ///< Releases a producer-owned capture buffer after import/conversion.
};

/**
Expand Down
66 changes: 66 additions & 0 deletions src/platform/linux/pipewire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
Expand All @@ -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);
Expand Down Expand Up @@ -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<buffer_release_state_t> 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;
Expand All @@ -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_t> buffer_release_state = std::make_shared<buffer_release_state_t>();
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;
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/platform/linux/test_graphics.cpp
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <iterator>

#include <src/platform/linux/graphics.h>

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