[fix] synchronize Linux Wayland cursor shapes

This commit is contained in:
dijunkun
2026-08-27 09:41:36 +08:00
parent d6d0a548b2
commit 126f455681
15 changed files with 920 additions and 117 deletions
+1 -1
View File
@@ -158,7 +158,7 @@ Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
+402
View File
@@ -0,0 +1,402 @@
#include "linux_cursor_shape.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xcursor/Xcursor.h>
#include <X11/Xlib.h>
#endif
namespace crossdesk {
namespace {
constexpr LinuxCursorAlias kCursorAliases[] = {
{"help", RemoteCursorShape::help},
{"question-arrow", RemoteCursorShape::help},
{"left-ptr-help", RemoteCursorShape::help},
{"whats-this", RemoteCursorShape::help},
{"dnd-ask", RemoteCursorShape::help},
{"pointer", RemoteCursorShape::pointer},
{"hand", RemoteCursorShape::pointer},
{"hand1", RemoteCursorShape::pointer},
{"hand2", RemoteCursorShape::pointer},
{"link", RemoteCursorShape::pointer},
{"pointing-hand", RemoteCursorShape::pointer},
{"progress", RemoteCursorShape::progress},
{"left-ptr-watch", RemoteCursorShape::progress},
{"half-busy", RemoteCursorShape::progress},
{"wait", RemoteCursorShape::wait},
{"watch", RemoteCursorShape::wait},
{"crosshair", RemoteCursorShape::crosshair},
{"cross", RemoteCursorShape::crosshair},
{"cross-reverse", RemoteCursorShape::crosshair},
{"tcross", RemoteCursorShape::crosshair},
{"cell", RemoteCursorShape::crosshair},
{"plus", RemoteCursorShape::crosshair},
{"target", RemoteCursorShape::crosshair},
{"dotbox", RemoteCursorShape::crosshair},
{"dot-box-mask", RemoteCursorShape::crosshair},
{"text", RemoteCursorShape::text},
{"xterm", RemoteCursorShape::text},
{"ibeam", RemoteCursorShape::text},
{"vertical-text", RemoteCursorShape::text},
{"alias", RemoteCursorShape::alias},
{"dnd-link", RemoteCursorShape::alias},
{"copy", RemoteCursorShape::copy},
{"dnd-copy", RemoteCursorShape::copy},
{"move", RemoteCursorShape::move},
{"fleur", RemoteCursorShape::move},
{"size-all", RemoteCursorShape::move},
{"all-scroll", RemoteCursorShape::move},
{"dnd-move", RemoteCursorShape::move},
{"no-drop", RemoteCursorShape::no_drop},
{"dnd-no-drop", RemoteCursorShape::no_drop},
{"dnd-none", RemoteCursorShape::no_drop},
{"not-allowed", RemoteCursorShape::not_allowed},
{"forbidden", RemoteCursorShape::not_allowed},
{"crossed-circle", RemoteCursorShape::not_allowed},
{"circle", RemoteCursorShape::not_allowed},
{"pirate", RemoteCursorShape::not_allowed},
{"grab", RemoteCursorShape::grab},
{"openhand", RemoteCursorShape::grab},
{"open-hand", RemoteCursorShape::grab},
{"grabbing", RemoteCursorShape::grabbing},
{"closedhand", RemoteCursorShape::grabbing},
{"closed-hand", RemoteCursorShape::grabbing},
{"col-resize", RemoteCursorShape::col_resize},
{"row-resize", RemoteCursorShape::row_resize},
{"n-resize", RemoteCursorShape::n_resize},
{"top-side", RemoteCursorShape::n_resize},
{"top-tee", RemoteCursorShape::n_resize},
{"sb-up-arrow", RemoteCursorShape::n_resize},
{"e-resize", RemoteCursorShape::e_resize},
{"right-side", RemoteCursorShape::e_resize},
{"right-tee", RemoteCursorShape::e_resize},
{"sb-right-arrow", RemoteCursorShape::e_resize},
{"s-resize", RemoteCursorShape::s_resize},
{"bottom-side", RemoteCursorShape::s_resize},
{"bottom-tee", RemoteCursorShape::s_resize},
{"sb-down-arrow", RemoteCursorShape::s_resize},
{"w-resize", RemoteCursorShape::w_resize},
{"left-side", RemoteCursorShape::w_resize},
{"left-tee", RemoteCursorShape::w_resize},
{"sb-left-arrow", RemoteCursorShape::w_resize},
{"ne-resize", RemoteCursorShape::ne_resize},
{"top-right-corner", RemoteCursorShape::ne_resize},
{"ur-angle", RemoteCursorShape::ne_resize},
{"nw-resize", RemoteCursorShape::nw_resize},
{"top-left-corner", RemoteCursorShape::nw_resize},
{"ul-angle", RemoteCursorShape::nw_resize},
{"se-resize", RemoteCursorShape::se_resize},
{"bottom-right-corner", RemoteCursorShape::se_resize},
{"lr-angle", RemoteCursorShape::se_resize},
{"sw-resize", RemoteCursorShape::sw_resize},
{"bottom-left-corner", RemoteCursorShape::sw_resize},
{"ll-angle", RemoteCursorShape::sw_resize},
{"ew-resize", RemoteCursorShape::ew_resize},
{"sb-h-double-arrow", RemoteCursorShape::ew_resize},
{"h-double-arrow", RemoteCursorShape::ew_resize},
{"size-hor", RemoteCursorShape::ew_resize},
{"split-h", RemoteCursorShape::ew_resize},
{"exchange", RemoteCursorShape::ew_resize},
{"ns-resize", RemoteCursorShape::ns_resize},
{"sb-v-double-arrow", RemoteCursorShape::ns_resize},
{"v-double-arrow", RemoteCursorShape::ns_resize},
{"double-arrow", RemoteCursorShape::ns_resize},
{"size-ver", RemoteCursorShape::ns_resize},
{"split-v", RemoteCursorShape::ns_resize},
{"nesw-resize", RemoteCursorShape::nesw_resize},
{"fd-double-arrow", RemoteCursorShape::nesw_resize},
{"size-bdiag", RemoteCursorShape::nesw_resize},
{"nwse-resize", RemoteCursorShape::nwse_resize},
{"bd-double-arrow", RemoteCursorShape::nwse_resize},
{"size-fdiag", RemoteCursorShape::nwse_resize},
{"default", RemoteCursorShape::default_cursor},
{"left-ptr", RemoteCursorShape::default_cursor},
{"left-arrow", RemoteCursorShape::default_cursor},
{"arrow", RemoteCursorShape::default_cursor},
{"top-left-arrow", RemoteCursorShape::default_cursor},
{"right-ptr", RemoteCursorShape::default_cursor},
{"center-ptr", RemoteCursorShape::default_cursor},
{"context-menu", RemoteCursorShape::default_cursor},
};
std::string NormalizeCursorName(const std::string& raw_name) {
std::string normalized;
normalized.reserve(raw_name.size());
for (unsigned char ch : raw_name) {
if (ch == '_' || std::isspace(ch)) {
normalized.push_back('-');
} else {
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
}
return normalized;
}
#if defined(__linux__) && !defined(__APPLE__)
struct CursorCandidate {
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
std::vector<uint32_t> argb;
};
uint8_t Alpha(uint32_t argb) {
return static_cast<uint8_t>((argb >> 24U) & 0xffU);
}
double BilinearAlpha(const CursorCandidate& candidate, double x, double y) {
if (candidate.width == 0 || candidate.height == 0) {
return 0.0;
}
x = std::max(0.0, std::min(x, static_cast<double>(candidate.width - 1)));
y = std::max(0.0, std::min(y, static_cast<double>(candidate.height - 1)));
const uint32_t x0 = static_cast<uint32_t>(std::floor(x));
const uint32_t y0 = static_cast<uint32_t>(std::floor(y));
const uint32_t x1 = std::min(x0 + 1, candidate.width - 1);
const uint32_t y1 = std::min(y0 + 1, candidate.height - 1);
const double fx = x - x0;
const double fy = y - y0;
const auto sample = [&](uint32_t px, uint32_t py) {
return static_cast<double>(
Alpha(candidate.argb[static_cast<size_t>(py) * candidate.width + px]));
};
const double top = sample(x0, y0) * (1.0 - fx) + sample(x1, y0) * fx;
const double bottom = sample(x0, y1) * (1.0 - fx) + sample(x1, y1) * fx;
return top * (1.0 - fy) + bottom * fy;
}
double SimilarityError(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (!image.argb || image.width == 0 || image.height == 0 ||
candidate.argb.empty()) {
return std::numeric_limits<double>::infinity();
}
double alpha_error = 0.0;
for (uint32_t y = 0; y < image.height; ++y) {
const double source_y =
(static_cast<double>(y) + 0.5) * candidate.height / image.height - 0.5;
for (uint32_t x = 0; x < image.width; ++x) {
const double source_x = (static_cast<double>(x) + 0.5) *
candidate.width / image.width -
0.5;
const double expected = BilinearAlpha(candidate, source_x, source_y);
const double actual = static_cast<double>(
Alpha(image.argb[static_cast<size_t>(y) * image.width + x]));
alpha_error += std::abs(actual - expected) / 255.0;
}
}
alpha_error /= static_cast<double>(image.width) * image.height;
const double observed_hot_x =
static_cast<double>(image.xhot) / std::max(1u, image.width);
const double observed_hot_y =
static_cast<double>(image.yhot) / std::max(1u, image.height);
const double candidate_hot_x =
static_cast<double>(candidate.xhot) / std::max(1u, candidate.width);
const double candidate_hot_y =
static_cast<double>(candidate.yhot) / std::max(1u, candidate.height);
const double hotspot_error =
std::abs(observed_hot_x - candidate_hot_x) +
std::abs(observed_hot_y - candidate_hot_y);
const double observed_aspect =
static_cast<double>(image.width) / image.height;
const double candidate_aspect =
static_cast<double>(candidate.width) / candidate.height;
const double aspect_error =
std::abs(std::log(observed_aspect / candidate_aspect));
return alpha_error + hotspot_error * 0.12 + aspect_error * 0.08;
}
bool SameImage(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (image.width != candidate.width || image.height != candidate.height ||
image.xhot != candidate.xhot || image.yhot != candidate.yhot) {
return false;
}
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
return std::equal(image.argb, image.argb + pixel_count,
candidate.argb.begin());
}
#endif
} // namespace
const LinuxCursorAlias* LinuxCursorAliases(size_t* count) {
if (count) {
*count = sizeof(kCursorAliases) / sizeof(kCursorAliases[0]);
}
return kCursorAliases;
}
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& raw_name) {
const std::string name = NormalizeCursorName(raw_name);
for (const auto& alias : kCursorAliases) {
if (name == alias.name) {
return alias.shape;
}
}
// Some toolkits prefix a theme or toolkit name to the standard cursor name.
// Restrict the fallback to separators so names such as "pointer-progress"
// still prefer their exact semantic alias above.
for (const auto& alias : kCursorAliases) {
const std::string suffix = std::string("-") + alias.name;
if (name.size() > suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
return alias.shape;
}
}
return RemoteCursorShape::default_cursor;
}
#if defined(__linux__) && !defined(__APPLE__)
struct LinuxCursorThemeMatcher::Impl {
Display* display = XOpenDisplay(nullptr);
std::string theme;
int default_size = 24;
std::unordered_map<int, std::vector<CursorCandidate>> candidates_by_size;
Impl() {
if (!display) {
return;
}
if (const char* active_theme = XcursorGetTheme(display)) {
theme = active_theme;
}
const int active_size = XcursorGetDefaultSize(display);
if (active_size > 0) {
default_size = active_size;
}
}
~Impl() {
if (display) {
XCloseDisplay(display);
}
}
const std::vector<CursorCandidate>& Candidates(int requested_size) {
requested_size = std::max(1, requested_size);
auto existing = candidates_by_size.find(requested_size);
if (existing != candidates_by_size.end()) {
return existing->second;
}
std::vector<CursorCandidate> candidates;
const char* theme_name = theme.empty() ? nullptr : theme.c_str();
for (const auto& alias : kCursorAliases) {
XcursorImages* images =
XcursorLibraryLoadImages(alias.name, theme_name, requested_size);
if (!images) {
continue;
}
for (int index = 0; index < images->nimage; ++index) {
const XcursorImage* image = images->images[index];
if (!image || !image->pixels || image->width == 0 ||
image->height == 0) {
continue;
}
CursorCandidate candidate;
candidate.shape = alias.shape;
candidate.width = image->width;
candidate.height = image->height;
candidate.xhot = image->xhot;
candidate.yhot = image->yhot;
const size_t pixel_count = static_cast<size_t>(image->width) *
static_cast<size_t>(image->height);
candidate.argb.assign(image->pixels, image->pixels + pixel_count);
const bool duplicate =
std::any_of(candidates.begin(), candidates.end(),
[&](const CursorCandidate& current) {
return current.shape == candidate.shape &&
current.width == candidate.width &&
current.height == candidate.height &&
current.xhot == candidate.xhot &&
current.yhot == candidate.yhot &&
current.argb == candidate.argb;
});
if (!duplicate) {
candidates.push_back(std::move(candidate));
}
}
XcursorImagesDestroy(images);
}
return candidates_by_size
.emplace(requested_size, std::move(candidates))
.first->second;
}
};
#else
struct LinuxCursorThemeMatcher::Impl {};
#endif
LinuxCursorThemeMatcher::LinuxCursorThemeMatcher()
: impl_(std::make_unique<Impl>()) {}
LinuxCursorThemeMatcher::~LinuxCursorThemeMatcher() = default;
RemoteCursorShape LinuxCursorThemeMatcher::Match(
const LinuxCursorImageView& image) {
#if defined(__linux__) && !defined(__APPLE__)
if (!impl_ || !image.argb || image.width == 0 || image.height == 0) {
return RemoteCursorShape::default_cursor;
}
const int requested_size =
static_cast<int>(std::max(image.width, image.height));
const auto& candidates = impl_->Candidates(requested_size);
for (const auto& candidate : candidates) {
if (SameImage(image, candidate)) {
return candidate.shape;
}
}
double best_error = std::numeric_limits<double>::infinity();
RemoteCursorShape best_shape = RemoteCursorShape::default_cursor;
for (const auto& candidate : candidates) {
const double error = SimilarityError(image, candidate);
if (error < best_error) {
best_error = error;
best_shape = candidate.shape;
}
}
// Scaled theme cursors normally remain well below this threshold, while an
// unrelated custom application cursor should fall back to the default.
return best_error <= 0.20 ? best_shape
: RemoteCursorShape::default_cursor;
#else
(void)image;
return RemoteCursorShape::default_cursor;
#endif
}
} // namespace crossdesk
+49
View File
@@ -0,0 +1,49 @@
#ifndef CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#define CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include "remote_cursor_shape.h"
namespace crossdesk {
struct LinuxCursorAlias {
const char* name;
RemoteCursorShape shape;
};
const LinuxCursorAlias* LinuxCursorAliases(size_t* count);
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& name);
struct LinuxCursorImageView {
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
const uint32_t* argb = nullptr;
};
// Matches a compositor-provided cursor bitmap against the active Xcursor
// theme. This is needed on Wayland, where XFixes only observes XWayland
// cursors and cannot report cursors owned by native Wayland surfaces.
class LinuxCursorThemeMatcher {
public:
LinuxCursorThemeMatcher();
~LinuxCursorThemeMatcher();
LinuxCursorThemeMatcher(const LinuxCursorThemeMatcher&) = delete;
LinuxCursorThemeMatcher& operator=(const LinuxCursorThemeMatcher&) = delete;
RemoteCursorShape Match(const LinuxCursorImageView& image);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
+45
View File
@@ -0,0 +1,45 @@
#ifndef CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
#define CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
#include <cstdint>
namespace crossdesk {
// Keep these values aligned with Slint's MouseCursor enum. The wire protocol
// intentionally carries a semantic cursor instead of a platform handle so a
// Windows, macOS or Linux host can control a different desktop platform.
enum class RemoteCursorShape : uint8_t {
default_cursor = 0,
none,
help,
pointer,
progress,
wait,
crosshair,
text,
alias,
copy,
move,
no_drop,
not_allowed,
grab,
grabbing,
col_resize,
row_resize,
n_resize,
e_resize,
s_resize,
w_resize,
ne_resize,
nw_resize,
se_resize,
sw_resize,
ew_resize,
ns_resize,
nesw_resize,
nwse_resize,
};
} // namespace crossdesk
#endif // CROSSDESK_COMMON_REMOTE_CURSOR_SHAPE_H_
+42
View File
@@ -0,0 +1,42 @@
#include "shared_cursor_state.h"
#include <mutex>
namespace crossdesk {
namespace {
std::mutex g_cursor_state_mutex;
SharedCursorState g_cursor_state;
bool g_cursor_state_available = false;
} // namespace
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = visible;
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
g_cursor_state_available = true;
}
bool GetSharedCursorState(SharedCursorState* state) {
if (!state) {
return false;
}
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
if (!g_cursor_state_available) {
return false;
}
*state = g_cursor_state;
return true;
}
void ClearSharedCursorState() {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = false;
g_cursor_state.shape = RemoteCursorShape::none;
g_cursor_state_available = false;
}
} // namespace crossdesk
+22
View File
@@ -0,0 +1,22 @@
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#include <cstdint>
#include "remote_cursor_shape.h"
namespace crossdesk {
struct SharedCursorState {
uint64_t generation = 0;
bool visible = false;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
bool GetSharedCursorState(SharedCursorState* state);
void ClearSharedCursorState();
} // namespace crossdesk
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
+1 -35
View File
@@ -15,6 +15,7 @@
#include <string>
#include "display_info.h"
#include "remote_cursor_shape.h"
using json = nlohmann::json;
namespace crossdesk {
@@ -71,41 +72,6 @@ typedef struct {
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
// Keep these values aligned with Slint's MouseCursor enum. The wire protocol
// intentionally carries a semantic cursor instead of a platform handle so a
// Windows, macOS or Linux host can control a different desktop platform.
enum class RemoteCursorShape : uint8_t {
default_cursor = 0,
none,
help,
pointer,
progress,
wait,
crosshair,
text,
alias,
copy,
move,
no_drop,
not_allowed,
grab,
grabbing,
col_resize,
row_resize,
n_resize,
e_resize,
s_resize,
w_resize,
ne_resize,
nw_resize,
se_resize,
sw_resize,
ew_resize,
ns_resize,
nesw_resize,
nwse_resize,
};
typedef struct {
uint32_t seq;
bool visible;
+5
View File
@@ -1853,6 +1853,11 @@ void GuiApplication::ShareLocalCursorState() {
return;
}
if (changed) {
LOG_INFO("Sent cursor state: seq={}, visible={}, shape={}", sampled.seq,
sampled.visible, static_cast<int>(sampled.shape));
}
last_shared_cursor_state_ = sampled;
has_shared_cursor_state_ = true;
last_cursor_state_share_time_ = now;
+18 -74
View File
@@ -60,83 +60,15 @@ bool CursorStateProvider::Sample(CursorState* state) {
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <algorithm>
#include <cctype>
#include <string>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
namespace crossdesk {
namespace {
std::string Lowercase(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char ch) { return std::tolower(ch); });
return value;
}
bool Contains(const std::string& value, const char* token) {
return value.find(token) != std::string::npos;
}
RemoteCursorShape ShapeFromXCursorName(const std::string& raw_name) {
const std::string name = Lowercase(raw_name);
if (Contains(name, "left_ptr_watch") || Contains(name, "progress"))
return RemoteCursorShape::progress;
if (Contains(name, "watch") || Contains(name, "wait"))
return RemoteCursorShape::wait;
if (Contains(name, "question") || Contains(name, "help"))
return RemoteCursorShape::help;
if (Contains(name, "xterm") || Contains(name, "vertical-text") ||
name == "text")
return RemoteCursorShape::text;
if (Contains(name, "crosshair") || name == "cross" || name == "tcross")
return RemoteCursorShape::crosshair;
if (Contains(name, "closedhand") || Contains(name, "grabbing"))
return RemoteCursorShape::grabbing;
if (Contains(name, "openhand") || Contains(name, "grab"))
return RemoteCursorShape::grab;
if (Contains(name, "dnd-link") || name == "alias")
return RemoteCursorShape::alias;
if (Contains(name, "hand") || Contains(name, "pointer") ||
Contains(name, "link"))
return RemoteCursorShape::pointer;
if (Contains(name, "dnd-copy") || name == "copy")
return RemoteCursorShape::copy;
if (Contains(name, "no-drop")) return RemoteCursorShape::no_drop;
if (Contains(name, "not-allowed") || Contains(name, "crossed_circle"))
return RemoteCursorShape::not_allowed;
if (name == "fleur" || Contains(name, "size_all") || name == "move")
return RemoteCursorShape::move;
if (Contains(name, "top_left_corner") ||
Contains(name, "bottom_right_corner") ||
Contains(name, "nwse-resize") || Contains(name, "size_fdiag"))
return RemoteCursorShape::nwse_resize;
if (Contains(name, "top_right_corner") ||
Contains(name, "bottom_left_corner") ||
Contains(name, "nesw-resize") || Contains(name, "size_bdiag"))
return RemoteCursorShape::nesw_resize;
if (Contains(name, "sb_h_double_arrow") || Contains(name, "ew-resize") ||
Contains(name, "size_hor"))
return RemoteCursorShape::ew_resize;
if (Contains(name, "sb_v_double_arrow") || Contains(name, "ns-resize") ||
Contains(name, "size_ver"))
return RemoteCursorShape::ns_resize;
if (Contains(name, "col-resize")) return RemoteCursorShape::col_resize;
if (Contains(name, "row-resize")) return RemoteCursorShape::row_resize;
if (Contains(name, "ne-resize")) return RemoteCursorShape::ne_resize;
if (Contains(name, "nw-resize")) return RemoteCursorShape::nw_resize;
if (Contains(name, "se-resize")) return RemoteCursorShape::se_resize;
if (Contains(name, "sw-resize")) return RemoteCursorShape::sw_resize;
if (Contains(name, "top_side") || name == "n-resize")
return RemoteCursorShape::n_resize;
if (Contains(name, "right_side") || name == "e-resize")
return RemoteCursorShape::e_resize;
if (Contains(name, "bottom_side") || name == "s-resize")
return RemoteCursorShape::s_resize;
if (Contains(name, "left_side") || name == "w-resize")
return RemoteCursorShape::w_resize;
return RemoteCursorShape::default_cursor;
}
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
@@ -160,7 +92,19 @@ CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
if (!state || !impl_ || !impl_->display) return false;
if (!state || !impl_) return false;
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
}
}
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
if (!image) return false;
@@ -169,7 +113,7 @@ bool CursorStateProvider::Sample(CursorState* state) {
state->seq = 0;
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromXCursorName(name)
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
XFree(image);
return true;
+9
View File
@@ -183,8 +183,17 @@ void PeerEventHandler::OnReceiveDataBuffer(
!props->remote_cursor_state_received_ ||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
if (is_newer) {
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
props->remote_cursor_state_ = remote_action.cs;
props->remote_cursor_state_received_ = true;
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
}
}
return;
}
@@ -12,8 +12,10 @@
#include <cstring>
#include <thread>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "rd_log.h"
#include "shared_cursor_state.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
@@ -35,7 +37,8 @@ constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200);
} // namespace
ScreenCapturerWayland::ScreenCapturerWayland() {}
ScreenCapturerWayland::ScreenCapturerWayland()
: cursor_theme_matcher_(std::make_unique<LinuxCursorThemeMatcher>()) {}
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
@@ -66,6 +69,10 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
callback_ = cb;
pointer_granted_ = false;
shared_session_registered_ = false;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
display_info_list_.clear();
display_info_list_.push_back(
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
@@ -100,6 +107,10 @@ int ScreenCapturerWayland::Start(bool show_cursor) {
}
show_cursor_ = show_cursor;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
paused_ = false;
pipewire_node_id_ = 0;
UpdateDisplayGeometry(
@@ -119,6 +130,10 @@ int ScreenCapturerWayland::Stop() {
thread_.join();
}
pipewire_node_id_ = 0;
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
UpdateDisplayGeometry(
logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_ : kFallbackHeight);
@@ -15,6 +15,7 @@ struct pw_thread_loop;
#include <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include <vector>
@@ -23,6 +24,8 @@ struct pw_thread_loop;
namespace crossdesk {
class LinuxCursorThemeMatcher;
class ScreenCapturerWayland : public ScreenCapturer {
public:
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
@@ -51,6 +54,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
bool CreatePortalSession();
bool SelectPortalDevices();
bool SelectPortalSource();
bool GetAvailableCursorModes(uint32_t* modes) const;
bool StartPortalSession();
bool EnsurePipeWireRuntimeAvailable() const;
bool OpenPipeWireRemote();
@@ -95,6 +99,9 @@ class ScreenCapturerWayland : public ScreenCapturer {
bool pipewire_thread_loop_started_ = false;
bool pointer_granted_ = false;
bool shared_session_registered_ = false;
bool cursor_metadata_enabled_ = false;
bool cursor_metadata_seen_ = false;
uint32_t cursor_metadata_missing_buffers_ = 0;
bool portal_has_logical_size_ = false;
uint32_t spa_video_format_ = 0;
int frame_width_ = 0;
@@ -104,6 +111,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
int portal_stream_height_ = 0;
int logical_width_ = 0;
int logical_height_ = 0;
std::unique_ptr<LinuxCursorThemeMatcher> cursor_theme_matcher_;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
@@ -17,7 +17,9 @@
#include "display_stream_id.h"
#include "libyuv.h"
#include "linux_cursor_shape.h"
#include "rd_log.h"
#include "shared_cursor_state.h"
namespace crossdesk {
@@ -199,6 +201,145 @@ int64_t NowMs() {
.count();
}
constexpr uint32_t kMaxCursorDimension = 512;
constexpr int kCursorMetadataSize =
static_cast<int>(sizeof(spa_meta_cursor) + sizeof(spa_meta_bitmap) +
64u + kMaxCursorDimension * kMaxCursorDimension * 4u);
enum class CursorBitmapResult { kNoUpdate, kInvisible, kImage, kInvalid };
CursorBitmapResult ReadCursorBitmap(const spa_meta& metadata,
const spa_meta_cursor& cursor,
std::vector<uint32_t>* argb,
LinuxCursorImageView* view) {
if (!metadata.data || !argb || !view || cursor.bitmap_offset == 0) {
return CursorBitmapResult::kNoUpdate;
}
const size_t bitmap_offset = cursor.bitmap_offset;
if (bitmap_offset > metadata.size ||
metadata.size - bitmap_offset < sizeof(spa_meta_bitmap)) {
return CursorBitmapResult::kInvalid;
}
const auto* metadata_bytes = static_cast<const uint8_t*>(metadata.data);
const auto* bitmap = reinterpret_cast<const spa_meta_bitmap*>(
metadata_bytes + bitmap_offset);
if (bitmap->format == 0) {
return CursorBitmapResult::kNoUpdate;
}
if (bitmap->offset == 0) {
return CursorBitmapResult::kInvisible;
}
const uint32_t width = bitmap->size.width;
const uint32_t height = bitmap->size.height;
if (width == 0 || height == 0 || width > kMaxCursorDimension ||
height > kMaxCursorDimension || bitmap->stride < 0 ||
static_cast<uint32_t>(bitmap->stride) < width * 4u) {
return CursorBitmapResult::kInvalid;
}
const size_t pixel_offset = bitmap_offset + bitmap->offset;
const size_t required_size =
static_cast<size_t>(bitmap->stride) * (height - 1u) + width * 4u;
if (pixel_offset > metadata.size ||
required_size > metadata.size - pixel_offset) {
return CursorBitmapResult::kInvalid;
}
enum class ChannelOrder { kBgra, kRgba, kArgb, kAbgr, kBgrx, kRgbx };
ChannelOrder channel_order;
switch (bitmap->format) {
case SPA_VIDEO_FORMAT_BGRA:
channel_order = ChannelOrder::kBgra;
break;
case SPA_VIDEO_FORMAT_RGBA:
channel_order = ChannelOrder::kRgba;
break;
case SPA_VIDEO_FORMAT_ARGB:
channel_order = ChannelOrder::kArgb;
break;
case SPA_VIDEO_FORMAT_ABGR:
channel_order = ChannelOrder::kAbgr;
break;
case SPA_VIDEO_FORMAT_BGRx:
channel_order = ChannelOrder::kBgrx;
break;
case SPA_VIDEO_FORMAT_RGBx:
channel_order = ChannelOrder::kRgbx;
break;
default:
return CursorBitmapResult::kInvalid;
}
argb->resize(static_cast<size_t>(width) * height);
const uint8_t* pixels = metadata_bytes + pixel_offset;
bool has_visible_pixel = false;
for (uint32_t y = 0; y < height; ++y) {
const uint8_t* row = pixels + static_cast<size_t>(y) * bitmap->stride;
for (uint32_t x = 0; x < width; ++x) {
const uint8_t* pixel = row + static_cast<size_t>(x) * 4u;
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
uint8_t alpha = 0xff;
switch (channel_order) {
case ChannelOrder::kBgra:
blue = pixel[0];
green = pixel[1];
red = pixel[2];
alpha = pixel[3];
break;
case ChannelOrder::kRgba:
red = pixel[0];
green = pixel[1];
blue = pixel[2];
alpha = pixel[3];
break;
case ChannelOrder::kArgb:
alpha = pixel[0];
red = pixel[1];
green = pixel[2];
blue = pixel[3];
break;
case ChannelOrder::kAbgr:
alpha = pixel[0];
blue = pixel[1];
green = pixel[2];
red = pixel[3];
break;
case ChannelOrder::kBgrx:
blue = pixel[0];
green = pixel[1];
red = pixel[2];
break;
case ChannelOrder::kRgbx:
red = pixel[0];
green = pixel[1];
blue = pixel[2];
break;
}
has_visible_pixel = has_visible_pixel || alpha != 0;
(*argb)[static_cast<size_t>(y) * width + x] =
(static_cast<uint32_t>(alpha) << 24U) |
(static_cast<uint32_t>(red) << 16U) |
(static_cast<uint32_t>(green) << 8U) | blue;
}
}
if (!has_visible_pixel) {
return CursorBitmapResult::kInvisible;
}
view->width = width;
view->height = height;
view->xhot = static_cast<uint32_t>(std::max(0, cursor.hotspot.x));
view->yhot = static_cast<uint32_t>(std::max(0, cursor.hotspot.y));
view->argb = argb->data();
return CursorBitmapResult::kImage;
}
double SnapLikelyFractionalScale(double observed_scale) {
static constexpr double kCandidates[] = {
1.0, 1.25, 1.3333333333, 1.5, 1.6666666667, 1.75, 2.0, 2.25, 2.5, 3.0};
@@ -500,7 +641,7 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[2];
const spa_pod* params[3];
uint32_t param_count = 0;
params[param_count++] =
@@ -525,10 +666,29 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_Int(sizeof(struct spa_meta_header))));
if (self->cursor_metadata_enabled_) {
params[param_count++] =
reinterpret_cast<const spa_pod*>(spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
CROSSDESK_SPA_PARAM_META_TYPE, SPA_POD_Id(SPA_META_Cursor),
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_CHOICE_RANGE_Int(
kCursorMetadataSize,
static_cast<int>(sizeof(struct spa_meta_cursor)),
kCursorMetadataSize)));
}
if (self->pw_stream_) {
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
if (pipewire) {
pipewire->stream_update_params(self->pw_stream_, params, param_count);
const int update_result = pipewire->stream_update_params(
self->pw_stream_, params, param_count);
LOG_INFO(
"PipeWire buffer parameters updated: cursor_metadata={}, "
"cursor_meta_max_size={}, result={}",
self->cursor_metadata_enabled_,
self->cursor_metadata_enabled_ ? kCursorMetadataSize : 0,
update_result);
}
}
self->pipewire_format_ready_.store(true);
@@ -786,6 +946,57 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
return;
}
if (cursor_metadata_enabled_) {
spa_meta* cursor_metadata =
spa_buffer_find_meta(spa_buffer, SPA_META_Cursor);
if (!cursor_metadata) {
++cursor_metadata_missing_buffers_;
if (cursor_metadata_missing_buffers_ == 120) {
LOG_WARN(
"PipeWire stream has not attached SPA_META_Cursor after 120 "
"buffers; cursor metadata negotiation was not accepted");
}
} else {
cursor_metadata_missing_buffers_ = 0;
if (!cursor_metadata_seen_) {
cursor_metadata_seen_ = true;
LOG_INFO("PipeWire cursor metadata attached, size={}",
cursor_metadata->size);
}
}
if (cursor_metadata && cursor_metadata->data &&
cursor_metadata->size >= sizeof(spa_meta_cursor)) {
const auto* cursor =
static_cast<const spa_meta_cursor*>(cursor_metadata->data);
if (spa_meta_cursor_is_valid(cursor)) {
std::vector<uint32_t> cursor_argb;
LinuxCursorImageView cursor_view;
const CursorBitmapResult cursor_result = ReadCursorBitmap(
*cursor_metadata, *cursor, &cursor_argb, &cursor_view);
if (cursor_result == CursorBitmapResult::kInvisible) {
PublishSharedCursorState(false, RemoteCursorShape::none);
LOG_INFO("PipeWire cursor became invisible, id={}", cursor->id);
} else if (cursor_result == CursorBitmapResult::kImage) {
const RemoteCursorShape shape =
cursor_theme_matcher_
? cursor_theme_matcher_->Match(cursor_view)
: RemoteCursorShape::default_cursor;
PublishSharedCursorState(true, shape);
LOG_INFO(
"PipeWire cursor shape update: id={}, size={}x{}, "
"hotspot=({},{}), shape={}",
cursor->id, cursor_view.width, cursor_view.height,
cursor_view.xhot, cursor_view.yhot, static_cast<int>(shape));
} else if (cursor_result == CursorBitmapResult::kInvalid) {
PublishSharedCursorState(true, RemoteCursorShape::default_cursor);
LOG_WARN("Using default shape for malformed PipeWire cursor "
"metadata, id={}",
cursor->id);
}
}
}
}
const spa_data& data = spa_buffer->datas[0];
if (!data.chunk) {
requeue();
@@ -12,6 +12,7 @@
#include <string>
#include "rd_log.h"
#include "shared_cursor_state.h"
namespace crossdesk {
@@ -35,6 +36,7 @@ constexpr const char* kPortalSessionPathPrefix =
constexpr uint32_t kScreenCastSourceMonitor = 1u;
constexpr uint32_t kCursorModeHidden = 1u;
constexpr uint32_t kCursorModeEmbedded = 2u;
constexpr uint32_t kCursorModeMetadata = 4u;
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
std::string MakeToken(const char* prefix) {
@@ -482,6 +484,47 @@ bool ScreenCapturerWayland::SelectPortalSource() {
return false;
}
uint32_t available_cursor_modes = kCursorModeHidden | kCursorModeEmbedded;
if (!GetAvailableCursorModes(&available_cursor_modes)) {
LOG_WARN(
"Unable to query portal cursor modes; using embedded cursor fallback");
}
uint32_t cursor_mode = 0;
cursor_metadata_enabled_ = false;
if (!show_cursor_ && (available_cursor_modes & kCursorModeMetadata) != 0) {
cursor_mode = kCursorModeMetadata;
cursor_metadata_enabled_ = true;
} else if ((available_cursor_modes & kCursorModeEmbedded) != 0) {
cursor_mode = kCursorModeEmbedded;
} else if ((available_cursor_modes & kCursorModeMetadata) != 0) {
cursor_mode = kCursorModeMetadata;
cursor_metadata_enabled_ = true;
} else if ((available_cursor_modes & kCursorModeHidden) != 0) {
cursor_mode = kCursorModeHidden;
}
if (cursor_mode == 0) {
LOG_ERROR("Wayland portal reports no supported cursor mode");
return false;
}
if (cursor_mode == kCursorModeEmbedded) {
// When metadata is unavailable, embedding is the only way to preserve the
// exact Wayland cursor. Hide the controller-side cursor to avoid a double
// cursor over the video stream.
PublishSharedCursorState(false, RemoteCursorShape::none);
} else if (!cursor_metadata_enabled_) {
// A portal without metadata or embedded cursor support cannot expose the
// native Wayland shape. Keep a visible default cursor on the controller.
PublishSharedCursorState(true, RemoteCursorShape::default_cursor);
}
LOG_INFO("Wayland portal cursor mode: {}",
cursor_mode == kCursorModeMetadata
? "metadata"
: (cursor_mode == kCursorModeEmbedded ? "embedded" : "hidden"));
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
@@ -496,9 +539,7 @@ bool ScreenCapturerWayland::SelectPortalSource() {
&options);
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
AppendDictEntryBool(&options, "multiple", false);
AppendDictEntryUint32(
&options, "cursor_mode",
show_cursor_ ? kCursorModeEmbedded : kCursorModeHidden);
AppendDictEntryUint32(&options, "cursor_mode", cursor_mode);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
@@ -515,6 +556,50 @@ bool ScreenCapturerWayland::SelectPortalSource() {
});
}
bool ScreenCapturerWayland::GetAvailableCursorModes(uint32_t* modes) const {
if (!dbus_connection_ || !modes) {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, "org.freedesktop.DBus.Properties",
"Get");
if (!message) {
return false;
}
const char* interface_name = kPortalScreenCastInterface;
const char* property_name = "AvailableCursorModes";
dbus_message_append_args(message, DBUS_TYPE_STRING, &interface_name,
DBUS_TYPE_STRING, &property_name,
DBUS_TYPE_INVALID);
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
dbus_connection_, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError("Get AvailableCursorModes", &error);
dbus_error_free(&error);
return false;
}
DBusMessageIter iter;
bool parsed = false;
if (dbus_message_iter_init(reply, &iter) &&
dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&iter, &variant);
if (dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_UINT32) {
dbus_message_iter_get_basic(&variant, modes);
parsed = true;
}
}
dbus_message_unref(reply);
return parsed;
}
bool ScreenCapturerWayland::SelectPortalDevices() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
+1 -1
View File
@@ -37,7 +37,7 @@ function setup_platform_settings()
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
"Xrandr", "Xfixes")
"Xrandr", "Xfixes", "Xcursor")
add_existing_include_dirs({
"/usr/include/freetype2",
"/usr/local/include/freetype2"