From 874880e5ea7ef1240e895dbdcf76f92e8486fb60 Mon Sep 17 00:00:00 2001
From: David Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: Tue, 3 Feb 2026 08:19:02 -0500
Subject: [PATCH] feat(linux)!: Support streaming through XDG portals and
Pipewire (#4417)
Co-authored-by: Carlos Garnacho
Co-authored-by: Carson Katri
Co-authored-by: Bond
Co-authored-by: d.bondarev
Co-authored-by: Conn O'Griofa
---
.github/workflows/ci-freebsd.yml | 2 +
cmake/FindSystemd.cmake | 12 +-
cmake/compile_definitions/linux.cmake | 19 +-
cmake/packaging/linux.cmake | 11 +-
cmake/prep/options.cmake | 2 +
.../prep/special_package_configuration.cmake | 2 +
docs/getting_started.md | 35 +-
docs/troubleshooting.md | 12 +-
packaging/linux/00-sunshine-kms.preset.in | 4 +
packaging/linux/Arch/PKGBUILD | 1 +
packaging/linux/copr/Sunshine.spec | 8 +-
.../dev.lizardbyte.app.Sunshine.metainfo.xml | 4 -
.../flatpak/dev.lizardbyte.app.Sunshine.yml | 5 +-
packaging/linux/sunshine-kms.service.in | 16 +
packaging/linux/sunshine.service.in | 2 +
packaging/sunshine.rb | 1 +
scripts/linux_build.sh | 5 +-
src/platform/linux/kmsgrab.cpp | 6 +-
src/platform/linux/misc.cpp | 52 +-
src/platform/linux/portalgrab.cpp | 1183 +++++++++++++++++
.../assets/web/configs/tabs/Advanced.vue | 2 +
third-party/glad/include/glad/egl.h | 10 +-
third-party/glad/src/egl.c | 12 +-
23 files changed, 1338 insertions(+), 68 deletions(-)
create mode 100644 packaging/linux/00-sunshine-kms.preset.in
create mode 100644 packaging/linux/sunshine-kms.service.in
create mode 100644 src/platform/linux/portalgrab.cpp
diff --git a/.github/workflows/ci-freebsd.yml b/.github/workflows/ci-freebsd.yml
index 4878e360..62f3b57d 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 \
@@ -167,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/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake
index ee5c70d3..3edc0419 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,12 @@ 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/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index 6f98c355..3758884f 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 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(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")
+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, libva, pipewire, wayland, x11, or (libdrm and libcap)")
endif()
# tray icon
diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake
index 3be29976..d9538071 100644
--- a/cmake/packaging/linux.cmake
+++ b/cmake/packaging/linux.cmake
@@ -18,6 +18,10 @@ 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")
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/00-sunshine-kms.preset"
+ DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user-preset")
else()
find_package(Systemd)
find_package(Udev)
@@ -29,6 +33,10 @@ 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 "${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()
@@ -105,10 +113,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/cmake/prep/options.cmake b/cmake/prep/options.cmake
index b1b916ac..6b732a95 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/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake
index 74613523..f7e51d31 100644
--- a/cmake/prep/special_package_configuration.cmake
+++ b/cmake/prep/special_package_configuration.cmake
@@ -26,6 +26,8 @@ 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 58532341..d2afb2b9 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
@@ -405,37 +400,21 @@ 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 Capture
-For X11 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
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 844693e7..61c1ec59 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -160,16 +160,18 @@ 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 --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 00000000..5019b829
--- /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/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD
index eb32de82..298f8964 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 84f1ee24..572f5183 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
@@ -194,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"
@@ -366,8 +368,10 @@ fi
%caps(cap_sys_admin+p) %{_bindir}/sunshine
%caps(cap_sys_admin+p) %{_bindir}/sunshine-*
-# Systemd unit file 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 6be35169..cd5b0815 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/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
index c4db2a74..65581614 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/packaging/linux/sunshine-kms.service.in b/packaging/linux/sunshine-kms.service.in
new file mode 100644
index 00000000..848c1715
--- /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 1ba3a138..79cd391b 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/packaging/sunshine.rb b/packaging/sunshine.rb
index 751613bf..860c890f 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 21a3d718..2b1d705e 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
@@ -545,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
diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp
index de140e6f..e8bcc37f 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;
}
diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp
index 7d3810e5..723b8806 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,15 @@ 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 +964,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 {};
}
@@ -990,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;
}
@@ -1019,33 +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") && verify_portal()) {
+ sources[source::PORTAL] = true;
}
#endif
diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp
new file mode 100644
index 00000000..4c27357d
--- /dev/null
+++ b/src/platform/linux/portalgrab.cpp
@@ -0,0 +1,1183 @@
+/**
+ * @file src/platform/linux/portalgrab.cpp
+ * @brief Definitions for XDG portal grab.
+ */
+// standard includes
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// lib includes
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// local includes
+#include "cuda.h"
+#include "graphics.h"
+#include "src/main.h"
+#include "src/platform/common.h"
+#include "src/video.h"
+#include "vaapi.h"
+#include "wayland.h"
+
+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;
+
+namespace portal {
+ // Forward declarations
+ class session_cache_t;
+
+ class restore_token_t {
+ public:
+ static std::string get() {
+ return *token_;
+ }
+
+ static void set(std::string_view value) {
+ *token_ = value;
+ }
+
+ static bool empty() {
+ return token_->empty();
+ }
+
+ static void load() {
+ std::ifstream file(get_file_path());
+ if (file.is_open()) {
+ std::getline(file, *token_);
+ if (!token_->empty()) {
+ BOOST_LOG(info) << "Loaded portal restore token from disk"sv;
+ }
+ }
+ }
+
+ static void save() {
+ if (token_->empty()) {
+ return;
+ }
+ std::ofstream file(get_file_path());
+ if (file.is_open()) {
+ file << *token_;
+ BOOST_LOG(info) << "Saved portal restore token to disk"sv;
+ } else {
+ BOOST_LOG(warning) << "Failed to save portal restore token"sv;
+ }
+ }
+
+ private:
+ static inline const std::unique_ptr token_ = std::make_unique();
+
+ static std::string get_file_path() {
+ return platf::appdata().string() + "/portal_token";
+ }
+ };
+
+ struct format_map_t {
+ uint64_t fourcc;
+ int32_t pw_format;
+ };
+
+ static constexpr std::array format_map = {{
+ {DRM_FORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA},
+ {DRM_FORMAT_XRGB8888, SPA_VIDEO_FORMAT_BGRx},
+ {0, 0},
+ }};
+
+ struct dbus_response_t {
+ GMainLoop *loop;
+ GVariant *response;
+ guint subscription_id;
+ };
+
+ struct 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() {
+ 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() {
+ restore_token_t::load();
+
+ conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
+ if (!conn) {
+ return -1;
+ }
+ remote_desktop_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, nullptr, PORTAL_NAME, PORTAL_PATH, REMOTE_DESKTOP_IFACE, nullptr, nullptr);
+ if (!remote_desktop_proxy) {
+ return -1;
+ }
+ screencast_proxy = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, nullptr, PORTAL_NAME, PORTAL_PATH, SCREENCAST_IFACE, nullptr, nullptr);
+ if (!screencast_proxy) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ int connect_to_portal() {
+ g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, FALSE);
+ g_autofree gchar *session_path = nullptr;
+ g_autofree gchar *session_token = nullptr;
+ create_session_path(conn, nullptr, &session_token);
+
+ // Try combined RemoteDesktop + ScreenCast session first
+ bool use_screencast_only = !try_remote_desktop_session(loop, &session_path, session_token);
+
+ // Fall back to ScreenCast-only if RemoteDesktop failed
+ if (use_screencast_only && try_screencast_only_session(loop, &session_path) < 0) {
+ return -1;
+ }
+
+ if (start_portal_session(loop, session_path, pipewire_node, width, height, use_screencast_only) < 0) {
+ return -1;
+ }
+
+ if (open_pipewire_remote(session_path, pipewire_fd) < 0) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ // Try to create a combined RemoteDesktop + ScreenCast session
+ // Returns true on success, false if should fall back to ScreenCast-only
+ bool try_remote_desktop_session(GMainLoop *loop, gchar **session_path, const gchar *session_token) {
+ if (create_portal_session(loop, session_path, session_token, false) < 0) {
+ return false;
+ }
+
+ if (select_remote_desktop_devices(loop, *session_path) < 0) {
+ BOOST_LOG(warning) << "RemoteDesktop.SelectDevices failed, falling back to ScreenCast-only mode"sv;
+ g_free(*session_path);
+ *session_path = nullptr;
+ return false;
+ }
+
+ if (select_screencast_sources(loop, *session_path) < 0) {
+ BOOST_LOG(warning) << "ScreenCast.SelectSources failed with RemoteDesktop session, trying ScreenCast-only mode"sv;
+ g_free(*session_path);
+ *session_path = nullptr;
+ return false;
+ }
+
+ return true;
+ }
+
+ // Create a ScreenCast-only session
+ int try_screencast_only_session(GMainLoop *loop, gchar **session_path) {
+ g_autofree gchar *new_session_token = nullptr;
+ create_session_path(conn, nullptr, &new_session_token);
+ if (create_portal_session(loop, session_path, new_session_token, true) < 0) {
+ return -1;
+ }
+ if (select_screencast_sources(loop, *session_path) < 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_portal_session(GMainLoop *loop, gchar **session_path_out, const gchar *session_token, bool use_screencast) {
+ GDBusProxy *proxy = use_screencast ? screencast_proxy : remote_desktop_proxy;
+ const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop";
+
+ dbus_response_t response = {
+ nullptr,
+ };
+ g_autofree gchar *request_token = nullptr;
+ create_request_path(conn, nullptr, &request_token);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("(a{sv})"));
+ g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
+ g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(session_token));
+ g_variant_builder_close(&builder);
+
+ g_autoptr(GError) err = nullptr;
+ g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "CreateSession", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err);
+
+ if (err) {
+ BOOST_LOG(error) << "Could not create "sv << session_type << " session: "sv << err->message;
+ return -1;
+ }
+
+ const gchar *request_path = nullptr;
+ g_variant_get(reply, "(o)", &request_path);
+ dbus_response_init(&response, loop, conn, request_path);
+
+ g_autoptr(GVariant) create_response = dbus_response_wait(&response);
+
+ if (!create_response) {
+ BOOST_LOG(error) << session_type << " CreateSession: no response received"sv;
+ return -1;
+ }
+
+ guint32 response_code;
+ g_autoptr(GVariant) results = nullptr;
+ g_variant_get(create_response, "(u@a{sv})", &response_code, &results);
+
+ BOOST_LOG(debug) << session_type << " CreateSession response_code: "sv << response_code;
+
+ if (response_code != 0) {
+ BOOST_LOG(error) << session_type << " CreateSession failed with response code: "sv << response_code;
+ return -1;
+ }
+
+ g_autoptr(GVariant) session_handle_v = g_variant_lookup_value(results, "session_handle", nullptr);
+ if (!session_handle_v) {
+ BOOST_LOG(error) << session_type << " CreateSession: session_handle not found in response"sv;
+ return -1;
+ }
+
+ if (g_variant_is_of_type(session_handle_v, G_VARIANT_TYPE_VARIANT)) {
+ g_autoptr(GVariant) inner = g_variant_get_variant(session_handle_v);
+ *session_path_out = g_strdup(g_variant_get_string(inner, nullptr));
+ } else {
+ *session_path_out = g_strdup(g_variant_get_string(session_handle_v, nullptr));
+ }
+
+ BOOST_LOG(debug) << session_type << " CreateSession: got session handle: "sv << *session_path_out;
+ return 0;
+ }
+
+ int select_remote_desktop_devices(GMainLoop *loop, const gchar *session_path) {
+ dbus_response_t response = {
+ nullptr,
+ };
+ g_autofree gchar *request_token = nullptr;
+ create_request_path(conn, nullptr, &request_token);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("(oa{sv})"));
+ g_variant_builder_add(&builder, "o", session_path);
+ g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
+ g_variant_builder_add(&builder, "{sv}", "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 = nullptr;
+ g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err);
+
+ if (err) {
+ BOOST_LOG(error) << "Could not select devices: "sv << err->message;
+ return -1;
+ }
+
+ const gchar *request_path = nullptr;
+ g_variant_get(reply, "(o)", &request_path);
+ dbus_response_init(&response, loop, conn, request_path);
+
+ g_autoptr(GVariant) devices_response = dbus_response_wait(&response);
+
+ if (!devices_response) {
+ BOOST_LOG(error) << "SelectDevices: no response received"sv;
+ return -1;
+ }
+
+ guint32 response_code;
+ g_variant_get(devices_response, "(u@a{sv})", &response_code, nullptr);
+ BOOST_LOG(debug) << "SelectDevices response_code: "sv << response_code;
+
+ if (response_code != 0) {
+ BOOST_LOG(error) << "SelectDevices failed with response code: "sv << response_code;
+ return -1;
+ }
+
+ return 0;
+ }
+
+ int select_screencast_sources(GMainLoop *loop, const gchar *session_path) {
+ dbus_response_t response = {
+ nullptr,
+ };
+ g_autofree gchar *request_token = nullptr;
+ create_request_path(conn, nullptr, &request_token);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("(oa{sv})"));
+ g_variant_builder_add(&builder, "o", session_path);
+ g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
+ g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(SOURCE_TYPE_MONITOR));
+ g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(CURSOR_MODE_EMBEDDED));
+ 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 = nullptr;
+ g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err);
+ if (err) {
+ BOOST_LOG(error) << "Could not select sources: "sv << err->message;
+ return -1;
+ }
+
+ const gchar *request_path = nullptr;
+ g_variant_get(reply, "(o)", &request_path);
+ dbus_response_init(&response, loop, conn, request_path);
+
+ g_autoptr(GVariant) sources_response = dbus_response_wait(&response);
+
+ if (!sources_response) {
+ BOOST_LOG(error) << "SelectSources: no response received"sv;
+ return -1;
+ }
+
+ guint32 response_code;
+ g_variant_get(sources_response, "(u@a{sv})", &response_code, nullptr);
+ BOOST_LOG(debug) << "SelectSources response_code: "sv << response_code;
+
+ if (response_code != 0) {
+ BOOST_LOG(error) << "SelectSources failed with response code: "sv << response_code;
+ return -1;
+ }
+
+ return 0;
+ }
+
+ int start_portal_session(GMainLoop *loop, const gchar *session_path, int &out_pipewire_node, int &out_width, int &out_height, bool use_screencast) {
+ GDBusProxy *proxy = use_screencast ? screencast_proxy : remote_desktop_proxy;
+ const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop";
+
+ dbus_response_t response = {
+ nullptr,
+ };
+ g_autofree gchar *request_token = nullptr;
+ create_request_path(conn, nullptr, &request_token);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("(osa{sv})"));
+ g_variant_builder_add(&builder, "o", session_path);
+ g_variant_builder_add(&builder, "s", ""); // parent_window
+ g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
+ g_variant_builder_close(&builder);
+
+ g_autoptr(GError) err = nullptr;
+ g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "Start", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err);
+ if (err) {
+ BOOST_LOG(error) << "Could not start "sv << session_type << " session: "sv << err->message;
+ return -1;
+ }
+
+ const gchar *request_path = nullptr;
+ g_variant_get(reply, "(o)", &request_path);
+ dbus_response_init(&response, loop, conn, request_path);
+
+ g_autoptr(GVariant) start_response = dbus_response_wait(&response);
+
+ if (!start_response) {
+ BOOST_LOG(error) << session_type << " Start: no response received"sv;
+ return -1;
+ }
+
+ guint32 response_code;
+ g_autoptr(GVariant) dict = nullptr;
+ g_autoptr(GVariant) streams = nullptr;
+ g_variant_get(start_response, "(u@a{sv})", &response_code, &dict);
+
+ BOOST_LOG(debug) << session_type << " Start response_code: "sv << response_code;
+
+ if (response_code != 0) {
+ BOOST_LOG(error) << session_type << " Start failed with response code: "sv << response_code;
+ return -1;
+ }
+
+ streams = g_variant_lookup_value(dict, "streams", G_VARIANT_TYPE("a(ua{sv})"));
+ if (!streams) {
+ BOOST_LOG(error) << session_type << " Start: no streams in response"sv;
+ return -1;
+ }
+
+ if (const gchar *new_token = nullptr; g_variant_lookup(dict, "restore_token", "s", &new_token) && new_token && new_token[0] != '\0' && restore_token_t::get() != new_token) {
+ restore_token_t::set(new_token);
+ restore_token_t::save();
+ }
+
+ GVariantIter iter;
+ g_autoptr(GVariant) value = nullptr;
+ g_variant_iter_init(&iter, streams);
+ while (g_variant_iter_next(&iter, "(u@a{sv})", &out_pipewire_node, &value)) {
+ g_variant_lookup(value, "size", "(ii)", &out_width, &out_height, nullptr);
+ }
+
+ return 0;
+ }
+
+ int open_pipewire_remote(const gchar *session_path, int &fd) {
+ GUnixFDList *fd_list;
+ GVariant *msg = g_variant_new("(oa{sv})", session_path, nullptr);
+
+ g_autoptr(GError) err = nullptr;
+ g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync(screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &fd_list, nullptr, &err);
+ if (err) {
+ BOOST_LOG(error) << "Could not open pipewire remote: "sv << err->message;
+ return -1;
+ }
+
+ int fd_handle;
+ g_variant_get(reply, "(h)", &fd_handle);
+ fd = g_unix_fd_list_get(fd_list, fd_handle, nullptr);
+ return 0;
+ }
+
+ static void on_response_received_cb([[maybe_unused]] GDBusConnection *connection, [[maybe_unused]] const gchar *sender_name, [[maybe_unused]] const gchar *object_path, [[maybe_unused]] const gchar *interface_name, [[maybe_unused]] const gchar *signal_name, GVariant *parameters, gpointer user_data) {
+ auto *response = static_cast(user_data);
+ response->response = g_variant_ref_sink(parameters);
+ g_main_loop_quit(response->loop);
+ }
+
+ static gchar *get_sender_string(GDBusConnection *conn) {
+ gchar *sender = g_strdup(g_dbus_connection_get_unique_name(conn) + 1);
+ gchar *dot;
+ while ((dot = strstr(sender, ".")) != nullptr) {
+ *dot = '_';
+ }
+ return sender;
+ }
+
+ static void create_request_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) {
+ static uint32_t request_count = 0;
+
+ request_count++;
+
+ if (out_token) {
+ *out_token = g_strdup_printf("Sunshine%u", request_count);
+ }
+ if (out_path) {
+ g_autofree gchar *sender = get_sender_string(conn);
+ *out_path = g_strdup(std::format("{}{}{}{}", REQUEST_PREFIX, sender, "/Sunshine", request_count).c_str());
+ }
+ }
+
+ static void create_session_path(GDBusConnection *conn, gchar **out_path, gchar **out_token) {
+ static uint32_t session_count = 0;
+
+ session_count++;
+
+ if (out_token) {
+ *out_token = g_strdup_printf("Sunshine%u", session_count);
+ }
+
+ if (out_path) {
+ g_autofree gchar *sender = get_sender_string(conn);
+ *out_path = g_strdup(std::format("{}{}{}{}", SESSION_PREFIX, sender, "/Sunshine", session_count).c_str());
+ }
+ }
+
+ static void dbus_response_init(struct dbus_response_t *response, GMainLoop *loop, GDBusConnection *conn, const char *request_path) {
+ response->loop = loop;
+ response->subscription_id = g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, on_response_received_cb, response, nullptr);
+ }
+
+ static GVariant *dbus_response_wait(struct dbus_response_t *response) {
+ g_main_loop_run(response->loop);
+ return response->response;
+ }
+ };
+
+ /**
+ * @brief Singleton cache for portal session data.
+ *
+ * This prevents creating multiple portal sessions during encoder probing,
+ * which would show multiple screen recording indicators in the system tray.
+ */
+ class session_cache_t {
+ public:
+ static session_cache_t &instance();
+
+ /**
+ * @brief Get or create a portal session.
+ *
+ * If a cached session exists and is valid, returns the cached data.
+ * Otherwise, creates a new session and caches it.
+ *
+ * @return 0 on success, -1 on failure
+ */
+ int get_or_create_session(int &pipewire_fd, int &pipewire_node, int &width, int &height) {
+ std::scoped_lock lock(mutex_);
+
+ if (valid_) {
+ // Return cached session data
+ pipewire_fd = dup(pipewire_fd_); // Duplicate FD for each caller
+ pipewire_node = pipewire_node_;
+ width = width_;
+ height = height_;
+ BOOST_LOG(debug) << "Reusing cached portal session"sv;
+ return 0;
+ }
+
+ // Create new session
+ dbus_ = std::make_unique();
+ if (dbus_->init() < 0) {
+ return -1;
+ }
+ if (dbus_->connect_to_portal() < 0) {
+ dbus_.reset();
+ return -1;
+ }
+
+ // Cache the session data
+ pipewire_fd_ = dbus_->pipewire_fd;
+ pipewire_node_ = dbus_->pipewire_node;
+ width_ = dbus_->width;
+ height_ = dbus_->height;
+ valid_ = true;
+
+ // Return to caller (duplicate FD so each caller has their own)
+ pipewire_fd = dup(pipewire_fd_);
+ pipewire_node = pipewire_node_;
+ width = width_;
+ height = height_;
+
+ BOOST_LOG(debug) << "Created new portal session (cached)"sv;
+ return 0;
+ }
+
+ /**
+ * @brief Invalidate the cached session.
+ *
+ * Call this when the session becomes invalid (e.g., on error).
+ */
+ void invalidate() {
+ 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::array storage;
+ static auto instance_ = new (storage.data()) session_cache_t();
+ return *instance_;
+ }
+
+ class pipewire_t {
+ public:
+ pipewire_t():
+ loop(pw_thread_loop_new("Pipewire thread", nullptr)) {
+ pw_thread_loop_start(loop);
+ }
+
+ ~pipewire_t() {
+ 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), 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(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) {
+ pw_thread_loop_lock(loop);
+ if (!stream_data.stream) {
+ struct pw_properties *props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
+
+ stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props);
+ pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data);
+
+ std::array buffer;
+ struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
+
+ int n_params = 0;
+ std::array params;
+
+ // Add preferred parameters for DMA-BUF with modifiers
+ // Use DMA-BUF for VAAPI, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system).
+ // On hybrid GPU systems (Intel+NVIDIA), DMA-BUFs come from the Intel GPU and cannot
+ // be imported into CUDA, so we fall back to memory buffers in that case.
+ bool use_dmabuf = n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi ||
+ (mem_type == platf::mem_type_e::cuda && display_is_nvidia));
+ if (use_dmabuf) {
+ for (int i = 0; i < n_dmabuf_infos; i++) {
+ auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers);
+ params[n_params] = format_param;
+ n_params++;
+ }
+ }
+
+ // Add fallback for memptr
+ for (const auto &fmt : format_map) {
+ if (fmt.fourcc == 0) {
+ break;
+ }
+ auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0);
+ params[n_params] = format_param;
+ n_params++;
+ }
+
+ pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params);
+ }
+ pw_thread_loop_unlock(loop);
+ }
+
+ 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) {
+ 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;
+ 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 = static_cast(buf->datas[0].data);
+ img->row_pitch = buf->datas[0].chunk->stride;
+ }
+ }
+ }
+
+ pw_thread_loop_unlock(loop);
+ }
+
+ private:
+ struct pw_thread_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct stream_data_t stream_data;
+ int fd;
+ int node;
+
+ static struct spa_pod *build_format_parameter(struct spa_pod_builder *b, uint32_t width, uint32_t height, uint32_t refresh_rate, int32_t format, uint64_t *modifiers, int n_modifiers) {
+ struct spa_pod_frame object_frame;
+ struct spa_pod_frame modifier_frame;
+ std::array sizes;
+ std::array framerates;
+
+ sizes[0] = SPA_RECTANGLE(width, height); // Preferred
+ sizes[1] = SPA_RECTANGLE(1, 1);
+ sizes[2] = SPA_RECTANGLE(8192, 4096);
+
+ framerates[0] = SPA_FRACTION(0, 1); // we only want variable rate, thus bypassing compositor pacing
+ framerates[1] = SPA_FRACTION(0, 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);
+ 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);
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&framerates[0], &framerates[1], &framerates[2]), 0);
+
+ if (n_modifiers) {
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE);
+ spa_pod_builder_push_choice(b, &modifier_frame, SPA_CHOICE_Enum, 0);
+
+ // Preferred value, we pick the first modifier be the preferred one
+ spa_pod_builder_long(b, modifiers[0]);
+ for (uint32_t i = 0; i < n_modifiers; i++) {
+ spa_pod_builder_long(b, modifiers[i]);
+ }
+
+ spa_pod_builder_pop(b, &modifier_frame);
+ }
+
+ return static_cast(spa_pod_builder_pop(b, &object_frame));
+ }
+
+ static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) {
+ BOOST_LOG(info) << "Connected to pipewire version "sv << pw_info->version;
+ }
+
+ static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) {
+ BOOST_LOG(info) << "Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message;
+ }
+
+ constexpr static const struct pw_core_events core_events = {
+ .version = PW_VERSION_CORE_EVENTS,
+ .info = on_core_info_cb,
+ .error = on_core_error_cb,
+ };
+
+ static void on_process(void *user_data) {
+ const auto d = static_cast(user_data);
+ struct pw_buffer *b = nullptr;
+
+ 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 == nullptr) {
+ 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) {
+ const auto d = static_cast(user_data);
+
+ d->current_buffer = nullptr;
+
+ if (param == nullptr || id != SPA_PARAM_Format) {
+ return;
+ }
+ if (spa_format_parse(param, &d->format.media_type, &d->format.media_subtype) < 0) {
+ return;
+ }
+ if (d->format.media_type != SPA_MEDIA_TYPE_video || d->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+ return;
+ }
+ if (spa_format_video_raw_parse(param, &d->format.info.raw) < 0) {
+ return;
+ }
+
+ BOOST_LOG(info) << "Video format: "sv << d->format.info.raw.format;
+ BOOST_LOG(info) << "Size: "sv << d->format.info.raw.size.width << "x"sv << d->format.info.raw.size.height;
+ if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) {
+ BOOST_LOG(info) << "Framerate (from compositor): 0/1 (variable rate capture)";
+ } else {
+ BOOST_LOG(info) << "Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom;
+ BOOST_LOG(info) << "Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom;
+ }
+
+ uint64_t drm_format = 0;
+ for (const auto &fmt : format_map) {
+ if (fmt.fourcc == 0) {
+ break;
+ }
+ if (fmt.pw_format == d->format.info.raw.format) {
+ drm_format = fmt.fourcc;
+ }
+ }
+ d->drm_format = drm_format;
+
+ uint32_t buffer_types = 0;
+ if (spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier) != nullptr && d->drm_format) {
+ BOOST_LOG(info) << "using DMA-BUF buffers"sv;
+ buffer_types |= 1 << SPA_DATA_DmaBuf;
+ } else {
+ BOOST_LOG(info) << "using memory buffers"sv;
+ buffer_types |= 1 << SPA_DATA_MemPtr;
+ }
+
+ // Ack the buffer type
+ std::array buffer;
+ std::array params;
+ int n_params = 0;
+ struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
+ auto buffer_param = static_cast(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)));
+ params[n_params] = buffer_param;
+ n_params++;
+ pw_stream_update_params(d->stream, params.data(), 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;
+ }
+
+ // Use cached portal session to avoid creating multiple screen recordings
+ int pipewire_fd = -1;
+ int pipewire_node = 0;
+ if (session_cache_t::instance().get_or_create_session(pipewire_fd, pipewire_node, width, height) < 0) {
+ return -1;
+ }
+
+ framerate = config.framerate;
+
+ pipewire.init(pipewire_fd, 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;
+ }
+
+ const auto img_egl = static_cast(img_out.get());
+ img_egl->reset();
+ pipewire.fill_img(img_egl);
+
+ // Check if we got valid data (either DMA-BUF fd or memory pointer)
+ 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;
+ }
+
+ std::shared_ptr alloc_img() override {
+ // Note: this img_t type is also used for memory buffers
+ auto img = std::make_shared();
+
+ img->width = width;
+ img->height = height;
+ img->pixel_pitch = 4;
+ img->row_pitch = img->pixel_pitch * width;
+ img->sequence = 0;
+ img->serial = std::numeric_limitsserial)>::max();
+ img->data = nullptr;
+ std::fill_n(img->sd.fds, 4, -1);
+
+ return img;
+ }
+
+ platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
+ auto next_frame = std::chrono::steady_clock::now();
+
+ pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia);
+ sleep_overshoot_logger.reset();
+
+ while (true) {
+ auto now = std::chrono::steady_clock::now();
+
+ if (next_frame > now) {
+ std::this_thread::sleep_for(next_frame - now);
+ sleep_overshoot_logger.first_point(next_frame);
+ sleep_overshoot_logger.second_point_now_and_log();
+ }
+
+ next_frame += delay;
+ if (next_frame < now) { // some major slowdown happened; we couldn't keep up
+ next_frame = now + delay;
+ }
+
+ std::shared_ptr img_out;
+ switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) {
+ case platf::capture_e::reinit:
+ case platf::capture_e::error:
+ case platf::capture_e::interrupted:
+ 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 << std::to_underlying(status) << ']';
+ return status;
+ }
+ }
+
+ return platf::capture_e::ok;
+ }
+
+ std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
+#ifdef SUNSHINE_BUILD_VAAPI
+ if (mem_type == platf::mem_type_e::vaapi) {
+ return va::make_avcodec_encode_device(width, height, n_dmabuf_infos > 0);
+ }
+#endif
+
+#ifdef SUNSHINE_BUILD_CUDA
+ if (mem_type == platf::mem_type_e::cuda) {
+ if (display_is_nvidia && n_dmabuf_infos > 0) {
+ // Display GPU is NVIDIA - can use DMA-BUF directly
+ return cuda::make_avcodec_gl_encode_device(width, height, 0, 0);
+ } else {
+ // Hybrid system (Intel display + NVIDIA encode) - use memory buffer path
+ // DMA-BUFs from Intel GPU cannot be imported into CUDA
+ return cuda::make_avcodec_encode_device(width, height, false);
+ }
+ }
+#endif
+
+ return std::make_unique();
+ }
+
+ int dummy_img(platf::img_t *img) override {
+ if (!img) {
+ return -1;
+ }
+
+ img->data = new std::uint8_t[img->height * img->row_pitch];
+ std::fill_n(img->data, img->height * img->row_pitch, 0);
+ return 0;
+ }
+
+ 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 =
+ static_cast(g_memdup2(mods.data(), sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers));
+ ++n_dmabuf_infos;
+ }
+ }
+
+ int get_dmabuf_modifiers() {
+ if (wl_display.init() < 0) {
+ return -1;
+ }
+
+ auto egl_display = egl::make_display(wl_display.get());
+ if (!egl_display) {
+ return -1;
+ }
+
+ // Detect if this is a pure NVIDIA system (not hybrid Intel+NVIDIA)
+ // On hybrid systems, the wayland compositor typically runs on Intel,
+ // so DMA-BUFs from portal will come from Intel and cannot be imported into CUDA.
+ // Check if Intel GPU exists - if so, assume hybrid system and disable CUDA DMA-BUF.
+ bool has_intel_gpu = std::ifstream("/sys/class/drm/card0/device/vendor").good() ||
+ std::ifstream("/sys/class/drm/card1/device/vendor").good();
+ if (has_intel_gpu) {
+ // Read vendor IDs to check for Intel (0x8086)
+ auto check_intel = [](const std::string &path) {
+ if (std::ifstream f(path); f.good()) {
+ std::string vendor;
+ f >> vendor;
+ return vendor == "0x8086";
+ }
+ return false;
+ };
+ bool intel_present = check_intel("/sys/class/drm/card0/device/vendor") ||
+ check_intel("/sys/class/drm/card1/device/vendor");
+ if (intel_present) {
+ BOOST_LOG(info) << "Hybrid GPU system detected (Intel + discrete) - CUDA will use memory buffers"sv;
+ display_is_nvidia = false;
+ } else {
+ // No Intel GPU found, check if NVIDIA is present
+ const char *vendor = eglQueryString(egl_display.get(), EGL_VENDOR);
+ if (vendor && std::string_view(vendor).contains("NVIDIA")) {
+ BOOST_LOG(info) << "Pure NVIDIA system - DMA-BUF will be enabled for CUDA"sv;
+ display_is_nvidia = true;
+ }
+ }
+ }
+
+ if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) {
+ query_dmabuf_formats(egl_display.get());
+ }
+
+ return 0;
+ }
+
+ platf::mem_type_e mem_type;
+ wl::display_t wl_display;
+ pipewire_t pipewire;
+ std::array dmabuf_infos;
+ int n_dmabuf_infos;
+ bool display_is_nvidia = false; // Track if display GPU is NVIDIA
+ std::chrono::nanoseconds delay;
+ std::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) {
+ using enum platf::mem_type_e;
+ if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda) {
+ BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
+ return nullptr;
+ }
+
+ auto portal = std::make_shared();
+ if (portal->init(hwdevice_type, display_name, config)) {
+ return nullptr;
+ }
+
+ return portal;
+ }
+
+ std::vector portal_display_names() {
+ std::vector display_names;
+ auto dbus = std::make_shared();
+
+ if (dbus->init() < 0) {
+ return {};
+ }
+
+ pw_init(nullptr, nullptr);
+
+ display_names.emplace_back("org.freedesktop.portal.Desktop");
+ return display_names;
+ }
+} // namespace platf
diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue
index 37191c7e..d63d095f 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)
+
+
diff --git a/third-party/glad/include/glad/egl.h b/third-party/glad/include/glad/egl.h
index 37a7f010..6d4e1159 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;
@@ -578,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 fa4ffba9..30433b5c 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;
@@ -246,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
@@ -394,4 +400,4 @@ void gladLoaderUnloadEGL() {
#ifdef __cplusplus
}
-#endif
\ No newline at end of file
+#endif