1use std::collections::HashMap;
2
3use bytemuck::{Pod, Zeroable};
4use wgpu::util::DeviceExt;
5
6use crate::color::Color;
7use crate::target::Targets;
8use crate::texture::{TextureId, TextureRegistry};
9
10#[repr(C)]
11#[derive(Copy, Clone, Pod, Zeroable)]
12struct QuadVertex {
13 pos: [f32; 2],
14 uv: [f32; 2],
15}
16
17const QUAD_VERTS: &[QuadVertex] = &[
18 QuadVertex {
19 pos: [0.0, 0.0],
20 uv: [0.0, 0.0],
21 },
22 QuadVertex {
23 pos: [1.0, 0.0],
24 uv: [1.0, 0.0],
25 },
26 QuadVertex {
27 pos: [1.0, 1.0],
28 uv: [1.0, 1.0],
29 },
30 QuadVertex {
31 pos: [0.0, 1.0],
32 uv: [0.0, 1.0],
33 },
34];
35const QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
36
37#[repr(C)]
38#[derive(Copy, Clone, Pod, Zeroable, Debug)]
39pub struct SpriteInstance {
40 pub pos: [f32; 2],
41 pub size: [f32; 2],
42 pub uv_min: [f32; 2],
43 pub uv_max: [f32; 2],
44 pub color: [f32; 4],
45 pub rotation: f32,
46 pub _pad: [f32; 3],
49}
50
51const PIXEL_REGION: f32 = 1.0;
53
54impl SpriteInstance {
55 pub fn at(pos: [f32; 2], size: [f32; 2]) -> Self {
56 Self {
57 pos,
58 size,
59 uv_min: [0.0, 0.0],
60 uv_max: [1.0, 1.0],
61 color: [1.0; 4],
62 rotation: 0.0,
63 _pad: [0.0; 3],
64 }
65 }
66 pub fn with_color(mut self, c: impl Into<Color>) -> Self {
67 self.color = c.into().to_array();
68 self
69 }
70 pub fn with_rotation(mut self, r: f32) -> Self {
71 self.rotation = r;
72 self
73 }
74 pub fn with_uv(mut self, min: [f32; 2], max: [f32; 2]) -> Self {
79 self.uv_min = min;
80 self.uv_max = max;
81 self
82 }
83 pub fn with_region(self, min: [f32; 2], size: [f32; 2], tex_size: [u32; 2]) -> Self {
88 debug_assert!(
89 tex_size[0] > 0 && tex_size[1] > 0,
90 "with_region: zero-sized texture {tex_size:?}"
91 );
92 let [tw, th] = [tex_size[0] as f32, tex_size[1] as f32];
93 self.with_uv(
94 [min[0] / tw, min[1] / th],
95 [(min[0] + size[0]) / tw, (min[1] + size[1]) / th],
96 )
97 }
98 pub fn with_pixel_region(mut self, min: [f32; 2], size: [f32; 2]) -> Self {
104 self.uv_min = min;
105 self.uv_max = [min[0] + size[0], min[1] + size[1]];
106 self._pad[0] = PIXEL_REGION;
107 self
108 }
109
110 pub(crate) fn has_pixel_region(&self) -> bool {
112 self._pad[0] == PIXEL_REGION
113 }
114
115 pub(crate) fn resolve_pixel_region(mut self, tex_size: [u32; 2]) -> Self {
117 let [tw, th] = [tex_size[0].max(1) as f32, tex_size[1].max(1) as f32];
118 self.uv_min = [self.uv_min[0] / tw, self.uv_min[1] / th];
119 self.uv_max = [self.uv_max[0] / tw, self.uv_max[1] / th];
120 self._pad[0] = 0.0;
121 self
122 }
123
124 pub fn flip_x(mut self) -> Self {
127 std::mem::swap(&mut self.uv_min[0], &mut self.uv_max[0]);
128 self
129 }
130 pub fn flip_y(mut self) -> Self {
133 std::mem::swap(&mut self.uv_min[1], &mut self.uv_max[1]);
134 self
135 }
136}
137
138#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
139pub enum BlendMode {
140 Alpha,
141 Additive,
142 Premultiplied,
143}
144
145impl BlendMode {
146 pub(crate) const ALL: [Self; 3] = [Self::Alpha, Self::Additive, Self::Premultiplied];
147
148 pub(crate) fn state(self) -> wgpu::BlendState {
150 match self {
151 Self::Alpha => wgpu::BlendState::ALPHA_BLENDING,
152 Self::Premultiplied => wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
153 Self::Additive => wgpu::BlendState {
154 color: wgpu::BlendComponent {
155 src_factor: wgpu::BlendFactor::SrcAlpha,
156 dst_factor: wgpu::BlendFactor::One,
157 operation: wgpu::BlendOperation::Add,
158 },
159 alpha: wgpu::BlendComponent::OVER,
160 },
161 }
162 }
163
164 pub(crate) fn label(self) -> &'static str {
165 match self {
166 Self::Alpha => "alpha",
167 Self::Additive => "add",
168 Self::Premultiplied => "premul",
169 }
170 }
171}
172
173#[derive(Copy, Clone, PartialEq, Eq, Hash)]
174struct BatchKey {
175 texture: TextureId,
176 layer: i16,
177 blend: BlendMode,
178}
179
180pub(crate) struct SpriteBatcher {
181 quad_vb: wgpu::Buffer,
182 quad_ib: wgpu::Buffer,
183 instance_vb: wgpu::Buffer,
184 instance_capacity: usize,
185 pipelines: HashMap<BlendMode, wgpu::RenderPipeline>,
186 pending: Vec<(BatchKey, SpriteInstance)>,
187}
188
189impl SpriteBatcher {
190 pub fn new(
191 device: &wgpu::Device,
192 surface_format: wgpu::TextureFormat,
193 camera_bgl: &wgpu::BindGroupLayout,
194 texture_bgl: &wgpu::BindGroupLayout,
195 sample_count: u32,
196 depth_format: Option<wgpu::TextureFormat>,
197 ) -> Self {
198 let multisample = crate::target::multisample(sample_count);
199 let depth_stencil = depth_format.map(crate::target::no_write_depth);
200 let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
201 label: Some("sprite.quad_vb"),
202 contents: bytemuck::cast_slice(QUAD_VERTS),
203 usage: wgpu::BufferUsages::VERTEX,
204 });
205 let quad_ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
206 label: Some("sprite.quad_ib"),
207 contents: bytemuck::cast_slice(QUAD_INDICES),
208 usage: wgpu::BufferUsages::INDEX,
209 });
210
211 let instance_capacity = 4096usize;
212 let instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
213 label: Some("sprite.instances"),
214 size: (instance_capacity * size_of::<SpriteInstance>()) as u64,
215 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
216 mapped_at_creation: false,
217 });
218
219 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
220 label: Some("sprite.shader"),
221 source: wgpu::ShaderSource::Wgsl(
222 concat!(include_str!("srgb.wgsl"), include_str!("sprite.wgsl")).into(),
223 ),
224 });
225
226 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
227 label: Some("sprite.layout"),
228 bind_group_layouts: &[Some(camera_bgl), Some(texture_bgl)],
229 immediate_size: 0,
230 });
231
232 let make_pipeline = |blend: wgpu::BlendState, label: &str| {
233 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
234 label: Some(label),
235 layout: Some(&layout),
236 vertex: wgpu::VertexState {
237 module: &shader,
238 entry_point: Some("vs_main"),
239 compilation_options: Default::default(),
240 buffers: &[
241 Some(wgpu::VertexBufferLayout {
242 array_stride: size_of::<QuadVertex>() as u64,
243 step_mode: wgpu::VertexStepMode::Vertex,
244 attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
245 }),
246 Some(wgpu::VertexBufferLayout {
247 array_stride: size_of::<SpriteInstance>() as u64,
248 step_mode: wgpu::VertexStepMode::Instance,
249 attributes: &wgpu::vertex_attr_array![
250 2 => Float32x2,
251 3 => Float32x2,
252 4 => Float32x2,
253 5 => Float32x2,
254 6 => Float32x4,
255 7 => Float32,
256 ],
257 }),
258 ],
259 },
260 fragment: Some(wgpu::FragmentState {
261 module: &shader,
262 entry_point: Some("fs_main"),
263 compilation_options: Default::default(),
264 targets: &[Some(wgpu::ColorTargetState {
265 format: surface_format,
266 blend: Some(blend),
267 write_mask: wgpu::ColorWrites::ALL,
268 })],
269 }),
270 primitive: wgpu::PrimitiveState::default(),
271 depth_stencil: depth_stencil.clone(),
272 multisample,
273 multiview_mask: None,
274 cache: None,
275 })
276 };
277
278 let pipelines = BlendMode::ALL
279 .into_iter()
280 .map(|blend| {
281 let label = format!("sprite.{}", blend.label());
282 (blend, make_pipeline(blend.state(), &label))
283 })
284 .collect();
285
286 Self {
287 quad_vb,
288 quad_ib,
289 instance_vb,
290 instance_capacity,
291 pipelines,
292 pending: Vec::new(),
293 }
294 }
295
296 pub fn draw(&mut self, tex: TextureId, layer: i16, blend: BlendMode, inst: SpriteInstance) {
297 self.pending.push((
298 BatchKey {
299 texture: tex,
300 layer,
301 blend,
302 },
303 inst,
304 ));
305 }
306
307 pub fn collect_layers(&self, out: &mut std::collections::BTreeSet<i16>) {
309 out.extend(self.pending.iter().map(|(k, _)| k.layer));
310 }
311
312 pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
317 if self.pending.is_empty() {
318 return;
319 }
320 self.pending.sort_by(|a, b| {
321 a.0.layer
322 .cmp(&b.0.layer)
323 .then((a.0.blend as u8).cmp(&(b.0.blend as u8)))
324 .then(a.0.texture.0.cmp(&b.0.texture.0))
325 });
326 if self.pending.len() > self.instance_capacity {
327 self.instance_capacity = self.pending.len().next_power_of_two();
328 self.instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
329 label: Some("sprite.instances"),
330 size: (self.instance_capacity * size_of::<SpriteInstance>()) as u64,
331 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
332 mapped_at_creation: false,
333 });
334 }
335 let flat: Vec<SpriteInstance> = self.pending.iter().map(|(_, i)| *i).collect();
336 queue.write_buffer(&self.instance_vb, 0, bytemuck::cast_slice(&flat));
337 }
338
339 pub fn draw_layer(
342 &self,
343 layer: i16,
344 encoder: &mut wgpu::CommandEncoder,
345 targets: &Targets<'_>,
346 camera_bg: &wgpu::BindGroup,
347 textures: &TextureRegistry,
348 ) {
349 let lo = self.pending.partition_point(|(k, _)| k.layer < layer);
351 let hi = self.pending.partition_point(|(k, _)| k.layer <= layer);
352 if lo == hi {
353 return;
354 }
355
356 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
357 label: Some("sprite.pass"),
358 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Load))],
359 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Load),
360 occlusion_query_set: None,
361 timestamp_writes: None,
362 multiview_mask: None,
363 });
364
365 pass.set_bind_group(0, camera_bg, &[]);
366 pass.set_vertex_buffer(0, self.quad_vb.slice(..));
367 pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
368 pass.set_vertex_buffer(1, self.instance_vb.slice(..));
369
370 let mut i = lo;
371 while i < hi {
372 let key = self.pending[i].0;
373 let start = i;
374 while i < hi
375 && self.pending[i].0.blend == key.blend
376 && self.pending[i].0.texture == key.texture
377 {
378 i += 1;
379 }
380 let count = (i - start) as u32;
381 pass.set_pipeline(&self.pipelines[&key.blend]);
382 pass.set_bind_group(1, textures.bind_group(key.texture), &[]);
383 pass.draw_indexed(0..6, 0, (start as u32)..(start as u32 + count));
384 }
385 }
386
387 pub fn clear(&mut self) {
388 self.pending.clear();
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn pixel_region_resolves_like_with_region() {
398 let tex = [64, 32];
399 let px = SpriteInstance::at([0.0; 2], [8.0; 2]).with_pixel_region([16.0, 8.0], [16.0, 8.0]);
400 assert!(px.has_pixel_region());
401 let resolved = px.resolve_pixel_region(tex);
402 let direct =
403 SpriteInstance::at([0.0; 2], [8.0; 2]).with_region([16.0, 8.0], [16.0, 8.0], tex);
404 assert!(!resolved.has_pixel_region());
405 assert_eq!(resolved.uv_min, direct.uv_min);
406 assert_eq!(resolved.uv_max, direct.uv_max);
407 }
408
409 #[test]
410 fn flip_before_resolving_a_pixel_region() {
411 let inst = SpriteInstance::at([0.0; 2], [8.0; 2])
412 .with_pixel_region([0.0, 0.0], [16.0, 16.0])
413 .flip_x()
414 .resolve_pixel_region([32, 32]);
415 assert_eq!(inst.uv_min, [0.5, 0.0]);
416 assert_eq!(inst.uv_max, [0.0, 0.5]);
417 }
418
419 #[test]
420 fn with_region_maps_pixels_to_uvs() {
421 let inst = SpriteInstance::at([0.0, 0.0], [32.0, 16.0]).with_region(
422 [64.0, 32.0],
423 [32.0, 16.0],
424 [256, 128],
425 );
426 assert_eq!(inst.uv_min, [0.25, 0.25]);
427 assert_eq!(inst.uv_max, [0.375, 0.375]);
428 }
429
430 #[test]
431 #[cfg(debug_assertions)]
432 #[should_panic(expected = "zero-sized texture")]
433 fn with_region_rejects_a_zero_sized_texture_in_debug_builds() {
434 let _ =
435 SpriteInstance::at([0.0, 0.0], [1.0, 1.0]).with_region([0.0, 0.0], [1.0, 1.0], [0, 64]);
436 }
437
438 #[test]
439 fn flip_x_swaps_u_bounds_only() {
440 let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0])
441 .with_uv([0.1, 0.2], [0.3, 0.4])
442 .flip_x();
443 assert_eq!(inst.uv_min, [0.3, 0.2]);
444 assert_eq!(inst.uv_max, [0.1, 0.4]);
445 }
446
447 #[test]
448 fn flip_y_swaps_v_bounds_only() {
449 let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0])
450 .with_uv([0.1, 0.2], [0.3, 0.4])
451 .flip_y();
452 assert_eq!(inst.uv_min, [0.1, 0.4]);
453 assert_eq!(inst.uv_max, [0.3, 0.2]);
454 }
455
456 #[test]
457 fn flipping_twice_restores_the_region() {
458 let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0]).with_region(
459 [8.0, 8.0],
460 [16.0, 16.0],
461 [64, 64],
462 );
463 let back = inst.flip_x().flip_y().flip_x().flip_y();
464 assert_eq!(back.uv_min, inst.uv_min);
465 assert_eq!(back.uv_max, inst.uv_max);
466 }
467}