use oxiui_core::geometry::Rect;
use oxiui_core::paint::{DrawCommand, DrawList};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PipelineKind {
SolidColor,
Textured,
Gradient,
Path,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
Overlay,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BatchKey {
pub texture_id: Option<u64>,
pub pipeline: PipelineKind,
pub blend: BlendMode,
}
pub struct DrawBatch {
pub key: BatchKey,
pub command_range: std::ops::Range<usize>,
pub instance_count: usize,
}
pub struct PreparedFrame {
pub batches: Vec<DrawBatch>,
pub culled_count: usize,
}
pub fn batch(list: &DrawList, active_clip: Option<[f32; 4]>) -> PreparedFrame {
let mut drawable: Vec<(usize, &DrawCommand)> = list
.iter()
.enumerate()
.filter(|(_, cmd)| !is_clip_ctrl(cmd))
.collect();
let mut culled_count = 0usize;
if let Some(clip) = active_clip {
let clip_rect = clip_array_to_rect(clip);
drawable.retain(|(_, cmd)| {
match command_bounds(cmd) {
None => true, Some(bounds) => {
if rects_intersect(bounds, clip_rect) {
true
} else {
culled_count += 1;
false
}
}
}
});
}
drawable.sort_by_key(|(_, cmd)| classify(cmd));
let mut batches: Vec<DrawBatch> = Vec::new();
let mut i = 0;
while i < drawable.len() {
let key = classify(drawable[i].1);
let orig_start = drawable[i].0;
let mut orig_end = orig_start + 1;
let run_start = i;
i += 1;
while i < drawable.len() && classify(drawable[i].1) == key {
orig_end = drawable[i].0 + 1;
i += 1;
}
let run_len = i - run_start;
batches.push(DrawBatch {
key,
command_range: orig_start..orig_end,
instance_count: run_len,
});
}
PreparedFrame {
batches,
culled_count,
}
}
fn is_clip_ctrl(cmd: &DrawCommand) -> bool {
matches!(cmd, DrawCommand::PushClip { .. } | DrawCommand::PopClip)
}
fn classify(cmd: &DrawCommand) -> BatchKey {
let (pipeline, texture_id) = match cmd {
DrawCommand::FillRect { .. }
| DrawCommand::StrokeRect { .. }
| DrawCommand::FillRoundedRect { .. }
| DrawCommand::FillRoundedRectPerCorner { .. }
| DrawCommand::FillCircle { .. }
| DrawCommand::FillEllipse { .. }
| DrawCommand::Line { .. }
| DrawCommand::LineAa { .. }
| DrawCommand::LineThick { .. }
| DrawCommand::LineDashed { .. }
| DrawCommand::BoxShadow { .. }
| DrawCommand::DrawText { .. } => (PipelineKind::SolidColor, None),
DrawCommand::Image { .. } | DrawCommand::NineSlice { .. } => (PipelineKind::Textured, None),
DrawCommand::LinearGradient { .. } | DrawCommand::RadialGradient { .. } => {
(PipelineKind::Gradient, None)
}
DrawCommand::FillPath { .. } | DrawCommand::StrokePath { .. } => (PipelineKind::Path, None),
_ => (PipelineKind::SolidColor, None),
};
BatchKey {
texture_id,
pipeline,
blend: BlendMode::Normal,
}
}
fn command_bounds(cmd: &DrawCommand) -> Option<Rect> {
match cmd {
DrawCommand::FillRect { rect, .. }
| DrawCommand::StrokeRect { rect, .. }
| DrawCommand::FillRoundedRect { rect, .. }
| DrawCommand::FillRoundedRectPerCorner { rect, .. }
| DrawCommand::LinearGradient { rect, .. }
| DrawCommand::RadialGradient { rect, .. }
| DrawCommand::Image { dest: rect, .. }
| DrawCommand::NineSlice { dest: rect, .. }
| DrawCommand::DrawText { rect, .. } => Some(*rect),
DrawCommand::BoxShadow {
rect,
offset,
blur_radius,
..
} => {
let pad = *blur_radius;
Some(Rect::new(
rect.left() + offset.x - pad,
rect.top() + offset.y - pad,
rect.width() + 2.0 * pad,
rect.height() + 2.0 * pad,
))
}
DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
center.x - radius,
center.y - radius,
radius * 2.0,
radius * 2.0,
)),
DrawCommand::FillEllipse { center, rx, ry, .. } => {
Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
}
DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
let x = from.x.min(to.x);
let y = from.y.min(to.y);
Some(Rect::new(
x,
y,
(from.x - to.x).abs(),
(from.y - to.y).abs(),
))
}
DrawCommand::LineThick {
from, to, width, ..
} => {
let pad = width / 2.0;
Some(Rect::new(
from.x.min(to.x) - pad,
from.y.min(to.y) - pad,
(from.x - to.x).abs() + *width,
(from.y - to.y).abs() + *width,
))
}
DrawCommand::LineDashed { from, to, .. } => {
let x = from.x.min(to.x);
let y = from.y.min(to.y);
Some(Rect::new(
x,
y,
(from.x - to.x).abs(),
(from.y - to.y).abs(),
))
}
DrawCommand::FillPath { path, .. } => path.bounds(),
DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
let pad = style.width / 2.0;
Rect::new(
b.left() - pad,
b.top() - pad,
b.width() + style.width,
b.height() + style.width,
)
}),
_ => None,
}
}
fn clip_array_to_rect(clip: [f32; 4]) -> Rect {
Rect::new(clip[0], clip[1], clip[2], clip[3])
}
fn rects_intersect(a: Rect, b: Rect) -> bool {
a.left() < b.right() && b.left() < a.right() && a.top() < b.bottom() && b.top() < a.bottom()
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::paint::{DrawList, ImageData, ImageFilter};
use oxiui_core::{
geometry::{Point, Rect},
Color,
};
fn red() -> Color {
Color(255, 0, 0, 255)
}
fn list_with_n_rects(n: usize) -> DrawList {
let mut list = DrawList::new();
for i in 0..n {
list.push_rect(Rect::new(i as f32, 0.0, 1.0, 1.0), red());
}
list
}
#[test]
fn batcher_1000_rects_5_textures_le_5_batches() {
let list = list_with_n_rects(1000);
let frame = batch(&list, None);
assert!(
frame.batches.len() <= 5,
"expected ≤5 batches, got {}",
frame.batches.len()
);
}
#[test]
fn batcher_preserves_relative_order_within_batch() {
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 1.0, 1.0), Color(255, 0, 0, 255));
list.push_rect(Rect::new(10.0, 0.0, 1.0, 1.0), Color(0, 255, 0, 255));
let frame = batch(&list, None);
assert_eq!(frame.batches.len(), 1);
assert_eq!(frame.batches[0].instance_count, 2);
assert_eq!(frame.batches[0].command_range.start, 0);
}
#[test]
fn batcher_visibility_culling_drops_offscreen() {
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
list.push_rect(Rect::new(500.0, 500.0, 10.0, 10.0), red());
let clip = [0.0_f32, 0.0, 100.0, 100.0];
let frame = batch(&list, Some(clip));
assert_eq!(frame.culled_count, 1, "off-screen rect must be culled");
let total_instances: usize = frame.batches.iter().map(|b| b.instance_count).sum();
assert_eq!(total_instances, 1);
}
#[test]
fn batcher_multiple_pipeline_kinds_produce_multiple_batches() {
let mut list = DrawList::new();
list.push_rect(Rect::new(0.0, 0.0, 10.0, 10.0), red());
list.push_gradient_linear(
Rect::new(10.0, 0.0, 10.0, 10.0),
Point::new(10.0, 0.0),
Point::new(20.0, 0.0),
vec![],
);
list.push_image(
ImageData::new(vec![0, 0, 0, 255], 1, 1),
Rect::new(20.0, 0.0, 10.0, 10.0),
ImageFilter::Nearest,
);
let frame = batch(&list, None);
assert_eq!(frame.batches.len(), 3);
}
}