Skip to main content

rusty_esp_image_core/
pool.rs

1//! A frame pool over one caller-owned buffer.
2//!
3//! A camera DMA engine fills one slot while the encoder reads another. On a
4//! chip the buffer is a static array (often in PSRAM); on the host it is a
5//! `Vec`. The pool never allocates and never hands out overlapping memory:
6//! slots are addressed by [`SlotId`], and the pool is borrowed for exactly as
7//! long as a slot's bytes are being touched.
8
9use rusty_esp_core::error::{Error, Result};
10use rusty_esp_core::frame::{Frame, Geometry};
11use rusty_esp_core::time::Micros;
12
13/// Index of a slot in a [`FramePool`].
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct SlotId(u8);
16
17impl SlotId {
18    /// The index.
19    #[must_use]
20    pub fn index(self) -> usize {
21        usize::from(self.0)
22    }
23}
24
25/// `N` equal slots carved from one buffer.
26#[derive(Debug)]
27pub struct FramePool<'m, const N: usize> {
28    buf: &'m mut [u8],
29    slot_len: usize,
30    in_use: [bool; N],
31    used: [usize; N],
32}
33
34impl<'m, const N: usize> FramePool<'m, N> {
35    /// Split `buf` into `N` slots. `N` must be 1..=255 and each slot at least
36    /// one byte; the trailing remainder of `buf` is unused.
37    pub fn new(buf: &'m mut [u8]) -> Result<Self> {
38        if N == 0 || N > 255 {
39            return Err(Error::Unsupported);
40        }
41        let slot_len = buf.len() / N;
42        if slot_len == 0 {
43            return Err(Error::BufferTooSmall { needed: N });
44        }
45        Ok(FramePool {
46            buf,
47            slot_len,
48            in_use: [false; N],
49            used: [0; N],
50        })
51    }
52
53    /// Bytes per slot.
54    #[must_use]
55    pub fn slot_len(&self) -> usize {
56        self.slot_len
57    }
58
59    /// Slots not currently acquired.
60    #[must_use]
61    pub fn free_slots(&self) -> usize {
62        self.in_use.iter().filter(|b| !**b).count()
63    }
64
65    /// Take a free slot, or `None` when all are in use (a dropped frame, by
66    /// policy of the caller — never a block).
67    pub fn acquire(&mut self) -> Option<SlotId> {
68        let i = self.in_use.iter().position(|b| !*b)?;
69        self.in_use[i] = true;
70        self.used[i] = 0;
71        Some(SlotId(i as u8))
72    }
73
74    /// Return a slot to the pool.
75    pub fn release(&mut self, id: SlotId) -> Result<()> {
76        let i = self.check(id)?;
77        self.in_use[i] = false;
78        self.used[i] = 0;
79        Ok(())
80    }
81
82    /// The whole slot, for filling.
83    pub fn slot_mut(&mut self, id: SlotId) -> Result<&mut [u8]> {
84        let i = self.check(id)?;
85        let start = i * self.slot_len;
86        Ok(&mut self.buf[start..start + self.slot_len])
87    }
88
89    /// Record how many bytes of the slot hold the frame (a JPEG is shorter
90    /// than its slot; a raw frame fills exactly its geometry).
91    pub fn commit(&mut self, id: SlotId, used: usize) -> Result<()> {
92        let i = self.check(id)?;
93        if used > self.slot_len {
94            return Err(Error::BufferTooSmall { needed: used });
95        }
96        self.used[i] = used;
97        Ok(())
98    }
99
100    /// The committed bytes of a slot.
101    pub fn slot(&self, id: SlotId) -> Result<&[u8]> {
102        let i = self.check(id)?;
103        let start = i * self.slot_len;
104        Ok(&self.buf[start..start + self.used[i]])
105    }
106
107    /// A validated frame view over the committed bytes of a slot.
108    pub fn frame(
109        &self,
110        id: SlotId,
111        geometry: Geometry,
112        timestamp: Micros,
113        sequence: u32,
114    ) -> Result<Frame<'_>> {
115        Frame::packed(geometry, timestamp, sequence, self.slot(id)?)
116    }
117
118    fn check(&self, id: SlotId) -> Result<usize> {
119        let i = id.index();
120        if i >= N || !self.in_use[i] {
121            return Err(Error::InvalidFormat);
122        }
123        Ok(i)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use rusty_esp_core::frame::PixelFormat;
131
132    #[test]
133    fn acquire_fill_commit_frame_release() {
134        let mut mem = [0u8; 64];
135        let mut pool: FramePool<'_, 2> = FramePool::new(&mut mem).unwrap();
136        assert_eq!(pool.slot_len(), 32);
137        assert_eq!(pool.free_slots(), 2);
138        let a = pool.acquire().unwrap();
139        let b = pool.acquire().unwrap();
140        assert!(pool.acquire().is_none(), "no blocking, no third slot");
141
142        // "DMA" writes a JPEG into slot a
143        let s = pool.slot_mut(a).unwrap();
144        s[..4].copy_from_slice(&[0xFF, 0xD8, 0xFF, 0xD9]);
145        pool.commit(a, 4).unwrap();
146        let g = Geometry::new(320, 240, PixelFormat::Jpeg).unwrap();
147        let f = pool.frame(a, g, Micros::from_millis(3), 7).unwrap();
148        assert_eq!(f.coded().map(<[u8]>::len), Some(4));
149        assert_eq!(f.sequence, 7);
150
151        // a raw frame that does not fit its geometry is refused
152        let g8 = Geometry::new(8, 8, PixelFormat::Gray8).unwrap();
153        pool.commit(b, 10).unwrap();
154        assert!(matches!(
155            pool.frame(b, g8, Micros::ZERO, 0),
156            Err(Error::BufferTooSmall { needed: 64 })
157        ));
158
159        pool.release(a).unwrap();
160        assert_eq!(pool.free_slots(), 1);
161        assert_eq!(pool.release(a), Err(Error::InvalidFormat), "double release");
162        assert_eq!(
163            pool.commit(b, 33),
164            Err(Error::BufferTooSmall { needed: 33 })
165        );
166    }
167
168    #[test]
169    fn construction_rules() {
170        let mut tiny = [0u8; 1];
171        assert!(matches!(
172            FramePool::<'_, 2>::new(&mut tiny),
173            Err(Error::BufferTooSmall { needed: 2 })
174        ));
175        let mut ok = [0u8; 3];
176        let pool: FramePool<'_, 2> = FramePool::new(&mut ok).unwrap();
177        assert_eq!(pool.slot_len(), 1);
178    }
179}