Skip to content

Commit 639fe89

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 6ad5f97 commit 639fe89

5 files changed

Lines changed: 189 additions & 1 deletion

File tree

src/config.cpp

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

865866
/**
@@ -1795,6 +1796,7 @@ namespace config {
17951796

17961797
bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
17971798
bool_f(vars, "native_pen_touch", input.native_pen_touch);
1799+
bool_f(vars, "absolute_mouse_as_relative", input.absolute_mouse_as_relative);
17981800

17991801
bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
18001802
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
@@ -289,6 +289,7 @@ namespace config {
289289

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

294295
namespace flag {

src/input.cpp

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

4242
using namespace std::literals;
4343

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

4658
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.
@@ -270,6 +282,17 @@ namespace input {
270282

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

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

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

836992
/**

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)