mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-11 19:07:33 +08:00
[feat] add Linux X11 resolution controls with disconnect restore
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user