[fix] restore remote cursors on Linux without a pointing device, refs #130

This commit is contained in:
dijunkun
2026-10-29 10:43:53 +08:00
parent 273cbc0adf
commit 199d29a6dd
9 changed files with 381 additions and 21 deletions
@@ -13,11 +13,16 @@ bool g_cursor_state_available = false;
} // namespace
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
void PublishSharedCursorState(CursorRenderMode mode, 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.visible = mode == CursorRenderMode::separate;
g_cursor_state.shape = g_cursor_state.visible ? shape : RemoteCursorShape::none;
g_cursor_state.render_mode = mode;
g_cursor_state.hidden_reason = mode == CursorRenderMode::hidden
? CursorHiddenReason::system_hidden
: mode == CursorRenderMode::unknown ? CursorHiddenReason::sampling_failed
: CursorHiddenReason::unspecified;
g_cursor_state_available = true;
}
@@ -38,6 +43,8 @@ void ClearSharedCursorState() {
++g_cursor_state.generation;
g_cursor_state.visible = false;
g_cursor_state.shape = RemoteCursorShape::none;
g_cursor_state.render_mode = CursorRenderMode::unknown;
g_cursor_state.hidden_reason = CursorHiddenReason::sampling_failed;
g_cursor_state_available = false;
}
@@ -3,7 +3,7 @@
#include <cstdint>
#include <remote_cursor_shape.h>
#include <remote_action.h>
namespace crossdesk {
@@ -11,9 +11,11 @@ struct SharedCursorState {
uint64_t generation = 0;
bool visible = false;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
CursorRenderMode render_mode = CursorRenderMode::unknown;
CursorHiddenReason hidden_reason = CursorHiddenReason::sampling_failed;
};
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
void PublishSharedCursorState(CursorRenderMode mode, RemoteCursorShape shape);
bool GetSharedCursorState(SharedCursorState* state);
void ClearSharedCursorState();
@@ -71,6 +71,7 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
#include <string>
#include "linux_cursor_shape.h"
#include "linux_pointer_presence.h"
#include "platform.h"
#include "shared_cursor_state.h"
#include "runtime/cursor_position.h"
@@ -90,7 +91,9 @@ bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
} // namespace
struct CursorStateProvider::Impl {
const bool wayland = IsWaylandSession();
Display* display = XOpenDisplay(nullptr);
LinuxPointerPresence pointer_presence;
~Impl() {
if (display) XCloseDisplay(display);
@@ -103,18 +106,26 @@ CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state || !impl_) return false;
*state = {};
ResetCursorPosition(state);
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
SharedCursorState shared{};
if (impl_->wayland && GetSharedCursorState(&shared)) {
// XWayland only knows about X11 surfaces, not the captured Wayland
// desktop. Missing native metadata must remain an unavailable sample.
state->visible = shared.visible;
state->shape = shared.shape;
state->render_mode = shared.render_mode;
state->hidden_reason = shared.hidden_reason;
if (state->render_mode == CursorRenderMode::hidden &&
impl_->pointer_presence.NoPointingDevice(nullptr, true)) {
state->hidden_reason = CursorHiddenReason::no_pointing_device;
}
return state->render_mode != CursorRenderMode::unknown;
}
// Linux may use X11 capture even in a Wayland session. Only an active
// portal publishes shared state; preserve X11 sampling for that backend.
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
@@ -126,6 +137,12 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
state->render_mode = state->visible ? CursorRenderMode::separate
: CursorRenderMode::hidden;
state->hidden_reason = state->visible ? CursorHiddenReason::unspecified
: impl_->pointer_presence.NoPointingDevice(impl_->display, false)
? CursorHiddenReason::no_pointing_device
: CursorHiddenReason::system_hidden;
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
state);
XFree(image);
@@ -0,0 +1,292 @@
#include "linux_pointer_presence.h"
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>
#include <dlfcn.h>
#include <poll.h>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <map>
#include <string>
namespace crossdesk {
namespace {
enum class PointerPresence { unknown, present, absent };
PointerPresence QueryX11PointerPresence(Display* display) {
if (!display) return PointerPresence::unknown;
int opcode = 0, event = 0, error = 0;
int major = 2, minor = 0;
if (!XQueryExtension(display, "XInputExtension", &opcode, &event, &error) ||
XIQueryVersion(display, &major, &minor) != Success || major < 2) {
return PointerPresence::unknown;
}
int count = 0;
XIDeviceInfo* devices = XIQueryDevice(display, XIAllDevices, &count);
if (!devices) return PointerPresence::unknown;
auto result = count > 0 ? PointerPresence::absent : PointerPresence::unknown;
for (int i = 0; i < count; ++i) {
const auto& device = devices[i];
if (!device.enabled || device.use != XISlavePointer) continue;
// Xorg creates an XTEST slave for every master even without hardware.
// Match its generated name to its master, not a broad "virtual" filter:
// mice exposed by xrdp and other virtual desktops still count as pointers.
bool xtest = false;
for (int j = 0; j < count && device.name; ++j) {
const auto& master = devices[j];
if (master.deviceid != device.attachment ||
master.use != XIMasterPointer || !master.name)
continue;
const std::string name = master.name;
constexpr char suffix[] = " pointer";
if (name.size() >= sizeof(suffix) - 1 &&
name.compare(name.size() - (sizeof(suffix) - 1), sizeof(suffix) - 1,
suffix) == 0) {
xtest = name.substr(0, name.size() - (sizeof(suffix) - 1)) +
" XTEST pointer" ==
device.name;
}
break;
}
if (!xtest) {
result = PointerPresence::present;
break;
}
}
XIFreeDeviceInfo(devices);
return result;
}
// Load the public client ABI, as the Wayland activation helper does. This
// keeps X11-only installations free of a new build/runtime dependency. The
// protocol interfaces themselves come from libwayland-client.
struct WaylandInterface;
struct WaylandApi {
void* library = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_LOCAL);
void* (*connect)(const char*) = nullptr;
void (*disconnect)(void*) = nullptr;
int (*get_fd)(void*) = nullptr;
int (*dispatch_pending)(void*) = nullptr;
int (*prepare_read)(void*) = nullptr;
void (*cancel_read)(void*) = nullptr;
int (*read_events)(void*) = nullptr;
int (*flush)(void*) = nullptr;
void (*destroy_proxy)(void*) = nullptr;
void* (*construct)(void*, uint32_t, const WaylandInterface*, uint32_t,
...) = nullptr;
int (*add_listener)(void*, void (**)(void), void*) = nullptr;
const WaylandInterface* registry_interface = nullptr;
const WaylandInterface* seat_interface = nullptr;
const WaylandInterface* callback_interface = nullptr;
bool available = false;
template <class T>
bool Load(T& target, const char* name) {
target = reinterpret_cast<T>(dlsym(library, name));
return target != nullptr;
}
WaylandApi() {
available = library && Load(connect, "wl_display_connect") &&
Load(disconnect, "wl_display_disconnect") &&
Load(get_fd, "wl_display_get_fd") &&
Load(dispatch_pending, "wl_display_dispatch_pending") &&
Load(prepare_read, "wl_display_prepare_read") &&
Load(cancel_read, "wl_display_cancel_read") &&
Load(read_events, "wl_display_read_events") &&
Load(flush, "wl_display_flush") &&
Load(destroy_proxy, "wl_proxy_destroy") &&
Load(construct, "wl_proxy_marshal_constructor_versioned") &&
Load(add_listener, "wl_proxy_add_listener") &&
Load(registry_interface, "wl_registry_interface") &&
Load(seat_interface, "wl_seat_interface") &&
Load(callback_interface, "wl_callback_interface");
}
~WaylandApi() {
if (library) dlclose(library);
}
};
class WaylandPointerPresence {
public:
~WaylandPointerPresence() { Reset(); }
PointerPresence Query() {
if (!api_.available) return PointerPresence::unknown;
if ((!display_ && !Connect()) || !Pump() || failed_) {
Reset();
return PointerPresence::unknown;
}
if (!registry_ready_) return PointerPresence::unknown;
bool pending = false;
for (const auto& [id, seat] : seats_) {
if (seat.presence == PointerPresence::present)
return PointerPresence::present;
pending = pending || seat.presence == PointerPresence::unknown;
}
return pending ? PointerPresence::unknown : PointerPresence::absent;
}
private:
struct Seat {
void* proxy = nullptr;
PointerPresence presence = PointerPresence::unknown;
};
bool Connect() {
// A dedicated connection avoids consuming Slint's events or depending on
// which toolkit/display backend the local window uses.
display_ = api_.connect(nullptr);
if (!display_) return false;
registry_ =
api_.construct(display_, 1, api_.registry_interface, 1, nullptr);
sync_ = api_.construct(display_, 0, api_.callback_interface, 1, nullptr);
static void (*registry_listener[])(void) = {
reinterpret_cast<void (*)(void)>(&Global),
reinterpret_cast<void (*)(void)>(&GlobalRemove)};
static void (*sync_listener[])(void) = {
reinterpret_cast<void (*)(void)>(&Ready)};
return registry_ && sync_ &&
api_.add_listener(registry_, registry_listener, this) == 0 &&
api_.add_listener(sync_, sync_listener, this) == 0;
}
bool Pump() {
if (api_.dispatch_pending(display_) < 0) return false;
if (api_.prepare_read(display_) != 0) return false;
const int flushed = api_.flush(display_);
if (flushed < 0 && errno != EAGAIN) {
api_.cancel_read(display_);
return false;
}
pollfd fd{api_.get_fd(display_), POLLIN, 0};
if (fd.fd < 0) {
api_.cancel_read(display_);
return false;
}
const int ready = poll(&fd, 1, 0);
if (ready < 0 || (fd.revents & (POLLERR | POLLHUP | POLLNVAL))) {
api_.cancel_read(display_);
return false;
}
if (!(fd.revents & POLLIN)) {
api_.cancel_read(display_);
return true;
}
// Never perform a blocking roundtrip on the UI's cursor-sampling tick.
return api_.read_events(display_) >= 0 &&
api_.dispatch_pending(display_) >= 0;
}
void Reset() {
for (auto& [id, seat] : seats_) {
if (seat.proxy) api_.destroy_proxy(seat.proxy);
}
seats_.clear();
if (sync_) api_.destroy_proxy(sync_);
if (registry_) api_.destroy_proxy(registry_);
if (display_) api_.disconnect(display_);
display_ = registry_ = sync_ = nullptr;
registry_ready_ = failed_ = false;
}
static void Global(void* data, void* registry, uint32_t id,
const char* interface, uint32_t version) {
auto& self = *static_cast<WaylandPointerPresence*>(data);
if (std::strcmp(interface, "wl_seat") != 0) return;
if (version < 1 || self.seats_.count(id)) {
self.failed_ = true;
return;
}
auto& seat = self.seats_[id];
seat.proxy = self.api_.construct(registry, 0, self.api_.seat_interface, 1,
id, "wl_seat", uint32_t{1}, nullptr);
// Version 1 only emits capabilities; seat names are not needed.
static void (*listener[])(void) = {
reinterpret_cast<void (*)(void)>(&Capabilities)};
if (!seat.proxy ||
self.api_.add_listener(seat.proxy, listener, &seat) != 0) {
self.failed_ = true;
}
}
static void GlobalRemove(void* data, void*, uint32_t id) {
auto& self = *static_cast<WaylandPointerPresence*>(data);
const auto it = self.seats_.find(id);
if (it == self.seats_.end()) return;
if (it->second.proxy) self.api_.destroy_proxy(it->second.proxy);
self.seats_.erase(it);
}
static void Capabilities(void* data, void*, uint32_t capabilities) {
// WL_SEAT_CAPABILITY_POINTER; keyboard/touch alone are not pointers.
static_cast<Seat*>(data)->presence = (capabilities & 1u)
? PointerPresence::present
: PointerPresence::absent;
}
static void Ready(void* data, void* callback, uint32_t) {
auto& self = *static_cast<WaylandPointerPresence*>(data);
self.registry_ready_ = true;
self.api_.destroy_proxy(callback);
self.sync_ = nullptr;
}
WaylandApi api_;
void* display_ = nullptr;
void* registry_ = nullptr;
void* sync_ = nullptr;
std::map<uint32_t, Seat> seats_;
bool registry_ready_ = false;
bool failed_ = false;
};
} // namespace
struct LinuxPointerPresence::Impl {
std::unique_ptr<WaylandPointerPresence> wayland;
std::chrono::steady_clock::time_point next_check{};
void* source_display = nullptr;
bool source_wayland = false;
bool absent_before = false;
bool confirmed = false;
};
LinuxPointerPresence::LinuxPointerPresence()
: impl_(std::make_unique<Impl>()) {}
LinuxPointerPresence::~LinuxPointerPresence() = default;
bool LinuxPointerPresence::NoPointingDevice(void* x11_display, bool wayland) {
if (impl_->source_wayland != wayland ||
impl_->source_display != x11_display) {
impl_->next_check = {};
impl_->absent_before = impl_->confirmed = false;
impl_->source_display = x11_display;
impl_->source_wayland = wayland;
}
const auto now = std::chrono::steady_clock::now();
if (now < impl_->next_check) return impl_->confirmed;
// Do not combine absence samples from unrelated, widely separated periods
// of cursor hiding. Device changes and failed queries break the sequence.
if (now > impl_->next_check + std::chrono::seconds(1))
impl_->absent_before = false;
impl_->next_check = now + std::chrono::seconds(1);
PointerPresence presence;
if (wayland) {
if (!impl_->wayland)
impl_->wayland = std::make_unique<WaylandPointerPresence>();
presence = impl_->wayland->Query();
} else {
presence = QueryX11PointerPresence(static_cast<Display*>(x11_display));
}
const bool absent = presence == PointerPresence::absent;
impl_->confirmed = absent && impl_->absent_before;
impl_->absent_before = absent;
return impl_->confirmed;
}
} // namespace crossdesk
@@ -0,0 +1,30 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-29
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LINUX_POINTER_PRESENCE_H_
#define _LINUX_POINTER_PRESENCE_H_
#include <memory>
namespace crossdesk {
// Owned by the cursor sampler. Only confirmed absence permits a remote
// locator; unavailable device information must preserve normal hiding.
class LinuxPointerPresence {
public:
LinuxPointerPresence();
~LinuxPointerPresence();
bool NoPointingDevice(void* x11_display, bool wayland);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif
@@ -110,7 +110,9 @@ int ScreenCapturerWayland::Start(bool show_cursor) {
cursor_metadata_enabled_ = false;
cursor_metadata_seen_ = false;
cursor_metadata_missing_buffers_ = 0;
ClearSharedCursorState();
// Claim the native cursor source before portal negotiation. Unknown data
// must not fall through to an unrelated XWayland cursor while we wait.
PublishSharedCursorState(CursorRenderMode::unknown, RemoteCursorShape::none);
paused_ = false;
pipewire_node_id_ = 0;
UpdateDisplayGeometry(
@@ -976,21 +976,23 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
const CursorBitmapResult cursor_result = ReadCursorBitmap(
*cursor_metadata, *cursor, &cursor_argb, &cursor_view);
if (cursor_result == CursorBitmapResult::kInvisible) {
PublishSharedCursorState(false, RemoteCursorShape::none);
PublishSharedCursorState(CursorRenderMode::hidden,
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);
PublishSharedCursorState(CursorRenderMode::separate, 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);
PublishSharedCursorState(CursorRenderMode::separate,
RemoteCursorShape::default_cursor);
LOG_WARN("Using default shape for malformed PipeWire cursor "
"metadata, id={}",
cursor->id);
@@ -489,7 +489,7 @@ bool ScreenCapturerWayland::SelectPortalSource() {
uint32_t available_cursor_modes = kCursorModeHidden | kCursorModeEmbedded;
if (!GetAvailableCursorModes(&available_cursor_modes)) {
LOG_WARN(
"Unable to query portal cursor modes; using embedded cursor fallback");
"Unable to query portal cursor modes; using hidden/embedded fallback");
}
uint32_t cursor_mode = 0;
@@ -497,6 +497,10 @@ bool ScreenCapturerWayland::SelectPortalSource() {
if (!show_cursor_ && (available_cursor_modes & kCursorModeMetadata) != 0) {
cursor_mode = kCursorModeMetadata;
cursor_metadata_enabled_ = true;
} else if (!show_cursor_ && (available_cursor_modes & kCursorModeHidden) != 0) {
// Native controllers can draw a default arrow. Without metadata, do not
// rely on an embedded pointer that may disappear with the last mouse.
cursor_mode = kCursorModeHidden;
} else if ((available_cursor_modes & kCursorModeEmbedded) != 0) {
cursor_mode = kCursorModeEmbedded;
} else if ((available_cursor_modes & kCursorModeMetadata) != 0) {
@@ -515,11 +519,14 @@ bool ScreenCapturerWayland::SelectPortalSource() {
// 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);
PublishSharedCursorState(CursorRenderMode::embedded, 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);
// Hidden capture provides no native shape. Keep a visible default cursor
// on the controller, independent of whether a local mouse is connected.
PublishSharedCursorState(CursorRenderMode::separate,
RemoteCursorShape::default_cursor);
} else {
PublishSharedCursorState(CursorRenderMode::unknown, RemoteCursorShape::none);
}
LOG_INFO("Wayland portal cursor mode: {}",
+1
View File
@@ -96,6 +96,7 @@ function setup_targets()
"apps/desktop/src/platform/linux/screen_lock.cpp",
"apps/desktop/src/platform/linux/common/*.cpp")
add_includedirs("apps/desktop/src/platform/linux/common")
add_syslinks("Xi", {public = true})
end
add_includedirs("apps/desktop/src/common", {public = true})