1use oxiui_core::geometry::Rect;
12use oxiui_core::paint::{DrawCommand, DrawList};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum PipelineKind {
19 SolidColor,
21 Textured,
23 Gradient,
25 Path,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum BlendMode {
32 Normal,
34 Multiply,
36 Screen,
38 Overlay,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct BatchKey {
50 pub texture_id: Option<u64>,
52 pub pipeline: PipelineKind,
54 pub blend: BlendMode,
56}
57
58pub struct DrawBatch {
62 pub key: BatchKey,
64 pub command_range: std::ops::Range<usize>,
67 pub instance_count: usize,
69}
70
71pub struct PreparedFrame {
75 pub batches: Vec<DrawBatch>,
77 pub culled_count: usize,
79}
80
81pub fn batch(list: &DrawList, active_clip: Option<[f32; 4]>) -> PreparedFrame {
98 let mut drawable: Vec<(usize, &DrawCommand)> = list
100 .iter()
101 .enumerate()
102 .filter(|(_, cmd)| !is_clip_ctrl(cmd))
103 .collect();
104
105 let mut culled_count = 0usize;
107 if let Some(clip) = active_clip {
108 let clip_rect = clip_array_to_rect(clip);
109 drawable.retain(|(_, cmd)| {
110 match command_bounds(cmd) {
111 None => true, Some(bounds) => {
113 if rects_intersect(bounds, clip_rect) {
114 true
115 } else {
116 culled_count += 1;
117 false
118 }
119 }
120 }
121 });
122 }
123
124 drawable.sort_by_key(|(_, cmd)| classify(cmd));
126
127 let mut batches: Vec<DrawBatch> = Vec::new();
129 let mut i = 0;
130 while i < drawable.len() {
131 let key = classify(drawable[i].1);
132 let orig_start = drawable[i].0;
133 let mut orig_end = orig_start + 1;
134 let run_start = i;
135 i += 1;
136 while i < drawable.len() && classify(drawable[i].1) == key {
137 orig_end = drawable[i].0 + 1;
138 i += 1;
139 }
140 let run_len = i - run_start;
141 batches.push(DrawBatch {
142 key,
143 command_range: orig_start..orig_end,
144 instance_count: run_len,
145 });
146 }
147
148 PreparedFrame {
149 batches,
150 culled_count,
151 }
152}
153
154fn is_clip_ctrl(cmd: &DrawCommand) -> bool {
158 matches!(cmd, DrawCommand::PushClip { .. } | DrawCommand::PopClip)
159}
160
161fn classify(cmd: &DrawCommand) -> BatchKey {
163 let (pipeline, texture_id) = match cmd {
164 DrawCommand::FillRect { .. }
165 | DrawCommand::StrokeRect { .. }
166 | DrawCommand::FillRoundedRect { .. }
167 | DrawCommand::FillRoundedRectPerCorner { .. }
168 | DrawCommand::FillCircle { .. }
169 | DrawCommand::FillEllipse { .. }
170 | DrawCommand::Line { .. }
171 | DrawCommand::LineAa { .. }
172 | DrawCommand::LineThick { .. }
173 | DrawCommand::LineDashed { .. }
174 | DrawCommand::BoxShadow { .. }
175 | DrawCommand::DrawText { .. } => (PipelineKind::SolidColor, None),
176
177 DrawCommand::Image { .. } | DrawCommand::NineSlice { .. } => (PipelineKind::Textured, None),
178
179 DrawCommand::LinearGradient { .. } | DrawCommand::RadialGradient { .. } => {
180 (PipelineKind::Gradient, None)
181 }
182
183 DrawCommand::FillPath { .. } | DrawCommand::StrokePath { .. } => (PipelineKind::Path, None),
184
185 _ => (PipelineKind::SolidColor, None),
187 };
188 BatchKey {
189 texture_id,
190 pipeline,
191 blend: BlendMode::Normal,
192 }
193}
194
195fn command_bounds(cmd: &DrawCommand) -> Option<Rect> {
200 match cmd {
201 DrawCommand::FillRect { rect, .. }
202 | DrawCommand::StrokeRect { rect, .. }
203 | DrawCommand::FillRoundedRect { rect, .. }
204 | DrawCommand::FillRoundedRectPerCorner { rect, .. }
205 | DrawCommand::LinearGradient { rect, .. }
206 | DrawCommand::RadialGradient { rect, .. }
207 | DrawCommand::Image { dest: rect, .. }
208 | DrawCommand::NineSlice { dest: rect, .. }
209 | DrawCommand::DrawText { rect, .. } => Some(*rect),
210
211 DrawCommand::BoxShadow {
212 rect,
213 offset,
214 blur_radius,
215 ..
216 } => {
217 let pad = *blur_radius;
218 Some(Rect::new(
219 rect.left() + offset.x - pad,
220 rect.top() + offset.y - pad,
221 rect.width() + 2.0 * pad,
222 rect.height() + 2.0 * pad,
223 ))
224 }
225
226 DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
227 center.x - radius,
228 center.y - radius,
229 radius * 2.0,
230 radius * 2.0,
231 )),
232
233 DrawCommand::FillEllipse { center, rx, ry, .. } => {
234 Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
235 }
236
237 DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
238 let x = from.x.min(to.x);
239 let y = from.y.min(to.y);
240 Some(Rect::new(
241 x,
242 y,
243 (from.x - to.x).abs(),
244 (from.y - to.y).abs(),
245 ))
246 }
247
248 DrawCommand::LineThick {
249 from, to, width, ..
250 } => {
251 let pad = width / 2.0;
252 Some(Rect::new(
253 from.x.min(to.x) - pad,
254 from.y.min(to.y) - pad,
255 (from.x - to.x).abs() + *width,
256 (from.y - to.y).abs() + *width,
257 ))
258 }
259
260 DrawCommand::LineDashed { from, to, .. } => {
261 let x = from.x.min(to.x);
262 let y = from.y.min(to.y);
263 Some(Rect::new(
264 x,
265 y,
266 (from.x - to.x).abs(),
267 (from.y - to.y).abs(),
268 ))
269 }
270
271 DrawCommand::FillPath { path, .. } => path.bounds(),
272
273 DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
274 let pad = style.width / 2.0;
275 Rect::new(
276 b.left() - pad,
277 b.top() - pad,
278 b.width() + style.width,
279 b.height() + style.width,
280 )
281 }),
282
283 _ => None,
285 }
286}
287
288fn clip_array_to_rect(clip: [f32; 4]) -> Rect {
290 Rect::new(clip[0], clip[1], clip[2], clip[3])
291}
292
293fn rects_intersect(a: Rect, b: Rect) -> bool {
295 a.left() < b.right() && b.left() < a.right() && a.top() < b.bottom() && b.top() < a.bottom()
296}
297
298#[cfg(test)]
301mod tests {
302 use super::*;
303 use oxiui_core::paint::{DrawList, ImageData, ImageFilter};
304 use oxiui_core::{
305 geometry::{Point, Rect},
306 Color,
307 };
308
309 fn red() -> Color {
310 Color(255, 0, 0, 255)
311 }
312
313 fn list_with_n_rects(n: usize) -> DrawList {
314 let mut list = DrawList::new();
315 for i in 0..n {
316 list.push_rect(Rect::new(i as f32, 0.0, 1.0, 1.0), red());
317 }
318 list
319 }
320
321 #[test]
322 fn batcher_1000_rects_5_textures_le_5_batches() {
323 let list = list_with_n_rects(1000);
326 let frame = batch(&list, None);
327 assert!(
329 frame.batches.len() <= 5,
330 "expected ≤5 batches, got {}",
331 frame.batches.len()
332 );
333 }
334
335 #[test]
336 fn batcher_preserves_relative_order_within_batch() {
337 let mut list = DrawList::new();
340 list.push_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Color(255, 0, 0, 255));
341 list.push_rect(Rect::new(10.0, 0.0, 1.0, 1.0), Color(0, 255, 0, 255));
342 let frame = batch(&list, None);
343 assert_eq!(frame.batches.len(), 1);
344 assert_eq!(frame.batches[0].instance_count, 2);
345 assert_eq!(frame.batches[0].command_range.start, 0);
347 }
348
349 #[test]
350 fn batcher_visibility_culling_drops_offscreen() {
351 let mut list = DrawList::new();
352 list.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
354 list.push_rect(Rect::new(500.0, 500.0, 10.0, 10.0), red());
356
357 let clip = [0.0_f32, 0.0, 100.0, 100.0];
358 let frame = batch(&list, Some(clip));
359 assert_eq!(frame.culled_count, 1, "off-screen rect must be culled");
360 let total_instances: usize = frame.batches.iter().map(|b| b.instance_count).sum();
362 assert_eq!(total_instances, 1);
363 }
364
365 #[test]
366 fn batcher_multiple_pipeline_kinds_produce_multiple_batches() {
367 let mut list = DrawList::new();
368 list.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
369 list.push_gradient_linear(
370 Rect::new(10.0, 0.0, 10.0, 10.0),
371 Point::new(10.0, 0.0),
372 Point::new(20.0, 0.0),
373 vec![],
374 );
375 list.push_image(
376 ImageData::new(vec![0, 0, 0, 255], 1, 1),
377 Rect::new(20.0, 0.0, 10.0, 10.0),
378 ImageFilter::Nearest,
379 );
380 let frame = batch(&list, None);
381 assert_eq!(frame.batches.len(), 3);
383 }
384}