From 17b4db25fb6c1a4e06a6e5b1b20960e12128a991 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 11 Nov 2026 12:50:49 +0800 Subject: [PATCH] [feat] add Linux X11 resolution controls with disconnect restore --- .../runtime/display_resolution_runtime.cpp | 18 + .../platform/linux/common/x11_error_trap.h | 51 +++ .../display_resolution_linux.cpp | 389 ++++++++++++++++++ .../display_resolution_linux.h | 30 ++ .../screen_capturer/screen_capturer_linux.cpp | 13 +- .../screen_capturer/screen_capturer_x11.cpp | 194 ++++----- .../screen_capturer/screen_capturer_x11.h | 8 +- apps/desktop/xmake/targets.lua | 1 + docs/USER_GUIDE.md | 2 + docs/USER_GUIDE_EN.md | 2 + 10 files changed, 612 insertions(+), 96 deletions(-) create mode 100644 apps/desktop/src/platform/linux/common/x11_error_trap.h create mode 100644 apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.cpp create mode 100644 apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.h diff --git a/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp b/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp index 30d3c1aa..823b6043 100644 --- a/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp +++ b/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp @@ -9,6 +9,8 @@ #include "platform/windows/screen_capturer/display_resolution_win.h" #elif defined(__APPLE__) #include "platform/macos/screen_capturer/display_resolution_mac.h" +#elif defined(__linux__) && !defined(__OHOS__) +#include "platform/linux/screen_capturer/display_resolution_linux.h" #endif namespace crossdesk { @@ -25,6 +27,9 @@ std::vector GuiRuntime::QueryHostDisplayResolutions() { #elif defined(__APPLE__) std::lock_guard lock(native_display_resolution_mutex); return QueryMacDisplayResolutions(devices_.display_info_list()); +#elif defined(__linux__) && !defined(__OHOS__) + std::lock_guard lock(native_display_resolution_mutex); + return QueryLinuxDisplayResolutions(devices_.display_info_list()); #else return {}; #endif @@ -128,6 +133,19 @@ void GuiRuntime::HandleDisplayResolutionChanges() { return ApplyMacDisplayResolution(displays, command.change); }); } +#elif defined(__linux__) && !defined(__OHOS__) + { + std::lock_guard lock(native_display_resolution_mutex); + reply.resolution_change.accepted = + display_resolution_restoration_.Apply( + command.change.display_key, + [&] { + return CaptureLinuxDisplayResolution(displays, command.change); + }, + [&] { + return ApplyLinuxDisplayResolution(displays, command.change); + }); + } #endif } reply.display_resolutions = QueryHostDisplayResolutions(); diff --git a/apps/desktop/src/platform/linux/common/x11_error_trap.h b/apps/desktop/src/platform/linux/common/x11_error_trap.h new file mode 100644 index 00000000..99c39e03 --- /dev/null +++ b/apps/desktop/src/platform/linux/common/x11_error_trap.h @@ -0,0 +1,51 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _X11_ERROR_TRAP_H_ +#define _X11_ERROR_TRAP_H_ + +#include + +#include +#include + +namespace crossdesk { + +// Xlib's error handler is process-wide. Capture and RandR transactions must +// share this lock so neither restores a handler while the other is using it. +inline std::mutex x11_error_handler_mutex; +inline std::atomic x11_last_error_code{0}; + +class ScopedX11ErrorTrap { + public: + explicit ScopedX11ErrorTrap(Display* display) + : display_(display), lock_(x11_error_handler_mutex) { + x11_last_error_code.store(0); + previous_handler_ = XSetErrorHandler([](Display*, XErrorEvent* event) { + x11_last_error_code.store(event ? event->error_code : -1); + return 0; + }); + } + + ~ScopedX11ErrorTrap() { + XSync(display_, False); + XSetErrorHandler(previous_handler_); + } + + int SyncAndGetError() const { + XSync(display_, False); + return x11_last_error_code.load(); + } + + private: + Display* display_; + std::unique_lock lock_; + XErrorHandler previous_handler_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.cpp b/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.cpp new file mode 100644 index 00000000..8fe913bd --- /dev/null +++ b/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.cpp @@ -0,0 +1,389 @@ +#include "display_resolution_linux.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "platform.h" +#include "x11_error_trap.h" + +namespace crossdesk { +namespace { + +template +using XOwner = std::unique_ptr; +using Resources = XOwner; +using Output = XOwner; +using Crtc = XOwner; + +struct ModeState { + RRCrtc crtc = None; + RRMode mode = None; + int x = 0, y = 0; + Rotation rotation = RR_Rotate_0; + std::vector outputs; +}; + +// Each transaction owns a connection and a short server grab. This keeps a +// hotplug or another settings client from invalidating a checked output before +// the mode switch, and also lets restoration outlive the capture connection. +class RandrSession { + public: + explicit RandrSession(const char* address = nullptr) + : display(IsWaylandSession() ? nullptr : XOpenDisplay(address)) { + if (!display) return; + trap = std::make_unique(display); + int opcode = 0, event = 0, error = 0; + int major = 1, minor = 3; + if (XQueryExtension(display, "XWAYLAND", &opcode, &event, &error) || + !XRRQueryVersion(display, &major, &minor) || + major < 1 || (major == 1 && minor < 3)) return; + root = DefaultRootWindow(display); + XGrabServer(display); + grabbed = true; + XRRSelectInput(display, root, RRScreenChangeNotifyMask); + resources.reset(XRRGetScreenResources(display, root)); + } + ~RandrSession() { + resources.reset(); + if (grabbed) XUngrabServer(display); + trap.reset(); + if (display) XCloseDisplay(display); + } + RandrSession(const RandrSession&) = delete; + RandrSession& operator=(const RandrSession&) = delete; + + Output GetOutput(RROutput id) const { + return Output(XRRGetOutputInfo(display, resources.get(), id), + XRRFreeOutputInfo); + } + Crtc GetCrtc(RRCrtc id) const { + return Crtc(XRRGetCrtcInfo(display, resources.get(), id), XRRFreeCrtcInfo); + } + const XRRModeInfo* FindMode(RRMode id) const { + for (int i = 0; i < resources->nmode; ++i) + if (resources->modes[i].id == id) return &resources->modes[i]; + return nullptr; + } + std::string OutputKey(RROutput id, const XRROutputInfo& output) const { + // EDID distinguishes a replacement monitor on the same connector. Virtual + // outputs without EDID still have a server-local output ID and name. + uint64_t hash = 14695981039346656037ull; + Atom type = None; + int format = 0; + unsigned long count = 0, remaining = 0; + unsigned char* data = nullptr; + const Atom edid = XInternAtom(display, "EDID", True); + if (edid != None && + XRRGetOutputProperty(display, id, edid, 0, 1024, False, False, + AnyPropertyType, &type, &format, &count, + &remaining, &data) == Success && + type == XA_INTEGER && format == 8 && data) { + for (unsigned long i = 0; i < count; ++i) + hash = (hash ^ data[i]) * 1099511628211ull; + } + if (data) XFree(data); + return std::to_string(id) + ":" + + std::string(output.name, output.nameLen) + ":" + std::to_string(hash); + } + + bool SimpleGeometry(RRCrtc id) const { + // Changing a scaled/panning desktop also requires compositor-specific + // policy. Do not overwrite those settings with an unscaled mode. + XRRCrtcTransformAttributes* transform = nullptr; + if (!XRRGetCrtcTransform(display, id, &transform) || !transform) return false; + bool identity = true; + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + const XFixed expected = row == col ? 65536 : 0; + identity &= transform->currentTransform.matrix[row][col] == expected && + transform->pendingTransform.matrix[row][col] == expected; + } + } + XFree(transform); + XOwner panning( + XRRGetPanning(display, resources.get(), id), XRRFreePanning); + return identity && (!panning || (!panning->width && !panning->height)); + } + + bool Resolve(RROutput id, const std::string& name, ModeState& state, + std::string& key) const { + if (!resources || + std::find(resources->outputs, resources->outputs + resources->noutput, + id) == resources->outputs + resources->noutput) return false; + auto output = GetOutput(id); + if (!output || output->connection != RR_Connected || !output->crtc || + name != std::string(output->name, output->nameLen)) return false; + auto crtc = GetCrtc(output->crtc); + if (!crtc || !crtc->mode || crtc->noutput <= 0 || + !SimpleGeometry(output->crtc)) return false; + state = {output->crtc, crtc->mode, crtc->x, crtc->y, crtc->rotation, + {crtc->outputs, crtc->outputs + crtc->noutput}}; + key = DisplayString(display); + // A mirrored CRTC changes all its outputs together. Validate every member + // on apply and restore, including replacements on a cloned connector. Use + // the same key for every member so requests through another mirrored slot + // share the original restoration baseline instead of saving an interim mode. + auto sorted = state.outputs; + std::sort(sorted.begin(), sorted.end()); + for (auto member : sorted) { + auto clone = GetOutput(member); + if (!clone || clone->connection != RR_Connected) return false; + key += "|" + OutputKey(member, *clone); + } + return key.size() <= 1024; + } + + Display* display = nullptr; + Window root = None; + Resources resources{nullptr, XRRFreeScreenResources}; + std::unique_ptr trap; + bool grabbed = false; +}; + +DisplayResolution ModeSize(const XRRModeInfo& mode, Rotation rotation) { + if (rotation & (RR_Rotate_90 | RR_Rotate_270)) + return {int(mode.height), int(mode.width)}; + return {int(mode.width), int(mode.height)}; +} + +double RefreshRate(const XRRModeInfo& mode) { + if (!mode.hTotal || !mode.vTotal) return 0; + return double(mode.dotClock) / mode.hTotal / mode.vTotal; +} + +std::vector Choices(const RandrSession& session, const ModeState& state) { + const auto* current = session.FindMode(state.mode); + if (!current) return {}; + std::vector outputs; + for (auto id : state.outputs) { + auto output = session.GetOutput(id); + if (!output) return {}; + outputs.push_back(std::move(output)); + } + std::map, const XRRModeInfo*> unique; + for (int i = 0; i < session.resources->nmode; ++i) { + const auto& mode = session.resources->modes[i]; + const auto size = ModeSize(mode, state.rotation); + if (!ValidDisplayResolution(size) || + (mode.modeFlags & (RR_Interlace | RR_DoubleScan))) continue; + bool supported = true; + for (const auto& output : outputs) { + if (std::find(output->modes, output->modes + output->nmode, + mode.id) == output->modes + output->nmode) { + supported = false; + break; + } + } + if (!supported) continue; + const auto key = std::make_pair(size.width, size.height); + const auto rank = [&](const XRRModeInfo& candidate) { + return std::make_pair(std::abs(RefreshRate(candidate) - RefreshRate(*current)), + candidate.id != current->id); + }; + auto found = unique.find(key); + if (found == unique.end() || rank(mode) < rank(*found->second)) + unique[key] = &mode; + } + std::vector result; + for (auto it = unique.rbegin(); it != unique.rend(); ++it) { + result.push_back(it->second->id); + if (result.size() == kMaxDisplayResolutionModes) break; + } + return result; +} + +struct ScreenSize { + int width, height, mm_width, mm_height; +}; + +ScreenSize CurrentScreen(const RandrSession& session) { + XWindowAttributes attr{}; + XGetWindowAttributes(session.display, session.root, &attr); + // Refresh Xlib's cached physical dimensions after our own resize as well. + // A stale DPI value can otherwise make rollback skip restoring millimeters. + while (XPending(session.display)) { + XEvent event{}; + XNextEvent(session.display, &event); + XRRUpdateConfiguration(&event); + } + const int screen = DefaultScreen(session.display); + return {attr.width, attr.height, DisplayWidthMM(session.display, screen), + DisplayHeightMM(session.display, screen)}; +} + +bool SetScreen(RandrSession& session, ScreenSize size) { + const auto current = CurrentScreen(session); + if (current.width != size.width || current.height != size.height || + current.mm_width != size.mm_width || current.mm_height != size.mm_height) { + XRRSetScreenSize(session.display, session.root, size.width, size.height, + std::max(1, size.mm_width), std::max(1, size.mm_height)); + } + const auto applied = CurrentScreen(session); + return session.trap->SyncAndGetError() == 0 && + applied.width == size.width && applied.height == size.height; +} + +bool SetCrtc(RandrSession& session, const ModeState& state) { + // SetScreenSize can advance the resource timestamp. + Resources fresh(XRRGetScreenResourcesCurrent(session.display, session.root), + XRRFreeScreenResources); + if (!fresh) return false; + auto outputs = state.outputs; + if (XRRSetCrtcConfig(session.display, fresh.get(), state.crtc, CurrentTime, + state.x, state.y, state.mode, state.rotation, + outputs.data(), int(outputs.size())) != RRSetConfigSuccess) + return false; + auto applied = session.GetCrtc(state.crtc); + return applied && applied->mode == state.mode && applied->x == state.x && + applied->y == state.y && applied->rotation == state.rotation; +} + +bool ChangeMode(RandrSession& session, const ModeState& before, RRMode mode, + const ScreenSize* restore_screen = nullptr) { + const auto* selected = session.FindMode(mode); + if (!selected) return false; + const auto size = ModeSize(*selected, before.rotation); + int width = before.x + size.width, height = before.y + size.height; + for (int i = 0; i < session.resources->ncrtc; ++i) { + const auto id = session.resources->crtcs[i]; + auto other = session.GetCrtc(id); + if (!other) return false; + if (!other->mode) continue; + if (!session.SimpleGeometry(id)) return false; + if (id != before.crtc) { + width = std::max(width, other->x + int(other->width)); + height = std::max(height, other->y + int(other->height)); + } + } + int min_width = 0, min_height = 0, max_width = 0, max_height = 0; + if (!XRRGetScreenSizeRange(session.display, session.root, &min_width, + &min_height, &max_width, &max_height)) return false; + width = std::max(width, min_width); + height = std::max(height, min_height); + if (restore_screen) { + width = std::max(width, restore_screen->width); + height = std::max(height, restore_screen->height); + } + if (before.x < 0 || before.y < 0 || width > max_width || height > max_height) + return false; + const auto original = CurrentScreen(session); + if (original.width <= 0 || original.height <= 0) return false; + const auto screen_size = [&](int w, int h) -> ScreenSize { + return {w, h, std::max(1, int(std::lround(double(original.mm_width) * w / + original.width))), + std::max(1, int(std::lround(double(original.mm_height) * h / + original.height)))}; + }; + const auto expanded = screen_size(std::max(width, original.width), + std::max(height, original.height)); + auto final = screen_size(width, height); + if (restore_screen && width == restore_screen->width && + height == restore_screen->height) final = *restore_screen; + auto target = before; + target.mode = mode; + // Grow first, switch the CRTC, then shrink. Other displays retain their + // positions, rotations and modes, and are always inside the framebuffer. + bool expanded_ok = SetScreen(session, expanded); + // RandR 1.1 emulation (for example Xephyr) can expose modes through 1.2 + // while ignoring SetScreenSize. Its sole CRTC resizes the root itself. Only + // allow this fallback for an origin-aligned, single-CRTC desktop with no X + // error; a real allocation/configuration error must still fail and roll back. + if (!expanded_ok && session.resources->ncrtc == 1 && !before.x && !before.y && + session.trap->SyncAndGetError() == 0) + expanded_ok = true; + if (expanded_ok && SetCrtc(session, target) && + SetScreen(session, final)) return true; + // Keep the larger framebuffer until rollback has restored the old CRTC. + SetScreen(session, expanded); + if (SetCrtc(session, before)) SetScreen(session, original); + return false; +} + +bool ResolveChange(const RandrSession& session, + const std::vector& displays, + const DisplayResolutionChange& change, ModeState& state) { + if (change.display_id < 0 || change.display_id >= int(displays.size()) || + !ValidDisplayResolution(change.resolution)) return false; + const auto& display = displays[change.display_id]; + std::string key; + return session.Resolve(reinterpret_cast(display.handle), + display.name, state, key) && key == change.display_key && + session.trap->SyncAndGetError() == 0; +} + +} // namespace + +std::vector QueryLinuxDisplayResolutions( + const std::vector& displays) { + if (displays.empty()) return {}; + RandrSession session; + if (!session.resources) return {}; + std::vector result; + for (int i = 0; i < int(displays.size()) && i < kMaxResolutionDisplays; ++i) { + DisplayResolutionInfo info; + info.display_id = i; + ModeState state; + if (!session.Resolve(reinterpret_cast(displays[i].handle), + displays[i].name, state, info.display_key)) continue; + const auto* current = session.FindMode(state.mode); + if (!current) continue; + info.current = ModeSize(*current, state.rotation); + if (!ValidDisplayResolution(info.current)) continue; + for (auto mode : Choices(session, state)) + info.modes.push_back(ModeSize(*session.FindMode(mode), state.rotation)); + result.push_back(std::move(info)); + } + if (session.trap->SyncAndGetError()) return {}; + return result; +} + +bool ApplyLinuxDisplayResolution(const std::vector& displays, + const DisplayResolutionChange& change) { + RandrSession session; + ModeState state; + if (!ResolveChange(session, displays, change, state)) return false; + const auto choices = Choices(session, state); + if (session.trap->SyncAndGetError()) return false; + for (auto mode : choices) { + if (ModeSize(*session.FindMode(mode), state.rotation) == change.resolution) + return mode == state.mode || ChangeMode(session, state, mode); + } + return false; +} + +std::function CaptureLinuxDisplayResolution( + const std::vector& displays, + const DisplayResolutionChange& change) { + RandrSession session; + ModeState original; + if (!ResolveChange(session, displays, change, original)) return {}; + const auto screen = CurrentScreen(session); + const std::string address = DisplayString(session.display); + const auto display = displays[change.display_id]; + const auto key = change.display_key; + return [address, display, key, original, screen] { + RandrSession session(address.c_str()); + ModeState current; + std::string current_key; + if (!session.Resolve(reinterpret_cast(display.handle), + display.name, current, current_key) || current_key != key || + session.trap->SyncAndGetError()) + return false; + // Restore the exact refresh rate as well as dimensions, while retaining + // any placement/rotation changes the local user made during the session. + const auto actual_screen = CurrentScreen(session); + if (current.mode == original.mode && actual_screen.width == screen.width && + actual_screen.height == screen.height && + actual_screen.mm_width == screen.mm_width && + actual_screen.mm_height == screen.mm_height) return true; + return ChangeMode(session, current, original.mode, &screen); + }; +} + +} // namespace crossdesk diff --git a/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.h b/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.h new file mode 100644 index 00000000..a73e5ded --- /dev/null +++ b/apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.h @@ -0,0 +1,30 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _DISPLAY_RESOLUTION_LINUX_H_ +#define _DISPLAY_RESOLUTION_LINUX_H_ + +#include +#include + +#include +#include "display_info.h" + +namespace crossdesk { + +// Native X11/RandR outputs only. Wayland/Xwayland cannot change the physical +// desktop through RandR and must not advertise these modes to controllers. +std::vector QueryLinuxDisplayResolutions( + const std::vector& displays); +bool ApplyLinuxDisplayResolution(const std::vector& displays, + const DisplayResolutionChange& change); +std::function CaptureLinuxDisplayResolution( + const std::vector& displays, + const DisplayResolutionChange& change); + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp index df06bd60..adbea741 100644 --- a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp +++ b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp @@ -560,8 +560,19 @@ void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) { ++current_index) { const auto& backend_display = backend_displays[current_index]; int canonical_index = -1; + // Native identity survives a resolution change; geometry may now coincide + // with another screen and must not redirect this stream to its slot. for (size_t i = 0; i < canonical_displays_.size(); ++i) { - if (!used[i] && similar(backend_display, canonical_displays_[i])) { + if (!used[i] && backend_display.handle && + backend_display.handle == canonical_displays_[i].handle && + backend_display.name == canonical_displays_[i].name) { + canonical_index = static_cast(i); + break; + } + } + for (size_t i = 0; i < canonical_displays_.size(); ++i) { + if (canonical_index < 0 && !used[i] && + similar(backend_display, canonical_displays_[i])) { canonical_index = static_cast(i); break; } diff --git a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp index eb4f966e..ab8c01ab 100644 --- a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp +++ b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp @@ -13,54 +13,10 @@ #include #include "libyuv.h" #include "rd_log.h" +#include "x11_error_trap.h" namespace crossdesk { -namespace { - -std::atomic g_x11_last_error_code{0}; -std::mutex g_x11_error_handler_mutex; - -int CaptureX11ErrorHandler([[maybe_unused]] Display* display, - XErrorEvent* error_event) { - if (error_event) { - g_x11_last_error_code.store(error_event->error_code); - } else { - g_x11_last_error_code.store(-1); - } - return 0; -} - -class ScopedX11ErrorTrap { - public: - explicit ScopedX11ErrorTrap(Display* display) - : display_(display), lock_(g_x11_error_handler_mutex) { - g_x11_last_error_code.store(0); - previous_handler_ = XSetErrorHandler(CaptureX11ErrorHandler); - } - - ~ScopedX11ErrorTrap() { - if (display_) { - XSync(display_, False); - } - XSetErrorHandler(previous_handler_); - } - - int SyncAndGetError() const { - if (display_) { - XSync(display_, False); - } - return g_x11_last_error_code.load(); - } - - private: - Display* display_ = nullptr; - int (*previous_handler_)(Display*, XErrorEvent*) = nullptr; - std::unique_lock lock_; -}; - -} // namespace - ScreenCapturerX11::ScreenCapturerX11() {} ScreenCapturerX11::~ScreenCapturerX11() { Destroy(); } @@ -92,40 +48,11 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) { int event_base = 0, error_base = 0; if (XRRQueryExtension(display_, &event_base, &error_base)) { - screen_res_ = XRRGetScreenResources(display_, root_); - } - if (screen_res_) { - for (int i = 0; i < screen_res_->noutput; ++i) { - XRROutputInfo* output = - XRRGetOutputInfo(display_, screen_res_, screen_res_->outputs[i]); - if (!output) continue; - if (output->connection == RR_Connected && output->crtc != 0) { - XRRCrtcInfo* crtc = XRRGetCrtcInfo(display_, screen_res_, output->crtc); - if (crtc) { - std::string name(output->name, output->nameLen); - if (name.empty()) name = "Display" + std::to_string(i + 1); - // Reject stale/offscreen CRTCs after hot-unplug. The root drawable - // remains a usable virtual screen even when no output is connected. - if (crtc->width > 1 && crtc->height > 1 && crtc->x >= 0 && - crtc->y >= 0 && - crtc->x + crtc->width <= static_cast(attr.width) && - crtc->y + crtc->height <= static_cast(attr.height)) { - display_info_list_.emplace_back( - display_, name, true, crtc->x, crtc->y, - crtc->x + crtc->width, crtc->y + crtc->height); - } - XRRFreeCrtcInfo(crtc); - } - } - XRRFreeOutputInfo(output); - } - } - if (display_info_list_.empty()) { - display_info_list_.emplace_back(display_, "Virtual Display", true, - 0, 0, width_, height_); - LOG_INFO("No active X11 outputs; capturing virtual root window {}x{}", - width_, height_); + randr_event_base_ = event_base; + XRRSelectInput(display_, root_, RRScreenChangeNotifyMask | + RRCrtcChangeNotifyMask | RROutputChangeNotifyMask); } + RefreshDisplays(); fps_ = fps; callback_ = cb; @@ -147,15 +74,15 @@ int ScreenCapturerX11::Destroy() { y_plane_.clear(); uv_plane_.clear(); - display_info_list_.clear(); + { + std::lock_guard lock(display_info_mutex_); + display_info_list_.clear(); + } monitor_index_ = initial_monitor_index_ = 0; root_ = 0; callback_ = nullptr; - if (screen_res_) { - XRRFreeScreenResources(screen_res_); - screen_res_ = nullptr; - } + randr_event_base_ = -1; if (display_) { XCloseDisplay(display_); @@ -178,6 +105,7 @@ int ScreenCapturerX11::Start(bool show_cursor) { while (running_) { const auto frame_start = clock::now(); + HandleDisplayEvents(); if (!paused_) { OnFrame(); } @@ -211,6 +139,7 @@ int ScreenCapturerX11::Resume(int monitor_index) { } int ScreenCapturerX11::SwitchTo(int monitor_index) { + std::lock_guard lock(display_info_mutex_); if (monitor_index < 0 || monitor_index >= static_cast(display_info_list_.size())) return -1; monitor_index_ = monitor_index; @@ -222,9 +151,90 @@ int ScreenCapturerX11::ResetToInitialMonitor() { return 0; } std::vector ScreenCapturerX11::GetDisplayInfoList() { + std::lock_guard lock(display_info_mutex_); return display_info_list_; } +void ScreenCapturerX11::HandleDisplayEvents() { + bool changed = false; + while (XPending(display_)) { + XEvent event{}; + XNextEvent(display_, &event); + if (randr_event_base_ >= 0 && + (event.type == randr_event_base_ + RRScreenChangeNotify || + event.type == randr_event_base_ + RRNotify)) { + XRRUpdateConfiguration(&event); + changed = true; + } + } + if (changed) RefreshDisplays(); +} + +void ScreenCapturerX11::RefreshDisplays() { + ScopedX11ErrorTrap trap(display_); + XWindowAttributes attr{}; + if (!XGetWindowAttributes(display_, root_, &attr)) return; + std::vector latest; + auto* resources = randr_event_base_ >= 0 + ? XRRGetScreenResources(display_, root_) + : nullptr; + if (resources) { + const auto primary = XRRGetOutputPrimary(display_, root_); + for (int i = 0; i < resources->noutput; ++i) { + const auto id = resources->outputs[i]; + auto* output = XRRGetOutputInfo(display_, resources, id); + if (!output) continue; + if (output->connection == RR_Connected && output->crtc) { + auto* crtc = XRRGetCrtcInfo(display_, resources, output->crtc); + if (crtc) { + if (crtc->mode && crtc->width > 1 && crtc->height > 1 && + crtc->x >= 0 && crtc->y >= 0 && + crtc->x + crtc->width <= static_cast(attr.width) && + crtc->y + crtc->height <= static_cast(attr.height)) { + latest.emplace_back(reinterpret_cast(id), + std::string(output->name, output->nameLen), + id == primary, crtc->x, crtc->y, + crtc->x + crtc->width, crtc->y + crtc->height); + } + XRRFreeCrtcInfo(crtc); + } + } + XRRFreeOutputInfo(output); + } + XRRFreeScreenResources(resources); + } + if (trap.SyncAndGetError()) return; + std::lock_guard lock(display_info_mutex_); + if (display_info_list_.empty()) { + if (latest.empty()) { + latest.emplace_back(nullptr, "Virtual Display", true, 0, 0, + attr.width & ~1, attr.height & ~1); + } + display_info_list_ = std::move(latest); + return; + } + // Keep stream slots stable across mode changes and unplug/replug. Newly + // attached outputs need peer stream registration before they can be used. + for (auto& existing : display_info_list_) { + if (!existing.handle) { + existing.right = existing.width = attr.width & ~1; + existing.bottom = existing.height = attr.height & ~1; + continue; + } + const auto found = std::find_if(latest.begin(), latest.end(), + [&](const auto& item) { + return item.handle == existing.handle && item.name == existing.name; + }); + if (found != latest.end()) { + existing = *found; + } else { + existing.width = existing.height = 0; + existing.right = existing.left; + existing.bottom = existing.top; + } + } +} + void ScreenCapturerX11::OnFrame() { if (!display_) { LOG_ERROR("Display is not initialized"); @@ -232,17 +242,17 @@ void ScreenCapturerX11::OnFrame() { } const int monitor_index = monitor_index_.load(); - if (monitor_index < 0 || - monitor_index >= static_cast(display_info_list_.size())) { - LOG_ERROR("Invalid monitor index: {}", monitor_index); - return; + { + std::lock_guard lock(display_info_mutex_); + if (monitor_index < 0 || + monitor_index >= static_cast(display_info_list_.size())) return; + const auto& selected = display_info_list_[monitor_index]; + left_ = selected.left; + top_ = selected.top; + width_ = selected.width & ~1; + height_ = selected.height & ~1; } - left_ = display_info_list_[monitor_index].left; - top_ = display_info_list_[monitor_index].top; - width_ = display_info_list_[monitor_index].width & ~1; - height_ = display_info_list_[monitor_index].height & ~1; - if (width_ <= 1 || height_ <= 1) { LOG_ERROR("Invalid capture size: {}x{}", width_, height_); return; diff --git a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.h b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.h index 9d1219fc..f8bb6d5e 100644 --- a/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.h +++ b/apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.h @@ -11,8 +11,6 @@ struct _XDisplay; typedef struct _XDisplay Display; typedef unsigned long Window; -struct _XRRScreenResources; -typedef struct _XRRScreenResources XRRScreenResources; struct _XImage; typedef struct _XImage XImage; @@ -21,6 +19,7 @@ typedef struct _XImage XImage; #include #include #include +#include #include #include @@ -52,11 +51,13 @@ class ScreenCapturerX11 : public ScreenCapturer { private: void DrawCursor(XImage* image, int x, int y); bool ProbeCapture(); + void RefreshDisplays(); + void HandleDisplayEvents(); private: Display* display_ = nullptr; Window root_ = 0; - XRRScreenResources* screen_res_ = nullptr; + int randr_event_base_ = -1; int left_ = 0; int top_ = 0; int width_ = 0; @@ -70,6 +71,7 @@ class ScreenCapturerX11 : public ScreenCapturer { int fps_ = 60; cb_desktop_data callback_; std::vector display_info_list_; + std::mutex display_info_mutex_; int capture_error_count_ = 0; bool use_abgr_to_nv12_ = false; diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 40866c16..8e19292f 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -385,6 +385,7 @@ function setup_targets() add_packages("libyuv") add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp") add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp") + add_files("apps/desktop/src/platform/linux/screen_capturer/display_resolution_linux.cpp") add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_drm.cpp") add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_factory.cpp") if is_config("USE_WAYLAND", true) then diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 55512f58..b39ed9b4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -139,6 +139,8 @@ macOS 保留一个 CrossDesk Dock 应用图标,主窗口与远程控制窗口 连接成功后,点击工具栏中的 **画面设置**(滑杆图标),直接选择低 / 中 / 高画质、**30 / 60 fps**,以及帧率优先 / 画质优先 / 平衡。选项会发送给当前被控端,无需断开重连;实际画面效果取决于网络、设备与编码器能力。旧版被控端不支持时,菜单会提示并禁用选项。 +被控端为 Windows、macOS 或 Linux X11 时,还可在 **画面设置 → 分辨率** 中选择当前显示器支持的桌面分辨率。调整无需重连,最后一个控制端断开后会恢复连接期间首次调整前的显示模式。Linux 需要使用 X11 采集及支持 RandR 的显示服务器;Wayland、XWayland 和使用自定义缩放或平移的显示配置暂不支持。可选分辨率由显示器或虚拟显示服务器提供,仅提供一个模式的虚拟屏不会额外生成模式。 +

当前远程会话的画面设置菜单:画面质量、采集帧率与画面偏好;背景为公开页面演示图

diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index d947d3d2..724df265 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -139,6 +139,8 @@ Network statistics in both ordinary sessions and DeskHub show the video codecs a After connecting, open **Video settings** (the sliders icon) in the toolbar. Choose low / medium / high quality, **30 / 60 fps**, and frame-rate priority / quality priority / balanced adaptation. Changes are sent to the current host without reconnecting; actual results depend on the network, device, and encoder. For older hosts that do not support live video settings, the menu explains the limitation and disables the options. +For Windows, macOS, and Linux X11 hosts, **Video settings → Resolution** also lists supported desktop resolutions for the selected monitor. Changes take effect without reconnecting; when the last controller disconnects, the display mode from before the first change is restored. Linux requires X11 capture and a RandR-capable display server. Wayland, XWayland, and custom scaling or panning configurations are not supported. Choices come from the monitor or virtual display server; no additional modes are created for virtual screens that expose only one mode. +

Current session video menu with quality, capture frame rate, and video preference; the background is a public page used for demonstration