mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-19 16:17:03 +00:00
fix(drm): measure the cursor hotspot on drivers without HOTSPOT_X/Y, confirm before publishing (#16122)
* drm: carry the cursor plane position on the frame header
CursorSnapshot gains the plane position x/y. It rides DmabufDesc.cursor_pos
and Data::DrmFrame.cursor_pos as a serde-default Option, so a producer that
predates the field reads as None on the consumer, and a consumer that predates
it ignores the key. The producer reads the cursor once per delivered frame,
after the grab, so a WouldBlock retry does no cursor work; the position is a
few ms newer than the frame, which the consumer tolerates because it only
counts consecutive identical positions. The consumer destructures the field
and does not use it yet.
* input: remember the last absolute peer move and forget it on any other move
The DRM cursor calibration subtracts a cursor plane origin from the point this
process injected, so the point has to be the post-remap uinput one and nothing
else: LATEST_PEER_INPUT_CURSOR stores an X-server coordinate on the relative
arm. The new sample carries an Instant, a sequence number (two measurements
against one sample are not independent) and the layout generation it was
mapped against. A relative delta and a move this process makes itself forget
it. display_service exposes the existing drift flag to readers.
* drm: resolve the cursor calibration context once, with the capturer
CalContext { rect, physical_size, built_gen } is computed in
IpcDrmCapturer::new from the same wayland snapshot the transform comes from,
and only when the snapshot is complete, the output is an identity match, the
unfolded transform is 0 (180 included) and the geometry is valid. It is
stored in Shared before the transform's Release store, so a receive thread
that has seen the transform sees the context. Nothing reads it yet.
* drm: measure the cursor hotspot and confirm it before publishing
On a driver without HOTSPOT_X/Y the wire carries infer_hotspot's guess, which
the bitmap cannot get right for a shape whose click point the theme put where
the alpha does not mark it. This process injects the tip and the plane origin
rides every frame: once both are still, tip - origin is the hotspot.
Only a guessed shape is measured. The first measurement is a candidate and
changes nothing visible. A second measurement confirms it only from another
stable plane position and another peer sample, and then the retained
candidate is the one value that reaches the cache, the served hotspot, the id
remix and the delivery. Near what is served, nothing changes, even when the
value is also near the wire, so a correction a few px from the wire does not
flap; near the wire otherwise, the wire is served and the cached correction is
dropped. A stable position is measured at most once, a layout generation
change stops measurement once the display service's poll has seen it, and the
cache is read only behind the context gate, so a correction measured upright
never reaches a rotated output.
Each of these gates has a test that goes red with the check removed or
altered (file restored byte for byte afterwards):
publish the first measurement -> retained candidate, same sample, still plane, local nudge
same sample confirms -> same sample never confirms, local nudge
confirm with the new value -> retained candidate, near wire, in band
drop the near-wire arm -> near wire serves the wire
drop the in-band arm -> in band changes nothing visible
drop the re-store after clear-on-cap -> in band stays cached
measure a kernel-measured hotspot -> never calibrated at the origin
read the cache without a context -> not served without a context
drop the once-per-position flag -> still plane measured once
drop the transform gate -> context only for an upright output
swap phys and extent -> the arithmetic, the scaled context
drop the clear-on-cap -> cache bounded
drop the partial-snapshot gate -> context only for an identity match
drop the generation check -> closed intervals
compare the wire before the served -> in band with both keeps the served correction
* drm: a cached hotspot correction is served only under the generation it was confirmed in
The calibration cache mapped a wire cursor id to its confirmed hotspot
and nothing else. A pair of stops taken inside the layout poll window,
after a monitor moved and before the poll saw it, could confirm a value
measured against the old layout; the generation gate then stopped that
stream, but the capturer rebuilt after the promotion seeded its served
hotspot from the cache and replayed the value under the new layout, until
another pair of its own replaced it (zhou, review of 25-sep).
Each entry now carries the layout generation it was confirmed under, and
a stream reads the cache under its own `built_gen` only: a value from
another generation is not served, the wire hotspot is, until a pair on
this generation confirms one. Checked at the read rather than by clearing
on promotion, and an entry is replaced or dropped only by its own
generation or a newer one, the bound included (at the cap the cache
clears what the writer's generation or an older one confirmed, and
caches nothing if only newer entries are left), so a receive thread
still running on the old layout can neither serve its value to the
rebuilt stream nor undo the value that stream confirmed.
Tests: a_correction_confirmed_under_an_older_generation_is_not_served (two
stops under one generation, the stream rebuilt under the next, its own
pair, then the old stream writing late),
the_bound_never_drops_an_entry_from_a_newer_generation. Mutations:
reading without the generation filter, replacing or dropping a newer
entry from an older generation, and at the cap either clearing
everything or caching when only newer entries are left, each turn one
of them red.
* 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.
* drm: forget the peer sample under the mouse lock on a move of this process
The temporary-move thread (the display probe `fill_displays` runs in the
PipeWire init, `check_init`) forgot the last absolute peer sample before
it took ENIGO. A peer move holding ENIGO at that moment moved the pointer
and recorded its sample after the forget, and the temporary move then
landed on top: the pointer somewhere else, the sample still the peer
point, with the current generation, count and a fresh sequence, so two
such overlaps could confirm a wrong hotspot (zhou, review of e6103daea).
The forget now runs under the lock the peer move records its sample
under, right before the move (`forget_sample_and_move`), so a peer move
lands entirely before the forget or entirely after the move. The drm-off
build keeps the line it had. The relative path of the peer already
forgets under the ENIGO guard of its move.
Test: a_move_of_this_process_forgets_the_sample_of_a_peer_move_it_waited_for
(the peer holds a stand-in lock when the move asks for it; nothing is
forgotten before the move has the lock; the peer records its sample and
releases it, and after the move the sample is gone). Mutations: the
forget taken before the lock (red in five runs of five) and no forget at
all, each turn it red. The call site in the thread, which passes ENIGO,
is covered by reading.
This commit is contained in:
@@ -2080,6 +2080,12 @@ jobs:
|
||||
cargo test --locked --lib --features drm drm_capturer_tests
|
||||
run_drm_tests "rustdesk ipc::test" \
|
||||
cargo test --locked --lib --features drm ipc::test
|
||||
run_drm_tests "rustdesk cursor_pos_tests" \
|
||||
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" \
|
||||
|
||||
@@ -36,6 +36,9 @@ pub struct CursorSnapshot {
|
||||
/// so the consumer must re-guess on the upright one.
|
||||
pub hot_measured: bool,
|
||||
pub colors: Vec<u8>,
|
||||
/// Cursor plane position (CRTC_X/CRTC_Y), in this reader's scanout pixels.
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
/// Fold everything that makes a cursor a DIFFERENT cursor into its id.
|
||||
@@ -639,6 +642,8 @@ impl DrmReader {
|
||||
hoty: 0,
|
||||
hot_measured: false,
|
||||
colors: vec![0, 0, 0, 0],
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
} else if !c.pixels.is_null()
|
||||
&& c.width > 0
|
||||
@@ -691,6 +696,8 @@ impl DrmReader {
|
||||
hoty,
|
||||
hot_measured,
|
||||
colors,
|
||||
x: c.x,
|
||||
y: c.y,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
+33
-1
@@ -540,7 +540,14 @@ pub enum Data {
|
||||
/// Service -> client: a frame header; the packed BGRA pixels follow via `send_raw()`.
|
||||
/// CPU-fallback path (no render node, or no transferable dma-buf): pixels cross the wire.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
DrmFrame { width: u32, height: u32 },
|
||||
DrmFrame {
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// See `DmabufDesc::cursor_pos`: the cursor plane position read right after this frame,
|
||||
/// or `None` when the cursor is hidden, the read failed or the producer predates the field.
|
||||
#[serde(default)]
|
||||
cursor_pos: Option<(i32, i32)>,
|
||||
},
|
||||
/// Service -> client: a zero-copy dma-buf frame descriptor. The scanout fd is NOT a field; when
|
||||
/// `desc.has_fd` it rides an SCM_RIGHTS ancillary message on the same `DrmConn::send_msg`, and
|
||||
/// there is NO trailing `send_raw()` body. The unprivileged `--server` imports the fd and does
|
||||
@@ -2369,4 +2376,29 @@ mod test {
|
||||
other => panic!("expected DrmCursor, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame header from a producer that predates the cursor plane position reads as `None`;
|
||||
/// a producer that sends one is read back exactly. A pin, not a gate: the consumer only ever
|
||||
/// measures against `Some`.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
#[test]
|
||||
fn a_drm_frame_without_a_cursor_position_reads_as_none() {
|
||||
let legacy = r#"{"t":"DrmFrame","c":{"width":8,"height":8}}"#;
|
||||
match serde_json::from_str::<Data>(legacy).expect("legacy DrmFrame must deserialize") {
|
||||
Data::DrmFrame {
|
||||
width,
|
||||
height,
|
||||
cursor_pos,
|
||||
} => {
|
||||
assert_eq!((width, height), (8, 8));
|
||||
assert_eq!(cursor_pos, None);
|
||||
}
|
||||
other => panic!("expected DrmFrame, got {other:?}"),
|
||||
}
|
||||
let modern = r#"{"t":"DrmFrame","c":{"width":8,"height":8,"cursor_pos":[3,4]}}"#;
|
||||
match serde_json::from_str::<Data>(modern).expect("modern DrmFrame must deserialize") {
|
||||
Data::DrmFrame { cursor_pos, .. } => assert_eq!(cursor_pos, Some((3, 4))),
|
||||
other => panic!("expected DrmFrame, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+155
-7
@@ -45,6 +45,13 @@ pub struct DmabufDesc {
|
||||
pub hdr_max_nits: u32,
|
||||
/// True: the fd rides this message's SCM_RIGHTS cmsg. False: import-once cache hit for `fb_id`.
|
||||
pub has_fd: bool,
|
||||
/// Cursor plane position read right after this frame was grabbed, in scanout pixels of the
|
||||
/// display this stream shows, so a few ms newer than the frame. `None` means the cursor is
|
||||
/// hidden, the read failed or was rejected, or the producer predates this field. It rides
|
||||
/// the frame instead of a stream of its own so it cannot queue and cannot backpressure the
|
||||
/// capture worker; the consumer only acts on a position that held still for several frames.
|
||||
#[serde(default)]
|
||||
pub cursor_pos: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
pub(crate) fn drm_ipc_path() -> String {
|
||||
@@ -100,6 +107,9 @@ enum DrmProducerMsg {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: Bytes,
|
||||
/// See `DmabufDesc::cursor_pos`. The dmabuf path carries it inside the descriptor; this
|
||||
/// one has no descriptor, so it travels beside the pixels.
|
||||
cursor_pos: Option<(i32, i32)>,
|
||||
},
|
||||
Cursor {
|
||||
id: u64,
|
||||
@@ -112,6 +122,105 @@ enum DrmProducerMsg {
|
||||
},
|
||||
}
|
||||
|
||||
/// Put this tick's cursor plane position on the frame it was read next to. The hidden sentinel
|
||||
/// carries no position; a shape message is left alone.
|
||||
fn stamp_cursor_pos(msg: &mut DrmProducerMsg, c: Option<&scrap::drm_reader::CursorSnapshot>) {
|
||||
let pos = c.and_then(|c| (c.id != scrap::drm_reader::HIDDEN_CURSOR_ID).then_some((c.x, c.y)));
|
||||
match msg {
|
||||
DrmProducerMsg::Frame { desc, .. } => desc.cursor_pos = pos,
|
||||
DrmProducerMsg::FrameCpu { cursor_pos, .. } => *cursor_pos = pos,
|
||||
DrmProducerMsg::Displays(_) | DrmProducerMsg::Cursor { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cursor_pos_tests {
|
||||
use super::*;
|
||||
|
||||
fn desc() -> DmabufDesc {
|
||||
DmabufDesc {
|
||||
buffer_id: 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
format: 0,
|
||||
modifier: 0,
|
||||
fb_id: 0,
|
||||
num_planes: 1,
|
||||
offsets: [0; 4],
|
||||
pitches: [16, 0, 0, 0],
|
||||
hdr_eotf: 0,
|
||||
hdr_max_nits: 0,
|
||||
has_fd: false,
|
||||
cursor_pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(id: u64) -> scrap::drm_reader::CursorSnapshot {
|
||||
scrap::drm_reader::CursorSnapshot {
|
||||
id,
|
||||
width: 2,
|
||||
height: 2,
|
||||
hotx: 0,
|
||||
hoty: 0,
|
||||
hot_measured: false,
|
||||
colors: vec![0; 16],
|
||||
x: 10,
|
||||
y: 20,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_plane_position_rides_both_frame_kinds_and_a_hidden_cursor_rides_as_none() {
|
||||
let visible = snapshot(7);
|
||||
let hidden = snapshot(scrap::drm_reader::HIDDEN_CURSOR_ID);
|
||||
|
||||
let mut frame = DrmProducerMsg::Frame { desc: desc(), fd: None };
|
||||
stamp_cursor_pos(&mut frame, Some(&visible));
|
||||
assert!(matches!(&frame, DrmProducerMsg::Frame { desc, .. } if desc.cursor_pos == Some((10, 20))));
|
||||
stamp_cursor_pos(&mut frame, Some(&hidden));
|
||||
assert!(matches!(&frame, DrmProducerMsg::Frame { desc, .. } if desc.cursor_pos.is_none()));
|
||||
|
||||
let mut cpu = DrmProducerMsg::FrameCpu {
|
||||
width: 4,
|
||||
height: 4,
|
||||
data: Bytes::new(),
|
||||
cursor_pos: None,
|
||||
};
|
||||
stamp_cursor_pos(&mut cpu, Some(&visible));
|
||||
assert!(matches!(&cpu, DrmProducerMsg::FrameCpu { cursor_pos: Some((10, 20)), .. }));
|
||||
stamp_cursor_pos(&mut cpu, None);
|
||||
assert!(matches!(&cpu, DrmProducerMsg::FrameCpu { cursor_pos: None, .. }));
|
||||
|
||||
let mut shape = DrmProducerMsg::Cursor {
|
||||
id: 7,
|
||||
width: 2,
|
||||
height: 2,
|
||||
hotx: 1,
|
||||
hoty: 1,
|
||||
hot_measured: false,
|
||||
colors: vec![0; 16],
|
||||
};
|
||||
stamp_cursor_pos(&mut shape, Some(&visible));
|
||||
assert!(matches!(&shape, DrmProducerMsg::Cursor { hotx: 1, hoty: 1, .. }));
|
||||
}
|
||||
|
||||
// A producer that predates the field: the descriptor arrives without it and reads as None.
|
||||
#[test]
|
||||
fn a_descriptor_without_a_cursor_position_reads_as_none() {
|
||||
let mut v = serde_json::to_value(desc()).unwrap();
|
||||
v.as_object_mut().unwrap().remove("cursor_pos").expect("the field is serialized");
|
||||
let d: DmabufDesc = serde_json::from_value(v).expect("a legacy descriptor must deserialize");
|
||||
assert_eq!(d.cursor_pos, None);
|
||||
let d: DmabufDesc = serde_json::from_value(serde_json::to_value(DmabufDesc {
|
||||
cursor_pos: Some((3, 4)),
|
||||
..desc()
|
||||
})
|
||||
.unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(d.cursor_pos, Some((3, 4)));
|
||||
}
|
||||
}
|
||||
|
||||
struct DrmStopGuard(std::sync::Arc<std::sync::atomic::AtomicBool>);
|
||||
impl Drop for DrmStopGuard {
|
||||
fn drop(&mut self) {
|
||||
@@ -960,8 +1069,17 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
cursor_pos,
|
||||
}) => {
|
||||
conn.send_msg(&Data::DrmFrame { width, height }, None).await?;
|
||||
conn.send_msg(
|
||||
&Data::DrmFrame {
|
||||
width,
|
||||
height,
|
||||
cursor_pos,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
conn.send_raw(data).await?;
|
||||
credit -= 1; // one frame in flight until the consumer acks it
|
||||
}
|
||||
@@ -1030,7 +1148,7 @@ fn drm_capture_worker(
|
||||
let mut stalled: u32 = 0;
|
||||
let mut logged_first = false;
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let grabbed: Option<std::io::Result<DrmProducerMsg>> = if frames_gated.load(Ordering::Relaxed)
|
||||
let mut grabbed: Option<std::io::Result<DrmProducerMsg>> = if frames_gated.load(Ordering::Relaxed)
|
||||
{
|
||||
// `stalled` is left untouched because the device is healthy -- the task bounds this
|
||||
// state itself (CREDIT_STALL) since our watchdog cannot advance.
|
||||
@@ -1051,6 +1169,7 @@ fn drm_capture_worker(
|
||||
hdr_eotf: d.hdr_eotf,
|
||||
hdr_max_nits: d.hdr_max_nits,
|
||||
has_fd: true, // every exported frame carries its fd; see the send below
|
||||
cursor_pos: None, // stamped below, from the one cursor read of this tick
|
||||
},
|
||||
fd: Some(fd),
|
||||
}),
|
||||
@@ -1062,10 +1181,22 @@ fn drm_capture_worker(
|
||||
width: w as u32,
|
||||
height: h as u32,
|
||||
data: Bytes::copy_from_slice(buf),
|
||||
cursor_pos: None,
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
})
|
||||
};
|
||||
// ONE cursor read per tick, and none on a stalled or failed grab: the plane position rides
|
||||
// the frame it was read next to, and the shape ships when it changes. The position is a
|
||||
// few ms newer than the frame; the consumer only measures a plane that held still for
|
||||
// several frames, so that skew never reaches a measurement.
|
||||
let cursor = match &grabbed {
|
||||
Some(Err(_)) => None,
|
||||
_ => reader.cursor(),
|
||||
};
|
||||
if let Some(Ok(msg)) = grabbed.as_mut() {
|
||||
stamp_cursor_pos(msg, cursor.as_ref());
|
||||
}
|
||||
match grabbed {
|
||||
None => {}
|
||||
Some(Ok(msg)) => {
|
||||
@@ -1108,7 +1239,7 @@ fn drm_capture_worker(
|
||||
}
|
||||
|
||||
// Ship the cursor shape only when it changes (id is a content hash or the hidden sentinel).
|
||||
if let Some(c) = reader.cursor() {
|
||||
if let Some(c) = cursor {
|
||||
if c.id != last_cursor_id {
|
||||
last_cursor_id = c.id;
|
||||
if frame_tx
|
||||
@@ -1538,7 +1669,14 @@ mod drm_conn_tests {
|
||||
let (a, b) = tokio::net::UnixStream::pair().unwrap();
|
||||
let mut tx = DrmConn::new(a);
|
||||
let mut rx = DrmConn::new(b);
|
||||
tx.send_msg(&Data::DrmFrame { width: 1920, height: 1080 }, None)
|
||||
tx.send_msg(
|
||||
&Data::DrmFrame {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
cursor_pos: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (data, fd) = rx.recv_msg().await.unwrap();
|
||||
@@ -1546,7 +1684,8 @@ mod drm_conn_tests {
|
||||
data,
|
||||
Data::DrmFrame {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
height: 1080,
|
||||
cursor_pos: None
|
||||
}
|
||||
));
|
||||
assert!(fd.is_none(), "no fd was sent, none must be reported");
|
||||
@@ -1558,7 +1697,14 @@ mod drm_conn_tests {
|
||||
let mut tx = DrmConn::new(a);
|
||||
let mut rx = DrmConn::new(b);
|
||||
let (rd, wr) = pipe();
|
||||
tx.send_msg(&Data::DrmFrame { width: 4, height: 4 }, Some(rd.as_fd()))
|
||||
tx.send_msg(
|
||||
&Data::DrmFrame {
|
||||
width: 4,
|
||||
height: 4,
|
||||
cursor_pos: None,
|
||||
},
|
||||
Some(rd.as_fd()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (_data, fd) = rx.recv_msg().await.unwrap();
|
||||
@@ -1672,6 +1818,7 @@ mod drm_conn_tests {
|
||||
let payload = serde_json::to_vec(&Data::DrmFrame {
|
||||
width: 8,
|
||||
height: 8,
|
||||
cursor_pos: None,
|
||||
})
|
||||
.unwrap();
|
||||
let prefix = (payload.len() as u32).to_be_bytes();
|
||||
@@ -1683,7 +1830,8 @@ mod drm_conn_tests {
|
||||
data,
|
||||
Data::DrmFrame {
|
||||
width: 8,
|
||||
height: 8
|
||||
height: 8,
|
||||
cursor_pos: None
|
||||
}
|
||||
));
|
||||
let kept = fd.expect("the first surplus fd must be kept");
|
||||
|
||||
@@ -136,11 +136,81 @@ impl WaylandLayout {
|
||||
#[cfg(target_os = "linux")]
|
||||
static WAYLAND_LAYOUT_DRIFTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// True while the layout has drifted from the session-init baseline AND the matching uinput
|
||||
/// range was applied, which is the condition `remap_wayland_uinput_coord` remaps under. While
|
||||
/// it holds, an injected point has been moved onto the live layout and no longer lives in the
|
||||
/// layout the DRM cursor calibration resolved its geometry from, so the calibration declines
|
||||
/// to measure. It is a secondary guard: the snapshot generation is what stops a measurement
|
||||
/// once the layout moved, and this covers the window in which the remap is active and the
|
||||
/// promotion that re-baselines is still owed. Both are written by the poll below, so a layout
|
||||
/// change is seen up to one check interval late; inside that window the bitmap bound in the
|
||||
/// measurement is the only guard, and a value that passes it is replaced by the next confirmed
|
||||
/// pair. A failed range apply stores false, and then nothing is remapped either.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
pub(crate) fn wayland_layout_drifted() -> bool {
|
||||
WAYLAND_LAYOUT_DRIFTED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
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"))]
|
||||
@@ -249,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!(
|
||||
@@ -269,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()
|
||||
@@ -289,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),
|
||||
@@ -849,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;
|
||||
|
||||
+1051
-4
File diff suppressed because it is too large
Load Diff
+141
-1
@@ -1077,6 +1077,77 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown<ControlKey>], en: &mut Enigo, ck: i3
|
||||
}
|
||||
}
|
||||
|
||||
/// The last ABSOLUTE peer-injected pointer position, in the post-remap uinput layout space, with
|
||||
/// when it was injected, a sequence number and the wayland snapshot generation it was mapped
|
||||
/// against. Deliberately NOT `LATEST_PEER_INPUT_CURSOR`: its relative arm stores
|
||||
/// `get_cursor_pos()`, an X-server coordinate that never went through the layout remap, and the
|
||||
/// 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, 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);
|
||||
|
||||
/// One absolute peer sample, as the calibration reads it.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct PeerAbsSample {
|
||||
pub pos: (i32, i32),
|
||||
pub age_ms: u64,
|
||||
/// Distinct per injected move. Two measurements taken against the same sample are not
|
||||
/// independent, whatever the cursor plane did in between.
|
||||
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. 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();
|
||||
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
|
||||
/// delta from the peer, or a move this process made on its own. The last sample no longer says
|
||||
/// where the pointer is; forget it, the next absolute move restores it.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
pub(crate) fn note_pointer_moved_without_absolute_sample() {
|
||||
*LATEST_PEER_ABS_POS.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
/// A move this process makes on its own. The forget runs once `lock` returned the guard the peer
|
||||
/// move records its sample under, so no peer move lands between the forget and this move and
|
||||
/// leaves its sample describing a pointer that has moved since.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
fn forget_sample_and_move<G>(lock: impl FnOnce() -> G, move_to: impl FnOnce(G)) {
|
||||
let guard = lock();
|
||||
note_pointer_moved_without_absolute_sample();
|
||||
move_to(guard);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
pub(crate) fn last_peer_abs_sample() -> Option<PeerAbsSample> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// Update time to avoid send cursor position event to the peer.
|
||||
// See `run_pos` --> `set_cursor_position` --> `exclude`
|
||||
#[inline]
|
||||
@@ -1249,7 +1320,9 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
// 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() {
|
||||
let uinput = wayland_use_uinput();
|
||||
#[cfg(target_os = "linux")]
|
||||
let (mx, my) = if uinput {
|
||||
super::display_service::remap_wayland_uinput_coord(evt.x, evt.y)
|
||||
} else {
|
||||
(evt.x, evt.y)
|
||||
@@ -1257,6 +1330,11 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let (mx, my) = (evt.x, evt.y);
|
||||
en.mouse_move_to(mx, my);
|
||||
// Only the uinput point is in the layout space the calibration measures in.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
if uinput {
|
||||
note_peer_absolute_move(mx, my);
|
||||
}
|
||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||
conn,
|
||||
time: get_time(),
|
||||
@@ -1279,6 +1357,10 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
let dy = evt
|
||||
.y
|
||||
.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
|
||||
// The absolute sample stops describing the pointer the moment a delta lands. Drop it
|
||||
// BEFORE the move, so no frame in between can measure the plane against it.
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
note_pointer_moved_without_absolute_sample();
|
||||
en.mouse_move_relative(dx, dy);
|
||||
// Get actual cursor position after relative movement for tracking
|
||||
if let Some((x, y)) = crate::get_cursor_pos() {
|
||||
@@ -2496,6 +2578,10 @@ impl TemporaryMouseMoveHandle {
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
log::debug!("TemporaryMouseMoveHandle thread started");
|
||||
for (x, y) in rx {
|
||||
// Not the peer's position: the calibration must not subtract from it.
|
||||
#[cfg(feature = "drm")]
|
||||
forget_sample_and_move(|| ENIGO.lock().unwrap(), |mut en| en.mouse_move_to(x, y));
|
||||
#[cfg(not(feature = "drm"))]
|
||||
ENIGO.lock().unwrap().mouse_move_to(x, y);
|
||||
}
|
||||
log::debug!("TemporaryMouseMoveHandle thread exiting");
|
||||
@@ -2724,3 +2810,57 @@ mod cursor_shape_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "linux", feature = "drm"))]
|
||||
mod peer_abs_sample_tests {
|
||||
use super::*;
|
||||
|
||||
// The two writers share one static and libtest runs tests concurrently.
|
||||
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn a_move_without_an_absolute_sample_forgets_the_last_one() {
|
||||
let _serial = SERIAL.lock().unwrap_or_else(|p| p.into_inner());
|
||||
note_peer_absolute_move(20, 30);
|
||||
let s = last_peer_abs_sample().expect("an absolute move leaves a sample");
|
||||
assert_eq!(s.pos, (20, 30));
|
||||
assert!(s.age_ms < 1_000);
|
||||
note_pointer_moved_without_absolute_sample();
|
||||
assert_eq!(last_peer_abs_sample(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_absolute_move_is_a_new_sample() {
|
||||
let _serial = SERIAL.lock().unwrap_or_else(|p| p.into_inner());
|
||||
note_peer_absolute_move(1, 1);
|
||||
let a = last_peer_abs_sample().unwrap().seq;
|
||||
note_peer_absolute_move(1, 1);
|
||||
let b = last_peer_abs_sample().unwrap().seq;
|
||||
assert!(b > a, "same point, distinct sample");
|
||||
note_pointer_moved_without_absolute_sample();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_of_this_process_forgets_the_sample_of_a_peer_move_it_waited_for() {
|
||||
let _serial = SERIAL.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let mouse = std::sync::Arc::new(std::sync::Mutex::new(()));
|
||||
note_peer_absolute_move(5, 5);
|
||||
// The peer holds the mouse lock when the move of this process starts.
|
||||
let peer = mouse.lock().unwrap();
|
||||
let (asked, asking) = std::sync::mpsc::channel();
|
||||
let m = mouse.clone();
|
||||
let mover = std::thread::spawn(move || {
|
||||
let lock = || {
|
||||
asked.send(()).unwrap();
|
||||
m.lock().unwrap()
|
||||
};
|
||||
forget_sample_and_move(lock, |_| {})
|
||||
});
|
||||
asking.recv().unwrap();
|
||||
assert!(last_peer_abs_sample().is_some(), "nothing forgotten before the lock");
|
||||
note_peer_absolute_move(50, 50);
|
||||
drop(peer);
|
||||
mover.join().unwrap();
|
||||
assert_eq!(last_peer_abs_sample(), None, "the move of this process came last");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user