/** * @file src/platform/linux/portalgrab.cpp * @brief Definitions for XDG portal grab. */ // standard includes #include #include #include #include #include #include #include #include #include // lib includes #include #include #include #include #include #include #include // local includes #include "cuda.h" #include "graphics.h" #include "src/main.h" #include "src/platform/common.h" #include "src/video.h" #include "vaapi.h" #include "wayland.h" #if !defined(__FreeBSD__) // platform includes #include #include #endif namespace { // Buffer and limit constants constexpr int SPA_POD_BUFFER_SIZE = 4096; constexpr int MAX_PARAMS = 200; constexpr int MAX_DMABUF_FORMATS = 200; constexpr int MAX_DMABUF_MODIFIERS = 200; // Portal configuration constants constexpr uint32_t SOURCE_TYPE_MONITOR = 1; constexpr uint32_t CURSOR_MODE_EMBEDDED = 2; constexpr uint32_t PERSIST_FORGET = 0; constexpr uint32_t PERSIST_WHILE_RUNNING = 1; constexpr uint32_t PERSIST_UNTIL_REVOKED = 2; constexpr uint32_t TYPE_KEYBOARD = 1; constexpr uint32_t TYPE_POINTER = 2; constexpr uint32_t TYPE_TOUCHSCREEN = 4; // Portal D-Bus interface names and paths constexpr const char *PORTAL_NAME = "org.freedesktop.portal.Desktop"; constexpr const char *PORTAL_PATH = "/org/freedesktop/portal/desktop"; constexpr const char *REMOTE_DESKTOP_IFACE = "org.freedesktop.portal.RemoteDesktop"; constexpr const char *SCREENCAST_IFACE = "org.freedesktop.portal.ScreenCast"; constexpr const char *REQUEST_IFACE = "org.freedesktop.portal.Request"; constexpr const char REQUEST_PREFIX[] = "/org/freedesktop/portal/desktop/request/"; constexpr const char SESSION_PREFIX[] = "/org/freedesktop/portal/desktop/session/"; } // namespace using namespace std::literals; namespace portal { // Forward declarations class session_cache_t; class restore_token_t { public: static std::string get() { return *token_; } static void set(std::string_view value) { *token_ = value; } static bool empty() { return token_->empty(); } static void load() { std::ifstream file(get_file_path()); if (file.is_open()) { std::getline(file, *token_); if (!token_->empty()) { BOOST_LOG(info) << "Loaded portal restore token from disk"sv; } } } static void save() { if (token_->empty()) { return; } std::ofstream file(get_file_path()); if (file.is_open()) { file << *token_; BOOST_LOG(info) << "Saved portal restore token to disk"sv; } else { BOOST_LOG(warning) << "Failed to save portal restore token"sv; } } private: static inline const std::unique_ptr token_ = std::make_unique(); static std::string get_file_path() { return platf::appdata().string() + "/portal_token"; } }; struct format_map_t { uint64_t fourcc; int32_t pw_format; }; static constexpr std::array format_map = {{ {DRM_FORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA}, {DRM_FORMAT_XRGB8888, SPA_VIDEO_FORMAT_BGRx}, {0, 0}, }}; struct dbus_response_t { GMainLoop *loop; GVariant *response; guint subscription_id; }; struct shared_state_t { std::atomic negotiated_width {0}; std::atomic negotiated_height {0}; std::atomic stream_dead {false}; }; struct stream_data_t { struct pw_stream *stream; struct spa_hook stream_listener; struct spa_video_info format; struct pw_buffer *current_buffer; uint64_t drm_format; std::shared_ptr shared; std::mutex frame_mutex; std::condition_variable frame_cv; size_t local_stride = 0; bool frame_ready = false; // Two distinct memory pools std::vector buffer_a; std::vector buffer_b; // Points to the buffer currently owned by fill_img std::vector *front_buffer; // Points to the buffer currently being written by on_process std::vector *back_buffer; stream_data_t(): front_buffer(&buffer_a), back_buffer(&buffer_b) {} }; struct dmabuf_format_info_t { int32_t format; uint64_t *modifiers; int n_modifiers; }; class dbus_t { public: ~dbus_t() noexcept { try { if (conn && !session_handle.empty()) { g_autoptr(GError) err = nullptr; // This is a blocking C call; it won't throw, but we wrap for safety g_dbus_connection_call_sync( conn, "org.freedesktop.portal.Desktop", session_handle.c_str(), "org.freedesktop.portal.Session", "Close", nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err ); if (err) { BOOST_LOG(warning) << "Failed to explicitly close portal session: "sv << err->message; } else { BOOST_LOG(debug) << "Explicitly closed portal session: "sv << session_handle; } } } catch (const std::exception &e) { BOOST_LOG(error) << "Standard exception caught in ~dbus_t: "sv << e.what(); } catch (...) { BOOST_LOG(error) << "Unknown exception caught in ~dbus_t"sv; } if (screencast_proxy) { g_clear_object(&screencast_proxy); } if (remote_desktop_proxy) { g_clear_object(&remote_desktop_proxy); } if (conn) { g_clear_object(&conn); } } int init() { restore_token_t::load(); conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); if (!conn) { return -1; } remote_desktop_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, nullptr, PORTAL_NAME, PORTAL_PATH, REMOTE_DESKTOP_IFACE, nullptr, nullptr); if (!remote_desktop_proxy) { return -1; } screencast_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, nullptr, PORTAL_NAME, PORTAL_PATH, SCREENCAST_IFACE, nullptr, nullptr); if (!screencast_proxy) { return -1; } return 0; } void finalize_portal_security() { #if !defined(__FreeBSD__) BOOST_LOG(debug) << "Finalizing Portal security: dropping capabilities and resetting dumpable"sv; cap_t caps = cap_get_proc(); if (!caps) { BOOST_LOG(error) << "Failed to get process capabilities"sv; return; } std::array effective_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; std::array permitted_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; cap_set_flag(caps, CAP_EFFECTIVE, effective_list.size(), effective_list.data(), CAP_CLEAR); cap_set_flag(caps, CAP_PERMITTED, permitted_list.size(), permitted_list.data(), CAP_CLEAR); if (cap_set_proc(caps) != 0) { BOOST_LOG(error) << "Failed to prune capabilities: "sv << std::strerror(errno); } cap_free(caps); // Reset dumpable AFTER the caps have been pruned to ensure the Portal can // access /proc/pid/root. if (prctl(PR_SET_DUMPABLE, 1) != 0) { BOOST_LOG(error) << "Failed to set PR_SET_DUMPABLE: "sv << std::strerror(errno); } #endif } int connect_to_portal() { g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, FALSE); g_autofree gchar *session_path = nullptr; g_autofree gchar *session_token = nullptr; create_session_path(conn, nullptr, &session_token); // Drop CAP_SYS_ADMIN and set DUMPABLE flag to allow XDG /root access finalize_portal_security(); // Try combined RemoteDesktop + ScreenCast session first bool use_screencast_only = !try_remote_desktop_session(loop, &session_path, session_token); // Fall back to ScreenCast-only if RemoteDesktop failed if (use_screencast_only && try_screencast_only_session(loop, &session_path) < 0) { return -1; } if (start_portal_session(loop, session_path, pipewire_node, width, height, use_screencast_only) < 0) { return -1; } if (open_pipewire_remote(session_path, pipewire_fd) < 0) { return -1; } return 0; } // Try to create a combined RemoteDesktop + ScreenCast session // Returns true on success, false if should fall back to ScreenCast-only bool try_remote_desktop_session(GMainLoop *loop, gchar **session_path, const gchar *session_token) { if (create_portal_session(loop, session_path, session_token, false) < 0) { return false; } if (select_remote_desktop_devices(loop, *session_path) < 0) { BOOST_LOG(warning) << "RemoteDesktop.SelectDevices failed, falling back to ScreenCast-only mode"sv; g_free(*session_path); *session_path = nullptr; return false; } if (select_screencast_sources(loop, *session_path, false) < 0) { BOOST_LOG(warning) << "ScreenCast.SelectSources failed with RemoteDesktop session, trying ScreenCast-only mode"sv; g_free(*session_path); *session_path = nullptr; return false; } return true; } // Create a ScreenCast-only session int try_screencast_only_session(GMainLoop *loop, gchar **session_path) { g_autofree gchar *new_session_token = nullptr; create_session_path(conn, nullptr, &new_session_token); if (create_portal_session(loop, session_path, new_session_token, true) < 0) { return -1; } if (select_screencast_sources(loop, *session_path, true) < 0) { g_free(*session_path); *session_path = nullptr; return -1; } return 0; } int pipewire_fd; int pipewire_node; int width; int height; private: GDBusConnection *conn; GDBusProxy *screencast_proxy; GDBusProxy *remote_desktop_proxy; std::string session_handle; int create_portal_session(GMainLoop *loop, gchar **session_path_out, const gchar *session_token, bool use_screencast) { GDBusProxy *proxy = use_screencast ? screencast_proxy : remote_desktop_proxy; const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop"; dbus_response_t response = { nullptr, }; g_autofree gchar *request_token = nullptr; create_request_path(conn, nullptr, &request_token); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(a{sv})")); g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(session_token)); g_variant_builder_close(&builder); g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "CreateSession", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { BOOST_LOG(error) << "Could not create "sv << session_type << " session: "sv << err->message; return -1; } const gchar *request_path = nullptr; g_variant_get(reply, "(o)", &request_path); dbus_response_init(&response, loop, conn, request_path); g_autoptr(GVariant) create_response = dbus_response_wait(&response); if (!create_response) { BOOST_LOG(error) << session_type << " CreateSession: no response received"sv; return -1; } guint32 response_code; g_autoptr(GVariant) results = nullptr; g_variant_get(create_response, "(u@a{sv})", &response_code, &results); BOOST_LOG(debug) << session_type << " CreateSession response_code: "sv << response_code; if (response_code != 0) { BOOST_LOG(error) << session_type << " CreateSession failed with response code: "sv << response_code; return -1; } g_autoptr(GVariant) session_handle_v = g_variant_lookup_value(results, "session_handle", nullptr); if (!session_handle_v) { BOOST_LOG(error) << session_type << " CreateSession: session_handle not found in response"sv; return -1; } if (g_variant_is_of_type(session_handle_v, G_VARIANT_TYPE_VARIANT)) { g_autoptr(GVariant) inner = g_variant_get_variant(session_handle_v); *session_path_out = g_strdup(g_variant_get_string(inner, nullptr)); } else { *session_path_out = g_strdup(g_variant_get_string(session_handle_v, nullptr)); } BOOST_LOG(debug) << session_type << " CreateSession: got session handle: "sv << *session_path_out; // Save it for the destructor to use during cleanup this->session_handle = *session_path_out; return 0; } int select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) { dbus_response_t response = { nullptr, }; g_autofree gchar *request_token = nullptr; create_request_path(conn, nullptr, &request_token); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(oa{sv})")); g_variant_builder_add(&builder, "o", session_path); g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(TYPE_KEYBOARD | TYPE_POINTER | TYPE_TOUCHSCREEN)); g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_UNTIL_REVOKED)); if (!restore_token_t::empty()) { g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string(restore_token_t::get().c_str())); } g_variant_builder_close(&builder); g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { BOOST_LOG(error) << "Could not select devices: "sv << err->message; return -1; } const gchar *request_path = nullptr; g_variant_get(reply, "(o)", &request_path); dbus_response_init(&response, loop, conn, request_path); g_autoptr(GVariant) devices_response = dbus_response_wait(&response); if (!devices_response) { BOOST_LOG(error) << "SelectDevices: no response received"sv; return -1; } guint32 response_code; g_variant_get(devices_response, "(u@a{sv})", &response_code, nullptr); BOOST_LOG(debug) << "SelectDevices response_code: "sv << response_code; if (response_code != 0) { BOOST_LOG(error) << "SelectDevices failed with response code: "sv << response_code; return -1; } return 0; } int select_screencast_sources(GMainLoop *loop, const gchar *session_path, bool persist) { dbus_response_t response = { nullptr, }; g_autofree gchar *request_token = nullptr; create_request_path(conn, nullptr, &request_token); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(oa{sv})")); g_variant_builder_add(&builder, "o", session_path); g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(SOURCE_TYPE_MONITOR)); g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(CURSOR_MODE_EMBEDDED)); if (persist) { g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_UNTIL_REVOKED)); if (!restore_token_t::empty()) { g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string(restore_token_t::get().c_str())); } } g_variant_builder_close(&builder); g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { BOOST_LOG(error) << "Could not select sources: "sv << err->message; return -1; } const gchar *request_path = nullptr; g_variant_get(reply, "(o)", &request_path); dbus_response_init(&response, loop, conn, request_path); g_autoptr(GVariant) sources_response = dbus_response_wait(&response); if (!sources_response) { BOOST_LOG(error) << "SelectSources: no response received"sv; return -1; } guint32 response_code; g_variant_get(sources_response, "(u@a{sv})", &response_code, nullptr); BOOST_LOG(debug) << "SelectSources response_code: "sv << response_code; if (response_code != 0) { BOOST_LOG(error) << "SelectSources failed with response code: "sv << response_code; return -1; } return 0; } int start_portal_session(GMainLoop *loop, const gchar *session_path, int &out_pipewire_node, int &out_width, int &out_height, bool use_screencast) { GDBusProxy *proxy = use_screencast ? screencast_proxy : remote_desktop_proxy; const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop"; dbus_response_t response = { nullptr, }; g_autofree gchar *request_token = nullptr; create_request_path(conn, nullptr, &request_token); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(osa{sv})")); g_variant_builder_add(&builder, "o", session_path); g_variant_builder_add(&builder, "s", ""); // parent_window g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_close(&builder); g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "Start", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { BOOST_LOG(error) << "Could not start "sv << session_type << " session: "sv << err->message; return -1; } const gchar *request_path = nullptr; g_variant_get(reply, "(o)", &request_path); dbus_response_init(&response, loop, conn, request_path); g_autoptr(GVariant) start_response = dbus_response_wait(&response); if (!start_response) { BOOST_LOG(error) << session_type << " Start: no response received"sv; return -1; } guint32 response_code; g_autoptr(GVariant) dict = nullptr; g_autoptr(GVariant) streams = nullptr; g_variant_get(start_response, "(u@a{sv})", &response_code, &dict); BOOST_LOG(debug) << session_type << " Start response_code: "sv << response_code; if (response_code != 0) { BOOST_LOG(error) << session_type << " Start failed with response code: "sv << response_code; return -1; } streams = g_variant_lookup_value(dict, "streams", G_VARIANT_TYPE("a(ua{sv})")); if (!streams) { BOOST_LOG(error) << session_type << " Start: no streams in response"sv; return -1; } if (const gchar *new_token = nullptr; g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && new_token[0] != '\0' && restore_token_t::get() != new_token) { restore_token_t::set(new_token); restore_token_t::save(); } GVariantIter iter; g_autoptr(GVariant) value = nullptr; g_variant_iter_init(&iter, streams); while (g_variant_iter_next(&iter, "(u@a{sv})", &out_pipewire_node, &value)) { g_variant_lookup(value, "size", "(ii)", &out_width, &out_height, nullptr); } return 0; } int open_pipewire_remote(const gchar *session_path, int &fd) { g_autoptr(GUnixFDList) fd_list = nullptr; g_autoptr(GVariant) msg = g_variant_ref_sink(g_variant_new("(oa{sv})", session_path, nullptr)); g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync(screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &fd_list, nullptr, &err); if (err) { BOOST_LOG(error) << "Could not open pipewire remote: "sv << err->message; return -1; } int fd_handle; g_variant_get(reply, "(h)", &fd_handle); fd = g_unix_fd_list_get(fd_list, fd_handle, nullptr); return 0; } static void on_response_received_cb([[maybe_unused]] GDBusConnection *connection, [[maybe_unused]] const gchar *sender_name, [[maybe_unused]] const gchar *object_path, [[maybe_unused]] const gchar *interface_name, [[maybe_unused]] const gchar *signal_name, GVariant *parameters, gpointer user_data) { auto *response = static_cast(user_data); response->response = g_variant_ref_sink(parameters); g_main_loop_quit(response->loop); } static gchar *get_sender_string(GDBusConnection *conn) { gchar *sender = g_strdup(g_dbus_connection_get_unique_name(conn) + 1); gchar *dot; while ((dot = strstr(sender, ".")) != nullptr) { *dot = '_'; } return sender; } static void create_request_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) { static uint32_t request_count = 0; request_count++; if (out_token) { *out_token = g_strdup_printf("Sunshine%u", request_count); } if (out_path) { g_autofree gchar *sender = get_sender_string(conn); *out_path = g_strdup(std::format("{}{}{}{}", REQUEST_PREFIX, sender, "/Sunshine", request_count).c_str()); } } static void create_session_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) { static uint32_t session_count = 0; session_count++; if (out_token) { *out_token = g_strdup_printf("Sunshine%u", session_count); } if (out_path) { g_autofree gchar *sender = get_sender_string(conn); *out_path = g_strdup(std::format("{}{}{}{}", SESSION_PREFIX, sender, "/Sunshine", session_count).c_str()); } } static void dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) { response->loop = loop; response->subscription_id = g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, on_response_received_cb, response, nullptr); } static GVariant *dbus_response_wait(struct dbus_response_t *response) { g_main_loop_run(response->loop); return response->response; } }; /** * @brief Singleton cache for portal session data. * * This prevents creating multiple portal sessions during encoder probing, * which would show multiple screen recording indicators in the system tray. */ class session_cache_t { public: static session_cache_t &instance(); /** * @brief Get or create a portal session. * * If a cached session exists and is valid, returns the cached data. * Otherwise, creates a new session and caches it. * * @return 0 on success, -1 on failure */ int get_or_create_session(int &pipewire_fd, int &pipewire_node, int &width, int &height) { std::scoped_lock lock(mutex_); if (valid_) { // Return cached session data pipewire_fd = dup(pipewire_fd_); // Duplicate FD for each caller pipewire_node = pipewire_node_; width = width_; height = height_; BOOST_LOG(debug) << "Reusing cached portal session"sv; return 0; } // Create new session dbus_ = std::make_unique(); if (dbus_->init() < 0) { return -1; } if (dbus_->connect_to_portal() < 0) { dbus_.reset(); return -1; } // Cache the session data pipewire_fd_ = dbus_->pipewire_fd; pipewire_node_ = dbus_->pipewire_node; width_ = dbus_->width; height_ = dbus_->height; valid_ = true; // Return to caller (duplicate FD so each caller has their own) pipewire_fd = dup(pipewire_fd_); pipewire_node = pipewire_node_; width = width_; height = height_; BOOST_LOG(debug) << "Created new portal session (cached)"sv; return 0; } /** * @brief Invalidate the cached session. * * Call this when the session becomes invalid (e.g., on error). */ void invalidate() noexcept { try { std::scoped_lock lock(mutex_); if (valid_) { BOOST_LOG(debug) << "Invalidating cached portal session"sv; if (pipewire_fd_ >= 0) { close(pipewire_fd_); pipewire_fd_ = -1; } dbus_.reset(); valid_ = false; } } catch (const std::exception &e) { BOOST_LOG(error) << "Exception during session invalidation: "sv << e.what(); } catch (...) { BOOST_LOG(error) << "Unknown error during session invalidation"sv; } } bool is_maxframerate_failed() const { return maxframerate_failed_; } void set_maxframerate_failed() { maxframerate_failed_ = true; } private: session_cache_t() = default; ~session_cache_t() { if (pipewire_fd_ >= 0) { close(pipewire_fd_); } } // Prevent copying session_cache_t(const session_cache_t &) = delete; session_cache_t &operator=(const session_cache_t &) = delete; std::mutex mutex_; std::unique_ptr dbus_; int pipewire_fd_ = -1; int pipewire_node_ = 0; int width_ = 0; int height_ = 0; bool valid_ = false; bool maxframerate_failed_ = false; }; session_cache_t &session_cache_t::instance() { alignas(session_cache_t) static std::array storage; static auto instance_ = new (storage.data()) session_cache_t(); return *instance_; } class pipewire_t { public: pipewire_t(): loop(pw_thread_loop_new("Pipewire thread", nullptr)) { pw_thread_loop_start(loop); } ~pipewire_t() { if (loop) { pw_thread_loop_stop(loop); } cleanup_stream(); pw_thread_loop_lock(loop); if (core) { pw_core_disconnect(core); core = nullptr; } if (context) { pw_context_destroy(context); context = nullptr; } pw_thread_loop_unlock(loop); if (fd >= 0) { close(fd); } pw_thread_loop_destroy(loop); } std::mutex &frame_mutex() { return stream_data.frame_mutex; } std::condition_variable &frame_cv() { return stream_data.frame_cv; } bool is_frame_ready() const { return stream_data.frame_ready; } void set_frame_ready(bool ready) { stream_data.frame_ready = ready; } void init(int stream_fd, int stream_node, std::shared_ptr shared_state) { fd = stream_fd; node = stream_node; stream_data.shared = std::move(shared_state); pw_thread_loop_lock(loop); context = pw_context_new(pw_thread_loop_get_loop(loop), nullptr, 0); if (context) { core = pw_context_connect_fd(context, dup(fd), nullptr, 0); if (core) { pw_core_add_listener(core, &core_listener, &core_events, nullptr); } } pw_thread_loop_unlock(loop); } void cleanup_stream() { if (loop && stream_data.stream) { pw_thread_loop_lock(loop); // 1. Lock the frame mutex to stop fill_img { std::scoped_lock lock(stream_data.frame_mutex); stream_data.frame_ready = false; stream_data.current_buffer = nullptr; } if (stream_data.stream) { pw_stream_destroy(stream_data.stream); stream_data.stream = nullptr; } pw_thread_loop_unlock(loop); } session_cache_t::instance().invalidate(); } void ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) { pw_thread_loop_lock(loop); if (!stream_data.stream) { struct pw_properties *props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr); stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props); pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data); std::array buffer; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); int n_params = 0; std::array params; // Add preferred parameters for DMA-BUF with modifiers // Use DMA-BUF for VAAPI, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system). // On hybrid GPU systems (Intel+NVIDIA), DMA-BUFs come from the Intel GPU and cannot // be imported into CUDA, so we fall back to memory buffers in that case. bool use_dmabuf = n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi || (mem_type == platf::mem_type_e::cuda && display_is_nvidia)); if (use_dmabuf) { for (int i = 0; i < n_dmabuf_infos; i++) { auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers); params[n_params] = format_param; n_params++; } } // Add fallback for memptr for (const auto &fmt : format_map) { if (fmt.fourcc == 0) { break; } auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0); params[n_params] = format_param; n_params++; } pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params); } pw_thread_loop_unlock(loop); } static void close_img_fds(egl::img_descriptor_t *img_descriptor) { for (int &fd : img_descriptor->sd.fds) { if (fd >= 0) { close(fd); fd = -1; } } } 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)) ); if (h) { img_descriptor->seq = h->seq; img_descriptor->pts = h->pts; } if (buf->n_datas > 0) { img_descriptor->pw_flags = buf->datas[0].chunk->flags; } struct spa_meta_region *damage = static_cast( spa_buffer_find_meta_data(buf, SPA_META_VideoDamage, sizeof(*damage)) ); img_descriptor->pw_damage = (damage && damage->region.size.width > 0 && damage->region.size.height > 0) ? std::optional(true) : std::nullopt; } static void fill_img_dmabuf(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf, const stream_data_t &d) { img_descriptor->sd.width = d.format.info.raw.size.width; img_descriptor->sd.height = d.format.info.raw.size.height; img_descriptor->sd.modifier = d.format.info.raw.modifier; img_descriptor->sd.fourcc = d.drm_format; for (int i = 0; i < MIN(buf->n_datas, 4); i++) { img_descriptor->sd.fds[i] = dup(buf->datas[i].fd); img_descriptor->sd.pitches[i] = buf->datas[i].chunk->stride; img_descriptor->sd.offsets[i] = buf->datas[i].chunk->offset; } } void fill_img(platf::img_t *img) { pw_thread_loop_lock(loop); std::scoped_lock lock(stream_data.frame_mutex); if (stream_data.shared && stream_data.shared->stream_dead.load()) { img->data = nullptr; close_img_fds(static_cast(img)); pw_thread_loop_unlock(loop); return; } if (!stream_data.current_buffer) { img->data = nullptr; pw_thread_loop_unlock(loop); return; } struct spa_buffer *buf = stream_data.current_buffer->buffer; if (buf->datas[0].chunk->size != 0) { auto *img_descriptor = static_cast(img); fill_img_metadata(img_descriptor, buf); if (buf->datas[0].type == SPA_DATA_DmaBuf) { fill_img_dmabuf(img_descriptor, buf, stream_data); } else { img->data = stream_data.front_buffer->data(); img->row_pitch = stream_data.local_stride; } } pw_thread_loop_unlock(loop); } private: struct pw_thread_loop *loop; struct pw_context *context; struct pw_core *core; struct spa_hook core_listener; struct stream_data_t stream_data; int fd; int node; static struct spa_pod *build_format_parameter(struct spa_pod_builder *b, uint32_t width, uint32_t height, uint32_t refresh_rate, int32_t format, uint64_t *modifiers, int n_modifiers) { struct spa_pod_frame object_frame; struct spa_pod_frame modifier_frame; std::array sizes; std::array framerates; sizes[0] = SPA_RECTANGLE(width, height); // Preferred sizes[1] = SPA_RECTANGLE(1, 1); sizes[2] = SPA_RECTANGLE(8192, 4096); framerates[0] = SPA_FRACTION(0, 1); // default; we only want variable rate, thus bypassing compositor pacing framerates[1] = SPA_FRACTION(0, 1); // min framerates[2] = SPA_FRACTION(0, 1); // max spa_pod_builder_push_object(b, &object_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&sizes[0], &sizes[1], &sizes[2]), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerates[0]), 0); if (!session_cache_t::instance().is_maxframerate_failed()) { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&framerates[0], &framerates[1], &framerates[2]), 0); } if (n_modifiers) { spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); spa_pod_builder_push_choice(b, &modifier_frame, SPA_CHOICE_Enum, 0); // Preferred value, we pick the first modifier be the preferred one spa_pod_builder_long(b, modifiers[0]); for (uint32_t i = 0; i < n_modifiers; i++) { spa_pod_builder_long(b, modifiers[i]); } spa_pod_builder_pop(b, &modifier_frame); } return static_cast(spa_pod_builder_pop(b, &object_frame)); } static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) { BOOST_LOG(info) << "Connected to pipewire version "sv << pw_info->version; } static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) { BOOST_LOG(info) << "Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message; } constexpr static const struct pw_core_events core_events = { .version = PW_VERSION_CORE_EVENTS, .info = on_core_info_cb, .error = on_core_error_cb, }; static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) { BOOST_LOG(debug) << "PipeWire stream state: " << pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state); auto *d = static_cast(user_data); switch (state) { case PW_STREAM_STATE_PAUSED: if (d->shared && old == PW_STREAM_STATE_STREAMING) { { std::scoped_lock lock(d->frame_mutex); d->frame_ready = false; d->current_buffer = nullptr; d->shared->stream_dead.store(true, std::memory_order_relaxed); } d->frame_cv.notify_all(); } break; case PW_STREAM_STATE_ERROR: if (old != PW_STREAM_STATE_STREAMING && !session_cache_t::instance().is_maxframerate_failed()) { BOOST_LOG(warning) << "Negotiation failed, will retry without maxFramerate"sv; session_cache_t::instance().set_maxframerate_failed(); } [[fallthrough]]; case PW_STREAM_STATE_UNCONNECTED: if (d->shared) { d->shared->stream_dead.store(true, std::memory_order_relaxed); d->frame_cv.notify_all(); } break; default: break; } } static void on_process(void *user_data) { const auto d = static_cast(user_data); struct pw_buffer *b = nullptr; // 1. Drain the queue: Always grab the most recent buffer while (struct pw_buffer *aux = pw_stream_dequeue_buffer(d->stream)) { if (b) { pw_stream_queue_buffer(d->stream, b); // Return the older, unused buffer } b = aux; } if (!b) { return; } // 2. Fast Path: DMA-BUF if (b->buffer->datas[0].type == SPA_DATA_DmaBuf) { std::scoped_lock lock(d->frame_mutex); if (d->current_buffer) { pw_stream_queue_buffer(d->stream, d->current_buffer); } d->current_buffer = b; d->frame_ready = true; } // 3. Optimized Path: Software/MemPtr else if (b->buffer->datas[0].data != nullptr) { size_t size = b->buffer->datas[0].chunk->size; // Perform the copy to the BACK buffer while NOT holding the lock if (d->back_buffer->size() < size) { d->back_buffer->resize(size); } std::memcpy(d->back_buffer->data(), b->buffer->datas[0].data, size); { // Lock only for the pointer swap and state update std::scoped_lock lock(d->frame_mutex); std::swap(d->front_buffer, d->back_buffer); d->local_stride = b->buffer->datas[0].chunk->stride; d->frame_ready = true; d->current_buffer = b; } // Release the PW buffer immediately after copy pw_stream_queue_buffer(d->stream, b); } d->frame_cv.notify_one(); } static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { const auto d = static_cast(user_data); d->current_buffer = nullptr; if (param == nullptr || id != SPA_PARAM_Format) { return; } if (spa_format_parse(param, &d->format.media_type, &d->format.media_subtype) < 0) { return; } if (d->format.media_type != SPA_MEDIA_TYPE_video || d->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) { return; } if (spa_format_video_raw_parse(param, &d->format.info.raw) < 0) { return; } BOOST_LOG(info) << "Video format: "sv << d->format.info.raw.format; BOOST_LOG(info) << "Size: "sv << d->format.info.raw.size.width << "x"sv << d->format.info.raw.size.height; if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) { BOOST_LOG(info) << "Framerate (from compositor): 0/1 (variable rate capture)"; } else { BOOST_LOG(info) << "Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom; BOOST_LOG(info) << "Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom; } int physical_w = d->format.info.raw.size.width; int physical_h = d->format.info.raw.size.height; if (d->shared) { int old_w = d->shared->negotiated_width.load(std::memory_order_relaxed); int old_h = d->shared->negotiated_height.load(std::memory_order_relaxed); if (physical_w != old_w || physical_h != old_h) { d->shared->negotiated_width.store(physical_w, std::memory_order_relaxed); d->shared->negotiated_height.store(physical_h, std::memory_order_relaxed); } } uint64_t drm_format = 0; for (const auto &fmt : format_map) { if (fmt.fourcc == 0) { break; } if (fmt.pw_format == d->format.info.raw.format) { drm_format = fmt.fourcc; } } d->drm_format = drm_format; uint32_t buffer_types = 0; if (spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier) != nullptr && d->drm_format) { BOOST_LOG(info) << "using DMA-BUF buffers"sv; buffer_types |= 1 << SPA_DATA_DmaBuf; } else { BOOST_LOG(info) << "using memory buffers"sv; buffer_types |= 1 << SPA_DATA_MemPtr; } // Ack the buffer type and metadata std::array buffer; std::array params; int n_params = 0; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); auto buffer_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types))); params[n_params] = buffer_param; n_params++; auto meta_param = static_cast(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)))); params[n_params] = meta_param; n_params++; int videoDamageRegionCount = 16; auto damage_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * videoDamageRegionCount, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * videoDamageRegionCount))); params[n_params] = damage_param; n_params++; pw_stream_update_params(d->stream, params.data(), n_params); } constexpr static const struct pw_stream_events stream_events = { .version = PW_VERSION_STREAM_EVENTS, .state_changed = on_stream_state_changed, .param_changed = on_param_changed, .process = on_process, }; }; class portal_t: public platf::display_t { public: int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { // calculate frame interval we should capture at framerate = config.framerate; if (config.framerateX100 > 0) { AVRational fps_strict = ::video::framerateX100_to_rational(config.framerateX100); delay = std::chrono::nanoseconds( (static_cast(fps_strict.den) * 1'000'000'000LL) / fps_strict.num ); BOOST_LOG(info) << "Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]"; } else { delay = std::chrono::nanoseconds {1s} / framerate; BOOST_LOG(info) << "Requested frame rate [" << framerate << "fps]"; } mem_type = hwdevice_type; if (get_dmabuf_modifiers() < 0) { return -1; } // Use cached portal session to avoid creating multiple screen recordings int pipewire_fd = -1; int pipewire_node = 0; if (session_cache_t::instance().get_or_create_session(pipewire_fd, pipewire_node, width, height) < 0) { return -1; } framerate = config.framerate; if (!shared_state) { shared_state = std::make_shared(); } else { shared_state->stream_dead.store(false); shared_state->negotiated_width.store(0); shared_state->negotiated_height.store(0); } pipewire.init(pipewire_fd, pipewire_node, shared_state); // Start PipeWire now so format negotiation can proceed before capture start pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia); int timeout_ms = 1500; int negotiated_w = 0; int negotiated_h = 0; while (timeout_ms > 0) { negotiated_w = shared_state->negotiated_width.load(); negotiated_h = shared_state->negotiated_height.load(); if (negotiated_w > 0 && negotiated_h > 0) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); timeout_ms -= 10; } // Check previous logical dimensions if (previous_width.load() == width && previous_height.load() == height) { if (capture_running.load()) { { std::scoped_lock lock(pipewire.frame_mutex()); stream_stopped.store(true); } pipewire.frame_cv().notify_all(); } } else { previous_width.store(width); previous_height.store(height); } if (negotiated_w > 0 && negotiated_h > 0 && (negotiated_w != width || negotiated_h != height)) { BOOST_LOG(info) << "Using negotiated resolution "sv << negotiated_w << "x" << negotiated_h; width = negotiated_w; height = negotiated_h; } // Set env dimensions to match the captured display. // Portal captures a single display, so the environment size equals the capture size. // Without this, touch input is silently dropped because touch_port_t::operator bool() // checks env_width and env_height are non-zero. env_width = width; env_height = height; return 0; } platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool show_cursor) { // FIXME: show_cursor is ignored auto deadline = std::chrono::steady_clock::now() + timeout; int retries = 0; while (std::chrono::steady_clock::now() < deadline) { if (!wait_for_frame(deadline)) { return stream_stopped.load() ? platf::capture_e::interrupted : platf::capture_e::timeout; } if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } auto *img_egl = static_cast(img_out.get()); img_egl->reset(); pipewire.fill_img(img_egl); // Check if we got valid data (either DMA-BUF fd or memory pointer), then filter duplicates if ((img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr) && !is_buffer_redundant(img_egl)) { // Update frame metadata update_metadata(img_egl, retries); return platf::capture_e::ok; } // No valid frame yet, or it was a duplicate retries++; } return platf::capture_e::timeout; } std::shared_ptr alloc_img() override { // Note: this img_t type is also used for memory buffers auto img = std::make_shared(); img->width = width; img->height = height; img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * width; img->sequence = 0; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; std::fill_n(img->sd.fds, 4, -1); return img; } platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia); sleep_overshoot_logger.reset(); capture_running.store(true); while (true) { // Check if PipeWire signaled a state change or error if (stream_stopped.load() || shared_state->stream_dead.exchange(false)) { // If stream is marked as stopped, clear state and send interrupted status if (stream_stopped.load()) { BOOST_LOG(warning) << "PipeWire stream stopped by user."sv; capture_running.store(false); stream_stopped.store(false); previous_height.store(0); previous_width.store(0); pipewire.frame_cv().notify_all(); return platf::capture_e::error; } else { BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv; 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(); } std::shared_ptr img_out; switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: capture_running.store(false); stream_stopped.store(false); previous_height.store(0); previous_width.store(0); pipewire.frame_cv().notify_all(); return status; case platf::capture_e::timeout: if (!pull_free_image_cb(img_out)) { // Detect if shutdown is pending BOOST_LOG(debug) << "PipeWire: timeout -> interrupt nudge"; capture_running.store(false); stream_stopped.store(false); previous_height.store(0); previous_width.store(0); pipewire.frame_cv().notify_all(); return platf::capture_e::interrupted; } push_captured_image_cb(std::move(img_out), false); break; case platf::capture_e::ok: push_captured_image_cb(std::move(img_out), true); break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } return platf::capture_e::ok; } std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, n_dmabuf_infos > 0); } #endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == platf::mem_type_e::cuda) { if (display_is_nvidia && n_dmabuf_infos > 0) { // Display GPU is NVIDIA - can use DMA-BUF directly return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); } else { // Hybrid system (Intel display + NVIDIA encode) - use memory buffer path // DMA-BUFs from Intel GPU cannot be imported into CUDA return cuda::make_avcodec_encode_device(width, height, false); } } #endif return std::make_unique(); } int dummy_img(platf::img_t *img) override { if (!img) { return -1; } img->data = new std::uint8_t[img->height * img->row_pitch]; std::fill_n(img->data, img->height * img->row_pitch, 0); return 0; } // This capture method is event driven; don't insert duplicate frames bool is_event_driven() override { return true; } private: bool is_buffer_redundant(const egl::img_descriptor_t *img) { // Check for corrupted frame if (img->pw_flags.has_value() && (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED)) { return true; } // If PTS is identical, only drop if damage metadata confirms no change if (img->pts.has_value() && last_pts.has_value() && img->pts.value() == last_pts.value()) { return img->pw_damage.has_value() && !img->pw_damage.value(); } return false; } void update_metadata(egl::img_descriptor_t *img, int retries) { last_seq = img->seq; last_pts = img->pts; img->sequence = ++sequence; if (retries > 0) { BOOST_LOG(debug) << "Processed frame after " << retries << " redundant events."sv; } } bool wait_for_frame(std::chrono::steady_clock::time_point deadline) { std::unique_lock lock(pipewire.frame_mutex()); bool success = pipewire.frame_cv().wait_until(lock, deadline, [&] { return pipewire.is_frame_ready() || stream_stopped.load() || shared_state->stream_dead.load(); }); if (success && !stream_stopped.load()) { pipewire.set_frame_ready(false); return true; } return false; } static uint32_t lookup_pw_format(uint64_t fourcc) { for (const auto &fmt : format_map) { if (fmt.fourcc == 0) { break; } if (fmt.fourcc == fourcc) { return fmt.pw_format; } } return 0; } void query_dmabuf_formats(EGLDisplay egl_display) { EGLint num_dmabuf_formats = 0; std::array dmabuf_formats = {0}; eglQueryDmaBufFormatsEXT(egl_display, MAX_DMABUF_FORMATS, dmabuf_formats.data(), &num_dmabuf_formats); if (num_dmabuf_formats > MAX_DMABUF_FORMATS) { BOOST_LOG(warning) << "Some DMA-BUF formats are being ignored"sv; } for (EGLint i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) { uint32_t pw_format = lookup_pw_format(dmabuf_formats[i]); if (pw_format == 0) { continue; } EGLint num_modifiers = 0; std::array mods = {0}; eglQueryDmaBufModifiersEXT(egl_display, dmabuf_formats[i], MAX_DMABUF_MODIFIERS, mods.data(), nullptr, &num_modifiers); if (num_modifiers > MAX_DMABUF_MODIFIERS) { BOOST_LOG(warning) << "Some DMA-BUF modifiers are being ignored"sv; } dmabuf_infos[n_dmabuf_infos].format = pw_format; dmabuf_infos[n_dmabuf_infos].n_modifiers = MIN(num_modifiers, MAX_DMABUF_MODIFIERS); dmabuf_infos[n_dmabuf_infos].modifiers = static_cast(g_memdup2(mods.data(), sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers)); ++n_dmabuf_infos; } } int get_dmabuf_modifiers() { if (wl_display.init() < 0) { return -1; } auto egl_display = egl::make_display(wl_display.get()); if (!egl_display) { return -1; } // Detect if this is a pure NVIDIA system (not hybrid Intel+NVIDIA) // On hybrid systems, the wayland compositor typically runs on Intel, // so DMA-BUFs from portal will come from Intel and cannot be imported into CUDA. // Check if Intel GPU exists - if so, assume hybrid system and disable CUDA DMA-BUF. bool has_intel_gpu = std::ifstream("/sys/class/drm/card0/device/vendor").good() || std::ifstream("/sys/class/drm/card1/device/vendor").good(); if (has_intel_gpu) { // Read vendor IDs to check for Intel (0x8086) auto check_intel = [](const std::string &path) { if (std::ifstream f(path); f.good()) { std::string vendor; f >> vendor; return vendor == "0x8086"; } return false; }; bool intel_present = check_intel("/sys/class/drm/card0/device/vendor") || check_intel("/sys/class/drm/card1/device/vendor"); if (intel_present) { BOOST_LOG(info) << "Hybrid GPU system detected (Intel + discrete) - CUDA will use memory buffers"sv; display_is_nvidia = false; } else { // No Intel GPU found, check if NVIDIA is present const char *vendor = eglQueryString(egl_display.get(), EGL_VENDOR); if (vendor && std::string_view(vendor).contains("NVIDIA")) { BOOST_LOG(info) << "Pure NVIDIA system - DMA-BUF will be enabled for CUDA"sv; display_is_nvidia = true; } } } if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) { query_dmabuf_formats(egl_display.get()); } return 0; } platf::mem_type_e mem_type; wl::display_t wl_display; pipewire_t pipewire; std::array dmabuf_infos; int n_dmabuf_infos; bool display_is_nvidia = false; // Track if display GPU is NVIDIA std::chrono::nanoseconds delay; std::optional last_pts {}; std::optional last_seq {}; std::uint64_t sequence {}; uint32_t framerate; static inline std::atomic previous_height {0}; static inline std::atomic previous_width {0}; static inline std::atomic stream_stopped {false}; static inline std::atomic capture_running {false}; std::shared_ptr shared_state; }; } // namespace portal namespace platf { std::shared_ptr portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { using enum platf::mem_type_e; if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; } auto portal = std::make_shared(); if (portal->init(hwdevice_type, display_name, config)) { return nullptr; } return portal; } std::vector portal_display_names() { std::vector display_names; auto dbus = std::make_shared(); if (dbus->init() < 0) { return {}; } pw_init(nullptr, nullptr); display_names.emplace_back("org.freedesktop.portal.Desktop"); return display_names; } } // namespace platf