Skip to content

Commit c4bbe39

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 c4bbe39

6 files changed

Lines changed: 254 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: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ constexpr int WHEEL_DELTA = 120; ///< Standard Windows wheel delta used to norm
4242

4343
using namespace std::literals;
4444

45+
namespace platf {
46+
kms_cursor_feedback_t&
47+
kms_cursor_feedback() {
48+
static kms_cursor_feedback_t fb;
49+
return fb;
50+
}
51+
}
52+
4553
namespace input {
4654

4755
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 +279,20 @@ namespace input {
271279

272280
input::touch_port_t touch_port; ///< Touch coordinate bounds for the current stream.
273281

282+
/// State of the absolute->relative mouse conversion (absolute_mouse_as_relative).
283+
struct abs_mouse_t {
284+
bool initialized = false; ///< Whether the absolute baseline is set.
285+
float frac_x = 0; ///< Accumulated fractional X delta.
286+
float frac_y = 0; ///< Accumulated fractional Y delta.
287+
float host_x = 0; ///< Dead-reckoning estimate of the host cursor X, touch-port pixels.
288+
float host_y = 0; ///< Dead-reckoning estimate of the host cursor Y, touch-port pixels.
289+
std::uint64_t seq_last = 0; ///< Cursor-feedback sequence seen by the previous event.
290+
float last_raw_x = 0; ///< Previous raw client X (idle detection).
291+
float last_raw_y = 0; ///< Previous raw client Y (idle detection).
292+
std::chrono::steady_clock::time_point last_client_move {}; ///< Last time the client coordinates changed.
293+
};
294+
abs_mouse_t abs_mouse; ///< Absolute->relative mouse conversion state.
295+
274296
int32_t accumulated_vscroll_delta; ///< Accumulated vscroll delta.
275297
int32_t accumulated_hscroll_delta; ///< Accumulated hscroll delta.
276298
};
@@ -778,6 +800,149 @@ namespace input {
778800
return {multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar)};
779801
}
780802

803+
/// Client mouse event data for the absolute->relative conversion.
804+
struct abs_mouse_event_t {
805+
std::pair<float, float> tpcoords; ///< Client coordinates mapped to touch-port pixels.
806+
float x; ///< Raw client X on the client surface.
807+
float y; ///< Raw client Y on the client surface.
808+
float width; ///< Client surface width.
809+
float height; ///< Client surface height.
810+
};
811+
812+
/**
813+
* @brief Resync the dead-reckoning estimate with the real cursor position.
814+
*
815+
* The KMS capture path publishes the cursor-plane position once per captured
816+
* frame. The feedback is only consumed when trustworthy: while the client is
817+
* idle (every in-flight move has landed), or on an axis saturated at the
818+
* client's own surface edge (the estimate may be mis-anchored there, and any
819+
* feedback lag only overshoots toward the edge, where the compositor clamps).
820+
*
821+
* @param input The input context.
822+
* @param client_idle Whether the client coordinates have been quiet lately.
823+
* @param sat_x Whether the X axis is saturated on the client surface.
824+
* @param sat_y Whether the Y axis is saturated on the client surface.
825+
* @param port_w Touch-port width in pixels.
826+
* @param port_h Touch-port height in pixels.
827+
*/
828+
static void
829+
abs_mouse_sync_estimate(const std::shared_ptr<input_t> &input, bool client_idle, bool sat_x, bool sat_y, float port_w, float port_h) {
830+
const auto &fb = platf::kms_cursor_feedback();
831+
832+
const auto seq = fb.seq.load();
833+
if (seq == 0 || seq == input->abs_mouse.seq_last) {
834+
return;
835+
}
836+
input->abs_mouse.seq_last = seq;
837+
838+
const auto phys_w = static_cast<float>(fb.desktop_w.load());
839+
const auto phys_h = static_cast<float>(fb.desktop_h.load());
840+
const auto logical_w = static_cast<float>(fb.logical_w.load());
841+
const auto logical_h = static_cast<float>(fb.logical_h.load());
842+
if (phys_w <= 0.0f || phys_h <= 0.0f || logical_w <= 0.0f || logical_h <= 0.0f) {
843+
return;
844+
}
845+
846+
// Desktop physical pixels -> compositor logical pixels.
847+
const auto real_x = static_cast<float>(fb.x.load()) * (logical_w / phys_w);
848+
const auto real_y = static_cast<float>(fb.y.load()) * (logical_h / phys_h);
849+
850+
if (client_idle) {
851+
input->abs_mouse.host_x = std::clamp(real_x, 0.0f, port_w - 1.0f);
852+
input->abs_mouse.host_y = std::clamp(real_y, 0.0f, port_h - 1.0f);
853+
}
854+
else {
855+
if (sat_x) input->abs_mouse.host_x = std::clamp(real_x, 0.0f, port_w - 1.0f);
856+
if (sat_y) input->abs_mouse.host_y = std::clamp(real_y, 0.0f, port_h - 1.0f);
857+
}
858+
}
859+
860+
/**
861+
* @brief Emulate relative mouse movement from absolute coordinates.
862+
*
863+
* Absolute motion arrives as PointerMotionAbsolute in the compositor, which
864+
* moves the cursor but does NOT update assistive features that only track
865+
* relative motion, e.g. the COSMIC screen magnifier focal point
866+
* (pop-os/cosmic-comp #2760). The first event anchors the cursor absolutely;
867+
* subsequent events are converted to relative deltas.
868+
*
869+
* Dead reckoning is the only thing in the smooth motion path (1:1, no
870+
* latency); the estimate is resynced with the real cursor position while the
871+
* client is idle. Phantom "walls" (movement blocked in one direction until
872+
* pushed back) come from the client saturating a coordinate in its own
873+
* surface while the host cursor sits mid-screen, so a saturated axis simply
874+
* targets the matching host edge — the primary loop itself drives the cursor
875+
* there, continuously and idempotently.
876+
*
877+
* @param input The input context.
878+
* @param abs_port Absolute-coordinate touch port.
879+
* @param event The client mouse event (raw and touch-port coordinates).
880+
* @param touch_port_dim_x Touch-port width in pixels.
881+
* @param touch_port_dim_y Touch-port height in pixels.
882+
*/
883+
static void
884+
abs_mouse_as_relative(const std::shared_ptr<input_t> &input, const platf::touch_port_t &abs_port, const abs_mouse_event_t &event,
885+
int touch_port_dim_x, int touch_port_dim_y) {
886+
const auto port_w = static_cast<float>(touch_port_dim_x);
887+
const auto port_h = static_cast<float>(touch_port_dim_y);
888+
889+
const auto &x = event.x;
890+
const auto &y = event.y;
891+
892+
// Saturation bands on the client's own surface.
893+
constexpr float kRawEdge = 8.0f;
894+
const auto at_left = x <= kRawEdge;
895+
const auto at_right = x >= event.width - kRawEdge;
896+
const auto at_top = y <= kRawEdge;
897+
const auto at_bottom = y >= event.height - kRawEdge;
898+
899+
// Client target in touch-port units; a saturated axis targets the host edge.
900+
auto target_x = std::clamp(event.tpcoords.first, 0.0f, port_w - 1.0f);
901+
auto target_y = std::clamp(event.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+
// Idle detection watches the client's own motion (raw coordinates), so a
908+
// cursor pinned against its edge still counts as idle once it stops.
909+
const auto now = std::chrono::steady_clock::now();
910+
if (std::fabs(x - input->abs_mouse.last_raw_x) > 0.01f ||
911+
std::fabs(y - input->abs_mouse.last_raw_y) > 0.01f) {
912+
input->abs_mouse.last_client_move = now;
913+
}
914+
input->abs_mouse.last_raw_x = x;
915+
input->abs_mouse.last_raw_y = y;
916+
917+
if (!input->abs_mouse.initialized) {
918+
platf::abs_mouse(platf_input, abs_port, event.tpcoords.first, event.tpcoords.second);
919+
input->abs_mouse.initialized = true;
920+
input->abs_mouse.host_x = target_x;
921+
input->abs_mouse.host_y = target_y;
922+
return;
923+
}
924+
925+
constexpr auto kIdleMs = std::chrono::milliseconds(150);
926+
const auto client_idle = now - input->abs_mouse.last_client_move > kIdleMs;
927+
abs_mouse_sync_estimate(input, client_idle, at_left || at_right, at_top || at_bottom, port_w, port_h);
928+
929+
// Primary loop: drive the estimate toward the (possibly edge-overridden)
930+
// target. Idempotent while resting inside the edge band.
931+
input->abs_mouse.frac_x += target_x - input->abs_mouse.host_x;
932+
input->abs_mouse.frac_y += target_y - input->abs_mouse.host_y;
933+
934+
const auto delta_x = static_cast<int>(input->abs_mouse.frac_x);
935+
const auto delta_y = static_cast<int>(input->abs_mouse.frac_y);
936+
input->abs_mouse.frac_x -= delta_x;
937+
input->abs_mouse.frac_y -= delta_y;
938+
939+
if (delta_x || delta_y) {
940+
platf::move_mouse(platf_input, delta_x, delta_y);
941+
input->abs_mouse.host_x = std::clamp(input->abs_mouse.host_x + delta_x, 0.0f, port_w - 1.0f);
942+
input->abs_mouse.host_y = std::clamp(input->abs_mouse.host_y + delta_y, 0.0f, port_h - 1.0f);
943+
}
944+
}
945+
781946
/**
782947
* @brief Forward a client input packet directly to the platform backend.
783948
*
@@ -831,7 +996,12 @@ namespace input {
831996
touch_port_dim_y
832997
};
833998

834-
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
999+
if (!config::input.absolute_mouse_as_relative) {
1000+
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
1001+
return;
1002+
}
1003+
1004+
abs_mouse_as_relative(input, abs_port, { *tpcoords, x, y, width, height }, touch_port_dim_x, touch_port_dim_y);
8351005
}
8361006

8371007
/**

src/platform/common.h

Lines changed: 24 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,29 @@ 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 config: absolute_mouse_as_relative). Coordinates are
82+
// in desktop physical pixels; the logical extents allow rescaling to the
83+
// compositor's logical space. seq starts at 0 and is bumped on every update,
84+
// so consumers can tell fresh values from stale ones.
85+
struct kms_cursor_feedback_t {
86+
std::atomic_int32_t x { -1 };
87+
std::atomic_int32_t y { -1 };
88+
std::atomic_int32_t desktop_w { 0 }; ///< Physical width of the streamed output.
89+
std::atomic_int32_t desktop_h { 0 }; ///< Physical height of the streamed output.
90+
std::atomic_int32_t logical_w { 0 }; ///< Logical width of the streamed output.
91+
std::atomic_int32_t logical_h { 0 }; ///< Logical height of the streamed output.
92+
std::atomic_uint64_t seq { 0 };
93+
};
94+
95+
/**
96+
* @brief Access the process-wide cursor feedback instance.
97+
*/
98+
kms_cursor_feedback_t&
99+
kms_cursor_feedback();
100+
77101
// Limited by bits in activeGamepadMask
78102
constexpr auto MAX_GAMEPADS = 16; ///< Maximum number of simultaneously tracked gamepads.
79103

src/platform/linux/kmsgrab.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,33 @@ 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 touch-port units.
1259+
auto &cursor_fb = platf::kms_cursor_feedback();
1260+
cursor_fb.x.store(offset_x + *prop_crtc_x);
1261+
cursor_fb.y.store(offset_y + *prop_crtc_y);
1262+
cursor_fb.desktop_w.store(width);
1263+
cursor_fb.desktop_h.store(height);
1264+
cursor_fb.logical_w.store(logical_width);
1265+
cursor_fb.logical_h.store(logical_height);
1266+
cursor_fb.seq.fetch_add(1);
1267+
1268+
// Publish the real cursor position for the abs->rel input conversion
1269+
// (see config: absolute_mouse_as_relative). Cursor-plane CRTC
1270+
// coordinates are CRTC-local physical pixels; add the output's desktop
1271+
// offset and publish the output extents so the consumer can rescale
1272+
// to logical units.
1273+
platf::kms_cursor_x.store(offset_x + *prop_crtc_x, std::memory_order_relaxed);
1274+
platf::kms_cursor_y.store(offset_y + *prop_crtc_y, std::memory_order_relaxed);
1275+
platf::kms_desktop_w.store(width, std::memory_order_relaxed);
1276+
platf::kms_desktop_h.store(height, std::memory_order_relaxed);
1277+
platf::kms_logical_w.store(logical_width, std::memory_order_relaxed);
1278+
platf::kms_logical_h.store(logical_height, std::memory_order_relaxed);
1279+
platf::kms_cursor_seq.fetch_add(1, std::memory_order_release);
1280+
12541281
// We're technically cheating a bit here by assuming that we can detect
12551282
// changes to the cursor plane via property adjustments. If this isn't
12561283
// true, we'll really have to mmap() the dmabuf and draw that every time.

0 commit comments

Comments
 (0)