Skip to main content

embedded_3dgfx/
tilebin.rs

1use crate::DrawPrimitive;
2use crate::command_buffer::{CommandBuffer, RenderCommand};
3use crate::error::RenderError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct TileConfig {
7    pub tile_width: usize,
8    pub tile_height: usize,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct TileGrid {
13    pub cols: usize,
14    pub rows: usize,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct TileBinStats {
19    pub draw_commands: usize,
20    pub bins_used: usize,
21}
22
23fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
24    match primitive {
25        DrawPrimitive::ColoredPoint(p, _) => (p.x, p.y, p.x, p.y),
26        DrawPrimitive::Line([a, b], _) => (a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)),
27        DrawPrimitive::ColoredTriangle(points, _)
28        | DrawPrimitive::ColoredTriangleWithDepth { points, .. }
29        | DrawPrimitive::TranslucentTriangleWithDepth { points, .. }
30        | DrawPrimitive::GouraudTriangle { points, .. }
31        | DrawPrimitive::GouraudTriangleWithDepth { points, .. }
32        | DrawPrimitive::TexturedTriangle { points, .. }
33        | DrawPrimitive::TexturedTriangleWithDepth { points, .. }
34        | DrawPrimitive::TexturedGouraudTriangleWithDepth { points, .. }
35        | DrawPrimitive::LightmappedTriangle { points, .. } => {
36            let min_x = points.iter().map(|p| p.x).min().unwrap_or(0);
37            let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
38            let max_x = points.iter().map(|p| p.x).max().unwrap_or(0);
39            let max_y = points.iter().map(|p| p.y).max().unwrap_or(0);
40            (min_x, min_y, max_x, max_y)
41        }
42    }
43}
44
45pub fn tile_grid(width: usize, height: usize, config: TileConfig) -> Result<TileGrid, RenderError> {
46    if config.tile_width == 0 || config.tile_height == 0 {
47        return Err(RenderError::InvalidInput("tile dimensions must be >= 1"));
48    }
49    let cols = width.div_ceil(config.tile_width);
50    let rows = height.div_ceil(config.tile_height);
51    Ok(TileGrid { cols, rows })
52}
53
54pub fn build_bins<const MAX: usize, const BIN_CAP: usize>(
55    commands: &CommandBuffer<MAX>,
56    width: usize,
57    height: usize,
58    config: TileConfig,
59) -> Result<
60    (
61        heapless::Vec<heapless::Vec<usize, BIN_CAP>, BIN_CAP>,
62        TileBinStats,
63    ),
64    RenderError,
65> {
66    let grid = tile_grid(width, height, config)?;
67    let bin_count = grid.cols * grid.rows;
68    if bin_count > BIN_CAP {
69        return Err(RenderError::InvalidInput("tile bin count exceeds BIN_CAP"));
70    }
71
72    let mut bins: heapless::Vec<heapless::Vec<usize, BIN_CAP>, BIN_CAP> = heapless::Vec::new();
73    for _ in 0..bin_count {
74        bins.push(heapless::Vec::new())
75            .map_err(|_| RenderError::InvalidInput("unable to allocate tile bins"))?;
76    }
77
78    let mut draw_commands = 0usize;
79    for (idx, command) in commands.iter().enumerate() {
80        let RenderCommand::Draw(primitive) = command else {
81            continue;
82        };
83        draw_commands += 1;
84        let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
85        let clamp =
86            |v: i32, max_v: usize| -> usize { v.clamp(0, max_v.saturating_sub(1) as i32) as usize };
87        let x0 = clamp(min_x, width) / config.tile_width;
88        let y0 = clamp(min_y, height) / config.tile_height;
89        let x1 = clamp(max_x, width) / config.tile_width;
90        let y1 = clamp(max_y, height) / config.tile_height;
91        for ty in y0..=y1 {
92            for tx in x0..=x1 {
93                let bin_index = ty * grid.cols + tx;
94                bins[bin_index].push(idx).map_err(|_| {
95                    RenderError::OutOfBudget(crate::error::BudgetKind::DrawPrimitives {
96                        attempted: idx + 1,
97                        max: BIN_CAP,
98                    })
99                })?;
100            }
101        }
102    }
103
104    let bins_used = bins.iter().filter(|bin| !bin.is_empty()).count();
105    Ok((
106        bins,
107        TileBinStats {
108            draw_commands,
109            bins_used,
110        },
111    ))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::command_buffer::RenderCommand;
118    use embedded_graphics_core::pixelcolor::Rgb565;
119    use nalgebra::Point2;
120
121    #[test]
122    fn tile_grid_rejects_zero_tile_size() {
123        let err = tile_grid(
124            64,
125            64,
126            TileConfig {
127                tile_width: 0,
128                tile_height: 8,
129            },
130        )
131        .expect_err("zero tile width must fail");
132        assert!(matches!(err, RenderError::InvalidInput(_)));
133    }
134
135    #[test]
136    fn build_bins_tracks_draw_count_and_bins_used() {
137        let mut commands: CommandBuffer<8> = CommandBuffer::new();
138        commands
139            .push(RenderCommand::Draw(DrawPrimitive::ColoredTriangle(
140                [
141                    Point2::new(18, 18),
142                    Point2::new(30, 18),
143                    Point2::new(24, 30),
144                ],
145                Rgb565::new(31, 0, 0),
146            )))
147            .unwrap();
148
149        let (bins, stats) = build_bins::<8, 64>(
150            &commands,
151            64,
152            64,
153            TileConfig {
154                tile_width: 16,
155                tile_height: 16,
156            },
157        )
158        .expect("binning should succeed");
159
160        assert_eq!(stats.draw_commands, 1);
161        assert_eq!(stats.bins_used, 1);
162        let populated = bins.iter().filter(|b| !b.is_empty()).count();
163        assert_eq!(populated, 1);
164    }
165
166    #[test]
167    fn build_bins_rejects_excessive_grid_for_capacity() {
168        let commands: CommandBuffer<4> = CommandBuffer::new();
169        let err = build_bins::<4, 8>(
170            &commands,
171            64,
172            64,
173            TileConfig {
174                tile_width: 1,
175                tile_height: 1,
176            },
177        )
178        .expect_err("grid should exceed BIN_CAP");
179        assert!(matches!(err, RenderError::InvalidInput(_)));
180    }
181}