From 0b368b4c234cc1cca672ed504aad341cc9f6418e Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Sat, 4 May 2024 11:33:38 +0200 Subject: [PATCH 01/24] third-party: Add stubs for EGL DMA-BUF query APIs This is not present in the glad version imported in Sunshine, plug the holes so that these functions may be used. --- third-party/glad/include/glad/egl.h | 8 ++++++++ third-party/glad/src/egl.c | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/third-party/glad/include/glad/egl.h b/third-party/glad/include/glad/egl.h index 37a7f010693..df11f82a135 100644 --- a/third-party/glad/include/glad/egl.h +++ b/third-party/glad/include/glad/egl.h @@ -467,6 +467,14 @@ typedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITSYNCPROC)(EGLDisplay dpy, EGLSync syn typedef EGLImageKHR(EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list); typedef EGLBoolean(EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay dpy, EGLImageKHR image); +typedef EGLenum(EGLAPIENTRYP PFNEGLQUERYDMABUFFORMATSEXTPROC)(EGLDisplay dpy, EGLint max_formats, EGLint *formats, EGLint *num_formats); +typedef EGLenum(EGLAPIENTRYP PFNEGLQUERYDMABUFMODIFIERSEXTPROC)(EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR *modifiers, EGLBoolean *external_only, EGLint *num_modifiers); + +GLAD_API_CALL PFNEGLQUERYDMABUFFORMATSEXTPROC glad_eglQueryDmaBufFormatsEXT; +#define eglQueryDmaBufFormatsEXT glad_eglQueryDmaBufFormatsEXT +GLAD_API_CALL PFNEGLQUERYDMABUFMODIFIERSEXTPROC glad_eglQueryDmaBufModifiersEXT; +#define eglQueryDmaBufModifiersEXT glad_eglQueryDmaBufModifiersEXT + GLAD_API_CALL PFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR; #define eglCreateImageKHR glad_eglCreateImageKHR GLAD_API_CALL PFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR; diff --git a/third-party/glad/src/egl.c b/third-party/glad/src/egl.c index fa4ffba9935..34d0303a537 100644 --- a/third-party/glad/src/egl.c +++ b/third-party/glad/src/egl.c @@ -75,6 +75,8 @@ PFNEGLWAITNATIVEPROC glad_eglWaitNative = NULL; PFNEGLWAITSYNCPROC glad_eglWaitSync = NULL; PFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR = NULL; PFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR = NULL; +PFNEGLQUERYDMABUFFORMATSEXTPROC glad_eglQueryDmaBufFormatsEXT = NULL; +PFNEGLQUERYDMABUFMODIFIERSEXTPROC glad_eglQueryDmaBufModifiersEXT = NULL; static void glad_egl_load_EGL_VERSION_1_0( GLADuserptrloadfunc load, void* userptr) { @@ -138,7 +140,10 @@ static void glad_egl_load_EGL_VERSION_1_5( GLADuserptrloadfunc load, void* userp glad_eglGetSyncAttrib = (PFNEGLGETSYNCATTRIBPROC) load(userptr, "eglGetSyncAttrib"); glad_eglWaitSync = (PFNEGLWAITSYNCPROC) load(userptr, "eglWaitSync"); } - +static void glad_egl_load_EGL_EXT_image_dma_buf_import_modifiers( GLADuserptrloadfunc load, void* userptr) { + glad_eglQueryDmaBufFormatsEXT = (PFNEGLQUERYDMABUFFORMATSEXTPROC) load(userptr, "eglQueryDmaBufFormatsEXT"); + glad_eglQueryDmaBufModifiersEXT = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC) load(userptr, "eglQueryDmaBufModifiersEXT"); +} static int glad_egl_get_extensions(EGLDisplay display, const char **extensions) { @@ -236,6 +241,7 @@ int gladLoadEGLUserPtr(EGLDisplay display, GLADuserptrloadfunc load, void* userp glad_egl_load_EGL_VERSION_1_2(load, userptr); glad_egl_load_EGL_VERSION_1_4(load, userptr); glad_egl_load_EGL_VERSION_1_5(load, userptr); + glad_egl_load_EGL_EXT_image_dma_buf_import_modifiers(load, userptr); if (!glad_egl_find_extensions_egl(display)) return 0; From 655e86defd25a0834b66da76506b612ead36160b Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Sun, 5 May 2024 16:16:23 -0400 Subject: [PATCH 02/24] feat(linux): Support streaming through XDG portals and Pipewire Add a new portal "grab" implementation that will request the necessary permissions over XDG portals and use Pipewire for video data streaming. This supports DMA-Buf buffer exchange for hardware accelerated encoding (VAAPI, nvenc), software encoding will only work with memory buffers. --- cmake/compile_definitions/linux.cmake | 19 +- cmake/prep/options.cmake | 2 + src/platform/linux/misc.cpp | 33 ++ src/platform/linux/portalgrab.cpp | 777 ++++++++++++++++++++++++++ 4 files changed, 830 insertions(+), 1 deletion(-) create mode 100644 src/platform/linux/portalgrab.cpp diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 6f98c355536..923cdbed352 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -168,12 +168,29 @@ if(X11_FOUND) "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.cpp") endif() +# XDG portal +if (${SUNSHINE_ENABLE_PORTAL}) + pkg_check_modules(GIO gio-2.0 gio-unix-2.0) + pkg_check_modules(PIPEWIRE libpipewire-0.3) +else() + set(GIO_FOUND OFF) + set(PIPEWIRE_FOUND OFF) +endif() +if(PIPEWIRE_FOUND) + add_compile_definitions(SUNSHINE_BUILD_PORTAL) + include_directories(${GIO_INCLUDE_DIRS} ${PIPEWIRE_INCLUDE_DIRS}) + list(APPEND PLATFORM_LIBRARIES ${GIO_LIBRARIES} ${PIPEWIRE_LIBRARIES}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/portalgrab.cpp") +endif() + if(NOT ${CUDA_FOUND} AND NOT ${WAYLAND_FOUND} AND NOT ${X11_FOUND} + AND NOT ${PIPEWIRE_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${LIBVA_FOUND}) - message(FATAL_ERROR "Couldn't find either cuda, wayland, x11, (libdrm and libcap), or libva") + message(FATAL_ERROR "Couldn't find either cuda, wayland, x11, pipewire, (libdrm and libcap), or libva") endif() # tray icon diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index b1b916ac8e2..6b732a957e6 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -64,4 +64,6 @@ elseif(UNIX) # Linux "Enable building wayland specific code." ON) option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available." ON) + option(SUNSHINE_ENABLE_PORTAL + "Enable XDG portal grab if available" ON) endif() diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 7d3810e5bd8..e4edcdbf289 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -888,6 +888,9 @@ namespace platf { #endif #ifdef SUNSHINE_BUILD_X11 X11, ///< X11 +#endif +#ifdef SUNSHINE_BUILD_PORTAL + PORTAL, ///< XDG PORTAL #endif MAX_FLAGS ///< The maximum number of flags }; @@ -931,6 +934,18 @@ namespace platf { } #endif +#ifdef SUNSHINE_BUILD_PORTAL + std::vector + portal_display_names(); + std::shared_ptr + portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + + bool + verify_portal() { + return !portal_display_names().empty(); + } +#endif + std::vector display_names(mem_type_e hwdevice_type) { #ifdef SUNSHINE_BUILD_CUDA // display using NvFBC only supports mem_type_e::cuda @@ -952,6 +967,11 @@ namespace platf { if (sources[source::X11]) { return x11_display_names(); } +#endif +#ifdef SUNSHINE_BUILD_PORTAL + if (sources[source::PORTAL]) { + return portal_display_names(); + } #endif return {}; } @@ -966,6 +986,12 @@ namespace platf { } std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { +#ifdef SUNSHINE_BUILD_PORTAL + if (sources[source::PORTAL]) { + BOOST_LOG(info) << "Screencasting with XDG portal"sv; + return portal_display(hwdevice_type, display_name, config); + } +#endif #ifdef SUNSHINE_BUILD_CUDA if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { BOOST_LOG(info) << "Screencasting with NvFBC"sv; @@ -1048,6 +1074,13 @@ namespace platf { } } #endif +#ifdef SUNSHINE_BUILD_PORTAL + if (config::video.capture.empty() || config::video.capture == "portal") { + if (verify_portal()) { + sources[source::PORTAL] = true; + } + } +#endif if (sources.none()) { BOOST_LOG(error) << "Unable to initialize capture method"sv; diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp new file mode 100644 index 00000000000..d7e7f8dde1d --- /dev/null +++ b/src/platform/linux/portalgrab.cpp @@ -0,0 +1,777 @@ +/** + * @file src/platform/linux/portalgrab.cpp + * @brief todo + */ +#include + +#include "src/platform/common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "src/main.h" +#include "src/video.h" + +#include "graphics.h" +#include "cuda.h" +#include "vaapi.h" +#include "wayland.h" + +#define SPA_POD_BUFFER_SIZE 4096 +#define MAX_PARAMS 200 +#define MAX_DMABUF_FORMATS 200 +#define MAX_DMABUF_MODIFIERS 200 + +#define SOURCE_TYPE_MONITOR 1 +#define CURSOR_MODE_EMBEDDED 2 + +#define PERSIST_FORGET 0 +#define PERSIST_WHILE_RUNNING 1 + +#define PORTAL_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define REMOTE_DESKTOP_IFACE "org.freedesktop.portal.RemoteDesktop" +#define SCREENCAST_IFACE "org.freedesktop.portal.ScreenCast" +#define REQUEST_IFACE "org.freedesktop.portal.Request" + +#define REQUEST_PREFIX "/org/freedesktop/portal/desktop/request/" +#define SESSION_PREFIX "/org/freedesktop/portal/desktop/session/" + +using namespace std::literals; + +namespace portal { + static char* restore_token; + + struct format_map_t { + uint64_t fourcc; + int32_t pw_format; + } 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 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; + }; + + struct dmabuf_format_info_t { + int32_t format; + uint64_t *modifiers; + int n_modifiers; + }; + + class dbus_t { + public: + ~dbus_t() { + g_object_unref (screencast_proxy); + g_object_unref (remote_desktop_proxy); + g_object_unref (conn); + } + + int + init() { + conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); + if (!conn) + return -1; + remote_desktop_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, PORTAL_NAME, PORTAL_PATH, REMOTE_DESKTOP_IFACE, NULL, NULL); + if (!remote_desktop_proxy) + return -1; + screencast_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, PORTAL_NAME, PORTAL_PATH, SCREENCAST_IFACE, NULL, NULL); + if (!screencast_proxy) + return -1; + + return 0; + } + + int + connect_to_portal() { + g_autoptr(GMainLoop) loop = g_main_loop_new(NULL, FALSE); + g_autofree gchar *session_path = NULL, *session_token = NULL; + create_session_path (conn, &session_path, &session_token); + + if (create_session(loop, session_path, session_token) < 0) + return -1; + if (select_remote_desktop_devices(loop, session_path) < 0) + return -1; + if (select_screencast_sources(loop, session_path) < 0) + return -1; + if (start_session(loop, session_path, pipewire_node, width, height) < 0) + return -1; + if (open_pipewire_remote(session_path, pipewire_fd) < 0) + return -1; + + return 0; + } + + int pipewire_fd; + int pipewire_node; + int width; + int height; + + private: + GDBusConnection *conn; + GDBusProxy *screencast_proxy; + GDBusProxy *remote_desktop_proxy; + + int + create_session(GMainLoop *loop, const gchar *session_path, const gchar *session_token) { + dbus_response_t response = {0,}; + g_autofree gchar *request_path = NULL, *request_token = NULL; + create_request_path (conn, &request_path, &request_token); + dbus_response_init (&response, loop, conn, request_path); + + 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 = NULL; + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "CreateSession", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + + if (err) { + BOOST_LOG(error) << "Could not create session: "sv << err->message; + return -1; + } + + g_autoptr(GVariant) ignore = dbus_response_wait(&response); + return 0; + } + + int + select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) { + dbus_response_t response = {0,}; + g_autofree gchar *request_path = NULL, *request_token = NULL; + create_request_path (conn, &request_path, &request_token); + dbus_response_init (&response, loop, conn, request_path); + + 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}", "persist_mode", g_variant_new_uint32 (PERSIST_WHILE_RUNNING)); + if (restore_token) + g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string (restore_token)); + g_variant_builder_close(&builder); + + g_autoptr(GError) err = NULL; + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + + if (err) { + BOOST_LOG(error) << "Could not select devices: "sv << err->message; + return -1; + } + + g_autoptr(GVariant) ignore = dbus_response_wait(&response); + return 0; + } + + int + select_screencast_sources(GMainLoop *loop, const gchar *session_path) { + dbus_response_t response = {0,}; + g_autofree gchar *request_path = NULL, *request_token = NULL; + create_request_path (conn, &request_path, &request_token); + dbus_response_init (&response, loop, conn, request_path); + + 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)); + g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32 (PERSIST_FORGET)); + g_variant_builder_close(&builder); + + g_autoptr(GError) err = NULL; + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + if (err) { + BOOST_LOG(error) << "Could not select sources: "sv << err->message; + return -1; + } + + g_autoptr(GVariant) ignore = dbus_response_wait(&response); + return 0; + } + + int + start_session(GMainLoop *loop, const gchar *session_path, int& pipewire_node, int& width, int& height) { + dbus_response_t response = {0,}; + g_autofree gchar *request_path = NULL, *request_token = NULL; + create_request_path (conn, &request_path, &request_token); + dbus_response_init (&response, loop, conn, request_path); + + 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", ""); + 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 = NULL; + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (remote_desktop_proxy, "Start", g_variant_builder_end (&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + if (err) { + BOOST_LOG(error) << "Could not start session: "sv << err->message; + return -1; + } + + g_autoptr(GVariant) start_response = dbus_response_wait(&response); + + g_autoptr(GVariant) dict = NULL, streams = NULL; + g_variant_get (start_response, "(u@a{sv})", NULL, &dict, NULL); + streams = g_variant_lookup_value (dict, "streams", G_VARIANT_TYPE("a(ua{sv})")); + // Preserve restore token for multiple runs (e.g. probing) + if (!restore_token) + g_variant_lookup (dict, "restore_token", "s", &restore_token, NULL); + + GVariantIter iter; + g_autoptr(GVariant) value = NULL; + g_variant_iter_init (&iter, streams); + while (g_variant_iter_next(&iter, "(u@a{sv})", &pipewire_node, &value)) + g_variant_lookup (value, "size", "(ii)", &width, &height, NULL); + + return 0; + } + + int + open_pipewire_remote(const gchar *session_path, int& fd) { + GUnixFDList *fd_list; + GVariant *msg = g_variant_new ("(oa{sv})", session_path, NULL); + + g_autoptr(GError) err = NULL; + g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync (screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &fd_list, NULL, &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, NULL); + return 0; + } + + static void + on_response_received_cb(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { + dbus_response_t *response = (dbus_response_t*) 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, ".")) != NULL) + *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_printf (REQUEST_PREFIX "%s/Sunshine%u", sender, request_count); + } + } + + 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_printf (SESSION_PREFIX "%s/Sunshine%u", sender, session_count); + } + } + + static void + dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) { + response->loop = loop; + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, on_response_received_cb, response, NULL); + } + + static GVariant* + dbus_response_wait(struct dbus_response_t *response) { + g_main_loop_run (response->loop); + return response->response; + } + }; + + class pipewire_t { + public: + pipewire_t() { + loop = pw_thread_loop_new("Pipewire thread", NULL); + pw_thread_loop_start (loop); + } + + ~pipewire_t() { + pw_thread_loop_stop (loop); + if (stream_data.stream) { + pw_stream_set_active (stream_data.stream, false); + pw_stream_disconnect (stream_data.stream); + pw_stream_destroy (stream_data.stream); + } + if (core) + pw_core_disconnect(core); + if (context) + pw_context_destroy (context); + if (fd >= 0) + close (fd); + pw_thread_loop_destroy (loop); + } + + void + init(int stream_fd, int stream_node) { + fd = stream_fd; + node = stream_node; + + context = pw_context_new (pw_thread_loop_get_loop(loop), NULL, 0); + core = pw_context_connect_fd(context, dup(fd), NULL, 0); + pw_core_add_listener(core, &core_listener, &core_events, NULL); + } + + void + ensure_stream(platf::mem_type_e mem_type, uint32_t width, uint32_t height, uint32_t refresh_rate, struct dmabuf_format_info_t *dmabuf_infos, int n_dmabuf_infos) { + pw_thread_loop_lock(loop); + if (!stream_data.stream) { + struct pw_properties *props; + + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL); + + 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); + + uint8_t buffer[SPA_POD_BUFFER_SIZE]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + int n_params = 0; + const struct spa_pod *params[MAX_PARAMS]; + + // Add preferred parameters for DMA-BUF with modifiers + if (n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi || mem_type == platf::mem_type_e::cuda)) { + for (int i = 0; i < n_dmabuf_infos; i++) { + params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers); + } + } + + // Add fallback for memptr + for (int i = 0; format_map[i].fourcc != 0; i++) + params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, format_map[i].pw_format, NULL, 0); + + pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags) (PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params, n_params); + } + pw_thread_loop_unlock(loop); + } + + void + fill_img(platf::img_t *img) { + pw_thread_loop_lock(loop); + + if (stream_data.current_buffer) { + struct spa_buffer *buf; + buf = stream_data.current_buffer->buffer; + if (buf->datas[0].chunk->size != 0) { + if (buf->datas[0].type == SPA_DATA_DmaBuf) { + auto img_descriptor = (egl::img_descriptor_t *) img; + img_descriptor->sd.width = stream_data.format.info.raw.size.width; + img_descriptor->sd.height = stream_data.format.info.raw.size.height; + img_descriptor->sd.modifier = stream_data.format.info.raw.modifier; + img_descriptor->sd.fourcc = stream_data.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; + } + } else { + img->data = (std::uint8_t *) buf->datas[0].data; + } + } + } + + 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, modifier_frame; + struct spa_rectangle sizes[3]; + struct spa_fraction framerates[3]; + + sizes[0] = SPA_RECTANGLE(width, height); // Preferred + sizes[1] = SPA_RECTANGLE(1, 1); + sizes[2] = SPA_RECTANGLE(8192, 4096); + + framerates[0] = SPA_FRACTION(refresh_rate, 1); // Preferred + framerates[1] = SPA_FRACTION(0, 1); + framerates[2] = SPA_FRACTION(1000, 1); + + 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_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 (struct spa_pod*) spa_pod_builder_pop(b, &object_frame); + } + + static void + on_core_info_cb(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(void *user_data, uint32_t id, int seq, 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_process(void *user_data) { + struct stream_data_t *d = (struct stream_data_t*) user_data; + struct pw_buffer *b = NULL; + + while (true) { + struct pw_buffer *aux = pw_stream_dequeue_buffer(d->stream); + if (!aux) + break; + if (b) + pw_stream_queue_buffer(d->stream, b); + b = aux; + } + + if (b == NULL) { + BOOST_LOG(warning) << "out of pipewire buffers"sv; + return; + } + + if (d->current_buffer) + pw_stream_queue_buffer(d->stream, d->current_buffer); + d->current_buffer = b; + } + + static void + on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { + struct stream_data_t *d = (struct stream_data_t*) user_data; + + d->current_buffer = NULL; + + if (param == NULL || 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; + BOOST_LOG(info) << "Framerate: "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom; + + uint64_t drm_format = 0; + for (int n = 0; format_map[n].fourcc != 0; n++) { + if (format_map[n].pw_format == d->format.info.raw.format) + drm_format = format_map[n].fourcc; + } + d->drm_format = drm_format; + + uint32_t buffer_types = 0; + if (spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier) != NULL && 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 + uint8_t buffer[SPA_POD_BUFFER_SIZE]; + const struct spa_pod *params[1]; + int n_params = 0; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + params[n_params++] = (const struct spa_pod *) spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)); + pw_stream_update_params(d->stream, params, n_params); + } + + constexpr static const struct pw_stream_events stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .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) { + framerate = config.framerate; + delay = std::chrono::nanoseconds { 1s } / framerate; + mem_type = hwdevice_type; + + if (get_dmabuf_modifiers() < 0) + return -1; + if (dbus.init() < 0) + return -1; + if (dbus.connect_to_portal() < 0) + return -1; + + width = dbus.width; + height = dbus.height; + framerate = config.framerate; + + pipewire.init(dbus.pipewire_fd, dbus.pipewire_node); + + 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 + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + + auto img_egl = (egl::img_descriptor_t *) img_out.get(); + img_egl->reset(); + pipewire.fill_img(img_egl); + img_egl->sequence = ++sequence; + + return platf::capture_e::ok; + } + + 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, (struct dmabuf_format_info_t*) dmabuf_infos, n_dmabuf_infos); + + while (true) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + case platf::capture_e::interrupted: + return status; + case platf::capture_e::timeout: + 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 << (int) 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 (n_dmabuf_infos > 0) + return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); + else + 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((std::uint8_t *) img->data, img->height * img->row_pitch, 0); + return 0; + } + + private: + 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; + + if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) { + EGLint num_dmabuf_formats = 0; + EGLint dmabuf_formats[MAX_DMABUF_FORMATS] = { 0, }; + eglQueryDmaBufFormatsEXT(egl_display.get(), MAX_DMABUF_FORMATS, (EGLint*)&dmabuf_formats, &num_dmabuf_formats); + if (num_dmabuf_formats > MAX_DMABUF_FORMATS) + BOOST_LOG(warning) << "Some DMA-BUF formats are being ignored"sv; + + EGLint i; + for (i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) { + uint32_t pw_format = 0; + for (int n = 0; format_map[n].fourcc != 0; n++) { + if (format_map[n].fourcc == dmabuf_formats[i]) + pw_format = format_map[n].pw_format; + } + + if (pw_format == 0) + continue; + + EGLint num_modifiers = 0; + EGLuint64KHR mods[MAX_DMABUF_MODIFIERS] = { 0, }; + EGLBoolean external_only; + eglQueryDmaBufModifiersEXT(egl_display.get(), dmabuf_formats[i], MAX_DMABUF_MODIFIERS, (EGLuint64KHR*)&mods, &external_only, &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 = + (uint64_t*) g_memdup2 (mods, sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers); + ++n_dmabuf_infos; + } + } + + return 0; + } + + platf::mem_type_e mem_type; + wl::display_t wl_display; + dbus_t dbus; + pipewire_t pipewire; + struct dmabuf_format_info_t dmabuf_infos[MAX_DMABUF_FORMATS]; + int n_dmabuf_infos; + std::chrono::nanoseconds delay; + std::uint64_t sequence {}; + uint32_t framerate; + }; +} // namespace portal + +namespace platf { + std::shared_ptr + portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::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(NULL, NULL); + + display_names.emplace_back("org.freedesktop.portal.Desktop"); + return display_names; + } + +} // namespace platf From 6f79ca59dc669467bc2cb2d7a55c71501ca6e56b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:11:13 -0500 Subject: [PATCH 03/24] style: fix lint errors --- src/platform/linux/misc.cpp | 11 +- src/platform/linux/portalgrab.cpp | 418 +++++++++++++++------------- third-party/glad/include/glad/egl.h | 2 +- third-party/glad/src/egl.c | 4 +- 4 files changed, 227 insertions(+), 208 deletions(-) diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index e4edcdbf289..fb8be514431 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -935,13 +935,10 @@ namespace platf { #endif #ifdef SUNSHINE_BUILD_PORTAL - std::vector - portal_display_names(); - std::shared_ptr - portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::vector portal_display_names(); + std::shared_ptr portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - bool - verify_portal() { + bool verify_portal() { return !portal_display_names().empty(); } #endif @@ -1077,7 +1074,7 @@ namespace platf { #ifdef SUNSHINE_BUILD_PORTAL if (config::video.capture.empty() || config::video.capture == "portal") { if (verify_portal()) { - sources[source::PORTAL] = true; + sources[source::PORTAL] = true; } } #endif diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index d7e7f8dde1d..f396d051e18 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -1,26 +1,27 @@ /** * @file src/platform/linux/portalgrab.cpp - * @brief todo + * @brief Definitions for XDG portal grab. */ +// standard includes +#include +#include #include -#include "src/platform/common.h" - +// lib includes #include #include +#include #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 "graphics.h" -#include "cuda.h" #include "vaapi.h" #include "wayland.h" @@ -47,15 +48,15 @@ using namespace std::literals; namespace portal { - static char* restore_token; + static char *restore_token; struct format_map_t { uint64_t fourcc; int32_t pw_format; } format_map[] = { - { DRM_FORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA }, - { DRM_FORMAT_XRGB8888, SPA_VIDEO_FORMAT_BGRx }, - { 0, 0 }, + {DRM_FORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA}, + {DRM_FORMAT_XRGB8888, SPA_VIDEO_FORMAT_BGRx}, + {0, 0}, }; struct dbus_response_t { @@ -81,42 +82,48 @@ namespace portal { class dbus_t { public: ~dbus_t() { - g_object_unref (screencast_proxy); - g_object_unref (remote_desktop_proxy); - g_object_unref (conn); + g_object_unref(screencast_proxy); + g_object_unref(remote_desktop_proxy); + g_object_unref(conn); } - int - init() { + int init() { conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); - if (!conn) + if (!conn) { return -1; + } remote_desktop_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, PORTAL_NAME, PORTAL_PATH, REMOTE_DESKTOP_IFACE, NULL, NULL); - if (!remote_desktop_proxy) + if (!remote_desktop_proxy) { return -1; + } screencast_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, PORTAL_NAME, PORTAL_PATH, SCREENCAST_IFACE, NULL, NULL); - if (!screencast_proxy) + if (!screencast_proxy) { return -1; + } return 0; } - int - connect_to_portal() { + int connect_to_portal() { g_autoptr(GMainLoop) loop = g_main_loop_new(NULL, FALSE); g_autofree gchar *session_path = NULL, *session_token = NULL; - create_session_path (conn, &session_path, &session_token); + create_session_path(conn, &session_path, &session_token); - if (create_session(loop, session_path, session_token) < 0) + if (create_session(loop, session_path, session_token) < 0) { return -1; - if (select_remote_desktop_devices(loop, session_path) < 0) + } + if (select_remote_desktop_devices(loop, session_path) < 0) { return -1; - if (select_screencast_sources(loop, session_path) < 0) + } + if (select_screencast_sources(loop, session_path) < 0) { return -1; - if (start_session(loop, session_path, pipewire_node, width, height) < 0) + } + if (start_session(loop, session_path, pipewire_node, width, height) < 0) { return -1; - if (open_pipewire_remote(session_path, pipewire_fd) < 0) + } + if (open_pipewire_remote(session_path, pipewire_fd) < 0) { return -1; + } return 0; } @@ -131,18 +138,19 @@ namespace portal { GDBusProxy *screencast_proxy; GDBusProxy *remote_desktop_proxy; - int - create_session(GMainLoop *loop, const gchar *session_path, const gchar *session_token) { - dbus_response_t response = {0,}; + int create_session(GMainLoop *loop, const gchar *session_path, const gchar *session_token) { + dbus_response_t response = { + 0, + }; g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path (conn, &request_path, &request_token); - dbus_response_init (&response, loop, conn, request_path); + create_request_path(conn, &request_path, &request_token); + dbus_response_init(&response, loop, conn, request_path); 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_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 = NULL; @@ -157,25 +165,27 @@ namespace portal { return 0; } - int - select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) { - dbus_response_t response = {0,}; + int select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) { + dbus_response_t response = { + 0, + }; g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path (conn, &request_path, &request_token); - dbus_response_init (&response, loop, conn, request_path); + create_request_path(conn, &request_path, &request_token); + dbus_response_init(&response, loop, conn, request_path); 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}", "persist_mode", g_variant_new_uint32 (PERSIST_WHILE_RUNNING)); - if (restore_token) - g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string (restore_token)); + g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); + g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_WHILE_RUNNING)); + if (restore_token) { + g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string(restore_token)); + } g_variant_builder_close(&builder); g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); if (err) { BOOST_LOG(error) << "Could not select devices: "sv << err->message; @@ -186,25 +196,26 @@ namespace portal { return 0; } - int - select_screencast_sources(GMainLoop *loop, const gchar *session_path) { - dbus_response_t response = {0,}; + int select_screencast_sources(GMainLoop *loop, const gchar *session_path) { + dbus_response_t response = { + 0, + }; g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path (conn, &request_path, &request_token); - dbus_response_init (&response, loop, conn, request_path); + create_request_path(conn, &request_path, &request_token); + dbus_response_init(&response, loop, conn, request_path); 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)); - g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32 (PERSIST_FORGET)); + 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)); + g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_FORGET)); g_variant_builder_close(&builder); g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); if (err) { BOOST_LOG(error) << "Could not select sources: "sv << err->message; return -1; @@ -214,23 +225,24 @@ namespace portal { return 0; } - int - start_session(GMainLoop *loop, const gchar *session_path, int& pipewire_node, int& width, int& height) { - dbus_response_t response = {0,}; + int start_session(GMainLoop *loop, const gchar *session_path, int &pipewire_node, int &width, int &height) { + dbus_response_t response = { + 0, + }; g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path (conn, &request_path, &request_token); - dbus_response_init (&response, loop, conn, request_path); + create_request_path(conn, &request_path, &request_token); + dbus_response_init(&response, loop, conn, request_path); 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", ""); 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}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_close(&builder); g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync (remote_desktop_proxy, "Start", g_variant_builder_end (&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "Start", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); if (err) { BOOST_LOG(error) << "Could not start session: "sv << err->message; return -1; @@ -239,93 +251,91 @@ namespace portal { g_autoptr(GVariant) start_response = dbus_response_wait(&response); g_autoptr(GVariant) dict = NULL, streams = NULL; - g_variant_get (start_response, "(u@a{sv})", NULL, &dict, NULL); - streams = g_variant_lookup_value (dict, "streams", G_VARIANT_TYPE("a(ua{sv})")); + g_variant_get(start_response, "(u@a{sv})", NULL, &dict, NULL); + streams = g_variant_lookup_value(dict, "streams", G_VARIANT_TYPE("a(ua{sv})")); // Preserve restore token for multiple runs (e.g. probing) - if (!restore_token) - g_variant_lookup (dict, "restore_token", "s", &restore_token, NULL); + if (!restore_token) { + g_variant_lookup(dict, "restore_token", "s", &restore_token, NULL); + } GVariantIter iter; g_autoptr(GVariant) value = NULL; - g_variant_iter_init (&iter, streams); - while (g_variant_iter_next(&iter, "(u@a{sv})", &pipewire_node, &value)) - g_variant_lookup (value, "size", "(ii)", &width, &height, NULL); + g_variant_iter_init(&iter, streams); + while (g_variant_iter_next(&iter, "(u@a{sv})", &pipewire_node, &value)) { + g_variant_lookup(value, "size", "(ii)", &width, &height, NULL); + } return 0; } - int - open_pipewire_remote(const gchar *session_path, int& fd) { + int open_pipewire_remote(const gchar *session_path, int &fd) { GUnixFDList *fd_list; - GVariant *msg = g_variant_new ("(oa{sv})", session_path, NULL); + GVariant *msg = g_variant_new("(oa{sv})", session_path, NULL); g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync (screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &fd_list, NULL, &err); + g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync(screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &fd_list, NULL, &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, NULL); + g_variant_get(reply, "(h)", &fd_handle); + fd = g_unix_fd_list_get(fd_list, fd_handle, NULL); return 0; } - static void - on_response_received_cb(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { - dbus_response_t *response = (dbus_response_t*) user_data; - response->response = g_variant_ref_sink (parameters); - g_main_loop_quit (response->loop); + static void on_response_received_cb(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { + dbus_response_t *response = (dbus_response_t *) user_data; + response->response = g_variant_ref_sink(parameters); + g_main_loop_quit(response->loop); } - static gchar * - get_sender_string(GDBusConnection *conn) { + 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, ".")) != NULL) + while ((dot = strstr(sender, ".")) != NULL) { *dot = '_'; + } return sender; } - static void - create_request_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) { + 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_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_printf (REQUEST_PREFIX "%s/Sunshine%u", sender, request_count); + *out_path = g_strdup_printf(REQUEST_PREFIX "%s/Sunshine%u", sender, request_count); } } - static void - create_session_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) { + 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_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_printf (SESSION_PREFIX "%s/Sunshine%u", sender, session_count); + *out_path = g_strdup_printf(SESSION_PREFIX "%s/Sunshine%u", sender, session_count); } } - static void - dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) { + static void dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) { response->loop = loop; g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, on_response_received_cb, response, NULL); } - static GVariant* - dbus_response_wait(struct dbus_response_t *response) { - g_main_loop_run (response->loop); + static GVariant *dbus_response_wait(struct dbus_response_t *response) { + g_main_loop_run(response->loop); return response->response; } }; @@ -334,37 +344,38 @@ namespace portal { public: pipewire_t() { loop = pw_thread_loop_new("Pipewire thread", NULL); - pw_thread_loop_start (loop); + pw_thread_loop_start(loop); } ~pipewire_t() { - pw_thread_loop_stop (loop); + pw_thread_loop_stop(loop); if (stream_data.stream) { - pw_stream_set_active (stream_data.stream, false); - pw_stream_disconnect (stream_data.stream); - pw_stream_destroy (stream_data.stream); + pw_stream_set_active(stream_data.stream, false); + pw_stream_disconnect(stream_data.stream); + pw_stream_destroy(stream_data.stream); } - if (core) + if (core) { pw_core_disconnect(core); - if (context) - pw_context_destroy (context); - if (fd >= 0) - close (fd); - pw_thread_loop_destroy (loop); + } + if (context) { + pw_context_destroy(context); + } + if (fd >= 0) { + close(fd); + } + pw_thread_loop_destroy(loop); } - void - init(int stream_fd, int stream_node) { + void init(int stream_fd, int stream_node) { fd = stream_fd; node = stream_node; - context = pw_context_new (pw_thread_loop_get_loop(loop), NULL, 0); + context = pw_context_new(pw_thread_loop_get_loop(loop), NULL, 0); core = pw_context_connect_fd(context, dup(fd), NULL, 0); pw_core_add_listener(core, &core_listener, &core_events, NULL); } - void - ensure_stream(platf::mem_type_e mem_type, uint32_t width, uint32_t height, uint32_t refresh_rate, struct dmabuf_format_info_t *dmabuf_infos, int n_dmabuf_infos) { + void ensure_stream(platf::mem_type_e mem_type, uint32_t width, uint32_t height, uint32_t refresh_rate, struct dmabuf_format_info_t *dmabuf_infos, int n_dmabuf_infos) { pw_thread_loop_lock(loop); if (!stream_data.stream) { struct pw_properties *props; @@ -372,7 +383,7 @@ namespace portal { props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL); 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); + pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data); uint8_t buffer[SPA_POD_BUFFER_SIZE]; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -388,16 +399,16 @@ namespace portal { } // Add fallback for memptr - for (int i = 0; format_map[i].fourcc != 0; i++) + for (int i = 0; format_map[i].fourcc != 0; i++) { params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, format_map[i].pw_format, NULL, 0); + } - pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags) (PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params, 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, n_params); } pw_thread_loop_unlock(loop); } - void - fill_img(platf::img_t *img) { + void fill_img(platf::img_t *img) { pw_thread_loop_lock(loop); if (stream_data.current_buffer) { @@ -411,7 +422,7 @@ namespace portal { img_descriptor->sd.modifier = stream_data.format.info.raw.modifier; img_descriptor->sd.fourcc = stream_data.drm_format; - for (int i = 0; i < MIN (buf->n_datas, 4); i++) { + 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; @@ -434,16 +445,16 @@ namespace portal { 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) { + 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, modifier_frame; struct spa_rectangle sizes[3]; struct spa_fraction framerates[3]; - sizes[0] = SPA_RECTANGLE(width, height); // Preferred + sizes[0] = SPA_RECTANGLE(width, height); // Preferred sizes[1] = SPA_RECTANGLE(1, 1); sizes[2] = SPA_RECTANGLE(8192, 4096); - framerates[0] = SPA_FRACTION(refresh_rate, 1); // Preferred + framerates[0] = SPA_FRACTION(refresh_rate, 1); // Preferred framerates[1] = SPA_FRACTION(0, 1); framerates[2] = SPA_FRACTION(1000, 1); @@ -460,22 +471,21 @@ namespace portal { // 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++) + for (uint32_t i = 0; i < n_modifiers; i++) { spa_pod_builder_long(b, modifiers[i]); + } spa_pod_builder_pop(b, &modifier_frame); } - return (struct spa_pod*) spa_pod_builder_pop(b, &object_frame); + return (struct spa_pod *) spa_pod_builder_pop(b, &object_frame); } - static void - on_core_info_cb(void *user_data, const struct pw_core_info *pw_info) { + static void on_core_info_cb(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(void *user_data, uint32_t id, int seq, int res, const char *message) { + static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, const char *message) { BOOST_LOG(info) << "Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message; } @@ -485,17 +495,18 @@ namespace portal { .error = on_core_error_cb, }; - static void - on_process(void *user_data) { - struct stream_data_t *d = (struct stream_data_t*) user_data; + static void on_process(void *user_data) { + struct stream_data_t *d = (struct stream_data_t *) user_data; struct pw_buffer *b = NULL; while (true) { struct pw_buffer *aux = pw_stream_dequeue_buffer(d->stream); - if (!aux) + if (!aux) { break; - if (b) + } + if (b) { pw_stream_queue_buffer(d->stream, b); + } b = aux; } @@ -504,25 +515,29 @@ namespace portal { return; } - if (d->current_buffer) + if (d->current_buffer) { pw_stream_queue_buffer(d->stream, d->current_buffer); + } d->current_buffer = b; } - static void - on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { - struct stream_data_t *d = (struct stream_data_t*) user_data; + static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { + struct stream_data_t *d = (struct stream_data_t *) user_data; d->current_buffer = NULL; - if (param == NULL || id != SPA_PARAM_Format) + if (param == NULL || id != SPA_PARAM_Format) { return; - if (spa_format_parse(param, &d->format.media_type, &d->format.media_subtype) < 0) + } + 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) + } + 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) + } + 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; @@ -530,8 +545,9 @@ namespace portal { uint64_t drm_format = 0; for (int n = 0; format_map[n].fourcc != 0; n++) { - if (format_map[n].pw_format == d->format.info.raw.format) + if (format_map[n].pw_format == d->format.info.raw.format) { drm_format = format_map[n].fourcc; + } } d->drm_format = drm_format; @@ -562,18 +578,20 @@ namespace portal { 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) { + int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { framerate = config.framerate; - delay = std::chrono::nanoseconds { 1s } / framerate; + delay = std::chrono::nanoseconds {1s} / framerate; mem_type = hwdevice_type; - if (get_dmabuf_modifiers() < 0) + if (get_dmabuf_modifiers() < 0) { return -1; - if (dbus.init() < 0) + } + if (dbus.init() < 0) { return -1; - if (dbus.connect_to_portal() < 0) + } + if (dbus.connect_to_portal() < 0) { return -1; + } width = dbus.width; height = dbus.height; @@ -584,8 +602,7 @@ namespace portal { 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) { + 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 if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; @@ -599,8 +616,7 @@ namespace portal { return platf::capture_e::ok; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { // Note: this img_t type is also used for memory buffers auto img = std::make_shared(); @@ -616,11 +632,10 @@ namespace portal { 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 { + 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, (struct dmabuf_format_info_t*) dmabuf_infos, n_dmabuf_infos); + pipewire.ensure_stream(mem_type, width, height, framerate, (struct dmabuf_format_info_t *) dmabuf_infos, n_dmabuf_infos); while (true) { auto now = std::chrono::steady_clock::now(); @@ -637,27 +652,26 @@ namespace portal { std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - case platf::capture_e::interrupted: - return status; - case platf::capture_e::timeout: - 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 << (int) status << ']'; - return status; + case platf::capture_e::reinit: + case platf::capture_e::error: + case platf::capture_e::interrupted: + return status; + case platf::capture_e::timeout: + 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 << (int) status << ']'; + return status; } } return platf::capture_e::ok; } - std::unique_ptr - make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { + 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); @@ -666,18 +680,18 @@ namespace portal { #ifdef SUNSHINE_BUILD_CUDA if (mem_type == platf::mem_type_e::cuda) { - if (n_dmabuf_infos > 0) + if (n_dmabuf_infos > 0) { return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); - else + } else { return cuda::make_avcodec_encode_device(width, height, false); + } } #endif return std::make_unique(); } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { if (!img) { return -1; }; @@ -688,44 +702,53 @@ namespace portal { } private: - int - get_dmabuf_modifiers() { - if (wl_display.init() < 0) + int get_dmabuf_modifiers() { + if (wl_display.init() < 0) { return -1; + } auto egl_display = egl::make_display(wl_display.get()); - if (!egl_display) + if (!egl_display) { return -1; + } if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) { EGLint num_dmabuf_formats = 0; - EGLint dmabuf_formats[MAX_DMABUF_FORMATS] = { 0, }; - eglQueryDmaBufFormatsEXT(egl_display.get(), MAX_DMABUF_FORMATS, (EGLint*)&dmabuf_formats, &num_dmabuf_formats); - if (num_dmabuf_formats > MAX_DMABUF_FORMATS) + EGLint dmabuf_formats[MAX_DMABUF_FORMATS] = { + 0, + }; + eglQueryDmaBufFormatsEXT(egl_display.get(), MAX_DMABUF_FORMATS, (EGLint *) &dmabuf_formats, &num_dmabuf_formats); + if (num_dmabuf_formats > MAX_DMABUF_FORMATS) { BOOST_LOG(warning) << "Some DMA-BUF formats are being ignored"sv; + } EGLint i; for (i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) { uint32_t pw_format = 0; for (int n = 0; format_map[n].fourcc != 0; n++) { - if (format_map[n].fourcc == dmabuf_formats[i]) + if (format_map[n].fourcc == dmabuf_formats[i]) { pw_format = format_map[n].pw_format; + } } - if (pw_format == 0) + if (pw_format == 0) { continue; + } EGLint num_modifiers = 0; - EGLuint64KHR mods[MAX_DMABUF_MODIFIERS] = { 0, }; + EGLuint64KHR mods[MAX_DMABUF_MODIFIERS] = { + 0, + }; EGLBoolean external_only; - eglQueryDmaBufModifiersEXT(egl_display.get(), dmabuf_formats[i], MAX_DMABUF_MODIFIERS, (EGLuint64KHR*)&mods, &external_only, &num_modifiers); - if (num_modifiers > MAX_DMABUF_MODIFIERS) + eglQueryDmaBufModifiersEXT(egl_display.get(), dmabuf_formats[i], MAX_DMABUF_MODIFIERS, (EGLuint64KHR *) &mods, &external_only, &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 = - (uint64_t*) g_memdup2 (mods, sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers); + (uint64_t *) g_memdup2(mods, sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers); ++n_dmabuf_infos; } } @@ -743,35 +766,34 @@ namespace portal { std::uint64_t sequence {}; uint32_t framerate; }; -} // namespace portal +} // namespace portal namespace platf { - std::shared_ptr - portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::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)) + if (portal->init(hwdevice_type, display_name, config)) { return nullptr; + } return portal; } - std::vector - portal_display_names() { + std::vector portal_display_names() { std::vector display_names; auto dbus = std::make_shared(); - if (dbus->init() < 0) + if (dbus->init() < 0) { return {}; + } pw_init(NULL, NULL); display_names.emplace_back("org.freedesktop.portal.Desktop"); return display_names; } - -} // namespace platf +} // namespace platf diff --git a/third-party/glad/include/glad/egl.h b/third-party/glad/include/glad/egl.h index df11f82a135..6d4e115948b 100644 --- a/third-party/glad/include/glad/egl.h +++ b/third-party/glad/include/glad/egl.h @@ -586,4 +586,4 @@ gladLoaderUnloadEGL(void); #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/third-party/glad/src/egl.c b/third-party/glad/src/egl.c index 34d0303a537..30433b5c4ed 100644 --- a/third-party/glad/src/egl.c +++ b/third-party/glad/src/egl.c @@ -252,7 +252,7 @@ int gladLoadEGL(EGLDisplay display, GLADloadfunc load) { return gladLoadEGLUserPtr(display, glad_egl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load); } - + #ifdef GLAD_EGL @@ -400,4 +400,4 @@ void gladLoaderUnloadEGL() { #ifdef __cplusplus } -#endif \ No newline at end of file +#endif From a31a931e8ef178a074ea1258eb84c971da36c57f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:57:25 -0500 Subject: [PATCH 04/24] Update XDG portal dependencies and error message Require GIO and PIPEWIRE packages for portal support and use SYSTEM include directories. Also, clarify the fatal error message order for missing dependencies. --- cmake/compile_definitions/linux.cmake | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 923cdbed352..3758884fdce 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -169,16 +169,16 @@ if(X11_FOUND) endif() # XDG portal -if (${SUNSHINE_ENABLE_PORTAL}) - pkg_check_modules(GIO gio-2.0 gio-unix-2.0) - pkg_check_modules(PIPEWIRE libpipewire-0.3) +if(${SUNSHINE_ENABLE_PORTAL}) + pkg_check_modules(GIO gio-2.0 gio-unix-2.0 REQUIRED) + pkg_check_modules(PIPEWIRE libpipewire-0.3 REQUIRED) else() set(GIO_FOUND OFF) set(PIPEWIRE_FOUND OFF) endif() if(PIPEWIRE_FOUND) add_compile_definitions(SUNSHINE_BUILD_PORTAL) - include_directories(${GIO_INCLUDE_DIRS} ${PIPEWIRE_INCLUDE_DIRS}) + include_directories(SYSTEM ${GIO_INCLUDE_DIRS} ${PIPEWIRE_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${GIO_LIBRARIES} ${PIPEWIRE_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/portalgrab.cpp") @@ -190,7 +190,7 @@ if(NOT ${CUDA_FOUND} AND NOT ${PIPEWIRE_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${LIBVA_FOUND}) - message(FATAL_ERROR "Couldn't find either cuda, wayland, x11, pipewire, (libdrm and libcap), or libva") + message(FATAL_ERROR "Couldn't find either cuda, libva, pipewire, wayland, x11, or (libdrm and libcap)") endif() # tray icon From 5ed7c50509f63773576a4cdfd5c4ddb8c7aed8a2 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:16:34 -0500 Subject: [PATCH 05/24] Add Pipewire as a dependency for Linux builds Pipewire has been added to the dependency lists for FreeBSD, Arch, Fedora, Homebrew, and Debian-based packaging and build scripts. This ensures proper support and integration for systems using Pipewire. --- .github/workflows/ci-freebsd.yml | 1 + cmake/packaging/linux.cmake | 3 ++- packaging/linux/Arch/PKGBUILD | 1 + packaging/linux/copr/Sunshine.spec | 1 + packaging/sunshine.rb | 1 + scripts/linux_build.sh | 2 ++ 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-freebsd.yml b/.github/workflows/ci-freebsd.yml index 4878e36030b..f58604b859e 100644 --- a/.github/workflows/ci-freebsd.yml +++ b/.github/workflows/ci-freebsd.yml @@ -114,6 +114,7 @@ jobs: graphics/wayland \ lang/python312 \ multimedia/libva \ + multimedia/pipewire \ net/miniupnpc \ ports-mgmt/pkg \ security/openssl \ diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 3be29976597..0877a79f39d 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -105,10 +105,11 @@ list(APPEND CPACK_FREEBSD_PACKAGE_DEPS audio/opus ftp/curl devel/libevdev + multimedia/pipewire net/avahi - x11/libX11 net/miniupnpc security/openssl + x11/libX11 ) if(NOT BOOST_USE_STATIC) diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD index eb32de82749..298f8964137 100644 --- a/packaging/linux/Arch/PKGBUILD +++ b/packaging/linux/Arch/PKGBUILD @@ -37,6 +37,7 @@ depends=( 'libevdev' 'libmfx' 'libnotify' + 'libpipewire' 'libpulse' 'libva' 'libx11' diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec index 84f1ee248c4..35d5e46b6c5 100644 --- a/packaging/linux/copr/Sunshine.spec +++ b/packaging/linux/copr/Sunshine.spec @@ -41,6 +41,7 @@ BuildRequires: libXinerama-devel BuildRequires: libXrandr-devel BuildRequires: libXtst-devel BuildRequires: openssl-devel +BuildRequires: pipewire-devel BuildRequires: rpm-build BuildRequires: systemd-rpm-macros BuildRequires: wget diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 751613bf19f..860c890fe05 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -91,6 +91,7 @@ class Sunshine < Formula depends_on "mesa" depends_on "numactl" depends_on "pango" + depends_on "pipewire" depends_on "pulseaudio" depends_on "systemd" depends_on "wayland" diff --git a/scripts/linux_build.sh b/scripts/linux_build.sh index 21a3d7181d7..d01e0a20043 100755 --- a/scripts/linux_build.sh +++ b/scripts/linux_build.sh @@ -234,6 +234,7 @@ function add_debian_based_deps() { "libnotify-dev" "libnuma-dev" "libopus-dev" + "libpipewire-0.3-dev" "libpulse-dev" "libssl-dev" "libsystemd-dev" @@ -322,6 +323,7 @@ function add_fedora_deps() { "numactl-devel" "openssl-devel" "opus-devel" + "pipewire-devel" "pulseaudio-libs-devel" "rpm-build" # if you want to build an RPM binary package "wget" # necessary for cuda install with `run` file From 0e636755762e4975cc6ade79af3279df86be43df Mon Sep 17 00:00:00 2001 From: David Lane <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:25:10 -0500 Subject: [PATCH 06/24] fix(portalgrab): change PERSIST_WHILE_RUNNING to 2 A value of 2 indicates "Remember this Selection". Co-authored-by: Carson Katri --- src/platform/linux/portalgrab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index f396d051e18..65831c53981 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -34,7 +34,7 @@ #define CURSOR_MODE_EMBEDDED 2 #define PERSIST_FORGET 0 -#define PERSIST_WHILE_RUNNING 1 +#define PERSIST_WHILE_RUNNING 2 #define PORTAL_NAME "org.freedesktop.portal.Desktop" #define PORTAL_PATH "/org/freedesktop/portal/desktop" From a617c25d11a9c0bf3b689f77006a5d4e3f74e0f9 Mon Sep 17 00:00:00 2001 From: Bond Date: Tue, 20 Jan 2026 05:24:16 +0200 Subject: [PATCH 07/24] feat(linux/portal): improve XDG portal capture for KDE and dual GPU systems (#4510) Co-authored-by: Carlos Garnacho Co-authored-by: Carson Katri Co-authored-by: d.bondarev --- src/platform/linux/portalgrab.cpp | 623 ++++++++++++++---- .../assets/web/configs/tabs/Advanced.vue | 2 + 2 files changed, 497 insertions(+), 128 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index 65831c53981..f025b093ae7 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -3,8 +3,13 @@ * @brief Definitions for XDG portal grab. */ // standard includes +#include #include +#include +#include +#include #include +#include #include // lib includes @@ -48,16 +53,64 @@ using namespace std::literals; namespace portal { - static char *restore_token; + // 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; - } format_map[] = { + }; + + 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; @@ -82,21 +135,29 @@ namespace portal { class dbus_t { public: ~dbus_t() { - g_object_unref(screencast_proxy); - g_object_unref(remote_desktop_proxy); - g_object_unref(conn); + if (screencast_proxy) { + g_object_unref(screencast_proxy); + } + if (remote_desktop_proxy) { + g_object_unref(remote_desktop_proxy); + } + if (conn) { + g_object_unref(conn); + } } int init() { - conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); + 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, NULL, PORTAL_NAME, PORTAL_PATH, REMOTE_DESKTOP_IFACE, NULL, NULL); + 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, NULL, PORTAL_NAME, PORTAL_PATH, SCREENCAST_IFACE, NULL, NULL); + 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; } @@ -105,26 +166,64 @@ namespace portal { } int connect_to_portal() { - g_autoptr(GMainLoop) loop = g_main_loop_new(NULL, FALSE); - g_autofree gchar *session_path = NULL, *session_token = NULL; - create_session_path(conn, &session_path, &session_token); + 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); + + // Try combined RemoteDesktop + ScreenCast session first + bool use_screencast_only = !try_remote_desktop_session(loop, &session_path, session_token); - if (create_session(loop, session_path, session_token) < 0) { + // Fall back to ScreenCast-only if RemoteDesktop failed + if (use_screencast_only && try_screencast_only_session(loop, &session_path) < 0) { return -1; } - if (select_remote_desktop_devices(loop, session_path) < 0) { + + if (start_portal_session(loop, session_path, pipewire_node, width, height, use_screencast_only) < 0) { return -1; } - if (select_screencast_sources(loop, session_path) < 0) { + + if (open_pipewire_remote(session_path, pipewire_fd) < 0) { return -1; } - if (start_session(loop, session_path, pipewire_node, width, height) < 0) { + + 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) < 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 (open_pipewire_remote(session_path, pipewire_fd) < 0) { + if (select_screencast_sources(loop, *session_path) < 0) { return -1; } - return 0; } @@ -138,13 +237,15 @@ namespace portal { GDBusProxy *screencast_proxy; GDBusProxy *remote_desktop_proxy; - int create_session(GMainLoop *loop, const gchar *session_path, const gchar *session_token) { + 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 = { 0, }; - g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path(conn, &request_path, &request_token); - dbus_response_init(&response, loop, conn, request_path); + 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})")); @@ -153,15 +254,50 @@ namespace portal { g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(session_token)); g_variant_builder_close(&builder); - g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "CreateSession", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + 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 session: "sv << err->message; + 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; } - g_autoptr(GVariant) ignore = dbus_response_wait(&response); + 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; return 0; } @@ -169,9 +305,8 @@ namespace portal { dbus_response_t response = { 0, }; - g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path(conn, &request_path, &request_token); - dbus_response_init(&response, loop, conn, request_path); + 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})")); @@ -179,20 +314,39 @@ namespace portal { 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}", "persist_mode", g_variant_new_uint32(PERSIST_WHILE_RUNNING)); - if (restore_token) { - g_variant_builder_add(&builder, "{sv}", "restore_token", g_variant_new_string(restore_token)); + 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 = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + 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; } - g_autoptr(GVariant) ignore = dbus_response_wait(&response); + 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; } @@ -200,9 +354,8 @@ namespace portal { dbus_response_t response = { 0, }; - g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path(conn, &request_path, &request_token); - dbus_response_init(&response, loop, conn, request_path); + 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})")); @@ -211,58 +364,109 @@ namespace portal { 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)); - g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_FORGET)); + g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_WHILE_RUNNING)); + 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 = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + 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; } - g_autoptr(GVariant) ignore = dbus_response_wait(&response); + 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_session(GMainLoop *loop, const gchar *session_path, int &pipewire_node, int &width, int &height) { + 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 = { 0, }; - g_autofree gchar *request_path = NULL, *request_token = NULL; - create_request_path(conn, &request_path, &request_token); - dbus_response_init(&response, loop, conn, request_path); + 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", ""); + 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 = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "Start", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + 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 session: "sv << err->message; + 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); - g_autoptr(GVariant) dict = NULL, streams = NULL; - g_variant_get(start_response, "(u@a{sv})", NULL, &dict, NULL); + 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})")); - // Preserve restore token for multiple runs (e.g. probing) - if (!restore_token) { - g_variant_lookup(dict, "restore_token", "s", &restore_token, NULL); + if (!streams) { + BOOST_LOG(error) << session_type << " Start: no streams in response"sv; + return -1; + } + + const gchar *new_token = nullptr; + if (g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && strlen(new_token) > 0) { + if (restore_token_t::get() != new_token) { + restore_token_t::set(new_token); + restore_token_t::save(); + } } GVariantIter iter; - g_autoptr(GVariant) value = NULL; + g_autoptr(GVariant) value = nullptr; g_variant_iter_init(&iter, streams); - while (g_variant_iter_next(&iter, "(u@a{sv})", &pipewire_node, &value)) { - g_variant_lookup(value, "size", "(ii)", &width, &height, NULL); + 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; @@ -270,10 +474,10 @@ namespace portal { int open_pipewire_remote(const gchar *session_path, int &fd) { GUnixFDList *fd_list; - GVariant *msg = g_variant_new("(oa{sv})", session_path, NULL); + GVariant *msg = g_variant_new("(oa{sv})", session_path, nullptr); - g_autoptr(GError) err = NULL; - g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync(screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &fd_list, NULL, &err); + 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; @@ -281,7 +485,7 @@ namespace portal { int fd_handle; g_variant_get(reply, "(h)", &fd_handle); - fd = g_unix_fd_list_get(fd_list, fd_handle, NULL); + fd = g_unix_fd_list_get(fd_list, fd_handle, nullptr); return 0; } @@ -294,7 +498,7 @@ namespace portal { 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, ".")) != NULL) { + while ((dot = strstr(sender, ".")) != nullptr) { *dot = '_'; } return sender; @@ -331,7 +535,7 @@ namespace portal { static void dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) { response->loop = loop; - g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, on_response_received_cb, response, NULL); + 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) { @@ -340,10 +544,114 @@ namespace portal { } }; + /** + * @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() { + 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; + } + } + + 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; + }; + + session_cache_t &session_cache_t::instance() { + alignas(session_cache_t) static std::byte storage[sizeof(session_cache_t)]; + static session_cache_t *instance_ = new (storage) session_cache_t(); + return *instance_; + } + class pipewire_t { public: - pipewire_t() { - loop = pw_thread_loop_new("Pipewire thread", NULL); + pipewire_t(): + loop(pw_thread_loop_new("Pipewire thread", nullptr)) { pw_thread_loop_start(loop); } @@ -370,17 +678,17 @@ namespace portal { fd = stream_fd; node = stream_node; - context = pw_context_new(pw_thread_loop_get_loop(loop), NULL, 0); - core = pw_context_connect_fd(context, dup(fd), NULL, 0); - pw_core_add_listener(core, &core_listener, &core_events, NULL); + context = pw_context_new(pw_thread_loop_get_loop(loop), nullptr, 0); + core = pw_context_connect_fd(context, dup(fd), nullptr, 0); + pw_core_add_listener(core, &core_listener, &core_events, nullptr); } - void ensure_stream(platf::mem_type_e mem_type, uint32_t width, uint32_t height, uint32_t refresh_rate, struct dmabuf_format_info_t *dmabuf_infos, int n_dmabuf_infos) { + void ensure_stream(platf::mem_type_e mem_type, uint32_t width, uint32_t height, uint32_t refresh_rate, struct dmabuf_format_info_t *dmabuf_infos, int n_dmabuf_infos, bool display_is_nvidia) { pw_thread_loop_lock(loop); if (!stream_data.stream) { struct pw_properties *props; - props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL); + 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); @@ -392,15 +700,23 @@ namespace portal { const struct spa_pod *params[MAX_PARAMS]; // Add preferred parameters for DMA-BUF with modifiers - if (n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi || mem_type == platf::mem_type_e::cuda)) { + // 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++) { params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers); } } // Add fallback for memptr - for (int i = 0; format_map[i].fourcc != 0; i++) { - params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, format_map[i].pw_format, NULL, 0); + for (const auto &fmt : format_map) { + if (fmt.fourcc == 0) { + break; + } + params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0); } pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params, n_params); @@ -429,6 +745,7 @@ namespace portal { } } else { img->data = (std::uint8_t *) buf->datas[0].data; + img->row_pitch = buf->datas[0].chunk->stride; } } } @@ -497,7 +814,7 @@ namespace portal { static void on_process(void *user_data) { struct stream_data_t *d = (struct stream_data_t *) user_data; - struct pw_buffer *b = NULL; + struct pw_buffer *b = nullptr; while (true) { struct pw_buffer *aux = pw_stream_dequeue_buffer(d->stream); @@ -510,7 +827,7 @@ namespace portal { b = aux; } - if (b == NULL) { + if (b == nullptr) { BOOST_LOG(warning) << "out of pipewire buffers"sv; return; } @@ -524,9 +841,9 @@ namespace portal { static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) { struct stream_data_t *d = (struct stream_data_t *) user_data; - d->current_buffer = NULL; + d->current_buffer = nullptr; - if (param == NULL || id != SPA_PARAM_Format) { + if (param == nullptr || id != SPA_PARAM_Format) { return; } if (spa_format_parse(param, &d->format.media_type, &d->format.media_subtype) < 0) { @@ -544,15 +861,18 @@ namespace portal { BOOST_LOG(info) << "Framerate: "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom; uint64_t drm_format = 0; - for (int n = 0; format_map[n].fourcc != 0; n++) { - if (format_map[n].pw_format == d->format.info.raw.format) { - drm_format = format_map[n].fourcc; + 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, NULL, SPA_FORMAT_VIDEO_modifier) != NULL && d->drm_format) { + 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 { @@ -586,18 +906,17 @@ namespace portal { if (get_dmabuf_modifiers() < 0) { return -1; } - if (dbus.init() < 0) { - return -1; - } - if (dbus.connect_to_portal() < 0) { + + // 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; } - width = dbus.width; - height = dbus.height; framerate = config.framerate; - pipewire.init(dbus.pipewire_fd, dbus.pipewire_node); + pipewire.init(pipewire_fd, pipewire_node); return 0; } @@ -611,6 +930,13 @@ namespace portal { auto img_egl = (egl::img_descriptor_t *) img_out.get(); img_egl->reset(); pipewire.fill_img(img_egl); + + // Check if we got valid data (either DMA-BUF fd or memory pointer) + if (img_egl->sd.fds[0] < 0 && img_egl->data == nullptr) { + // No buffer available yet from pipewire + return platf::capture_e::timeout; + } + img_egl->sequence = ++sequence; return platf::capture_e::ok; @@ -635,7 +961,7 @@ namespace portal { 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, (struct dmabuf_format_info_t *) dmabuf_infos, n_dmabuf_infos); + pipewire.ensure_stream(mem_type, width, height, framerate, (struct dmabuf_format_info_t *) dmabuf_infos, n_dmabuf_infos, display_is_nvidia); while (true) { auto now = std::chrono::steady_clock::now(); @@ -680,9 +1006,12 @@ namespace portal { #ifdef SUNSHINE_BUILD_CUDA if (mem_type == platf::mem_type_e::cuda) { - if (n_dmabuf_infos > 0) { + 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); } } @@ -702,6 +1031,50 @@ namespace portal { } private: + 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}; + EGLBoolean external_only; + eglQueryDmaBufModifiersEXT(egl_display, dmabuf_formats[i], MAX_DMABUF_MODIFIERS, mods.data(), &external_only, &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 = + (uint64_t *) 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; @@ -712,56 +1085,50 @@ namespace portal { return -1; } - if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) { - EGLint num_dmabuf_formats = 0; - EGLint dmabuf_formats[MAX_DMABUF_FORMATS] = { - 0, - }; - eglQueryDmaBufFormatsEXT(egl_display.get(), MAX_DMABUF_FORMATS, (EGLint *) &dmabuf_formats, &num_dmabuf_formats); - if (num_dmabuf_formats > MAX_DMABUF_FORMATS) { - BOOST_LOG(warning) << "Some DMA-BUF formats are being ignored"sv; - } - - EGLint i; - for (i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) { - uint32_t pw_format = 0; - for (int n = 0; format_map[n].fourcc != 0; n++) { - if (format_map[n].fourcc == dmabuf_formats[i]) { - pw_format = format_map[n].pw_format; - } - } - - if (pw_format == 0) { - continue; + // 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"; } - - EGLint num_modifiers = 0; - EGLuint64KHR mods[MAX_DMABUF_MODIFIERS] = { - 0, - }; - EGLBoolean external_only; - eglQueryDmaBufModifiersEXT(egl_display.get(), dmabuf_formats[i], MAX_DMABUF_MODIFIERS, (EGLuint64KHR *) &mods, &external_only, &num_modifiers); - if (num_modifiers > MAX_DMABUF_MODIFIERS) { - BOOST_LOG(warning) << "Some DMA-BUF modifiers are being ignored"sv; + 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; } - - 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 = - (uint64_t *) g_memdup2(mods, sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers); - ++n_dmabuf_infos; } } + if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) { + query_dmabuf_formats(egl_display.get()); + } + return 0; } platf::mem_type_e mem_type; wl::display_t wl_display; - dbus_t dbus; pipewire_t pipewire; struct dmabuf_format_info_t dmabuf_infos[MAX_DMABUF_FORMATS]; int n_dmabuf_infos; + bool display_is_nvidia = false; // Track if display GPU is NVIDIA std::chrono::nanoseconds delay; std::uint64_t sequence {}; uint32_t framerate; @@ -791,7 +1158,7 @@ namespace platf { return {}; } - pw_init(NULL, NULL); + pw_init(nullptr, nullptr); display_names.emplace_back("org.freedesktop.portal.Desktop"); return display_names; diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index 37191c7e1b5..d63d095f2d9 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -67,12 +67,14 @@ const config = ref(props.config)