From 4b23f4d4e8035bbb63d8fb2ae84571e0dcdbc1d4 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 00:59:26 +0200 Subject: [PATCH 1/7] First version --- cmake/compile_definitions/linux.cmake | 10 +- cmake/packaging/linux.cmake | 7 +- src/config.cpp | 4 + src/config.h | 2 + src/platform/linux/vdisplay_helper.cpp | 156 +++ src/platform/linux/virtual_display.cpp | 965 ++++++++++++++++++ src/platform/linux/virtual_display.h | 40 + src/process.cpp | 126 ++- src/process.h | 13 +- .../assets/web/configs/tabs/AudioVideo.vue | 20 + .../assets/web/public/assets/locale/en.json | 5 + src_assets/linux/misc/postinst | 35 + src_assets/linux/misc/sunshine-sysusers.conf | 1 + 13 files changed, 1333 insertions(+), 51 deletions(-) create mode 100644 src/platform/linux/vdisplay_helper.cpp create mode 100644 src/platform/linux/virtual_display.cpp create mode 100644 src/platform/linux/virtual_display.h create mode 100644 src_assets/linux/misc/sunshine-sysusers.conf diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 1130ed9750..7cd04c9cee 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -94,8 +94,16 @@ if(LIBDRM_FOUND AND LIBCAP_FOUND) include_directories(SYSTEM ${LIBCAP_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${LIBCAP_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES - "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp") + "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/virtual_display.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/virtual_display.cpp") list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) + + # Privileged helper for virtual display debugfs writes (cap_dac_override) + add_executable(sunshine-vdisplay-helper + "${CMAKE_SOURCE_DIR}/src/platform/linux/vdisplay_helper.cpp") + set_target_properties(sunshine-vdisplay-helper PROPERTIES CXX_STANDARD 17) + install(TARGETS sunshine-vdisplay-helper RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") endif() # evdev diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index e59663bf97..267ec8309b 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -31,6 +31,9 @@ else() DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/sunshine-sysusers.conf" + DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/sysusers.d" + RENAME "sunshine.conf") endif() endif() @@ -40,7 +43,9 @@ set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc # Apply setcap for RPM # https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071 -set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}") +set(CPACK_RPM_USER_FILELIST + "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}" + "%attr(0750,root,sunshine) %caps(cap_dac_override+ep) %{_bindir}/sunshine-vdisplay-helper") # Dependencies set(CPACK_DEB_COMPONENT_INSTALL ON) diff --git a/src/config.cpp b/src/config.cpp index 6892e3029c..f08849bff3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -516,6 +516,8 @@ namespace config { "1920x1080x60", // fallback_mode false, // isolated Display false, // ignore_encoder_probe_failure + false, // linux_virtual_display_experimental + {}, // vdisplay_connector }; audio_t audio { @@ -1209,6 +1211,8 @@ namespace config { string_f(vars, "fallback_mode", video.fallback_mode); bool_f(vars, "isolated_virtual_display_option", video.isolated_virtual_display_option); bool_f(vars, "ignore_encoder_probe_failure", video.ignore_encoder_probe_failure); + bool_f(vars, "linux_virtual_display_experimental", video.linux_virtual_display_experimental); + string_f(vars, "vdisplay_connector", video.vdisplay_connector); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); diff --git a/src/config.h b/src/config.h index a6d7c83386..77a3989308 100644 --- a/src/config.h +++ b/src/config.h @@ -149,6 +149,8 @@ namespace config { std::string fallback_mode; bool isolated_virtual_display_option; bool ignore_encoder_probe_failure; + bool linux_virtual_display_experimental; + std::string vdisplay_connector; }; struct audio_t { diff --git a/src/platform/linux/vdisplay_helper.cpp b/src/platform/linux/vdisplay_helper.cpp new file mode 100644 index 0000000000..6f2415ed74 --- /dev/null +++ b/src/platform/linux/vdisplay_helper.cpp @@ -0,0 +1,156 @@ +/** + * @file src/platform/linux/vdisplay_helper.cpp + * @brief Privileged helper for virtual display debugfs/sysfs writes. + * + * Installed with cap_dac_override+ep so the main sunshine process + * does not need CAP_DAC_OVERRIDE in its capability set. + */ +#include +#include +#include +#include +#include +#include +#include + +static bool + validate_path(const char *path) { + if (strcmp(path, "/sys/kernel/debug/dri") == 0) { + return true; + } + + static const char *patterns[] = { + "^/sys/kernel/debug/dri/[0-9]+/[A-Za-z]+(-[A-Za-z]+)*-[0-9]+/edid_override$", + "^/sys/kernel/debug/dri/[0-9]+/[A-Za-z]+(-[A-Za-z]+)*-[0-9]+/trigger_hotplug$", + "^/sys/class/drm/card[0-9]+-[A-Za-z]+(-[A-Za-z]+)*-[0-9]+/status$", + }; + + for (auto pattern : patterns) { + regex_t re; + if (regcomp(&re, pattern, REG_EXTENDED | REG_NOSUB) != 0) { + continue; + } + int match = regexec(&re, path, 0, nullptr, 0); + regfree(&re); + if (match == 0) { + return true; + } + } + + return false; +} + +static int + safe_open(const char *path) { + return open(path, O_WRONLY | O_NOFOLLOW); +} + +static int + cmd_write_edid(const char *path) { + uint8_t buf[256]; + ssize_t total = 0; + while (total < static_cast(sizeof(buf))) { + ssize_t n = read(STDIN_FILENO, buf + total, sizeof(buf) - total); + if (n <= 0) { + break; + } + total += n; + } + if (total != 128 && total != 256) { + fprintf(stderr, "Invalid EDID size: %zd\n", total); + return 3; + } + int fd = safe_open(path); + if (fd < 0) { + perror("open"); + return 4; + } + ssize_t written = write(fd, buf, total); + close(fd); + if (written != total) { + perror("write"); + return 5; + } + return 0; +} + +static int + cmd_clear_edid(const char *path) { + int fd = safe_open(path); + if (fd < 0) { + perror("open"); + return 4; + } + ssize_t n = write(fd, "reset", 5); + if (n < 0) { + lseek(fd, 0, SEEK_SET); + write(fd, "", 0); + } + close(fd); + return 0; +} + +static int + cmd_write_string(const char *path, const char *value) { + int fd = safe_open(path); + if (fd < 0) { + perror("open"); + return 4; + } + ssize_t n = write(fd, value, strlen(value)); + close(fd); + if (n < 0) { + perror("write"); + return 5; + } + return 0; +} + +int main(int argc, char *argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage: %s [value]\n", argv[0]); + return 1; + } + + const char *command = argv[1]; + const char *path = argv[2]; + + if (!validate_path(path)) { + fprintf(stderr, "Path rejected: %s\n", path); + return 2; + } + + if (strcmp(command, "write-edid") == 0) { + return cmd_write_edid(path); + } + if (strcmp(command, "clear-edid") == 0) { + return cmd_clear_edid(path); + } + if (strcmp(command, "write-string") == 0) { + if (argc < 4) { + fprintf(stderr, "write-string requires a value argument\n"); + return 1; + } + const char *value = argv[3]; + static const char *allowed_values[] = {"1", "on", "off", "detect"}; + bool valid = false; + for (auto v : allowed_values) { + if (strcmp(value, v) == 0) { + valid = true; + break; + } + } + if (!valid) { + fprintf(stderr, "Value rejected: %s\n", value); + return 2; + } + return cmd_write_string(path, value); + } + if (strcmp(command, "check-path") == 0) { + struct stat st; + return stat(path, &st) == 0 ? 0 : 1; + } + + fprintf(stderr, "Unknown command: %s\n", command); + return 1; +} diff --git a/src/platform/linux/virtual_display.cpp b/src/platform/linux/virtual_display.cpp new file mode 100644 index 0000000000..6a0f95c7af --- /dev/null +++ b/src/platform/linux/virtual_display.cpp @@ -0,0 +1,965 @@ +/** + * @file src/platform/linux/virtual_display.cpp + * @brief [Experimental] Linux virtual display via DRM EDID override. + */ +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// lib includes +#include +#include +#include +#include +#include + +// platform includes +#include +#include + +// local includes +#include "misc.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/utility.h" +#include "virtual_display.h" + +using namespace std::literals; +namespace bp = boost::process::v1; +namespace fs = std::filesystem; + +namespace linux_vdisplay { + + struct cvt_timing_t { + uint32_t pixel_clock_khz; + uint16_t h_active, h_blanking, h_front_porch, h_sync_width; + uint16_t v_active, v_blanking, v_front_porch, v_sync_width; + uint16_t h_total, v_total; + }; + + // CVT spec according to VESA for vsync width + static int + cvt_vsync_width(int width, int height) { + int g = std::gcd(width, height); + int aw = width / g, ah = height / g; + if (aw == 4 && ah == 3) { + return 4; + } + if (aw == 16 && ah == 9) { + return 5; + } + if (aw == 16 && ah == 10) { + return 6; + } + if (aw == 5 && ah == 4) { + return 7; + } + if (aw == 15 && ah == 9) { + return 7; + } + return 10; + } + + static cvt_timing_t + calculate_cvt_rb(int width, int height, int refresh_hz) { + // Fixed constants from VESA CVT Reduced Blanking v1 spec + constexpr int RB_H_BLANK = 160; + constexpr int RB_H_SYNC = 32; + constexpr int RB_H_FRONT_PORCH = 48; + constexpr int RB_V_FRONT_PORCH = 3; + constexpr double RB_MIN_VBLANK_US = 460.0; + + cvt_timing_t t {}; + t.h_active = static_cast(width & ~7); // EDID DTD requires multiple of 8 + t.h_blanking = RB_H_BLANK; + t.h_front_porch = RB_H_FRONT_PORCH; + t.h_sync_width = RB_H_SYNC; + t.h_total = t.h_active + RB_H_BLANK; + + t.v_active = static_cast(height); + t.v_front_porch = RB_V_FRONT_PORCH; + t.v_sync_width = static_cast(cvt_vsync_width(width, height)); + + double h_period_us = (1e6 / refresh_hz - RB_MIN_VBLANK_US) / (height + RB_V_FRONT_PORCH); + int vbi_lines = static_cast(std::ceil(RB_MIN_VBLANK_US / h_period_us)) + 1; + int min_vbi = t.v_sync_width + t.v_front_porch + 1; + if (vbi_lines < min_vbi) { + vbi_lines = min_vbi; + } + + t.v_blanking = static_cast(vbi_lines); + t.v_total = t.v_active + t.v_blanking; + t.pixel_clock_khz = static_cast( + static_cast(t.h_total) * t.v_total * refresh_hz / 1000 + ); + + return t; + } + + // Pack timing into EDID 1.4 Detailed Timing Descriptor (18 bytes) + static void + build_dtd(uint8_t dtd[18], const cvt_timing_t &t, int h_mm, int v_mm) { + uint16_t pclk_10khz = static_cast(t.pixel_clock_khz / 10); + dtd[0] = pclk_10khz & 0xFF; + dtd[1] = (pclk_10khz >> 8) & 0xFF; + dtd[2] = t.h_active & 0xFF; + dtd[3] = t.h_blanking & 0xFF; + dtd[4] = static_cast(((t.h_active >> 8) & 0x0F) << 4 | ((t.h_blanking >> 8) & 0x0F)); + dtd[5] = t.v_active & 0xFF; + dtd[6] = t.v_blanking & 0xFF; + dtd[7] = static_cast(((t.v_active >> 8) & 0x0F) << 4 | ((t.v_blanking >> 8) & 0x0F)); + dtd[8] = t.h_front_porch & 0xFF; + dtd[9] = t.h_sync_width & 0xFF; + dtd[10] = static_cast(((t.v_front_porch & 0x0F) << 4) | (t.v_sync_width & 0x0F)); + dtd[11] = static_cast( + ((t.h_front_porch >> 8) & 0x03) << 6 | + ((t.h_sync_width >> 8) & 0x03) << 4 | + ((t.v_front_porch >> 4) & 0x03) << 2 | + ((t.v_sync_width >> 4) & 0x03) + ); + dtd[12] = h_mm & 0xFF; + dtd[13] = v_mm & 0xFF; + dtd[14] = static_cast(((h_mm >> 8) & 0x0F) << 4 | ((v_mm >> 8) & 0x0F)); + dtd[15] = 0; + dtd[16] = 0; + // Non-interlaced, digital separate, H+ V- (CVT-RB) + dtd[17] = 0x1A; + } + + static uint8_t + edid_checksum(const uint8_t *block, size_t len) { + uint8_t sum = 0; + for (size_t i = 0; i < len - 1; i++) { + sum += block[i]; + } + return static_cast((256 - sum) % 256); + } + + static void + build_monitor_name(uint8_t desc[18], int width, int height) { + desc[0] = desc[1] = desc[2] = 0x00; + desc[3] = 0xFC; // EDID descriptor tag: monitor name + desc[4] = 0x00; + + char name[14]; + int n = snprintf(name, sizeof(name), "APL %dx%d", width, height); + if (n < 0 || n >= 14) { + snprintf(name, sizeof(name), "APL Virtual"); + } + + int i = 0; + for (; i < 13 && name[i]; i++) { + desc[5 + i] = static_cast(name[i]); + } + if (i < 13) { + desc[5 + i] = 0x0A; // EDID requires LF terminator after name + i++; + } + for (; i < 13; i++) { + desc[5 + i] = 0x20; // pad with spaces per EDID spec + } + } + + static void + build_range_limits(uint8_t desc[18], int min_v, int max_v, int max_pclk_mhz, const cvt_timing_t &t) { + desc[0] = desc[1] = desc[2] = 0x00; + desc[3] = 0xFD; // EDID descriptor tag: monitor range limits + desc[4] = 0x00; + desc[5] = static_cast(std::clamp(min_v, 1, 255)); + desc[6] = static_cast(std::clamp(max_v, 1, 255)); + + int h_rate_khz = static_cast(t.pixel_clock_khz / t.h_total); + desc[7] = static_cast(std::clamp(h_rate_khz - 1, 1, 255)); + desc[8] = static_cast(std::clamp(h_rate_khz + 1, 1, 255)); + + desc[9] = static_cast(std::clamp((max_pclk_mhz + 9) / 10, 1, 255)); + desc[10] = 0x01; // default GTF timing type + desc[11] = 0x0A; // LF terminator + for (int i = 12; i < 18; i++) { + desc[i] = 0x20; // space padding + } + } + + // We need to give each client a unique hash ID for the EDID serial. + // This lets compositors actually remember the vdisplay settings per client. + // Uses FNV-1a. + static uint32_t + hash_client_id(const std::string &client_id) { + uint32_t hash = 0x811c9dc5; + for (auto c : client_id) { + hash ^= static_cast(c); + hash *= 0x01000193; + } + return hash; + } + + static std::array + generate_edid(int width, int height, int refresh_hz, const std::string &client_id) { + std::array edid {}; + + auto t = calculate_cvt_rb(width, height, refresh_hz); + + // DTD pixel clock field is 16-bit (units of 10 kHz), clamp refresh if it overflows + uint32_t pclk_10khz = t.pixel_clock_khz / 10; + if (pclk_10khz > 65535) { + int max_refresh = static_cast(65535ULL * 10000 / (static_cast(t.h_total) * t.v_total)); + BOOST_LOG(warning) << "[Experimental] Pixel clock overflow for "sv << width << "x"sv << height + << "@"sv << refresh_hz << "Hz, clamping to "sv << max_refresh << "Hz"sv; + refresh_hz = max_refresh; + t = calculate_cvt_rb(width, height, refresh_hz); + } + + // Physical size in mm since EDID must report it. + int h_mm = static_cast(width * 25.4 / 96.0 + 0.5); + int v_mm = static_cast(height * 25.4 / 96.0 + 0.5); + + // --- Base block (bytes 0-127) --- + // Header + edid[0] = 0x00; + edid[1] = edid[2] = edid[3] = edid[4] = edid[5] = edid[6] = 0xFF; + edid[7] = 0x00; + + // Manufacturer "APL": A=1, P=16, L=12 → ((1<<10)|(16<<5)|12) = 0x060C + edid[8] = 0x06; + edid[9] = 0x0C; + // Product code + edid[10] = 0x01; + edid[11] = 0x00; + // Serial — hash of client ID for per-client display identity + uint32_t serial = client_id.empty() ? 1 : hash_client_id(client_id); + edid[12] = static_cast(serial & 0xFF); + edid[13] = static_cast((serial >> 8) & 0xFF); + edid[14] = static_cast((serial >> 16) & 0xFF); + edid[15] = static_cast((serial >> 24) & 0xFF); + // Week 1, year 2024 (2024-1990=34) + edid[16] = 0x01; + edid[17] = 0x22; + // EDID 1.4 + edid[18] = 0x01; + edid[19] = 0x04; + // Digital, 8-bit, DisplayPort + edid[20] = 0xA5; + // Physical size in cm + edid[21] = static_cast(std::clamp(h_mm / 10, 1, 255)); + edid[22] = static_cast(std::clamp(v_mm / 10, 1, 255)); + // Gamma 2.2 — encoded as (2.2 * 100) - 100 = 120 = 0x78 + edid[23] = 0x78; + // Features bitmap: RGB color, preferred timing is native, continuous frequency support + edid[24] = 0x3B; + + // Standard sRGB chromaticity coordinates (CIE 1931 color primaries) + static constexpr uint8_t SRGB_CHROMA[] = { + 0x0C, + 0xC1, + 0xAF, + 0x4F, + 0x40, + 0xAB, + 0x25, + 0x14, + 0x50, + 0x54 + }; + std::copy(std::begin(SRGB_CHROMA), std::end(SRGB_CHROMA), &edid[25]); + + // Established timings — none + edid[35] = edid[36] = edid[37] = 0x00; + + // Standard timings — 0x0101 is the EDID "unused slot" marker + for (int i = 38; i < 54; i += 2) { + edid[i] = 0x01; + edid[i + 1] = 0x01; + } + + // DTD1: preferred mode (target resolution) + build_dtd(&edid[54], t, h_mm, v_mm); + + // DTD2: 1080p@60 fallback (if primary isn't already 1080p@60) + if (width != 1920 || height != 1080 || refresh_hz != 60) { + auto t_fallback = calculate_cvt_rb(1920, 1080, 60); + int fb_h_mm = static_cast(1920 * 25.4 / 96.0 + 0.5); + int fb_v_mm = static_cast(1080 * 25.4 / 96.0 + 0.5); + build_dtd(&edid[72], t_fallback, fb_h_mm, fb_v_mm); + } else { + // Serial number descriptor as placeholder + edid[72] = edid[73] = edid[74] = 0x00; + edid[75] = 0xFF; + edid[76] = 0x00; + edid[77] = '0'; + edid[78] = '0'; + edid[79] = '0'; + edid[80] = '1'; + edid[81] = 0x0A; + for (int i = 82; i < 90; i++) { + edid[i] = 0x20; + } + } + + // Monitor name + build_monitor_name(&edid[90], width, height); + + // Range limits + int min_v = std::min(refresh_hz - 1, 24); + int max_v = std::max(refresh_hz, 144); + int max_pclk_mhz = static_cast(t.pixel_clock_khz / 1000) + 10; + build_range_limits(&edid[108], min_v, max_v, max_pclk_mhz, t); + + // Extension count + edid[126] = 0x01; + // Checksum + edid[127] = edid_checksum(&edid[0], 128); + + // --- CTA extension block (bytes 128-255) --- + edid[128] = 0x02; // CTA extension tag + edid[129] = 0x03; // CTA revision 3 + + // Data blocks start at byte 132 (offset 4 within extension) + int cta_offset = 4; + + // Video Data Block: tag 0x40 | length + // VIC 16 = 1080p60, VIC 4 = 720p60, VIC 1 = 640x480p60 + edid[128 + cta_offset] = 0x43; // tag 2, length 3 + edid[128 + cta_offset + 1] = 0x90; // VIC 16 with native bit set + edid[128 + cta_offset + 2] = 0x04; // VIC 4 + edid[128 + cta_offset + 3] = 0x01; // VIC 1 + cta_offset += 4; + + // Audio Data Block: 2ch LPCM, 48kHz, 16-bit + edid[128 + cta_offset] = 0x23; // tag 1, length 3 + edid[128 + cta_offset + 1] = 0x09; // LPCM, 2 channels + edid[128 + cta_offset + 2] = 0x07; // 32/44.1/48 kHz + edid[128 + cta_offset + 3] = 0x01; // 16-bit + cta_offset += 4; + + // Speaker Allocation: FL + FR + edid[128 + cta_offset] = 0x83; // tag 4, length 3 + edid[128 + cta_offset + 1] = 0x01; // FL + FR + edid[128 + cta_offset + 2] = 0x00; + edid[128 + cta_offset + 3] = 0x00; + cta_offset += 4; + + // DTD offset in CTA block + edid[130] = static_cast(cta_offset); + // CTA features: underscan supported (0x80) + basic audio (0x40), no YCbCr + edid[131] = 0xC0; + + // DTD in CTA block: same as primary + if (cta_offset + 18 <= 127) { + build_dtd(&edid[128 + cta_offset], t, h_mm, v_mm); + cta_offset += 18; + } + + // Pad remainder with zeros (already zero from array init) + + // CTA checksum + edid[255] = edid_checksum(&edid[128], 128); + + return edid; + } + + struct active_state_t { + connector_info_t connector; + bool edid_written = false; + bool reprobed = false; + }; + + static std::optional active_state; + + using version_ptr = util::safe_ptr; + using res_ptr = util::safe_ptr; + using conn_ptr = util::safe_ptr; + using enc_ptr = util::safe_ptr; + + static std::string + connector_type_name(std::uint32_t type) { + switch (type) { + case DRM_MODE_CONNECTOR_VGA: + return "VGA"; + case DRM_MODE_CONNECTOR_DVII: + return "DVI-I"; + case DRM_MODE_CONNECTOR_DVID: + return "DVI-D"; + case DRM_MODE_CONNECTOR_DVIA: + return "DVI-A"; + case DRM_MODE_CONNECTOR_Composite: + return "Composite"; + case DRM_MODE_CONNECTOR_SVIDEO: + return "SVIDEO"; + case DRM_MODE_CONNECTOR_LVDS: + return "LVDS"; + case DRM_MODE_CONNECTOR_Component: + return "Component"; + case DRM_MODE_CONNECTOR_9PinDIN: + return "DIN"; + case DRM_MODE_CONNECTOR_DisplayPort: + return "DP"; + case DRM_MODE_CONNECTOR_HDMIA: + return "HDMI-A"; + case DRM_MODE_CONNECTOR_HDMIB: + return "HDMI-B"; + case DRM_MODE_CONNECTOR_TV: + return "TV"; + case DRM_MODE_CONNECTOR_eDP: + return "eDP"; + case DRM_MODE_CONNECTOR_VIRTUAL: + return "Virtual"; + case DRM_MODE_CONNECTOR_DSI: + return "DSI"; + case DRM_MODE_CONNECTOR_DPI: + return "DPI"; + case DRM_MODE_CONNECTOR_WRITEBACK: + return "Writeback"; + case DRM_MODE_CONNECTOR_SPI: + return "SPI"; +#ifdef DRM_MODE_CONNECTOR_USB + case DRM_MODE_CONNECTOR_USB: + return "USB"; +#endif + default: + return "Unknown" + std::to_string(type); + } + } + + static bool + card_is_nvidia(int fd) { + version_ptr ver {drmGetVersion(fd)}; + return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0; + } + + // Arbitrary but deterministic ordering for auto-selection when multiple connectors are available + static int + connector_type_priority(std::uint32_t type) { + switch (type) { + case DRM_MODE_CONNECTOR_HDMIA: + return 0; + case DRM_MODE_CONNECTOR_DisplayPort: + return 1; + case DRM_MODE_CONNECTOR_HDMIB: + return 2; + case DRM_MODE_CONNECTOR_DVID: + return 3; + case DRM_MODE_CONNECTOR_DVII: + return 4; + case DRM_MODE_CONNECTOR_VGA: + return 5; + default: + return 99; + } + } + + static std::vector + find_disconnected_connectors() { + std::vector result; + + if (!fs::exists("/dev/dri"sv)) { + return result; + } + + for (auto &entry : fs::directory_iterator {"/dev/dri"sv}) { + auto filename = entry.path().filename().string(); + if (filename.substr(0, 4) != "card"sv) { + continue; + } + + int card_index = -1; + try { + card_index = std::stoi(filename.substr(4)); + } catch (...) { + continue; + } + + int fd = open(entry.path().c_str(), O_RDWR); + if (fd < 0) { + continue; + } + + auto close_fd = util::fail_guard([fd]() { + close(fd); + }); + + // Nvidia not tested but may have issues with EDID override + NVKMS, so log a warning if detected + if (card_is_nvidia(fd)) { + BOOST_LOG(warning) << "[Experimental] NVIDIA card detected ("sv << filename << ") — EDID override may cause issues"sv; + } + + res_ptr resources {drmModeGetResources(fd)}; + if (!resources) { + continue; + } + + // Track per-type index to match kernel sysfs naming + std::map type_count; + + for (int i = 0; i < resources->count_connectors; i++) { + conn_ptr conn {drmModeGetConnector(fd, resources->connectors[i])}; + if (!conn) { + continue; + } + + // Increment before the connected check — kernel numbers ALL connectors of a type, not just disconnected ones + auto index = ++type_count[conn->connector_type]; + + if (conn->connection == DRM_MODE_CONNECTED) { + continue; + } + + auto type_name = connector_type_name(conn->connector_type); + auto connector_name = type_name + "-" + std::to_string(index); + auto sysfs_name = filename + "-" + connector_name; + + auto debugfs_dir = "/sys/kernel/debug/dri/"s + std::to_string(card_index) + "/" + connector_name; + auto debugfs_path = debugfs_dir + "/edid_override"; + auto hotplug_path = debugfs_dir + "/trigger_hotplug"; + auto status_path = "/sys/class/drm/"s + sysfs_name + "/status"; + + std::error_code ec; + if (!fs::exists(status_path, ec) || ec) { + BOOST_LOG(debug) << "[Experimental] sysfs path missing: "sv << status_path; + continue; + } + + result.push_back(connector_info_t { + entry.path().string(), + card_index, + conn->connector_id, + conn->connector_type, + index, + sysfs_name, + debugfs_path, + status_path, + hotplug_path, + }); + + BOOST_LOG(debug) << "[Experimental] Found disconnected connector: "sv << sysfs_name; + } + } + + return result; + } + + static connector_info_t + select_best(const std::vector &candidates) { + auto best = candidates.begin(); + for (auto it = candidates.begin(); it != candidates.end(); ++it) { + if (connector_type_priority(it->connector_type) < connector_type_priority(best->connector_type)) { + best = it; + } + } + return *best; + } + + // Prefer helper binary next to our own executable, fall back to PATH lookup + static boost::filesystem::path + find_helper() { + std::error_code ec; + auto self = fs::read_symlink("/proc/self/exe", ec); + if (!ec) { + auto sibling = self.parent_path() / "sunshine-vdisplay-helper"; + if (fs::exists(sibling, ec)) { + return boost::filesystem::path(sibling.string()); + } + } + return bp::search_path("sunshine-vdisplay-helper"); + } + + // Instead of doing debugfs edits here which would require cap_dac_override caps + // for the entire process we offload this to a helper bin. + // + // See vdisplay_helper.cpp + static bool + run_helper(const std::vector &args, const uint8_t *stdin_data = nullptr, size_t stdin_len = 0) { + auto helper = find_helper(); + if (helper.empty()) { + BOOST_LOG(error) << "[Experimental] sunshine-vdisplay-helper not found"sv; + return false; + } + + try { + if (stdin_data && stdin_len > 0) { + bp::pipe stdin_pipe; + bp::child proc(helper, bp::args(args), bp::std_in bp::null, bp::std_err > bp::null); + stdin_pipe.write(reinterpret_cast(stdin_data), static_cast(stdin_len)); + stdin_pipe.close(); + proc.wait(); + return proc.exit_code() == 0; + } + + bp::child proc(helper, bp::args(args), bp::std_in bp::null, bp::std_err > bp::null); + proc.wait(); + return proc.exit_code() == 0; + } catch (const std::exception &e) { + BOOST_LOG(error) << "[Experimental] Helper exec failed: "sv << e.what(); + return false; + } + } + + static bool + helper_check_path(const std::string &path) { + return run_helper({"check-path", path}); + } + + static bool + write_edid(const connector_info_t &conn, const uint8_t *edid_data, size_t edid_len) { + if (!run_helper({"write-edid", conn.debugfs_edid_path}, edid_data, edid_len)) { + BOOST_LOG(error) << "[Experimental] Helper failed to write EDID to "sv << conn.debugfs_edid_path; + return false; + } + BOOST_LOG(info) << "[Experimental] EDID written to "sv << conn.debugfs_edid_path; + return true; + } + + static bool + clear_edid(const connector_info_t &conn) { + if (!run_helper({"clear-edid", conn.debugfs_edid_path})) { + BOOST_LOG(warning) << "[Experimental] Helper failed to clear EDID on "sv << conn.sysfs_name; + return false; + } + BOOST_LOG(info) << "[Experimental] EDID override cleared on "sv << conn.sysfs_name; + return true; + } + + static bool + force_status(const connector_info_t &conn, const char *value) { + if (!run_helper({"write-string", conn.sysfs_status_path, value})) { + BOOST_LOG(error) << "[Experimental] Helper failed to set status on "sv << conn.sysfs_name; + return false; + } + BOOST_LOG(info) << "[Experimental] Connector status set to '"sv << value << "' on "sv << conn.sysfs_name; + return true; + } + + static bool + trigger_hotplug(const connector_info_t &conn) { + if (conn.debugfs_hotplug_path.empty()) { + return false; + } + if (!run_helper({"write-string", conn.debugfs_hotplug_path, "1"})) { + BOOST_LOG(warning) << "[Experimental] Helper failed to trigger hotplug on "sv << conn.sysfs_name; + return false; + } + BOOST_LOG(info) << "[Experimental] Hotplug triggered on "sv << conn.sysfs_name; + return true; + } + + // Poll until the compositor assigns a CRTC to the connector (meaning it recognized the new display) + static bool + wait_for_connector_active(const connector_info_t &conn, int timeout_ms) { + int fd = open(conn.card_path.c_str(), O_RDWR); + if (fd < 0) { + return false; + } + auto close_fd = util::fail_guard([fd]() { + close(fd); + }); + + constexpr int poll_interval_ms = 200; + int elapsed = 0; + + while (elapsed < timeout_ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(poll_interval_ms)); + elapsed += poll_interval_ms; + + conn_ptr drm_conn {drmModeGetConnector(fd, conn.connector_id)}; + if (!drm_conn) { + continue; + } + + if (drm_conn->connection != DRM_MODE_CONNECTED) { + BOOST_LOG(debug) << "[Experimental] Waiting for connector... ("sv << elapsed << "ms)"sv; + continue; + } + + // Connected — check if a CRTC is assigned (compositor picked it up) + if (drm_conn->encoder_id) { + enc_ptr enc {drmModeGetEncoder(fd, drm_conn->encoder_id)}; + if (enc && enc->crtc_id) { + BOOST_LOG(info) << "[Experimental] Connector active with CRTC after "sv << elapsed << "ms"sv; + return true; + } + } + + // Connected but no CRTC yet — compositor may still be processing + BOOST_LOG(debug) << "[Experimental] Connector connected, waiting for CRTC assignment... ("sv << elapsed << "ms)"sv; + } + + BOOST_LOG(warning) << "[Experimental] Timed out waiting for compositor to pick up virtual display ("sv << timeout_ms << "ms)"sv; + return false; + } + + // X11 may needs an explicit xrandr call to activate the new output; Wayland compositors do it automatically (untested) + static void + configure_xrandr(const connector_info_t &conn) { + auto type_name = connector_type_name(conn.connector_type); + auto output_name = type_name + "-" + std::to_string(conn.connector_index); + + auto cmd = "xrandr --output "s + output_name + " --auto 2>&1"; + BOOST_LOG(info) << "[Experimental] Configuring X11 output: "sv << cmd; + + auto ret = system(cmd.c_str()); + if (ret != 0) { + BOOST_LOG(warning) << "[Experimental] xrandr returned "sv << ret; + } + } + + // Walk DRM planes the same way kmsgrab does so the index we return matches what it captures + static int + find_kms_index(const connector_info_t &conn) { + int fd = open(conn.card_path.c_str(), O_RDWR); + if (fd < 0) { + return -1; + } + auto close_fd = util::fail_guard([fd]() { + close(fd); + }); + + // Find which CRTC is assigned to our connector + res_ptr resources {drmModeGetResources(fd)}; + if (!resources) { + return -1; + } + + std::uint32_t target_crtc = 0; + for (int i = 0; i < resources->count_connectors; i++) { + conn_ptr c {drmModeGetConnector(fd, resources->connectors[i])}; + if (!c || c->connector_id != conn.connector_id) { + continue; + } + if (c->encoder_id) { + enc_ptr enc {drmModeGetEncoder(fd, c->encoder_id)}; + if (enc) { + target_crtc = enc->crtc_id; + } + } + break; + } + + if (!target_crtc) { + return -1; + } + + // Iterate planes same way kmsgrab does to find the index + drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + auto plane_res = drmModeGetPlaneResources(fd); + if (!plane_res) { + return -1; + } + + int monitor = 0; + for (std::uint32_t i = 0; i < plane_res->count_planes; i++) { + auto plane = drmModeGetPlane(fd, plane_res->planes[i]); + if (!plane) { + continue; + } + + bool has_fb = plane->fb_id != 0; + auto plane_crtc = plane->crtc_id; + drmModeFreePlane(plane); + + if (!has_fb) { + continue; + } + + // Skip cursor planes + auto props = drmModeObjectGetProperties(fd, plane_res->planes[i], DRM_MODE_OBJECT_PLANE); + if (props) { + bool is_cursor = false; + for (std::uint32_t j = 0; j < props->count_props; j++) { + auto prop = drmModeGetProperty(fd, props->props[j]); + if (prop) { + if (std::string_view(prop->name) == "type"sv && props->prop_values[j] == DRM_PLANE_TYPE_CURSOR) { + is_cursor = true; + } + drmModeFreeProperty(prop); + } + if (is_cursor) { + break; + } + } + drmModeFreeObjectProperties(props); + if (is_cursor) { + continue; + } + } + + if (plane_crtc == target_crtc) { + drmModeFreePlaneResources(plane_res); + BOOST_LOG(info) << "[Experimental] Virtual display is at KMS monitor index "sv << monitor; + return monitor; + } + ++monitor; + } + + drmModeFreePlaneResources(plane_res); + return -1; + } + + bool + is_supported() { + std::error_code ec; + + if (!helper_check_path("/sys/kernel/debug/dri")) { + BOOST_LOG(debug) << "[Experimental] debugfs DRI not available"sv; + return false; + } + + if (!fs::exists("/dev/dri"sv, ec) || ec) { + return false; + } + + for (auto &entry : fs::directory_iterator {"/dev/dri"sv, ec}) { + auto filename = entry.path().filename().string(); + if (filename.substr(0, 4) != "card"sv) { + continue; + } + + int fd = open(entry.path().c_str(), O_RDWR); + if (fd < 0) { + continue; + } + + close(fd); + return true; + } + + return false; + } + + static status_e + create_impl(std::string &out_display_name, int width, int height, int fps_millihertz, const std::string &client_id) { + if (!is_supported()) { + BOOST_LOG(info) << "[Experimental] Virtual display not supported on this system"sv; + return status_e::NOT_SUPPORTED; + } + + if (active_state) { + remove(); + } + + auto candidates = find_disconnected_connectors(); + if (candidates.empty()) { + BOOST_LOG(warning) << "[Experimental] No disconnected GPU connectors found"sv; + return status_e::NO_CONNECTOR; + } + + connector_info_t conn; + const auto &preferred = config::video.vdisplay_connector; + if (!preferred.empty()) { + auto it = std::find_if(candidates.begin(), candidates.end(), [&](const connector_info_t &c) { + return c.sysfs_name == preferred; + }); + if (it != candidates.end()) { + conn = *it; + BOOST_LOG(info) << "[Experimental] Using configured connector: "sv << conn.sysfs_name; + } else { + BOOST_LOG(warning) << "[Experimental] Configured connector '"sv << preferred << "' not available, falling back to auto-select"sv; + conn = select_best(candidates); + } + } else { + conn = select_best(candidates); + } + BOOST_LOG(info) << "[Experimental] Selected connector: "sv << conn.sysfs_name; + + active_state = active_state_t {conn, false, false}; + + if (width <= 0 || height <= 0 || fps_millihertz <= 0) { + BOOST_LOG(error) << "[Experimental] Missing resolution/refresh parameters for virtual display"sv; + active_state.reset(); + return status_e::EDID_WRITE_FAILED; + } + + int refresh_hz = std::clamp((fps_millihertz + 500) / 1000, 24, 360); + auto edid = generate_edid(width, height, refresh_hz, client_id); + + if (!write_edid(conn, edid.data(), edid.size())) { + active_state.reset(); + return status_e::EDID_WRITE_FAILED; + } + active_state->edid_written = true; + + BOOST_LOG(info) << "[Experimental] Generated EDID for "sv << width << "x"sv << height << "@"sv << refresh_hz << "Hz"sv + << (client_id.empty() ? ""sv : " (client: "sv) << client_id << (client_id.empty() ? ""sv : ")"sv); + + if (!force_status(conn, "on")) { + clear_edid(conn); + active_state.reset(); + return status_e::REPROBE_FAILED; + } + active_state->reprobed = true; + + trigger_hotplug(conn); + + if (!wait_for_connector_active(conn, 5000)) { + BOOST_LOG(warning) << "[Experimental] Proceeding despite compositor timeout"sv; + } + + if (window_system == window_system_e::X11) { + configure_xrandr(conn); + } + + out_display_name = conn.sysfs_name; + return status_e::OK; + } + + status_e + create(std::string &out_display_name, int width, int height, int fps_millihertz, const std::string &client_id) { + try { + return create_impl(out_display_name, width, height, fps_millihertz, client_id); + } catch (const std::exception &e) { + BOOST_LOG(error) << "[Experimental] Virtual display creation threw: "sv << e.what(); + active_state.reset(); + return status_e::NOT_SUPPORTED; + } + } + + status_e + remove() { + if (!active_state) { + return status_e::OK; + } + + auto conn = active_state->connector; + + if (active_state->edid_written) { + clear_edid(conn); + } + + force_status(conn, "detect"); + trigger_hotplug(conn); + std::this_thread::sleep_for(500ms); + + if (window_system == window_system_e::X11) { + auto type_name = connector_type_name(conn.connector_type); + auto output_name = type_name + "-" + std::to_string(conn.connector_index); + auto cmd = "xrandr --output "s + output_name + " --off 2>&1"; + if (system(cmd.c_str()) != 0) { + BOOST_LOG(debug) << "[Experimental] xrandr --off returned non-zero"sv; + } + } + + active_state.reset(); + BOOST_LOG(info) << "[Experimental] Virtual display removed"sv; + return status_e::OK; + } + + bool + is_active() { + return active_state.has_value(); + } + + int get_kms_index() { + if (!active_state) { + return -1; + } + return find_kms_index(active_state->connector); + } + +} // namespace linux_vdisplay diff --git a/src/platform/linux/virtual_display.h b/src/platform/linux/virtual_display.h new file mode 100644 index 0000000000..a8145ad38a --- /dev/null +++ b/src/platform/linux/virtual_display.h @@ -0,0 +1,40 @@ +/** + * @file src/platform/linux/virtual_display.h + * @brief [Experimental] Linux virtual display via DRM EDID override. + */ +#pragma once + +// standard includes +#include +#include + +namespace linux_vdisplay { + + enum class status_e { + OK = 0, + NOT_SUPPORTED = -1, + NO_CONNECTOR = -2, + EDID_WRITE_FAILED = -3, + REPROBE_FAILED = -4, + COMPOSITOR_TIMEOUT = -5, + }; + + struct connector_info_t { + std::string card_path; + int card_index; + std::uint32_t connector_id; + std::uint32_t connector_type; + std::uint32_t connector_index; + std::string sysfs_name; + std::string debugfs_edid_path; + std::string sysfs_status_path; + std::string debugfs_hotplug_path; + }; + + bool is_supported(); + status_e create(std::string &out_display_name, int width, int height, int fps_millihertz, const std::string &client_id); + status_e remove(); + bool is_active(); + int get_kms_index(); + +} // namespace linux_vdisplay diff --git a/src/process.cpp b/src/process.cpp index d7d4af4ba5..e88a85af5b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -5,7 +5,7 @@ #define BOOST_BIND_GLOBAL_PLACEHOLDERS #ifndef BOOST_PROCESS_VERSION - #define BOOST_PROCESS_VERSION 1 + #define BOOST_PROCESS_VERSION 1 #endif // standard includes #include @@ -28,14 +28,14 @@ #include "crypto.h" #include "display_device.h" #include "file_handler.h" +#include "httpcommon.h" #include "logging.h" #include "platform/common.h" #include "process.h" -#include "httpcommon.h" #include "system_tray.h" #include "utility.h" -#include "video.h" #include "uuid.h" +#include "video.h" #ifdef _WIN32 // from_utf8() string conversion function @@ -175,7 +175,7 @@ namespace proc { #endif } - int proc_t::execute(const ctx_t& app, std::shared_ptr launch_session) { + int proc_t::execute(const ctx_t &app, std::shared_ptr launch_session) { if (_app_id == input_only_app_id) { terminate(false, false); std::this_thread::sleep_for(1s); @@ -202,8 +202,8 @@ namespace proc { } if (scale_factor != 100) { - render_width *= ((float)scale_factor / 100); - render_height *= ((float)scale_factor / 100); + render_width *= ((float) scale_factor / 100); + render_height *= ((float) scale_factor / 100); // Chop the last bit to ensure the scaled resolution is even numbered // Most odd resolutions won't work well @@ -235,10 +235,10 @@ namespace proc { #ifdef _WIN32 if ( - config::video.headless_mode // Headless mode - || launch_session->virtual_display // User requested virtual display - || _app.virtual_display // App is configured to use virtual display - || !video::allow_encoder_probing() // No active display presents + config::video.headless_mode // Headless mode + || launch_session->virtual_display // User requested virtual display + || _app.virtual_display // App is configured to use virtual display + || !video::allow_encoder_probing() // No active display presents ) { if (vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { // Try init driver again @@ -342,6 +342,31 @@ namespace proc { display_device::reset_persistence(); } +#elif defined(__linux__) && defined(SUNSHINE_BUILD_DRM) + + if ( + config::video.linux_virtual_display_experimental && + (config::video.headless_mode || launch_session->virtual_display || _app.virtual_display) + ) { + std::string vdisplay_name; + auto status = linux_vdisplay::create(vdisplay_name, render_width, render_height, launch_session->fps, launch_session->unique_id); + if (status == linux_vdisplay::status_e::OK) { + BOOST_LOG(info) << "[Experimental] Linux virtual display created: "sv << vdisplay_name; + this->virtual_display = true; + + auto kms_idx = linux_vdisplay::get_kms_index(); + if (kms_idx >= 0) { + this->original_output_name = config::video.output_name; + config::video.output_name = std::to_string(kms_idx); + BOOST_LOG(info) << "[Experimental] Capturing virtual display at KMS index "sv << kms_idx; + } + } else { + BOOST_LOG(warning) << "[Experimental] Linux virtual display failed (status "sv << static_cast(status) << ')'; + } + } + + display_device::configure_display(config::video, *launch_session); + #else display_device::configure_display(config::video, *launch_session); @@ -362,7 +387,7 @@ namespace proc { std::string fps_str; char fps_buf[8]; - snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float)launch_session->fps / 1000.0f); + snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float) launch_session->fps / 1000.0f); fps_str = fps_buf; // Add Stream-specific environment variables @@ -371,7 +396,7 @@ namespace proc { _env["SUNSHINE_APP_NAME"] = _app.name; _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); - _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float)launch_session->fps / 1000.0f)) : fps_str; + _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float) launch_session->fps / 1000.0f)) : fps_str; _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; @@ -495,8 +520,8 @@ namespace proc { _app_launch_time = std::chrono::steady_clock::now(); - #ifdef _WIN32 - auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{ +#ifdef _WIN32 + auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr] { // Windows doesn't seem to be able to set HDR correctly when a display is just connected / changed resolution, // so we have tooggle HDR for the virtual display manually after a delay. auto retryInterval = 200ms; @@ -549,7 +574,7 @@ namespace proc { }); resetHDRThread.detach(); - #endif +#endif fg.disable(); @@ -584,11 +609,11 @@ namespace proc { BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; placebo = true; - #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (_process.native_exit_code() != 0) { system_tray::update_tray_launch_error(proc::proc.get_last_run_app_name(), _process.native_exit_code()); } - #endif +#endif return _app_id; } @@ -606,7 +631,6 @@ namespace proc { if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable { - _env["APOLLO_APP_STATUS"] = "RESUMING"; std::error_code ec; @@ -775,6 +799,21 @@ namespace proc { } else { display_device::revert_configuration(); } +#elif defined(__linux__) && defined(SUNSHINE_BUILD_DRM) + if (this->virtual_display && linux_vdisplay::is_active()) { + linux_vdisplay::remove(); + BOOST_LOG(info) << "[Experimental] Linux virtual display removed"sv; + if (!this->original_output_name.empty()) { + config::video.output_name = this->original_output_name; + BOOST_LOG(info) << "[Experimental] Restored output_name to "sv << this->original_output_name; + } + } + if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { + if (this->virtual_display) { + display_device::reset_persistence(); + } else { + display_device::revert_configuration(); + } #else if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { display_device::revert_configuration(); @@ -1060,7 +1099,7 @@ namespace proc { * @param fileTree_p Pointer to the JSON object representing the file tree. * @param inputTree_p Pointer to the JSON object representing the new app. */ - void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p) { + void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p) { std::string new_app_uuid; if (inputTree_p) { @@ -1114,14 +1153,14 @@ namespace proc { (*fileTree_p)["apps"] = newApps; } - void migration_v2(nlohmann::json& fileTree) { + void migration_v2(nlohmann::json &fileTree) { static const int this_version = 2; // Determine the current migration version (default to 1 if not present). int file_version = 1; if (fileTree.contains("version")) { try { file_version = fileTree["version"].get(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { BOOST_LOG(info) << "Cannot parse apps.json version, treating as v1: " << e.what(); } } @@ -1153,7 +1192,7 @@ namespace proc { for (auto &app : fileTree["apps"]) { for (const auto &key : boolean_keys) { if (app.contains(key)) { - auto& _key = app[key]; + auto &_key = app[key]; if (_key.is_string()) { std::string s = _key.get(); std::transform(s.begin(), s.end(), s.begin(), ::tolower); // Normalize to lowercase for comparison @@ -1213,7 +1252,7 @@ namespace proc { } } - void migrate(nlohmann::json& fileTree, const std::string& fileName) { + void migrate(nlohmann::json &fileTree, const std::string &fileName) { int last_version = 2; int file_version = 0; @@ -1228,7 +1267,6 @@ namespace proc { } std::optional parse(const std::string &file_name) { - // Prepare environment variables. auto this_env = boost::this_process::environment(); @@ -1243,7 +1281,7 @@ namespace proc { try { std::string content = file_handler::read_file(file_name.c_str()); tree = nlohmann::json::parse(content); - } catch (const std::exception& e) { + } catch (const std::exception &e) { BOOST_LOG(warning) << "Couldn't read apps.json properly! Apps will not be loaded."sv; break; } @@ -1334,26 +1372,29 @@ namespace proc { } // Process other fields. - if (app_node.contains("output")) + if (app_node.contains("output")) { ctx.output = parse_env_val(this_env, app_node.value("output", "")); + } std::string name = parse_env_val(this_env, app_node.value("name", "")); - if (app_node.contains("cmd")) + if (app_node.contains("cmd")) { ctx.cmd = parse_env_val(this_env, app_node.value("cmd", "")); + } if (app_node.contains("working-dir")) { ctx.working_dir = parse_env_val(this_env, app_node.value("working-dir", "")); - #ifdef _WIN32 +#ifdef _WIN32 // The working directory, unlike the command itself, should not be quoted. boost::erase_all(ctx.working_dir, "\""); ctx.working_dir += '\\'; - #endif +#endif } - if (app_node.contains("image-path")) + if (app_node.contains("image-path")) { ctx.image_path = parse_env_val(this_env, app_node.value("image-path", "")); + } ctx.elevated = app_node.value("elevated", false); ctx.auto_detach = app_node.value("auto-detach", true); ctx.wait_all = app_node.value("wait-all", true); - ctx.exit_timeout = std::chrono::seconds { app_node.value("exit-timeout", 5) }; + ctx.exit_timeout = std::chrono::seconds {app_node.value("exit-timeout", 5)}; ctx.virtual_display = app_node.value("virtual-display", false); ctx.scale_factor = app_node.value("scale-factor", 100); ctx.use_app_identity = app_node.value("use-app-identity", false); @@ -1418,7 +1459,7 @@ namespace proc { BOOST_LOG(warning) << "No applications configured, adding fallback Desktop entry."; proc::ctx_t ctx; ctx.idx = std::to_string(i); - ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID + ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID ctx.name = "Desktop (fallback)"; ctx.image_path = parse_env_val(this_env, "desktop-alt.png"); ctx.virtual_display = false; @@ -1430,7 +1471,7 @@ namespace proc { ctx.elevated = false; ctx.auto_detach = true; - ctx.wait_all = false; // Desktop doesn't have a specific command to wait for + ctx.wait_all = false; // Desktop doesn't have a specific command to wait for ctx.exit_timeout = 5s; // Calculate unique ID @@ -1448,7 +1489,7 @@ namespace proc { } // Virtual Display entry - #ifdef _WIN32 +#ifdef _WIN32 if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { proc::ctx_t ctx; ctx.idx = std::to_string(i); @@ -1471,8 +1512,7 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } - else { + } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1480,7 +1520,7 @@ namespace proc { apps.emplace_back(std::move(ctx)); } - #endif +#endif if (config::input.enable_input_only_mode) { // Input Only entry @@ -1495,7 +1535,7 @@ namespace proc { ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; - ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing + ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing ctx.elevated = false; ctx.auto_detach = true; @@ -1506,8 +1546,7 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } - else { + } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1542,8 +1581,7 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } - else { + } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1567,7 +1605,7 @@ namespace proc { proc.terminate(false, false); } - #ifdef _WIN32 +#ifdef _WIN32 size_t fail_count = 0; while (fail_count < 5 && vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { initVDisplayDriver(); @@ -1578,7 +1616,7 @@ namespace proc { fail_count += 1; std::this_thread::sleep_for(1s); } - #endif +#endif auto proc_opt = proc::parse(file_name); diff --git a/src/process.h b/src/process.h index 2f52d7ff18..db66e8e522 100644 --- a/src/process.h +++ b/src/process.h @@ -18,8 +18,8 @@ // lib includes #include -#include #include +#include #include #include #include @@ -32,6 +32,8 @@ #ifdef _WIN32 #include "platform/windows/virtual_display.h" +#elif defined(__linux__) && defined(SUNSHINE_BUILD_DRM) + #include "platform/linux/virtual_display.h" #endif #define VIRTUAL_DISPLAY_UUID "8902CB19-674A-403D-A587-41B092E900BA" @@ -96,7 +98,7 @@ namespace proc { bool per_client_app_identity; bool allow_client_commands; bool terminate_on_pause; - int scale_factor; + int scale_factor; std::chrono::seconds exit_timeout; }; @@ -109,6 +111,7 @@ namespace proc { std::string mode_changed_display; bool initial_hdr = false; bool virtual_display = false; + std::string original_output_name; bool allow_client_commands = false; proc_t( @@ -121,7 +124,7 @@ namespace proc { void launch_input_only(); - int execute(const ctx_t& _app, std::shared_ptr launch_session); + int execute(const ctx_t &_app, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` @@ -165,7 +168,7 @@ namespace proc { }; boost::filesystem::path - find_working_directory(const std::string &cmd, const boost::process::v1::environment &env); + find_working_directory(const std::string &cmd, const boost::process::v1::environment &env); /** * @brief Calculate a stable id based on name and image data @@ -175,7 +178,7 @@ namespace proc { std::string validate_app_image_path(std::string app_image_path); void refresh(const std::string &file_name, bool needs_terminate = true); - void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p); + void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p); std::optional parse(const std::string &file_name); /** diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 41eeab11ad..342668e9b9 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -176,6 +176,26 @@ const validateFallbackMode = (event) => {
Please ensure SudoVDA driver is installed to the latest version and enabled properly.
+ + + + + diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 6a2dfef819..aaade700a0 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -305,6 +305,11 @@ "ignore_encoder_probe_failure": "Ignore Encoder Probe Failure", "ignore_encoder_probe_failure_desc": "Allow streaming to continue even if probing for encoders fails. This may result in streaming failure if no encoder is available.", "install_steam_audio_drivers": "Install Steam Audio Drivers", + "linux_virtual_display_experimental": "Virtual Display (Experimental)", + "linux_virtual_display_experimental_desc": "Enable experimental virtual display support using DRM EDID override. Requires AMD or Intel GPU with available disconnected connector. When enabled, streaming sessions can use a virtual display instead of an existing monitor.", + "vdisplay_connector": "Virtual Display Connector", + "vdisplay_connector_desc": "Specify which GPU connector to use for the virtual display (e.g. card0-HDMI-A-2). Leave empty to auto-select the first available disconnected connector.", + "vdisplay_connector_placeholder": "Auto (first available)", "install_steam_audio_drivers_desc": "If Steam is installed, this will automatically install the Steam Streaming Speakers driver to support 5.1/7.1 surround sound and muting host audio.", "isolated_virtual_display_option": "Move the Virtual Display to the bottom right-most corner of the display layout", "isolated_virtual_display_option_desc": "This makes the display isolated from all other display and contains mouse movements to the virtual screen. This reorganizes the displays such that the all other displays are to the left of the virtual display.", diff --git a/src_assets/linux/misc/postinst b/src_assets/linux/misc/postinst index 4783c2ec20..b75ff47a20 100644 --- a/src_assets/linux/misc/postinst +++ b/src_assets/linux/misc/postinst @@ -8,14 +8,49 @@ modprobe uhid if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." + # Create sunshine system group if it doesn't exist + if ! getent group sunshine >/dev/null 2>&1; then + if [ -x "$(command -v groupadd)" ]; then + groupadd --system sunshine + echo "Created 'sunshine' system group." + fi + fi + + # Add the installing user to the sunshine group + if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then + target_user="$SUDO_USER" + elif [ -n "$PKEXEC_UID" ]; then + target_user=$(id -nu "$PKEXEC_UID" 2>/dev/null) + else + target_user="" + fi + + if [ -n "$target_user" ]; then + if ! id -nG "$target_user" 2>/dev/null | grep -qw sunshine; then + usermod -aG sunshine "$target_user" + echo "Added '$target_user' to 'sunshine' group. Log out and back in for group to take effect." + fi + fi + # Ensure Sunshine can grab images from KMS path_to_setcap=$(which setcap) path_to_sunshine=$(readlink -f "$(which sunshine)") + path_to_vdisplay_helper=$(readlink -f "$(which sunshine-vdisplay-helper)" 2>/dev/null) if [ -x "$path_to_setcap" ] ; then echo "Setting CAP_SYS_ADMIN capability on Sunshine binary." echo "$path_to_setcap cap_sys_admin+p $path_to_sunshine" $path_to_setcap cap_sys_admin+p $path_to_sunshine echo "CAP_SYS_ADMIN capability set on Sunshine binary." + if [ -n "$path_to_vdisplay_helper" ] ; then + if getent group sunshine >/dev/null 2>&1; then + chown root:sunshine "$path_to_vdisplay_helper" + chmod 0750 "$path_to_vdisplay_helper" + echo "Set sunshine-vdisplay-helper ownership to root:sunshine (0750)." + fi + echo "Setting CAP_DAC_OVERRIDE capability on virtual display helper." + $path_to_setcap cap_dac_override+ep $path_to_vdisplay_helper + echo "CAP_DAC_OVERRIDE capability set on virtual display helper." + fi else echo "error: setcap not found or not executable." fi diff --git a/src_assets/linux/misc/sunshine-sysusers.conf b/src_assets/linux/misc/sunshine-sysusers.conf new file mode 100644 index 0000000000..9d63780bf2 --- /dev/null +++ b/src_assets/linux/misc/sunshine-sysusers.conf @@ -0,0 +1 @@ +g sunshine - - From 060ff58fb093b58b1c9a0ffe62808fedf3704a22 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 01:24:04 +0200 Subject: [PATCH 2/7] Fix locale str --- src_assets/common/assets/web/public/assets/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index aaade700a0..3fefe45817 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -306,7 +306,7 @@ "ignore_encoder_probe_failure_desc": "Allow streaming to continue even if probing for encoders fails. This may result in streaming failure if no encoder is available.", "install_steam_audio_drivers": "Install Steam Audio Drivers", "linux_virtual_display_experimental": "Virtual Display (Experimental)", - "linux_virtual_display_experimental_desc": "Enable experimental virtual display support using DRM EDID override. Requires AMD or Intel GPU with available disconnected connector. When enabled, streaming sessions can use a virtual display instead of an existing monitor.", + "linux_virtual_display_experimental_desc": "Enable experimental virtual display support using DRM EDID override. Requires a GPU with an available disconnected connector. NVIDIA GPUs may have limited compatibility. When enabled, streaming sessions can use a virtual display instead of an existing monitor.", "vdisplay_connector": "Virtual Display Connector", "vdisplay_connector_desc": "Specify which GPU connector to use for the virtual display (e.g. card0-HDMI-A-2). Leave empty to auto-select the first available disconnected connector.", "vdisplay_connector_placeholder": "Auto (first available)", From 6f76412a3b5018bbd3d7be786cd76b8d90dc89e5 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 01:28:32 +0200 Subject: [PATCH 3/7] Fix post inst --- packaging/linux/Arch/sunshine.install | 13 +++++++++++++ packaging/linux/fedora/Sunshine.spec | 28 +++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install index d7b87737fc..f7850d89c7 100644 --- a/packaging/linux/Arch/sunshine.install +++ b/packaging/linux/Arch/sunshine.install @@ -2,6 +2,17 @@ do_setcap() { setcap cap_sys_admin+p $(readlink -f usr/bin/sunshine) } +do_vdisplay_helper() { + local helper=$(readlink -f usr/bin/sunshine-vdisplay-helper 2>/dev/null) + if [ -x "$helper" ]; then + if getent group sunshine >/dev/null 2>&1; then + chown root:sunshine "$helper" + chmod 0750 "$helper" + fi + setcap cap_dac_override+ep "$helper" + fi +} + do_udev_reload() { udevadm control --reload-rules udevadm trigger --property-match=DEVNAME=/dev/uinput @@ -12,12 +23,14 @@ do_udev_reload() { post_install() { do_setcap + do_vdisplay_helper do_udev_reload modprobe uhid } post_upgrade() { do_setcap + do_vdisplay_helper do_udev_reload modprobe uhid } diff --git a/packaging/linux/fedora/Sunshine.spec b/packaging/linux/fedora/Sunshine.spec index 44cf39ae9e..4da5c09002 100644 --- a/packaging/linux/fedora/Sunshine.spec +++ b/packaging/linux/fedora/Sunshine.spec @@ -212,7 +212,7 @@ cd %{_builddir}/Sunshine/build %make_install %post -# Note: this is copied from the postinst script +# Note: this should be kept in sync with src_assets/linux/misc/postinst # Load uhid (DS5 emulation) echo "Loading uhid kernel module for DS5 emulation." @@ -222,6 +222,30 @@ modprobe uhid if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." + # Create sunshine system group if it doesn't exist + if ! getent group sunshine >/dev/null 2>&1; then + if [ -x "$(command -v groupadd)" ]; then + groupadd --system sunshine + echo "Created 'sunshine' system group." + fi + fi + + # Add the installing user to the sunshine group + if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then + target_user="$SUDO_USER" + elif [ -n "$PKEXEC_UID" ]; then + target_user=$(id -nu "$PKEXEC_UID" 2>/dev/null) + else + target_user="" + fi + + if [ -n "$target_user" ]; then + if ! id -nG "$target_user" 2>/dev/null | grep -qw sunshine; then + usermod -aG sunshine "$target_user" + echo "Added '$target_user' to 'sunshine' group. Log out and back in for group to take effect." + fi + fi + # Trigger udev rule reload for /dev/uinput and /dev/uhid path_to_udevadm=$(which udevadm) if [ -x "$path_to_udevadm" ]; then @@ -240,7 +264,7 @@ fi %files # Executables %caps(cap_sys_admin+p) %{_bindir}/sunshine -%caps(cap_sys_admin+p) %{_bindir}/sunshine-* +%attr(0750,root,sunshine) %caps(cap_dac_override+ep) %{_bindir}/sunshine-vdisplay-helper # Systemd unit file for user services %{_userunitdir}/sunshine.service From 18f54cf7f00d32308d52e23fd71bb6546b1e3c49 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 16:02:19 +0200 Subject: [PATCH 4/7] Rename stuff to apollo --- cmake/compile_definitions/linux.cmake | 6 ++--- cmake/packaging/linux.cmake | 6 ++--- packaging/linux/Arch/sunshine.install | 6 ++--- packaging/linux/fedora/Sunshine.spec | 18 +++++++-------- src/platform/linux/vdisplay_helper.cpp | 2 +- src/platform/linux/virtual_display.cpp | 6 ++--- src_assets/linux/misc/apollo-sysusers.conf | 1 + src_assets/linux/misc/postinst | 24 ++++++++++---------- src_assets/linux/misc/sunshine-sysusers.conf | 1 - 9 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 src_assets/linux/misc/apollo-sysusers.conf delete mode 100644 src_assets/linux/misc/sunshine-sysusers.conf diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 7cd04c9cee..cff6237bda 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -100,10 +100,10 @@ if(LIBDRM_FOUND AND LIBCAP_FOUND) list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) # Privileged helper for virtual display debugfs writes (cap_dac_override) - add_executable(sunshine-vdisplay-helper + add_executable(apollo-vdisplay-helper "${CMAKE_SOURCE_DIR}/src/platform/linux/vdisplay_helper.cpp") - set_target_properties(sunshine-vdisplay-helper PROPERTIES CXX_STANDARD 17) - install(TARGETS sunshine-vdisplay-helper RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") + set_target_properties(apollo-vdisplay-helper PROPERTIES CXX_STANDARD 17) + install(TARGETS apollo-vdisplay-helper RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") endif() # evdev diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 267ec8309b..03d9d618b8 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -31,9 +31,9 @@ else() DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}") - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/sunshine-sysusers.conf" + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/apollo-sysusers.conf" DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/sysusers.d" - RENAME "sunshine.conf") + RENAME "apollo.conf") endif() endif() @@ -45,7 +45,7 @@ set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc # https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071 set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}" - "%attr(0750,root,sunshine) %caps(cap_dac_override+ep) %{_bindir}/sunshine-vdisplay-helper") + "%attr(0750,root,apollo) %caps(cap_dac_override+ep) %{_bindir}/apollo-vdisplay-helper") # Dependencies set(CPACK_DEB_COMPONENT_INSTALL ON) diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install index f7850d89c7..db9b33c942 100644 --- a/packaging/linux/Arch/sunshine.install +++ b/packaging/linux/Arch/sunshine.install @@ -3,10 +3,10 @@ do_setcap() { } do_vdisplay_helper() { - local helper=$(readlink -f usr/bin/sunshine-vdisplay-helper 2>/dev/null) + local helper=$(readlink -f usr/bin/apollo-vdisplay-helper 2>/dev/null) if [ -x "$helper" ]; then - if getent group sunshine >/dev/null 2>&1; then - chown root:sunshine "$helper" + if getent group apollo >/dev/null 2>&1; then + chown root:apollo "$helper" chmod 0750 "$helper" fi setcap cap_dac_override+ep "$helper" diff --git a/packaging/linux/fedora/Sunshine.spec b/packaging/linux/fedora/Sunshine.spec index 4da5c09002..b5ecb0fa21 100644 --- a/packaging/linux/fedora/Sunshine.spec +++ b/packaging/linux/fedora/Sunshine.spec @@ -222,15 +222,15 @@ modprobe uhid if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." - # Create sunshine system group if it doesn't exist - if ! getent group sunshine >/dev/null 2>&1; then + # Create apollo system group if it doesn't exist + if ! getent group apollo >/dev/null 2>&1; then if [ -x "$(command -v groupadd)" ]; then - groupadd --system sunshine - echo "Created 'sunshine' system group." + groupadd --system apollo + echo "Created 'apollo' system group." fi fi - # Add the installing user to the sunshine group + # Add the installing user to the apollo group if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then target_user="$SUDO_USER" elif [ -n "$PKEXEC_UID" ]; then @@ -240,9 +240,9 @@ if [ ! -x "$(command -v rpm-ostree)" ]; then fi if [ -n "$target_user" ]; then - if ! id -nG "$target_user" 2>/dev/null | grep -qw sunshine; then - usermod -aG sunshine "$target_user" - echo "Added '$target_user' to 'sunshine' group. Log out and back in for group to take effect." + if ! id -nG "$target_user" 2>/dev/null | grep -qw apollo; then + usermod -aG apollo "$target_user" + echo "Added '$target_user' to 'apollo' group. Log out and back in for group to take effect." fi fi @@ -264,7 +264,7 @@ fi %files # Executables %caps(cap_sys_admin+p) %{_bindir}/sunshine -%attr(0750,root,sunshine) %caps(cap_dac_override+ep) %{_bindir}/sunshine-vdisplay-helper +%attr(0750,root,apollo) %caps(cap_dac_override+ep) %{_bindir}/apollo-vdisplay-helper # Systemd unit file for user services %{_userunitdir}/sunshine.service diff --git a/src/platform/linux/vdisplay_helper.cpp b/src/platform/linux/vdisplay_helper.cpp index 6f2415ed74..aa13b91d6b 100644 --- a/src/platform/linux/vdisplay_helper.cpp +++ b/src/platform/linux/vdisplay_helper.cpp @@ -2,7 +2,7 @@ * @file src/platform/linux/vdisplay_helper.cpp * @brief Privileged helper for virtual display debugfs/sysfs writes. * - * Installed with cap_dac_override+ep so the main sunshine process + * Installed with cap_dac_override+ep so the main apollo process * does not need CAP_DAC_OVERRIDE in its capability set. */ #include diff --git a/src/platform/linux/virtual_display.cpp b/src/platform/linux/virtual_display.cpp index 6a0f95c7af..008972faab 100644 --- a/src/platform/linux/virtual_display.cpp +++ b/src/platform/linux/virtual_display.cpp @@ -563,12 +563,12 @@ namespace linux_vdisplay { std::error_code ec; auto self = fs::read_symlink("/proc/self/exe", ec); if (!ec) { - auto sibling = self.parent_path() / "sunshine-vdisplay-helper"; + auto sibling = self.parent_path() / "apollo-vdisplay-helper"; if (fs::exists(sibling, ec)) { return boost::filesystem::path(sibling.string()); } } - return bp::search_path("sunshine-vdisplay-helper"); + return bp::search_path("apollo-vdisplay-helper"); } // Instead of doing debugfs edits here which would require cap_dac_override caps @@ -579,7 +579,7 @@ namespace linux_vdisplay { run_helper(const std::vector &args, const uint8_t *stdin_data = nullptr, size_t stdin_len = 0) { auto helper = find_helper(); if (helper.empty()) { - BOOST_LOG(error) << "[Experimental] sunshine-vdisplay-helper not found"sv; + BOOST_LOG(error) << "[Experimental] apollo-vdisplay-helper not found"sv; return false; } diff --git a/src_assets/linux/misc/apollo-sysusers.conf b/src_assets/linux/misc/apollo-sysusers.conf new file mode 100644 index 0000000000..b897427b45 --- /dev/null +++ b/src_assets/linux/misc/apollo-sysusers.conf @@ -0,0 +1 @@ +g apollo - - diff --git a/src_assets/linux/misc/postinst b/src_assets/linux/misc/postinst index b75ff47a20..e5d8e86383 100644 --- a/src_assets/linux/misc/postinst +++ b/src_assets/linux/misc/postinst @@ -8,15 +8,15 @@ modprobe uhid if [ ! -x "$(command -v rpm-ostree)" ]; then echo "Not in an rpm-ostree environment, proceeding with post install steps." - # Create sunshine system group if it doesn't exist - if ! getent group sunshine >/dev/null 2>&1; then + # Create apollo system group if it doesn't exist + if ! getent group apollo >/dev/null 2>&1; then if [ -x "$(command -v groupadd)" ]; then - groupadd --system sunshine - echo "Created 'sunshine' system group." + groupadd --system apollo + echo "Created 'apollo' system group." fi fi - # Add the installing user to the sunshine group + # Add the installing user to the apollo group if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then target_user="$SUDO_USER" elif [ -n "$PKEXEC_UID" ]; then @@ -26,26 +26,26 @@ if [ ! -x "$(command -v rpm-ostree)" ]; then fi if [ -n "$target_user" ]; then - if ! id -nG "$target_user" 2>/dev/null | grep -qw sunshine; then - usermod -aG sunshine "$target_user" - echo "Added '$target_user' to 'sunshine' group. Log out and back in for group to take effect." + if ! id -nG "$target_user" 2>/dev/null | grep -qw apollo; then + usermod -aG apollo "$target_user" + echo "Added '$target_user' to 'apollo' group. Log out and back in for group to take effect." fi fi # Ensure Sunshine can grab images from KMS path_to_setcap=$(which setcap) path_to_sunshine=$(readlink -f "$(which sunshine)") - path_to_vdisplay_helper=$(readlink -f "$(which sunshine-vdisplay-helper)" 2>/dev/null) + path_to_vdisplay_helper=$(readlink -f "$(which apollo-vdisplay-helper)" 2>/dev/null) if [ -x "$path_to_setcap" ] ; then echo "Setting CAP_SYS_ADMIN capability on Sunshine binary." echo "$path_to_setcap cap_sys_admin+p $path_to_sunshine" $path_to_setcap cap_sys_admin+p $path_to_sunshine echo "CAP_SYS_ADMIN capability set on Sunshine binary." if [ -n "$path_to_vdisplay_helper" ] ; then - if getent group sunshine >/dev/null 2>&1; then - chown root:sunshine "$path_to_vdisplay_helper" + if getent group apollo >/dev/null 2>&1; then + chown root:apollo "$path_to_vdisplay_helper" chmod 0750 "$path_to_vdisplay_helper" - echo "Set sunshine-vdisplay-helper ownership to root:sunshine (0750)." + echo "Set apollo-vdisplay-helper ownership to root:apollo (0750)." fi echo "Setting CAP_DAC_OVERRIDE capability on virtual display helper." $path_to_setcap cap_dac_override+ep $path_to_vdisplay_helper diff --git a/src_assets/linux/misc/sunshine-sysusers.conf b/src_assets/linux/misc/sunshine-sysusers.conf deleted file mode 100644 index 9d63780bf2..0000000000 --- a/src_assets/linux/misc/sunshine-sysusers.conf +++ /dev/null @@ -1 +0,0 @@ -g sunshine - - From 258d6129b84e867fb070bc84c7f45ba92106e968 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 16:04:31 +0200 Subject: [PATCH 5/7] Remove format changes --- src/process.cpp | 82 +++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index e88a85af5b..d14b1dbc81 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -5,7 +5,7 @@ #define BOOST_BIND_GLOBAL_PLACEHOLDERS #ifndef BOOST_PROCESS_VERSION - #define BOOST_PROCESS_VERSION 1 + #define BOOST_PROCESS_VERSION 1 #endif // standard includes #include @@ -175,7 +175,7 @@ namespace proc { #endif } - int proc_t::execute(const ctx_t &app, std::shared_ptr launch_session) { + int proc_t::execute(const ctx_t& app, std::shared_ptr launch_session) { if (_app_id == input_only_app_id) { terminate(false, false); std::this_thread::sleep_for(1s); @@ -202,8 +202,8 @@ namespace proc { } if (scale_factor != 100) { - render_width *= ((float) scale_factor / 100); - render_height *= ((float) scale_factor / 100); + render_width *= ((float)scale_factor / 100); + render_height *= ((float)scale_factor / 100); // Chop the last bit to ensure the scaled resolution is even numbered // Most odd resolutions won't work well @@ -235,10 +235,10 @@ namespace proc { #ifdef _WIN32 if ( - config::video.headless_mode // Headless mode - || launch_session->virtual_display // User requested virtual display - || _app.virtual_display // App is configured to use virtual display - || !video::allow_encoder_probing() // No active display presents + config::video.headless_mode // Headless mode + || launch_session->virtual_display // User requested virtual display + || _app.virtual_display // App is configured to use virtual display + || !video::allow_encoder_probing() // No active display presents ) { if (vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { // Try init driver again @@ -387,7 +387,7 @@ namespace proc { std::string fps_str; char fps_buf[8]; - snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float) launch_session->fps / 1000.0f); + snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float)launch_session->fps / 1000.0f); fps_str = fps_buf; // Add Stream-specific environment variables @@ -396,7 +396,7 @@ namespace proc { _env["SUNSHINE_APP_NAME"] = _app.name; _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); - _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float) launch_session->fps / 1000.0f)) : fps_str; + _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float)launch_session->fps / 1000.0f)) : fps_str; _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; @@ -520,8 +520,8 @@ namespace proc { _app_launch_time = std::chrono::steady_clock::now(); -#ifdef _WIN32 - auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr] { + #ifdef _WIN32 + auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{ // Windows doesn't seem to be able to set HDR correctly when a display is just connected / changed resolution, // so we have tooggle HDR for the virtual display manually after a delay. auto retryInterval = 200ms; @@ -574,7 +574,7 @@ namespace proc { }); resetHDRThread.detach(); -#endif + #endif fg.disable(); @@ -609,11 +609,11 @@ namespace proc { BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; placebo = true; -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (_process.native_exit_code() != 0) { system_tray::update_tray_launch_error(proc::proc.get_last_run_app_name(), _process.native_exit_code()); } -#endif + #endif return _app_id; } @@ -631,6 +631,7 @@ namespace proc { if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable { + _env["APOLLO_APP_STATUS"] = "RESUMING"; std::error_code ec; @@ -1099,7 +1100,7 @@ namespace proc { * @param fileTree_p Pointer to the JSON object representing the file tree. * @param inputTree_p Pointer to the JSON object representing the new app. */ - void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p) { + void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p) { std::string new_app_uuid; if (inputTree_p) { @@ -1153,14 +1154,14 @@ namespace proc { (*fileTree_p)["apps"] = newApps; } - void migration_v2(nlohmann::json &fileTree) { + void migration_v2(nlohmann::json& fileTree) { static const int this_version = 2; // Determine the current migration version (default to 1 if not present). int file_version = 1; if (fileTree.contains("version")) { try { file_version = fileTree["version"].get(); - } catch (const std::exception &e) { + } catch (const std::exception& e) { BOOST_LOG(info) << "Cannot parse apps.json version, treating as v1: " << e.what(); } } @@ -1192,7 +1193,7 @@ namespace proc { for (auto &app : fileTree["apps"]) { for (const auto &key : boolean_keys) { if (app.contains(key)) { - auto &_key = app[key]; + auto& _key = app[key]; if (_key.is_string()) { std::string s = _key.get(); std::transform(s.begin(), s.end(), s.begin(), ::tolower); // Normalize to lowercase for comparison @@ -1252,7 +1253,7 @@ namespace proc { } } - void migrate(nlohmann::json &fileTree, const std::string &fileName) { + void migrate(nlohmann::json& fileTree, const std::string& fileName) { int last_version = 2; int file_version = 0; @@ -1267,6 +1268,7 @@ namespace proc { } std::optional parse(const std::string &file_name) { + // Prepare environment variables. auto this_env = boost::this_process::environment(); @@ -1281,7 +1283,7 @@ namespace proc { try { std::string content = file_handler::read_file(file_name.c_str()); tree = nlohmann::json::parse(content); - } catch (const std::exception &e) { + } catch (const std::exception& e) { BOOST_LOG(warning) << "Couldn't read apps.json properly! Apps will not be loaded."sv; break; } @@ -1372,29 +1374,26 @@ namespace proc { } // Process other fields. - if (app_node.contains("output")) { + if (app_node.contains("output")) ctx.output = parse_env_val(this_env, app_node.value("output", "")); - } std::string name = parse_env_val(this_env, app_node.value("name", "")); - if (app_node.contains("cmd")) { + if (app_node.contains("cmd")) ctx.cmd = parse_env_val(this_env, app_node.value("cmd", "")); - } if (app_node.contains("working-dir")) { ctx.working_dir = parse_env_val(this_env, app_node.value("working-dir", "")); -#ifdef _WIN32 + #ifdef _WIN32 // The working directory, unlike the command itself, should not be quoted. boost::erase_all(ctx.working_dir, "\""); ctx.working_dir += '\\'; -#endif + #endif } - if (app_node.contains("image-path")) { + if (app_node.contains("image-path")) ctx.image_path = parse_env_val(this_env, app_node.value("image-path", "")); - } ctx.elevated = app_node.value("elevated", false); ctx.auto_detach = app_node.value("auto-detach", true); ctx.wait_all = app_node.value("wait-all", true); - ctx.exit_timeout = std::chrono::seconds {app_node.value("exit-timeout", 5)}; + ctx.exit_timeout = std::chrono::seconds { app_node.value("exit-timeout", 5) }; ctx.virtual_display = app_node.value("virtual-display", false); ctx.scale_factor = app_node.value("scale-factor", 100); ctx.use_app_identity = app_node.value("use-app-identity", false); @@ -1459,7 +1458,7 @@ namespace proc { BOOST_LOG(warning) << "No applications configured, adding fallback Desktop entry."; proc::ctx_t ctx; ctx.idx = std::to_string(i); - ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID + ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID ctx.name = "Desktop (fallback)"; ctx.image_path = parse_env_val(this_env, "desktop-alt.png"); ctx.virtual_display = false; @@ -1471,7 +1470,7 @@ namespace proc { ctx.elevated = false; ctx.auto_detach = true; - ctx.wait_all = false; // Desktop doesn't have a specific command to wait for + ctx.wait_all = false; // Desktop doesn't have a specific command to wait for ctx.exit_timeout = 5s; // Calculate unique ID @@ -1479,7 +1478,8 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } else { + } + else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1489,7 +1489,7 @@ namespace proc { } // Virtual Display entry -#ifdef _WIN32 + #ifdef _WIN32 if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { proc::ctx_t ctx; ctx.idx = std::to_string(i); @@ -1520,7 +1520,7 @@ namespace proc { apps.emplace_back(std::move(ctx)); } -#endif + #endif if (config::input.enable_input_only_mode) { // Input Only entry @@ -1535,7 +1535,7 @@ namespace proc { ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; - ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing + ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing ctx.elevated = false; ctx.auto_detach = true; @@ -1546,7 +1546,8 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } else { + } + else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1581,7 +1582,8 @@ namespace proc { if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } else { + } + else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -1605,7 +1607,7 @@ namespace proc { proc.terminate(false, false); } -#ifdef _WIN32 + #ifdef _WIN32 size_t fail_count = 0; while (fail_count < 5 && vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { initVDisplayDriver(); @@ -1616,7 +1618,7 @@ namespace proc { fail_count += 1; std::this_thread::sleep_for(1s); } -#endif + #endif auto proc_opt = proc::parse(file_name); From 3839950577615e0055e987e5a5fe3f753e337d15 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 16:06:48 +0200 Subject: [PATCH 6/7] Remove more format changes --- src/process.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/process.h b/src/process.h index db66e8e522..25cf8146b3 100644 --- a/src/process.h +++ b/src/process.h @@ -98,7 +98,7 @@ namespace proc { bool per_client_app_identity; bool allow_client_commands; bool terminate_on_pause; - int scale_factor; + int scale_factor; std::chrono::seconds exit_timeout; }; @@ -124,7 +124,7 @@ namespace proc { void launch_input_only(); - int execute(const ctx_t &_app, std::shared_ptr launch_session); + int execute(const ctx_t& _app, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` @@ -168,7 +168,7 @@ namespace proc { }; boost::filesystem::path - find_working_directory(const std::string &cmd, const boost::process::v1::environment &env); + find_working_directory(const std::string &cmd, const boost::process::v1::environment &env); /** * @brief Calculate a stable id based on name and image data @@ -178,7 +178,7 @@ namespace proc { std::string validate_app_image_path(std::string app_image_path); void refresh(const std::string &file_name, bool needs_terminate = true); - void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p); + void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p); std::optional parse(const std::string &file_name); /** From b805ef0f7d6d7fb07614bd7a118cdbefea3501b6 Mon Sep 17 00:00:00 2001 From: Adi Date: Sun, 26 Apr 2026 16:22:14 +0200 Subject: [PATCH 7/7] Persist state and recover in case of crash --- src/main.cpp | 8 ++++ src/platform/linux/virtual_display.cpp | 59 ++++++++++++++++++++++++++ src/platform/linux/virtual_display.h | 1 + 3 files changed, 68 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 73209c7288..b4ce89611d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,8 @@ #ifdef _WIN32 #include "platform/windows/misc.h" #include "platform/windows/virtual_display.h" +#elif defined(__linux__) && defined(SUNSHINE_BUILD_DRM) + #include "platform/linux/virtual_display.h" #endif #define PROBE_DISPLAY_UUID "38F72B96-B00C-4F21-8B6C-E1BFF1602B0E" @@ -206,6 +208,12 @@ int main(int argc, char *argv[]) { BOOST_LOG(error) << "Display device session failed to initialize"sv; } +#if defined(__linux__) && defined(SUNSHINE_BUILD_DRM) + if (config::video.linux_virtual_display_experimental) { + linux_vdisplay::recover_crash_state(); + } +#endif + #ifdef _WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { diff --git a/src/platform/linux/virtual_display.cpp b/src/platform/linux/virtual_display.cpp index 008972faab..d70590b6ad 100644 --- a/src/platform/linux/virtual_display.cpp +++ b/src/platform/linux/virtual_display.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ #include "misc.h" #include "src/config.h" #include "src/logging.h" +#include "src/platform/common.h" #include "src/utility.h" #include "virtual_display.h" @@ -374,6 +376,29 @@ namespace linux_vdisplay { static std::optional active_state; + static fs::path + state_file_path() { + return platf::appdata() / "vdisplay.state"; + } + + static void + save_state(const connector_info_t &conn) { + std::error_code ec; + std::ofstream f(state_file_path(), std::ios::trunc); + if (f) { + f << conn.debugfs_edid_path << '\n' + << conn.sysfs_status_path << '\n' + << conn.debugfs_hotplug_path << '\n' + << conn.sysfs_name << '\n'; + } + } + + static void + clear_state() { + std::error_code ec; + fs::remove(state_file_path(), ec); + } + using version_ptr = util::safe_ptr; using res_ptr = util::safe_ptr; using conn_ptr = util::safe_ptr; @@ -833,6 +858,38 @@ namespace linux_vdisplay { return false; } + void + recover_crash_state() { + auto path = state_file_path(); + std::error_code ec; + if (!fs::exists(path, ec) || ec) { + return; + } + + BOOST_LOG(warning) << "[Experimental] Found stale vdisplay state file — recovering from previous crash"sv; + + std::ifstream f(path); + std::string edid_path, status_path, hotplug_path, sysfs_name; + if (!std::getline(f, edid_path) || !std::getline(f, status_path) || !std::getline(f, hotplug_path) || !std::getline(f, sysfs_name)) { + BOOST_LOG(warning) << "[Experimental] Corrupt state file, removing"sv; + clear_state(); + return; + } + + connector_info_t conn {}; + conn.debugfs_edid_path = edid_path; + conn.sysfs_status_path = status_path; + conn.debugfs_hotplug_path = hotplug_path; + conn.sysfs_name = sysfs_name; + + clear_edid(conn); + force_status(conn, "detect"); + trigger_hotplug(conn); + + clear_state(); + BOOST_LOG(info) << "[Experimental] Crash recovery complete — cleared stale EDID on "sv << sysfs_name; + } + static status_e create_impl(std::string &out_display_name, int width, int height, int fps_millihertz, const std::string &client_id) { if (!is_supported()) { @@ -905,6 +962,7 @@ namespace linux_vdisplay { configure_xrandr(conn); } + save_state(conn); out_display_name = conn.sysfs_name; return status_e::OK; } @@ -946,6 +1004,7 @@ namespace linux_vdisplay { } active_state.reset(); + clear_state(); BOOST_LOG(info) << "[Experimental] Virtual display removed"sv; return status_e::OK; } diff --git a/src/platform/linux/virtual_display.h b/src/platform/linux/virtual_display.h index a8145ad38a..4d9128d9cd 100644 --- a/src/platform/linux/virtual_display.h +++ b/src/platform/linux/virtual_display.h @@ -32,6 +32,7 @@ namespace linux_vdisplay { }; bool is_supported(); + void recover_crash_state(); status_e create(std::string &out_display_name, int width, int height, int fps_millihertz, const std::string &client_id); status_e remove(); bool is_active();