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}