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