fix(drm): drop a frame equal to the previous one, as the other capturers do (#16450)

* fix(drm): drop a frame equal to the previous one, as the other capturers do

The producer grabs on a timer, so a still screen reached the encoder as the
same frame every tick and the client decoded about 30 frames per second of
nothing new (rustdesk#16449). The x11, pipewire and quartz capturers drop a
repeat with would_block_if_equal; the drm capturer now does the same.

A regression test checks that a repeat is not encoded again and that the next
new frame is. The frame helper of the tests now differs per call, and the 180
test forgets the frame its last case repeats on purpose.

* fix(drm): a frame whose encode failed is not dropped as a repeat

The capturer marked a frame as seen when it returned it, so when its encode
failed and the screen then stayed still, every later grab compared equal to
it and it was never sent: the client kept the previous image, and a hardware
encoder never reached its failure count to switch away.

handle_one_frame now also says whether the frame was encoded, and when it was
not the loop calls TraitCapturer::forget_last_frame(), a new method whose
default does nothing. The DRM capturer then returns the next repeat instead of
dropping it.

The DRM capturer also stops keeping a copy of the last frame: it compares the
new buffer with the one it returned last before replacing it, and a turned
frame is turned into a spare buffer first.

* fix(linux): x11 and pipewire do not drop a failed frame as a repeat either

Both capturers drop a frame equal to the one they returned last, so they had
the same hole as the drm one: a frame whose encode failed was not sent again
while the screen stayed still. They now forget it when the loop says so.

* fix(linux): every video run starts by forgetting the last frame

A pipewire recorder outlives a restarted run while another display keeps the
session open, and so did the frame it returned last. A run restarted by an
encoder switch or a new subscriber then dropped the first repeat and sent
nothing until the screen changed. The capturers of the other paths are built
fresh for each run, so for them the call does nothing.

* fix(drm): keep the repeat drop in the drm capturer, with one repeat a second

The shared part of the three commits before is reverted: TraitCapturer,
the video_service loop, x11 and pipewire are as on master again. So is the
restructure of frame() from the second commit: the drop is again the one
of the first commit, would_block_if_equal on the frame returned last, as
the other capturers do.

A repeat is dropped until a second has passed since frame() last returned
a frame, so a frame whose encode failed reaches the encoder again a second
later even if the screen stays still. The tests check the drop, the repeat
a second later, and that the second starts over after it.

* fix(drm): a frame is a repeat only when its format matches too

The same bytes as BGRA and as RGBA are two pictures when red and blue
differ, and the convert path can return either, so a change of format
clears the saved frame and the new one goes out. The 180 test no longer
checks a turned repeat: nothing reported asked for it.

* fix(drm): compare a frame with cur before replacing it, without a copy

saved_raw_data copied every changed frame (8 MB at 1080p). The previous
frame is still in cur when a new one arrives, so the bytes and the
format are compared with it before the swap; a rotated output turns the
frame into a spare buffer first. The 180 test checks the turned repeat
again, since that path compares on its own now.
This commit is contained in:
Mariano Abad
2026-10-12 11:29:57 +08:00
committed by GitHub
parent ccfa714d2e
commit 109c39bccb
+90 -6
View File
@@ -24,6 +24,9 @@ const HANDSHAKE_WAIT_MS: u64 = DRM_CONNECT_TIMEOUT_MS + DISPLAY_LIST_TIMEOUT_MS
/// Only the header read rechecks `stop`, so bound the body read here rather than relying on
/// `next_raw_into`'s own cap.
const BODY_READ_TIMEOUT: Duration = Duration::from_secs(5);
/// A repeat of the frame returned last goes out anyway once this much time has passed since
/// then: a frame whose encode failed reaches the encoder again, even if nothing moves.
const REPEAT_EVERY: Duration = Duration::from_secs(1);
struct FrameSlot {
// Row stride is `pixels.len() / height`, possibly padded; the format and the plane rotation
@@ -103,6 +106,10 @@ pub struct IpcDrmCapturer {
cur_h: usize,
cur_fmt: Pixfmt,
got_frame: bool,
// A rotated output turns each frame into here first, to compare it with `cur`.
spare: Vec<u8>,
// When frame() last returned a frame; see REPEAT_EVERY.
returned_at: Instant,
}
/// A list index is NOT an identity: `drm_enumerate_all_displays` concatenates per-card lists.
@@ -441,6 +448,8 @@ impl IpcDrmCapturer {
cur_h: 0,
cur_fmt: Pixfmt::BGRA,
got_frame: false,
spare: Vec::new(),
returned_at: Instant::now(),
},
displays,
wire_idx,
@@ -486,7 +495,7 @@ impl Drop for IpcDrmCapturer {
impl TraitCapturer for IpcDrmCapturer {
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
let deadline = Instant::now() + timeout;
{
let repeat = {
let mut slot = self.shared.slot.lock().unwrap();
loop {
if slot.latest.is_some() || slot.ended.is_some() {
@@ -541,9 +550,14 @@ impl TraitCapturer for IpcDrmCapturer {
),
));
}
if t == 0 {
// The producer grabs on a timer, so a still screen arrives as the same bytes every
// tick. `cur` still holds the previous frame; the same bytes in another format are
// another picture.
let repeat = if t == 0 {
let repeat = fmt == self.cur_fmt && buf == self.cur;
let previous = std::mem::replace(&mut self.cur, buf);
self.shared.slot.lock().unwrap().recycle(previous);
repeat
} else if !matches!(fmt, Pixfmt::BGRA | Pixfmt::RGBA) {
// Unreachable with today's producers (the convert path emits 4-byte pixels
// and the CPU path hardcodes BGRA); kept so a future non-4-byte producer
@@ -560,9 +574,12 @@ impl TraitCapturer for IpcDrmCapturer {
),
));
} else {
unrotate_bgra(&buf, w, h, t, &mut self.cur);
unrotate_bgra(&buf, w, h, t, &mut self.spare);
let repeat = fmt == self.cur_fmt && self.spare == self.cur;
std::mem::swap(&mut self.cur, &mut self.spare);
self.shared.slot.lock().unwrap().recycle(buf);
}
repeat
};
self.cur_w = fw;
self.cur_h = fh;
self.cur_fmt = fmt;
@@ -579,6 +596,7 @@ impl TraitCapturer for IpcDrmCapturer {
}
}
}
repeat
} else {
let err = slot
.ended
@@ -589,7 +607,11 @@ impl TraitCapturer for IpcDrmCapturer {
}
return Err(io::Error::new(io::ErrorKind::Other, err));
}
};
if repeat && self.returned_at.elapsed() < REPEAT_EVERY {
return Err(io::ErrorKind::WouldBlock.into());
}
self.returned_at = Instant::now();
Ok(Frame::PixelBuffer(PixelBuffer::new(
&self.cur,
self.cur_fmt,
@@ -2401,6 +2423,8 @@ mod drm_capturer_tests {
cur_h: 0,
cur_fmt: Pixfmt::BGRA,
got_frame: false,
spare: Vec::new(),
returned_at: Instant::now(),
}
}
@@ -2931,6 +2955,12 @@ mod drm_capturer_tests {
Ok(_) => panic!("expected a pixel-buffer frame"),
Err(err) => panic!("expected a delivered frame, got {err}"),
}
// The same scanout again: once turned it repeats the frame above, so it is dropped.
put_frame_with(&c, w, h, Some(0x1), &src);
assert!(matches!(
c.frame(Duration::from_millis(50)),
Err(e) if e.kind() == io::ErrorKind::WouldBlock
));
// Hardware rotation (i915 + mutter): the scanout is already upright and is left alone.
put_frame_with(&c, w, h, Some(0x4), &src);
match c.frame(Duration::from_millis(50)) {
@@ -2941,16 +2971,67 @@ mod drm_capturer_tests {
Err(err) => panic!("expected a delivered frame, got {err}"),
}
// A producer that cannot say (pre-0.5.8 library) keeps the old rule: 180 left alone.
put_frame_with(&c, w, h, None, &src);
// A new picture: `src` left alone is the frame above and would be dropped as a repeat.
let (other, _, _) = px_frame(&[&[7, 8, 9], &[10, 11, 12]], 0);
put_frame_with(&c, w, h, None, &other);
match c.frame(Duration::from_millis(50)) {
Ok(Frame::PixelBuffer(pb)) => {
assert_eq!(labels_of(pb.data(), w, h), vec![vec![1, 2, 3], vec![4, 5, 6]]);
assert_eq!(labels_of(pb.data(), w, h), vec![vec![7, 8, 9], vec![10, 11, 12]]);
}
Ok(_) => panic!("expected a pixel-buffer frame"),
Err(err) => panic!("expected a delivered frame, got {err}"),
}
}
#[test]
fn a_frame_equal_to_the_last_one_goes_out_once_a_second() {
let (still, w, h) = px_frame(&[&[1, 2, 3], &[4, 5, 6]], 0);
let (moved, _, _) = px_frame(&[&[1, 2, 3], &[4, 5, 7]], 0);
let mut c = capturer_with(Some((w, h)));
put_frame_with(&c, w, h, None, &still);
assert!(c.frame(Duration::from_millis(50)).is_ok());
put_frame_with(&c, w, h, None, &still);
assert!(matches!(
c.frame(Duration::from_millis(50)),
Err(e) if e.kind() == io::ErrorKind::WouldBlock
));
// A second later it goes out again, in case its encode failed, and the second starts over.
std::thread::sleep(REPEAT_EVERY);
put_frame_with(&c, w, h, None, &still);
assert!(c.frame(Duration::from_millis(50)).is_ok());
put_frame_with(&c, w, h, None, &still);
assert!(matches!(
c.frame(Duration::from_millis(50)),
Err(e) if e.kind() == io::ErrorKind::WouldBlock
));
put_frame_with(&c, w, h, None, &moved);
assert!(c.frame(Duration::from_millis(50)).is_ok());
}
#[test]
fn the_same_bytes_in_another_format_are_not_a_repeat() {
// Blue as BGRA, red as RGBA.
let px = [255u8, 0, 0, 255];
let put = |c: &IpcDrmCapturer, fmt| {
c.shared
.slot
.lock()
.unwrap()
.publish(1, 1, fmt, None, px.to_vec());
c.shared.cv.notify_one();
};
let mut c = capturer_with(Some((1, 1)));
put(&c, Pixfmt::BGRA);
assert!(c.frame(Duration::from_millis(50)).is_ok());
put(&c, Pixfmt::RGBA);
assert!(c.frame(Duration::from_millis(50)).is_ok());
put(&c, Pixfmt::RGBA);
assert!(matches!(
c.frame(Duration::from_millis(50)),
Err(e) if e.kind() == io::ErrorKind::WouldBlock
));
}
#[test]
fn a_plane_that_rotated_90_in_hardware_hands_over_a_portrait_scanout() {
use scrap::TraitPixelBuffer;
@@ -3062,9 +3143,12 @@ mod drm_capturer_tests {
}
fn put_frame_rot(c: &IpcDrmCapturer, w: usize, h: usize, plane_rotation: Option<u32>) {
// Each call differs in its first pixel: frame() drops a frame equal to the last one.
static SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let mut buf = c.shared.slot.lock().unwrap().take_free().unwrap_or_default();
buf.clear();
buf.resize(w * h * 4, 0);
buf[..4].copy_from_slice(&SEQ.fetch_add(1, Ordering::Relaxed).to_le_bytes());
let mut slot = c.shared.slot.lock().unwrap();
slot.publish(w, h, Pixfmt::BGRA, plane_rotation, buf);
c.shared.cv.notify_one();