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)
+
+
From 703347c8c985a2a9b503994acf5aa4300f9504cb Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 18:23:47 -0500
Subject: [PATCH 08/24] fix(xdg)!: remove default setcap on sunshine binaries
---
cmake/packaging/linux.cmake | 4 ----
docs/building.md | 8 ++++----
docs/getting_started.md | 4 ++--
packaging/linux/AppImage/AppRun | 3 ---
packaging/linux/Arch/sunshine.install | 6 ------
packaging/linux/copr/Sunshine.spec | 7 +++++--
src_assets/linux/misc/postinst | 12 ------------
7 files changed, 11 insertions(+), 33 deletions(-)
diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake
index 0877a79f39d..63dd78f0889 100644
--- a/cmake/packaging/linux.cmake
+++ b/cmake/packaging/linux.cmake
@@ -62,10 +62,6 @@ if(FREEBSD)
list(APPEND CPACK_POST_BUILD_SCRIPTS "${CMAKE_MODULE_PATH}/packaging/freebsd_custom_cpack.cmake")
endif()
-# Apply setcap for RPM
-# https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071
-set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}")
-
# Dependencies
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "\
diff --git a/docs/building.md b/docs/building.md
index 67b2066cd49..36b97edbad0 100644
--- a/docs/building.md
+++ b/docs/building.md
@@ -50,12 +50,12 @@ pkg install -y \
#### Linux
Dependencies vary depending on the distribution. You can reference our
[linux_build.sh](https://github.com/LizardByte/Sunshine/blob/master/scripts/linux_build.sh) script for a list of
-dependencies we use in Debian-based, Fedora-based and Arch-based distributions. Please submit a PR if you would like to extend the
-script to support other distributions.
+dependencies we use in Debian-based, Fedora-based and Arch-based distributions. Please submit a PR if you would like to
+extend the script to support other distributions.
##### KMS Capture
-If you are using KMS, patching the Sunshine binary with `setcap` is required. Some post-install scripts handle this. If building
-from source and using the binary directly, this will also work:
+If you are using KMS, patching the Sunshine binary with `setcap` is required. Some post-install scripts handle this.
+If building from source and using the binary directly, this will also work:
```bash
sudo cp build/sunshine /tmp
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 585323419de..6c450a71e4d 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -419,8 +419,8 @@ After adding yourself to the group, log out and log back in for the changes to t
sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))
```
-#### X11 Capture
-For X11 capture to work, you may need to disable the capabilities that were set for KMS capture.
+#### X11/XDG Capture
+For X11 or XDG Portal capture to work, you may need to disable the capabilities that were set for KMS capture.
```bash
sudo setcap -r $(readlink -f $(which sunshine))
diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun
index c021e425936..abd64ef449d 100644
--- a/packaging/linux/AppImage/AppRun
+++ b/packaging/linux/AppImage/AppRun
@@ -58,9 +58,6 @@ function install() {
cp -r "$SUNSHINE_SHARE_HERE/systemd/user/" ~/.config/systemd/
# patch service executable path
sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine.service
-
- # setcap
- sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")"
}
function remove() {
diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install
index d7b87737fc1..6e073358420 100644
--- a/packaging/linux/Arch/sunshine.install
+++ b/packaging/linux/Arch/sunshine.install
@@ -1,7 +1,3 @@
-do_setcap() {
- setcap cap_sys_admin+p $(readlink -f usr/bin/sunshine)
-}
-
do_udev_reload() {
udevadm control --reload-rules
udevadm trigger --property-match=DEVNAME=/dev/uinput
@@ -11,13 +7,11 @@ do_udev_reload() {
}
post_install() {
- do_setcap
do_udev_reload
modprobe uhid
}
post_upgrade() {
- do_setcap
do_udev_reload
modprobe uhid
}
diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec
index 35d5e46b6c5..aaf76dc7a3c 100644
--- a/packaging/linux/copr/Sunshine.spec
+++ b/packaging/linux/copr/Sunshine.spec
@@ -336,6 +336,10 @@ echo "npm version: $(npm --version)"
cd %{_builddir}/Sunshine/build
%make_install
+# setcap will break XDG Desktop Portal's security policy, so create a copy that can have elevated capabilities
+# this is only necessary for immutable distributions with rpm-ostree
+install -Dm755 %{_builddir}/Sunshine/build/sunshine %{_bindir}/sunshine-kms
+
%post
# Note: this is copied from the postinst script
@@ -364,8 +368,7 @@ fi
%files
# Executables
-%caps(cap_sys_admin+p) %{_bindir}/sunshine
-%caps(cap_sys_admin+p) %{_bindir}/sunshine-*
+%caps(cap_sys_admin+p) %{_bindir}/sunshine-kms
# Systemd unit file for user services
%{_userunitdir}/sunshine.service
diff --git a/src_assets/linux/misc/postinst b/src_assets/linux/misc/postinst
index 4783c2ec202..a6d6fd1b8ad 100644
--- a/src_assets/linux/misc/postinst
+++ b/src_assets/linux/misc/postinst
@@ -8,18 +8,6 @@ modprobe uhid
if [ ! -x "$(command -v rpm-ostree)" ]; then
echo "Not in an rpm-ostree environment, proceeding with post install steps."
- # Ensure Sunshine can grab images from KMS
- path_to_setcap=$(which setcap)
- path_to_sunshine=$(readlink -f "$(which sunshine)")
- if [ -x "$path_to_setcap" ] ; then
- echo "Setting CAP_SYS_ADMIN capability on Sunshine binary."
- echo "$path_to_setcap cap_sys_admin+p $path_to_sunshine"
- $path_to_setcap cap_sys_admin+p $path_to_sunshine
- echo "CAP_SYS_ADMIN capability set on Sunshine binary."
- else
- echo "error: setcap not found or not executable."
- fi
-
# Trigger udev rule reload for /dev/uinput and /dev/uhid
path_to_udevadm=$(which udevadm)
if [ -x "$path_to_udevadm" ] ; then
From 2e83d2f36f35b97618a887091d5edebc1c822f1a Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 19:37:46 -0500
Subject: [PATCH 09/24] refactor: move xdg portal capture after x11
---
src/platform/linux/misc.cpp | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp
index fb8be514431..e766ccf51cf 100644
--- a/src/platform/linux/misc.cpp
+++ b/src/platform/linux/misc.cpp
@@ -983,12 +983,6 @@ 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;
@@ -1013,6 +1007,12 @@ namespace platf {
return x11_display(hwdevice_type, display_name, config);
}
#endif
+#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
return nullptr;
}
From e4481bc57802b4ebebb4d60d3e4e7a72fec4207b Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 21:04:50 -0500
Subject: [PATCH 10/24] Fix install path for sunshine-kms binary
Corrects the installation path for sunshine-kms to use %{buildroot}%{_bindir} instead of %{_bindir}, ensuring proper packaging for RPM builds.
---
packaging/linux/copr/Sunshine.spec | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec
index aaf76dc7a3c..79820a86a0a 100644
--- a/packaging/linux/copr/Sunshine.spec
+++ b/packaging/linux/copr/Sunshine.spec
@@ -338,7 +338,7 @@ cd %{_builddir}/Sunshine/build
# setcap will break XDG Desktop Portal's security policy, so create a copy that can have elevated capabilities
# this is only necessary for immutable distributions with rpm-ostree
-install -Dm755 %{_builddir}/Sunshine/build/sunshine %{_bindir}/sunshine-kms
+install -Dm755 %{_builddir}/Sunshine/build/sunshine %{buildroot}%{_bindir}/sunshine-kms
%post
# Note: this is copied from the postinst script
@@ -368,6 +368,8 @@ fi
%files
# Executables
+%{_bindir}/sunshine
+%{_bindir}/sunshine-%{build_version}
%caps(cap_sys_admin+p) %{_bindir}/sunshine-kms
# Systemd unit file for user services
From 1d432e56cf5927b791fc593f7c0bb10234423b83 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 21:44:25 -0500
Subject: [PATCH 11/24] style: fix several sonar warnings
---
src/platform/linux/misc.cpp | 30 +++++----------
src/platform/linux/portalgrab.cpp | 61 ++++++++++++++++---------------
2 files changed, 42 insertions(+), 49 deletions(-)
diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp
index e766ccf51cf..723b8806c4f 100644
--- a/src/platform/linux/misc.cpp
+++ b/src/platform/linux/misc.cpp
@@ -1042,40 +1042,30 @@ namespace platf {
#endif
#ifdef SUNSHINE_BUILD_CUDA
- if ((config::video.capture.empty() && sources.none()) || config::video.capture == "nvfbc") {
- if (verify_nvfbc()) {
- sources[source::NVFBC] = true;
- }
+ if (((config::video.capture.empty() && sources.none()) || config::video.capture == "nvfbc") && verify_nvfbc()) {
+ sources[source::NVFBC] = true;
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
- if ((config::video.capture.empty() && sources.none()) || config::video.capture == "wlr") {
- if (verify_wl()) {
- sources[source::WAYLAND] = true;
- }
+ if (((config::video.capture.empty() && sources.none()) || config::video.capture == "wlr") && verify_wl()) {
+ sources[source::WAYLAND] = true;
}
#endif
#ifdef SUNSHINE_BUILD_DRM
- if ((config::video.capture.empty() && sources.none()) || config::video.capture == "kms") {
- if (verify_kms()) {
- sources[source::KMS] = true;
- }
+ if (((config::video.capture.empty() && sources.none()) || config::video.capture == "kms") && verify_kms()) {
+ sources[source::KMS] = true;
}
#endif
#ifdef SUNSHINE_BUILD_X11
// We enumerate this capture backend regardless of other suitable sources,
// since it may be needed as a NvFBC fallback for software encoding on X11.
- if (config::video.capture.empty() || config::video.capture == "x11") {
- if (verify_x11()) {
- sources[source::X11] = true;
- }
+ if ((config::video.capture.empty() || config::video.capture == "x11") && verify_x11()) {
+ sources[source::X11] = true;
}
#endif
#ifdef SUNSHINE_BUILD_PORTAL
- if (config::video.capture.empty() || config::video.capture == "portal") {
- if (verify_portal()) {
- sources[source::PORTAL] = true;
- }
+ if ((config::video.capture.empty() || config::video.capture == "portal") && verify_portal()) {
+ sources[source::PORTAL] = true;
}
#endif
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index f025b093ae7..3f2e88beea7 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -30,25 +30,30 @@
#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 2
-
-#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/"
+namespace {
+ // Buffer and limit constants
+ constexpr int SPA_POD_BUFFER_SIZE = 4096;
+ constexpr int MAX_PARAMS = 200;
+ constexpr int MAX_DMABUF_FORMATS = 200;
+ constexpr int MAX_DMABUF_MODIFIERS = 200;
+
+ // Portal configuration constants
+ constexpr uint32_t SOURCE_TYPE_MONITOR = 1;
+ constexpr uint32_t CURSOR_MODE_EMBEDDED = 2;
+
+ constexpr uint32_t PERSIST_FORGET = 0;
+ constexpr uint32_t PERSIST_WHILE_RUNNING = 2;
+
+ // Portal D-Bus interface names and paths
+ constexpr const char *PORTAL_NAME = "org.freedesktop.portal.Desktop";
+ constexpr const char *PORTAL_PATH = "/org/freedesktop/portal/desktop";
+ constexpr const char *REMOTE_DESKTOP_IFACE = "org.freedesktop.portal.RemoteDesktop";
+ constexpr const char *SCREENCAST_IFACE = "org.freedesktop.portal.ScreenCast";
+ constexpr const char *REQUEST_IFACE = "org.freedesktop.portal.Request";
+
+ constexpr const char *REQUEST_PREFIX = "/org/freedesktop/portal/desktop/request/";
+ constexpr const char *SESSION_PREFIX = "/org/freedesktop/portal/desktop/session/";
+} // namespace
using namespace std::literals;
@@ -242,7 +247,7 @@ namespace portal {
const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop";
dbus_response_t response = {
- 0,
+ nullptr,
};
g_autofree gchar *request_token = nullptr;
create_request_path(conn, nullptr, &request_token);
@@ -303,7 +308,7 @@ namespace portal {
int select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) {
dbus_response_t response = {
- 0,
+ nullptr,
};
g_autofree gchar *request_token = nullptr;
create_request_path(conn, nullptr, &request_token);
@@ -352,7 +357,7 @@ namespace portal {
int select_screencast_sources(GMainLoop *loop, const gchar *session_path) {
dbus_response_t response = {
- 0,
+ nullptr,
};
g_autofree gchar *request_token = nullptr;
create_request_path(conn, nullptr, &request_token);
@@ -405,7 +410,7 @@ namespace portal {
const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop";
dbus_response_t response = {
- 0,
+ nullptr,
};
g_autofree gchar *request_token = nullptr;
create_request_path(conn, nullptr, &request_token);
@@ -455,11 +460,9 @@ namespace portal {
}
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();
- }
+ if (g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && strlen(new_token) > 0 && restore_token_t::get() != new_token) {
+ restore_token_t::set(new_token);
+ restore_token_t::save();
}
GVariantIter iter;
@@ -489,7 +492,7 @@ namespace portal {
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) {
+ static void on_response_received_cb([[maybe_unused]] GDBusConnection *connection, [[maybe_unused]] const gchar *sender_name, [[maybe_unused]] const gchar *object_path, [[maybe_unused]] const gchar *interface_name, [[maybe_unused]] const gchar *signal_name, GVariant *parameters, gpointer user_data) {
dbus_response_t *response = (dbus_response_t *) user_data;
response->response = g_variant_ref_sink(parameters);
g_main_loop_quit(response->loop);
From cf9cc3d44af6c75fa998c5e85b500de83c10b4c1 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 22:09:13 -0500
Subject: [PATCH 12/24] Refactor to use std::array for buffer and struct
storage
Replaced raw arrays with std::array for type safety and clarity in buffer and struct allocations throughout portalgrab.cpp. Updated related code to use .data() and .size() where appropriate, improving maintainability and reducing potential for errors.
---
src/platform/linux/portalgrab.cpp | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index 3f2e88beea7..a7e7947229f 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -493,7 +493,7 @@ namespace portal {
}
static void on_response_received_cb([[maybe_unused]] GDBusConnection *connection, [[maybe_unused]] const gchar *sender_name, [[maybe_unused]] const gchar *object_path, [[maybe_unused]] const gchar *interface_name, [[maybe_unused]] const gchar *signal_name, GVariant *parameters, gpointer user_data) {
- dbus_response_t *response = (dbus_response_t *) user_data;
+ auto *response = static_cast(user_data);
response->response = g_variant_ref_sink(parameters);
g_main_loop_quit(response->loop);
}
@@ -646,8 +646,8 @@ namespace portal {
};
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();
+ alignas(session_cache_t) static std::array storage;
+ static auto instance_ = new (storage.data()) session_cache_t();
return *instance_;
}
@@ -696,11 +696,11 @@ namespace portal {
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));
+ std::array buffer;
+ struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
int n_params = 0;
- const struct spa_pod *params[MAX_PARAMS];
+ std::array params;
// Add preferred parameters for DMA-BUF with modifiers
// Use DMA-BUF for VAAPI, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system).
@@ -767,8 +767,8 @@ namespace portal {
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];
+ std::array sizes;
+ std::array framerates;
sizes[0] = SPA_RECTANGLE(width, height); // Preferred
sizes[1] = SPA_RECTANGLE(1, 1);
@@ -884,10 +884,10 @@ namespace portal {
}
// Ack the buffer type
- uint8_t buffer[SPA_POD_BUFFER_SIZE];
- const struct spa_pod *params[1];
+ std::array buffer;
+ std::array params;
int n_params = 0;
- struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
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);
}
@@ -964,7 +964,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, display_is_nvidia);
+ pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia);
while (true) {
auto now = std::chrono::steady_clock::now();
@@ -1129,7 +1129,7 @@ namespace portal {
platf::mem_type_e mem_type;
wl::display_t wl_display;
pipewire_t pipewire;
- struct dmabuf_format_info_t dmabuf_infos[MAX_DMABUF_FORMATS];
+ std::array dmabuf_infos;
int n_dmabuf_infos;
bool display_is_nvidia = false; // Track if display GPU is NVIDIA
std::chrono::nanoseconds delay;
From 6ed15e6b60410539b9e7da7b8ff7affe028a6796 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 22:34:16 -0500
Subject: [PATCH 13/24] Refactor array usage and variable declarations in
portalgrab.cpp
Changed C-style string constants to array declarations for REQUEST_PREFIX and SESSION_PREFIX. Updated function calls to use std::array::data() for params. Split spa_pod_frame variable declarations for clarity.
---
src/platform/linux/portalgrab.cpp | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index a7e7947229f..76d737a0be0 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -51,8 +51,8 @@ namespace {
constexpr const char *SCREENCAST_IFACE = "org.freedesktop.portal.ScreenCast";
constexpr const char *REQUEST_IFACE = "org.freedesktop.portal.Request";
- constexpr const char *REQUEST_PREFIX = "/org/freedesktop/portal/desktop/request/";
- constexpr const char *SESSION_PREFIX = "/org/freedesktop/portal/desktop/session/";
+ constexpr const char REQUEST_PREFIX[] = "/org/freedesktop/portal/desktop/request/";
+ constexpr const char SESSION_PREFIX[] = "/org/freedesktop/portal/desktop/session/";
} // namespace
using namespace std::literals;
@@ -722,7 +722,7 @@ namespace portal {
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);
+ pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params);
}
pw_thread_loop_unlock(loop);
}
@@ -766,7 +766,8 @@ namespace portal {
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_pod_frame object_frame;
+ struct spa_pod_frame modifier_frame;
std::array sizes;
std::array framerates;
@@ -889,7 +890,7 @@ namespace portal {
int n_params = 0;
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
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);
+ pw_stream_update_params(d->stream, params.data(), n_params);
}
constexpr static const struct pw_stream_events stream_events = {
From e302bc025fdccd29a547e00ef63784788b2fd397 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Thu, 22 Jan 2026 23:01:57 -0500
Subject: [PATCH 14/24] Replace g_strdup_printf with std::format for path
generation
Updated path string construction in portalgrab.cpp to use std::format instead of g_strdup_printf, improving type safety and readability.
---
src/platform/linux/portalgrab.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index 76d737a0be0..814ae9b625a 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -5,6 +5,7 @@
// standard includes
#include
#include
+#include
#include
#include
#include
@@ -517,7 +518,7 @@ namespace portal {
}
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(std::format("{}{}{}{}", REQUEST_PREFIX, sender, "/Sunshine", request_count).c_str());
}
}
@@ -532,7 +533,7 @@ namespace portal {
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(std::format("{}{}{}{}", SESSION_PREFIX, sender, "/Sunshine", session_count).c_str());
}
}
From cff079a47ed641d4b87a9ae4141e733f514b0d2d Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 23 Jan 2026 13:16:48 -0500
Subject: [PATCH 15/24] Refactor portalgrab.cpp for code clarity
Applied minor refactoring for improved code clarity and consistency: added [[maybe_unused]] to unused parameters, replaced C-style casts with static_cast, removed unnecessary semicolon, and simplified std::fill_n usage.
---
src/platform/linux/portalgrab.cpp | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index 814ae9b625a..abe602d8ed7 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -807,7 +807,7 @@ namespace portal {
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([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) {
BOOST_LOG(info) << "Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message;
}
@@ -818,7 +818,7 @@ namespace portal {
};
static void on_process(void *user_data) {
- struct stream_data_t *d = (struct stream_data_t *) user_data;
+ const auto d = static_cast(user_data);
struct pw_buffer *b = nullptr;
while (true) {
@@ -844,7 +844,7 @@ 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;
+ const auto d = static_cast(user_data);
d->current_buffer = nullptr;
@@ -1028,10 +1028,10 @@ namespace portal {
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);
+ std::fill_n(img->data, img->height * img->row_pitch, 0);
return 0;
}
From b098814d51b85b269056bf7f7988f7e9a614015f Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 23 Jan 2026 13:34:29 -0500
Subject: [PATCH 16/24] Refactor parameter assignment and improve type usage
Refactored parameter assignment to avoid post-increment in array indexing and improve clarity. Updated token check to use direct character comparison. Replaced enum class usage with 'using enum' for mem_type_e. Improved logging for unrecognized capture status by using std::to_underlying.
---
src/platform/linux/portalgrab.cpp | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index abe602d8ed7..8985608e5c6 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -460,8 +460,7 @@ namespace portal {
return -1;
}
- const gchar *new_token = nullptr;
- if (g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && strlen(new_token) > 0 && restore_token_t::get() != new_token) {
+ if (const gchar *new_token = nullptr; g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && new_token[0] != '\0' && restore_token_t::get() != new_token) {
restore_token_t::set(new_token);
restore_token_t::save();
}
@@ -711,7 +710,9 @@ namespace portal {
(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);
+ auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers);
+ params[n_params] = format_param;
+ n_params++;
}
}
@@ -720,7 +721,9 @@ namespace portal {
if (fmt.fourcc == 0) {
break;
}
- params[n_params++] = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0);
+ auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0);
+ params[n_params] = format_param;
+ n_params++;
}
pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params);
@@ -890,7 +893,9 @@ namespace portal {
std::array params;
int n_params = 0;
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
- 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));
+ auto buffer_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)));
+ params[n_params] = buffer_param;
+ n_params++;
pw_stream_update_params(d->stream, params.data(), n_params);
}
@@ -994,7 +999,7 @@ namespace portal {
push_captured_image_cb(std::move(img_out), true);
break;
default:
- BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
+ BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']';
return status;
}
}
@@ -1142,7 +1147,8 @@ 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) {
+ using enum platf::mem_type_e;
+ if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda) {
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
return nullptr;
}
From 346d28c8a932c653b976731a52fa667a7b725e44 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 23 Jan 2026 14:23:17 -0500
Subject: [PATCH 17/24] Refactor portalgrab.cpp for const correctness and C++
casts
Updated function parameters and local variables to use const qualifiers where appropriate. Replaced C-style casts with C++ static_cast for improved type safety and clarity. Minor code cleanups and style improvements.
---
src/platform/linux/portalgrab.cpp | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index 8985608e5c6..08aded69ae6 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -686,12 +686,10 @@ namespace portal {
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, bool display_is_nvidia) {
+ void ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) {
pw_thread_loop_lock(loop);
if (!stream_data.stream) {
- struct pw_properties *props;
-
- props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
+ struct pw_properties *props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props);
pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data);
@@ -739,7 +737,7 @@ namespace portal {
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;
+ const auto img_descriptor = static_cast(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;
@@ -751,7 +749,7 @@ namespace portal {
img_descriptor->sd.offsets[i] = buf->datas[i].chunk->offset;
}
} else {
- img->data = (std::uint8_t *) buf->datas[0].data;
+ img->data = static_cast(buf->datas[0].data);
img->row_pitch = buf->datas[0].chunk->stride;
}
}
@@ -803,10 +801,10 @@ namespace portal {
spa_pod_builder_pop(b, &modifier_frame);
}
- return (struct spa_pod *) spa_pod_builder_pop(b, &object_frame);
+ return static_cast(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([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) {
BOOST_LOG(info) << "Connected to pipewire version "sv << pw_info->version;
}
@@ -937,7 +935,7 @@ namespace portal {
return platf::capture_e::interrupted;
}
- auto img_egl = (egl::img_descriptor_t *) img_out.get();
+ const auto img_egl = static_cast(img_out.get());
img_egl->reset();
pipewire.fill_img(img_egl);
@@ -986,8 +984,7 @@ namespace portal {
next_frame = now + delay;
std::shared_ptr img_out;
- auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
- switch (status) {
+ switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) {
case platf::capture_e::reinit:
case platf::capture_e::error:
case platf::capture_e::interrupted:
@@ -1080,7 +1077,7 @@ namespace portal {
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);
+ static_cast(g_memdup2(mods.data(), sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers));
++n_dmabuf_infos;
}
}
From 857091a860167d0eb7d1504fb18d228ceee0a1ff Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 23 Jan 2026 14:52:46 -0500
Subject: [PATCH 18/24] Enable SUNSHINE_ENABLE_PORTAL in build configurations
Added the -DSUNSHINE_ENABLE_PORTAL=ON flag to FreeBSD CI, COPR spec, Flatpak, and linux_build.sh scripts to ensure the portal feature is enabled across all build targets. Also reordered some CMake flags for consistency.
---
.github/workflows/ci-freebsd.yml | 1 +
packaging/linux/copr/Sunshine.spec | 3 ++-
packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml | 5 +++--
scripts/linux_build.sh | 3 ++-
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci-freebsd.yml b/.github/workflows/ci-freebsd.yml
index f58604b859e..62f3b57d6c6 100644
--- a/.github/workflows/ci-freebsd.yml
+++ b/.github/workflows/ci-freebsd.yml
@@ -168,6 +168,7 @@ jobs:
-DSUNSHINE_EXECUTABLE_PATH=/usr/local/bin/sunshine \
-DSUNSHINE_ENABLE_CUDA=OFF \
-DSUNSHINE_ENABLE_DRM=OFF \
+ -DSUNSHINE_ENABLE_PORTAL=ON \
-DSUNSHINE_ENABLE_WAYLAND=ON \
-DSUNSHINE_ENABLE_X11=ON \
-DSUNSHINE_PUBLISHER_NAME="${GITHUB_REPOSITORY_OWNER}" \
diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec
index 79820a86a0a..1de89718f60 100644
--- a/packaging/linux/copr/Sunshine.spec
+++ b/packaging/linux/copr/Sunshine.spec
@@ -195,9 +195,10 @@ cmake_args=(
"-DCMAKE_INSTALL_PREFIX=%{_prefix}"
"-DSUNSHINE_ASSETS_DIR=%{_datadir}/sunshine"
"-DSUNSHINE_EXECUTABLE_PATH=%{_bindir}/sunshine"
+ "-DSUNSHINE_ENABLE_DRM=ON"
+ "-DSUNSHINE_ENABLE_PORTAL=ON"
"-DSUNSHINE_ENABLE_WAYLAND=ON"
"-DSUNSHINE_ENABLE_X11=ON"
- "-DSUNSHINE_ENABLE_DRM=ON"
"-DSUNSHINE_PUBLISHER_NAME=LizardByte"
"-DSUNSHINE_PUBLISHER_WEBSITE=https://app.lizardbyte.dev"
"-DSUNSHINE_PUBLISHER_ISSUE_URL=https://app.lizardbyte.dev/support"
diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
index c4db2a74adf..655816147f7 100644
--- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
+++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
@@ -69,10 +69,11 @@ modules:
- -DSUNSHINE_ASSETS_DIR=share/sunshine
- -DSUNSHINE_BUILD_FLATPAK=ON
- -DSUNSHINE_EXECUTABLE_PATH=/app/bin/sunshine
+ - -DSUNSHINE_ENABLE_CUDA=ON
+ - -DSUNSHINE_ENABLE_DRM=ON
+ - -DSUNSHINE_ENABLE_PORTAL=ON
- -DSUNSHINE_ENABLE_WAYLAND=ON
- -DSUNSHINE_ENABLE_X11=ON
- - -DSUNSHINE_ENABLE_DRM=ON
- - -DSUNSHINE_ENABLE_CUDA=ON
- -DSUNSHINE_PUBLISHER_NAME='LizardByte'
- -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev'
- -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support'
diff --git a/scripts/linux_build.sh b/scripts/linux_build.sh
index d01e0a20043..2b1d705ee7b 100755
--- a/scripts/linux_build.sh
+++ b/scripts/linux_build.sh
@@ -547,9 +547,10 @@ function run_step_cmake() {
"-DCMAKE_INSTALL_PREFIX=/usr"
"-DSUNSHINE_ASSETS_DIR=share/sunshine"
"-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine"
+ "-DSUNSHINE_ENABLE_DRM=ON"
+ "-DSUNSHINE_ENABLE_PORTAL=ON"
"-DSUNSHINE_ENABLE_WAYLAND=ON"
"-DSUNSHINE_ENABLE_X11=ON"
- "-DSUNSHINE_ENABLE_DRM=ON"
)
if [[ "$appimage_build" == 1 ]]; then
From 3ce55ccd932337cac09d199425704365074c0abc Mon Sep 17 00:00:00 2001
From: Conn O'Griofa
Date: Sat, 24 Jan 2026 00:05:48 +0000
Subject: [PATCH 19/24] fix(linux): Split service unit into privileged and
unprivileged variants (#4619)
---
cmake/packaging/linux.cmake | 8 +++++++
.../prep/special_package_configuration.cmake | 1 +
docs/building.md | 8 +++----
docs/getting_started.md | 23 +++----------------
docs/troubleshooting.md | 6 +++--
packaging/linux/AppImage/AppRun | 5 ++++
packaging/linux/Arch/sunshine.install | 6 +++++
packaging/linux/copr/Sunshine.spec | 12 ++++------
.../flatpak/scripts/additional-install.sh | 5 ++--
.../scripts/remove-additional-install.sh | 3 ++-
packaging/linux/sunshine-kms.service.in | 16 +++++++++++++
packaging/linux/sunshine.service.in | 2 ++
src_assets/linux/misc/postinst | 12 ++++++++++
13 files changed, 70 insertions(+), 37 deletions(-)
create mode 100644 packaging/linux/sunshine-kms.service.in
diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake
index 63dd78f0889..fbdbf13545d 100644
--- a/cmake/packaging/linux.cmake
+++ b/cmake/packaging/linux.cmake
@@ -18,6 +18,8 @@ if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK})
DESTINATION "${SUNSHINE_ASSETS_DIR}/modules-load.d")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service"
DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user")
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine-kms.service"
+ DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user")
else()
find_package(Systemd)
find_package(Udev)
@@ -29,6 +31,8 @@ else()
if(SYSTEMD_FOUND)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service"
DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}")
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine-kms.service"
+ DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}")
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf"
DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}")
endif()
@@ -62,6 +66,10 @@ if(FREEBSD)
list(APPEND CPACK_POST_BUILD_SCRIPTS "${CMAKE_MODULE_PATH}/packaging/freebsd_custom_cpack.cmake")
endif()
+# Apply setcap for RPM
+# https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071
+set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}")
+
# Dependencies
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "\
diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake
index 74613523596..aa19ef38ec0 100644
--- a/cmake/prep/special_package_configuration.cmake
+++ b/cmake/prep/special_package_configuration.cmake
@@ -26,6 +26,7 @@ elseif(UNIX)
# configure service
configure_file(packaging/linux/sunshine.service.in sunshine.service @ONLY)
+ configure_file(packaging/linux/sunshine-kms.service.in sunshine-kms.service @ONLY)
# configure the arch linux pkgbuild
if(${SUNSHINE_CONFIGURE_PKGBUILD})
diff --git a/docs/building.md b/docs/building.md
index 36b97edbad0..67b2066cd49 100644
--- a/docs/building.md
+++ b/docs/building.md
@@ -50,12 +50,12 @@ pkg install -y \
#### Linux
Dependencies vary depending on the distribution. You can reference our
[linux_build.sh](https://github.com/LizardByte/Sunshine/blob/master/scripts/linux_build.sh) script for a list of
-dependencies we use in Debian-based, Fedora-based and Arch-based distributions. Please submit a PR if you would like to
-extend the script to support other distributions.
+dependencies we use in Debian-based, Fedora-based and Arch-based distributions. Please submit a PR if you would like to extend the
+script to support other distributions.
##### KMS Capture
-If you are using KMS, patching the Sunshine binary with `setcap` is required. Some post-install scripts handle this.
-If building from source and using the binary directly, this will also work:
+If you are using KMS, patching the Sunshine binary with `setcap` is required. Some post-install scripts handle this. If building
+from source and using the binary directly, this will also work:
```bash
sudo cp build/sunshine /tmp
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 6c450a71e4d..c17dcb294b9 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -405,28 +405,11 @@ After adding yourself to the group, log out and log back in for the changes to t
### Linux
-#### KMS Capture
-
-> [!WARNING]
-> Capture of most Wayland-based desktop environments will fail unless this step is performed.
+#### Services
> [!NOTE]
-> `cap_sys_admin` may as well be root, except you don't need to be root to run the program. This is necessary to
-> allow Sunshine to use KMS capture.
-
-##### Enable
-```bash
-sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))
-```
-
-#### X11/XDG Capture
-For X11 or XDG Portal capture to work, you may need to disable the capabilities that were set for KMS capture.
-
-```bash
-sudo setcap -r $(readlink -f $(which sunshine))
-```
-
-#### Service
+> Two service unit files are available. Pick "sunshine" for unprivileged XDG Portal or X11 capture, otherwise
+> pick "sunshine-kms" for privileged KMS capture.
**Start once**
```bash
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 844693e746e..ea6636ed3d3 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -160,10 +160,12 @@ sudo usermod -aG input $USER
```
### KMS Streaming fails
-If screencasting fails with KMS, you may need to run the following to force unprivileged screencasting.
+If screencasting fails with KMS, you may be using the unprivileged sunshine service unit. Switch to the privileged
+sunshine-kms service:
```bash
-sudo setcap -r $(readlink -f $(which sunshine))
+systemctl --user disable sunshine
+systemctl --user enable sunshine-kms --now
```
> [!NOTE]
diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun
index abd64ef449d..a0cd5cdf491 100644
--- a/packaging/linux/AppImage/AppRun
+++ b/packaging/linux/AppImage/AppRun
@@ -58,6 +58,10 @@ function install() {
cp -r "$SUNSHINE_SHARE_HERE/systemd/user/" ~/.config/systemd/
# patch service executable path
sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine.service
+ sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine-kms.service
+
+ # setcap
+ sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")"
}
function remove() {
@@ -69,6 +73,7 @@ function remove() {
# remove service
sudo rm -f ~/.config/systemd/user/sunshine.service
+ sudo rm -f ~/.config/systemd/user/sunshine-kms.service
}
# process arguments
diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install
index 6e073358420..d7b87737fc1 100644
--- a/packaging/linux/Arch/sunshine.install
+++ b/packaging/linux/Arch/sunshine.install
@@ -1,3 +1,7 @@
+do_setcap() {
+ setcap cap_sys_admin+p $(readlink -f usr/bin/sunshine)
+}
+
do_udev_reload() {
udevadm control --reload-rules
udevadm trigger --property-match=DEVNAME=/dev/uinput
@@ -7,11 +11,13 @@ do_udev_reload() {
}
post_install() {
+ do_setcap
do_udev_reload
modprobe uhid
}
post_upgrade() {
+ do_setcap
do_udev_reload
modprobe uhid
}
diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec
index 1de89718f60..274bc0989cb 100644
--- a/packaging/linux/copr/Sunshine.spec
+++ b/packaging/linux/copr/Sunshine.spec
@@ -337,10 +337,6 @@ echo "npm version: $(npm --version)"
cd %{_builddir}/Sunshine/build
%make_install
-# setcap will break XDG Desktop Portal's security policy, so create a copy that can have elevated capabilities
-# this is only necessary for immutable distributions with rpm-ostree
-install -Dm755 %{_builddir}/Sunshine/build/sunshine %{buildroot}%{_bindir}/sunshine-kms
-
%post
# Note: this is copied from the postinst script
@@ -369,12 +365,12 @@ fi
%files
# Executables
-%{_bindir}/sunshine
-%{_bindir}/sunshine-%{build_version}
-%caps(cap_sys_admin+p) %{_bindir}/sunshine-kms
+%caps(cap_sys_admin+p) %{_bindir}/sunshine
+%caps(cap_sys_admin+p) %{_bindir}/sunshine-*
-# Systemd unit file for user services
+# Systemd unit files for user services
%{_userunitdir}/sunshine.service
+%{_userunitdir}/sunshine-kms.service
# Udev rules
%{_udevrulesdir}/*-sunshine.rules
diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh
index 5cd6cfbc809..b1bbba3bce9 100644
--- a/packaging/linux/flatpak/scripts/additional-install.sh
+++ b/packaging/linux/flatpak/scripts/additional-install.sh
@@ -3,8 +3,9 @@
# User Service
mkdir -p ~/.config/systemd/user
cp "/app/share/sunshine/systemd/user/sunshine.service" "$HOME/.config/systemd/user/sunshine.service"
-echo "Sunshine User Service has been installed."
-echo "Use [systemctl --user enable sunshine] once to autostart Sunshine on login."
+cp "/app/share/sunshine/systemd/user/sunshine-kms.service" "$HOME/.config/systemd/user/sunshine-kms.service"
+echo "Sunshine User Services have been installed."
+echo "Use [systemctl --user enable sunshine] or [systemctl --user enable sunshine-kms] once to autostart Sunshine on login."
# Load uhid (DS5 emulation)
UHID=$(cat /app/share/sunshine/modules-load.d/60-sunshine.conf)
diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh
index b3b30149bc6..6a1a8dfb117 100644
--- a/packaging/linux/flatpak/scripts/remove-additional-install.sh
+++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh
@@ -3,8 +3,9 @@
# User Service
systemctl --user stop sunshine
rm "$HOME/.config/systemd/user/sunshine.service"
+rm "$HOME/.config/systemd/user/sunshine-kms.service"
systemctl --user daemon-reload
-echo "Sunshine User Service has been removed."
+echo "Sunshine User Services have been removed."
# Remove rules
flatpak-spawn --host pkexec sh -c "rm /etc/modules-load.d/60-sunshine.conf"
diff --git a/packaging/linux/sunshine-kms.service.in b/packaging/linux/sunshine-kms.service.in
new file mode 100644
index 00000000000..848c17157f3
--- /dev/null
+++ b/packaging/linux/sunshine-kms.service.in
@@ -0,0 +1,16 @@
+[Unit]
+Description=@PROJECT_DESCRIPTION@
+StartLimitIntervalSec=500
+StartLimitBurst=5
+Conflicts=sunshine.service
+
+[Service]
+# Avoid starting Sunshine before the desktop is fully initialized.
+ExecStartPre=/bin/sleep 5
+@SUNSHINE_SERVICE_START_COMMAND@
+@SUNSHINE_SERVICE_STOP_COMMAND@
+Restart=on-failure
+RestartSec=5s
+
+[Install]
+WantedBy=xdg-desktop-autostart.target
diff --git a/packaging/linux/sunshine.service.in b/packaging/linux/sunshine.service.in
index 1ba3a138b46..79cd391b8ab 100644
--- a/packaging/linux/sunshine.service.in
+++ b/packaging/linux/sunshine.service.in
@@ -2,6 +2,7 @@
Description=@PROJECT_DESCRIPTION@
StartLimitIntervalSec=500
StartLimitBurst=5
+Conflicts=sunshine-kms.service
[Service]
# Avoid starting Sunshine before the desktop is fully initialized.
@@ -10,6 +11,7 @@ ExecStartPre=/bin/sleep 5
@SUNSHINE_SERVICE_STOP_COMMAND@
Restart=on-failure
RestartSec=5s
+NoNewPrivileges=true
[Install]
WantedBy=xdg-desktop-autostart.target
diff --git a/src_assets/linux/misc/postinst b/src_assets/linux/misc/postinst
index a6d6fd1b8ad..4783c2ec202 100644
--- a/src_assets/linux/misc/postinst
+++ b/src_assets/linux/misc/postinst
@@ -8,6 +8,18 @@ modprobe uhid
if [ ! -x "$(command -v rpm-ostree)" ]; then
echo "Not in an rpm-ostree environment, proceeding with post install steps."
+ # Ensure Sunshine can grab images from KMS
+ path_to_setcap=$(which setcap)
+ path_to_sunshine=$(readlink -f "$(which sunshine)")
+ if [ -x "$path_to_setcap" ] ; then
+ echo "Setting CAP_SYS_ADMIN capability on Sunshine binary."
+ echo "$path_to_setcap cap_sys_admin+p $path_to_sunshine"
+ $path_to_setcap cap_sys_admin+p $path_to_sunshine
+ echo "CAP_SYS_ADMIN capability set on Sunshine binary."
+ else
+ echo "error: setcap not found or not executable."
+ fi
+
# Trigger udev rule reload for /dev/uinput and /dev/uhid
path_to_udevadm=$(which udevadm)
if [ -x "$path_to_udevadm" ] ; then
From 97fa599e9170adaf6ac5e1e2fe1a9d54f05680f1 Mon Sep 17 00:00:00 2001
From: Conn O'Griofa
Date: Sat, 24 Jan 2026 15:43:50 +0000
Subject: [PATCH 20/24] fix(linux/xdgportal): sync capture delay logic/logging
with x11/kms (#4622)
---
src/platform/linux/portalgrab.cpp | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index 08aded69ae6..e571e5558cd 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -970,18 +970,21 @@ namespace portal {
auto next_frame = std::chrono::steady_clock::now();
pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia);
+ sleep_overshoot_logger.reset();
while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
- std::this_thread::sleep_for((next_frame - now) / 3 * 2);
+ std::this_thread::sleep_for(next_frame - now);
+ sleep_overshoot_logger.first_point(next_frame);
+ sleep_overshoot_logger.second_point_now_and_log();
}
- while (next_frame > now) {
- std::this_thread::sleep_for(1ns);
- now = std::chrono::steady_clock::now();
+
+ next_frame += delay;
+ if (next_frame < now) { // some major slowdown happened; we couldn't keep up
+ next_frame = now + delay;
}
- next_frame = now + delay;
std::shared_ptr img_out;
switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) {
From 8414c6bc9b0f9864803ff859232b9b72f2f73fb6 Mon Sep 17 00:00:00 2001
From: Conn O'Griofa
Date: Sun, 25 Jan 2026 19:31:02 +0000
Subject: [PATCH 21/24] fix(linux/xdgportal): set variable rate capture (#4624)
---
src/platform/linux/portalgrab.cpp | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
index e571e5558cd..4c27357d5fc 100644
--- a/src/platform/linux/portalgrab.cpp
+++ b/src/platform/linux/portalgrab.cpp
@@ -777,9 +777,9 @@ namespace portal {
sizes[1] = SPA_RECTANGLE(1, 1);
sizes[2] = SPA_RECTANGLE(8192, 4096);
- framerates[0] = SPA_FRACTION(refresh_rate, 1); // Preferred
+ framerates[0] = SPA_FRACTION(0, 1); // we only want variable rate, thus bypassing compositor pacing
framerates[1] = SPA_FRACTION(0, 1);
- framerates[2] = SPA_FRACTION(1000, 1);
+ framerates[2] = SPA_FRACTION(0, 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);
@@ -787,6 +787,7 @@ namespace portal {
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);
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&framerates[0], &framerates[1], &framerates[2]), 0);
if (n_modifiers) {
spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE);
@@ -864,7 +865,12 @@ namespace portal {
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;
+ if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) {
+ BOOST_LOG(info) << "Framerate (from compositor): 0/1 (variable rate capture)";
+ } else {
+ BOOST_LOG(info) << "Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom;
+ BOOST_LOG(info) << "Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom;
+ }
uint64_t drm_format = 0;
for (const auto &fmt : format_map) {
From eb063e1e4c0fe7cece54a7fbc36473f0cab2a9a7 Mon Sep 17 00:00:00 2001
From: Conn O'Griofa
Date: Mon, 26 Jan 2026 13:40:31 +0000
Subject: [PATCH 22/24] fix(linux): install vendor preset for sunshine-kms
service (#4626)
---
cmake/FindSystemd.cmake | 7 ++++++-
cmake/packaging/linux.cmake | 4 ++++
cmake/prep/special_package_configuration.cmake | 1 +
docs/getting_started.md | 12 ++++--------
docs/troubleshooting.md | 10 +++++-----
packaging/linux/00-sunshine-kms.preset.in | 4 ++++
packaging/linux/AppImage/AppRun | 2 --
packaging/linux/copr/Sunshine.spec | 3 ++-
.../linux/dev.lizardbyte.app.Sunshine.metainfo.xml | 4 ----
.../linux/flatpak/scripts/additional-install.sh | 5 ++---
.../flatpak/scripts/remove-additional-install.sh | 3 +--
11 files changed, 29 insertions(+), 26 deletions(-)
create mode 100644 packaging/linux/00-sunshine-kms.preset.in
diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake
index ee5c70d3e62..74b3d4fce14 100644
--- a/cmake/FindSystemd.cmake
+++ b/cmake/FindSystemd.cmake
@@ -19,6 +19,11 @@ IF (NOT WIN32)
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR)
+ execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
+ --variable=systemd_user_preset_dir systemd
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ OUTPUT_VARIABLE SYSTEMD_USER_PRESET_INSTALL_DIR)
+
execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
--variable=systemd_system_unit_dir systemd
OUTPUT_STRIP_TRAILING_WHITESPACE
@@ -29,7 +34,7 @@ IF (NOT WIN32)
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE SYSTEMD_MODULES_LOAD_DIR)
- mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR SYSTEMD_MODULES_LOAD_DIR)
+ mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_USER_PRESET_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR SYSTEMD_MODULES_LOAD_DIR)
endif ()
diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake
index fbdbf13545d..d9538071f14 100644
--- a/cmake/packaging/linux.cmake
+++ b/cmake/packaging/linux.cmake
@@ -20,6 +20,8 @@ if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK})
DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine-kms.service"
DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user")
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/00-sunshine-kms.preset"
+ DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user-preset")
else()
find_package(Systemd)
find_package(Udev)
@@ -33,6 +35,8 @@ else()
DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine-kms.service"
DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}")
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/00-sunshine-kms.preset"
+ DESTINATION "${SYSTEMD_USER_PRESET_INSTALL_DIR}")
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf"
DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}")
endif()
diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake
index aa19ef38ec0..f7e51d31e4b 100644
--- a/cmake/prep/special_package_configuration.cmake
+++ b/cmake/prep/special_package_configuration.cmake
@@ -27,6 +27,7 @@ elseif(UNIX)
# configure service
configure_file(packaging/linux/sunshine.service.in sunshine.service @ONLY)
configure_file(packaging/linux/sunshine-kms.service.in sunshine-kms.service @ONLY)
+ configure_file(packaging/linux/00-sunshine-kms.preset.in 00-sunshine-kms.preset @ONLY)
# configure the arch linux pkgbuild
if(${SUNSHINE_CONFIGURE_PKGBUILD})
diff --git a/docs/getting_started.md b/docs/getting_started.md
index c17dcb294b9..d2afb2b9354 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -274,16 +274,11 @@ flatpak install --user ./sunshine_{arch}.flatpak
flatpak run --command=additional-install.sh dev.lizardbyte.app.Sunshine
```
-##### Run with NVFBC capture (X11 Only)
+##### Run with NVFBC capture (X11 Only) or XDG Portal (Wayland Only)
```bash
flatpak run dev.lizardbyte.app.Sunshine
```
-##### Run with KMS capture (Wayland & X11)
-```bash
-sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run dev.lizardbyte.app.Sunshine
-```
-
##### Uninstall
```bash
flatpak run --command=remove-additional-install.sh dev.lizardbyte.app.Sunshine
@@ -416,9 +411,10 @@ After adding yourself to the group, log out and log back in for the changes to t
systemctl --user start sunshine
```
-**Start on boot**
+**Start on boot (unprivileged; swap logic for KMS)**
```bash
-systemctl --user enable sunshine
+systemctl --user --now disable sunshine-kms
+systemctl --user --now enable sunshine
```
### macOS
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index ea6636ed3d3..61c1ec594b3 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -164,14 +164,14 @@ If screencasting fails with KMS, you may be using the unprivileged sunshine serv
sunshine-kms service:
```bash
-systemctl --user disable sunshine
-systemctl --user enable sunshine-kms --now
+systemctl --user --now disable sunshine
+systemctl --user --now enable sunshine-kms
```
> [!NOTE]
-> The above command will not work with the AppImage or Flatpak packages. Please refer to the
-> [AppImage setup](md_docs_2getting__started.html#appimage) or
-> [Flatpak setup](md_docs_2getting__started.html#flatpak) for more specific instructions.
+> The above commands will not work with the AppImage or Flatpak packages, as KMS screencasting
+> requires elevated privileges which are not allowed by their respective packaging security policies.
+> As an alternative, XDG Portal capture is recommended.
### KMS streaming fails on Nvidia GPUs
If KMS screen capture results in a black screen being streamed, you may need to
diff --git a/packaging/linux/00-sunshine-kms.preset.in b/packaging/linux/00-sunshine-kms.preset.in
new file mode 100644
index 00000000000..5019b8292e9
--- /dev/null
+++ b/packaging/linux/00-sunshine-kms.preset.in
@@ -0,0 +1,4 @@
+# @PROJECT_DESCRIPTION@
+# KMS service should preset to disabled
+
+disable sunshine-kms.service
diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun
index a0cd5cdf491..c021e425936 100644
--- a/packaging/linux/AppImage/AppRun
+++ b/packaging/linux/AppImage/AppRun
@@ -58,7 +58,6 @@ function install() {
cp -r "$SUNSHINE_SHARE_HERE/systemd/user/" ~/.config/systemd/
# patch service executable path
sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine.service
- sed -i -e "s#$SUNSHINE_PATH#$(readlink -f $ARGV0)#g" ~/.config/systemd/user/sunshine-kms.service
# setcap
sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")"
@@ -73,7 +72,6 @@ function remove() {
# remove service
sudo rm -f ~/.config/systemd/user/sunshine.service
- sudo rm -f ~/.config/systemd/user/sunshine-kms.service
}
# process arguments
diff --git a/packaging/linux/copr/Sunshine.spec b/packaging/linux/copr/Sunshine.spec
index 274bc0989cb..572f518350c 100644
--- a/packaging/linux/copr/Sunshine.spec
+++ b/packaging/linux/copr/Sunshine.spec
@@ -368,9 +368,10 @@ fi
%caps(cap_sys_admin+p) %{_bindir}/sunshine
%caps(cap_sys_admin+p) %{_bindir}/sunshine-*
-# Systemd unit files for user services
+# Systemd unit/preset files for user services
%{_userunitdir}/sunshine.service
%{_userunitdir}/sunshine-kms.service
+%{_userpresetdir}/00-sunshine-kms.preset
# Udev rules
%{_udevrulesdir}/*-sunshine.rules
diff --git a/packaging/linux/dev.lizardbyte.app.Sunshine.metainfo.xml b/packaging/linux/dev.lizardbyte.app.Sunshine.metainfo.xml
index 6be351690d1..cd5b0815cfe 100644
--- a/packaging/linux/dev.lizardbyte.app.Sunshine.metainfo.xml
+++ b/packaging/linux/dev.lizardbyte.app.Sunshine.metainfo.xml
@@ -33,10 +33,6 @@
flatpak run --command=additional-install.sh @PROJECT_FQDN@
NOTE: Sunshine uses a self-signed certificate. The web browser will report it as not secure, but it is safe.
- NOTE: KMS Grab (Flatpak)
-
- sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run @PROJECT_FQDN@
-
diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh
index b1bbba3bce9..5cd6cfbc809 100644
--- a/packaging/linux/flatpak/scripts/additional-install.sh
+++ b/packaging/linux/flatpak/scripts/additional-install.sh
@@ -3,9 +3,8 @@
# User Service
mkdir -p ~/.config/systemd/user
cp "/app/share/sunshine/systemd/user/sunshine.service" "$HOME/.config/systemd/user/sunshine.service"
-cp "/app/share/sunshine/systemd/user/sunshine-kms.service" "$HOME/.config/systemd/user/sunshine-kms.service"
-echo "Sunshine User Services have been installed."
-echo "Use [systemctl --user enable sunshine] or [systemctl --user enable sunshine-kms] once to autostart Sunshine on login."
+echo "Sunshine User Service has been installed."
+echo "Use [systemctl --user enable sunshine] once to autostart Sunshine on login."
# Load uhid (DS5 emulation)
UHID=$(cat /app/share/sunshine/modules-load.d/60-sunshine.conf)
diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh
index 6a1a8dfb117..b3b30149bc6 100644
--- a/packaging/linux/flatpak/scripts/remove-additional-install.sh
+++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh
@@ -3,9 +3,8 @@
# User Service
systemctl --user stop sunshine
rm "$HOME/.config/systemd/user/sunshine.service"
-rm "$HOME/.config/systemd/user/sunshine-kms.service"
systemctl --user daemon-reload
-echo "Sunshine User Services have been removed."
+echo "Sunshine User Service has been removed."
# Remove rules
flatpak-spawn --host pkexec sh -c "rm /etc/modules-load.d/60-sunshine.conf"
From da6a8e52beef8735b9662c24829147a4fe91cb62 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Mon, 26 Jan 2026 21:40:28 -0500
Subject: [PATCH 23/24] Format mark_as_advanced call for readability
Reformats the mark_as_advanced call in FindSystemd.cmake to list each variable on a separate line, improving readability and maintainability.
---
cmake/FindSystemd.cmake | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake
index 74b3d4fce14..3edc0419596 100644
--- a/cmake/FindSystemd.cmake
+++ b/cmake/FindSystemd.cmake
@@ -34,7 +34,12 @@ IF (NOT WIN32)
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE SYSTEMD_MODULES_LOAD_DIR)
- mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_USER_PRESET_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR SYSTEMD_MODULES_LOAD_DIR)
+ mark_as_advanced(
+ SYSTEMD_USER_UNIT_INSTALL_DIR
+ SYSTEMD_USER_PRESET_INSTALL_DIR
+ SYSTEMD_SYSTEM_UNIT_INSTALL_DIR
+ SYSTEMD_MODULES_LOAD_DIR
+ )
endif ()
From cb67b3efa6b2440be4e21250e8f3b7afdadf0491 Mon Sep 17 00:00:00 2001
From: Conn O'Griofa
Date: Tue, 27 Jan 2026 15:24:08 +0000
Subject: [PATCH 24/24] fix(linux/kmsgrab): deprioritize setcap error message
for non-KMS Wayland users (#4635)
---
src/platform/linux/kmsgrab.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp
index de140e6f740..e8bcc37f5db 100644
--- a/src/platform/linux/kmsgrab.cpp
+++ b/src/platform/linux/kmsgrab.cpp
@@ -1660,9 +1660,9 @@ namespace platf {
if (!fb->handles[0]) {
BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv;
- BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error)
- << "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\n"sv
- << "If you installed from AppImage or Flatpak, please refer to the official documentation:\n"sv
+ BOOST_LOG((config::video.capture == "kms") ? fatal : error)
+ << "If you installed from AppImage or Flatpak, KMS capture is not supported.\n"sv
+ << "Please refer to the official documentation:\n"sv
<< "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html#linux"sv;
break;
}