Skip to main content

virtual_lcd_renderer/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::fs;
4use std::path::Path;
5
6use virtual_lcd_core::Framebuffer;
7use minifb::{Key, Scale, ScaleMode, Window, WindowOptions};
8use resvg::tiny_skia::{Pixmap, Transform};
9use resvg::usvg::{Options, Tree};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct ScreenRect {
13    pub x: usize,
14    pub y: usize,
15    pub width: usize,
16    pub height: usize,
17}
18
19impl ScreenRect {
20    pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
21        Self { x, y, width, height }
22    }
23}
24
25#[derive(Debug)]
26pub enum RendererError {
27    Window(minifb::Error),
28    Io(std::io::Error),
29    SvgParse(String),
30    SvgRender(String),
31}
32
33impl std::fmt::Display for RendererError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Window(error) => write!(f, "window error: {error}"),
37            Self::Io(error) => write!(f, "io error: {error}"),
38            Self::SvgParse(error) => write!(f, "svg parse error: {error}"),
39            Self::SvgRender(error) => write!(f, "svg render error: {error}"),
40        }
41    }
42}
43
44impl std::error::Error for RendererError {}
45
46impl From<minifb::Error> for RendererError {
47    fn from(value: minifb::Error) -> Self {
48        Self::Window(value)
49    }
50}
51
52impl From<std::io::Error> for RendererError {
53    fn from(value: std::io::Error) -> Self {
54        Self::Io(value)
55    }
56}
57
58pub type Result<T> = std::result::Result<T, RendererError>;
59
60#[derive(Debug)]
61pub struct SvgFrame {
62    width: usize,
63    height: usize,
64    base_buffer: Vec<u32>,
65    screen: ScreenRect,
66}
67
68impl SvgFrame {
69    pub fn load(path: impl AsRef<Path>, screen: ScreenRect) -> Result<Self> {
70        let data = fs::read(path)?;
71        let options = Options::default();
72        let tree =
73            Tree::from_data(&data, &options).map_err(|error| RendererError::SvgParse(error.to_string()))?;
74        let size = tree.size().to_int_size();
75        let mut pixmap = Pixmap::new(size.width(), size.height())
76            .ok_or_else(|| RendererError::SvgRender("unable to allocate svg pixmap".to_string()))?;
77        let mut pixmap_mut = pixmap.as_mut();
78        resvg::render(&tree, Transform::identity(), &mut pixmap_mut);
79
80        let width = size.width() as usize;
81        let height = size.height() as usize;
82        if screen.x + screen.width > width || screen.y + screen.height > height {
83            return Err(RendererError::SvgRender(
84                "screen rect exceeds rendered svg bounds".to_string(),
85            ));
86        }
87
88        let base_buffer = pixmap
89            .data()
90            .chunks_exact(4)
91            .map(|rgba| ((rgba[0] as u32) << 16) | ((rgba[1] as u32) << 8) | rgba[2] as u32)
92            .collect();
93
94        Ok(Self {
95            width,
96            height,
97            base_buffer,
98            screen,
99        })
100    }
101}
102
103#[derive(Debug)]
104pub struct WindowRenderer {
105    window: Window,
106    frame: SvgFrame,
107    buffer: Vec<u32>,
108}
109
110impl WindowRenderer {
111    pub fn new(title: &str, frame: SvgFrame) -> Result<Self> {
112        let mut window = Window::new(
113            title,
114            frame.width,
115            frame.height,
116            WindowOptions {
117                resize: false,
118                scale: Scale::X1,
119                scale_mode: ScaleMode::Center,
120                ..WindowOptions::default()
121            },
122        )?;
123        window.set_target_fps(60);
124
125        Ok(Self {
126            buffer: frame.base_buffer.clone(),
127            window,
128            frame,
129        })
130    }
131
132    pub fn is_open(&self) -> bool {
133        self.window.is_open() && !self.window.is_key_down(Key::Escape)
134    }
135
136    pub fn update(&mut self, lcd_frame: &Framebuffer) -> Result<()> {
137        self.buffer.clone_from(&self.frame.base_buffer);
138        composite_framebuffer(
139            &mut self.buffer,
140            self.frame.width,
141            self.frame.height,
142            lcd_frame,
143            self.frame.screen,
144        );
145        self.window
146            .update_with_buffer(&self.buffer, self.frame.width, self.frame.height)?;
147        Ok(())
148    }
149}
150
151fn composite_framebuffer(
152    output: &mut [u32],
153    output_width: usize,
154    output_height: usize,
155    framebuffer: &Framebuffer,
156    screen: ScreenRect,
157) {
158    let fit_width = screen.width as f32 / framebuffer.width() as f32;
159    let fit_height = screen.height as f32 / framebuffer.height() as f32;
160    let scale = fit_width.min(fit_height);
161
162    let draw_width = ((framebuffer.width() as f32 * scale).round() as usize).max(1);
163    let draw_height = ((framebuffer.height() as f32 * scale).round() as usize).max(1);
164    let offset_x = screen.x + (screen.width.saturating_sub(draw_width)) / 2;
165    let offset_y = screen.y + (screen.height.saturating_sub(draw_height)) / 2;
166
167    for y in 0..screen.height {
168        let row = (screen.y + y) * output_width;
169        for x in 0..screen.width {
170            output[row + screen.x + x] = 0x000000;
171        }
172    }
173
174    for y in 0..draw_height {
175        let src_y = ((y as f32 / draw_height as f32) * framebuffer.height() as f32).floor() as u16;
176        let dst_y = offset_y + y;
177        if dst_y >= output_height {
178            continue;
179        }
180
181        let row = dst_y * output_width;
182        for x in 0..draw_width {
183            let src_x =
184                ((x as f32 / draw_width as f32) * framebuffer.width() as f32).floor() as u16;
185            let dst_x = offset_x + x;
186            if dst_x >= output_width {
187                continue;
188            }
189
190            if let Some(pixel) = framebuffer.get_pixel(
191                src_x.min(framebuffer.width() - 1),
192                src_y.min(framebuffer.height() - 1),
193            ) {
194                output[row + dst_x] =
195                    ((pixel.r as u32) << 16) | ((pixel.g as u32) << 8) | pixel.b as u32;
196            }
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::{composite_framebuffer, ScreenRect};
204    use virtual_lcd_core::{Color, Framebuffer};
205
206    #[test]
207    fn composite_writes_inside_screen_rect() {
208        let mut output = vec![0x112233; 16 * 16];
209        let mut frame = Framebuffer::new(2, 2);
210        frame.set_pixel(0, 0, Color::RED).expect("pixel should be valid");
211        frame.set_pixel(1, 0, Color::GREEN).expect("pixel should be valid");
212        frame.set_pixel(0, 1, Color::BLUE).expect("pixel should be valid");
213        frame.set_pixel(1, 1, Color::WHITE).expect("pixel should be valid");
214
215        composite_framebuffer(&mut output, 16, 16, &frame, ScreenRect::new(4, 4, 8, 8));
216
217        assert_eq!(output[0], 0x112233);
218        assert_eq!(output[4 * 16 + 4], 0xFF0000);
219        assert_eq!(output[4 * 16 + 11], 0x00FF00);
220        assert_eq!(output[11 * 16 + 4], 0x0000FF);
221    }
222}