1use crate::{Color, Pixmap, Scene};
5use oxideav_raster::Renderer;
6use stipple_geometry::{PhysicalSize, Rect, ScaleFactor};
7
8#[derive(Debug, Default)]
11pub struct SoftwareRenderer {
12 background: Color,
13}
14
15impl SoftwareRenderer {
16 pub fn new() -> Self {
17 Self::default()
18 }
19
20 pub fn with_background(mut self, color: Color) -> Self {
22 self.background = color;
23 self
24 }
25
26 pub fn render(&self, scene: Scene, scale: ScaleFactor) -> Pixmap {
34 let physical = scale.to_physical(scene.logical_size());
35 self.render_at(scene, physical)
36 }
37
38 pub fn render_at(&self, scene: Scene, physical: PhysicalSize) -> Pixmap {
40 let frame = scene.into_vector_frame();
41 self.rasterize_frame(frame, physical)
42 }
43
44 pub fn render_region(&self, scene: Scene, view: Rect, physical: PhysicalSize) -> Pixmap {
53 let frame = scene.into_vector_frame_region(view);
54 self.rasterize_frame(frame, physical)
55 }
56
57 fn rasterize_frame(&self, frame: oxideav_core::VectorFrame, physical: PhysicalSize) -> Pixmap {
60 let (w, h) = (physical.width.max(1), physical.height.max(1));
61 let mut renderer = Renderer::new(w, h);
62 renderer.background = self.background.to_oxideav();
63 let video = renderer.render(&frame);
64
65 let plane = &video.planes[0];
68 let dst_stride = w as usize * 4;
69 let total = dst_stride * h as usize;
70 let mut data = vec![0u8; total];
71 if plane.stride == dst_stride {
72 data.copy_from_slice(&plane.data[..total]);
73 } else {
74 for y in 0..h as usize {
75 let src = &plane.data[y * plane.stride..y * plane.stride + dst_stride];
76 data[y * dst_stride..(y + 1) * dst_stride].copy_from_slice(src);
77 }
78 }
79 Pixmap::from_rgba8(PhysicalSize::new(w, h), data)
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use stipple_geometry::{Rect, Size};
87
88 #[test]
89 fn fills_pixel_at_expected_location() {
90 let mut scene = Scene::new(Size::new(100.0, 100.0));
93 scene.fill_rect(
94 Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
95 Color::rgb(255, 0, 0),
96 );
97 let pm = SoftwareRenderer::new().render(scene, ScaleFactor::IDENTITY);
98
99 assert_eq!(pm.size(), PhysicalSize::new(100, 100));
100 let [r, g, b, a] = pm.pixel(50, 50).unwrap();
101 assert_eq!((r, g, b, a), (255, 0, 0, 255));
102 }
103
104 #[test]
105 fn hidpi_scale_doubles_resolution() {
106 let scene = Scene::new(Size::new(100.0, 80.0));
107 let pm = SoftwareRenderer::new().render(scene, ScaleFactor::new(2.0));
108 assert_eq!(pm.size(), PhysicalSize::new(200, 160));
109 }
110
111 #[test]
112 fn render_region_matches_full_render_within_the_region() {
113 let make = || {
115 let mut s = Scene::new(Size::new(100.0, 100.0));
116 s.fill_rect(
117 Rect::from_xywh(10.0, 10.0, 20.0, 20.0),
118 Color::rgb(255, 0, 0),
119 );
120 s.fill_rect(
121 Rect::from_xywh(70.0, 70.0, 20.0, 20.0),
122 Color::rgb(0, 0, 255),
123 );
124 s
125 };
126 let r = SoftwareRenderer::new().with_background(Color::rgb(0, 0, 0));
127 let full = r.render(make(), ScaleFactor::IDENTITY);
128
129 let view = Rect::from_xywh(10.0, 10.0, 20.0, 20.0);
131 let region = r.render_region(make(), view, PhysicalSize::new(20, 20));
132 assert_eq!(region.size(), PhysicalSize::new(20, 20));
133
134 for y in 0..20u32 {
136 for x in 0..20u32 {
137 assert_eq!(
138 region.pixel(x, y),
139 full.pixel(10 + x, 10 + y),
140 "mismatch at region ({x},{y})"
141 );
142 }
143 }
144 assert_eq!(region.pixel(10, 10), Some([255, 0, 0, 255]));
146 }
147}