feat(capture/linux): add KWin direct screencast capture method (#5009)

Co-authored-by: Ramalama2 <6314556+Ramalama2@users.noreply.github.com>
This commit is contained in:
Kishi
2026-05-05 02:58:35 +02:00
committed by GitHub
parent bdef150c6b
commit a90d3068e9
17 changed files with 813 additions and 18 deletions
+1
View File
@@ -173,6 +173,7 @@ jobs:
-DSUNSHINE_EXECUTABLE_PATH=/usr/local/bin/sunshine \
-DSUNSHINE_ENABLE_CUDA=OFF \
-DSUNSHINE_ENABLE_DRM=OFF \
-DSUNSHINE_ENABLE_KWIN=ON \
-DSUNSHINE_ENABLE_PORTAL=ON \
-DSUNSHINE_ENABLE_WAYLAND=ON \
-DSUNSHINE_ENABLE_X11=ON \
+4
View File
@@ -45,6 +45,10 @@
path = third-party/nvapi
url = https://github.com/NVIDIA/nvapi.git
branch = main
[submodule "third-party/plasma-wayland-protocols"]
path = third-party/plasma-wayland-protocols
url = https://github.com/KDE/plasma-wayland-protocols.git
branch = master
[submodule "third-party/Simple-Web-Server"]
path = third-party/Simple-Web-Server
url = https://github.com/LizardByte-infrastructure/Simple-Web-Server.git
+14
View File
@@ -276,6 +276,13 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
<td></td>
<td></td>
</tr>
<tr>
<td>KWin Screencast</td>
<td>✅</td>
<td>✅</td>
<td></td>
<td></td>
</tr>
</table>
<table>
@@ -322,6 +329,13 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>KWin Screencast</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
</table>
**Legend:** ✅ Supported | 🟡 Partial Support | ❌ Not Yet Supported | Not Applicable
+22 -8
View File
@@ -76,8 +76,8 @@ if(CUDA_FOUND)
add_compile_definitions(SUNSHINE_BUILD_CUDA)
endif()
# libdrm is required for both DRM (KMS) and Wayland
if(${SUNSHINE_ENABLE_DRM} OR ${SUNSHINE_ENABLE_WAYLAND})
# libdrm is required for DRM (KMS), KWin ScreenCast and Wayland
if(${SUNSHINE_ENABLE_DRM} OR ${SUNSHINE_ENABLE_KWIN} OR ${SUNSHINE_ENABLE_WAYLAND})
find_package(LIBDRM REQUIRED)
else()
set(LIBDRM_FOUND OFF)
@@ -243,7 +243,7 @@ if(GIO_FOUND)
endif()
# Pipewire
if(${SUNSHINE_ENABLE_PORTAL})
if(${SUNSHINE_ENABLE_KWIN} OR ${SUNSHINE_ENABLE_PORTAL})
pkg_check_modules(PIPEWIRE libpipewire-0.3 REQUIRED)
else()
set(PIPEWIRE_FOUND OFF)
@@ -264,13 +264,27 @@ if(PIPEWIRE_FOUND AND GIO_FOUND AND ${SUNSHINE_ENABLE_PORTAL})
"${CMAKE_SOURCE_DIR}/src/platform/linux/portalgrab.cpp")
endif()
# KWin ScreenCast (direct Wayland protocol, bypasses portal)
set(KWIN_FOUND OFF)
if(PIPEWIRE_FOUND AND WAYLAND_FOUND AND ${SUNSHINE_ENABLE_KWIN})
set(KWIN_FOUND ON)
add_compile_definitions(SUNSHINE_BUILD_KWIN)
GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/plasma-wayland-protocols/src/protocols" "" kde-output-order-v1)
GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/plasma-wayland-protocols/src/protocols" "" zkde-screencast-unstable-v1)
list(APPEND PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/linux/kwingrab.cpp")
elseif(${SUNSHINE_ENABLE_KWIN} AND NOT WAYLAND_FOUND)
message(FATAL_ERROR "SUNSHINE_ENABLE_KWIN requires SUNSHINE_ENABLE_WAYLAND — KWin capture disabled")
endif()
if(NOT ${CUDA_FOUND}
AND NOT ${WAYLAND_FOUND}
AND NOT ${X11_FOUND}
AND NOT ${PORTAL_FOUND}
AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND})
AND NOT ${LIBVA_FOUND})
message(FATAL_ERROR "Couldn't find either cuda, libva, pipewire, wayland, x11, or (libdrm and libcap)")
AND NOT ${LIBVA_FOUND}
AND NOT ${KWIN_FOUND}
AND NOT ${PORTAL_FOUND}
AND NOT ${WAYLAND_FOUND}
AND NOT ${X11_FOUND})
message(FATAL_ERROR "Couldn't find either cuda, (libdrm and libcap), libva, kwin, pipewire, portal, wayland or x11")
endif()
# tray icon
+4
View File
@@ -173,6 +173,10 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.desktop"
if(NOT ${SUNSHINE_BUILD_APPIMAGE} AND NOT ${SUNSHINE_BUILD_FLATPAK})
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.terminal.desktop"
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
if(${SUNSHINE_ENABLE_KWIN})
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.kwin.desktop"
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
endif()
endif()
# metadata file
+2
View File
@@ -64,6 +64,8 @@ elseif(UNIX) # Linux
"Enable building wayland specific code." ON)
option(SUNSHINE_ENABLE_X11
"Enable X11 grab if available." ON)
option(SUNSHINE_ENABLE_KWIN
"Enable KWin ScreenCast grab if available" ON)
option(SUNSHINE_ENABLE_PORTAL
"Enable XDG portal grab if available" ON)
endif()
@@ -26,6 +26,11 @@ elseif(UNIX)
# configure service
configure_file(packaging/linux/app-${PROJECT_FQDN}.service.in app-${PROJECT_FQDN}.service @ONLY)
# configure kwin desktop permission file
if (${SUNSHINE_ENABLE_KWIN})
configure_file(packaging/linux/${PROJECT_FQDN}.kwin.desktop.in ${PROJECT_FQDN}.kwin.desktop @ONLY)
endif()
# configure the arch linux pkgbuild
if(${SUNSHINE_CONFIGURE_PKGBUILD})
configure_file(packaging/linux/Arch/PKGBUILD PKGBUILD @ONLY)
+1 -9
View File
@@ -31,15 +31,7 @@ endif()
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS})
target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})
if(APPLE AND NOT SUNSHINE_BUILD_HOMEBREW)
# codesign on Mac won't sign an .app that uses a symlink
set_target_properties(sunshine PROPERTIES CXX_STANDARD 23)
else()
# symlink sunshine -> sunshine-PROJECT_VERSION
set_target_properties(sunshine PROPERTIES CXX_STANDARD 23
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR})
endif()
set_target_properties(sunshine PROPERTIES CXX_STANDARD 23)
# CLion complains about unknown flags after running cmake, and cannot add symbols to the index for cuda files
if(CUDA_INHERIT_COMPILE_OPTIONS)
+5
View File
@@ -2101,6 +2101,11 @@ editing the `conf` file in a text editor. Use the examples as reference.
<td>DRM/KMS screen capture from the kernel. This requires that Sunshine has `cap_sys_admin` capability.
@note{Applies to Linux only.}</td>
</tr>
<tr>
<td>kwin</td>
<td>Capture with KDE/KWin Wayland compositor via KDE screencasting.
@note{Applies to Linux only.}</td>
</tr>
<tr>
<td>x11</td>
<td>Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible.
+1 -1
View File
@@ -219,6 +219,7 @@ cmake_args=(
"-DSUNSHINE_ASSETS_DIR=%{_datadir}/sunshine"
"-DSUNSHINE_EXECUTABLE_PATH=%{_bindir}/sunshine"
"-DSUNSHINE_ENABLE_DRM=ON"
"-DSUNSHINE_ENABLE_KWIN=ON"
"-DSUNSHINE_ENABLE_PORTAL=ON"
"-DSUNSHINE_ENABLE_WAYLAND=ON"
"-DSUNSHINE_ENABLE_X11=ON"
@@ -405,7 +406,6 @@ fi
%files
# Executables
%caps(cap_sys_admin,cap_sys_nice+p) %{_bindir}/sunshine
%caps(cap_sys_admin,cap_sys_nice+p) %{_bindir}/sunshine-*
# Systemd unit files for user services
%{_userunitdir}/*.service
@@ -0,0 +1,7 @@
[Desktop Entry]
Exec=@SUNSHINE_EXECUTABLE_PATH@
X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1
Type=Application
Name=@PROJECT_FQDN@-kwin-wayland-permission
Comment=Sunshine KWin screencast permission
NoDisplay=true
@@ -82,6 +82,7 @@ modules:
- -DSUNSHINE_EXECUTABLE_PATH=/app/bin/sunshine
- -DSUNSHINE_ENABLE_CUDA=ON
- -DSUNSHINE_ENABLE_DRM=ON
- -DSUNSHINE_ENABLE_KWIN=ON
- -DSUNSHINE_ENABLE_PORTAL=ON
- -DSUNSHINE_ENABLE_WAYLAND=ON
- -DSUNSHINE_ENABLE_X11=ON
+1
View File
@@ -578,6 +578,7 @@ function run_step_cmake() {
"-DSUNSHINE_ASSETS_DIR=share/sunshine"
"-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine"
"-DSUNSHINE_ENABLE_DRM=ON"
"-DSUNSHINE_ENABLE_KWIN=ON"
"-DSUNSHINE_ENABLE_PORTAL=ON"
"-DSUNSHINE_ENABLE_WAYLAND=ON"
"-DSUNSHINE_ENABLE_X11=ON"
+713
View File
@@ -0,0 +1,713 @@
/**
* @file src/platform/linux/kwingrab.cpp
* @brief KWin direct ScreenCast capture via zkde_screencast_unstable_v1 Wayland protocol.
*
* Bypasses xdg-desktop-portal entirely. Sunshine connects directly to KWin's
* Wayland protocol to obtain a PipeWire node_id, then streams frames via PipeWire.
*
* Chain: KWin -> Wayland kde_screencast -> PipeWire -> Sunshine
*/
// standard includes
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <fstream>
#include <memory>
#include <pwd.h>
#include <ranges>
#include <string>
#include <string_view>
#include <thread>
// lib includes
#include <pipewire/pipewire.h>
#include <poll.h>
#include <unistd.h>
#include <wayland-client.h>
// generated protocol header
#include <kde-output-order-v1.h>
#include <zkde-screencast-unstable-v1.h>
// local includes
#include "cuda.h"
#include "graphics.h"
#include "pipewire.cpp"
#include "src/platform/common.h"
#include "src/video.h"
using namespace std::literals;
namespace kwin {
/**
* KWin Wayland ScreenCast permissions
*
* To have access to zkde_screencast_unstable_v1 KWin checks for a .desktop file with
* X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1 and the current executable name
* in the Exec= parameter.
*/
class screencast_permission_helper_t {
public:
static bool is_permission_system_deactivated() {
return getenvstr("KWIN_WAYLAND_NO_PERMISSION_CHECKS") == "1";
}
static void setup() {
if (initialized) {
return;
}
auto filenameprefix = std::format("{}.kwin", PROJECT_FQDN);
auto executablepath = get_executable_full_path();
// System: Check system XDG applications for permission (usually installed with Sunshine)
if (check_kwin_system_permissions(filenameprefix, executablepath)) {
create_file = false;
initialized = true;
return;
}
// If we do not have a system permission, check if we need a temporary permission via user's application directory
if (is_permission_system_deactivated()) {
BOOST_LOG(info) << "[kwingrab] No permission desktop file necessary. KWin permission system deactivated.";
create_file = false;
initialized = true;
return;
}
// User: Check and (if necessary) update user's XDG applications for permission
auto user_applications = get_xdg_user_applications_path();
if (user_applications.empty()) {
BOOST_LOG(error) << "[kwingrab] Failed to determine user application directory. Cannot continue with permission setup.";
return;
}
// Create non-existing application directory so we can write into it
if (!std::filesystem::exists(user_applications) && !std::filesystem::create_directories(user_applications)) {
// In case of failure log and return
BOOST_LOG(error) << "[kwingrab] Failed to create application directory. Cannot continue with permission setup.";
create_file = false;
initialized = true;
return;
}
auto user_filepathprefix = (std::filesystem::path(user_applications) / filenameprefix).string();
for (const auto &path : std::filesystem::directory_iterator(user_applications)) {
// List existing files for prefix and check if they contain this executable or remove them
const auto entry = path.path().string();
if (entry.starts_with(user_filepathprefix)) {
auto entry_executablepath = get_executable_from_desktop_file(entry);
if (!entry_executablepath.empty() && entry_executablepath == executablepath) {
// This entry is exactly the one we need
BOOST_LOG(debug) << "[kwingrab] Ignoring current temporary KWin wayland permission file: "sv << entry;
create_file = false;
continue;
}
if (!entry_executablepath.empty() && std::filesystem::exists(entry_executablepath)) {
// This entry is for another sunshine executable that still exists
BOOST_LOG(debug) << "[kwingrab] Ignoring other valid temporary KWin wayland permission file: "sv << entry;
continue;
}
if (std::filesystem::remove(path)) {
BOOST_LOG(info) << "[kwingrab] Removed stale temporary KWin wayland permission file: "sv << entry << " executable: "sv << entry_executablepath;
} else {
BOOST_LOG(warning) << "[kwingrab] Failed to remove stale temporary KWin wayland permission file: "sv << entry << " executable: "sv << entry_executablepath;
}
}
}
if (create_file) {
// Generate a unique file identifier based on current unixtime
auto user_filepathidentifier = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1);
auto user_filepath = std::format("{}{}.desktop", user_filepathprefix, user_filepathidentifier);
// Write new file if necessary
std::ofstream filestream(user_filepath);
if (filestream.is_open()) {
filestream << "[Desktop Entry]" << std::endl
<< "Exec=" << executablepath << std::endl
<< "X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1" << std::endl
<< "Type=Application" << std::endl
<< "Name="sv << PROJECT_FQDN << "-kwin-wayland-permission" << std::endl
<< "Comment=Sunshine KWin screencast permission" << std::endl
<< "NoDisplay=true" << std::endl;
filestream.close();
// Give KWin time to catch up to the new desktop file
BOOST_LOG(info) << "[kwingrab] Created temporary KWin wayland permission file: "sv << user_filepath << " - Waiting 3 seconds for KDE to pick up new file.";
std::this_thread::sleep_for(std::chrono::milliseconds(3000));
} else {
BOOST_LOG(warning) << "[kwingrab] Failed to open temporary KWin wayland permission file: "sv << user_filepath;
}
}
initialized = true;
}
static bool is_newly_initialized() {
return create_file;
}
private:
static inline bool initialized = false;
static inline bool create_file = true;
static std::string getenvstr(std::string const &key) {
char const *val = std::getenv(key.c_str());
if (!val) {
return "";
}
return val;
}
static std::filesystem::path get_home_dir() {
// Check HOME environment variable
if (std::string homedir = getenvstr("HOME"); !homedir.empty()) {
return homedir;
}
// Fall back to home directory from NSS passwd
// Note: This should be thread-safe as we're always accessing the same entry for Sunshine
return getpwuid(geteuid())->pw_dir;
}
static std::filesystem::path get_xdg_user_applications_path() {
// Follow the XDG base directory specification for user data home:
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
std::filesystem::path xdg_data_home;
if (std::string dir = getenvstr("XDG_DATA_HOME"); !dir.empty()) {
xdg_data_home = std::filesystem::path(dir);
} else {
const auto homedir = get_home_dir();
if (homedir.empty()) {
return "";
}
xdg_data_home = std::filesystem::path(homedir) / ".local"sv / "share"sv;
}
return xdg_data_home / "applications";
}
static std::string get_executable_full_path() {
// Adapted from https://linuxvox.com/blog/how-do-i-find-the-location-of-the-executable-in-c/
constexpr auto path_len = PATH_MAX; // PATH_MAX is defined in limits.h (e.g., 4096 on Linux)
auto path_exe = std::make_unique<char[]>(path_len);
// Read the symlink /proc/self/exe into path_exe
const ssize_t len = readlink("/proc/self/exe", &path_exe[0], path_len - 1);
if (len == -1) {
return "";
}
// Return path_exe as a proper std::string with len returned by readlink
return std::string(path_exe.get(), len);
}
static std::string get_executable_from_desktop_file(const std::string &path) {
if (std::ifstream file(path); file.is_open()) {
std::string line;
while (std::getline(file, line)) {
if (line.starts_with("Exec=") && line.length() > 5) {
return line.substr(5);
}
}
}
return "";
}
static bool check_kwin_system_permissions(const std::string_view &filenameprefix, const std::string_view &executablepath) {
// Find data dirs to check from XDG_DATA_DIRS
std::vector<std::string> xdg_data_dirs;
if (const std::string e = getenvstr("XDG_DATA_DIRS"); !e.empty()) {
std::stringstream ss(e);
std::string item;
while (getline(ss, item, ':')) { // : is likely valid for all OSes supported, if a constant is available it should be used instead
xdg_data_dirs.push_back(item);
}
}
// Use defaults from https://specifications.freedesktop.org/basedir/latest/ if ENV var was empty
if (xdg_data_dirs.empty()) {
xdg_data_dirs.emplace_back("/usr/local/share/");
xdg_data_dirs.emplace_back("/usr/share/");
}
// Check for ${filenameprefix}.desktop in each directory
for (auto const &dir : xdg_data_dirs) {
std::string filename = std::format("{0}{1}applications{1}{2}.desktop", dir, boost::filesystem::path::preferred_separator, filenameprefix);
if (std::filesystem::exists(filename)) {
auto file_executablepath = get_executable_from_desktop_file(filename);
if (file_executablepath == executablepath) {
BOOST_LOG(info) << "[kwingrab] Found matching system KWin desktop permission file: "sv << filename;
return true;
}
}
}
return false;
}
};
// Output parameters
struct output_parameter_t {
std::string name = "";
int width = 0;
int height = 0;
int pos_x = 0;
int pos_y = 0;
// order is needed to get a sorted output list and should be updated before sorting to have current values
size_t order = SIZE_MAX; // Use high number to keep monitors with uninitialized order value to the back
};
/**
* Wayland KDE ScreenCast session
*
* Owns its own wl_display connection. Binds zkde_screencast_unstable_v1
* and wl_output from the registry, then calls stream_output() to start
* a ScreenCast. Waits for the created(node_id) event from KWin.
*/
class screencast_t {
public:
screencast_t &operator=(screencast_t &&) = delete; // Do not allow to copying
~screencast_t() {
if (kde_screencast_stream_v1_) {
zkde_screencast_stream_unstable_v1_close(kde_screencast_stream_v1_);
kde_screencast_stream_v1_ = nullptr;
}
if (kde_screencast_v1_) {
zkde_screencast_unstable_v1_destroy(kde_screencast_v1_);
kde_screencast_v1_ = nullptr;
}
if (kde_output_order) {
kde_output_order_v1_destroy(kde_output_order);
kde_output_order = nullptr;
}
// wl_output is owned by the registry, released on disconnect
for (const auto &out : outputs | std::views::keys) {
wl_output_destroy(out);
}
outputs.clear();
if (wl_registry) {
wl_registry_destroy(wl_registry);
wl_registry = nullptr;
}
if (wl_display) {
wl_display_disconnect(wl_display);
wl_display = nullptr;
}
}
/**
* @brief Connect to KWin wayland, enumerate outputs.
* @param setup_permissions - Try to setup KWin permissions (default: true)
* @return 0 on success, -1 on failure. On success, node_id and
* output width/height/x/y are populated.
*/
int init(const bool setup_permissions = true) {
if (setup_permissions) {
// Try to set up permissions for zkde_screencast_unstable_v1
screencast_permission_helper_t::setup();
}
const char *wl_name = std::getenv("WAYLAND_DISPLAY");
if (!wl_name) {
BOOST_LOG(error) << "[kwingrab] WAYLAND_DISPLAY not set"sv;
return -1;
}
wl_display = wl_display_connect(wl_name);
if (!wl_display) {
BOOST_LOG(error) << "[kwingrab] cannot connect to Wayland display: "sv << wl_name;
return -1;
}
wl_registry = wl_display_get_registry(wl_display);
wl_registry_add_listener(wl_registry, &registry_listener, this);
wl_display_roundtrip(wl_display);
// We need a second roundtrip after binding outputs to get wl_output events
wl_display_roundtrip(wl_display);
return 0;
}
/**
* @brief Generate a sorted list of known output names.
* @return List of strings with output names to pass to start()
*/
std::vector<std::string> get_output_names() {
std::vector<std::shared_ptr<output_parameter_t>> sorted_outputs;
for (const auto &output_parameter : outputs | std::views::values) {
output_parameter->order = get_order_for_output_name(output_parameter->name);
sorted_outputs.emplace_back(output_parameter);
}
std::ranges::sort(sorted_outputs, [](const auto &a, const auto &b) {
return a->order < b->order || a->pos_x < b->pos_x || a->pos_y < b->pos_y;
});
std::vector<std::string> output_names;
for (const auto &output_parameter : sorted_outputs) {
BOOST_LOG(info) << "[kwingrab] Found output: "sv << output_parameter->name << " order: "sv << output_parameter->order << " position: "sv << output_parameter->pos_x << "x"sv << output_parameter->pos_y << " resolution: "sv << output_parameter->width << "x"sv << output_parameter->height;
output_names.emplace_back(output_parameter->name);
}
return output_names;
}
/**
* @brief Check if KWin is available for potential screencasting
* @return True if KWin is detected
*/
bool kwin_available() const {
// Detect KWin using kde_output_order_v1 extension
if (kde_output_order) {
return true;
}
return false;
}
/**
* @brief Request a screencast stream.
* @param output_name Which wl_output to capture.
* @return 0 on success, -1 on failure. On success, node_id and
* output width/height/x/y are populated.
*/
int start(const std::string_view &output_name) {
// Try find correct output by name
if (outputs.empty()) {
BOOST_LOG(error) << "[kwingrab] no wl_output found"sv;
return -1;
}
struct wl_output *output = nullptr;
if (!output_name.empty()) {
for (auto const &[output_, params_] : outputs) {
if (params_->name == output_name) {
output = output_;
out_params = params_;
}
}
}
// Fall back to first element from the map in case of error
if (!output || !out_params) {
const auto output_ = outputs.begin();
output = output_->first;
out_params = output_->second;
}
// Request a stream for the chosen output with embedded cursor
if (kde_screencast_v1_) {
kde_screencast_stream_v1_ = zkde_screencast_unstable_v1_stream_output(kde_screencast_v1_, output, ZKDE_SCREENCAST_UNSTABLE_V1_POINTER_EMBEDDED);
zkde_screencast_stream_unstable_v1_add_listener(kde_screencast_stream_v1_, &stream_listener, this);
} else {
// No screencast protocol found. Output an error based on newly initialized permission file.
if (screencast_permission_helper_t::is_newly_initialized()) {
BOOST_LOG(error) << "[kwingrab] zkde_screencast_unstable_v1 not found in registry. "sv
"A new permission desktop file was automatically created but might now have been recognized yet. "sv
"Try restarting sunshine or set KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 to fully disable permission checks."sv;
} else {
BOOST_LOG(error) << "[kwingrab] zkde_screencast_unstable_v1 not found in registry. Check permission desktop file "sv
"for sunshine binary or set KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 to fully disable permission checks."sv;
}
return -1;
}
if (wait_for_stream() < 0) {
return -1;
}
if (stream_failed) {
BOOST_LOG(error) << "[kwingrab] stream_output failed: "sv << stream_error_msg;
return -1;
}
// Check for valid node_id and/or object serial values here, stream_ready is just an internal flag
if (out_node_id == PW_ID_ANY && (out_objectserial & SPA_ID_INVALID) == SPA_ID_INVALID) {
BOOST_LOG(error) << "[kwingrab] timeout waiting for created event"sv;
return -1;
}
BOOST_LOG(info) << "[kwingrab] stream created, PipeWire node "sv << out_node_id;
if (out_params->width == 0 || out_params->height == 0) {
BOOST_LOG(error) << "[kwingrab] could not determine output dimensions"sv;
return -1;
}
BOOST_LOG(info) << "[kwingrab] Screencasting output"sv
<< " name "sv << out_params->name
<< " position "sv << out_params->pos_x << "x"sv << out_params->pos_y
<< " resolution "sv << out_params->width << "x"sv << out_params->height;
return 0;
}
uint32_t out_node_id = PW_ID_ANY;
uint64_t out_objectserial = SPA_ID_INVALID;
std::shared_ptr<output_parameter_t> out_params = nullptr;
private:
// Wayland objects
struct wl_display *wl_display = nullptr;
struct wl_registry *wl_registry = nullptr;
struct kde_output_order_v1 *kde_output_order = nullptr;
struct zkde_screencast_unstable_v1 *kde_screencast_v1_ = nullptr;
struct zkde_screencast_stream_unstable_v1 *kde_screencast_stream_v1_ = nullptr;
std::map<struct wl_output *, std::shared_ptr<output_parameter_t>> outputs;
std::vector<std::string> output_order;
bool stream_failed = false;
bool stream_ready = false;
std::string stream_error_msg;
// Misc functions
int wait_for_stream() {
// Dispatch until we get created/failed, with a 5s timeout
auto deadline = std::chrono::steady_clock::now() + 5s;
while (!stream_ready && !stream_failed && std::chrono::steady_clock::now() < deadline) {
wl_display_flush(wl_display);
struct pollfd pfd = {};
pfd.fd = wl_display_get_fd(wl_display);
pfd.events = POLLIN;
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()
);
if (remaining.count() <= 0) {
break;
}
if (poll(&pfd, 1, remaining.count()) > 0 && (pfd.revents & POLLIN) && wl_display_dispatch(wl_display) < 0) {
BOOST_LOG(error) << "[kwingrab] wl_display_dispatch failed"sv;
return -1;
}
}
return 0;
}
size_t get_order_for_output_name(const std::string_view &name) const {
for (size_t i = 0; i < output_order.size(); i++) {
if (output_order[i] == name) {
return i;
}
}
// If nothing matches return list size (to ensure highest order)
return output_order.size();
}
// Registry listener
static void on_registry_global(void *data, struct wl_registry *reg, const uint32_t name, const char *interface, const uint32_t version) {
auto *self = static_cast<screencast_t *>(data);
if (!std::strcmp(interface, kde_output_order_v1_interface.name)) {
// Bind version 1
uint32_t bind_ver = std::min(version, static_cast<uint32_t>(1));
self->kde_output_order = static_cast<struct kde_output_order_v1 *>(
wl_registry_bind(reg, name, &kde_output_order_v1_interface, bind_ver)
);
kde_output_order_v1_add_listener(self->kde_output_order, &output_order_listener, self);
BOOST_LOG(debug) << "[kwingrab] bound kde_output_order_v1 version "sv << bind_ver;
} else if (!std::strcmp(interface, zkde_screencast_unstable_v1_interface.name)) {
// Bind version 1 to 6 — We use stream_output from v1 for node_id (deprecated but good as a fall-back)
// but also try to get the newer (re-use safe) pipewire objectserial from v6
uint32_t bind_ver = std::min(version, static_cast<uint32_t>(6));
self->kde_screencast_v1_ = static_cast<struct zkde_screencast_unstable_v1 *>(
wl_registry_bind(reg, name, &zkde_screencast_unstable_v1_interface, bind_ver)
);
BOOST_LOG(debug) << "[kwingrab] bound zkde_screencast_unstable_v1 version "sv << bind_ver;
} else if (!std::strcmp(interface, wl_output_interface.name)) {
// Bind version 4 - we need wl_output name for matching
uint32_t bind_ver = std::min(version, static_cast<uint32_t>(4));
auto *output = static_cast<struct wl_output *>(
wl_registry_bind(reg, name, &wl_output_interface, bind_ver)
);
const auto [_, inserted] = self->outputs.try_emplace(output, std::make_shared<output_parameter_t>());
if (inserted) {
wl_output_add_listener(output, &output_listener, self);
BOOST_LOG(debug) << "[kwingrab] bound wl_output version "sv << bind_ver << " instance: "sv << output;
} else {
// If we for some odd reason cannot add the output to the map clean it up and log a warning
BOOST_LOG(warning) << "[kwingrab] Ignoring output "sv << output << " because map emplace failed."sv;
wl_output_destroy(output);
}
}
}
static void on_registry_global_remove(void *data [[maybe_unused]], struct wl_registry *reg [[maybe_unused]], uint32_t name [[maybe_unused]]) {
// We don't handle output hot-unplug during init
}
static constexpr struct wl_registry_listener registry_listener = {
.global = on_registry_global,
.global_remove = on_registry_global_remove,
};
// wl_output listener (for mode/dimensions/name)
static void on_output_geometry(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t pw [[maybe_unused]], int32_t ph [[maybe_unused]], int32_t subpixel [[maybe_unused]], const char *make [[maybe_unused]], const char *model [[maybe_unused]], int32_t transform [[maybe_unused]]) {
const auto *self = static_cast<screencast_t *>(data);
const auto output_parameter = self->outputs.at(output);
output_parameter->pos_x = x;
output_parameter->pos_y = y;
}
static void on_output_mode(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh [[maybe_unused]]) {
if (!(flags & WL_OUTPUT_MODE_CURRENT)) {
return;
}
const auto *self = static_cast<screencast_t *>(data);
const auto output_parameter = self->outputs.at(output);
output_parameter->width = width;
output_parameter->height = height;
}
static void on_output_done(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]]) {
// Currently unused
}
static void on_output_scale(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], int32_t factor [[maybe_unused]]) {
// Currently unused
}
static void on_output_name(void *data, struct wl_output *output, const char *name) {
const auto *self = static_cast<screencast_t *>(data);
self->outputs.at(output)->name = name;
}
static void on_output_description(void *data [[maybe_unused]], struct wl_output *output [[maybe_unused]], const char *description [[maybe_unused]]) {
// Currently unused
}
static constexpr struct wl_output_listener output_listener = {
.geometry = on_output_geometry,
.mode = on_output_mode,
.done = on_output_done,
.scale = on_output_scale,
.name = on_output_name,
.description = on_output_description,
};
// Output order listener
static void on_output_order_output(void *data, struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]], const char *output_name) {
auto *self = static_cast<screencast_t *>(data);
self->output_order.emplace_back(output_name);
}
static void on_output_order_done(void *data [[maybe_unused]], struct kde_output_order_v1 *kde_output_order_v1 [[maybe_unused]]) {
// Currently unused
}
static constexpr kde_output_order_v1_listener output_order_listener = {
.output = on_output_order_output,
.done = on_output_order_done,
};
// ScreenCast v1 stream listener
static void on_stream_closed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]]) {
auto *self = static_cast<screencast_t *>(data);
BOOST_LOG(warning) << "[kwingrab] stream closed by server"sv;
self->stream_failed = false;
self->stream_ready = false;
self->stream_error_msg = "stream closed by server";
}
static void on_stream_created(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const uint32_t node) {
auto *self = static_cast<screencast_t *>(data);
self->out_node_id = node;
self->stream_failed = false;
self->stream_ready = true;
BOOST_LOG(debug) << "[kwingrab] created event, node_id="sv << node;
}
static void on_stream_failed(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], const char *err_msg) {
auto *self = static_cast<screencast_t *>(data);
self->stream_failed = true;
self->stream_ready = false;
self->stream_error_msg = err_msg ? err_msg : "unknown error";
BOOST_LOG(error) << "[kwingrab] failed event: "sv << self->stream_error_msg;
}
static void on_stream_serial(void *data, struct zkde_screencast_stream_unstable_v1 *stream [[maybe_unused]], uint32_t object_serial_hi, uint32_t object_serial_low) {
auto *self = static_cast<screencast_t *>(data);
self->out_objectserial = static_cast<uint64_t>(object_serial_hi) << 32 | object_serial_low;
// serial event always preceded the created event with the node id, so we only set stream_ready in created for v1
BOOST_LOG(debug) << "[kwingrab] serial event, objectserial="sv << self->out_objectserial;
}
static constexpr struct zkde_screencast_stream_unstable_v1_listener stream_listener = {
.closed = on_stream_closed,
.created = on_stream_created,
.failed = on_stream_failed,
.serial = on_stream_serial,
};
};
/**
* Display backend
*
* Orchestrates screencast_t and implements pipewire_display_t
*/
class kwin_t: public pipewire::pipewire_display_t {
public:
int configure_stream(const std::string &display_name, int &out_pipewire_fd, uint32_t &out_pipewire_node, uint64_t &out_pipewire_objectserial) override {
screencast = std::make_unique<screencast_t>();
if (screencast->init(true) < 0) {
return -1;
}
if (screencast->start(display_name) < 0) {
return -1;
}
if (screencast->out_params) {
// Return values for pipewire init
out_pipewire_fd = -1; // KWin screencast capture runs on the local pipewire core
out_pipewire_node = screencast->out_node_id;
out_pipewire_objectserial = screencast->out_objectserial;
// Set/update basic stream parameters on display_t
this->offset_x = screencast->out_params->pos_x;
this->offset_y = screencast->out_params->pos_y;
this->width = screencast->out_params->width;
this->height = screencast->out_params->height;
this->logical_width = 0; // Explicitly mark for pipewire_display_t to try to figure this out.
this->logical_height = 0; // Explicitly Mark for pipewire_display_t to try to figure this out.
return 0;
}
return -1;
}
std::unique_ptr<screencast_t> screencast;
};
} // namespace kwin
// Public API for misc.cpp
namespace platf {
std::shared_ptr<display_t> kwin_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (!pipewire::pipewire_display_t::init_pipewire_and_check_hwdevice_type(hwdevice_type)) {
BOOST_LOG(error) << "[kwingrab] Could not initialize pipewire-based display with the given hw device type."sv;
return nullptr;
}
// Drop CAP_SYS_ADMIN so KWin's permission check (if active) can see and match the executable
if (!kwin::screencast_permission_helper_t::is_permission_system_deactivated() && has_elevated_privileges()) {
drop_elevated_privileges();
}
auto display = std::make_shared<kwin::kwin_t>();
if (display->init(hwdevice_type, display_name, config)) {
return nullptr;
}
return display;
}
std::vector<std::string> kwin_display_names() {
if (!kwin::screencast_permission_helper_t::is_permission_system_deactivated() && has_elevated_privileges()) {
// We're still in the probing phase of Sunshine startup. Dropping portal security early will break KMS.
// Just return a dummy screen for now. Display re-enumeration after encoder probing will yield full result.
std::vector<std::string> display_names;
display_names.emplace_back("");
return display_names;
}
const auto screencast = std::make_unique<kwin::screencast_t>();
if (screencast->init() < 0) {
return {};
}
return screencast->get_output_names();
}
bool kwin_available() {
// Init screencast without permission setup (to not cause unneeded logs / temporary desktop files) and check KWin availability
if (const auto screencast = std::make_unique<kwin::screencast_t>(); screencast->init(false) < 0 || !screencast->kwin_available()) {
return false;
}
return true;
}
} // namespace platf
+30
View File
@@ -963,6 +963,9 @@ namespace platf {
#ifdef SUNSHINE_BUILD_X11
X11, ///< X11
#endif
#ifdef SUNSHINE_BUILD_KWIN
KWIN, ///< KWin ScreenCast
#endif
#ifdef SUNSHINE_BUILD_PORTAL
PORTAL, ///< XDG PORTAL
#endif
@@ -1017,6 +1020,17 @@ namespace platf {
}
#endif
#ifdef SUNSHINE_BUILD_KWIN
bool kwin_available();
std::vector<std::string> kwin_display_names();
std::shared_ptr<display_t> kwin_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);
bool verify_kwin() {
// Note: The separate kwin_available check is necessary because with CAP_SYS_ADMIN kwin_display_names is never empty during startup
return window_system == window_system_e::WAYLAND && kwin_available() && !kwin_display_names().empty();
}
#endif
std::vector<std::string> display_names(mem_type_e hwdevice_type) {
#ifdef SUNSHINE_BUILD_CUDA
// display using NvFBC only supports mem_type_e::cuda
@@ -1043,6 +1057,11 @@ namespace platf {
if (sources[source::PORTAL]) {
return portal_display_names();
}
#endif
#ifdef SUNSHINE_BUILD_KWIN
if (sources[source::KWIN]) {
return kwin_display_names();
}
#endif
return {};
}
@@ -1087,6 +1106,12 @@ namespace platf {
return portal_display(hwdevice_type, display_name, config);
}
#endif
#ifdef SUNSHINE_BUILD_KWIN
if (sources[source::KWIN]) {
BOOST_LOG(info) << "Screencasting with KWin ScreenCast"sv;
return kwin_display(hwdevice_type, display_name, config);
}
#endif
return nullptr;
}
@@ -1145,6 +1170,11 @@ namespace platf {
sources[source::PORTAL] = true;
}
#endif
#ifdef SUNSHINE_BUILD_KWIN
if (((config::video.capture.empty() && sources.none()) || config::video.capture == "kwin") && verify_kwin()) {
sources[source::KWIN] = true;
}
#endif
if (sources.none()) {
BOOST_LOG(error) << "Unable to initialize capture method"sv;
@@ -74,6 +74,7 @@ const config = ref(props.config)
<option value="wlr">wlroots</option>
<option value="kms">KMS</option>
<option value="x11">X11</option>
<option value="kwin">KWin Screencast</option>
<option value="portal">XDG Portal</option>
</template>
<template #windows>