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 6cfb0a0f07a..7d9b9076c29 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -394,6 +394,209 @@ 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 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 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); + 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; + } + + 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. * @@ -401,15 +604,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 +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; @@ -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 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) { @@ -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(); } else { @@ -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 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