Skip to main content

firefly_runtime/
frame_buffer.rs

1use crate::color::{FromRGB, Rgb16};
2use alloc::boxed::Box;
3use core::convert::Infallible;
4use core::marker::PhantomData;
5use embedded_graphics::pixelcolor::Gray4;
6use embedded_graphics::prelude::*;
7use embedded_graphics::primitives::Rectangle;
8
9pub const WIDTH: usize = 240;
10pub const HEIGHT: usize = 160;
11/// Bits per pixel.
12const BPP: usize = 4;
13/// Pixels per byte.
14const PPB: usize = 8 / BPP;
15/// Bytes needed to store all pixels.
16const BUFFER_SIZE: usize = WIDTH * HEIGHT / PPB;
17
18// https://lospec.com/palette-list/sweetie-16
19// https://github.com/nesbox/TIC-80/wiki/Palette
20const DEFAULT_PALETTE: [Rgb16; 16] = [
21    Rgb16::from_rgb(0x1a, 0x1c, 0x2c), // #1a1c2c, black
22    Rgb16::from_rgb(0x5d, 0x27, 0x5d), // #5d275d, purple
23    Rgb16::from_rgb(0xb1, 0x3e, 0x53), // #b13e53, red
24    Rgb16::from_rgb(0xef, 0x7d, 0x57), // #ef7d57, orange
25    Rgb16::from_rgb(0xff, 0xcd, 0x75), // #ffcd75, yellow
26    Rgb16::from_rgb(0xa7, 0xf0, 0x70), // #a7f070, light green
27    Rgb16::from_rgb(0x38, 0xb7, 0x64), // #38b764, green
28    Rgb16::from_rgb(0x25, 0x71, 0x79), // #257179, dark green
29    Rgb16::from_rgb(0x29, 0x36, 0x6f), // #29366f, dark blue
30    Rgb16::from_rgb(0x3b, 0x5d, 0xc9), // #3b5dc9, blue
31    Rgb16::from_rgb(0x41, 0xa6, 0xf6), // #41a6f6, light blue
32    Rgb16::from_rgb(0x73, 0xef, 0xf7), // #73eff7, cyan
33    Rgb16::from_rgb(0xf4, 0xf4, 0xf4), // #f4f4f4, white
34    Rgb16::from_rgb(0x94, 0xb0, 0xc2), // #94b0c2, light gray
35    Rgb16::from_rgb(0x56, 0x6c, 0x86), // #566c86, gray
36    Rgb16::from_rgb(0x33, 0x3c, 0x57), // #333c57, dark gray
37];
38
39pub trait RenderFB {
40    type Error;
41    fn render_fb(&mut self, frame: &mut FrameBuffer) -> Result<(), Self::Error>;
42}
43
44pub struct FrameBuffer {
45    /// Tightly packed pixel data, 4 bits per pixel (2 pixels per byte).
46    pub(crate) data: Box<[u8; BUFFER_SIZE]>,
47    /// The color palette. Maps 16-color packed pixels to RGB colors.
48    pub(crate) palette: [Rgb16; 16],
49    pub(crate) dirty: bool,
50}
51
52impl FrameBuffer {
53    pub(crate) fn new() -> Self {
54        Self {
55            data: Box::new([0; BUFFER_SIZE]),
56            palette: DEFAULT_PALETTE,
57            dirty: false,
58        }
59    }
60
61    pub fn iter_pairs(&self) -> impl Iterator<Item = (Rgb16, Rgb16)> + use<'_> {
62        self.data.iter().map(|b| {
63            let right = self.palette[usize::from(b & 0xf)];
64            let left = self.palette[usize::from(b >> 4) & 0xf];
65            (right, left)
66        })
67    }
68
69    /// Optimized rendering of horizontal line.
70    ///
71    /// Must behave exactly like embedded-graphics with the same parameters
72    /// but damn faster.
73    pub(crate) fn draw_hline(&mut self, x1: i32, x2: i32, y: i32, w: u32, c: Gray4) {
74        let mut left = x1;
75        let mut right = x2;
76        let mut y = y - (w / 2) as i32;
77        if left > right {
78            (left, right) = (right, left);
79            if w.is_multiple_of(2) {
80                y += 1;
81            }
82        }
83        let area = Rectangle {
84            top_left: Point::new(left, y),
85            size: Size::new((right - left + 1) as u32, w),
86        };
87        _ = self.fill_solid(&area, c);
88    }
89
90    /// Optimized rendering of vertical line.
91    ///
92    /// Must behave exactly like embedded-graphics with the same parameters
93    /// but damn faster.
94    pub(crate) fn draw_vline(&mut self, x: i32, y1: i32, y2: i32, w: u32, c: Gray4) {
95        let mut top = y1;
96        let mut down = y2;
97        let mut x = x - (w / 2) as i32;
98        if top > down {
99            (down, top) = (top, down);
100            if w.is_multiple_of(2) {
101                x += 1;
102            }
103        }
104        let area = Rectangle {
105            top_left: Point::new(x, top),
106            size: Size::new(w, (down - top + 1) as u32),
107        };
108        _ = self.fill_solid(&area, c);
109    }
110
111    /// Render 1-pixel-wide vertical line.
112    ///
113    /// SAFETY: HEIGHT > bottom_y >= top_y.
114    fn draw_vline1(&mut self, x: usize, top_y: usize, bottom_y: usize, c: Gray4) {
115        let color = c.into_storage();
116        debug_assert!(color < 16);
117        let shift = if x.is_multiple_of(2) { 0 } else { 4 };
118        let mask = !(0b1111 << shift);
119        let start_i = (top_y * WIDTH + x) / PPB;
120        let end_i = (bottom_y * WIDTH + x) / PPB;
121        for pixel_index in (start_i..end_i).step_by(WIDTH / PPB) {
122            let byte_index = pixel_index;
123            // Safety: It's up to the caller to ensure that
124            // y within WIDTH and HEIGHT.
125            let byte = unsafe { self.data.get_unchecked_mut(byte_index) };
126            *byte = (color << shift) | (*byte & mask);
127        }
128    }
129}
130
131/// Required by the [DrawTarget] trait.
132impl OriginDimensions for FrameBuffer {
133    fn size(&self) -> Size {
134        Size::new(WIDTH as u32, HEIGHT as u32)
135    }
136}
137
138/// Allow drawing 16-color elements on the framebuffer.
139impl DrawTarget for FrameBuffer {
140    type Color = Gray4;
141    type Error = Infallible;
142
143    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
144    where
145        I: IntoIterator<Item = Pixel<Self::Color>>,
146    {
147        self.dirty = true;
148        for pixel in pixels {
149            let Pixel(point, color) = pixel;
150            self.set_pixel(point, color);
151        }
152        Ok(())
153    }
154
155    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
156        self.dirty = true;
157        let new_byte = color_to_byte(&color);
158        self.data.fill(new_byte);
159        Ok(())
160    }
161
162    fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
163        self.dirty = true;
164        let new_byte = color_to_byte(&color);
165
166        let left_x = area.top_left.x.clamp(0, WIDTH as _) as usize;
167        let right_x = area.top_left.x + area.size.width as i32;
168        let right_x = right_x.clamp(0, WIDTH as _) as usize;
169
170        let top_y = area.top_left.y.clamp(0, HEIGHT as _) as usize;
171        let bottom_y = area.top_left.y + area.size.height as i32;
172        let bottom_y = bottom_y.clamp(0, HEIGHT as _) as usize;
173
174        if right_x - left_x <= 4 {
175            for x in left_x..right_x {
176                self.draw_vline1(x, top_y, bottom_y, color);
177            }
178            return Ok(());
179        }
180
181        let left_fract = left_x % 2 == 1;
182        let right_fract = right_x % 2 == 1;
183        let start_x = left_x + usize::from(left_fract);
184        let end_x = right_x - usize::from(right_fract);
185        let width = end_x - start_x;
186        debug_assert_eq!(width % 2, 0);
187
188        for y in top_y..bottom_y {
189            let start_i = y * WIDTH / 2 + start_x / 2;
190            let end_i = start_i + width / 2;
191            // TODO: IT PANICS (end_i overflows)
192            self.data[start_i..end_i].fill(new_byte);
193        }
194
195        if left_fract {
196            self.draw_vline1(left_x, top_y, bottom_y, color);
197        }
198        if right_fract {
199            self.draw_vline1(right_x - 1, top_y, bottom_y, color);
200        }
201        Ok(())
202    }
203
204    // TODO(@orsinium): Optimize. Used by draw_qr.
205    fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
206    where
207        I: IntoIterator<Item = Self::Color>,
208    {
209        self.draw_iter(
210            area.points()
211                .zip(colors)
212                .map(|(pos, color)| Pixel(pos, color)),
213        )
214    }
215}
216
217impl FrameBuffer {
218    /// Draw the framebuffer on an RGB screen.
219    pub fn draw<D, C, E>(&mut self, target: &mut D) -> Result<(), E>
220    where
221        C: RgbColor + FromRGB,
222        D: DrawTarget<Color = C, Error = E>,
223    {
224        if !self.dirty {
225            return Ok(());
226        }
227        self.dirty = false;
228        let colors = ColorIter {
229            data: &self.data,
230            palette: &self.palette,
231            index: 0,
232            color: PhantomData,
233        };
234        let area = Rectangle::new(Point::zero(), Size::new(WIDTH as u32, HEIGHT as u32));
235        target.fill_contiguous(&area, colors)
236    }
237
238    /// Set color of a single pixel at the given coordinates.
239    ///
240    /// Does NOT mark the buffer as dirty. This must be done by the caller.
241    pub(crate) fn set_pixel(&mut self, point: Point, color: Gray4) {
242        // Negative values will be wrapped and filtered out
243        // because any wrapped value is bigger than WIDTH/HEIGHT.
244        let x = point.x as usize;
245        let y = point.y as usize;
246        if y >= HEIGHT || x >= WIDTH {
247            return; // the pixel is out of bounds
248        }
249        let pixel_index = y * WIDTH + x;
250        let byte_index = pixel_index / PPB;
251        let shift = if pixel_index.is_multiple_of(2) { 0 } else { 4 };
252        let mask = !(0b1111 << shift);
253        // Safety: if y within WIDTH and HEIGHT (which we checked),
254        // the byte_index is is within the buffer.
255        let byte = unsafe { self.data.get_unchecked_mut(byte_index) };
256        let color = color.luma();
257        debug_assert!(color < 16);
258        *byte = (color << shift) | (*byte & mask);
259    }
260}
261
262struct ColorIter<'a, C>
263where
264    C: RgbColor + FromRGB,
265{
266    data: &'a [u8; BUFFER_SIZE],
267    palette: &'a [Rgb16; 16],
268    index: usize,
269    color: PhantomData<C>,
270}
271
272impl<C> Iterator for ColorIter<'_, C>
273where
274    C: RgbColor + FromRGB,
275{
276    type Item = C;
277
278    fn next(&mut self) -> Option<Self::Item> {
279        let byte_index = self.index / PPB;
280        let byte = self.data.get(byte_index)?;
281        let shift = self.index % PPB;
282        let luma = (byte >> (shift * BPP)) & 0b1111;
283        debug_assert!(luma < 16);
284        self.index += 1;
285        let rgb16 = self.palette[luma as usize];
286        Some(C::from_rgb(rgb16))
287    }
288}
289
290/// Duplicate the color and pack into 1 byte.
291fn color_to_byte(c: &Gray4) -> u8 {
292    let luma = c.luma();
293    luma | (luma << 4)
294}