diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 083502239..57b898321 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -2084,6 +2084,8 @@ jobs: cargo test --locked --lib --features drm cursor_pos_tests run_drm_tests "rustdesk peer_abs_sample_tests" \ cargo test --locked --lib --features drm peer_abs_sample_tests + run_drm_tests "rustdesk input_map_tests" \ + cargo test --locked --lib --features drm input_map_tests run_drm_tests "scrap hotspot_provenance_tests" \ cargo test --locked -p scrap --features drm hotspot_provenance_tests run_drm_tests "scrap hotspot_guess_tests" \ diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 1e7f403f4..b2332704e 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -156,6 +156,61 @@ pub(super) fn set_wayland_uinput_rect(rect: (i32, i32, i32, i32)) { WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect); } +/// The layout generation whose uinput range the device runs, and a count raised when an apply +/// starts and when the device acknowledges one. The DRM cursor calibration measures only under its +/// own generation and against a sample injected under the current count: a stale range maps the +/// point elsewhere, and two stops share that error, so agreement cannot reject it. +#[cfg(all(target_os = "linux", feature = "drm"))] +static INPUT_MAP_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(u64::MAX); +#[cfg(all(target_os = "linux", feature = "drm"))] +static INPUT_MAP_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +/// Read before `input_map_epoch`: seeing a generation means seeing the count its adoption raised. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(crate) fn input_map_gen() -> u64 { + INPUT_MAP_GEN.load(Ordering::Acquire) +} +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(crate) fn input_map_epoch() -> u64 { + INPUT_MAP_EPOCH.load(Ordering::Relaxed) +} +/// The device acknowledged a range for generation `gen` (on the session-init path, the one read +/// before the rect was computed, which can predate a move the poll has not seen yet). Samples +/// injected before now were mapped by the previous range; the count moves first. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(super) fn note_input_map_adopted(gen: u64) { + INPUT_MAP_EPOCH.fetch_add(1, Ordering::Relaxed); + INPUT_MAP_GEN.store(gen, Ordering::Release); +} +/// The range the device already runs fits the layout of `gen`. Not an adoption: the samples +/// injected under it stay valid. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(super) fn note_input_map_ready(gen: u64) { + INPUT_MAP_GEN.store(gen, Ordering::Release); +} +/// An apply is starting: until the device acknowledges it no range is ready, samples from before +/// now do not count, and the recorded rect is forgotten, so a failed or timed-out apply (the device +/// may still take the range late) leaves nothing ready and the next check applies again. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(super) fn note_input_map_unknown() { + WAYLAND_UINPUT_RECT.lock().unwrap().rect = None; + note_input_map_adopted(u64::MAX); +} +/// The uinput range checks, applies and what they record run under this on the DRM paths: the +/// layout poll and `update_uinput_resolution` run on different threads. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(super) static UINPUT_APPLY: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); +/// The generation a range is recorded under: none for the raw DRM union, which is not a compositor +/// layout any stream was built from. +#[cfg(all(target_os = "linux", feature = "drm"))] +pub(super) fn input_map_label(gen: u64, from_compositor: bool) -> u64 { + if from_compositor { + gen + } else { + u64::MAX + } +} + // The uinput ABS range currently programmed into the device, for the DRM path's "reapply only when // it changed" check. The PipeWire path compares it inline in refresh_wayland_uinput_rect_if_changed. #[cfg(all(target_os = "linux", feature = "drm"))] @@ -264,17 +319,24 @@ fn refresh_wayland_uinput_rect_if_changed() { } #[cfg(not(feature = "drm"))] let _ = live_changed; + // 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. + #[cfg(feature = "drm")] + let _apply = UINPUT_APPLY.blocking_lock(); + let mut range_ok = WAYLAND_UINPUT_RECT.lock().unwrap().rect == Some(rect); + // The device already runs a range that fits this layout: the DRM calibration may measure. + #[cfg(feature = "drm")] + if range_ok { + note_input_map_ready(scrap::wayland::display::wayland_snapshot_generation()); + } // At a login screen the DRM path owns the rect; only the range/remap update is skipped, // the snapshot invalidation above must still run (a greeter session has no other trigger). #[cfg(feature = "drm")] if crate::platform::linux::is_login_screen_wayland_cached() { return; } - // 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!( @@ -284,6 +346,8 @@ fn refresh_wayland_uinput_rect_if_changed() { miny, maxy ); + #[cfg(feature = "drm")] + note_input_map_unknown(); match tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -304,6 +368,8 @@ fn refresh_wayland_uinput_rect_if_changed() { // failure is retried on the next check. Ok(Ok(())) => { WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect); + #[cfg(feature = "drm")] + note_input_map_adopted(scrap::wayland::display::wayland_snapshot_generation()); range_ok = true; } Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err), @@ -864,6 +930,50 @@ mod tests { } } +#[cfg(all(test, target_os = "linux", feature = "drm"))] +mod input_map_tests { + use super::{ + input_map_epoch, input_map_gen, input_map_label, note_input_map_adopted, + note_input_map_ready, note_input_map_unknown, set_wayland_uinput_rect, + wayland_uinput_rect, + }; + + // The input-map statics are process-wide and libtest runs tests concurrently. + static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[test] + fn an_apply_leaves_no_range_ready_until_it_is_acknowledged() { + let _serial = SERIAL.lock().unwrap_or_else(|p| p.into_inner()); + set_wayland_uinput_rect((0, 1920, 0, 1080)); + note_input_map_adopted(41); + let before = input_map_epoch(); + note_input_map_unknown(); + assert_eq!(input_map_gen(), u64::MAX, "no generation is ready"); + assert_eq!(input_map_epoch(), before + 1, "samples from before do not count"); + assert_eq!(wayland_uinput_rect(), None, "the poll applies again"); + } + + #[test] + fn the_raw_drm_union_labels_a_range_with_no_generation() { + assert_eq!(input_map_label(5, true), 5); + assert_eq!(input_map_label(5, false), u64::MAX); + } + + #[test] + fn an_acknowledged_range_is_an_adoption_and_a_fitting_one_is_not() { + let _serial = SERIAL.lock().unwrap_or_else(|p| p.into_inner()); + let before = input_map_epoch(); + note_input_map_adopted(41); + assert_eq!((input_map_gen(), input_map_epoch()), (41, before + 1)); + note_input_map_ready(42); + assert_eq!( + (input_map_gen(), input_map_epoch()), + (42, before + 1), + "a range that already fits is not an adoption" + ); + } +} + #[cfg(all(test, target_os = "linux"))] mod wayland_layout_tests { use super::WaylandLayout; diff --git a/src/server/drm_capturer.rs b/src/server/drm_capturer.rs index dffa22a56..e489193bb 100644 --- a/src/server/drm_capturer.rs +++ b/src/server/drm_capturer.rs @@ -692,6 +692,8 @@ async fn recv_thread( super::input_service::last_peer_abs_sample, super::display_service::wayland_layout_drifted(), scrap::wayland::display::wayland_snapshot_generation(), + super::display_service::input_map_gen(), + super::display_service::input_map_epoch(), display, cursor_epoch, ); @@ -763,6 +765,8 @@ async fn recv_thread( super::input_service::last_peer_abs_sample, super::display_service::wayland_layout_drifted(), scrap::wayland::display::wayland_snapshot_generation(), + super::display_service::input_map_gen(), + super::display_service::input_map_epoch(), display, cursor_epoch, ); @@ -1051,8 +1055,9 @@ struct CursorCal { wire_hot: Hotspot, /// What the client is being served: the cache's confirmed value, else the wire's. current_hot: Hotspot, - /// An unconfirmed measurement, with the peer sample it was taken against. - candidate: Option<(Hotspot, u64)>, + /// An unconfirmed measurement, with the peer sample it was taken against and the + /// input-mapping epoch of that sample. + candidate: Option<(Hotspot, u64, u64)>, plane: Option, stable_ticks: u32, measured_this_position: bool, @@ -1084,28 +1089,32 @@ fn settled_unmeasured(c: &CursorCal) -> bool { /// The peer sample a measurement may subtract from, or none: the layout must not have moved /// since the context was built (the remap flag is a secondary guard for the window in which the -/// promotion is still owed), the sample must have been mapped against that same layout, and it -/// must be old enough to have been consumed and young enough to still describe the pointer. -/// The generation and the flag are both advanced by the display service's layout poll, so a -/// monitor moved mid-session is seen up to one check interval late; a measurement inside that -/// window is bounded by the bitmap and replaced by the next confirmed pair. +/// promotion is still owed), the device must run the uinput range of that layout and the sample +/// must have been injected after it adopted that range (a stale range maps the point elsewhere, +/// and two stops share the error), the sample must have been mapped against that same layout, +/// and it must be old enough to have been consumed and young enough to still describe the +/// pointer. The generation and the flag are both advanced by the display service's layout poll, +/// so a monitor moved mid-session is seen up to one check interval late; a measurement inside +/// that window is bounded by the bitmap and cached under its generation only. fn usable_sample( ctx: &CalContext, s: Option, drifted: bool, current_gen: u64, -) -> Option<(Pos, u64)> { - if drifted || current_gen != ctx.built_gen { + map_gen: u64, + map_epoch: u64, +) -> Option<(Pos, u64, u64)> { + if drifted || current_gen != ctx.built_gen || map_gen != ctx.built_gen { return None; } let s = s?; - if s.gen != ctx.built_gen { + if s.gen != ctx.built_gen || s.map_epoch != map_epoch { return None; } if !(CURSOR_CAL_MIN_INPUT_AGE_MS..=CURSOR_CAL_MAX_INPUT_AGE_MS).contains(&s.age_ms) { return None; } - Some((s.pos, s.seq)) + Some((s.pos, s.seq, s.map_epoch)) } fn near(a: Hotspot, b: Hotspot) -> bool { @@ -1115,16 +1124,18 @@ fn near(a: Hotspot, b: Hotspot) -> bool { /// The confirmation policy. `Some` only when a second measurement agrees with the candidate, /// and then it IS the candidate, never the second raw value. Two measurements against the same /// peer sample are not independent: the plane moved and the peer did not, which is what a local -/// hand on the mouse looks like, so they never confirm each other. -fn observe_measurement(c: &mut CursorCal, m: Hotspot, seq: u64) -> Option { +/// hand on the mouse looks like, so they never confirm each other. Nor do two taken under +/// different input mappings: the device adopted a new range in between, and the candidate was +/// measured against the old one. +fn observe_measurement(c: &mut CursorCal, m: Hotspot, seq: u64, map_epoch: u64) -> Option { match c.candidate { - Some((_, kseq)) if kseq == seq => None, - Some((k, _)) if near(k, m) => { + Some((_, kseq, _)) if kseq == seq => None, + Some((k, _, kepoch)) if kepoch == map_epoch && near(k, m) => { c.candidate = None; Some(k) } _ => { - c.candidate = Some((m, seq)); + c.candidate = Some((m, seq, map_epoch)); None } } @@ -1281,12 +1292,15 @@ fn on_cursor_shape( /// A frame reported where the plane is. The sample is read only once the plane has settled, so /// most frames cost one comparison and no lock. +#[allow(clippy::too_many_arguments)] fn on_cursor_plane( cal: &mut Option, pos: Option, sample: impl FnOnce() -> Option, drifted: bool, current_gen: u64, + map_gen: u64, + map_epoch: u64, display: i32, cursor_epoch: u64, ) { @@ -1297,14 +1311,16 @@ fn on_cursor_plane( if !settled_unmeasured(c) { return; } - let Some((injected, seq)) = usable_sample(&c.ctx, sample(), drifted, current_gen) else { + let Some((injected, seq, epoch)) = + usable_sample(&c.ctx, sample(), drifted, current_gen, map_gen, map_epoch) + else { return; }; c.measured_this_position = true; let Some(m) = measure_hotspot(&c.ctx, injected, p, (c.width as i32, c.height as i32)) else { return; }; - let Some(accepted) = observe_measurement(c, m, seq) else { + let Some(accepted) = observe_measurement(c, m, seq, epoch) else { log::debug!("drm: cursor hotspot for display {display}: measured {m:?} (sample {seq}), waiting for a second one"); return; }; @@ -2956,15 +2972,21 @@ mod drm_capturer_tests { } } fn peer(pos: Pos, seq: u64) -> crate::server::input_service::PeerAbsSample { - peer_under(pos, seq, CAL_GEN) + peer_under(pos, seq, CAL_GEN, 0) } - /// A sample injected under layout generation `gen`. - fn peer_under(pos: Pos, seq: u64, gen: u64) -> crate::server::input_service::PeerAbsSample { + /// A sample injected under layout generation `gen` and input-mapping count `map_epoch`. + fn peer_under( + pos: Pos, + seq: u64, + gen: u64, + map_epoch: u64, + ) -> crate::server::input_service::PeerAbsSample { crate::server::input_service::PeerAbsSample { pos, age_ms: 400, seq, gen, + map_epoch, } } /// A guessed shape arriving on a stream with a context, as the receive loop registers it. @@ -2992,19 +3014,20 @@ mod drm_capturer_tests { display: i32, epoch: u64, ) { - settle_under(cal, plane, sample, display, epoch, CAL_GEN); + settle_under(cal, plane, sample, display, epoch, (CAL_GEN, CAL_GEN, 0)); } - /// `settle` for a stream of layout generation `gen`. + /// `settle` under (layout generation, generation whose uinput range the device runs, + /// input-mapping count), as the receive loop reads them. fn settle_under( cal: &mut Option, plane: Pos, sample: Option, display: i32, epoch: u64, - gen: u64, + (gen, map_gen, map_epoch): (u64, u64, u64), ) { for _ in 0..=CURSOR_CAL_STABLE_TICKS { - on_cursor_plane(cal, Some(plane), || sample, false, gen, display, epoch); + on_cursor_plane(cal, Some(plane), || sample, false, gen, map_gen, map_epoch, display, epoch); } } fn served(display: i32) -> (u64, i32, i32) { @@ -3032,7 +3055,7 @@ mod drm_capturer_tests { assert_eq!(served(d), (plain(shape), 4, 4)); // (500,300) - (488,288) = (12,12): the candidate. settle(&mut cal, (488, 288), Some(peer((500, 300), 1)), d, e); - assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1, 0))); assert_eq!(cal.as_ref().unwrap().current_hot, (4, 4)); assert_eq!(served(d), (plain(shape), 4, 4), "unconfirmed: nothing visible changes"); assert_eq!(cached_cursor_cal(shape, CAL_GEN), None); @@ -3111,15 +3134,15 @@ mod drm_capturer_tests { #[test] fn a_disagreeing_measurement_replaces_the_candidate_and_the_same_sample_never_confirms() { let mut c = arrive(9_703, next_cursor_epoch(), 0x7a03, (4, 4)).unwrap(); - assert_eq!(observe_measurement(&mut c, (12, 12), 1), None); - assert_eq!(c.candidate, Some(((12, 12), 1))); - assert_eq!(observe_measurement(&mut c, (30, 4), 2), None, "disagreement replaces"); - assert_eq!(c.candidate, Some(((30, 4), 2))); - assert_eq!(observe_measurement(&mut c, (31, 4), 3), Some((30, 4)), "the retained one"); + assert_eq!(observe_measurement(&mut c, (12, 12), 1, 0), None); + assert_eq!(c.candidate, Some(((12, 12), 1, 0))); + assert_eq!(observe_measurement(&mut c, (30, 4), 2, 0), None, "disagreement replaces"); + assert_eq!(c.candidate, Some(((30, 4), 2, 0))); + assert_eq!(observe_measurement(&mut c, (31, 4), 3, 0), Some((30, 4)), "the retained one"); assert_eq!(c.candidate, None); - assert_eq!(observe_measurement(&mut c, (12, 12), 4), None); - assert_eq!(observe_measurement(&mut c, (12, 12), 4), None, "same sample: not independent"); - assert_eq!(c.candidate, Some(((12, 12), 4))); + assert_eq!(observe_measurement(&mut c, (12, 12), 4, 0), None); + assert_eq!(observe_measurement(&mut c, (12, 12), 4, 0), None, "same sample: not independent"); + assert_eq!(c.candidate, Some(((12, 12), 4, 0))); } // The local hand on the mouse (review of rustdesk#16122): the remote peer stops, a local user @@ -3133,12 +3156,12 @@ mod drm_capturer_tests { let before = served(d); let stale = Some(peer((500, 300), 1)); settle(&mut cal, (493, 288), stale, d, e); // (7,12) - assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1, 0))); settle(&mut cal, (494, 288), stale, d, e); // (6,12), same sample - assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1, 0))); settle(&mut cal, (400, 400), stale, d, e); // away: out of the bitmap settle(&mut cal, (493, 288), stale, d, e); // back: (7,12) again, same sample - assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((7, 12), 1, 0))); assert_eq!(served(d), before); assert_eq!(cached_cursor_cal(shape, CAL_GEN), None); } @@ -3259,12 +3282,12 @@ mod drm_capturer_tests { let mut cal = arrive(d, e, shape, (4, 4)); let before = served(d); settle(&mut cal, (488, 288), Some(peer((500, 300), 1)), d, e); - assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1, 0))); for _ in 0..CURSOR_CAL_WINDOW_TICKS { let fresh = Some(peer((500, 300), 2)); - on_cursor_plane(&mut cal, Some((488, 288)), || fresh, false, CAL_GEN, d, e); + on_cursor_plane(&mut cal, Some((488, 288)), || fresh, false, CAL_GEN, CAL_GEN, 0, d, e); } - assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1))); + assert_eq!(cal.as_ref().unwrap().candidate, Some(((12, 12), 1, 0))); assert_eq!(served(d), before); assert_eq!(cached_cursor_cal(shape, CAL_GEN), None); } @@ -3292,18 +3315,60 @@ mod drm_capturer_tests { age_ms: age, seq: 9, gen, + map_epoch: 3, }) }; let min = CURSOR_CAL_MIN_INPUT_AGE_MS; let max = CURSOR_CAL_MAX_INPUT_AGE_MS; - assert_eq!(usable_sample(&ctx, s(min, CAL_GEN), false, CAL_GEN), Some(((1, 1), 9))); - assert_eq!(usable_sample(&ctx, s(max, CAL_GEN), false, CAL_GEN), Some(((1, 1), 9))); - assert_eq!(usable_sample(&ctx, s(min - 1, CAL_GEN), false, CAL_GEN), None); - assert_eq!(usable_sample(&ctx, s(max + 1, CAL_GEN), false, CAL_GEN), None); - assert_eq!(usable_sample(&ctx, None, false, CAL_GEN), None); - assert_eq!(usable_sample(&ctx, s(400, CAL_GEN), true, CAL_GEN), None, "drift"); - assert_eq!(usable_sample(&ctx, s(400, CAL_GEN), false, CAL_GEN + 1), None, "layout moved"); - assert_eq!(usable_sample(&ctx, s(400, CAL_GEN - 1), false, CAL_GEN), None, "old sample"); + let g = CAL_GEN; + assert_eq!(usable_sample(&ctx, s(min, g), false, g, g, 3), Some(((1, 1), 9, 3))); + assert_eq!(usable_sample(&ctx, s(max, g), false, g, g, 3), Some(((1, 1), 9, 3))); + assert_eq!(usable_sample(&ctx, s(min - 1, g), false, g, g, 3), None); + assert_eq!(usable_sample(&ctx, s(max + 1, g), false, g, g, 3), None); + assert_eq!(usable_sample(&ctx, None, false, g, g, 3), None); + assert_eq!(usable_sample(&ctx, s(400, g), true, g, g, 3), None, "drift"); + assert_eq!(usable_sample(&ctx, s(400, g), false, g + 1, g, 3), None, "layout moved"); + assert_eq!(usable_sample(&ctx, s(400, g - 1), false, g, g, 3), None, "old sample"); + } + + /// zhou, 25-sep review of #16122: while the device runs the range of another layout (an + /// update pending or failed after the generation moved), or before any range was adopted, an + /// injected point lands elsewhere than the peer meant, and two stops share that error; and a + /// sample injected before the last adoption was mapped by the old range even when the + /// generation matches. Neither is a calibration reference. + #[test] + fn a_sample_is_a_reference_only_under_the_adopted_input_mapping() { + let ctx = ctx1(); + let s = Some(crate::server::input_service::PeerAbsSample { + pos: (1, 1), + age_ms: 400, + seq: 9, + gen: CAL_GEN, + map_epoch: 3, + }); + assert_eq!(usable_sample(&ctx, s, false, CAL_GEN, CAL_GEN, 3), Some(((1, 1), 9, 3))); + assert_eq!( + usable_sample(&ctx, s, false, CAL_GEN, CAL_GEN - 1, 3), + None, + "the device still runs the range of the previous layout" + ); + assert_eq!(usable_sample(&ctx, s, false, CAL_GEN, u64::MAX, 3), None, "no range adopted"); + assert_eq!( + usable_sample(&ctx, s, false, CAL_GEN, CAL_GEN, 4), + None, + "injected before the device adopted the current range" + ); + } + + /// The candidate was measured under the previous input mapping: a measurement under the new + /// one starts over instead of confirming it, however well the two agree. + #[test] + fn a_candidate_does_not_confirm_across_an_input_mapping_change() { + let mut c = arrive(9_721, next_cursor_epoch(), 0x7a21, (4, 4)).unwrap(); + assert_eq!(observe_measurement(&mut c, (30, 4), 1, 3), None); + assert_eq!(observe_measurement(&mut c, (30, 4), 2, 4), None, "a new range in between"); + assert_eq!(c.candidate, Some(((30, 4), 2, 4)), "the newer one is the candidate"); + assert_eq!(observe_measurement(&mut c, (31, 4), 3, 4), Some((30, 4))); } /// zhou, 25-sep review of #16122: a correction two stops confirmed under one layout @@ -3330,18 +3395,18 @@ mod drm_capturer_tests { settle_under( &mut new, (480, 280), - Some(peer_under((500, 300), 3, next)), + Some(peer_under((500, 300), 3, next, 0)), d2, e2, - next, + (next, next, 0), ); settle_under( &mut new, (580, 380), - Some(peer_under((600, 400), 4, next)), + Some(peer_under((600, 400), 4, next, 0)), d2, e2, - next, + (next, next, 0), ); assert_eq!(cached_cursor_cal(shape, next), Some((20, 20))); assert_eq!(served(d2), (corrected(shape, (20, 20)), 20, 20)); @@ -3351,6 +3416,67 @@ mod drm_capturer_tests { forget_cursor_cal(shape, next); } + /// zhou, 25-sep review of #16122: generations equal and no drift, but the device still runs + /// the uinput range of the previous layout: two otherwise valid stops publish and cache + /// nothing. After the device acknowledges the range, a sample injected before that does not + /// count, and fresh ones confirm. + #[test] + fn a_stale_input_mapping_publishes_nothing_until_fresh_samples_follow_the_adoption() { + let _serial = CACHE_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let (d, e, shape) = (9_725, next_cursor_epoch(), 0x7a25); + forget_cursor_cal(shape, CAL_GEN); + let mut cal = arrive(d, e, shape, (4, 4)); + let stale = (CAL_GEN, CAL_GEN - 1, 0); + settle_under( + &mut cal, + (488, 288), + Some(peer_under((500, 300), 1, CAL_GEN, 0)), + d, + e, + stale, + ); + settle_under( + &mut cal, + (587, 388), + Some(peer_under((600, 400), 2, CAL_GEN, 0)), + d, + e, + stale, + ); + assert_eq!(cal.as_ref().unwrap().candidate, None, "nothing measured"); + assert_eq!(cached_cursor_cal(shape, CAL_GEN), None); + assert_eq!(served(d), (plain(shape), 4, 4)); + let adopted = (CAL_GEN, CAL_GEN, 1); + settle_under( + &mut cal, + (478, 278), + Some(peer_under((500, 300), 3, CAL_GEN, 0)), + d, + e, + adopted, + ); + assert_eq!(cal.as_ref().unwrap().candidate, None, "injected before the adoption"); + settle_under( + &mut cal, + (488, 288), + Some(peer_under((500, 300), 4, CAL_GEN, 1)), + d, + e, + adopted, + ); + settle_under( + &mut cal, + (587, 388), + Some(peer_under((600, 400), 5, CAL_GEN, 1)), + d, + e, + adopted, + ); + assert_eq!(cached_cursor_cal(shape, CAL_GEN), Some((12, 12))); + assert_eq!(served(d), (corrected(shape, (12, 12)), 12, 12)); + forget_cursor_cal(shape, CAL_GEN); + } + #[test] fn a_settle_lands_one_sample_later_than_the_constant_reads() { let mut c = arrive(9_710, next_cursor_epoch(), 0x7a10, (4, 4)).unwrap(); @@ -3433,11 +3559,11 @@ mod drm_capturer_tests { fn the_first_position_after_a_shape_never_counts_as_settled() { let (d, e, shape) = (9_711, next_cursor_epoch(), 0x7a11); let mut cal = arrive(d, e, shape, (4, 4)); - on_cursor_plane(&mut cal, None, || None, false, CAL_GEN, d, e); + on_cursor_plane(&mut cal, None, || None, false, CAL_GEN, CAL_GEN, 0, d, e); assert_eq!(cal.as_ref().unwrap().plane, None, "no position, no state change"); for _ in 0..CURSOR_CAL_STABLE_TICKS { let s = Some(peer((500, 300), 1)); - on_cursor_plane(&mut cal, Some((488, 288)), || s, false, CAL_GEN, d, e); + on_cursor_plane(&mut cal, Some((488, 288)), || s, false, CAL_GEN, CAL_GEN, 0, d, e); } let c = cal.as_ref().unwrap(); assert_eq!(c.stable_ticks, CURSOR_CAL_STABLE_TICKS - 1); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7c2d0231f..46063729a 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1084,8 +1084,9 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i3 /// DRM cursor calibration subtracts a cursor-plane position from this, so the two spaces must /// match. Only the uinput absolute path writes here. #[cfg(all(target_os = "linux", feature = "drm"))] -static LATEST_PEER_ABS_POS: std::sync::Mutex> = - std::sync::Mutex::new(None); +static LATEST_PEER_ABS_POS: std::sync::Mutex< + Option<((i32, i32), std::time::Instant, u64, u64, u64)>, +> = std::sync::Mutex::new(None); #[cfg(all(target_os = "linux", feature = "drm"))] static PEER_ABS_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); @@ -1100,14 +1101,21 @@ pub(crate) struct PeerAbsSample { pub seq: u64, /// The layout generation the point was mapped against. pub gen: u64, + /// The input-mapping epoch at injection: a sample from before a range adoption was mapped by + /// the old range. See `display_service::input_map_epoch`. + pub map_epoch: u64, } -/// The peer moved the pointer to an absolute, post-remap position. +/// The peer moved the pointer to an absolute, post-remap position. Called under the ENIGO guard +/// of the move, which an adoption also needs for its refresh: a move stamped with the count of an +/// adoption was injected after that adoption's refresh. #[cfg(all(target_os = "linux", feature = "drm"))] pub(crate) fn note_peer_absolute_move(x: i32, y: i32) { let seq = PEER_ABS_SEQ.fetch_add(1, Ordering::Relaxed) + 1; let gen = scrap::wayland::display::wayland_snapshot_generation(); - *LATEST_PEER_ABS_POS.lock().unwrap() = Some(((x, y), std::time::Instant::now(), seq, gen)); + let map_epoch = super::display_service::input_map_epoch(); + *LATEST_PEER_ABS_POS.lock().unwrap() = + Some(((x, y), std::time::Instant::now(), seq, gen, map_epoch)); } /// The pointer moved by a path that has no post-remap absolute position to offer: a relative @@ -1120,12 +1128,13 @@ pub(crate) fn note_pointer_moved_without_absolute_sample() { #[cfg(all(target_os = "linux", feature = "drm"))] pub(crate) fn last_peer_abs_sample() -> Option { - let (pos, at, seq, gen) = (*LATEST_PEER_ABS_POS.lock().unwrap())?; + let (pos, at, seq, gen, map_epoch) = (*LATEST_PEER_ABS_POS.lock().unwrap())?; Some(PeerAbsSample { pos, age_ms: at.elapsed().as_millis() as u64, seq, gen, + map_epoch, }) } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 9fcf6d168..7349c8a7c 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -371,6 +371,10 @@ pub(super) async fn update_uinput_resolution() { // runs on current-thread runtimes (session init and the hotplug worker). The layout baseline // is computed in the SAME task: a failed lookup is not cached, so asking for the rects // afterwards would rerun the whole socket probe synchronously. + // + // The generation read before the rect is computed labels the range for the DRM cursor + // calibration; the raw DRM union below is no compositor layout and is labelled with none. + let gen = scrap::wayland::display::wayland_snapshot_generation(); let (rect, layout) = match hbb_common::tokio::task::spawn_blocking(|| { scrap::wayland::display::clear_wayland_displays_cache(); match scrap::wayland::display::get_desktop_rect_for_uinput() { @@ -393,6 +397,7 @@ pub(super) async fn update_uinput_resolution() { return; } }; + let gen = super::display_service::input_map_label(gen, !layout.is_empty()); // Re-snapshot the baseline on every call: this runs at session init and after every hotplug, and // the baseline is what the client's coordinates are measured against. let snapshot_layout = || { @@ -401,12 +406,15 @@ pub(super) async fn update_uinput_resolution() { // Reprogram the device only when the range actually changes. A display stuck in a rebuild loop // calls this about once a second, and reapplying an identical range is an IPC roundtrip plus a // uinput device reconfiguration under a user who may be at the console. + let _apply = super::display_service::UINPUT_APPLY.lock().await; if super::display_service::wayland_uinput_rect() == Some(rect) { + super::display_service::note_input_map_ready(gen); snapshot_layout(); return; } let (minx, maxx, miny, maxy) = rect; log::info!("update mouse resolution: ({minx}, {maxx}), ({miny}, {maxy})"); + super::display_service::note_input_map_unknown(); match timeout( 3_000, input_service::update_mouse_resolution(minx, maxx, miny, maxy), @@ -417,6 +425,7 @@ pub(super) async fn update_uinput_resolution() { // next call instead of being remembered as applied. Ok(Ok(())) => { super::display_service::set_wayland_uinput_rect(rect); + super::display_service::note_input_map_adopted(gen); snapshot_layout(); } Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err), @@ -468,6 +477,10 @@ pub(super) async fn check_init() -> ResultType<()> { 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 + // Read before the rect: an apply that races a layout move is recorded under the + // older generation, which only streams built before the move measure under. + #[cfg(feature = "drm")] + let gen = scrap::wayland::display::wayland_snapshot_generation(); scrap::wayland::display::clear_wayland_displays_cache(); if let Some((minx, maxx, miny, maxy)) = scrap::wayland::display::get_desktop_rect_for_uinput() @@ -479,6 +492,12 @@ pub(super) async fn check_init() -> ResultType<()> { miny, maxy ); + // A display that falls back to PipeWire in a DRM session runs this apply + // too: record it for the calibration the way the DRM paths do. + #[cfg(feature = "drm")] + let _apply = super::display_service::UINPUT_APPLY.lock().await; + #[cfg(feature = "drm")] + super::display_service::note_input_map_unknown(); // Bound the IPC wait like the periodic refresh does, so a hung // response can't stall session init. match timeout( @@ -491,6 +510,8 @@ pub(super) async fn check_init() -> ResultType<()> { super::display_service::set_wayland_uinput_rect(( minx, maxx, miny, maxy, )); + #[cfg(feature = "drm")] + super::display_service::note_input_map_adopted(gen); // 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.