Skip to content

Commit 7d00aae

Browse files
committed
feat(linux/xdgportal): implement reactive capture with duplicate detection
Replaces polling-based frame capture with event-driven approach using condition variables for immediate notification when new frames arrive. Changes: - on_process: Notify snapshot() via condition variable on new buffer - fill_img: Extract and pass through PipeWire seq/pts metadata - snapshot: Block on condition variable with timeout; detect and skip duplicate frames using seq/pts metadata to avoid redundant processing - capture: Simplify catch-up logic to maintain timeline consistency by advancing in delay intervals without resetting origin This eliminates tight polling loops and reduces unnecessary frame processing when compositor reuses buffers.
1 parent c9bcee4 commit 7d00aae

2 files changed

Lines changed: 95 additions & 24 deletions

File tree

src/platform/linux/graphics.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ namespace egl {
295295

296296
// Increment sequence when new rgb_t needs to be created
297297
std::uint64_t sequence;
298+
299+
// PipeWire metadata
300+
std::optional<uint64_t> pts;
301+
std::optional<uint64_t> seq;
298302
};
299303

300304
class sws_t {

src/platform/linux/portalgrab.cpp

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ namespace portal {
137137
struct pw_buffer *current_buffer;
138138
uint64_t drm_format;
139139
std::shared_ptr<shared_state_t> shared;
140+
std::mutex frame_mutex;
141+
std::condition_variable frame_cv;
140142
};
141143

142144
struct dmabuf_format_info_t {
@@ -690,6 +692,14 @@ namespace portal {
690692
pw_thread_loop_destroy(loop);
691693
}
692694

695+
std::mutex &frame_mutex() {
696+
return stream_data.frame_mutex;
697+
}
698+
699+
std::condition_variable &frame_cv() {
700+
return stream_data.frame_cv;
701+
}
702+
693703
void init(int stream_fd, int stream_node, std::shared_ptr<shared_state_t> shared_state) {
694704
fd = stream_fd;
695705
node = stream_node;
@@ -768,9 +778,21 @@ namespace portal {
768778
if (stream_data.current_buffer) {
769779
struct spa_buffer *buf;
770780
buf = stream_data.current_buffer->buffer;
781+
struct spa_meta_header *h;
782+
h = static_cast<struct spa_meta_header *>(
783+
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
784+
);
785+
771786
if (buf->datas[0].chunk->size != 0) {
772787
const auto img_descriptor = static_cast<egl::img_descriptor_t *>(img);
773788
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();
789+
790+
// Passthrough PipeWire metadata
791+
if (h) {
792+
img_descriptor->seq = h->seq;
793+
img_descriptor->pts = h->pts;
794+
}
795+
774796
if (buf->datas[0].type == SPA_DATA_DmaBuf) {
775797
img_descriptor->sd.width = stream_data.format.info.raw.size.width;
776798
img_descriptor->sd.height = stream_data.format.info.raw.size.height;
@@ -898,10 +920,17 @@ namespace portal {
898920
return;
899921
}
900922

901-
if (d->current_buffer) {
902-
pw_stream_queue_buffer(d->stream, d->current_buffer);
923+
// Update current_buffer atomically
924+
{
925+
std::scoped_lock lock(d->frame_mutex);
926+
927+
if (d->current_buffer) {
928+
pw_stream_queue_buffer(d->stream, d->current_buffer);
929+
}
930+
931+
d->current_buffer = b;
903932
}
904-
d->current_buffer = b;
933+
d->frame_cv.notify_one();
905934
}
906935

907936
static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) {
@@ -964,14 +993,18 @@ namespace portal {
964993
buffer_types |= 1 << SPA_DATA_MemPtr;
965994
}
966995

967-
// Ack the buffer type
996+
// Ack the buffer type and metadata
968997
std::array<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
969-
std::array<const struct spa_pod *, 1> params;
998+
std::array<const struct spa_pod *, 2> params;
970999
int n_params = 0;
9711000
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
9721001
auto buffer_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)));
9731002
params[n_params] = buffer_param;
9741003
n_params++;
1004+
auto meta_param = static_cast<const struct spa_pod *>(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))));
1005+
params[n_params] = meta_param;
1006+
n_params++;
1007+
9751008
pw_stream_update_params(d->stream, params.data(), n_params);
9761009
}
9771010

@@ -1049,23 +1082,56 @@ namespace portal {
10491082

10501083
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool show_cursor) {
10511084
// FIXME: show_cursor is ignored
1052-
if (!pull_free_image_cb(img_out)) {
1053-
return platf::capture_e::interrupted;
1054-
}
1085+
auto start_time = std::chrono::steady_clock::now();
1086+
auto end_time = start_time;
1087+
1088+
while (true) {
1089+
if (!pull_free_image_cb(img_out)) {
1090+
return platf::capture_e::interrupted;
1091+
}
10551092

1056-
const auto img_egl = static_cast<egl::img_descriptor_t *>(img_out.get());
1057-
img_egl->reset();
1058-
pipewire.fill_img(img_egl);
1093+
const auto img_egl = static_cast<egl::img_descriptor_t *>(img_out.get());
1094+
img_egl->reset();
1095+
pipewire.fill_img(img_egl);
10591096

1060-
// Check if we got valid data (either DMA-BUF fd or memory pointer)
1061-
if (img_egl->sd.fds[0] < 0 && img_egl->data == nullptr) {
1062-
// No buffer available yet from pipewire
1063-
return platf::capture_e::timeout;
1064-
}
1097+
// Check if we got valid data (either DMA-BUF fd or memory pointer)
1098+
if (img_egl->sd.fds[0] < 0 && img_egl->data == nullptr) {
1099+
// No buffer available yet from pipewire
1100+
goto retry_logic;
1101+
}
10651102

1066-
img_egl->sequence = ++sequence;
1103+
// Duplicate detection: PipeWire seq increments on each new frame,
1104+
// pts advances with each buffer update. Both must advance to accept frame.
1105+
if (img_egl->seq.has_value() && img_egl->pts.has_value()) {
1106+
if (last_pts.has_value() && last_seq.has_value() &&
1107+
img_egl->pts.value() == last_pts.value() &&
1108+
img_egl->seq.value() == last_seq.value()) {
1109+
goto retry_logic;
1110+
}
1111+
}
10671112

1068-
return platf::capture_e::ok;
1113+
// Frame found; check deadline
1114+
end_time = std::chrono::steady_clock::now();
1115+
if (end_time - start_time > timeout) {
1116+
return platf::capture_e::timeout;
1117+
} else {
1118+
if (img_egl->seq.has_value() && img_egl->pts.has_value()) {
1119+
last_seq = img_egl->seq.value();
1120+
last_pts = img_egl->pts.value();
1121+
}
1122+
img_egl->sequence = ++sequence;
1123+
return platf::capture_e::ok;
1124+
}
1125+
1126+
retry_logic:
1127+
end_time = std::chrono::steady_clock::now();
1128+
if (end_time - start_time >= timeout) {
1129+
return platf::capture_e::timeout;
1130+
}
1131+
1132+
std::unique_lock lock(pipewire.frame_mutex());
1133+
pipewire.frame_cv().wait_until(lock, start_time + timeout);
1134+
}
10691135
}
10701136

10711137
std::shared_ptr<platf::img_t> alloc_img() override {
@@ -1113,19 +1179,18 @@ namespace portal {
11131179
}
11141180
}
11151181

1182+
// Advance to (or catch up with) next delay interval
11161183
auto now = std::chrono::steady_clock::now();
1184+
while (next_frame < now) {
1185+
next_frame += delay;
1186+
}
11171187

11181188
if (next_frame > now) {
1119-
std::this_thread::sleep_for(next_frame - now);
1189+
std::this_thread::sleep_until(next_frame);
11201190
sleep_overshoot_logger.first_point(next_frame);
11211191
sleep_overshoot_logger.second_point_now_and_log();
11221192
}
11231193

1124-
next_frame += delay;
1125-
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
1126-
next_frame = now + delay;
1127-
}
1128-
11291194
std::shared_ptr<platf::img_t> img_out;
11301195
switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) {
11311196
case platf::capture_e::reinit:
@@ -1281,6 +1346,8 @@ namespace portal {
12811346
int n_dmabuf_infos;
12821347
bool display_is_nvidia = false; // Track if display GPU is NVIDIA
12831348
std::chrono::nanoseconds delay;
1349+
std::optional<std::uint64_t> last_pts {};
1350+
std::optional<std::uint64_t> last_seq {};
12841351
std::uint64_t sequence {};
12851352
uint32_t framerate;
12861353
static inline std::atomic<uint32_t> previous_height {0};

0 commit comments

Comments
 (0)