Skip to main content

rusty_esp_image_core/
source.rs

1//! The capture seam.
2//!
3//! An [`ImageSource`] writes one frame **into memory the caller owns** and
4//! returns a validated view of it. A DVP engine, a MIPI-CSI engine, a file on
5//! the host and the [`TestPattern`] below all fit the same two methods, so
6//! everything downstream (encoders, packetizers, the mesh) is written once.
7
8use rusty_esp_core::error::{Error, Result};
9use rusty_esp_core::frame::{Frame, Geometry, PixelFormat};
10use rusty_esp_core::time::Micros;
11
12/// Something that produces frames.
13pub trait ImageSource {
14    /// The geometry every frame from this source has right now.
15    fn geometry(&self) -> Geometry;
16
17    /// Capture one frame into `out`. The returned view borrows `out`; for a
18    /// JPEG source only the coded bytes are meaningful and `out` may be
19    /// larger than the frame.
20    fn grab<'b>(&mut self, out: &'b mut [u8]) -> Result<Frame<'b>>;
21}
22
23/// A deterministic synthetic source: SMPTE-style colour bars with a moving
24/// marker, in Gray8, RGB565 or RGB888. It exists so a pipeline can be
25/// exercised on the host, and so a firmware can prove its wiring before a
26/// sensor is attached. Frame `n` is identical on every platform.
27#[derive(Debug, Clone)]
28pub struct TestPattern {
29    geometry: Geometry,
30    frame_micros: u64,
31    sequence: u32,
32    timestamp: Micros,
33}
34
35impl TestPattern {
36    /// A pattern source at `fps` frames per second (the timestamps advance
37    /// by `1_000_000 / fps` microseconds per frame). Only uncompressed packed
38    /// formats are supported.
39    pub fn new(geometry: Geometry, fps: u32) -> Result<Self> {
40        if fps == 0 {
41            return Err(Error::InvalidGeometry);
42        }
43        match geometry.format {
44            PixelFormat::Gray8 | PixelFormat::Rgb565 | PixelFormat::Rgb888 => Ok(TestPattern {
45                geometry,
46                frame_micros: 1_000_000 / u64::from(fps),
47                sequence: 0,
48                timestamp: Micros::ZERO,
49            }),
50            _ => Err(Error::Unsupported),
51        }
52    }
53
54    /// The seven bars, as RGB888.
55    const BARS: [[u8; 3]; 7] = [
56        [0xC0, 0xC0, 0xC0], // white
57        [0xC0, 0xC0, 0x00], // yellow
58        [0x00, 0xC0, 0xC0], // cyan
59        [0x00, 0xC0, 0x00], // green
60        [0xC0, 0x00, 0xC0], // magenta
61        [0xC0, 0x00, 0x00], // red
62        [0x00, 0x00, 0xC0], // blue
63    ];
64
65    // `colour_at(x, y)` lived here and was called once per pixel. It has no
66    // caller now: it depended on `y` only through `y == marker_y`, so `grab`
67    // builds the two distinct rows it could ever return and copies them. Its
68    // two per-pixel 64-bit divisions -- `sequence % h` and `(x * 7) / w`, both
69    // libcalls on a 32-bit core -- left the frame with it.
70}
71
72/// One pixel of `colour` into `dst`, in the packed format `bpp` names.
73/// Lifted out of the pixel loop so the format is matched once per ROW kind
74/// rather than once per pixel.
75fn encode_pixel(colour: [u8; 3], bpp: usize, dst: &mut [u8; 3]) {
76    let [r, g, b] = colour;
77    match bpp {
78        // BT.601 luma, integer.
79        1 => dst[0] = ((77 * u32::from(r) + 150 * u32::from(g) + 29 * u32::from(b)) >> 8) as u8,
80        2 => dst[..2].copy_from_slice(&crate::ops::pack_rgb565(r, g, b).to_le_bytes()),
81        _ => *dst = colour,
82    }
83}
84
85impl ImageSource for TestPattern {
86    fn geometry(&self) -> Geometry {
87        self.geometry
88    }
89
90    fn grab<'b>(&mut self, out: &'b mut [u8]) -> Result<Frame<'b>> {
91        let needed = self.geometry.byte_len().ok_or(Error::Unsupported)?;
92        if out.len() < needed {
93            return Err(Error::BufferTooSmall { needed });
94        }
95        let (w, h) = (self.geometry.width, self.geometry.height);
96        // The format is a property of the SOURCE, not of a pixel, and the old
97        // shape re-matched it -- including the unreachable `Unsupported` arm
98        // that `new` has already rejected -- once per pixel.
99        let bpp = match self.geometry.format {
100            PixelFormat::Gray8 => 1usize,
101            PixelFormat::Rgb565 => 2,
102            PixelFormat::Rgb888 => 3,
103            _ => return Err(Error::Unsupported),
104        };
105        let row = w as usize * bpp;
106        if row != 0 && h != 0 {
107            // `colour_at` depends on `y` ONLY through `y == marker_y`, so the
108            // whole frame is TWO distinct rows: the bars, and a solid marker.
109            // The old shape rebuilt both from scratch for every pixel of every
110            // row -- and `colour_at` carried `sequence % h` and `(x * 7) / w`,
111            // two 64-bit divisions, which are LIBCALLS on a 32-bit core. Both
112            // are now hoisted out of the frame entirely.
113            let marker_y = (self.sequence % h) as usize;
114            let buf = &mut out[..row * h as usize];
115
116            // One pixel of the marker colour, then a row of it.
117            let mut solid = [0u8; 3];
118            encode_pixel([0xFF, 0xFF, 0xFF], bpp, &mut solid);
119            for px in buf[marker_y * row..(marker_y + 1) * row].chunks_exact_mut(bpp) {
120                px.copy_from_slice(&solid[..bpp]);
121            }
122
123            // One bars row, built where a bars row belongs, then copied. The
124            // bar index is `floor(x * 7 / w)` and `x` steps by one, so the
125            // REMAINDER walks instead of a division running per pixel.
126            let bars_at = if marker_y != 0 {
127                0
128            } else if h > 1 {
129                1
130            } else {
131                usize::MAX
132            };
133            if bars_at != usize::MAX {
134                let (mut bar, mut acc) = (0usize, 0u32);
135                for px in buf[bars_at * row..(bars_at + 1) * row].chunks_exact_mut(bpp) {
136                    let mut p = [0u8; 3];
137                    encode_pixel(Self::BARS[bar.min(6)], bpp, &mut p);
138                    px.copy_from_slice(&p[..bpp]);
139                    acc += 7;
140                    while acc >= w {
141                        acc -= w;
142                        bar += 1;
143                    }
144                }
145                for y in 0..h as usize {
146                    if y != marker_y && y != bars_at {
147                        buf.copy_within(bars_at * row..(bars_at + 1) * row, y * row);
148                    }
149                }
150            }
151        }
152        let frame = Frame::packed(self.geometry, self.timestamp, self.sequence, &out[..needed])?;
153        self.sequence = self.sequence.wrapping_add(1);
154        self.timestamp = self.timestamp.add_micros(self.frame_micros);
155        Ok(frame)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn pattern_is_deterministic_and_advances() {
165        let g = Geometry::new(14, 4, PixelFormat::Rgb888).unwrap();
166        let mut a = TestPattern::new(g, 10).unwrap();
167        let mut b = TestPattern::new(g, 10).unwrap();
168        let mut ba = [0u8; 14 * 4 * 3];
169        let mut bb = [0u8; 14 * 4 * 3];
170        let fa = a.grab(&mut ba).unwrap();
171        assert_eq!(fa.sequence, 0);
172        assert_eq!(fa.timestamp, Micros::ZERO);
173        let fb = b.grab(&mut bb).unwrap();
174        assert_eq!(fa.byte_len(), fb.byte_len());
175        assert_eq!(ba, bb);
176        // second frame: marker moved, timestamp advanced by 100 ms
177        let f2 = a.grab(&mut ba).unwrap();
178        assert_eq!(f2.sequence, 1);
179        assert_eq!(f2.timestamp.as_millis(), 100);
180        assert_ne!(ba, bb);
181        // frame 0 (still in `bb`): row 0 is the marker, row 1 starts with bar 0 (0xC0)
182        let row1 = 14 * 3;
183        assert_eq!(&bb[row1..row1 + 3], &[0xC0, 0xC0, 0xC0]);
184        assert_eq!(&bb[..3], &[0xFF, 0xFF, 0xFF], "marker row of frame 0");
185        // frame 1 (in `ba`): the marker moved to row 1
186        assert_eq!(&ba[row1..row1 + 3], &[0xFF, 0xFF, 0xFF]);
187    }
188
189    #[test]
190    fn rejects_unsupported_and_small_buffers() {
191        let g = Geometry::new(8, 8, PixelFormat::Jpeg).unwrap();
192        assert_eq!(TestPattern::new(g, 10).err(), Some(Error::Unsupported));
193        let g = Geometry::new(8, 8, PixelFormat::Gray8).unwrap();
194        assert_eq!(TestPattern::new(g, 0).err(), Some(Error::InvalidGeometry));
195        let mut p = TestPattern::new(g, 30).unwrap();
196        let mut small = [0u8; 10];
197        assert_eq!(
198            p.grab(&mut small).err(),
199            Some(Error::BufferTooSmall { needed: 64 })
200        );
201    }
202}