1use core::fmt::Debug;
2
3use embedded_graphics_core::{
4 Pixel,
5 draw_target::DrawTarget,
6 pixelcolor::Rgb565,
7 prelude::{OriginDimensions, Point},
8};
9
10use crate::{
11 DrawPrimitive,
12 command_buffer::{CommandBuffer, RenderCommand},
13 draw::draw_zbuffered,
14 error::{BudgetKind, RenderError},
15};
16
17pub struct FrameCtx<'a> {
18 pub zbuffer: &'a mut [u32],
19 pub width: usize,
20 pub height: usize,
21}
22
23impl<'a> FrameCtx<'a> {
24 pub fn validate(&self) -> Result<(), RenderError> {
25 let expected = self.width * self.height;
26 if self.zbuffer.len() != expected {
27 return Err(RenderError::OutOfBudget(BudgetKind::ZBufferLength {
28 expected,
29 got: self.zbuffer.len(),
30 }));
31 }
32 Ok(())
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct DirtyRegion {
38 pub x: usize,
39 pub y: usize,
40 pub width: usize,
41 pub height: usize,
42}
43
44impl DirtyRegion {
45 fn from_bounds(min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> Option<Self> {
46 if max_x < min_x || max_y < min_y {
47 return None;
48 }
49 Some(Self {
50 x: min_x as usize,
51 y: min_y as usize,
52 width: (max_x - min_x + 1) as usize,
53 height: (max_y - min_y + 1) as usize,
54 })
55 }
56}
57
58fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
59 match primitive {
60 DrawPrimitive::ColoredPoint(p, _) => (p.x, p.y, p.x, p.y),
61 DrawPrimitive::Line([a, b], _) => (a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)),
62 DrawPrimitive::ColoredTriangle(points, _)
63 | DrawPrimitive::ColoredTriangleWithDepth { points, .. }
64 | DrawPrimitive::GouraudTriangle { points, .. }
65 | DrawPrimitive::GouraudTriangleWithDepth { points, .. }
66 | DrawPrimitive::TexturedTriangle { points, .. }
67 | DrawPrimitive::TexturedTriangleWithDepth { points, .. } => {
68 let min_x = points.iter().map(|p| p.x).min().unwrap_or(0);
69 let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
70 let max_x = points.iter().map(|p| p.x).max().unwrap_or(0);
71 let max_y = points.iter().map(|p| p.y).max().unwrap_or(0);
72 (min_x, min_y, max_x, max_y)
73 }
74 }
75}
76
77fn clamp_bounds_to_frame(
78 min_x: i32,
79 min_y: i32,
80 max_x: i32,
81 max_y: i32,
82 width: usize,
83 height: usize,
84) -> Option<(i32, i32, i32, i32)> {
85 let w = width as i32;
86 let h = height as i32;
87 let clamped_min_x = min_x.clamp(0, w.saturating_sub(1));
88 let clamped_min_y = min_y.clamp(0, h.saturating_sub(1));
89 let clamped_max_x = max_x.clamp(0, w.saturating_sub(1));
90 let clamped_max_y = max_y.clamp(0, h.saturating_sub(1));
91 if clamped_max_x < clamped_min_x || clamped_max_y < clamped_min_y {
92 return None;
93 }
94 Some((clamped_min_x, clamped_min_y, clamped_max_x, clamped_max_y))
95}
96
97pub fn execute_commands<D, const MAX: usize>(
98 fb: &mut D,
99 frame: &mut FrameCtx<'_>,
100 cmd: &CommandBuffer<MAX>,
101) -> Result<(), RenderError>
102where
103 D: DrawTarget<Color = Rgb565> + OriginDimensions,
104 D::Error: Debug,
105{
106 let _ = execute_commands_with_dirty_region(fb, frame, cmd)?;
107 Ok(())
108}
109
110pub fn execute_commands_with_dirty_region<D, const MAX: usize>(
111 fb: &mut D,
112 frame: &mut FrameCtx<'_>,
113 cmd: &CommandBuffer<MAX>,
114) -> Result<Option<DirtyRegion>, RenderError>
115where
116 D: DrawTarget<Color = Rgb565> + OriginDimensions,
117 D::Error: Debug,
118{
119 frame.validate()?;
120 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
121
122 for c in cmd.iter() {
123 match c {
124 RenderCommand::ClearColor(color) => {
125 let w = frame.width as i32;
126 let h = frame.height as i32;
127 for y in 0..h {
128 for x in 0..w {
129 fb.draw_iter([Pixel(Point::new(x, y), *color)])
130 .map_err(|_| {
131 RenderError::InvalidInput("draw target rejected clear write")
132 })?;
133 }
134 }
135 }
136 RenderCommand::ClearDepth(value) => {
137 frame.zbuffer.fill(*value);
138 }
139 RenderCommand::Draw(primitive) => {
140 draw_zbuffered(primitive.clone(), fb, frame.zbuffer, frame.width);
141 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
142 if let Some((min_x, min_y, max_x, max_y)) =
143 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
144 {
145 dirty_bounds = Some(match dirty_bounds {
146 Some((cx0, cy0, cx1, cy1)) => (
147 cx0.min(min_x),
148 cy0.min(min_y),
149 cx1.max(max_x),
150 cy1.max(max_y),
151 ),
152 None => (min_x, min_y, max_x, max_y),
153 });
154 }
155 }
156 }
157 }
158
159 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
160 Ok(region)
161}
162
163pub fn execute_commands_tiled<D, const MAX: usize, const BIN_CAP: usize>(
164 fb: &mut D,
165 frame: &mut FrameCtx<'_>,
166 cmd: &CommandBuffer<MAX>,
167 tile: crate::tilebin::TileConfig,
168) -> Result<crate::tilebin::TileBinStats, RenderError>
169where
170 D: DrawTarget<Color = Rgb565> + OriginDimensions,
171 D::Error: Debug,
172{
173 frame.validate()?;
174 let (bins, stats) =
175 crate::tilebin::build_bins::<MAX, BIN_CAP>(cmd, frame.width, frame.height, tile)?;
176 let mut executed_draw = [false; MAX];
177
178 for command in cmd.iter() {
179 match command {
180 RenderCommand::ClearColor(color) => {
181 let w = frame.width as i32;
182 let h = frame.height as i32;
183 for y in 0..h {
184 for x in 0..w {
185 fb.draw_iter([Pixel(Point::new(x, y), *color)])
186 .map_err(|_| {
187 RenderError::InvalidInput("draw target rejected clear write")
188 })?;
189 }
190 }
191 }
192 RenderCommand::ClearDepth(value) => frame.zbuffer.fill(*value),
193 RenderCommand::Draw(_) => {}
194 }
195 }
196
197 for bin in bins.iter() {
198 for idx in bin.iter().copied() {
199 if idx >= MAX || executed_draw[idx] {
200 continue;
201 }
202 let Some(RenderCommand::Draw(primitive)) = cmd.get(idx) else {
203 continue;
204 };
205 draw_zbuffered(primitive.clone(), fb, frame.zbuffer, frame.width);
206 executed_draw[idx] = true;
207 }
208 }
209
210 Ok(stats)
211}