[feat] add Linux X11 resolution controls with disconnect restore

This commit is contained in:
dijunkun
2026-11-11 12:50:49 +08:00
parent abcc0eef0a
commit 17b4db25fb
10 changed files with 612 additions and 96 deletions
@@ -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<DisplayResolutionInfo> 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();
@@ -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 <X11/Xlib.h>
#include <atomic>
#include <mutex>
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<int> 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<std::mutex> lock_;
XErrorHandler previous_handler_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,389 @@
#include "display_resolution_linux.h"
#include <X11/Xatom.h>
#include <X11/extensions/Xrandr.h>
#include <algorithm>
#include <cmath>
#include <map>
#include <memory>
#include <utility>
#include "platform.h"
#include "x11_error_trap.h"
namespace crossdesk {
namespace {
template <typename T, auto Free>
using XOwner = std::unique_ptr<T, decltype(Free)>;
using Resources = XOwner<XRRScreenResources, XRRFreeScreenResources>;
using Output = XOwner<XRROutputInfo, XRRFreeOutputInfo>;
using Crtc = XOwner<XRRCrtcInfo, XRRFreeCrtcInfo>;
struct ModeState {
RRCrtc crtc = None;
RRMode mode = None;
int x = 0, y = 0;
Rotation rotation = RR_Rotate_0;
std::vector<RROutput> 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<ScopedX11ErrorTrap>(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<XRRPanning, XRRFreePanning> 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<ScopedX11ErrorTrap> 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<RRMode> Choices(const RandrSession& session, const ModeState& state) {
const auto* current = session.FindMode(state.mode);
if (!current) return {};
std::vector<Output> outputs;
for (auto id : state.outputs) {
auto output = session.GetOutput(id);
if (!output) return {};
outputs.push_back(std::move(output));
}
std::map<std::pair<int, int>, 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<RRMode> 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<DisplayInfo>& 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<uintptr_t>(display.handle),
display.name, state, key) && key == change.display_key &&
session.trap->SyncAndGetError() == 0;
}
} // namespace
std::vector<DisplayResolutionInfo> QueryLinuxDisplayResolutions(
const std::vector<DisplayInfo>& displays) {
if (displays.empty()) return {};
RandrSession session;
if (!session.resources) return {};
std::vector<DisplayResolutionInfo> 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<uintptr_t>(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<DisplayInfo>& 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<bool()> CaptureLinuxDisplayResolution(
const std::vector<DisplayInfo>& 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<uintptr_t>(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
@@ -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 <functional>
#include <vector>
#include <display_resolution.h>
#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<DisplayResolutionInfo> QueryLinuxDisplayResolutions(
const std::vector<DisplayInfo>& displays);
bool ApplyLinuxDisplayResolution(const std::vector<DisplayInfo>& displays,
const DisplayResolutionChange& change);
std::function<bool()> CaptureLinuxDisplayResolution(
const std::vector<DisplayInfo>& displays,
const DisplayResolutionChange& change);
} // namespace crossdesk
#endif
@@ -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<int>(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<int>(i);
break;
}
@@ -13,54 +13,10 @@
#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"
#include "x11_error_trap.h"
namespace crossdesk {
namespace {
std::atomic<int> 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<std::mutex> 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<unsigned>(attr.width) &&
crtc->y + crtc->height <= static_cast<unsigned>(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<int>(display_info_list_.size())) return -1;
monitor_index_ = monitor_index;
@@ -222,9 +151,90 @@ int ScreenCapturerX11::ResetToInitialMonitor() {
return 0;
}
std::vector<DisplayInfo> 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<DisplayInfo> 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<unsigned>(attr.width) &&
crtc->y + crtc->height <= static_cast<unsigned>(attr.height)) {
latest.emplace_back(reinterpret_cast<void*>(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<int>(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<int>(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;
@@ -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 <cstring>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
@@ -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<DisplayInfo> display_info_list_;
std::mutex display_info_mutex_;
int capture_error_count_ = 0;
bool use_abgr_to_nv12_ = false;
+1
View File
@@ -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
+2
View File
@@ -139,6 +139,8 @@ macOS 保留一个 CrossDesk Dock 应用图标,主窗口与远程控制窗口
连接成功后,点击工具栏中的 **画面设置**(滑杆图标),直接选择低 / 中 / 高画质、**30 / 60 fps**,以及帧率优先 / 画质优先 / 平衡。选项会发送给当前被控端,无需断开重连;实际画面效果取决于网络、设备与编码器能力。旧版被控端不支持时,菜单会提示并禁用选项。
被控端为 Windows、macOS 或 Linux X11 时,还可在 **画面设置 → 分辨率** 中选择当前显示器支持的桌面分辨率。调整无需重连,最后一个控制端断开后会恢复连接期间首次调整前的显示模式。Linux 需要使用 X11 采集及支持 RandR 的显示服务器;Wayland、XWayland 和使用自定义缩放或平移的显示配置暂不支持。可选分辨率由显示器或虚拟显示服务器提供,仅提供一个模式的虚拟屏不会额外生成模式。
<p align="center">
<img src="images/desktop-stream-video-settings-zh.png" width="800" alt="当前远程会话的画面设置菜单:画面质量、采集帧率与画面偏好;背景为公开页面演示图" />
</p>
+2
View File
@@ -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.
<p align="center">
<img src="images/desktop-stream-video-settings-en.png" width="800" alt="Current session video menu with quality, capture frame rate, and video preference; the background is a public page used for demonstration" />
</p>