Skip to main content

android/emulator/
frame.rs

1//! Raw frames from the controller and change detection over them.
2//!
3//! The controller streams a frame whenever the guest posts one, changed or not,
4//! and the status bar clock changes on its own (spike S7). A frame is therefore
5//! reduced to a key, its dimensions plus an xxh3 hash of its pixels without the
6//! status bar rows, and a change is a key that differs from the previous one.
7
8use std::fmt;
9
10use prost::bytes::Bytes;
11use xxhash_rust::xxh3::xxh3_64;
12
13/// Bytes per pixel of the RGB888 frames the client requests.
14pub const BYTES_PER_PIXEL: usize = 3;
15
16/// A raw RGB888 frame.
17///
18/// Row 0 is the top of the screen. The proto documents raw rows as bottom-up,
19/// but the emulator sends them top-down: S7 matched a raw frame read top-down
20/// against the PNG of the same screen, and tap locations only lined up that way.
21#[derive(Clone, PartialEq, Eq)]
22pub struct Frame {
23    width: u32,
24    height: u32,
25    pixels: Bytes,
26    seq: u32,
27    timestamp_us: u64,
28}
29
30/// Why bytes do not make a frame.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FrameError {
33    /// Width or height is zero: the display is off or has not posted yet.
34    Empty,
35    /// The buffer is not `width * height * 3` bytes long.
36    SizeMismatch { expected: usize, actual: usize },
37}
38
39impl fmt::Display for FrameError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            FrameError::Empty => write!(f, "the frame has no pixels"),
43            FrameError::SizeMismatch { expected, actual } => write!(
44                f,
45                "an RGB888 frame of these dimensions has {expected} bytes, got {actual}"
46            ),
47        }
48    }
49}
50
51impl std::error::Error for FrameError {}
52
53impl Frame {
54    /// A frame of `pixels` in RGB888, rows top-down.
55    pub fn new(width: u32, height: u32, pixels: Bytes) -> Result<Self, FrameError> {
56        if width == 0 || height == 0 {
57            return Err(FrameError::Empty);
58        }
59        let expected = width as usize * height as usize * BYTES_PER_PIXEL;
60        if pixels.len() != expected {
61            return Err(FrameError::SizeMismatch {
62                expected,
63                actual: pixels.len(),
64            });
65        }
66        Ok(Self {
67            width,
68            height,
69            pixels,
70            seq: 0,
71            timestamp_us: 0,
72        })
73    }
74
75    /// Sets the stream sequence number and the emulator's production time.
76    pub fn with_timing(self, seq: u32, timestamp_us: u64) -> Self {
77        Self {
78            seq,
79            timestamp_us,
80            ..self
81        }
82    }
83
84    pub fn width(&self) -> u32 {
85        self.width
86    }
87
88    pub fn height(&self) -> u32 {
89        self.height
90    }
91
92    /// RGB888 pixels, `width * 3` bytes per row, top row first.
93    pub fn pixels(&self) -> &[u8] {
94        &self.pixels
95    }
96
97    /// Row `y`, counted from the top of the screen.
98    ///
99    /// # Panics
100    ///
101    /// When `y` is not below the height.
102    pub fn row(&self, y: u32) -> &[u8] {
103        assert!(
104            y < self.height,
105            "row {y} of a frame {} rows high",
106            self.height
107        );
108        let stride = self.width as usize * BYTES_PER_PIXEL;
109        &self.pixels[y as usize * stride..(y as usize + 1) * stride]
110    }
111
112    /// The emulator's sequence number in its stream; gaps are frames it skipped.
113    pub fn seq(&self) -> u32 {
114        self.seq
115    }
116
117    /// When the emulator estimates it produced the frame, in microseconds since
118    /// the Unix epoch; before the frame was copied, scaled and sent.
119    pub fn timestamp_us(&self) -> u64 {
120        self.timestamp_us
121    }
122}
123
124impl fmt::Debug for Frame {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.debug_struct("Frame")
127            .field("width", &self.width)
128            .field("height", &self.height)
129            .field("bytes", &self.pixels.len())
130            .field("seq", &self.seq)
131            .field("timestamp_us", &self.timestamp_us)
132            .finish()
133    }
134}
135
136/// Identity of a frame's content outside the status bar.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub struct FrameKey {
139    pub width: u32,
140    pub height: u32,
141    pub hash: u64,
142}
143
144/// Hashes frame content without the status bar.
145#[derive(Debug, Clone, Copy, PartialEq)]
146pub struct FrameHasher {
147    status_bar_fraction: f32,
148}
149
150impl FrameHasher {
151    /// The share of the screen height taken for the status bar when nothing
152    /// better is known: 6 %, 144 of 2400 rows, which S7 found covers the Pixel 8
153    /// profile's status bar.
154    pub const DEFAULT_STATUS_BAR_FRACTION: f32 = 0.06;
155
156    /// A hasher that leaves out the top `status_bar_fraction` of every frame,
157    /// clamped to 0 through 1; NaN leaves nothing out.
158    pub fn new(status_bar_fraction: f32) -> Self {
159        let fraction = if status_bar_fraction.is_nan() {
160            0.0
161        } else {
162            status_bar_fraction.clamp(0.0, 1.0)
163        };
164        Self {
165            status_bar_fraction: fraction,
166        }
167    }
168
169    /// Rows at the top of a frame `height` rows high that the hash leaves out.
170    pub fn status_bar_rows(&self, height: u32) -> u32 {
171        ((height as f32 * self.status_bar_fraction).round() as u32).min(height)
172    }
173
174    /// The key of `frame`.
175    pub fn key(&self, frame: &Frame) -> FrameKey {
176        let stride = frame.width as usize * BYTES_PER_PIXEL;
177        let skipped = self.status_bar_rows(frame.height) as usize * stride;
178        FrameKey {
179            width: frame.width,
180            height: frame.height,
181            hash: xxh3_64(&frame.pixels[skipped..]),
182        }
183    }
184}
185
186impl Default for FrameHasher {
187    fn default() -> Self {
188        Self::new(Self::DEFAULT_STATUS_BAR_FRACTION)
189    }
190}
191
192/// What a frame means relative to the one before it.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Observation {
195    /// The first frame since the detector started or was reset.
196    First,
197    /// Same content as the previous frame outside the status bar.
198    Unchanged,
199    /// Different content, or different dimensions, from the previous frame.
200    Changed,
201}
202
203/// Detects content changes in a sequence of frames, such as a frame stream.
204/// Feeds the settle signal: frames keep arriving while nothing changes.
205#[derive(Debug, Clone)]
206pub struct ChangeDetector {
207    hasher: FrameHasher,
208    last: Option<FrameKey>,
209}
210
211impl ChangeDetector {
212    pub fn new(hasher: FrameHasher) -> Self {
213        Self { hasher, last: None }
214    }
215
216    /// Classifies `frame` against the previous frame and remembers it.
217    pub fn observe(&mut self, frame: &Frame) -> Observation {
218        let key = self.hasher.key(frame);
219        match self.last.replace(key) {
220            None => Observation::First,
221            Some(last) if last == key => Observation::Unchanged,
222            Some(_) => Observation::Changed,
223        }
224    }
225
226    /// Forgets the previous frame, so the next one is [`Observation::First`].
227    pub fn reset(&mut self) {
228        self.last = None;
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    const W: u32 = 4;
237    const H: u32 = 50;
238
239    fn pixels(fill: u8) -> Vec<u8> {
240        vec![fill; W as usize * H as usize * BYTES_PER_PIXEL]
241    }
242
243    fn frame(data: Vec<u8>) -> Frame {
244        Frame::new(W, H, Bytes::from(data)).unwrap()
245    }
246
247    /// Changes one pixel on screen row `row`, which is buffer row `row`.
248    fn poke(data: &mut [u8], row: u32, col: u32) {
249        data[(row * W + col) as usize * BYTES_PER_PIXEL] ^= 0xff;
250    }
251
252    #[test]
253    fn a_frame_must_hold_width_times_height_rgb_pixels() {
254        assert_eq!(
255            Frame::new(0, 0, Bytes::new()).unwrap_err(),
256            FrameError::Empty
257        );
258        assert_eq!(
259            Frame::new(W, 0, Bytes::new()).unwrap_err(),
260            FrameError::Empty
261        );
262        let short = pixels(1)[1..].to_vec();
263        assert_eq!(
264            Frame::new(W, H, Bytes::from(short)).unwrap_err(),
265            FrameError::SizeMismatch {
266                expected: 600,
267                actual: 599
268            }
269        );
270        // An RGBA buffer is not an RGB888 frame of the same size.
271        let rgba = vec![0u8; W as usize * H as usize * 4];
272        assert!(matches!(
273            Frame::new(W, H, Bytes::from(rgba)),
274            Err(FrameError::SizeMismatch { .. })
275        ));
276        let f = frame(pixels(1)).with_timing(7, 1_000);
277        assert_eq!((f.width(), f.height(), f.pixels().len()), (W, H, 600));
278        assert_eq!((f.seq(), f.timestamp_us()), (7, 1_000));
279    }
280
281    #[test]
282    fn rows_are_top_down() {
283        let mut data = pixels(0);
284        data[..W as usize * BYTES_PER_PIXEL].fill(9);
285        let f = frame(data);
286        assert!(
287            f.row(0).iter().all(|&b| b == 9),
288            "row 0 is the first buffer row"
289        );
290        assert!(f.row(H - 1).iter().all(|&b| b == 0));
291        assert_eq!(f.row(1).len(), W as usize * BYTES_PER_PIXEL);
292    }
293
294    #[test]
295    fn the_status_bar_share_becomes_whole_rows() {
296        let hasher = FrameHasher::default();
297        assert_eq!(hasher.status_bar_rows(2400), 144);
298        assert_eq!(hasher.status_bar_rows(1200), 72);
299        assert_eq!(FrameHasher::new(0.0).status_bar_rows(1200), 0);
300        assert_eq!(FrameHasher::new(2.0).status_bar_rows(1200), 1200);
301        assert_eq!(FrameHasher::new(-1.0).status_bar_rows(1200), 0);
302        assert_eq!(FrameHasher::new(f32::NAN).status_bar_rows(1200), 0);
303    }
304
305    #[test]
306    fn the_first_frame_then_identical_content_is_unchanged() {
307        let mut d = ChangeDetector::new(FrameHasher::new(0.0));
308        assert_eq!(d.observe(&frame(pixels(10))), Observation::First);
309        assert_eq!(d.observe(&frame(pixels(10))), Observation::Unchanged);
310    }
311
312    #[test]
313    fn one_changed_pixel_is_a_change_and_becomes_the_new_reference() {
314        let a = pixels(10);
315        let mut b = a.clone();
316        poke(&mut b, 30, 3);
317        let mut d = ChangeDetector::new(FrameHasher::new(0.0));
318        d.observe(&frame(a.clone()));
319        assert_eq!(d.observe(&frame(b.clone())), Observation::Changed);
320        assert_eq!(d.observe(&frame(b)), Observation::Unchanged);
321        assert_eq!(d.observe(&frame(a)), Observation::Changed);
322    }
323
324    #[test]
325    fn a_change_in_the_status_bar_is_unchanged() {
326        // 0.1 of 50 rows: screen rows 0 to 4 are the status bar.
327        let base = pixels(10);
328        let mut clock = base.clone();
329        poke(&mut clock, 4, 0);
330        let mut content = base.clone();
331        poke(&mut content, 5, 0);
332        let mut d = ChangeDetector::new(FrameHasher::new(0.1));
333        d.observe(&frame(base));
334        assert_eq!(d.observe(&frame(clock)), Observation::Unchanged);
335        assert_eq!(d.observe(&frame(content)), Observation::Changed);
336    }
337
338    #[test]
339    fn the_status_bar_is_at_the_start_of_the_buffer() {
340        // A reader that took raw rows as bottom-up would leave out the last
341        // rows instead and miss this change at the bottom of the screen.
342        let base = pixels(10);
343        let mut bottom = base.clone();
344        poke(&mut bottom, H - 1, 2);
345        let mut d = ChangeDetector::new(FrameHasher::new(0.1));
346        d.observe(&frame(base));
347        assert_eq!(d.observe(&frame(bottom)), Observation::Changed);
348    }
349
350    #[test]
351    fn a_dimension_change_is_a_change() {
352        let mut d = ChangeDetector::new(FrameHasher::default());
353        d.observe(&frame(pixels(10)));
354        let rotated = Frame::new(H, W, Bytes::from(pixels(10))).unwrap();
355        assert_eq!(d.observe(&rotated), Observation::Changed);
356    }
357
358    #[test]
359    fn reset_starts_over() {
360        let mut d = ChangeDetector::new(FrameHasher::default());
361        d.observe(&frame(pixels(10)));
362        d.reset();
363        assert_eq!(d.observe(&frame(pixels(10))), Observation::First);
364    }
365
366    #[test]
367    fn keys_carry_the_dimensions() {
368        let key = FrameHasher::default().key(&frame(pixels(3)));
369        assert_eq!((key.width, key.height), (W, H));
370        assert_eq!(key, FrameHasher::default().key(&frame(pixels(3))));
371        assert_ne!(key.hash, FrameHasher::default().key(&frame(pixels(4))).hash);
372    }
373}