Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/platform/linux/kmsgrab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ namespace platf {
_CONVERT("HDMI-B"sv, HDMIB);
_CONVERT("TV"sv, TV);
_CONVERT("eDP"sv, eDP);
_CONVERT("Meta"sv, VIRTUAL); ///< GNOME Shell (Mutter) virtual monitor
_CONVERT("VIRTUAL"sv, VIRTUAL);
_CONVERT("Virtual"sv, VIRTUAL);
_CONVERT("DSI"sv, DSI);
Expand Down
320 changes: 223 additions & 97 deletions src/platform/linux/pipewire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,22 +394,231 @@ namespace pipewire {
}
}

/**
* @brief Compositors that require version-based checks for optimal stream setup.
*/
enum class compositor_type_e {
unknown, ///< Unknown
gnome, ///< GNOME Shell is a proxy for Mutter.
kwin ///< KWin
};

/**
* @brief Compositor type and version information.
*/
struct compositor_version_t {
compositor_type_e type = compositor_type_e::unknown; ///< Compositor type (gnome, kde...).
std::vector<int> version; ///< Compositor version.
};

/**
* @brief Fetch compositor type and version for processing.
*
* @return A struct containing the compositor type and version.
*/
static compositor_version_t get_running_compositor() {
using enum compositor_type_e;

if (auto version = get_running_compositor_version(kwin); !version.empty()) {
return {kwin, std::move(version)};
}

if (auto version = get_running_compositor_version(gnome); !version.empty()) {
return {gnome, std::move(version)};
}

return {};
}

/**
* @brief Fetch compositor version information using DBus calls.
*
* @param compositor_type The compositor we should attempt to fetch version information for.
* @return A vector with 2-3 elements containing the major.minor.micro versions or an empty vector if the version could not be determined.
*/
static std::vector<int> get_running_compositor_version(enum compositor_type_e compositor_type) {
#if !GLIB_CHECK_VERSION(2, 74, 0)
// Compatibility for Ubuntu 22.04 (Glib 2.72)
constexpr auto G_REGEX_DEFAULT = static_cast<GRegexCompileFlags>(0);
constexpr auto G_REGEX_MATCH_DEFAULT = static_cast<GRegexMatchFlags>(0);
#endif
auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
std::vector<int> result;

if (!conn) {
return result;
}

const gchar *version_regex;
GVariant *reply;

using enum compositor_type_e;
if (compositor_type == kwin) {
version_regex = "KWin version: ([0-9]+)\\.([0-9]+)\\.([0-9]+)";
reply = g_dbus_connection_call_sync(
conn,
"org.kde.KWin",
"/KWin",
"org.kde.KWin",
"supportInformation",
nullptr,
G_VARIANT_TYPE("(s)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
nullptr
);
} else {
version_regex = "([0-9]+)\\.([0-9]+)(?:\\.([0-9]+))?";
reply = g_dbus_connection_call_sync(
conn,
"org.gnome.Shell",
"/org/gnome/Shell",
"org.freedesktop.DBus.Properties",
"Get",
g_variant_new("(ss)", "org.gnome.Shell", "ShellVersion"),
G_VARIANT_TYPE("(v)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
nullptr
);
}

if (!reply) {
g_clear_object(&conn);
return result;
}

g_autofree gchar *version_str = nullptr;

if (compositor_type == kwin) {
g_variant_get(reply, "(s)", &version_str);
} else {
GVariant *inner_variant = nullptr;
g_variant_get(reply, "(v)", &inner_variant);
if (inner_variant) {
version_str = g_variant_dup_string(inner_variant, nullptr);
g_variant_unref(inner_variant);
}
}

if (!version_str) {
g_variant_unref(reply);
g_clear_object(&conn);
return result;
}

if (auto *regex = g_regex_new(version_regex, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, nullptr); regex) {
GMatchInfo *match_info = nullptr;
g_regex_match(regex, version_str, G_REGEX_MATCH_DEFAULT, &match_info);

if (g_match_info_matches(match_info)) {
g_autofree const gchar *major = g_match_info_fetch(match_info, 1);
g_autofree const gchar *minor = g_match_info_fetch(match_info, 2);
g_autofree const gchar *micro = g_match_info_fetch(match_info, 3);

if (major) {
result.emplace_back(std::atoi(major));
}
if (minor) {
result.emplace_back(std::atoi(minor));
}
if (micro && *micro != '\0') {
result.emplace_back(std::atoi(micro));
}
}
g_match_info_free(match_info);
g_regex_unref(regex);
}

g_variant_unref(reply);
g_clear_object(&conn);
return result;
}

/**
* @brief Determine if Pipewire's pts metadata is suitable for client pacing based on compositor type/version whitelist.
*
* @param compositor Struct containing compositor type and version.
* @param selected_display_name Name of display - specifically the connector type - such as DP-1, HDMI-1, etc.
* @return True if pts metadata is suitable.
*/
static bool use_pipewire_pts(const compositor_version_t &compositor, const std::string &selected_display_name) {
// KWin: use Pipewire pts metadata for versions 6.7.80+ (6.8 beta) or newer.
// Mutter: use Pipewire pts metadata for Mutter 51 onwards, but pts is reliable only for virtual monitors (Meta-).
// All other cases: don't use Pipewire pts metadata directly.

bool use_pts = false;
bool using_virtual_monitor = false;

using enum compositor_type_e;
switch (compositor.type) {
case gnome:
using_virtual_monitor = selected_display_name.starts_with("Meta-");
use_pts = (using_virtual_monitor && compositor.version[0] >= 51);
break;
case kwin:
use_pts = (compositor.version[0] > 6 || (compositor.version[0] == 6 && (compositor.version[1] > 7 || (compositor.version[1] == 7 && compositor.version[2] > 79))));
break;
default:
break;
}

BOOST_LOG(info) << "[pipewire] Using frame_timestamp (pts) metadata source: "sv
<< (use_pts ? "Pipewire (via compositor)" : PROJECT_NAME);
return use_pts;
}

/**
* @brief Determine if the active compositor is suited for variable rate capture based on type/version whitelist.
*
* @param compositor Struct containing the compositor type and version.
* @return True if variable rate capture is suitable.
*/
static bool
use_variable_rate(const compositor_version_t &compositor) {
// If the active compositor is KWin, request variable rate (0, 1) capture for versions 5.x-6.7.79 (up to 6.7 stable series).
// Issue: KWin <=6.7 has a ~3% fixed-rate pacing deficit vs the requested framerate; variable rate avoids this and prioritizes gaming smoothness.
// KWin 6.7 regresses variable rate (desktop animations run at half speed, but doesn't affect in-game pacing). Ref: https://bugs.kde.org/show_bug.cgi?id=524129
// Issue: KWin 6.8 still has desktop animation pacing issues with variable rate, but fixes the 3% fixed-rate pacing deficit. Fixed-rate pacing has
// new regression tied to 'commit-timing'/VK_KHR_present_timing support when Vsync/FIFO is enabled. Ref: https://bugs.kde.org/show_bug.cgi?id=525619
// Summary: KWin 5.5-6.6 have excellent (variable) pacing. KWin 6.7 has poor desktop animation pacing (variable) but good game pacing.
// KWin 6.8+ will have good overall (fixed) pacing if #525619 can be resolved, otherwise we will update docs advising to disable VSync in games.

// All other compositors (including Mutter) will default to variable rate.
bool variable_rate = true;

using enum compositor_type_e;
if (compositor.type == kwin) {
variable_rate = (compositor.version[0] == 5 || (compositor.version[0] == 6 && (compositor.version[1] < 7 || (compositor.version[1] == 7 && compositor.version[2] < 80))));
}

return variable_rate;
}

/**
* @brief Copy PipeWire metadata into the Sunshine image descriptor.
*
* @param img_descriptor Image descriptor receiving timestamps, sequence, and damage flags.
* @param buf Raw byte buffer used for serialization.
*/
static void fill_img_metadata(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf) {
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();

struct spa_meta_header *h = static_cast<struct spa_meta_header *>(
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
);

img_descriptor->seq.reset();
img_descriptor->pts.reset();
if (h) {
img_descriptor->seq = h->seq;
img_descriptor->pts = h->pts;
if (h->pts > 0) {
img_descriptor->pts = h->pts;
}
}
img_descriptor->frame_timestamp = (img_descriptor->pts.has_value() && prefer_pipewire_pts) ?
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(img_descriptor->pts.value())) :
std::chrono::steady_clock::now();

if (buf->n_datas > 0) {
img_descriptor->pw_flags = buf->datas[0].chunk->flags;
Expand Down Expand Up @@ -490,6 +699,9 @@ namespace pipewire {
negotiate_maxframerate_ = negotiate_maxframerate;
}

inline static std::atomic prefer_pipewire_pts {false}; ///< Whether the session should directly passthrough Pipewire pts metadata for client pacing.
inline static std::atomic negotiate_variable_rate {true}; ///< Whether the session should request variable rate (maxFrameRate = 0/1) capture.

private:
struct pw_thread_loop *loop;
struct pw_context *context;
Expand Down Expand Up @@ -844,18 +1056,12 @@ namespace pipewire {
// calculate frame interval we should capture at
delay = ::video::capture_frame_interval(config);

// WORKAROUND: if the active compositor is KWin, request variable rate (0, 1) capture for versions 5.x-6.7.79 (6.7.80+ are 6.8 preview releases).
// Issue: KWin <=6.7 has a ~3% fixed-rate pacing deficit vs the requested framerate; variable rate avoids this and prioritizes gaming smoothness.
// KWin 6.7 regresses variable rate (desktop animations run at half speed, but doesn't affect in-game pacing). Ref: https://bugs.kde.org/show_bug.cgi?id=524129
// Issue: KWin 6.8 still has desktop animation pacing issues with variable rate, but fixes the 3% fixed-rate pacing deficit. Fixed-rate pacing has
// new regression tied to 'commit-timing'/VK_KHR_present_timing support when Vsync/FIFO is enabled. Ref: https://bugs.kde.org/show_bug.cgi?id=525619
// Summary: KWin 5.5-6.6 have excellent (variable) pacing. KWin 6.7 has poor desktop animation pacing (variable) but good game pacing.
// KWin 6.8+ will have good overall (fixed) pacing if #525619 can be resolved, otherwise we will update docs advising to disable VSync in games.
// Also negotiate variable rate for all other compositors. Mutter's variable rate pacing is superior.
const static std::vector<int> kwin_version = get_running_kwin_version();
const static bool negotiate_variable_rate = kwin_version.empty() || (kwin_version[0] == 5 || (kwin_version[0] == 6 && (kwin_version[1] < 7 || (kwin_version[1] == 7 && kwin_version[2] < 80))));
const static auto compositor = pipewire.get_running_compositor();

const AVRational fps = (negotiate_variable_rate ? AVRational {0, 1} : ::video::framerate_to_rational(config));
// Determine if variable rate should be negotiated based on compositor type/versioning.
pipewire.negotiate_variable_rate = pipewire.use_variable_rate(compositor);

const AVRational fps = (pipewire.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;
} else if (fps.num == 0 && fps.den == 1) {
Expand Down Expand Up @@ -883,6 +1089,9 @@ namespace pipewire {
// Verify or update display parameters for streaming to ensure absolute touch inputs work as expected
verify_and_update_display_parameters();

// Determine if pts metadata should be sourced from Pipewire or sampled by Sunshine at time of capture.
pipewire.prefer_pipewire_pts = pipewire.use_pipewire_pts(compositor, display_name);

if (!shared_state) {
shared_state = std::make_shared<shared_state_t>();
} else {
Expand Down Expand Up @@ -1182,89 +1391,6 @@ namespace pipewire {
return false;
}

/**
* Fetch the currently running KWin version (if available from its DBUS support information method)
*
* @return A vector with 3 elements containing KWin's major.minor.micro version or an empty vector if KWin's version could not be determined
*/
static std::vector<int> get_running_kwin_version() {
#if !GLIB_CHECK_VERSION(2, 74, 0)
// Compatibility for Ubuntu 22.04 (Glib 2.72)
constexpr auto G_REGEX_DEFAULT = static_cast<GRegexCompileFlags>(0);
constexpr auto G_REGEX_MATCH_DEFAULT = static_cast<GRegexMatchFlags>(0);
#endif
auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
std::vector<int> result;

if (!conn) {
return result;
}

auto reply = g_dbus_connection_call_sync(
conn,
"org.kde.KWin",
"/KWin",
"org.kde.KWin",
"supportInformation",
nullptr,
G_VARIANT_TYPE("(s)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
nullptr
);

if (!reply) {
g_clear_object(&conn);
return result;
}

g_autofree gchar *support_info = nullptr;
g_variant_get(reply, "(s)", &support_info);

if (!support_info) {
g_variant_unref(reply);
g_clear_object(&conn);
return result;
}

auto *regex = g_regex_new(
"KWin version: ([0-9]+)\\.([0-9]+)\\.([0-9]+)",
G_REGEX_DEFAULT,
G_REGEX_MATCH_DEFAULT,
nullptr
);

if (!regex) {
g_variant_unref(reply);
g_clear_object(&conn);
return result;
}

GMatchInfo *match_info = nullptr;
g_regex_match(regex, support_info, G_REGEX_MATCH_DEFAULT, &match_info);

if (g_match_info_matches(match_info)) {
g_autofree const gchar *major =
g_match_info_fetch(match_info, 1);
g_autofree const gchar *minor =
g_match_info_fetch(match_info, 2);
g_autofree const gchar *micro =
g_match_info_fetch(match_info, 3);

result.emplace_back(std::atoi(major));
result.emplace_back(std::atoi(minor));
result.emplace_back(std::atoi(micro));
}

g_match_info_free(match_info);
g_regex_unref(regex);
g_variant_unref(reply);
g_clear_object(&conn);

return result;
}

private:
bool is_buffer_redundant(const egl::img_descriptor_t *img) {
// Check for corrupted frame
Expand Down
Loading