Skip to content
Open
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
27 changes: 25 additions & 2 deletions src/platform/linux/graphics.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
#pragma once

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

// lib includes
#include <glad/egl.h>
Expand Down Expand Up @@ -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]);
Expand All @@ -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
Expand All @@ -632,6 +654,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
75 changes: 75 additions & 0 deletions src/platform/linux/pipewire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
// standard includes
#include <fstream>
#include <utility>

// lib includes
#include <gio/gio.h>
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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<buffer_release_state_t> 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_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
61 changes: 61 additions & 0 deletions tests/unit/platform/linux/test_graphics.cpp
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <exception>

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

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