use crate::material::ColorFilter;
use crate::{BlendMode, Error, Material, Result, Scissor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ClipRole {
#[default]
Content,
Narrow,
Widen,
}
impl ClipRole {
pub const fn writes_color(self) -> bool {
matches!(self, Self::Content)
}
pub const fn writes_stencil(self) -> bool {
!matches!(self, Self::Content)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ClipState {
pub reference: u32,
pub role: ClipRole,
}
impl ClipState {
pub const UNCLIPPED: Self = Self {
reference: 0,
role: ClipRole::Content,
};
pub const fn content(reference: u32) -> Self {
Self {
reference,
role: ClipRole::Content,
}
}
pub const fn narrow(from: u32) -> Self {
Self {
reference: from,
role: ClipRole::Narrow,
}
}
pub const fn widen(from: u32) -> Self {
Self {
reference: from,
role: ClipRole::Widen,
}
}
pub const fn needs_stencil(self) -> bool {
self.reference != 0 || self.role.writes_stencil()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[repr(C)]
pub struct Vertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub color: [f32; 4],
}
impl Vertex {
pub const fn new(position: [f32; 2], uv: [f32; 2]) -> Self {
Self::projected([position[0], position[1], 1.0], uv)
}
pub const fn at(position: [f32; 2]) -> Self {
Self::new(position, [0.0, 0.0])
}
pub const fn projected(position: [f32; 3], uv: [f32; 2]) -> Self {
Self {
position,
uv,
color: WHITE,
}
}
pub const fn at_projected(position: [f32; 3]) -> Self {
Self::projected(position, [0.0, 0.0])
}
pub const fn with_color(mut self, color: [f32; 4]) -> Self {
self.color = color;
self
}
}
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
#[derive(Debug, Clone)]
pub struct BatchDraw {
pub first_index: u32,
pub index_count: u32,
pub material: Material,
pub filter: ColorFilter,
pub blend: BlendMode,
pub clip: Option<Scissor>,
pub stencil: ClipState,
pub tint_blend: BlendMode,
pub paint_at_texture_coords: bool,
}
impl BatchDraw {
pub fn to_uniform(&self, target: crate::PixelFormat) -> [f32; crate::MATERIAL_FLOATS] {
let mut out = self.material.to_uniform();
self.filter.pack_into(&mut out);
out[crate::material::layout::FILTER_PARAMS + 1] = self.tint_blend.code();
if self.paint_at_texture_coords {
out[crate::material::layout::GEOMETRY] = 1.0;
}
let dither = crate::material::layout::DITHER;
out[dither] = if target.quantization_step() > 0.0 {
1.0 / 64.0
} else {
0.0
};
out
}
}
#[derive(Debug, Default, Clone)]
pub struct Batch {
vertices: Vec<Vertex>,
indices: Vec<u32>,
draws: Vec<BatchDraw>,
}
impl Batch {
pub fn new() -> Self {
Self::default()
}
pub fn push(
&mut self,
vertices: &[[f32; 2]],
indices: &[u32],
material: Material,
blend: BlendMode,
) -> Result<()> {
self.push_clipped(vertices, indices, material, blend, None)
}
pub fn push_clipped(
&mut self,
vertices: &[[f32; 2]],
indices: &[u32],
material: Material,
blend: BlendMode,
clip: Option<Scissor>,
) -> Result<()> {
self.push_with(
vertices,
indices,
material,
ColorFilter::None,
blend,
clip,
ClipState::UNCLIPPED,
)
}
#[allow(clippy::too_many_arguments)]
pub fn push_with(
&mut self,
positions: &[[f32; 2]],
indices: &[u32],
material: Material,
filter: ColorFilter,
blend: BlendMode,
clip: Option<Scissor>,
stencil: ClipState,
) -> Result<()> {
let vertices: Vec<Vertex> = positions.iter().copied().map(Vertex::at).collect();
self.push_mesh(&vertices, indices, material, filter, blend, clip, stencil)
}
#[allow(clippy::too_many_arguments)]
pub fn push_mesh(
&mut self,
vertices: &[Vertex],
indices: &[u32],
material: Material,
filter: ColorFilter,
blend: BlendMode,
clip: Option<Scissor>,
stencil: ClipState,
) -> Result<()> {
self.push_mesh_tinted(
vertices,
indices,
material,
filter,
blend,
clip,
stencil,
BlendMode::Modulate,
false,
)
}
#[allow(clippy::too_many_arguments)]
pub fn push_mesh_tinted(
&mut self,
vertices: &[Vertex],
indices: &[u32],
material: Material,
filter: ColorFilter,
blend: BlendMode,
clip: Option<Scissor>,
stencil: ClipState,
tint_blend: BlendMode,
paint_at_texture_coords: bool,
) -> Result<()> {
if clip.is_some_and(Scissor::is_empty) {
return Ok(());
}
if indices.len() % 3 != 0 {
return Err(Error::Unsupported("index count is not a whole triangle"));
}
if let Some(&max) = indices.iter().max() {
if max as usize >= vertices.len() {
return Err(Error::Backend {
backend: "vulkan",
detail: format!(
"index {max} addresses past the {} vertices supplied",
vertices.len()
),
});
}
}
if indices.is_empty() {
return Ok(());
}
let base = u32::try_from(self.vertices.len()).map_err(|_| Error::LimitExceeded {
what: "batch vertex count",
requested: self.vertices.len() as u64,
limit: u32::MAX as u64,
})?;
let first_index = self.indices.len() as u32;
self.vertices.extend_from_slice(vertices);
self.indices.extend(indices.iter().map(|i| i + base));
if let Some(last) = self.draws.last_mut() {
if last.first_index + last.index_count == first_index
&& last.material == material
&& last.filter == filter
&& last.blend == blend
&& last.clip == clip
&& last.stencil == stencil
&& last.tint_blend == tint_blend
&& last.paint_at_texture_coords == paint_at_texture_coords
{
last.index_count += indices.len() as u32;
return Ok(());
}
}
self.draws.push(BatchDraw {
filter,
first_index,
index_count: indices.len() as u32,
material,
blend,
clip,
stencil,
tint_blend,
paint_at_texture_coords,
});
Ok(())
}
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
self.draws.clear();
}
pub fn draw_count(&self) -> usize {
self.draws.len()
}
pub fn is_empty(&self) -> bool {
self.draws.is_empty()
}
pub fn uses_stencil(&self) -> bool {
self.draws.iter().any(|draw| draw.stencil.needs_stencil())
}
pub const MAX_CLIP_DEPTH: u32 = 255;
pub fn check_clip_depth(&self) -> Result<()> {
let depth = self.max_clip_depth();
if depth > Self::MAX_CLIP_DEPTH {
return Err(Error::LimitExceeded {
what: "clip nesting depth",
requested: depth as u64,
limit: Self::MAX_CLIP_DEPTH as u64,
});
}
Ok(())
}
pub fn max_clip_depth(&self) -> u32 {
self.draws
.iter()
.map(|draw| match draw.stencil.role {
ClipRole::Narrow => draw.stencil.reference + 1,
_ => draw.stencil.reference,
})
.max()
.unwrap_or(0)
}
pub fn texture_slots(&self) -> Vec<u32> {
let mut slots: Vec<u32> = self
.draws
.iter()
.flat_map(|draw| draw.material.texture_slots())
.flatten()
.collect();
slots.sort_unstable();
slots.dedup();
slots
}
pub fn pipeline_binds(&self) -> usize {
let mut binds = 0;
let mut current: Option<BlendMode> = None;
for draw in &self.draws {
if current != Some(draw.blend) {
binds += 1;
current = Some(draw.blend);
}
}
binds
}
}
impl Batch {
pub fn rebase_scissors(&mut self, dx: u32, dy: u32, extent: crate::Extent2D) {
for draw in &mut self.draws {
if let Some(clip) = draw.clip {
draw.clip = Some(clip.shifted(dx, dy, extent));
}
}
}
pub fn vertices(&self) -> &[Vertex] {
&self.vertices
}
pub fn indices(&self) -> &[u32] {
&self.indices
}
pub fn draws(&self) -> &[BatchDraw] {
&self.draws
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRI: [[f32; 2]; 3] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
#[test]
fn indices_are_rebased_onto_the_shared_buffer() {
let mut batch = Batch::new();
batch
.push(&TRI, &[0, 1, 2], Material::solid([1.0; 4]), BlendMode::Src)
.unwrap();
batch
.push(&TRI, &[0, 1, 2], Material::solid([0.5; 4]), BlendMode::Src)
.unwrap();
assert_eq!(batch.indices, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(batch.vertices.len(), 6);
assert_eq!(batch.draws[1].first_index, 3);
assert_eq!(batch.draw_count(), 2);
}
#[test]
fn a_draw_that_differs_from_the_one_before_it_in_nothing_is_not_a_second_draw() {
let mut batch = Batch::new();
for _ in 0..4 {
batch
.push(&TRI, &[0, 1, 2], Material::solid([1.0; 4]), BlendMode::Src)
.unwrap();
}
assert_eq!(batch.draw_count(), 1, "four alike draws should be one");
assert_eq!(batch.indices, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
assert_eq!(batch.draws[0].index_count, 12);
batch
.push(
&TRI,
&[0, 1, 2],
Material::solid([1.0; 4]),
BlendMode::SrcOver,
)
.unwrap();
assert_eq!(batch.draw_count(), 2);
}
#[test]
fn pipeline_binds_count_transitions_not_draws() {
let mut batch = Batch::new();
for (i, blend) in [
BlendMode::Src,
BlendMode::Src,
BlendMode::SrcOver,
BlendMode::SrcOver,
BlendMode::Src,
]
.into_iter()
.enumerate()
{
let shade = i as f32 / 8.0;
batch
.push(&TRI, &[0, 1, 2], Material::solid([shade; 4]), blend)
.unwrap();
}
assert_eq!(batch.draw_count(), 5);
assert_eq!(batch.pipeline_binds(), 3);
}
#[test]
fn an_empty_draw_adds_nothing() {
let mut batch = Batch::new();
batch
.push(&[], &[], Material::solid([1.0; 4]), BlendMode::Src)
.unwrap();
assert!(batch.is_empty());
assert_eq!(batch.draw_count(), 0);
}
#[test]
fn malformed_geometry_is_refused_where_it_is_pushed() {
let mut batch = Batch::new();
assert!(batch
.push(
&[[0.0, 0.0]],
&[0, 1, 2],
Material::solid([1.0; 4]),
BlendMode::Src
)
.is_err());
assert!(batch
.push(
&[[0.0, 0.0]],
&[0, 0],
Material::solid([1.0; 4]),
BlendMode::Src
)
.is_err());
assert!(batch.is_empty(), "a refused draw must leave no residue");
}
#[test]
fn clearing_keeps_the_batch_reusable() {
let mut batch = Batch::new();
batch
.push(&TRI, &[0, 1, 2], Material::solid([1.0; 4]), BlendMode::Src)
.unwrap();
batch.clear();
assert!(batch.is_empty());
batch
.push(&TRI, &[0, 1, 2], Material::solid([1.0; 4]), BlendMode::Src)
.unwrap();
assert_eq!(batch.indices, vec![0, 1, 2]);
}
#[test]
fn the_vertex_layout_is_what_both_backends_describe() {
use std::mem::{offset_of, size_of};
assert_eq!(size_of::<Vertex>(), 36);
assert_eq!(offset_of!(Vertex, position), 0);
assert_eq!(offset_of!(Vertex, uv), 12);
assert_eq!(offset_of!(Vertex, color), 20);
}
#[test]
fn an_ordinary_vertex_carries_a_w_of_one() {
assert_eq!(Vertex::at([3.0, 4.0]).position, [3.0, 4.0, 1.0]);
assert_eq!(
Vertex::new([3.0, 4.0], [0.5, 0.5]).position,
[3.0, 4.0, 1.0]
);
assert_eq!(
Vertex::at_projected([3.0, 4.0, 2.0]).position,
[3.0, 4.0, 2.0]
);
}
}