mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-25 07:36:41 +00:00
fix: refresh wayland uinput range on compositor layout change (#15628)
* fix: refresh wayland uinput range on compositor layout change The uinput absolute range is computed once at session init. If the compositor layout changes mid-session (monitor scale or position change, or a portal virtual output appearing once capture starts), injected coordinates are rescaled by the stale range and land offset. Poll the live desktop bounding box from the display service loop while subscribed (one wayland roundtrip, throttled to 1.5s, no subprocesses) and re-apply the uinput resolution when it changes. Also read a fresh layout when computing the initial range in check_init, since the cache is not cleared when a session closes through the restore-token path. This is the X component of #15601. The stale advertised origins (the Y component) are not touched here: re-advertising DisplayInfo mid-session trips the portal re-negotiation and can drop displays. Signed-off-by: Cody Harris <codyharris7188@gmail.com> * fix: bound the mouse resolution IPC wait during session init Wrap update_mouse_resolution in the same 3s timeout the periodic refresh uses, so a hung IPC response can't stall check_init. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: build timeout future inside runtime, split linux lazy_static Constructing the timeout future eagerly as the block_on argument panics with 'there is no reactor running'; move it into the async block so it is built inside the runtime context. Also move WAYLAND_UINPUT_RECT into its own cfg-gated lazy_static block, an attribute on a single item inside the shared block does not compile. * fix: confirm uinput mouse device adopted new range before caching rect send_refresh() now waits for the mouse service to ack that it recreated the device with the new range instead of firing and forgetting, and update_mouse_resolution() propagates that result. The layout poller only caches the rect after the device actually adopts the range, so a failed refresh errors and retries on the next check. The ack read is bounded by IPC_REQUEST_TIMEOUT, matching the keyboard get-key-state path. * fix: propagate refresh failures instead of caching a stale range - input_service: error when the custom-mouse downcast fails so the poller retries instead of caching an unconfirmed refresh - uinput: on device recreation failure, keep the current device and the IPC connection and withhold the ack so the client retries, instead of killing the mouse handler * fix: remap injected wayland coords onto the live layout after a monitor moves The range refresh corrects the uinput ABS bounds, but a single-display client sends whole-desktop coordinates offset by the origin of the display it follows, taken from the layout advertised at session init. When another monitor is rescaled or moved that origin shifts, so the coordinate lands offset before it reaches uinput and the range refresh cannot recover it. Snapshot the per-display layout at init, poll the live layout on the existing 1.5s throttle, and when they differ remap each injected move into the followed display's current rectangle (matched by connector name, index fallback when the compositor reports none). No-op and lock-free while the layout is unchanged. --------- Signed-off-by: Cody Harris <codyharris7188@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
beaa754299
commit
b4af82157b
@@ -14,6 +14,9 @@ lazy_static! {
|
||||
static ref DISPLAYS: Mutex<Option<Arc<Displays>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
static MISSING_LOGICAL_SIZE_WARNED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
pub struct Displays {
|
||||
@@ -217,7 +220,26 @@ pub fn clear_wayland_displays_cache() {
|
||||
// Return (min_x, max_x, min_y, max_y)
|
||||
pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
let wayland_displays = get_displays();
|
||||
let displays = &wayland_displays.displays;
|
||||
desktop_rect_of(&wayland_displays.displays)
|
||||
}
|
||||
|
||||
// The desktop rect and per-display logical rects, always read live from the
|
||||
// compositor in a single roundtrip. Skips the displays cache and the primary-monitor
|
||||
// detection (which may spawn external commands), so it is cheap enough to poll for
|
||||
// layout changes. https://github.com/rustdesk/rustdesk/issues/15601
|
||||
pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<DisplayRect>)> {
|
||||
match get_wayland_displays() {
|
||||
Ok(displays) => {
|
||||
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn desktop_rect_of(displays: &[WaylandDisplayInfo]) -> Option<(i32, i32, i32, i32)> {
|
||||
if displays.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -243,10 +265,13 @@ pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
// This may occur if the Wayland compositor does not provide logical size information,
|
||||
// or if display information is incomplete. We fall back to physical size, which provides
|
||||
// usable dimensions, but may not always be correct depending on compositor behavior.
|
||||
warn!(
|
||||
// Warn only once, the live path polls this while a session is active.
|
||||
if !MISSING_LOGICAL_SIZE_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
warn!(
|
||||
"Display at ({}, {}) is missing logical_size; falling back to physical size ({}, {}).",
|
||||
d.x, d.y, d.width, d.height
|
||||
);
|
||||
}
|
||||
(d.width, d.height)
|
||||
};
|
||||
max_x = max_x.max(d.x + size.0);
|
||||
@@ -254,3 +279,289 @@ pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
}
|
||||
Some((min_x, max_x, min_y, max_y))
|
||||
}
|
||||
|
||||
/// One display's logical rectangle in the desktop coordinate space the client uses:
|
||||
/// logical origin plus logical size, falling back to physical size when the compositor
|
||||
/// reports no logical size (matching `desktop_rect_of`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DisplayRect {
|
||||
pub name: String,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub w: i32,
|
||||
pub h: i32,
|
||||
}
|
||||
|
||||
fn logical_rects_of(displays: &[WaylandDisplayInfo]) -> Vec<DisplayRect> {
|
||||
// Match `desktop_rect_of`: a single display uses its physical size (its scale is
|
||||
// reported as 1.0 to the client), multiple displays use logical size. This keeps a
|
||||
// single display a no-op for the remap (its origin never shifts) and keeps the rects
|
||||
// in the same coordinate space the client's coordinates are expressed in.
|
||||
let single = displays.len() == 1;
|
||||
displays
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let (w, h) = if single {
|
||||
(d.width, d.height)
|
||||
} else {
|
||||
d.logical_size.unwrap_or((d.width, d.height))
|
||||
};
|
||||
DisplayRect {
|
||||
name: d.name.clone(),
|
||||
x: d.x,
|
||||
y: d.y,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Per-display logical rects from the cached init snapshot. The client's injected
|
||||
// coordinates are `local + origin` in this layout, so it is the baseline to map from.
|
||||
pub fn get_display_rects_for_uinput() -> Vec<DisplayRect> {
|
||||
logical_rects_of(&get_displays().displays)
|
||||
}
|
||||
|
||||
/// Remap an injected coordinate from the layout the client still believes in
|
||||
/// (`baseline`, captured at session init) to the current compositor layout (`live`).
|
||||
///
|
||||
/// A single-display client sends whole-desktop coordinates: `local + baseline_origin[d]`
|
||||
/// for whichever display `d` it is following. If that display's origin or logical size
|
||||
/// has since changed (e.g. another monitor was rescaled, shifting this one), the
|
||||
/// coordinate lands offset. We find the baseline display the point falls in, then map
|
||||
/// the point into the same display's live rectangle, matched by connector name (or, when
|
||||
/// the compositor reports no names, by index while the display count is unchanged).
|
||||
///
|
||||
/// Returns the input unchanged when the point is outside every baseline display or the
|
||||
/// matched display is gone, so a failed match never moves the cursor further off than
|
||||
/// leaving it alone. https://github.com/rustdesk/rustdesk/issues/15601
|
||||
pub fn remap_to_live_layout(
|
||||
x: i32,
|
||||
y: i32,
|
||||
baseline: &[DisplayRect],
|
||||
live: &[DisplayRect],
|
||||
) -> (i32, i32) {
|
||||
let Some((bi, b)) = baseline
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, r)| x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h)
|
||||
else {
|
||||
return (x, y);
|
||||
};
|
||||
let matched = if b.name.is_empty() {
|
||||
// Nameless compositor: index-match, but only while the count is unchanged. A
|
||||
// named display that is simply gone from the live layout must fall through to
|
||||
// "unchanged" below, not get index-matched to whatever now sits at its index.
|
||||
if baseline.len() == live.len() {
|
||||
live.get(bi)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
live.iter().find(|r| r.name == b.name)
|
||||
};
|
||||
let Some(l) = matched else {
|
||||
return (x, y);
|
||||
};
|
||||
// Map the point into the live rectangle, preserving position within the display so a
|
||||
// scale change on the followed display itself is corrected too, not only a shift.
|
||||
// Scale by (extent - 1) so both endpoints land exactly: the client clamps its
|
||||
// coordinate to `[origin, origin + w - 1]`, and mapping that span to the live span's
|
||||
// `[0, w' - 1]` keeps the far edge reachable (hot corners) in both directions, and
|
||||
// stays an exact shift when the size is unchanged.
|
||||
let nx = map_axis(x, b.x, b.w, l.x, l.w);
|
||||
let ny = map_axis(y, b.y, b.h, l.y, l.h);
|
||||
(nx, ny)
|
||||
}
|
||||
|
||||
fn map_axis(v: i32, base_origin: i32, base_extent: i32, live_origin: i32, live_extent: i32) -> i32 {
|
||||
if base_extent <= 1 || live_extent <= 1 {
|
||||
return live_origin;
|
||||
}
|
||||
live_origin + ((v - base_origin) as i64 * (live_extent - 1) as i64 / (base_extent - 1) as i64) as i32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn display(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
logical_size: Option<(i32, i32)>,
|
||||
) -> WaylandDisplayInfo {
|
||||
WaylandDisplayInfo {
|
||||
name: "".to_owned(),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
logical_size,
|
||||
refresh_rate: 60,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desktop_rect_empty() {
|
||||
assert_eq!(desktop_rect_of(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desktop_rect_single_display_uses_physical_size() {
|
||||
let displays = [display(0, 0, 2880, 1800, Some((1859, 1162)))];
|
||||
assert_eq!(desktop_rect_of(&displays), Some((0, 2880, 0, 1800)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desktop_rect_multi_display_uses_logical_size() {
|
||||
// Laptop panel at 155% below two stacked externals at 100%.
|
||||
let displays = [
|
||||
display(0, 718, 2880, 1800, Some((1859, 1162))),
|
||||
display(1859, 0, 1920, 1080, Some((1920, 1080))),
|
||||
display(1859, 1080, 1920, 1080, Some((1920, 1080))),
|
||||
];
|
||||
assert_eq!(desktop_rect_of(&displays), Some((0, 3779, 0, 2160)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_desktop_rect_missing_logical_size_falls_back_to_physical() {
|
||||
let displays = [
|
||||
display(0, 0, 2560, 1440, None),
|
||||
display(2560, 0, 2560, 1440, Some((2560, 1440))),
|
||||
];
|
||||
assert_eq!(desktop_rect_of(&displays), Some((0, 5120, 0, 1440)));
|
||||
}
|
||||
|
||||
fn rect(name: &str, x: i32, y: i32, w: i32, h: i32) -> DisplayRect {
|
||||
DisplayRect {
|
||||
name: name.to_owned(),
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
}
|
||||
|
||||
// The reported failure: connect to the second display, rescale the primary.
|
||||
// Baseline: two 2560-wide displays side by side, both at 100%.
|
||||
// Live: the primary (DP-1) rescaled to 125% -> 2048 logical wide, so the second
|
||||
// display (DP-2) shifts left from x=2560 to x=2048. A client following DP-2 keeps
|
||||
// sending coordinates offset by DP-2's old origin (2560).
|
||||
#[test]
|
||||
fn test_remap_primary_rescale_shifts_second_display() {
|
||||
let baseline = [
|
||||
rect("DP-1", 0, 0, 2560, 1440),
|
||||
rect("DP-2", 2560, 0, 2560, 1440),
|
||||
];
|
||||
let live = [
|
||||
rect("DP-1", 0, 0, 2048, 1440),
|
||||
rect("DP-2", 2048, 0, 2560, 1440),
|
||||
];
|
||||
// Top-left of DP-2: client sends (2560, 0), should land at live DP-2 origin.
|
||||
assert_eq!(remap_to_live_layout(2560, 0, &baseline, &live), (2048, 0));
|
||||
// Middle of DP-2 keeps its fractional position.
|
||||
assert_eq!(
|
||||
remap_to_live_layout(3840, 720, &baseline, &live),
|
||||
(3328, 720)
|
||||
);
|
||||
}
|
||||
|
||||
// A point on the rescaled display itself is squeezed to its new logical width.
|
||||
#[test]
|
||||
fn test_remap_scales_within_resized_display() {
|
||||
let baseline = [rect("DP-1", 0, 0, 2560, 1440), rect("DP-2", 2560, 0, 2560, 1440)];
|
||||
let live = [rect("DP-1", 0, 0, 2048, 1440), rect("DP-2", 2048, 0, 2560, 1440)];
|
||||
// x=1280 across the 2560-wide baseline DP-1 -> proportionally across the 2048-wide
|
||||
// live DP-1 (endpoint-preserving scale, so ~1px off the naive midpoint).
|
||||
assert_eq!(remap_to_live_layout(1280, 500, &baseline, &live), (1023, 500));
|
||||
}
|
||||
|
||||
// The far edge of the followed display stays reachable when it is enlarged, so hot
|
||||
// corners keep working. Baseline DP-1 is 2048 wide, live DP-1 is 2560 wide; the
|
||||
// client's last column (2047) must map to the live last column (2559), not 2558.
|
||||
#[test]
|
||||
fn test_remap_enlarged_display_reaches_far_edge() {
|
||||
let baseline = [rect("DP-1", 0, 0, 2048, 1440), rect("DP-2", 2048, 0, 1920, 1080)];
|
||||
let live = [rect("DP-1", 0, 0, 2560, 1440), rect("DP-2", 2560, 0, 1920, 1080)];
|
||||
assert_eq!(remap_to_live_layout(2047, 0, &baseline, &live), (2559, 0));
|
||||
assert_eq!(remap_to_live_layout(0, 0, &baseline, &live), (0, 0));
|
||||
}
|
||||
|
||||
// No drift: identical layouts map every point to itself.
|
||||
#[test]
|
||||
fn test_remap_identity_when_unchanged() {
|
||||
let layout = [rect("DP-1", 0, 0, 2560, 1440), rect("DP-2", 2560, 0, 2560, 1440)];
|
||||
assert_eq!(remap_to_live_layout(3000, 700, &layout, &layout), (3000, 700));
|
||||
}
|
||||
|
||||
// Point outside every baseline display is left untouched.
|
||||
#[test]
|
||||
fn test_remap_point_outside_all_displays_unchanged() {
|
||||
let baseline = [rect("DP-1", 0, 0, 2560, 1440)];
|
||||
let live = [rect("DP-1", 0, 0, 2048, 1440)];
|
||||
assert_eq!(remap_to_live_layout(9000, 9000, &baseline, &live), (9000, 9000));
|
||||
}
|
||||
|
||||
// Matched display gone from the live layout (e.g. unplugged): leave the point be
|
||||
// rather than mapping it somewhere wrong.
|
||||
#[test]
|
||||
fn test_remap_display_removed_unchanged() {
|
||||
let baseline = [rect("DP-1", 0, 0, 2560, 1440), rect("DP-2", 2560, 0, 2560, 1440)];
|
||||
let live = [rect("DP-1", 0, 0, 2560, 1440)];
|
||||
assert_eq!(remap_to_live_layout(2600, 100, &baseline, &live), (2600, 100));
|
||||
}
|
||||
|
||||
// Nameless compositor: fall back to index matching while the count is unchanged.
|
||||
#[test]
|
||||
fn test_remap_nameless_index_fallback() {
|
||||
let baseline = [rect("", 0, 0, 2560, 1440), rect("", 2560, 0, 2560, 1440)];
|
||||
let live = [rect("", 0, 0, 2048, 1440), rect("", 2048, 0, 2560, 1440)];
|
||||
assert_eq!(remap_to_live_layout(2560, 0, &baseline, &live), (2048, 0));
|
||||
}
|
||||
|
||||
// Nameless compositor with a changed count: cannot index-match safely, so no-op.
|
||||
#[test]
|
||||
fn test_remap_nameless_count_changed_unchanged() {
|
||||
let baseline = [rect("", 0, 0, 2560, 1440), rect("", 2560, 0, 2560, 1440)];
|
||||
let live = [rect("", 0, 0, 2048, 1440)];
|
||||
assert_eq!(remap_to_live_layout(2560, 0, &baseline, &live), (2560, 0));
|
||||
}
|
||||
|
||||
// A named display absent from the live layout, but the count is unchanged (e.g. a
|
||||
// monitor was swapped for a different one at the same index): the index fallback is
|
||||
// for nameless layouts only, so a named miss stays unchanged rather than mapping to
|
||||
// whatever now occupies that index.
|
||||
#[test]
|
||||
fn test_remap_named_miss_equal_count_unchanged() {
|
||||
let baseline = [rect("DP-1", 0, 0, 2560, 1440), rect("DP-2", 2560, 0, 2560, 1440)];
|
||||
let live = [rect("DP-1", 0, 0, 2048, 1440), rect("HDMI-1", 2048, 0, 1920, 1080)];
|
||||
assert_eq!(remap_to_live_layout(2600, 100, &baseline, &live), (2600, 100));
|
||||
}
|
||||
|
||||
// A single display uses physical size in both baseline and live (scale reported as
|
||||
// 1.0), so it never drifts and the remap is a no-op even across a rescale.
|
||||
#[test]
|
||||
fn test_logical_rects_single_display_uses_physical() {
|
||||
let displays = [display(0, 0, 2560, 1440, Some((2048, 1152)))];
|
||||
assert_eq!(
|
||||
logical_rects_of(&displays),
|
||||
vec![rect("", 0, 0, 2560, 1440)]
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple displays use logical size, falling back to physical when absent.
|
||||
#[test]
|
||||
fn test_logical_rects_multi_display_uses_logical() {
|
||||
let displays = [
|
||||
display(0, 0, 2560, 1440, Some((2048, 1152))),
|
||||
display(2048, 0, 1920, 1080, None),
|
||||
];
|
||||
assert_eq!(
|
||||
logical_rects_of(&displays),
|
||||
vec![rect("", 0, 0, 2048, 1152), rect("", 2048, 0, 1920, 1080)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,144 @@ lazy_static::lazy_static! {
|
||||
static ref SYNC_DISPLAYS: Arc<Mutex<SyncDisplaysInfo>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref WAYLAND_UINPUT_RECT: Mutex<WaylandUinputRect> = Default::default();
|
||||
static ref WAYLAND_LAYOUT: Mutex<WaylandLayout> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_LAYOUT_CHECK_INTERVAL: Duration = Duration::from_millis(1500);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Default)]
|
||||
struct WaylandUinputRect {
|
||||
rect: Option<(i32, i32, i32, i32)>,
|
||||
last_check: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
// Per-display layout used to correct injected coordinates when the compositor moves a
|
||||
// monitor mid-session. The client keeps sending coordinates offset by the layout it was
|
||||
// told at session init (`baseline`); we remap them onto the current layout (`live`).
|
||||
// https://github.com/rustdesk/rustdesk/issues/15601
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Default)]
|
||||
struct WaylandLayout {
|
||||
baseline: Vec<scrap::wayland::display::DisplayRect>,
|
||||
live: Vec<scrap::wayland::display::DisplayRect>,
|
||||
}
|
||||
|
||||
// Whether `live` differs from `baseline`. Read on every mouse move, so it is an atomic:
|
||||
// the common (no-drift) case never touches the layout mutex.
|
||||
#[cfg(target_os = "linux")]
|
||||
static WAYLAND_LAYOUT_DRIFTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn set_wayland_uinput_rect(rect: (i32, i32, i32, i32)) {
|
||||
WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn set_wayland_layout_baseline(baseline: Vec<scrap::wayland::display::DisplayRect>) {
|
||||
WAYLAND_LAYOUT_DRIFTED.store(false, Ordering::Relaxed);
|
||||
let mut lock = WAYLAND_LAYOUT.lock().unwrap();
|
||||
lock.baseline = baseline;
|
||||
lock.live.clear();
|
||||
}
|
||||
|
||||
// Remap an injected coordinate onto the live compositor layout when it has drifted from
|
||||
// what the client was told at session init. Lock-free no-op otherwise.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn remap_wayland_uinput_coord(x: i32, y: i32) -> (i32, i32) {
|
||||
if !WAYLAND_LAYOUT_DRIFTED.load(Ordering::Relaxed) {
|
||||
return (x, y);
|
||||
}
|
||||
let lock = WAYLAND_LAYOUT.lock().unwrap();
|
||||
scrap::wayland::display::remap_to_live_layout(x, y, &lock.baseline, &lock.live)
|
||||
}
|
||||
|
||||
// The uinput absolute range is set when the session inits. If the compositor layout
|
||||
// changes afterwards (monitor scale/position change, or a portal virtual output
|
||||
// appearing once the capture starts), injected coordinates get rescaled by the stale
|
||||
// range and land offset, https://github.com/rustdesk/rustdesk/issues/15601
|
||||
#[cfg(target_os = "linux")]
|
||||
fn refresh_wayland_uinput_rect_if_changed() {
|
||||
if is_x11() || !crate::input_service::wayland_use_uinput() {
|
||||
return;
|
||||
}
|
||||
{
|
||||
let mut lock = WAYLAND_UINPUT_RECT.lock().unwrap();
|
||||
if let Some(last_check) = lock.last_check {
|
||||
if last_check.elapsed() < WAYLAND_LAYOUT_CHECK_INTERVAL {
|
||||
return;
|
||||
}
|
||||
}
|
||||
lock.last_check = Some(std::time::Instant::now());
|
||||
}
|
||||
let Some((rect, live_rects)) = scrap::wayland::display::get_layout_for_uinput_live() else {
|
||||
return;
|
||||
};
|
||||
// Refresh the per-display layout every poll: monitor origins can shift (e.g. two
|
||||
// displays swap positions) without changing the overall desktop rect, and the mouse
|
||||
// path needs the current per-display geometry to correct coordinates.
|
||||
let drifted = {
|
||||
let mut layout = WAYLAND_LAYOUT.lock().unwrap();
|
||||
let drifted = !layout.baseline.is_empty()
|
||||
&& !live_rects.is_empty()
|
||||
&& layout.baseline != live_rects;
|
||||
layout.live = live_rects;
|
||||
drifted
|
||||
};
|
||||
// The remap corrects for per-display origin shifts; the uinput ABS range corrects for
|
||||
// the overall bounding box. Only enable the remap once the range matches the live
|
||||
// layout, otherwise moves would be remapped into a range the device is not yet using.
|
||||
// A drift with no bbox change (origins swapped) needs no range update and enables now.
|
||||
let mut range_ok = WAYLAND_UINPUT_RECT.lock().unwrap().rect == Some(rect);
|
||||
if !range_ok {
|
||||
let (minx, maxx, miny, maxy) = rect;
|
||||
log::info!(
|
||||
"desktop layout changed, update mouse resolution: ({}, {}), ({}, {})",
|
||||
minx,
|
||||
maxx,
|
||||
miny,
|
||||
maxy
|
||||
);
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => {
|
||||
// Bound the IPC wait, this runs on the display service loop and
|
||||
// `set_resolution()` has no timeout on the response read.
|
||||
// timeout must be built inside the runtime, or it panics
|
||||
// "there is no reactor running". See clipboard_service.rs.
|
||||
match rt.block_on(async {
|
||||
timeout(
|
||||
3_000,
|
||||
crate::input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
||||
)
|
||||
.await
|
||||
}) {
|
||||
// Record the rect only after a successful apply, so a transient
|
||||
// failure is retried on the next check.
|
||||
Ok(Ok(())) => {
|
||||
WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect);
|
||||
range_ok = true;
|
||||
}
|
||||
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to build tokio runtime: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Publish the flag last: a `true` read is always backed by a current `live` and a
|
||||
// matching uinput range. A failed range apply leaves this false and retries next poll.
|
||||
WAYLAND_LAYOUT_DRIFTED.store(drifted && range_ok, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// https://github.com/rustdesk/rustdesk/pull/8537
|
||||
static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -231,6 +369,12 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||||
sp.send(msg_out);
|
||||
log::info!("Displays changed");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if sp.has_subscribes() {
|
||||
refresh_wayland_uinput_rect_if_changed();
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
|
||||
|
||||
+23
-10
@@ -661,20 +661,22 @@ pub async fn setup_rdp_input() -> ResultType<(), Box<dyn std::error::Error>> {
|
||||
pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
|
||||
set_uinput_resolution(minx, maxx, miny, maxy).await?;
|
||||
|
||||
std::thread::spawn(|| {
|
||||
// Confirm the device adopted the new range before the caller caches it.
|
||||
// spawn_blocking because ENIGO is a std Mutex and send_refresh blocks on IPC.
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() {
|
||||
if let Some(mouse) = mouse
|
||||
.as_mut_any()
|
||||
.downcast_mut::<super::uinput::client::UInputMouse>()
|
||||
{
|
||||
allow_err!(mouse.send_refresh());
|
||||
} else {
|
||||
log::error!("failed downcast uinput mouse");
|
||||
return mouse.send_refresh();
|
||||
}
|
||||
bail!("failed to downcast custom mouse to UInputMouse");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
// No custom mouse: nothing to refresh.
|
||||
Ok(())
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1098,12 +1100,23 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
MOUSE_TYPE_MOVE => {
|
||||
// Switching back to absolute movement implicitly disables relative mouse mode.
|
||||
set_relative_mouse_active(conn, false);
|
||||
en.mouse_move_to(evt.x, evt.y);
|
||||
// On Wayland with uinput, the client sends coordinates in the layout it was
|
||||
// told at session init. If the compositor has since moved a monitor, correct
|
||||
// them onto the current layout. https://github.com/rustdesk/rustdesk/issues/15601
|
||||
#[cfg(target_os = "linux")]
|
||||
let (mx, my) = if wayland_use_uinput() {
|
||||
super::display_service::remap_wayland_uinput_coord(evt.x, evt.y)
|
||||
} else {
|
||||
(evt.x, evt.y)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let (mx, my) = (evt.x, evt.y);
|
||||
en.mouse_move_to(mx, my);
|
||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||
conn,
|
||||
time: get_time(),
|
||||
x: evt.x,
|
||||
y: evt.y,
|
||||
x: mx,
|
||||
y: my,
|
||||
};
|
||||
}
|
||||
// MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications.
|
||||
|
||||
+26
-8
@@ -130,7 +130,16 @@ pub mod client {
|
||||
}
|
||||
|
||||
pub fn send_refresh(&mut self) -> ResultType<()> {
|
||||
self.send(Data::Mouse(DataMouse::Refresh))
|
||||
self.rt
|
||||
.block_on(self.conn.send(&Data::Mouse(DataMouse::Refresh)))?;
|
||||
// Wait for the service to confirm it recreated the device, so a
|
||||
// failed refresh is distinguishable from a good one.
|
||||
match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) {
|
||||
Ok(Some(Data::Empty)) => Ok(()),
|
||||
Ok(Some(resp)) => bail!("unexpected uinput mouse refresh response: {:?}", &resp),
|
||||
Ok(None) => bail!("uinput mouse refresh failed, connection closed"),
|
||||
Err(e) => bail!("uinput mouse refresh timeout {}, {}", IPC_REQUEST_TIMEOUT, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,9 +860,10 @@ pub mod service {
|
||||
match data {
|
||||
Data::Mouse(data) => {
|
||||
if let DataMouse::Refresh = data {
|
||||
let resolution = RESOLUTION.lock().unwrap();
|
||||
let rng_x = resolution.0.clone();
|
||||
let rng_y = resolution.1.clone();
|
||||
let (rng_x, rng_y) = {
|
||||
let resolution = RESOLUTION.lock().unwrap();
|
||||
(resolution.0.clone(), resolution.1.clone())
|
||||
};
|
||||
log::info!(
|
||||
"Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})",
|
||||
rng_x.0,
|
||||
@@ -861,11 +871,19 @@ pub mod service {
|
||||
rng_y.0,
|
||||
rng_y.1
|
||||
);
|
||||
mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
||||
Ok(mouse) => mouse,
|
||||
match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
||||
Ok(m) => {
|
||||
mouse = m;
|
||||
// Ack: device adopted the new range.
|
||||
allow_err!(stream.send(&Data::Empty).await);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create mouse, {}", e);
|
||||
return;
|
||||
// Keep the current device; withhold the ack
|
||||
// so the client times out and retries.
|
||||
log::error!(
|
||||
"Failed to recreate uinput mouse, keeping current: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+25
-3
@@ -137,6 +137,9 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
if !is_x11() {
|
||||
if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
|
||||
if crate::input_service::wayland_use_uinput() {
|
||||
// The cached layout may predate compositor changes made while no session
|
||||
// was active, https://github.com/rustdesk/rustdesk/issues/15601
|
||||
scrap::wayland::display::clear_wayland_displays_cache();
|
||||
if let Some((minx, maxx, miny, maxy)) =
|
||||
scrap::wayland::display::get_desktop_rect_for_uinput()
|
||||
{
|
||||
@@ -147,9 +150,28 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
miny,
|
||||
maxy
|
||||
);
|
||||
allow_err!(
|
||||
input_service::update_mouse_resolution(minx, maxx, miny, maxy).await
|
||||
);
|
||||
// Bound the IPC wait like the periodic refresh does, so a hung
|
||||
// response can't stall session init.
|
||||
match timeout(
|
||||
3_000,
|
||||
input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
super::display_service::set_wayland_uinput_rect((
|
||||
minx, maxx, miny, maxy,
|
||||
));
|
||||
// Snapshot the per-display layout the client's coordinates
|
||||
// will be based on, so the mouse path can correct them if
|
||||
// the compositor moves a monitor mid-session.
|
||||
super::display_service::set_wayland_layout_baseline(
|
||||
scrap::wayland::display::get_display_rects_for_uinput(),
|
||||
);
|
||||
}
|
||||
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
}
|
||||
} else {
|
||||
log::warn!("Failed to get desktop rect for uinput");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user