[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