Skip to main content

embedded_3dgfx/
renderer.rs

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}