Skip to main content

oxstack_gfx/
framebuffer.rs

1use core::convert::Infallible;
2use core::ops::Range;
3
4use embedded_graphics_core::Pixel;
5use embedded_graphics_core::draw_target::DrawTarget;
6use embedded_graphics_core::geometry::{OriginDimensions, Point, Size};
7use embedded_graphics_core::pixelcolor::{Rgb565, raw::RawU16};
8use embedded_graphics_core::prelude::{Dimensions, IntoStorage};
9use embedded_graphics_core::primitives::Rectangle;
10
11/// A `W`×`H` RGB565 framebuffer in display wire format.
12///
13/// At 240×135 it is 64 800 bytes, so it should live in a `static`, never on
14/// the stack. [`Framebuffer::new`] is `const` and all-zero, which places the
15/// buffer in `.bss`: it costs RAM but **no flash**. A non-zero initial value
16/// would put 64 KB of initialiser data in the firmware image.
17///
18/// ```
19/// use oxstack_gfx::{Framebuffer, Rgb565};
20/// use embedded_graphics::prelude::*;
21///
22/// static mut FB: Framebuffer<240, 135> = Framebuffer::new();
23/// # let fb = unsafe { &mut *core::ptr::addr_of_mut!(FB) };
24/// fb.clear(Rgb565::BLUE).unwrap();
25/// let dirty = fb.take_dirty().unwrap();
26/// assert_eq!((dirty.y, dirty.rows), (0, 135));
27/// assert_eq!(dirty.bytes.len(), 240 * 135 * 2);
28/// ```
29pub struct Framebuffer<const W: usize, const H: usize> {
30    /// Pixels as big-endian RGB565 (`u16::to_be` of the colour's storage).
31    rows: [[u16; W]; H],
32    /// Changed rows as the half-open range `dirty_start..dirty_end`; empty
33    /// when `dirty_start >= dirty_end`. Zero-initialised means "clean", which
34    /// keeps `new()` all-zero.
35    dirty_start: u16,
36    dirty_end: u16,
37}
38
39/// A run of changed, full-width rows ready to be sent to the panel.
40#[derive(Debug)]
41pub struct DirtyRows<'a> {
42    /// First row.
43    pub y: u16,
44    /// Number of rows.
45    pub rows: u16,
46    /// `rows * W * 2` bytes of big-endian RGB565.
47    pub bytes: &'a [u8],
48}
49
50impl<const W: usize, const H: usize> Framebuffer<W, H> {
51    const ASSERT_FITS_U16: () =
52        assert!(W > 0 && H > 0 && W <= u16::MAX as usize && H <= u16::MAX as usize);
53
54    /// An all-black framebuffer with nothing marked dirty.
55    pub const fn new() -> Self {
56        let () = Self::ASSERT_FITS_U16;
57        Self {
58            rows: [[0; W]; H],
59            dirty_start: 0,
60            dirty_end: 0,
61        }
62    }
63
64    /// Width in pixels.
65    pub const fn width(&self) -> usize {
66        W
67    }
68
69    /// Height in pixels.
70    pub const fn height(&self) -> usize {
71        H
72    }
73
74    /// Colour at `(x, y)`, or `None` outside the buffer.
75    pub fn pixel(&self, x: usize, y: usize) -> Option<Rgb565> {
76        let raw = *self.rows.get(y)?.get(x)?;
77        Some(RawU16::new(u16::from_be(raw)).into())
78    }
79
80    /// Marks every row as changed, e.g. after the panel lost its contents.
81    pub fn mark_all_dirty(&mut self) {
82        self.mark_rows(0, H);
83    }
84
85    /// Hands out the changed rows and marks the buffer clean. The caller
86    /// must send them to the display before drawing again.
87    pub fn take_dirty(&mut self) -> Option<DirtyRows<'_>> {
88        let (start, end) = (self.dirty_start, self.dirty_end);
89        if start >= end {
90            return None;
91        }
92        self.dirty_start = 0;
93        self.dirty_end = 0;
94        let bytes = &self.as_bytes()[usize::from(start) * W * 2..usize::from(end) * W * 2];
95        Some(DirtyRows {
96            y: start,
97            rows: end - start,
98            bytes,
99        })
100    }
101
102    /// Mutable access to rows `rows` in wire format, for renderers that
103    /// write pixels one by one (a 3D rasteriser testing depth per pixel
104    /// cannot use `fill_solid`). Those rows are marked dirty, so a flush
105    /// cannot miss them. Store colours converted with [`wire_color`].
106    ///
107    /// # Panics
108    ///
109    /// If `rows` is out of bounds or decreasing, like slice indexing.
110    ///
111    /// ```
112    /// use oxstack_gfx::{Framebuffer, Rgb565, wire_color};
113    /// use embedded_graphics::prelude::*;
114    ///
115    /// let mut fb = Framebuffer::<8, 6>::new();
116    /// let red = wire_color(Rgb565::RED);
117    /// for row in fb.wire_rows_mut(2..4) {
118    ///     row[1..3].fill(red);
119    /// }
120    /// assert_eq!(fb.pixel(1, 3), Some(Rgb565::RED));
121    /// let dirty = fb.take_dirty().unwrap();
122    /// assert_eq!((dirty.y, dirty.rows), (2, 2));
123    /// ```
124    pub fn wire_rows_mut(&mut self, rows: Range<usize>) -> &mut [[u16; W]] {
125        // Bounds-check (and panic) before marking anything.
126        let _ = &self.rows[rows.clone()];
127        if !rows.is_empty() {
128            self.mark_rows(rows.start, rows.end);
129        }
130        &mut self.rows[rows]
131    }
132
133    /// The whole buffer in wire format.
134    pub fn as_bytes(&self) -> &[u8] {
135        let px = self.rows.as_flattened();
136        // SAFETY: `u16` has no padding or invalid bit patterns, `u8` has
137        // alignment 1, and the byte length is exactly `2 * px.len()`.
138        unsafe { core::slice::from_raw_parts(px.as_ptr().cast::<u8>(), px.len() * 2) }
139    }
140
141    #[inline]
142    fn mark_rows(&mut self, start: usize, end: usize) {
143        // Both fit in u16 per `ASSERT_FITS_U16`.
144        let (start, end) = (start as u16, end as u16);
145        if self.dirty_start >= self.dirty_end {
146            self.dirty_start = start;
147            self.dirty_end = end;
148        } else {
149            self.dirty_start = self.dirty_start.min(start);
150            self.dirty_end = self.dirty_end.max(end);
151        }
152    }
153
154    #[inline]
155    fn wire(color: Rgb565) -> u16 {
156        wire_color(color)
157    }
158}
159
160/// `color` in the framebuffer's wire format (big-endian RGB565), the value
161/// [`Framebuffer::wire_rows_mut`] rows hold.
162#[inline]
163pub fn wire_color(color: Rgb565) -> u16 {
164    color.into_storage().to_be()
165}
166
167impl<const W: usize, const H: usize> Default for Framebuffer<W, H> {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl<const W: usize, const H: usize> OriginDimensions for Framebuffer<W, H> {
174    fn size(&self) -> Size {
175        Size::new(W as u32, H as u32)
176    }
177}
178
179impl<const W: usize, const H: usize> DrawTarget for Framebuffer<W, H> {
180    type Color = Rgb565;
181    type Error = Infallible;
182
183    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Infallible>
184    where
185        I: IntoIterator<Item = Pixel<Rgb565>>,
186    {
187        let (mut lo, mut hi) = (usize::MAX, 0);
188        for Pixel(Point { x, y }, color) in pixels {
189            // Negative coordinates wrap to huge values and fail the bounds check.
190            let (x, y) = (x as usize, y as usize);
191            if x < W && y < H {
192                self.rows[y][x] = Self::wire(color);
193                lo = lo.min(y);
194                hi = hi.max(y + 1);
195            }
196        }
197        if lo < hi {
198            self.mark_rows(lo, hi);
199        }
200        Ok(())
201    }
202
203    fn fill_solid(&mut self, area: &Rectangle, color: Rgb565) -> Result<(), Infallible> {
204        let area = area.intersection(&self.bounding_box());
205        let Some(br) = area.bottom_right() else {
206            return Ok(());
207        };
208        let (x0, y0) = (area.top_left.x as usize, area.top_left.y as usize);
209        let (x1, y1) = (br.x as usize + 1, br.y as usize + 1);
210        let raw = Self::wire(color);
211        for row in &mut self.rows[y0..y1] {
212            row[x0..x1].fill(raw);
213        }
214        self.mark_rows(y0, y1);
215        Ok(())
216    }
217
218    fn clear(&mut self, color: Rgb565) -> Result<(), Infallible> {
219        self.rows.as_flattened_mut().fill(Self::wire(color));
220        self.mark_all_dirty();
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use embedded_graphics::prelude::*;
229    use embedded_graphics::primitives::{Line, PrimitiveStyle};
230
231    type Fb = Framebuffer<8, 6>;
232
233    #[test]
234    fn new_is_clean_and_black() {
235        let mut fb = Fb::new();
236        assert!(fb.take_dirty().is_none());
237        assert_eq!(fb.pixel(0, 0), Some(Rgb565::BLACK));
238        assert_eq!(fb.pixel(8, 0), None);
239    }
240
241    #[test]
242    fn pixels_are_stored_big_endian() {
243        let mut fb = Fb::new();
244        let c = Rgb565::new(0b11111, 0, 0b00001); // 0xF801
245        Pixel(Point::new(1, 0), c).draw(&mut fb).unwrap();
246        assert_eq!(&fb.as_bytes()[2..4], &[0xF8, 0x01]);
247        assert_eq!(fb.pixel(1, 0), Some(c));
248    }
249
250    #[test]
251    fn dirty_rows_track_changes() {
252        let mut fb = Fb::new();
253        Line::new(Point::new(0, 2), Point::new(7, 3))
254            .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1))
255            .draw(&mut fb)
256            .unwrap();
257        let d = fb.take_dirty().unwrap();
258        assert_eq!((d.y, d.rows, d.bytes.len()), (2, 2, 2 * 8 * 2), "span");
259        assert!(fb.take_dirty().is_none(), "take_dirty marks clean");
260
261        Pixel(Point::new(0, 1), Rgb565::RED).draw(&mut fb).unwrap();
262        Pixel(Point::new(0, 4), Rgb565::RED).draw(&mut fb).unwrap();
263        let d = fb.take_dirty().unwrap();
264        assert_eq!((d.y, d.rows), (1, 4), "separate draws merge");
265    }
266
267    #[test]
268    fn wire_rows_mut_marks_rows_dirty() {
269        let mut fb = Fb::new();
270        assert!(fb.wire_rows_mut(3..3).is_empty());
271        assert!(fb.take_dirty().is_none(), "empty range marks nothing");
272
273        fb.wire_rows_mut(4..5)[0][7] = wire_color(Rgb565::BLUE);
274        fb.wire_rows_mut(1..2);
275        assert_eq!(fb.pixel(7, 4), Some(Rgb565::BLUE));
276        let d = fb.take_dirty().unwrap();
277        assert_eq!((d.y, d.rows), (1, 4), "ranges merge");
278    }
279
280    #[test]
281    fn out_of_bounds_drawing_is_clipped() {
282        let mut fb = Fb::new();
283        Pixel(Point::new(-1, 0), Rgb565::RED).draw(&mut fb).unwrap();
284        Pixel(Point::new(0, 6), Rgb565::RED).draw(&mut fb).unwrap();
285        assert!(fb.take_dirty().is_none());
286
287        fb.fill_solid(
288            &Rectangle::new(Point::new(6, 4), Size::new(10, 10)),
289            Rgb565::GREEN,
290        )
291        .unwrap();
292        assert_eq!(fb.pixel(7, 5), Some(Rgb565::GREEN));
293        assert_eq!(fb.pixel(5, 5), Some(Rgb565::BLACK));
294        let d = fb.take_dirty().unwrap();
295        assert_eq!((d.y, d.rows), (4, 2));
296
297        fb.fill_solid(
298            &Rectangle::new(Point::new(20, 20), Size::new(2, 2)),
299            Rgb565::GREEN,
300        )
301        .unwrap();
302        assert!(fb.take_dirty().is_none());
303    }
304}