//! Quad-based draw call batcher //! //! Based on FNA's `SpriteBatch`. You would want to make some wrapper that provides a fluent API. use { anyhow::{Error, Result}, std::mem, }; use super::gfx::{Shader2d, Vertex}; #[derive(Debug, Clone, Default)] pub struct QuadData(pub [Vertex; 4]); /// Creates an iterator of draw calls. /// /// It's wrapped into [`Batcher`] so that it can be flushed running draw calls. #[derive(Debug)] struct Batch { device: fna3d::Device, vbuf: *mut fna3d::Buffer, vbind: fna3d::VertexBufferBinding, ibuf: *mut fna3d::Buffer, quads: Vec, /// The number of quads stored in this batch n_quads: usize, /// `track.len() == quads.len()` track: Vec<*mut fna3d::Texture>, } impl Drop for Batch { fn drop(&mut self) { self.device.add_dispose_vertex_buffer(self.vbuf); self.device.add_dispose_index_buffer(self.ibuf); } } /// Creates index buffer for quadliterals /// /// Each index element has 16 bits length macro_rules! gen_quad_indices { ( $n_quads:expr ) => {{ let mut indices = [0; 6 * $n_quads as usize]; for q in 0..$n_quads as i16 { let (i, v) = (q * 6, q * 4); indices[i as usize] = v as i16; indices[(i + 1) as usize] = v + 1 as i16; indices[(i + 2) as usize] = v + 2 as i16; indices[(i + 3) as usize] = v + 3 as i16; indices[(i + 4) as usize] = v + 2 as i16; indices[(i + 5) as usize] = v + 1 as i16; } indices }}; } impl Batch { pub fn new(device: &fna3d::Device) -> Result { // number of quads to allocate const N_QUADS: u32 = 2048; // GPU vertex buffer (marked as "dynamic") let n_verts = 4 * N_QUADS; let vbuf = device.gen_vertex_buffer( true, // dynamic fna3d::BufferUsage::WriteOnly, n_verts * mem::size_of::() as u32, ); // GPU index buffer (marked as "static") let n_indices = 6 * N_QUADS; // each index element has 16 bits length let ibuf = device.gen_index_buffer(false, fna3d::BufferUsage::WriteOnly, n_indices * 16); device.set_index_buffer_data( ibuf, 0, // offset (in bytes) &gen_quad_indices!(N_QUADS), fna3d::SetDataOptions::None, ); // vertex attributes // META: such types are just re-exported from FFI to FNA3D and don't have snake_case fields let vbind = fna3d::VertexBufferBinding { vertexBuffer: vbuf, vertexDeclaration: Vertex::DECLARATION, vertexOffset: 0, instanceFrequency: 0, }; let quads = vec![QuadData::default(); N_QUADS as usize]; let track = vec![std::ptr::null_mut(); N_QUADS as usize]; Ok(Self { device: device.clone(), vbuf, vbind, ibuf, quads, n_quads: 0, track, }) } pub unsafe fn next_quad_mut(&mut self) -> &mut QuadData { let quad = &mut self.quads[self.n_quads]; self.n_quads += 1; quad } /// Make sure the [`Batch`] is not yet satured before calling this method pub unsafe fn push_quad(&mut self, quad: &QuadData, tex: *mut fna3d::Texture) { self.quads[self.n_quads] = quad.clone(); self.track[self.n_quads] = tex; self.n_quads += 1; } pub fn draw_calls(&self) -> DrawCallIterator { DrawCallIterator::from_batch(self) } } /// Quad span [lo, hi) with texture #[derive(Debug)] pub struct DrawCall { pub tex: *mut fna3d::Texture, /// low quad index (inclusive) pub lo: usize, /// high quad index (exclusive) pub hi: usize, } impl DrawCall { pub fn n_quads(&self) -> usize { self.hi - self.lo } pub fn n_verts(&self) -> usize { 4 * self.n_quads() } pub fn n_indices(&self) -> usize { 6 * self.n_quads() } pub fn n_triangles(&self) -> usize { 2 * self.n_quads() } pub fn base_vtx(&self) -> usize { 4 * self.lo } pub fn base_idx(&self) -> usize { 6 * self.lo } } pub struct DrawCallIterator<'a> { /// Source batch: &'a Batch, /// Index of next quad ix: usize, } impl<'a> DrawCallIterator<'a> { fn from_batch(batch: &'a Batch) -> Self { Self { batch, ix: 0 } } } impl<'a> Iterator for DrawCallIterator<'a> { type Item = DrawCall; fn next(&mut self) -> Option { if self.ix >= self.batch.n_quads { return None; } let lo = self.ix; let tex = self.batch.track[lo]; for hi in lo..self.batch.n_quads { let new_tex = self.batch.track[hi]; if new_tex != tex { self.ix = hi; return Some(DrawCall { lo, hi, tex }); } } let hi = self.batch.n_quads; self.ix = hi; return Some(DrawCall { lo, hi, tex }); } } /// Batches draw calls pub struct Batcher { batch: Batch, shader: Shader2d, } impl Batcher { pub fn new(device: &fna3d::Device, shader: Shader2d) -> Result { Ok(Self { batch: Batch::new(device)?, shader, }) } pub fn next_quad_mut(&mut self) -> &mut QuadData { self.flush_if_satured(); unsafe { self.batch.next_quad_mut() } } pub fn push_quad(&mut self, quad: &QuadData, tex: *mut fna3d::Texture) { self.flush_if_satured(); unsafe { self.batch.push_quad(quad, tex); } } fn flush_if_satured(&mut self) { if self.batch.n_quads >= self.batch.quads.len() { self.flush(); } } pub fn flush(&mut self) { if self.batch.n_quads == 0 { return; } // we have to update the shader's orthographic offcenter matrix in real application where we // can resize window self.shader.apply_to_device(); // upload the CPU vertices to the GPU vertices self.batch.device.set_vertex_buffer_data( self.batch.vbuf, 0, // vertex offset &self.batch.quads[0..self.batch.n_quads], fna3d::SetDataOptions::None, ); for call in self.batch.draw_calls() { // remove this line in real applications println!("draw call: {:?}", call); self.draw(&call); } self.batch.n_quads = 0; } fn draw(&self, call: &DrawCall) { let device = &self.batch.device; device.verify_sampler(0, call.tex, &fna3d::SamplerState::default()); device.apply_vertex_buffer_bindings(&[self.batch.vbind], true, call.base_vtx() as u32); device.draw_indexed_primitives( fna3d::PrimitiveType::TriangleList, call.base_vtx() as u32, 0, call.n_verts() as u32, call.base_idx() as u32, call.n_triangles() as u32, self.batch.ibuf, fna3d::IndexElementSize::Bits16, ); } }