From 4962012ca4959d380addd9f1989c72314eea580f Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Thu, 24 Sep 2026 01:20:38 +0100 Subject: [PATCH 1/7] feat(linux/pipewire): add pts passthrough whitelist, cleanups Allow passthrough of more accurate pts metadata for specific compositor versions that are known to generate valid timestamps. * Add GNOME Shell (Mutter) detection function. * Reuse existing KWin detection for pts whitelist. * Guard against stale Pipewire pts metadata. * Initialize variables in init() once to avoid first-run stalls in fill_img_metadata() when determining pts source. * Refactor logic to determine if variable framerate should be negotiated into separate function. --- src/platform/linux/pipewire.cpp | 308 ++++++++++++++++++++++---------- 1 file changed, 211 insertions(+), 97 deletions(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 6cfb0a0f07a..6bfc5b12ea3 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -394,6 +394,195 @@ namespace pipewire { } } + /** + * 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 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(0); + constexpr auto G_REGEX_MATCH_DEFAULT = static_cast(0); +#endif + auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + std::vector 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) { + 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; + } + + /** + * Fetch the currently running GNOME Shell version from its ShellVersion DBus property. The version is used as a proxy for the Mutter version. + * + * @return A vector with 2-3 elements containing Shell/Mutter's major.minor.micro version or an empty vector if the Shell version could not be determined. + */ + static std::vector get_running_gnome_shell_version() { + std::vector result; + auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + + if (!conn) { + return result; + } + + auto 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; + } + + GVariant *variant = nullptr; + g_variant_get(reply, "(v)", &variant); + + if (!variant) { + g_variant_unref(reply); + g_clear_object(&conn); + return result; + } + + const char *version_str = g_variant_get_string(variant, nullptr); + if (version_str) { + int major = 0, minor = 0, micro = 0; + + int scanned = sscanf(version_str, "%d.%d.%d", &major, &minor, µ); + if (scanned >= 1) { + result.emplace_back(major); + } + if (scanned >= 2) { + result.emplace_back(minor); + } + if (scanned >= 3) { + result.emplace_back(micro); + } + } + + g_variant_unref(variant); + g_variant_unref(reply); + g_clear_object(&conn); + + return result; + } + + /** + * Determine if Pipewire's pts metadata is suitable for client pacing based on compositor type/version whitelist. + * + * @return True if pts metadata is suitable. + */ + static bool use_pipewire_pts() { + // KWin: use Pipewire pts metadata for versions 6.7.80+ (6.8 beta) or newer. + // Mutter: use Pipewire pts metadata for Mutter 51 onwards. + // All other cases: don't use Pipewire pts metadata directly. + + bool use_pts = false; + std::vector gnome_version; + std::vector kwin_version; + + if (!(kwin_version = get_running_kwin_version()).empty()) { + use_pts = (kwin_version[0] > 6 || (kwin_version[0] == 6 && (kwin_version[1] > 7 || (kwin_version[1] == 7 && kwin_version[2] > 79)))); + } else if (!(gnome_version = get_running_gnome_shell_version()).empty()) { + use_pts = gnome_version[0] >= 51; + } + + BOOST_LOG(info) << "[pipewire] Using frame_timestamp (pts) metadata source: "sv + << (use_pts ? "Pipewire (via compositor)" : "Sunshine"); + return use_pts; + } + + /** + * Determine if the active compositor is suited for variable rate capture based on type/version whitelist. + * + * @return True if variable rate capture is suitable. + */ + static bool use_variable_rate() { + // 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; + + const static std::vector kwin_version = get_running_kwin_version(); + if (!kwin_version.empty()) { + variable_rate = (kwin_version[0] == 5 || (kwin_version[0] == 6 && (kwin_version[1] < 7 || (kwin_version[1] == 7 && kwin_version[2] < 80)))); + } + + return variable_rate; + } + /** * @brief Copy PipeWire metadata into the Sunshine image descriptor. * @@ -401,15 +590,21 @@ namespace pipewire { * @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( 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; @@ -490,6 +685,10 @@ namespace pipewire { negotiate_maxframerate_ = negotiate_maxframerate; } + inline static std::once_flag compositor_probe_once; + inline static bool prefer_pipewire_pts = false; + inline static bool negotiate_variable_rate = true; + private: struct pw_thread_loop *loop; struct pw_context *context; @@ -844,18 +1043,16 @@ 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 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)))); + // These compositor checks are not expected to change during the process lifecycle. + std::call_once(pipewire.compositor_probe_once, [this] { + // Determine if variable rate should be negotiated based on compositor type/versioning. + pipewire.negotiate_variable_rate = pipewire.use_variable_rate(); + + // 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(); + }); - const AVRational fps = (negotiate_variable_rate ? AVRational {0, 1} : ::video::framerate_to_rational(config)); + 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) { @@ -1182,89 +1379,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 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(0); - constexpr auto G_REGEX_MATCH_DEFAULT = static_cast(0); -#endif - auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); - std::vector 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 From 63c35986be0ebc0c86f7881817b80c8ef47db810 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Thu, 24 Sep 2026 19:00:12 +0100 Subject: [PATCH 2/7] fixup: merge compositor detection function and narrow mutter detection --- src/platform/linux/kmsgrab.cpp | 1 + src/platform/linux/pipewire.cpp | 249 +++++++++++++++++--------------- 2 files changed, 132 insertions(+), 118 deletions(-) diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 120fe9cc954..909b84a06a0 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -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); diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 6bfc5b12ea3..0b070777d16 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -395,11 +395,47 @@ namespace pipewire { } /** - * Fetch the currently running KWin version (if available from its DBus support information method) + * @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; + std::vector version; + }; + + /** + * Fetch compositor type and version for processing. * - * @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. + * @return A struct containing the compositor type and version. */ - static std::vector get_running_kwin_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 {}; + } + + /** + * Fetch compositor version information using DBus calls. + * + * @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 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(0); @@ -412,53 +448,84 @@ namespace pipewire { 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 - ); + 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 *support_info = nullptr; - g_variant_get(reply, "(s)", &support_info); + g_autofree gchar *version_str = nullptr; - if (!support_info) { + 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; } - auto *regex = g_regex_new( - "KWin version: ([0-9]+)\\.([0-9]+)\\.([0-9]+)", - G_REGEX_DEFAULT, - G_REGEX_MATCH_DEFAULT, - nullptr - ); - - if (regex) { + 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, support_info, G_REGEX_MATCH_DEFAULT, &match_info); + 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); - result.emplace_back(std::atoi(major)); - result.emplace_back(std::atoi(minor)); - result.emplace_back(std::atoi(micro)); + 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); @@ -469,92 +536,34 @@ namespace pipewire { return result; } - /** - * Fetch the currently running GNOME Shell version from its ShellVersion DBus property. The version is used as a proxy for the Mutter version. - * - * @return A vector with 2-3 elements containing Shell/Mutter's major.minor.micro version or an empty vector if the Shell version could not be determined. - */ - static std::vector get_running_gnome_shell_version() { - std::vector result; - auto conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); - - if (!conn) { - return result; - } - - auto 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; - } - - GVariant *variant = nullptr; - g_variant_get(reply, "(v)", &variant); - - if (!variant) { - g_variant_unref(reply); - g_clear_object(&conn); - return result; - } - - const char *version_str = g_variant_get_string(variant, nullptr); - if (version_str) { - int major = 0, minor = 0, micro = 0; - - int scanned = sscanf(version_str, "%d.%d.%d", &major, &minor, µ); - if (scanned >= 1) { - result.emplace_back(major); - } - if (scanned >= 2) { - result.emplace_back(minor); - } - if (scanned >= 3) { - result.emplace_back(micro); - } - } - - g_variant_unref(variant); - g_variant_unref(reply); - g_clear_object(&conn); - - return result; - } - /** * Determine if Pipewire's pts metadata is suitable for client pacing based on compositor type/version whitelist. * * @return True if pts metadata is suitable. */ - static bool use_pipewire_pts() { + 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. + // 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; - std::vector gnome_version; - std::vector kwin_version; + bool using_virtual_monitor = false; - if (!(kwin_version = get_running_kwin_version()).empty()) { - use_pts = (kwin_version[0] > 6 || (kwin_version[0] == 6 && (kwin_version[1] > 7 || (kwin_version[1] == 7 && kwin_version[2] > 79)))); - } else if (!(gnome_version = get_running_gnome_shell_version()).empty()) { - use_pts = gnome_version[0] >= 51; + using enum compositor_type_e; + switch (compositor.type) { + case gnome: + using_virtual_monitor = (selected_display_name.find("Meta-", 0) == 0); + 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)" : "Sunshine"); + << (use_pts ? "Pipewire (via compositor)" : PROJECT_NAME); return use_pts; } @@ -563,7 +572,8 @@ namespace pipewire { * * @return True if variable rate capture is suitable. */ - static bool use_variable_rate() { + 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 @@ -575,9 +585,13 @@ namespace pipewire { // All other compositors (including Mutter) will default to variable rate. bool variable_rate = true; - const static std::vector kwin_version = get_running_kwin_version(); - if (!kwin_version.empty()) { - variable_rate = (kwin_version[0] == 5 || (kwin_version[0] == 6 && (kwin_version[1] < 7 || (kwin_version[1] == 7 && kwin_version[2] < 80)))); + using enum compositor_type_e; + switch (compositor.type) { + case kwin: + variable_rate = (compositor.version[0] == 5 || (compositor.version[0] == 6 && (compositor.version[1] < 7 || (compositor.version[1] == 7 && compositor.version[2] < 80)))); + break; + default: + break; } return variable_rate; @@ -685,9 +699,8 @@ namespace pipewire { negotiate_maxframerate_ = negotiate_maxframerate; } - inline static std::once_flag compositor_probe_once; - inline static bool prefer_pipewire_pts = false; - inline static bool negotiate_variable_rate = true; + inline static std::atomic prefer_pipewire_pts = false; + inline static std::atomic negotiate_variable_rate = true; private: struct pw_thread_loop *loop; @@ -1043,14 +1056,11 @@ namespace pipewire { // calculate frame interval we should capture at delay = ::video::capture_frame_interval(config); - // These compositor checks are not expected to change during the process lifecycle. - std::call_once(pipewire.compositor_probe_once, [this] { - // Determine if variable rate should be negotiated based on compositor type/versioning. - pipewire.negotiate_variable_rate = pipewire.use_variable_rate(); + using enum pipewire_t::compositor_type_e; + const auto compositor = pipewire.get_running_compositor(); - // 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(); - }); + // 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) { @@ -1080,6 +1090,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(); } else { From 75347e8632e65435940e7fda916d93ba3b3610f8 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Fri, 25 Sep 2026 00:52:06 +0100 Subject: [PATCH 3/7] fixup: sonar issues --- src/platform/linux/pipewire.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 0b070777d16..e8bae3fc84a 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -586,12 +586,8 @@ namespace pipewire { bool variable_rate = true; using enum compositor_type_e; - switch (compositor.type) { - case kwin: - variable_rate = (compositor.version[0] == 5 || (compositor.version[0] == 6 && (compositor.version[1] < 7 || (compositor.version[1] == 7 && compositor.version[2] < 80)))); - break; - default: - break; + 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; @@ -699,8 +695,8 @@ namespace pipewire { negotiate_maxframerate_ = negotiate_maxframerate; } - inline static std::atomic prefer_pipewire_pts = false; - inline static std::atomic negotiate_variable_rate = true; + inline static std::atomic prefer_pipewire_pts {false}; + inline static std::atomic negotiate_variable_rate {true}; private: struct pw_thread_loop *loop; From 8911a722beeffade3a19121bc76a770126f3d567 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Fri, 25 Sep 2026 01:07:10 +0100 Subject: [PATCH 4/7] fixup: avoid multiple DBus calls across init cycles --- src/platform/linux/pipewire.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index e8bae3fc84a..1369384385e 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -1052,8 +1052,7 @@ namespace pipewire { // calculate frame interval we should capture at delay = ::video::capture_frame_interval(config); - using enum pipewire_t::compositor_type_e; - const auto compositor = pipewire.get_running_compositor(); + const static auto compositor = pipewire.get_running_compositor(); // Determine if variable rate should be negotiated based on compositor type/versioning. pipewire.negotiate_variable_rate = pipewire.use_variable_rate(compositor); From bded576c323e635e882531be67cb8ea8b5b020c5 Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Fri, 25 Sep 2026 01:13:12 +0100 Subject: [PATCH 5/7] fixup: starts_with --- src/platform/linux/pipewire.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 1369384385e..3a16f792611 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -552,7 +552,7 @@ namespace pipewire { using enum compositor_type_e; switch (compositor.type) { case gnome: - using_virtual_monitor = (selected_display_name.find("Meta-", 0) == 0); + using_virtual_monitor = selected_display_name.starts_with("Meta-"); use_pts = (using_virtual_monitor && compositor.version[0] >= 51); break; case kwin: From e040264252ef6930f7ce4d424a531c5f295f47ac Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Fri, 25 Sep 2026 02:35:48 +0100 Subject: [PATCH 6/7] fixup: doxygen issues --- src/platform/linux/pipewire.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 3a16f792611..7bed00155c3 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -407,8 +407,8 @@ namespace pipewire { * @brief Compositor type and version information. */ struct compositor_version_t { - compositor_type_e type = compositor_type_e::unknown; - std::vector version; + compositor_type_e type = compositor_type_e::unknown; ///< Compositor type (gnome, kde...). + std::vector version; ///< Compositor version. }; /** @@ -433,6 +433,7 @@ namespace pipewire { /** * 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 get_running_compositor_version(enum compositor_type_e compositor_type) { @@ -539,6 +540,8 @@ namespace pipewire { /** * 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) { @@ -570,6 +573,7 @@ namespace pipewire { /** * 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 @@ -695,8 +699,8 @@ namespace pipewire { negotiate_maxframerate_ = negotiate_maxframerate; } - inline static std::atomic prefer_pipewire_pts {false}; - inline static std::atomic negotiate_variable_rate {true}; + 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; From 1520bee44dc5dc0d444c4b842623b42fd834f8bf Mon Sep 17 00:00:00 2001 From: Conn O'Griofa Date: Fri, 25 Sep 2026 02:51:02 +0100 Subject: [PATCH 7/7] fixup: doxygen: add @brief --- src/platform/linux/pipewire.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 7bed00155c3..7d9b9076c29 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -412,7 +412,7 @@ namespace pipewire { }; /** - * Fetch compositor type and version for processing. + * @brief Fetch compositor type and version for processing. * * @return A struct containing the compositor type and version. */ @@ -431,7 +431,7 @@ namespace pipewire { } /** - * Fetch compositor version information using DBus calls. + * @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. @@ -538,7 +538,7 @@ namespace pipewire { } /** - * Determine if Pipewire's pts metadata is suitable for client pacing based on compositor type/version whitelist. + * @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. @@ -571,7 +571,7 @@ namespace pipewire { } /** - * Determine if the active compositor is suited for variable rate capture based on type/version whitelist. + * @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.