drm: calibrate only against samples injected under the layout's adopted uinput range

`usable_sample` took the remap flag being clear as "the input mapping is
ready". It is not: the display service bumps the layout generation and
promotes the baseline before the uinput range update completes, and a
failed or pending update leaves the device mapping absolute coordinates
with the range of the previous layout. Every sample then carries the new
generation while its point lands scaled by the old range; two stops share
that error and agreement confirms it (zhou, review of 25-sep: a desktop
narrowed from 4000 to 3800 px puts a hotx of 4 at 54, inside the bitmap).

The display service now keeps the layout generation whose uinput range
the device runs, and a count, raised when an apply starts and again when
the device acknowledges it. Both move when `update_mouse_resolution`
returns Ok, on the DRM session-init path (also run by the hotplug
worker) and in the layout poll; the session-init path labels the range
with the generation read before it computed the rect, and the raw DRM
union it falls back to when the compositor cannot be asked with no
generation, so no stream measures on it until the poll applies or finds
a compositor range. A range that already fits the layout moves the
generation only, so the samples injected under it stay valid. From the
moment an apply starts, on any path, no generation is ready, the count
is raised and the recorded rect is forgotten, so an apply that fails or
times out (the device may still take the range late) leaves nothing
ready, and only an acknowledged apply records a generation again. The
poll and `update_uinput_resolution` run on different threads; they
check, apply and record under one lock. `check_init`, which a display
falling back to PipeWire runs inside a DRM session, records its apply
the same way (cfg(feature = "drm") lines only; the drm-off build is
unchanged). It has no fits check and applies on every run, as before, so
each run moves the count. A path that read its generation before the
poll bumped it records under the older one; no stream measures then
until a later layout check records the current one. A sample is stamped
under the ENIGO guard of its move, which an adoption also needs for its
refresh, so a move stamped with the count of an adoption was injected
after that adoption's refresh. The count is raised before the generation
is published and read after it. A stream measures only while that
generation is its own; each peer sample carries the count at injection
and is a reference only under the current one; a candidate measured
under one count starts over instead of confirming under the next. The
remap flag is untouched.

Tests: a_stale_input_mapping_publishes_nothing_until_fresh_samples_follow_the_adoption
(generations equal, no drift, the device on the previous range: two
stops publish and cache nothing; after the adoption a sample from before
it does not count, and fresh ones confirm),
a_sample_is_a_reference_only_under_the_adopted_input_mapping,
a_candidate_does_not_confirm_across_an_input_mapping_change,
an_acknowledged_range_is_an_adoption_and_a_fitting_one_is_not,
the_raw_drm_union_labels_a_range_with_no_generation,
an_apply_leaves_no_range_ready_until_it_is_acknowledged; the drm CI step runs
input_map_tests. Mutations: dropping the adopted-generation gate,
dropping the count gate, confirming across counts, an adoption that does
not count, a fitting range that counts, the union labelled with the
generation, and an apply that keeps the recorded rect or leaves a
generation ready, each turn one of them red. The call sites (the lock,
the calls before and after each apply on the three paths, the range-fits
branches) run IPC and are covered by reading.
This commit is contained in:
Mariano Abad committed 2026-09-26 00:17:42 -03:00
1 parent 9c41279f7d
commit e6103daea8
5 files changed
+332 -64

No files matched your search

+2
View File
@@ -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" \
+115 -5
View File
@@ -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;
+180 -54
View File
@@ -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<Pos>,
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<super::input_service::PeerAbsSample>,
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<Hotspot> {
/// 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<Hotspot> {
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<CursorCal>,
pos: Option<Pos>,
sample: impl FnOnce() -> Option<super::input_service::PeerAbsSample>,
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<CursorCal>,
plane: Pos,
sample: Option<crate::server::input_service::PeerAbsSample>,
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);
+14 -5
View File
@@ -1084,8 +1084,9 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown<ControlKey>], 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<Option<((i32, i32), std::time::Instant, u64, u64)>> =
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<PeerAbsSample> {
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,
})
}
+21
View File
@@ -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.