ref:cb72dffa3233c5815cd5ba88f09f049dd679ba75

feat(linux/pipewire): add support for unpaced capture when appropriate (#5629)

SHA: cb72dffa3233c5815cd5ba88f09f049dd679ba75
Author: Conn O'Griofa <connogriofa@gmail.com>
Date: 2026-09-06 22:25
Parents: 05b22c5
6 files changed +73 -98
Type
src/platform/linux/cuda.cpp +1 −11
@@ -1042,17 +1042,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor);
src/platform/linux/kmsgrab.cpp +2 −24
@@ -1522,18 +1522,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
@@ -1799,18 +1788,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
src/platform/linux/misc.h +26 −0
@@ -5,7 +5,9 @@
#pragma once
// standard includes
#include <chrono>
#include <fcntl.h>
#include <filesystem>
#include <thread>
#include <unistd.h>
#include <vector>
@@ -67,4 +69,28 @@
* @return A file descriptor on success, or `-1` if `open()` itself fails.
*/
int open_drm_card_fd(const std::filesystem::path &path, int flags = O_RDWR);
/**
* @brief Generic frame pacing logic for Linux capture methods.
*
* Advances the frame pacing timeline and re-anchors it if the capture thread falls behind.
*
* @param next_frame Time point that the next frame will be targeted against.
* @param delay Delay interval used to pace next frame.
* @param logger The sleep_overshoot_logger for tracking discontinuities.
*/
inline void handle_pacing(std::chrono::steady_clock::time_point &next_frame, std::chrono::nanoseconds delay, auto &logger) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_until(next_frame);
logger.first_point(next_frame);
logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
}
} // namespace platf
src/platform/linux/pipewire.cpp +40 −15
@@ -225,6 +225,34 @@
}
/**
* @brief Check and log whether the active session will require Sunshine to perform pacing.
*
* @param requested_framerate The framerate that we requested.
* @param requested_delay The delay corresponding to the requested framerate.
* @return True when Sunshine pacing is required.
*/
bool is_pacing_required(AVRational requested_framerate, std::chrono::nanoseconds requested_delay) {
AVRational negotiated_rate =
{
static_cast<int32_t>(stream_data.format.info.raw.max_framerate.num),
static_cast<int32_t>(stream_data.format.info.raw.max_framerate.denom)
};
int rate_comparison = av_cmp_q(negotiated_rate, requested_framerate);
bool variable_rate = negotiated_rate.num == 0 && negotiated_rate.den == 1;
bool pacing_required = variable_rate || rate_comparison > 0;
if (!variable_rate && rate_comparison < 0) {
BOOST_LOG(warning)
<< "[pipewire] Sunshine frame pacing: disabled (negotiated rate lower than requested rate)"sv;
} else {
BOOST_LOG(info) << "[pipewire] Sunshine frame pacing: "sv
<< (pacing_required ? std::format("enabled ({}ms)", std::chrono::duration<double, std::milli>(requested_delay).count()) : "disabled (event-driven capture)");
}
return pacing_required;
}
/**
* @brief Set frame ready.
*
* @param ready Whether the PipeWire frame is ready for capture.
@@ -656,8 +684,9 @@
BOOST_LOG(info) << "[pipewire] Color primaries: "sv << d->format.info.raw.color_primaries;
BOOST_LOG(info) << "[pipewire] Transfer function: "sv << d->format.info.raw.transfer_function;
if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) {
BOOST_LOG(info) << "[pipewire] Framerate (from compositor): 0/1 (variable rate capture)";
BOOST_LOG(info) << "[pipewire] Compositor negotiated frame rate: 0/1 (variable rate capture)"sv;
} else {
BOOST_LOG(info) << "[pipewire] Compositor negotiated frame rate: "sv
<< d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom
BOOST_LOG(info) << "[pipewire] Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom
<< ", max: "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom;
}
@@ -823,11 +852,11 @@
const AVRational fps = (negotiate_variable_rate ? AVRational {0, 1} : ::video::framerate_to_rational(config));
if (fps.den != 1) {
BOOST_LOG(info) << "[pipewire] Requested frame rate: "sv << fps.num << "/"sv << fps.den << ", approx. "sv << av_q2d(fps) << " fps"sv;
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << fps.num << "/" << fps.den << ", approx. " << av_q2d(fps) << " fps]";
} else if (fps.num == 0 && fps.den == 1) {
BOOST_LOG(info) << "[pipewire] Requested variable rate capture [host pacing: " << std::chrono::duration<double, std::milli>(delay).count() << "ms]";
BOOST_LOG(info) << "[pipewire] Requested variable frame rate (Sunshine pacing required: "sv << std::chrono::duration<double, std::milli>(delay).count() << "ms)"sv;
} else {
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << fps.num << "fps]";
BOOST_LOG(info) << "[pipewire] Requested frame rate: "sv << fps.num << "fps"sv;
}
this->target_framerate = fps;
mem_type = hwdevice_type;
@@ -982,6 +1011,9 @@
return platf::capture_e::error;
}
sleep_overshoot_logger.reset();
// Check if pacing is required
bool pacing_required = pipewire.is_pacing_required(target_framerate, delay);
while (true) {
// Check if PipeWire signaled a dead stream
@@ -995,16 +1027,9 @@
return platf::capture_e::reinit;
}
// Advance to (or catch up with) next delay interval
auto now = std::chrono::steady_clock::now();
while (next_frame < now) {
next_frame += delay;
}
if (next_frame > now) {
std::this_thread::sleep_until(next_frame);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
// Use unpaced event driven capture when possible
if (pacing_required) {
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
}
std::shared_ptr<platf::img_t> img_out;
src/platform/linux/wlgrab.cpp +2 −24
@@ -204,18 +204,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
@@ -368,18 +357,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
src/platform/linux/x11grab.cpp +2 −24
@@ -565,18 +565,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
@@ -741,18 +730,7 @@
sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for(next_frame - now);
sleep_overshoot_logger.first_point(next_frame);
sleep_overshoot_logger.second_point_now_and_log();
}
next_frame += delay;
if (next_frame < now) { // some major slowdown happened; we couldn't keep up
next_frame = now + delay;
}
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);