Skip to content

Commit 00ed013

Browse files
author
danalec
committed
linux/input: optionally emulate absolute mouse as relative motion
Some compositors implement assistive features that only track relative pointer motion; Wayland's PointerMotionAbsolute moves the cursor but such features do not follow it (e.g. the COSMIC screen magnifier focal point, pop-os/cosmic-comp#2760). Add an absolute_mouse_as_relative option (disabled by default) that converts absolute client coordinates into relative deltas. The first absolute event still anchors the cursor absolutely. Subsequent events are turned into relative deltas driven by dead reckoning. The KMS capture path publishes the real cursor-plane position every frame; the estimate snaps to it while the client is idle, and a coordinate saturated at the client's own surface edge targets the matching host edge so movement cannot stall mid-screen. On backends without cursor feedback the conversion still works from dead reckoning alone.
1 parent f273ce8 commit 00ed013

6 files changed

Lines changed: 218 additions & 1 deletion

File tree

docs/configuration.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,35 @@ editing the `conf` file in a text editor. Use the examples as reference.
691691
</tr>
692692
</table>
693693

694+
### absolute_mouse_as_relative
695+
696+
<table>
697+
<tr>
698+
<td>Description</td>
699+
<td colspan="2">
700+
When enabled, absolute mouse positions from Moonlight clients are converted to relative motion.
701+
<br>
702+
The first event still places the cursor absolutely; subsequent events are emitted as relative deltas,
703+
using the cursor position captured from the KMS cursor plane (when available) to stay in sync.
704+
<br>
705+
This can be useful on compositors whose assistive features (e.g. screen magnifiers) only track relative
706+
pointer motion.
707+
</td>
708+
</tr>
709+
<tr>
710+
<td>Default</td>
711+
<td colspan="2">@code{}
712+
disabled
713+
@endcode</td>
714+
</tr>
715+
<tr>
716+
<td>Example</td>
717+
<td colspan="2">@code{}
718+
absolute_mouse_as_relative = enabled
719+
@endcode</td>
720+
</tr>
721+
</table>
722+
694723
### keybindings
695724

696725
<table>

src/config.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ namespace config {
861861
true, // always send scancodes
862862
true, // high resolution scrolling
863863
true, // native pen/touch support
864+
false, // absolute mouse as relative (opt-in)
864865
};
865866

866867
/**
@@ -1803,6 +1804,7 @@ namespace config {
18031804

18041805
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
18051806
bool_f(vars, "native_pen_touch", input.native_pen_touch);
1807+
bool_f(vars, "absolute_mouse_as_relative", input.absolute_mouse_as_relative);
18061808

18071809
bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
18081810
bool_f(vars, "system_tray", sunshine.system_tray);

src/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ namespace config {
290290

291291
bool high_resolution_scrolling; ///< Enable high-resolution mouse-wheel events.
292292
bool native_pen_touch; ///< Enable native pen and touch injection.
293+
bool absolute_mouse_as_relative; ///< Emulate absolute client mouse input as relative motion (for compositors whose assistive features only track relative motion).
293294
};
294295

295296
namespace flag {

src/input.cpp

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ constexpr int WHEEL_DELTA = 120; ///< Standard Windows wheel delta used to norm
4242

4343
using namespace std::literals;
4444

45+
// Definitions for the cursor-position feedback published by the capture
46+
// pipeline (declarations in platform/common.h; stores in linux/kmsgrab.cpp).
47+
namespace platf {
48+
std::atomic<std::int32_t> kms_cursor_x { -1 };
49+
std::atomic<std::int32_t> kms_cursor_y { -1 };
50+
std::atomic<std::int32_t> kms_desktop_w { 0 };
51+
std::atomic<std::int32_t> kms_desktop_h { 0 };
52+
std::atomic<std::int32_t> kms_logical_w { 0 };
53+
std::atomic<std::int32_t> kms_logical_h { 0 };
54+
std::atomic<std::uint64_t> kms_cursor_seq { 0 };
55+
}
56+
4557
namespace input {
4658

4759
constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); ///< Maximum gamepads representable by the active gamepad mask.
@@ -271,6 +283,17 @@ namespace input {
271283

272284
input::touch_port_t touch_port; ///< Touch coordinate bounds for the current stream.
273285

286+
bool abs_mouse_initialized = false; ///< Whether the absolute mouse baseline is set.
287+
float abs_mouse_frac_x = 0; ///< Accumulated fractional X delta.
288+
float abs_mouse_frac_y = 0; ///< Accumulated fractional Y delta.
289+
float host_cursor_x = 0; ///< Dead-reckoning estimate of the host cursor X, touch-port pixels.
290+
float host_cursor_y = 0; ///< Dead-reckoning estimate of the host cursor Y, touch-port pixels.
291+
std::uint64_t cursor_seq_last = 0; ///< Cursor-feedback sequence seen by the previous event.
292+
float last_raw_x = 0; ///< Previous raw client X (idle detection).
293+
float last_raw_y = 0; ///< Previous raw client Y (idle detection).
294+
std::chrono::steady_clock::time_point last_client_move {}; ///< Last time the client target changed.
295+
std::chrono::steady_clock::time_point abs_dbg_last {}; ///< Rate limit for debug logging.
296+
274297
int32_t accumulated_vscroll_delta; ///< Accumulated vscroll delta.
275298
int32_t accumulated_hscroll_delta; ///< Accumulated hscroll delta.
276299
};
@@ -831,7 +854,140 @@ namespace input {
831854
touch_port_dim_y
832855
};
833856

834-
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
857+
// Optional (absolute_mouse_as_relative): emulate relative mouse movement
858+
// from absolute coordinates. Absolute motion arrives as
859+
// PointerMotionAbsolute in the compositor, which moves the cursor but does
860+
// NOT update assistive features that track relative motion only, e.g. the
861+
// COSMIC screen magnifier focal point (pop-os/cosmic-comp #2760). The
862+
// first event anchors the cursor absolutely; subsequent events are
863+
// converted to relative deltas so such features track the cursor.
864+
//
865+
if (!config::input.absolute_mouse_as_relative) {
866+
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
867+
return;
868+
}
869+
//
870+
// v6: dead reckoning is the ONLY thing in the smooth motion path (1:1, no
871+
// latency). The real cursor position published by the KMS capture path
872+
// (platf::kms_cursor_*) is consumed only when trustworthy: (a) while the
873+
// client is IDLE (>kIdleMs without raw coordinate change) the estimate is
874+
// snapped to reality — by then every in-flight move has landed; (b) while
875+
// an axis is SATURATED in the client's RAW coordinate domain, that axis
876+
// gets fresh feedback as its source of truth and its target becomes the
877+
// matching host edge. Acting on lagged feedback during free motion was
878+
// wrong twice over: v3 jumped (full-strength corrections), v4 accelerated
879+
// (partial corrections added distance proportional to speed).
880+
//
881+
// Phantom "walls" (movement blocked in one direction until you push back)
882+
// happen when the client saturates in its own coordinate space while the
883+
// host cursor sits mid-screen, and v5's one-shot wall fix could compute a
884+
// zero slide from a mis-anchored estimate. v6 removes the separate wall
885+
// fix entirely: a saturated axis simply TARGETS the host edge, so the
886+
// primary loop itself drives the cursor there, continuously and
887+
// idempotently — resting in the edge band cannot buzz, and re-entering
888+
// the band after a desync retries the slide using fresh feedback.
889+
// Set SUNSHINE_DEBUG_ABSMOUSE=1 for rate-limited diagnostics.
890+
const float port_w = (float) touch_port_dim_x;
891+
const float port_h = (float) touch_port_dim_y;
892+
893+
constexpr float kRawEdge = 8.0f;
894+
const bool at_left = x <= kRawEdge;
895+
const bool at_right = x >= width - kRawEdge;
896+
const bool at_top = y <= kRawEdge;
897+
const bool at_bottom = y >= height - kRawEdge;
898+
899+
// Client target in touch-port units; a saturated axis targets the host edge.
900+
auto target_x = std::clamp(tpcoords->first, 0.0f, port_w - 1.0f);
901+
auto target_y = std::clamp(tpcoords->second, 0.0f, port_h - 1.0f);
902+
if (at_left) target_x = 0.0f;
903+
else if (at_right) target_x = port_w - 1.0f;
904+
if (at_top) target_y = 0.0f;
905+
else if (at_bottom) target_y = port_h - 1.0f;
906+
907+
auto move_estimated = [&](int delta_x, int delta_y) {
908+
platf::move_mouse(platf_input, delta_x, delta_y);
909+
input->host_cursor_x = std::clamp(input->host_cursor_x + delta_x, 0.0f, port_w - 1.0f);
910+
input->host_cursor_y = std::clamp(input->host_cursor_y + delta_y, 0.0f, port_h - 1.0f);
911+
};
912+
913+
// Idle detection watches the client's OWN motion (raw coordinates), so a
914+
// cursor pinned against its edge still counts as idle once it stops.
915+
const auto now = std::chrono::steady_clock::now();
916+
if (std::fabs(x - input->last_raw_x) > 0.01f ||
917+
std::fabs(y - input->last_raw_y) > 0.01f) {
918+
input->last_client_move = now;
919+
}
920+
input->last_raw_x = x;
921+
input->last_raw_y = y;
922+
923+
if (!input->abs_mouse_initialized) {
924+
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
925+
input->abs_mouse_initialized = true;
926+
input->host_cursor_x = target_x;
927+
input->host_cursor_y = target_y;
928+
}
929+
else {
930+
const auto seq = platf::kms_cursor_seq.load(std::memory_order_acquire);
931+
const bool feedback_fresh = seq != 0 && seq != input->cursor_seq_last;
932+
933+
if (feedback_fresh) {
934+
input->cursor_seq_last = seq;
935+
936+
const auto phys_w = (float) platf::kms_desktop_w.load(std::memory_order_relaxed);
937+
const auto phys_h = (float) platf::kms_desktop_h.load(std::memory_order_relaxed);
938+
const auto logical_w = (float) platf::kms_logical_w.load(std::memory_order_relaxed);
939+
const auto logical_h = (float) platf::kms_logical_h.load(std::memory_order_relaxed);
940+
941+
if (phys_w > 0 && phys_h > 0 && logical_w > 0 && logical_h > 0) {
942+
// Desktop physical pixels -> compositor logical pixels.
943+
const auto real_x = (float) platf::kms_cursor_x.load(std::memory_order_relaxed) * (logical_w / phys_w);
944+
const auto real_y = (float) platf::kms_cursor_y.load(std::memory_order_relaxed) * (logical_h / phys_h);
945+
946+
constexpr auto kIdleMs = std::chrono::milliseconds(150);
947+
const bool client_idle = now - input->last_client_move > kIdleMs;
948+
949+
// Idle: snap both axes (all in-flight moves have landed).
950+
// Saturated axis: fresh feedback is authoritative — the estimate may
951+
// be mis-anchored (the very cause of the wall), and any feedback lag
952+
// only overshoots toward the edge, where the compositor clamps.
953+
if (client_idle) {
954+
input->host_cursor_x = std::clamp(real_x, 0.0f, port_w - 1.0f);
955+
input->host_cursor_y = std::clamp(real_y, 0.0f, port_h - 1.0f);
956+
}
957+
else {
958+
if (at_left || at_right) input->host_cursor_x = std::clamp(real_x, 0.0f, port_w - 1.0f);
959+
if (at_top || at_bottom) input->host_cursor_y = std::clamp(real_y, 0.0f, port_h - 1.0f);
960+
}
961+
962+
static const bool dbg = ::getenv("SUNSHINE_DEBUG_ABSMOUSE") != nullptr;
963+
if (dbg) {
964+
if (now - input->abs_dbg_last > std::chrono::milliseconds(200)) {
965+
input->abs_dbg_last = now;
966+
BOOST_LOG(info) << "absmouse: raw=" << x << "," << y << "/" << width << "x" << height
967+
<< " sat=" << (at_left ? "L" : "") << (at_right ? "R" : "")
968+
<< (at_top ? "T" : "") << (at_bottom ? "B" : "")
969+
<< " target=" << target_x << "," << target_y
970+
<< " est=" << input->host_cursor_x << "," << input->host_cursor_y
971+
<< " real=" << real_x << "," << real_y;
972+
}
973+
}
974+
}
975+
}
976+
977+
// Primary loop: drive the estimate toward the (possibly edge-overridden)
978+
// target. Idempotent while resting inside the edge band.
979+
input->abs_mouse_frac_x += target_x - input->host_cursor_x;
980+
input->abs_mouse_frac_y += target_y - input->host_cursor_y;
981+
982+
auto delta_x = (int) input->abs_mouse_frac_x;
983+
auto delta_y = (int) input->abs_mouse_frac_y;
984+
input->abs_mouse_frac_x -= delta_x;
985+
input->abs_mouse_frac_y -= delta_y;
986+
987+
if (delta_x || delta_y) {
988+
move_estimated(delta_x, delta_y);
989+
}
990+
}
835991
}
836992

837993
/**

src/platform/common.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#pragma once
66

77
// standard includes
8+
#include <atomic>
89
#include <bitset>
910
#include <filesystem>
1011
#include <functional>
@@ -74,6 +75,21 @@ namespace nvenc {
7475
}
7576

7677
namespace platf {
78+
// Real cursor position feedback published by the capture pipeline.
79+
// The KMS backend reads the cursor plane position once per captured frame and
80+
// publishes it here so the input path can close the loop of the abs->rel
81+
// mouse conversion (see patches/abs-mouse-relative.patch). Coordinates are in
82+
// desktop physical pixels; the logical extents allow rescaling to the
83+
// compositor's logical space. kms_cursor_seq starts at 0 and is bumped on
84+
// every update, so consumers can tell fresh values from stale ones.
85+
extern std::atomic<std::int32_t> kms_cursor_x;
86+
extern std::atomic<std::int32_t> kms_cursor_y;
87+
extern std::atomic<std::int32_t> kms_desktop_w; ///< Physical width of the streamed output.
88+
extern std::atomic<std::int32_t> kms_desktop_h; ///< Physical height of the streamed output.
89+
extern std::atomic<std::int32_t> kms_logical_w; ///< Logical width of the streamed output.
90+
extern std::atomic<std::int32_t> kms_logical_h; ///< Logical height of the streamed output.
91+
extern std::atomic<std::uint64_t> kms_cursor_seq;
92+
7793
// Limited by bits in activeGamepadMask
7894
constexpr auto MAX_GAMEPADS = 16; ///< Maximum number of simultaneously tracked gamepads.
7995

src/platform/linux/kmsgrab.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,19 @@ namespace platf {
12511251
captured_cursor.dst_w = *prop_crtc_w;
12521252
captured_cursor.dst_h = *prop_crtc_h;
12531253

1254+
// Publish the real cursor position for the abs->rel input conversion
1255+
// (see config: absolute_mouse_as_relative). Cursor-plane CRTC
1256+
// coordinates are CRTC-local physical pixels; add the output's desktop
1257+
// offset and publish the output extents so the consumer can rescale
1258+
// to logical units.
1259+
platf::kms_cursor_x.store(offset_x + *prop_crtc_x, std::memory_order_relaxed);
1260+
platf::kms_cursor_y.store(offset_y + *prop_crtc_y, std::memory_order_relaxed);
1261+
platf::kms_desktop_w.store(width, std::memory_order_relaxed);
1262+
platf::kms_desktop_h.store(height, std::memory_order_relaxed);
1263+
platf::kms_logical_w.store(logical_width, std::memory_order_relaxed);
1264+
platf::kms_logical_h.store(logical_height, std::memory_order_relaxed);
1265+
platf::kms_cursor_seq.fetch_add(1, std::memory_order_release);
1266+
12541267
// We're technically cheating a bit here by assuming that we can detect
12551268
// changes to the cursor plane via property adjustments. If this isn't
12561269
// true, we'll really have to mmap() the dmabuf and draw that every time.

0 commit comments

Comments
 (0)