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