diff --git a/libs/scrap/src/wayland/display.rs b/libs/scrap/src/wayland/display.rs index a5c937491..bed90fd76 100644 --- a/libs/scrap/src/wayland/display.rs +++ b/libs/scrap/src/wayland/display.rs @@ -14,6 +14,9 @@ lazy_static! { static ref DISPLAYS: Mutex>> = 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)> { + 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 { + // 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 { + 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)] + ); + } +} diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 946952ccd..8531076a9 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -28,6 +28,144 @@ lazy_static::lazy_static! { static ref SYNC_DISPLAYS: Arc> = Default::default(); } +#[cfg(target_os = "linux")] +lazy_static::lazy_static! { + static ref WAYLAND_UINPUT_RECT: Mutex = Default::default(); + static ref WAYLAND_LAYOUT: Mutex = 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, +} + +// 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, + live: Vec, +} + +// 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) { + 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)); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 91a2901dc..1d4deeb65 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -661,20 +661,22 @@ pub async fn setup_rdp_input() -> ResultType<(), Box> { 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::() { - 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. diff --git a/src/server/uinput.rs b/src/server/uinput.rs index a1947d79f..496da709f 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -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 { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 7927096a6..dacce9485 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -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"); }