use crate::color::Color;
use crate::error::Result;
use crate::sprite::SpriteBatch;
use crate::vertex::Vertex2D;
use wgpu::util::DeviceExt;
pub use crate::batch::{
MAX_SPRITES_PER_BATCH, batch_to_vertices, batch_to_vertices_into, batch_to_vertices_u32,
batch_to_vertices_u32_into,
};
fn orthographic_projection(width: f32, height: f32) -> [f32; 16] {
[
2.0 / width,
0.0,
0.0,
0.0,
0.0,
-2.0 / height,
0.0,
0.0,
0.0,
0.0,
-0.5,
0.0,
-1.0,
1.0,
0.5,
1.0,
]
}
pub struct SpriteBuffers {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
vertex_capacity: usize,
index_capacity: usize,
vertices: Vec<Vertex2D>,
indices: Vec<u16>,
}
impl SpriteBuffers {
#[must_use]
pub fn new(device: &wgpu::Device, sprite_capacity: usize) -> Self {
let vert_cap = sprite_capacity.saturating_mul(4);
let idx_cap = sprite_capacity.saturating_mul(6);
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_vertex_buffer_persistent"),
size: (vert_cap * std::mem::size_of::<Vertex2D>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_index_buffer_persistent"),
size: (idx_cap * std::mem::size_of::<u16>()) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
vertex_buffer,
index_buffer,
vertex_capacity: vert_cap,
index_capacity: idx_cap,
vertices: Vec::with_capacity(vert_cap),
indices: Vec::with_capacity(idx_cap),
}
}
pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, batch: &SpriteBatch) {
batch_to_vertices_into(batch, &mut self.vertices, &mut self.indices);
if self.vertices.len() > self.vertex_capacity {
self.vertex_capacity = self.vertices.len();
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_vertex_buffer_persistent"),
size: (self.vertex_capacity * std::mem::size_of::<Vertex2D>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
if self.indices.len() > self.index_capacity {
self.index_capacity = self.indices.len();
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite_index_buffer_persistent"),
size: (self.index_capacity * std::mem::size_of::<u16>()) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
}
#[must_use]
#[inline]
pub fn index_count(&self) -> u32 {
self.indices.len() as u32
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FrameStats {
pub draw_calls: u32,
pub triangles: u32,
pub sprites: u32,
}
pub struct SpriteBatchDrawParams<'a> {
pub view: &'a wgpu::TextureView,
pub batch: &'a SpriteBatch,
pub texture_cache: &'a crate::texture::TextureCache,
pub fallback_bind_group: &'a wgpu::BindGroup,
pub clear_color: Option<Color>,
}
pub struct SpritePipeline {
pipeline: mabda::RenderPipeline,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
}
impl SpritePipeline {
pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Result<Self> {
tracing::debug!(?surface_format, "creating sprite pipeline");
let pipeline = mabda::RenderPipelineBuilder::new(
device,
include_str!("sprite.wgsl"),
"vs_main",
"fs_main",
)
.label("sprite_pipeline")
.vertex_layout(Vertex2D::layout())
.bind_group(
mabda::BindGroupLayoutBuilder::new()
.uniform_buffer(wgpu::ShaderStages::VERTEX)
.into_entries(),
)
.bind_group(
mabda::BindGroupLayoutBuilder::new()
.texture_2d(wgpu::ShaderStages::FRAGMENT)
.sampler(wgpu::ShaderStages::FRAGMENT)
.into_entries(),
)
.color_target(surface_format, Some(wgpu::BlendState::ALPHA_BLENDING))
.build()?;
let uniform_buffer = mabda::create_uniform_buffer(
device,
bytemuck::cast_slice(&[0.0_f32; 16]),
"sprite_uniform_buffer",
);
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sprite_uniform_bind_group"),
layout: pipeline.bind_group_layout(0).ok_or_else(|| {
crate::error::RenderError::Pipeline(
"sprite pipeline missing bind group layout 0".into(),
)
})?,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
Ok(Self {
pipeline,
uniform_buffer,
uniform_bind_group,
})
}
pub fn texture_bind_group_layout(&self) -> Result<&wgpu::BindGroupLayout> {
self.pipeline.bind_group_layout(1).ok_or_else(|| {
crate::error::RenderError::Pipeline(
"sprite pipeline missing bind group layout 1".into(),
)
})
}
pub fn update_projection(&self, queue: &wgpu::Queue, width: f32, height: f32) {
if width <= 0.0 || height <= 0.0 {
tracing::warn!(
width,
height,
"zero or negative viewport size, skipping projection update"
);
return;
}
let proj = orthographic_projection(width, height);
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&proj));
}
pub fn draw(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
view: &wgpu::TextureView,
batch: &SpriteBatch,
texture_bind_group: &wgpu::BindGroup,
clear_color: Option<Color>,
) -> FrameStats {
let mut stats = FrameStats::default();
tracing::debug!(
sprite_count = batch.sprites.len(),
has_clear = clear_color.is_some(),
"drawing sprites"
);
if batch.is_empty() && clear_color.is_none() {
return stats;
}
let (vertices, indices) = batch_to_vertices(batch);
stats.sprites = batch.sprites.len() as u32;
stats.triangles = indices.len() as u32 / 3;
let (vertex_buffer, index_buffer) = Self::upload_buffers(device, &vertices, &indices);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("sprite_encoder"),
});
{
let mut render_pass = Self::begin_pass(&mut encoder, view, clear_color);
if !batch.is_empty() {
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, texture_bind_group, &[]);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
stats.draw_calls = 1;
}
}
queue.submit(std::iter::once(encoder.finish()));
stats
}
pub fn draw_with_buffers(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
view: &wgpu::TextureView,
buffers: &SpriteBuffers,
texture_bind_group: &wgpu::BindGroup,
clear_color: Option<Color>,
) -> FrameStats {
let mut stats = FrameStats::default();
let index_count = buffers.index_count();
tracing::debug!(
index_count,
has_clear = clear_color.is_some(),
"drawing sprites with buffers"
);
if index_count == 0 && clear_color.is_none() {
return stats;
}
stats.sprites = index_count / 6;
stats.triangles = index_count / 3;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("sprite_encoder"),
});
{
let mut render_pass = Self::begin_pass(&mut encoder, view, clear_color);
if index_count > 0 {
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, texture_bind_group, &[]);
render_pass.set_vertex_buffer(0, buffers.vertex_buffer.slice(..));
render_pass
.set_index_buffer(buffers.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..index_count, 0, 0..1);
stats.draw_calls = 1;
}
}
queue.submit(std::iter::once(encoder.finish()));
stats
}
pub fn draw_batched(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
params: &SpriteBatchDrawParams<'_>,
) -> FrameStats {
let mut stats = FrameStats::default();
tracing::debug!(
sprite_count = params.batch.sprites.len(),
has_clear = params.clear_color.is_some(),
"drawing sprites batched"
);
if params.batch.is_empty() && params.clear_color.is_none() {
return stats;
}
let (vertices, indices) = batch_to_vertices(params.batch);
stats.sprites = params.batch.sprites.len() as u32;
stats.triangles = indices.len() as u32 / 3;
let (vertex_buffer, index_buffer) = Self::upload_buffers(device, &vertices, &indices);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("sprite_encoder"),
});
{
let mut render_pass = Self::begin_pass(&mut encoder, params.view, params.clear_color);
if !params.batch.is_empty() {
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
let mut run_start: u32 = 0;
let mut current_tex_id = params.batch.sprites[0].texture_id;
for (i, sprite) in params.batch.sprites.iter().enumerate() {
if sprite.texture_id != current_tex_id {
let bind_group = params
.texture_cache
.get_bind_group(current_tex_id)
.unwrap_or(params.fallback_bind_group);
render_pass.set_bind_group(1, bind_group, &[]);
let idx_start = run_start * 6;
let idx_end = (i as u32) * 6;
render_pass.draw_indexed(idx_start..idx_end, 0, 0..1);
stats.draw_calls += 1;
run_start = i as u32;
current_tex_id = sprite.texture_id;
}
}
let bind_group = params
.texture_cache
.get_bind_group(current_tex_id)
.unwrap_or(params.fallback_bind_group);
render_pass.set_bind_group(1, bind_group, &[]);
let idx_start = run_start * 6;
let idx_end = params.batch.sprites.len() as u32 * 6;
render_pass.draw_indexed(idx_start..idx_end, 0, 0..1);
stats.draw_calls += 1;
}
}
queue.submit(std::iter::once(encoder.finish()));
stats
}
fn upload_buffers(
device: &wgpu::Device,
vertices: &[Vertex2D],
indices: &[u16],
) -> (wgpu::Buffer, wgpu::Buffer) {
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("sprite_vertex_buffer"),
contents: bytemuck::cast_slice(vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("sprite_index_buffer"),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX,
});
(vertex_buffer, index_buffer)
}
fn begin_pass<'a>(
encoder: &'a mut wgpu::CommandEncoder,
view: &'a wgpu::TextureView,
clear_color: Option<Color>,
) -> wgpu::RenderPass<'a> {
let load_op = match clear_color {
Some(c) => wgpu::LoadOp::Clear(c.to_wgpu()),
None => wgpu::LoadOp::Load,
};
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sprite_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
})
}
pub fn draw_into_pass<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
batch: &SpriteBatch,
texture_bind_group: &'a wgpu::BindGroup,
device: &wgpu::Device,
) {
tracing::debug!(
sprite_count = batch.sprites.len(),
"drawing sprites into pass"
);
if batch.is_empty() {
return;
}
let (vertices, indices) = batch_to_vertices(batch);
let (vertex_buffer, index_buffer) = Self::upload_buffers(device, &vertices, &indices);
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, texture_bind_group, &[]);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sprite::Sprite;
#[test]
fn orthographic_projection_values() {
let proj = orthographic_projection(800.0, 600.0);
assert_eq!(proj.len(), 16);
assert!((proj[0] - 2.0 / 800.0).abs() < f32::EPSILON);
assert!((proj[5] - (-2.0 / 600.0)).abs() < f32::EPSILON);
assert!((proj[12] - (-1.0)).abs() < f32::EPSILON);
assert!((proj[13] - 1.0).abs() < f32::EPSILON);
}
#[test]
fn batch_to_vertices_empty() {
let batch = SpriteBatch::new();
let (verts, indices) = batch_to_vertices(&batch);
assert!(verts.is_empty());
assert!(indices.is_empty());
}
#[test]
fn batch_to_vertices_single_sprite() {
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(10.0, 20.0, 32.0, 32.0));
let (verts, indices) = batch_to_vertices(&batch);
assert_eq!(verts.len(), 4);
assert_eq!(indices.len(), 6);
assert_eq!(indices, vec![0, 1, 2, 2, 3, 0]);
assert_eq!(verts[0].position, [10.0, 20.0]);
assert_eq!(verts[1].position, [42.0, 20.0]);
assert_eq!(verts[2].position, [42.0, 52.0]);
assert_eq!(verts[3].position, [10.0, 52.0]);
}
#[test]
fn batch_to_vertices_rotated_sprite() {
let mut batch = SpriteBatch::new();
let half_pi = std::f32::consts::FRAC_PI_2;
batch.push(Sprite::new(0.0, 0.0, 100.0, 100.0).with_rotation(half_pi));
let (verts, _) = batch_to_vertices(&batch);
let eps = 0.01;
assert!((verts[0].position[0] - 100.0).abs() < eps);
assert!((verts[0].position[1] - 0.0).abs() < eps);
assert!((verts[2].position[0] - 0.0).abs() < eps);
assert!((verts[2].position[1] - 100.0).abs() < eps);
}
#[test]
fn batch_to_vertices_zero_rotation_matches_unrotated() {
let mut batch_a = SpriteBatch::new();
batch_a.push(Sprite::new(10.0, 20.0, 50.0, 30.0));
let mut batch_b = SpriteBatch::new();
batch_b.push(Sprite::new(10.0, 20.0, 50.0, 30.0).with_rotation(0.0));
let (va, _) = batch_to_vertices(&batch_a);
let (vb, _) = batch_to_vertices(&batch_b);
for (a, b) in va.iter().zip(vb.iter()) {
assert_eq!(a.position, b.position);
}
}
#[test]
fn batch_to_vertices_multiple_sprites() {
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(0.0, 0.0, 10.0, 10.0));
batch.push(Sprite::new(50.0, 50.0, 20.0, 20.0));
let (verts, indices) = batch_to_vertices(&batch);
assert_eq!(verts.len(), 8);
assert_eq!(indices.len(), 12);
assert_eq!(indices[6..], [4, 5, 6, 6, 7, 4]);
}
#[test]
fn batch_to_vertices_preserves_color() {
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(0.0, 0.0, 10.0, 10.0).with_color(Color::RED));
let (verts, _) = batch_to_vertices(&batch);
for v in &verts {
assert_eq!(v.color, [1.0, 0.0, 0.0, 1.0]);
}
}
#[test]
fn batch_to_vertices_tex_coords() {
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(0.0, 0.0, 10.0, 10.0));
let (verts, _) = batch_to_vertices(&batch);
assert_eq!(verts[0].tex_coords, [0.0, 0.0]);
assert_eq!(verts[1].tex_coords, [1.0, 0.0]);
assert_eq!(verts[2].tex_coords, [1.0, 1.0]);
assert_eq!(verts[3].tex_coords, [0.0, 1.0]);
}
#[test]
fn batch_to_vertices_into_reuses_buffers() {
let mut verts = Vec::new();
let mut indices = Vec::new();
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(0.0, 0.0, 10.0, 10.0));
batch_to_vertices_into(&batch, &mut verts, &mut indices);
assert_eq!(verts.len(), 4);
assert_eq!(indices.len(), 6);
batch.push(Sprite::new(20.0, 0.0, 10.0, 10.0));
batch_to_vertices_into(&batch, &mut verts, &mut indices);
assert_eq!(verts.len(), 8);
assert_eq!(indices.len(), 12);
assert!(verts.capacity() >= 8);
}
#[test]
fn frame_stats_default() {
let stats = FrameStats::default();
assert_eq!(stats.draw_calls, 0);
assert_eq!(stats.triangles, 0);
assert_eq!(stats.sprites, 0);
}
#[test]
fn batch_to_vertices_with_uv() {
use crate::sprite::UvRect;
let uv = UvRect::from_pixel_rect(0, 0, 16, 16, 64, 64);
let mut batch = SpriteBatch::new();
batch.push(Sprite::new(0.0, 0.0, 32.0, 32.0).with_uv(uv));
let (verts, _) = batch_to_vertices(&batch);
assert_eq!(verts[0].tex_coords, [uv.u_min, uv.v_min]);
assert_eq!(verts[1].tex_coords, [uv.u_max, uv.v_min]);
assert_eq!(verts[2].tex_coords, [uv.u_max, uv.v_max]);
assert_eq!(verts[3].tex_coords, [uv.u_min, uv.v_max]);
}
#[test]
fn batch_to_vertices_into_matches_batch_to_vertices() {
let mut batch = SpriteBatch::new();
for i in 0..10 {
batch.push(Sprite::new(i as f32 * 10.0, 0.0, 32.0, 32.0).with_color(Color::RED));
}
let (verts_a, indices_a) = batch_to_vertices(&batch);
let mut verts_b = Vec::new();
let mut indices_b = Vec::new();
batch_to_vertices_into(&batch, &mut verts_b, &mut indices_b);
assert_eq!(verts_a, verts_b);
assert_eq!(indices_a, indices_b);
}
#[test]
fn max_sprites_constant() {
assert_eq!(MAX_SPRITES_PER_BATCH, 16383);
}
#[test]
fn batch_u32_overflow_protection() {
let mut batch = SpriteBatch::new();
for i in 0..100 {
batch.push(Sprite::new(i as f32, 0.0, 1.0, 1.0));
}
let (verts, indices) = batch_to_vertices_u32(&batch);
assert_eq!(verts.len(), 400);
assert_eq!(indices.len(), 600);
for &idx in &indices {
assert!((idx as usize) < verts.len());
}
}
#[test]
fn batch_to_vertices_respects_limit() {
let mut batch = SpriteBatch::new();
for i in 0..16400 {
batch.push(Sprite::new(i as f32, 0.0, 1.0, 1.0));
}
let (verts, indices) = batch_to_vertices(&batch);
assert_eq!(verts.len(), MAX_SPRITES_PER_BATCH * 4);
assert_eq!(indices.len(), MAX_SPRITES_PER_BATCH * 6);
for &idx in &indices {
assert!(idx < u16::MAX);
}
}
#[test]
fn batch_to_vertices_u32_no_limit() {
let mut batch = SpriteBatch::new();
for i in 0..20000 {
batch.push(Sprite::new(i as f32, 0.0, 1.0, 1.0));
}
let (verts, indices) = batch_to_vertices_u32(&batch);
assert_eq!(verts.len(), 20000 * 4);
assert_eq!(indices.len(), 20000 * 6);
}
#[test]
fn batch_to_vertices_u32_matches_u16_for_small() {
let mut batch = SpriteBatch::new();
for i in 0..10 {
batch.push(Sprite::new(i as f32, 0.0, 32.0, 32.0));
}
let (verts_16, indices_16) = batch_to_vertices(&batch);
let (verts_32, indices_32) = batch_to_vertices_u32(&batch);
assert_eq!(verts_16, verts_32);
for (a, b) in indices_16.iter().zip(indices_32.iter()) {
assert_eq!(*a as u32, *b);
}
}
#[test]
fn batch_to_vertices_at_exact_limit() {
let mut batch = SpriteBatch::new();
for i in 0..MAX_SPRITES_PER_BATCH {
batch.push(Sprite::new(i as f32, 0.0, 1.0, 1.0));
}
let (verts, indices) = batch_to_vertices(&batch);
assert_eq!(verts.len(), MAX_SPRITES_PER_BATCH * 4);
assert_eq!(indices.len(), MAX_SPRITES_PER_BATCH * 6);
let last_idx = *indices.last().unwrap();
assert!(last_idx < u16::MAX);
}
}