From 0c0cabd799cfcef1c4cb4853a14500602bdf318a Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Tue, 24 Feb 2026 06:08:45 +0000 Subject: [PATCH 1/3] feat(linux/xdgportal): implement event-driven capture Summary of changes: * Synchronized capture: - Move frame_cv wait to beginning of loop as a more efficient way to wait for a new buffer whilst ensuring that unintentional buffer reuse doesn't occur. * Improved duplicate detection: - Transition to duplicate detection logic using a combination of SPA_META_Header (PTS) and SPA_META_VideoDamage metadata. - Drop frames when PTS delta is zero and no damage is reported; this helps to smoothen out stuttering unique to KWin. --- src/platform/linux/graphics.h | 2 + src/platform/linux/portalgrab.cpp | 136 ++++++++++++++++++++++++------ 2 files changed, 110 insertions(+), 28 deletions(-) diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index 7abbdc0b7e4..89548a234ed 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -301,6 +301,8 @@ namespace egl { // PipeWire metadata std::optional pts; std::optional seq; + std::optional pw_damage; + std::optional pw_flags; }; class sws_t { diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index e4d78fae73f..042c4fa0620 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -734,6 +734,14 @@ namespace portal { return stream_data.frame_cv; } + bool is_frame_ready() const { + return stream_data.frame_ready; + } + + void set_frame_ready(bool ready) { + stream_data.frame_ready = ready; + } + void init(int stream_fd, int stream_node, std::shared_ptr shared_state) { fd = stream_fd; node = stream_node; @@ -843,14 +851,14 @@ namespace portal { } // 2. Validate we have a buffer and a signal that it's "new" - if (stream_data.current_buffer && stream_data.frame_ready) { + if (stream_data.current_buffer) { struct spa_buffer *buf = stream_data.current_buffer->buffer; if (buf->datas[0].chunk->size != 0) { const auto img_descriptor = static_cast(img); img_descriptor->frame_timestamp = std::chrono::steady_clock::now(); - // Passthrough PipeWire metadata + // PipeWire header metadata struct spa_meta_header *h = static_cast( spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)) ); @@ -859,6 +867,23 @@ namespace portal { img_descriptor->pts = h->pts; } + // PipeWire flags + if (buf->n_datas > 0) { + img_descriptor->pw_flags = buf->datas[0].chunk->flags; + } + + // PipeWire damage metadata + struct spa_meta_region *damage = (struct spa_meta_region *) spa_buffer_find_meta_data( + stream_data.current_buffer->buffer, + SPA_META_VideoDamage, + sizeof(*damage) + ); + if (damage) { + img_descriptor->pw_damage = (damage->region.size.width > 0 && damage->region.size.height > 0); + } else { + img_descriptor->pw_damage = std::nullopt; + } + if (buf->datas[0].type == SPA_DATA_DmaBuf) { img_descriptor->sd.width = stream_data.format.info.raw.size.width; img_descriptor->sd.height = stream_data.format.info.raw.size.height; @@ -874,10 +899,6 @@ namespace portal { // Point the encoder to the front buffer img->data = stream_data.front_buffer->data(); img->row_pitch = stream_data.local_stride; - - // Reset flags - stream_data.frame_ready = false; - stream_data.current_buffer = nullptr; } } } else { @@ -1088,7 +1109,7 @@ namespace portal { // Ack the buffer type and metadata std::array buffer; - std::array params; + std::array params; int n_params = 0; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); auto buffer_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types))); @@ -1097,6 +1118,10 @@ namespace portal { auto meta_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)))); params[n_params] = meta_param; n_params++; + int videoDamageRegionCount = 16; + auto damage_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * videoDamageRegionCount, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * videoDamageRegionCount))); + params[n_params] = damage_param; + n_params++; pw_stream_update_params(d->stream, params.data(), n_params); } @@ -1192,9 +1217,17 @@ namespace portal { platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool show_cursor) { // FIXME: show_cursor is ignored - auto start_time = std::chrono::steady_clock::now(); + auto deadline = std::chrono::steady_clock::now() + timeout; + int retries = 0; while (true) { + if (!wait_for_frame(deadline)) { + if (stream_stopped.load()) { + return platf::capture_e::interrupted; + } + return platf::capture_e::timeout; + } + if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } @@ -1204,35 +1237,27 @@ namespace portal { pipewire.fill_img(img_egl); // Check if we got valid data (either DMA-BUF fd or memory pointer) - bool is_valid_data = (img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr); - - // Duplicate detection: PipeWire seq increments on each new frame, - // pts advances with each buffer update. Both must advance to accept frame. - bool is_duplicate = (img_egl->seq.has_value() && img_egl->pts.has_value() && last_pts.has_value() && last_seq.has_value() && img_egl->pts.value() == last_pts.value() && img_egl->seq.value() == last_seq.value()); - - if (is_valid_data && !is_duplicate) { - // Frame found; check deadline - auto end_time = std::chrono::steady_clock::now(); - if (end_time - start_time > timeout) { + bool is_valid = (img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr); + // Check for duplicates + bool is_dup = last_pts.has_value() && is_buffer_redundant(img_egl, last_pts); + + // Valid & non-duplicate frame + if (is_valid && !is_dup) { + // Check deadline + if (std::chrono::steady_clock::now() >= deadline) { return platf::capture_e::timeout; } - if (img_egl->seq.has_value() && img_egl->pts.has_value()) { - last_seq = img_egl->seq.value(); - last_pts = img_egl->pts.value(); - } - img_egl->sequence = ++sequence; + // Update frame metadata + update_frame_metadata(img_egl, retries); return platf::capture_e::ok; } // No valid frame yet, or it was a duplicate - auto now = std::chrono::steady_clock::now(); - if (now - start_time >= timeout) { + retries++; + if (std::chrono::steady_clock::now() >= deadline) { return platf::capture_e::timeout; } - - std::unique_lock lock(pipewire.frame_mutex()); - pipewire.frame_cv().wait_until(lock, start_time + timeout); } } @@ -1349,6 +1374,61 @@ namespace portal { } private: + static bool is_buffer_redundant(const egl::img_descriptor_t *img, const std::optional &last_pts) { + // Check for corrupted frame + if (img->pw_flags.has_value()) { + if (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED) { + return true; + } + } + + // Damage & PTS checks + if (img->pts.has_value() && last_pts.has_value()) { + int64_t delta = (int64_t) img->pts.value() - (int64_t) last_pts.value(); + + // PTS hasn't advanced: + if (delta <= 0) { + // Case A: Compositor says "No Damage" + if (img->pw_damage.has_value() && !img->pw_damage.value()) { + BOOST_LOG(debug) << "Dropping frame: PTS delta 0 and Damage metadata confirms NO pixels changed."sv; + return true; + } + // Case B: No damage metadata available, but delta is 0 + if (!img->pw_damage.has_value()) { + return true; + } + } + } + + return false; + } + + void update_frame_metadata(egl::img_descriptor_t *img_egl, int retries) { + if (img_egl->seq.has_value() && img_egl->pts.has_value()) { + last_seq = img_egl->seq.value(); + last_pts = img_egl->pts.value(); + } + img_egl->sequence = ++sequence; + + if (retries > 0) { + BOOST_LOG(debug) << "Processed frame after " << retries << " redundant events."sv; + } + } + + bool wait_for_frame(std::chrono::steady_clock::time_point deadline) { + std::unique_lock lock(pipewire.frame_mutex()); + + bool success = pipewire.frame_cv().wait_until(lock, deadline, [&] { + return pipewire.is_frame_ready() || stream_stopped.load(); + }); + + if (success && !stream_stopped.load()) { + pipewire.set_frame_ready(false); + return true; + } + return false; + } + static uint32_t lookup_pw_format(uint64_t fourcc) { for (const auto &fmt : format_map) { if (fmt.fourcc == 0) { From 0994d44086fb1ac01571ae77814f31b3bc36fd41 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Mon, 23 Feb 2026 08:18:08 +0000 Subject: [PATCH 2/3] fix(linux/xdgportal): fix pipewire teardown logic, GNOME XDG stop icon * Destroying the core and context during a reinit can cause a deadlock which can be reproduced when PW_STREAM_FLAG_RT_PROCESS is set. Fix by ensuring that full teardown is only done in pipewire destructor. * Add a delay before sending the interrupt signal, as the GNOME XDG portal needs time to detect changes, otherwise Sunshine will crash when the users manually stops the stream on mutter. --- src/platform/linux/portalgrab.cpp | 33 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index 042c4fa0620..da905e8e7db 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -723,6 +723,24 @@ namespace portal { ~pipewire_t() { cleanup_stream(); + + pw_thread_loop_lock(loop); + + if (core) { + pw_core_disconnect(core); + core = nullptr; + } + if (context) { + pw_context_destroy(context); + context = nullptr; + } + + pw_thread_loop_unlock(loop); + + pw_thread_loop_stop(loop); + if (fd >= 0) { + close(fd); + } pw_thread_loop_destroy(loop); } @@ -775,21 +793,8 @@ namespace portal { pw_stream_destroy(stream_data.stream); stream_data.stream = nullptr; } - if (core) { - pw_core_disconnect(core); - core = nullptr; - } - if (context) { - pw_context_destroy(context); - context = nullptr; - } pw_thread_loop_unlock(loop); - - pw_thread_loop_stop(loop); - if (fd >= 0) { - close(fd); - } } session_cache_t::instance().invalidate(); } @@ -1299,6 +1304,8 @@ namespace portal { stream_stopped.store(false); previous_height.store(0); previous_width.store(0); + // Delay interrupt signal to give Portal time to detect change + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); return platf::capture_e::interrupted; } else { BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv; From 5c5135012621e845e9ba7fa83167b0f026c47db2 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Tue, 24 Feb 2026 21:01:57 +0000 Subject: [PATCH 3/3] fix(linux/xdgportal): make duplicate check less strict Reduce duplicate checking complexity as stricter edge-case is not needed. --- src/platform/linux/portalgrab.cpp | 66 ++++++++----------------------- 1 file changed, 17 insertions(+), 49 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index da905e8e7db..9c5ed398041 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -1225,45 +1225,30 @@ namespace portal { auto deadline = std::chrono::steady_clock::now() + timeout; int retries = 0; - while (true) { + while (std::chrono::steady_clock::now() < deadline) { if (!wait_for_frame(deadline)) { - if (stream_stopped.load()) { - return platf::capture_e::interrupted; - } - return platf::capture_e::timeout; + return stream_stopped.load() ? platf::capture_e::interrupted : platf::capture_e::timeout; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } - const auto img_egl = static_cast(img_out.get()); + auto *img_egl = static_cast(img_out.get()); img_egl->reset(); pipewire.fill_img(img_egl); - // Check if we got valid data (either DMA-BUF fd or memory pointer) - bool is_valid = (img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr); - // Check for duplicates - bool is_dup = last_pts.has_value() && is_buffer_redundant(img_egl, last_pts); - - // Valid & non-duplicate frame - if (is_valid && !is_dup) { - // Check deadline - if (std::chrono::steady_clock::now() >= deadline) { - return platf::capture_e::timeout; - } - + // Check if we got valid data (either DMA-BUF fd or memory pointer), then filter duplicates + if ((img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr) && !is_buffer_redundant(img_egl)) { // Update frame metadata - update_frame_metadata(img_egl, retries); + update_metadata(img_egl, retries); return platf::capture_e::ok; } // No valid frame yet, or it was a duplicate retries++; - if (std::chrono::steady_clock::now() >= deadline) { - return platf::capture_e::timeout; - } } + return platf::capture_e::timeout; } std::shared_ptr alloc_img() override { @@ -1381,41 +1366,24 @@ namespace portal { } private: - static bool is_buffer_redundant(const egl::img_descriptor_t *img, const std::optional &last_pts) { + bool is_buffer_redundant(const egl::img_descriptor_t *img) { // Check for corrupted frame - if (img->pw_flags.has_value()) { - if (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED) { - return true; - } + if (img->pw_flags.has_value() && (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED)) { + return true; } - // Damage & PTS checks - if (img->pts.has_value() && last_pts.has_value()) { - int64_t delta = (int64_t) img->pts.value() - (int64_t) last_pts.value(); - - // PTS hasn't advanced: - if (delta <= 0) { - // Case A: Compositor says "No Damage" - if (img->pw_damage.has_value() && !img->pw_damage.value()) { - BOOST_LOG(debug) << "Dropping frame: PTS delta 0 and Damage metadata confirms NO pixels changed."sv; - return true; - } - // Case B: No damage metadata available, but delta is 0 - if (!img->pw_damage.has_value()) { - return true; - } - } + // If PTS is identical, only drop if damage metadata confirms no change + if (img->pts.has_value() && last_pts.has_value() && img->pts.value() == last_pts.value()) { + return img->pw_damage.has_value() && !img->pw_damage.value(); } return false; } - void update_frame_metadata(egl::img_descriptor_t *img_egl, int retries) { - if (img_egl->seq.has_value() && img_egl->pts.has_value()) { - last_seq = img_egl->seq.value(); - last_pts = img_egl->pts.value(); - } - img_egl->sequence = ++sequence; + void update_metadata(egl::img_descriptor_t *img, int retries) { + last_seq = img->seq; + last_pts = img->pts; + img->sequence = ++sequence; if (retries > 0) { BOOST_LOG(debug) << "Processed frame after " << retries << " redundant events."sv;