From 70e345a76d9857905501e79066b031dfa9b51ef7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 14 Sep 2026 12:32:16 +0800 Subject: [PATCH] Fix Wayland cursor density in physical desktop layouts --- libs/base/src/platform/linux.rs | 6 +++ libs/scrap/src/wayland/display.rs | 63 ++++++++++++++++++++++ src/platform/linux/cursor.rs | 33 +++++++++++- src/server/drm_capturer.rs | 1 + src/server/drm_capturer/cursor_metadata.rs | 1 + 5 files changed, 103 insertions(+), 1 deletion(-) diff --git a/libs/base/src/platform/linux.rs b/libs/base/src/platform/linux.rs index aa1228ca6..4aaaa056b 100644 --- a/libs/base/src/platform/linux.rs +++ b/libs/base/src/platform/linux.rs @@ -320,6 +320,9 @@ pub struct WaylandDisplayInfo { pub width: i32, pub height: i32, pub logical_size: Option<(i32, i32)>, + // wl_output.scale is independent of desktop coordinates. Older probes omit it. + #[serde(default)] + pub scale_factor: i32, pub refresh_rate: i32, /// Output rotation in degrees (0/90/180/270), from `wl_output.geometry`. The mode keeps its /// unrotated dimensions and `logical_size` arrives already swapped, so without this field a @@ -450,6 +453,7 @@ fn collect_wayland_displays(conn: &Connection) -> ResultType Option<((i32, i32, i32, i32), Vec { + #[cfg(feature = "drm")] + { + // Output density can change without moving any desktop-coordinate rectangle. + let mut cache = DISPLAYS.lock().unwrap(); + if let Some(updated) = cache + .as_deref() + .and_then(|cached| updated_cursor_scales(cached, &displays)) + { + *cache = Some(Arc::new(updated)); + } + } desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays))) } Err(_err) => { @@ -366,6 +377,30 @@ pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec Option { + let mut displays = cached.displays.clone(); + let mut changed = false; + for output in &mut displays { + // Geometry changes retain the existing layout invalidation/rebuild path. + let Some(current) = live.iter().find(|d| { + d.name == output.name + && (d.x, d.y) == (output.x, output.y) + && (d.width, d.height) == (output.width, output.height) + && d.transform == output.transform + && d.logical_size == output.logical_size + }) else { + continue; + }; + changed |= output.scale_factor != current.scale_factor; + output.scale_factor = current.scale_factor; + } + changed.then_some(Displays { + primary: cached.primary, + displays, + }) +} + fn desktop_rect_of(displays: &[WaylandDisplayInfo]) -> Option<(i32, i32, i32, i32)> { if displays.is_empty() { return None; @@ -604,6 +639,7 @@ mod tests { width, height, logical_size, + scale_factor: 1, refresh_rate: 60, transform: 0, } @@ -614,6 +650,33 @@ mod tests { assert_eq!(desktop_rect_of(&[]), None); } + #[cfg(feature = "drm")] + #[test] + fn output_scale_refresh_preserves_layout_and_existing_snapshots() { + let mut output = display(0, 0, 2560, 1600, Some((2560, 1600))); + output.name = "eDP-1".into(); + output.scale_factor = 3; + let cached = Displays { + primary: 0, + displays: vec![output.clone()], + }; + output.scale_factor = 2; + let updated = updated_cursor_scales(&cached, &[output.clone()]).unwrap(); + assert_eq!(updated.displays[0].scale_factor, 2); + assert_eq!(cached.displays[0].scale_factor, 3); + assert_eq!(updated.primary, cached.primary); + assert_eq!( + logical_rects_of(&updated.displays), + logical_rects_of(&cached.displays) + ); + assert!(updated_cursor_scales(&updated, &[output.clone()]).is_none()); + output.name = "HDMI-1".into(); + assert!(updated_cursor_scales(&cached, &[output.clone()]).is_none()); + output.name = "eDP-1".into(); + output.logical_size = Some((1280, 800)); + assert!(updated_cursor_scales(&cached, &[output]).is_none()); + } + #[test] fn test_desktop_rect_single_display_uses_physical_size() { let displays = [display(0, 0, 2880, 1800, Some((1859, 1162)))]; diff --git a/src/platform/linux/cursor.rs b/src/platform/linux/cursor.rs index 0b7a9e1a9..6eced5159 100644 --- a/src/platform/linux/cursor.rs +++ b/src/platform/linux/cursor.rs @@ -128,7 +128,14 @@ fn wayland_scale(display: &base::platform::linux::WaylandDisplayInfo) -> ResultT } else { display.width }; - Ok(f64::from(width) / f64::from(logical_width)) + let scale = f64::from(width) / f64::from(logical_width); + // Mutter can report physical desktop coordinates even at 2x/3x output scale. + // Keep geometry-derived fractional densities for logically scaled desktops. + Ok(if scale == 1.0 && display.scale_factor > 1 { + f64::from(display.scale_factor) + } else { + scale + }) } #[cfg(test)] @@ -155,6 +162,7 @@ mod tests { width: 1280, height: 800, logical_size: Some((600, 960)), + scale_factor: 2, refresh_rate: 60000, transform: 90, }; @@ -162,4 +170,27 @@ mod tests { assert_ne!(cache_id(1, 1.0), cache_id(1, 2.0)); assert_eq!(cache_id(1, 0.0), 1); } + + #[cfg(feature = "drm")] + #[test] + fn cursor_density_distinguishes_output_scale_from_desktop_coordinates() { + for (scale_factor, logical_size, expected) in [ + (3, (2560, 1600), 3.0), // GNOME can use physical desktop coordinates. + (2, (2048, 1280), 1.25), // Keep fractional scaling from logical geometry. + (0, (2560, 1600), 1.0), // Older probe snapshots omit wl_output.scale. + ] { + let display = base::platform::linux::WaylandDisplayInfo { + name: "eDP-1".into(), + x: 0, + y: 0, + width: 2560, + height: 1600, + logical_size: Some(logical_size), + scale_factor, + refresh_rate: 120000, + transform: 0, + }; + assert_eq!(wayland_scale(&display).unwrap(), expected); + } + } } diff --git a/src/server/drm_capturer.rs b/src/server/drm_capturer.rs index 1427e94d8..497bd6fa0 100644 --- a/src/server/drm_capturer.rs +++ b/src/server/drm_capturer.rs @@ -2158,6 +2158,7 @@ mod drm_capturer_tests { width: w, height: h, logical_size: Some((w, h)), + scale_factor: 1, refresh_rate: 60, transform: 0, } diff --git a/src/server/drm_capturer/cursor_metadata.rs b/src/server/drm_capturer/cursor_metadata.rs index 81cbc92b3..5255f65c0 100644 --- a/src/server/drm_capturer/cursor_metadata.rs +++ b/src/server/drm_capturer/cursor_metadata.rs @@ -60,6 +60,7 @@ mod tests { width: 1280, height: 800, logical_size: Some((logical_width, logical_width * 800 / 1280)), + scale_factor: 1, refresh_rate: 60000, transform: 0, }],