diff --git a/src/server/drm_capturer.rs b/src/server/drm_capturer.rs index 09446a036..656efa80f 100644 --- a/src/server/drm_capturer.rs +++ b/src/server/drm_capturer.rs @@ -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, + // 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> { 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) { + // 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();