1use std::collections::{HashMap, HashSet};
26use std::sync::{Arc, Mutex};
27
28use bytemuck::{Pod, Zeroable};
29use glam::Mat4;
30use repose_render_wgpu::{CallbackRenderPass, CallbackResources, ScreenDescriptor, WgpuCallback};
31use wgpu::util::DeviceExt;
32
33use super::{SpriteBlend, SpriteInstance};
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
37pub enum TextureFilter {
38 #[default]
40 Nearest,
41 Linear,
42}
43
44#[derive(Clone, Copy, Debug)]
46pub struct BatchDesc {
47 pub layer_size: u32,
49 pub layers: u32,
51 pub filter: TextureFilter,
52 pub uploads_gen: u64,
54}
55
56impl Default for BatchDesc {
57 fn default() -> Self {
58 Self {
59 layer_size: 2048,
60 layers: 4,
61 filter: TextureFilter::Nearest,
62 uploads_gen: 0,
63 }
64 }
65}
66
67#[derive(Clone, Debug)]
70pub struct AtlasUpload {
71 pub page: u32,
72 pub x: u32,
73 pub y: u32,
74 pub w: u32,
75 pub h: u32,
76 pub rgba: Vec<u8>,
77}
78
79impl AtlasUpload {
80 pub fn from_write(write: &repame_atlas::AtlasWrite, rgba: Vec<u8>) -> Self {
84 Self {
85 page: write.page,
86 x: write.x,
87 y: write.y,
88 w: write.w,
89 h: write.h,
90 rgba,
91 }
92 }
93
94 pub fn white(page: u32, x: u32, y: u32) -> Self {
99 Self {
100 page,
101 x,
102 y,
103 w: 1,
104 h: 1,
105 rgba: vec![255, 255, 255, 255],
106 }
107 }
108
109 pub fn white_for(write: &repame_atlas::AtlasWrite) -> Self {
112 Self {
113 page: write.page,
114 x: write.x,
115 y: write.y,
116 w: write.w.max(1),
117 h: write.h.max(1),
118 rgba: vec![255; write.w.max(1) as usize * write.h.max(1) as usize * 4],
119 }
120 }
121}
122
123pub fn instance_rows(
128 center: [f32; 2],
129 size: [f32; 2],
130 rotation: f32,
131 anchor: [f32; 2],
132 flip_x: bool,
133 flip_y: bool,
134) -> ([f32; 4], [f32; 4]) {
135 let (c, s) = (rotation.cos(), rotation.sin());
136 let sx = if flip_x { -size[0] } else { size[0] };
137 let sy = if flip_y { -size[1] } else { size[1] };
138 let (ax, ay) = (0.5 - anchor[0], 0.5 - anchor[1]);
141 (
142 [c * sx, -s * sy, 0.0, center[0] + c * sx * ax - s * sy * ay],
143 [s * sx, c * sy, 0.0, center[1] + s * sx * ax + c * sy * ay],
144 )
145}
146
147pub fn frame_uv(hframes: u32, vframes: u32, frame: u32) -> ([f32; 2], [f32; 2]) {
169 let hf = hframes.max(1);
170 let vf = vframes.max(1);
171 let f = frame.min(hf * vf - 1);
172 let col = f % hf;
173 let row = f / hf;
174 (
175 [col as f32 / hf as f32, row as f32 / vf as f32],
176 [(col + 1) as f32 / hf as f32, (row + 1) as f32 / vf as f32],
177 )
178}
179
180pub fn sprite_aabb(
198 center: [f32; 2],
199 size: [f32; 2],
200 rotation: f32,
201 anchor: [f32; 2],
202 flip_x: bool,
203 flip_y: bool,
204) -> ([f32; 2], [f32; 2]) {
205 let (row0, row1) = instance_rows(center, size, rotation, anchor, flip_x, flip_y);
206 let mut min = [f32::INFINITY, f32::INFINITY];
207 let mut max = [f32::NEG_INFINITY, f32::NEG_INFINITY];
208 for corner in [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]] {
209 let x = row0[0] * corner[0] + row0[1] * corner[1] + row0[3];
210 let y = row1[0] * corner[0] + row1[1] * corner[1] + row1[3];
211 min[0] = min[0].min(x);
212 min[1] = min[1].min(y);
213 max[0] = max[0].max(x);
214 max[1] = max[1].max(y);
215 }
216 (min, max)
217}
218
219pub fn screen_camera(viewport_px: [f32; 2]) -> Mat4 {
224 glam::camera::rh::proj::directx::orthographic(
226 0.0,
227 viewport_px[0].max(1.0),
228 viewport_px[1].max(1.0),
229 0.0,
230 -1000.0,
231 1000.0,
232 )
233}
234
235#[repr(C)]
236#[derive(Clone, Copy, Pod, Zeroable)]
237struct CameraUniform {
238 view_proj: [[f32; 4]; 4],
239}
240
241#[repr(C)]
242#[derive(Clone, Copy, Pod, Zeroable)]
243struct BatchInstance {
244 row0: [f32; 4],
245 row1: [f32; 4],
246 uv_min: [f32; 2],
247 uv_max: [f32; 2],
248 tint: [f32; 4],
249 page: f32,
250 z: f32,
251 flags: u32,
252 texel: f32,
255}
256
257const _: () = assert!(size_of::<BatchInstance>() == 80);
263
264pub struct SpriteBatch {
272 id: String,
273 desc: BatchDesc,
274 camera: [[f32; 4]; 4],
275 instances: Vec<BatchInstance>,
276 uploads: Vec<AtlasUpload>,
277}
278
279impl SpriteBatch {
280 pub fn new(desc: BatchDesc) -> Self {
281 Self::with_id("sprite_batch.default", desc)
282 }
283
284 pub fn with_id(id: impl Into<String>, desc: BatchDesc) -> Self {
285 Self {
286 id: id.into(),
287 desc,
288 camera: Mat4::IDENTITY.to_cols_array_2d(),
289 instances: Vec::new(),
290 uploads: Vec::new(),
291 }
292 }
293
294 pub fn id(&self) -> &str {
295 &self.id
296 }
297
298 pub fn clear(&mut self) {
299 self.instances.clear();
300 self.uploads.clear();
301 }
302
303 pub fn len(&self) -> usize {
304 self.instances.len()
305 }
306
307 pub fn is_empty(&self) -> bool {
308 self.instances.is_empty()
309 }
310
311 pub fn set_camera(&mut self, view_proj: Mat4) {
314 self.camera = view_proj.to_cols_array_2d();
315 }
316
317 pub fn upload(&mut self, upload: AtlasUpload) {
319 self.uploads.push(upload);
320 }
321
322 pub fn extend_uploads(&mut self, uploads: impl IntoIterator<Item = AtlasUpload>) {
324 self.uploads.extend(uploads);
325 }
326
327 #[allow(clippy::too_many_arguments)]
329 pub fn push(
330 &mut self,
331 center: [f32; 2],
332 size: [f32; 2],
333 rotation: f32,
334 anchor: [f32; 2],
335 flip_x: bool,
336 flip_y: bool,
337 uv_min: [f32; 2],
338 uv_max: [f32; 2],
339 tint: [f32; 4],
340 page: u32,
341 ) {
342 self.push_blended(
343 center,
344 size,
345 rotation,
346 anchor,
347 flip_x,
348 flip_y,
349 uv_min,
350 uv_max,
351 tint,
352 page,
353 0.0,
354 SpriteBlend::Alpha,
355 );
356 }
357
358 #[allow(clippy::too_many_arguments)]
360 pub fn push_blended(
361 &mut self,
362 center: [f32; 2],
363 size: [f32; 2],
364 rotation: f32,
365 anchor: [f32; 2],
366 flip_x: bool,
367 flip_y: bool,
368 uv_min: [f32; 2],
369 uv_max: [f32; 2],
370 tint: [f32; 4],
371 page: u32,
372 z: f32,
373 blend: SpriteBlend,
374 ) {
375 let (row0, row1) = instance_rows(center, size, rotation, anchor, flip_x, flip_y);
376 let flags = match blend {
377 SpriteBlend::Alpha => 0,
378 SpriteBlend::Additive => 1,
379 SpriteBlend::Multiply => 2,
380 };
381 self.instances.push(BatchInstance {
382 row0,
383 row1,
384 uv_min,
385 uv_max,
386 tint,
387 page: page as f32,
388 z,
389 flags,
390 texel: 0.5 / self.desc.layer_size.max(1) as f32,
391 });
392 }
393
394 pub fn push_sprite(&mut self, s: &SpriteInstance) {
396 self.push_blended(
397 [s.center.x, s.center.y],
398 [s.size.x, s.size.y],
399 s.rotation,
400 [s.anchor.x, s.anchor.y],
401 s.flip_x,
402 s.flip_y,
403 [s.uv_min.x, s.uv_min.y],
404 [s.uv_max.x, s.uv_max.y],
405 s.color,
406 s.page,
407 s.z,
408 s.blend,
409 );
410 }
411}
412
413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
414struct AtlasDescriptor {
415 layer_size: u32,
416 layers: u32,
417 filter: TextureFilter,
418}
419
420#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
421struct AtlasKey {
422 descriptor: AtlasDescriptor,
423 generation: u64,
424}
425
426struct AtlasLayout {
427 tex_layout: wgpu::BindGroupLayout,
428 sampler: wgpu::Sampler,
429}
430
431struct AtlasResources {
432 texture: wgpu::Texture,
433 tex_bind: wgpu::BindGroup,
434 applied_gen: Arc<Mutex<Option<u64>>>,
435}
436
437struct BatchEntry {
438 key: (wgpu::TextureFormat, u32, u32, u32, TextureFilter),
439 pipeline_alpha: wgpu::RenderPipeline,
440 pipeline_multiply: wgpu::RenderPipeline,
441 pipeline_additive: wgpu::RenderPipeline,
442 corners: wgpu::Buffer,
443 instances: wgpu::Buffer,
444 instance_cap: usize,
445 last_alpha: u32,
448 last_multiply_end: u32,
449 last_total: u32,
450 camera: wgpu::Buffer,
451 cam_bind: wgpu::BindGroup,
452 atlas: Arc<AtlasResources>,
453 atlas_key: AtlasKey,
454}
455
456struct BatchResources {
457 batches: HashMap<String, BatchEntry>,
458 atlases: HashMap<AtlasKey, Arc<AtlasResources>>,
459 layouts: HashMap<AtlasDescriptor, Arc<AtlasLayout>>,
460}
461
462impl BatchResources {
463 fn prune_atlases(&mut self) {
464 let live: HashSet<AtlasKey> = self.batches.values().map(|entry| entry.atlas_key).collect();
465 self.atlases.retain(|key, _| live.contains(key));
466 }
467}
468
469const CORNERS: &[f32] = &[
470 -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, ];
477
478fn sampler_desc(filter: TextureFilter) -> wgpu::SamplerDescriptor<'static> {
479 let f = match filter {
480 TextureFilter::Nearest => wgpu::FilterMode::Nearest,
481 TextureFilter::Linear => wgpu::FilterMode::Linear,
482 };
483 let m = match filter {
484 TextureFilter::Nearest => wgpu::MipmapFilterMode::Nearest,
485 TextureFilter::Linear => wgpu::MipmapFilterMode::Linear,
486 };
487 wgpu::SamplerDescriptor {
488 label: Some("sprite_batch_sampler"),
489 address_mode_u: wgpu::AddressMode::ClampToEdge,
490 address_mode_v: wgpu::AddressMode::ClampToEdge,
491 address_mode_w: wgpu::AddressMode::ClampToEdge,
492 mag_filter: f,
493 min_filter: f,
494 mipmap_filter: m,
495 lod_min_clamp: 0.0,
496 lod_max_clamp: 1.0,
497 compare: None,
498 anisotropy_clamp: 1,
499 border_color: None,
500 }
501}
502
503fn create_atlas_layout(device: &wgpu::Device, filter: TextureFilter) -> AtlasLayout {
504 let sampler = device.create_sampler(&sampler_desc(filter));
505 let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
506 label: Some("sprite_batch_tex_bgl"),
507 entries: &[
508 wgpu::BindGroupLayoutEntry {
509 binding: 0,
510 visibility: wgpu::ShaderStages::FRAGMENT,
511 ty: wgpu::BindingType::Texture {
512 sample_type: wgpu::TextureSampleType::Float { filterable: true },
513 view_dimension: wgpu::TextureViewDimension::D2Array,
514 multisampled: false,
515 },
516 count: None,
517 },
518 wgpu::BindGroupLayoutEntry {
519 binding: 1,
520 visibility: wgpu::ShaderStages::FRAGMENT,
521 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
522 count: None,
523 },
524 ],
525 });
526 AtlasLayout {
527 tex_layout,
528 sampler,
529 }
530}
531
532fn create_atlas(device: &wgpu::Device, key: AtlasKey, layout: &AtlasLayout) -> AtlasResources {
533 let descriptor = key.descriptor;
534 let texture = device.create_texture(&wgpu::TextureDescriptor {
535 label: Some("sprite_batch_atlas"),
536 size: wgpu::Extent3d {
537 width: descriptor.layer_size,
538 height: descriptor.layer_size,
539 depth_or_array_layers: descriptor.layers,
540 },
541 mip_level_count: 1,
542 sample_count: 1,
543 dimension: wgpu::TextureDimension::D2,
544 format: wgpu::TextureFormat::Rgba8UnormSrgb,
545 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
546 view_formats: &[],
547 });
548 let view = texture.create_view(&wgpu::TextureViewDescriptor {
549 dimension: Some(wgpu::TextureViewDimension::D2Array),
550 ..Default::default()
551 });
552 let tex_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
553 label: Some("sprite_batch_tex_bg"),
554 layout: &layout.tex_layout,
555 entries: &[
556 wgpu::BindGroupEntry {
557 binding: 0,
558 resource: wgpu::BindingResource::TextureView(&view),
559 },
560 wgpu::BindGroupEntry {
561 binding: 1,
562 resource: wgpu::BindingResource::Sampler(&layout.sampler),
563 },
564 ],
565 });
566 AtlasResources {
567 texture,
568 tex_bind,
569 applied_gen: Arc::new(Mutex::new(None)),
570 }
571}
572
573impl SpriteBatch {
574 fn ensure_resources(
575 &self,
576 device: &wgpu::Device,
577 screen: &ScreenDescriptor,
578 resources: &mut CallbackResources,
579 ) {
580 let descriptor = AtlasDescriptor {
581 layer_size: self.desc.layer_size,
582 layers: self.desc.layers,
583 filter: self.desc.filter,
584 };
585 let atlas_key = AtlasKey {
586 descriptor,
587 generation: self.desc.uploads_gen,
588 };
589 let key = (
590 screen.target_format,
591 screen.sample_count,
592 self.desc.layer_size,
593 self.desc.layers,
594 self.desc.filter,
595 );
596 if resources.get::<BatchResources>().is_none() {
597 resources.insert(BatchResources {
598 batches: HashMap::new(),
599 atlases: HashMap::new(),
600 layouts: HashMap::new(),
601 });
602 }
603 let layout = resources
604 .get_mut::<BatchResources>()
605 .expect("batch resources initialized")
606 .layouts
607 .entry(descriptor)
608 .or_insert_with(|| Arc::new(create_atlas_layout(device, descriptor.filter)))
609 .clone();
610 let atlas = resources
611 .get_mut::<BatchResources>()
612 .expect("batch resources initialized")
613 .atlases
614 .entry(atlas_key)
615 .or_insert_with(|| Arc::new(create_atlas(device, atlas_key, &layout)))
616 .clone();
617 let needs = resources
618 .get::<BatchResources>()
619 .expect("batch resources initialized")
620 .batches
621 .get(self.id.as_str())
622 .is_none_or(|entry| entry.key != key);
623 if !needs {
624 if let Some(all) = resources.get_mut::<BatchResources>()
625 && let Some(entry) = all.batches.get_mut(self.id.as_str())
626 {
627 entry.atlas = atlas;
628 entry.atlas_key = atlas_key;
629 all.prune_atlases();
630 }
631 return;
632 }
633 if resources
634 .get::<BatchResources>()
635 .is_some_and(|all| all.batches.contains_key(self.id.as_str()))
636 {
637 log::warn!(
638 "sprite_batch[{}]: rebuilding pipeline for format/sample/descriptor change",
639 self.id
640 );
641 }
642 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
643 label: Some("sprite_batch"),
644 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/sprite.wgsl").into()),
645 });
646 let corners = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
647 label: Some("sprite_batch_corners"),
648 contents: bytemuck::cast_slice(CORNERS),
649 usage: wgpu::BufferUsages::VERTEX,
650 });
651 let camera = device.create_buffer(&wgpu::BufferDescriptor {
652 label: Some("sprite_batch_camera"),
653 size: 64,
654 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
655 mapped_at_creation: false,
656 });
657 let instances = device.create_buffer(&wgpu::BufferDescriptor {
658 label: Some("sprite_batch_instances"),
659 size: (self.instances.len().max(1) * size_of::<BatchInstance>()) as u64,
660 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
661 mapped_at_creation: false,
662 });
663 let cam_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
664 label: Some("sprite_batch_cam_bgl"),
665 entries: &[wgpu::BindGroupLayoutEntry {
666 binding: 0,
667 visibility: wgpu::ShaderStages::VERTEX,
668 ty: wgpu::BindingType::Buffer {
669 ty: wgpu::BufferBindingType::Uniform,
670 has_dynamic_offset: false,
671 min_binding_size: None,
672 },
673 count: None,
674 }],
675 });
676 let cam_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
677 label: Some("sprite_batch_cam_bg"),
678 layout: &cam_layout,
679 entries: &[wgpu::BindGroupEntry {
680 binding: 0,
681 resource: camera.as_entire_binding(),
682 }],
683 });
684 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
685 label: Some("sprite_batch_pl"),
686 bind_group_layouts: &[Some(&cam_layout), Some(&layout.tex_layout)],
687 immediate_size: 0,
688 });
689 let vertex_buffers = [
690 Some(wgpu::VertexBufferLayout {
691 array_stride: 8,
692 step_mode: wgpu::VertexStepMode::Vertex,
693 attributes: &[wgpu::VertexAttribute {
694 format: wgpu::VertexFormat::Float32x2,
695 offset: 0,
696 shader_location: 0,
697 }],
698 }),
699 Some(wgpu::VertexBufferLayout {
700 array_stride: size_of::<BatchInstance>() as u64,
701 step_mode: wgpu::VertexStepMode::Instance,
702 attributes: &[
703 wgpu::VertexAttribute {
704 format: wgpu::VertexFormat::Float32x4,
705 offset: 0,
706 shader_location: 2,
707 },
708 wgpu::VertexAttribute {
709 format: wgpu::VertexFormat::Float32x4,
710 offset: 16,
711 shader_location: 3,
712 },
713 wgpu::VertexAttribute {
714 format: wgpu::VertexFormat::Float32x2,
715 offset: 32,
716 shader_location: 4,
717 },
718 wgpu::VertexAttribute {
719 format: wgpu::VertexFormat::Float32x2,
720 offset: 40,
721 shader_location: 5,
722 },
723 wgpu::VertexAttribute {
724 format: wgpu::VertexFormat::Float32x4,
725 offset: 48,
726 shader_location: 6,
727 },
728 wgpu::VertexAttribute {
729 format: wgpu::VertexFormat::Float32,
730 offset: 64,
731 shader_location: 7,
732 },
733 wgpu::VertexAttribute {
734 format: wgpu::VertexFormat::Float32,
735 offset: 68,
736 shader_location: 8,
737 },
738 wgpu::VertexAttribute {
739 format: wgpu::VertexFormat::Uint32,
740 offset: 72,
741 shader_location: 9,
742 },
743 wgpu::VertexAttribute {
744 format: wgpu::VertexFormat::Float32,
745 offset: 76,
746 shader_location: 10,
747 },
748 ],
749 }),
750 ];
751 let mk_pipeline = |label: &'static str, blend: wgpu::BlendState| {
752 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
753 label: Some(label),
754 layout: Some(&pipeline_layout),
755 vertex: wgpu::VertexState {
756 module: &shader,
757 entry_point: Some("vs_main"),
758 buffers: &vertex_buffers,
759 compilation_options: Default::default(),
760 },
761 fragment: Some(wgpu::FragmentState {
762 module: &shader,
763 entry_point: Some("fs_main"),
764 targets: &[Some(wgpu::ColorTargetState {
765 format: screen.target_format,
766 blend: Some(blend),
767 write_mask: wgpu::ColorWrites::ALL,
768 })],
769 compilation_options: Default::default(),
770 }),
771 primitive: wgpu::PrimitiveState {
772 topology: wgpu::PrimitiveTopology::TriangleList,
773 ..Default::default()
774 },
775 depth_stencil: Some(wgpu::DepthStencilState {
776 format: wgpu::TextureFormat::Depth24PlusStencil8,
777 depth_write_enabled: Some(false),
778 depth_compare: Some(wgpu::CompareFunction::Always),
779 stencil: wgpu::StencilState {
780 front: wgpu::StencilFaceState {
781 compare: wgpu::CompareFunction::LessEqual,
782 ..Default::default()
783 },
784 back: wgpu::StencilFaceState {
785 compare: wgpu::CompareFunction::LessEqual,
786 ..Default::default()
787 },
788 ..Default::default()
789 },
790 bias: wgpu::DepthBiasState::default(),
791 }),
792 multisample: wgpu::MultisampleState {
793 count: screen.sample_count,
794 mask: !0,
795 alpha_to_coverage_enabled: false,
796 },
797 multiview_mask: None,
798 cache: None,
799 })
800 };
801 let pipeline_alpha = mk_pipeline("sprite_batch_pipeline", wgpu::BlendState::ALPHA_BLENDING);
802 let pipeline_multiply = mk_pipeline(
803 "sprite_batch_pipeline_mul",
804 wgpu::BlendState {
805 color: wgpu::BlendComponent {
806 src_factor: wgpu::BlendFactor::Dst,
807 dst_factor: wgpu::BlendFactor::Zero,
808 operation: wgpu::BlendOperation::Add,
809 },
810 alpha: wgpu::BlendComponent::OVER,
811 },
812 );
813 let pipeline_additive = mk_pipeline(
814 "sprite_batch_pipeline_add",
815 wgpu::BlendState {
816 color: wgpu::BlendComponent {
817 src_factor: wgpu::BlendFactor::SrcAlpha,
818 dst_factor: wgpu::BlendFactor::One,
819 operation: wgpu::BlendOperation::Add,
820 },
821 alpha: wgpu::BlendComponent {
822 src_factor: wgpu::BlendFactor::One,
823 dst_factor: wgpu::BlendFactor::One,
824 operation: wgpu::BlendOperation::Add,
825 },
826 },
827 );
828 let entry = BatchEntry {
829 key,
830 pipeline_alpha,
831 pipeline_multiply,
832 pipeline_additive,
833 corners,
834 instances,
835 instance_cap: self.instances.len().max(1),
836 last_alpha: 0,
837 last_multiply_end: 0,
838 last_total: 0,
839 camera,
840 cam_bind,
841 atlas,
842 atlas_key,
843 };
844 resources
845 .get_mut::<BatchResources>()
846 .expect("batch resources initialized")
847 .batches
848 .insert(self.id.clone(), entry);
849 resources
850 .get_mut::<BatchResources>()
851 .expect("batch resources initialized")
852 .prune_atlases();
853 }
854
855 fn finish_prepare(
859 &self,
860 alpha: u32,
861 multiply_end: u32,
862 total: u32,
863 resources: &mut CallbackResources,
864 ) {
865 if let Some(all) = resources.get_mut::<BatchResources>()
866 && let Some(res) = all.batches.get_mut(self.id.as_str())
867 {
868 res.last_alpha = alpha;
869 res.last_multiply_end = multiply_end;
870 res.last_total = total;
871 }
872 }
873
874 fn blend_group(flags: u32) -> u32 {
875 match flags {
876 1 => 2, 2 => 1, _ => 0, }
880 }
881
882 fn sorted_instances(&self) -> (Vec<BatchInstance>, u32, u32) {
891 let mut v: Vec<(usize, BatchInstance)> =
892 self.instances.iter().copied().enumerate().collect();
893 v.sort_by(|(ia, a), (ib, b)| {
894 Self::blend_group(a.flags)
895 .cmp(&Self::blend_group(b.flags))
896 .then_with(|| a.z.total_cmp(&b.z).then_with(|| ia.cmp(ib)))
897 });
898 let v: Vec<BatchInstance> = v.into_iter().map(|(_, inst)| inst).collect();
899 let alpha = v
900 .iter()
901 .take_while(|i| Self::blend_group(i.flags) == 0)
902 .count() as u32;
903 let multiply_end = alpha
904 + v[alpha as usize..]
905 .iter()
906 .take_while(|i| Self::blend_group(i.flags) == 1)
907 .count() as u32;
908 (v, alpha, multiply_end)
909 }
910
911 pub(crate) fn prepare_with_uploads(
912 &self,
913 device: &wgpu::Device,
914 queue: &wgpu::Queue,
915 _encoder: &mut wgpu::CommandEncoder,
916 screen: &ScreenDescriptor,
917 resources: &mut CallbackResources,
918 uploads: &[AtlasUpload],
919 ) -> Vec<wgpu::CommandBuffer> {
920 self.ensure_resources(device, screen, resources);
921 let (sorted, alpha, multiply_end) = self.sorted_instances();
922 let total = sorted.len() as u32;
923 let Some(all) = resources.get_mut::<BatchResources>() else {
924 return Vec::new();
925 };
926 let Some(res) = all.batches.get_mut(self.id.as_str()) else {
927 return Vec::new();
928 };
929
930 if sorted.len() > res.instance_cap {
931 let new_cap = sorted.len().next_power_of_two().max(64);
932 res.instances = device.create_buffer(&wgpu::BufferDescriptor {
933 label: Some("sprite_batch_instances"),
934 size: (new_cap * size_of::<BatchInstance>()) as u64,
935 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
936 mapped_at_creation: false,
937 });
938 res.instance_cap = new_cap;
939 }
940 queue.write_buffer(
941 &res.camera,
942 0,
943 bytemuck::cast_slice(&[CameraUniform {
944 view_proj: self.camera,
945 }]),
946 );
947 if !sorted.is_empty() {
948 queue.write_buffer(&res.instances, 0, bytemuck::cast_slice(&sorted));
949 }
950 let atlas = res.atlas.clone();
951 let fresh_atlas = atlas
952 .applied_gen
953 .lock()
954 .map(|generation| *generation != Some(self.desc.uploads_gen))
955 .unwrap_or(true);
956 if fresh_atlas && uploads.is_empty() {
957 self.finish_prepare(alpha, multiply_end, total, resources);
958 return Vec::new();
959 }
960 let mut wrote_any = false;
961 for up in uploads {
962 if !fresh_atlas {
963 continue;
964 }
965 let expected = up.w as usize * up.h as usize * 4;
966 if up.page >= self.desc.layers
967 || up.x + up.w > self.desc.layer_size
968 || up.y + up.h > self.desc.layer_size
969 || up.rgba.len() != expected
970 {
971 log::warn!(
972 "sprite_batch: dropping out-of-range upload page={} {}x{}+{}+{} ({} bytes)",
973 up.page,
974 up.w,
975 up.h,
976 up.x,
977 up.y,
978 up.rgba.len()
979 );
980 continue;
981 }
982 queue.write_texture(
983 wgpu::TexelCopyTextureInfo {
984 texture: &atlas.texture,
985 mip_level: 0,
986 origin: wgpu::Origin3d {
987 x: up.x,
988 y: up.y,
989 z: up.page,
990 },
991 aspect: wgpu::TextureAspect::All,
992 },
993 &up.rgba,
994 wgpu::TexelCopyBufferLayout {
995 offset: 0,
996 bytes_per_row: Some(up.w * 4),
997 rows_per_image: Some(up.h),
998 },
999 wgpu::Extent3d {
1000 width: up.w,
1001 height: up.h,
1002 depth_or_array_layers: 1,
1003 },
1004 );
1005 wrote_any = true;
1006 }
1007 if fresh_atlas
1008 && wrote_any
1009 && let Ok(mut generation) = atlas.applied_gen.lock()
1010 {
1011 *generation = Some(self.desc.uploads_gen);
1012 }
1013 self.finish_prepare(alpha, multiply_end, total, resources);
1014 Vec::new()
1015 }
1016}
1017
1018trait BatchRenderPass {
1019 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]);
1020 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>);
1021 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline);
1022 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>);
1023}
1024
1025impl BatchRenderPass for wgpu::RenderPass<'_> {
1026 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
1027 self.set_bind_group(index, bind_group, offsets);
1028 }
1029
1030 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>) {
1031 self.set_vertex_buffer(slot, buffer);
1032 }
1033
1034 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
1035 self.set_pipeline(pipeline);
1036 }
1037
1038 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
1039 self.draw(vertices, instances);
1040 }
1041}
1042
1043impl BatchRenderPass for CallbackRenderPass<'_, '_> {
1044 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
1045 self.set_bind_group(index, bind_group, offsets);
1046 }
1047
1048 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>) {
1049 self.set_vertex_buffer(slot, buffer);
1050 }
1051
1052 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
1053 self.set_pipeline(pipeline);
1054 }
1055
1056 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
1057 self.draw(vertices, instances);
1058 }
1059}
1060
1061fn draw_batch_with_pass<P: BatchRenderPass>(
1062 id: &str,
1063 rpass: &mut P,
1064 resources: &CallbackResources,
1065) {
1066 let Some(all) = resources.get::<BatchResources>() else {
1067 return;
1068 };
1069 let Some(res) = all.batches.get(id) else {
1070 return;
1071 };
1072 if res.last_total == 0 {
1073 return;
1074 }
1075 rpass.set_bind_group(0, &res.cam_bind, &[]);
1076 rpass.set_bind_group(1, &res.atlas.tex_bind, &[]);
1077 rpass.set_vertex_buffer(0, res.corners.slice(..));
1078 rpass.set_vertex_buffer(1, res.instances.slice(..));
1079 if res.last_alpha > 0 {
1080 rpass.set_pipeline(&res.pipeline_alpha);
1081 rpass.draw(0..6, 0..res.last_alpha);
1082 }
1083 if res.last_multiply_end > res.last_alpha {
1084 rpass.set_pipeline(&res.pipeline_multiply);
1085 rpass.draw(0..6, res.last_alpha..res.last_multiply_end);
1086 }
1087 if res.last_total > res.last_multiply_end {
1088 rpass.set_pipeline(&res.pipeline_additive);
1089 rpass.draw(0..6, res.last_multiply_end..res.last_total);
1090 }
1091}
1092
1093pub fn draw_batch_with_id(
1096 id: &str,
1097 rpass: &mut wgpu::RenderPass<'_>,
1098 resources: &CallbackResources,
1099) {
1100 draw_batch_with_pass(id, rpass, resources);
1101}
1102
1103pub(crate) fn draw_batch_with_id_callback(
1104 id: &str,
1105 rpass: &mut CallbackRenderPass<'_, '_>,
1106 resources: &CallbackResources,
1107) {
1108 draw_batch_with_pass(id, rpass, resources);
1109}
1110
1111pub fn draw_batch(rpass: &mut wgpu::RenderPass<'_>, resources: &CallbackResources) {
1113 draw_batch_with_id("sprite_batch.default", rpass, resources);
1114}
1115
1116impl WgpuCallback for SpriteBatch {
1117 fn resource_key(&self) -> Option<&str> {
1118 Some("repame.sprite_batch.resources")
1119 }
1120
1121 fn prepare(
1122 &self,
1123 device: &wgpu::Device,
1124 queue: &wgpu::Queue,
1125 encoder: &mut wgpu::CommandEncoder,
1126 screen: &ScreenDescriptor,
1127 resources: &mut CallbackResources,
1128 ) -> Vec<wgpu::CommandBuffer> {
1129 self.prepare_with_uploads(device, queue, encoder, screen, resources, &self.uploads)
1130 }
1131
1132 fn paint(
1133 &self,
1134 _info: repose_core::PaintCallbackInfo,
1135 rpass: &mut CallbackRenderPass<'_, '_>,
1136 resources: &CallbackResources,
1137 ) {
1138 draw_batch_with_id_callback(self.id.as_str(), rpass, resources);
1139 }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use super::*;
1145
1146 fn apply(rows: ([f32; 4], [f32; 4]), corner: [f32; 2]) -> [f32; 2] {
1147 let (r0, r1) = rows;
1148 [
1149 r0[0] * corner[0] + r0[1] * corner[1] + r0[3],
1150 r1[0] * corner[0] + r1[1] * corner[1] + r1[3],
1151 ]
1152 }
1153
1154 #[test]
1155 fn default_rows_match_canvas_top_left() {
1156 let rows = instance_rows([400.0, 300.0], [64.0, 80.0], 0.0, [0.5, 0.5], false, false);
1157 let tl = apply(rows, [-0.5, -0.5]);
1158 let br = apply(rows, [0.5, 0.5]);
1159 assert_eq!(tl, [368.0, 260.0]);
1160 assert_eq!(br, [432.0, 340.0]);
1161 }
1162
1163 #[test]
1164 fn frame_uv_indexes_row_major() {
1165 let (mn, mx) = frame_uv(4, 2, 5);
1167 assert_eq!(mn, [0.25, 0.5]);
1168 assert_eq!(mx, [0.5, 1.0]);
1169 assert_eq!(frame_uv(4, 2, 0).0, [0.0, 0.0]);
1171 assert_eq!(frame_uv(4, 2, 99), frame_uv(4, 2, 7));
1172 assert_eq!(frame_uv(0, 0, 3), ([0.0, 0.0], [1.0, 1.0]));
1173 }
1174
1175 #[test]
1176 fn sprite_aabb_matches_unrotated_quad() {
1177 let (mn, mx) = sprite_aabb([400.0, 300.0], [64.0, 80.0], 0.0, [0.5, 0.5], false, false);
1178 assert_eq!(mn, [368.0, 260.0]);
1179 assert_eq!(mx, [432.0, 340.0]);
1180 let (mn, mx) = sprite_aabb(
1182 [0.0, 0.0],
1183 [64.0, 80.0],
1184 std::f32::consts::FRAC_PI_2,
1185 [0.5, 0.5],
1186 false,
1187 false,
1188 );
1189 assert!((mn[0] + 40.0).abs() < 1e-4 && (mn[1] + 32.0).abs() < 1e-4);
1190 assert!((mx[0] - 40.0).abs() < 1e-4 && (mx[1] - 32.0).abs() < 1e-4);
1191 }
1192
1193 #[test]
1194 fn quarter_turn_rotates_footprint() {
1195 let rows = instance_rows(
1196 [0.0, 0.0],
1197 [64.0, 80.0],
1198 std::f32::consts::FRAC_PI_2,
1199 [0.5, 0.5],
1200 false,
1201 false,
1202 );
1203 let p = apply(rows, [-0.5, -0.5]);
1205 assert!(
1206 (p[0] - 40.0).abs() < 1e-4 && (p[1] + 32.0).abs() < 1e-4,
1207 "got {p:?}"
1208 );
1209 }
1210
1211 #[test]
1212 fn top_left_anchor_pins_corner() {
1213 let rows = instance_rows([100.0, 100.0], [64.0, 80.0], 0.0, [0.0, 0.0], false, false);
1214 let tl = apply(rows, [-0.5, -0.5]);
1215 assert_eq!(tl, [100.0, 100.0]);
1216 }
1217
1218 #[test]
1219 fn flip_x_mirrors_around_anchor() {
1220 let plain = instance_rows([0.0, 0.0], [64.0, 8.0], 0.0, [0.5, 0.5], false, false);
1221 let flipped = instance_rows([0.0, 0.0], [64.0, 8.0], 0.0, [0.5, 0.5], true, false);
1222 let a = apply(plain, [-0.5, 0.0]);
1223 let b = apply(flipped, [-0.5, 0.0]);
1224 assert_eq!(a, [-32.0, 0.0]);
1225 assert_eq!(b, [32.0, 0.0]);
1226 }
1227
1228 #[test]
1229 fn screen_camera_is_y_down() {
1230 let cam = screen_camera([800.0, 600.0]);
1231 let top_left = cam.project_point3(glam::Vec3::new(0.0, 0.0, 0.0));
1232 let bottom_right = cam.project_point3(glam::Vec3::new(800.0, 600.0, 0.0));
1233 let center = cam.project_point3(glam::Vec3::new(400.0, 300.0, 0.0));
1234 assert!((top_left.x + 1.0).abs() < 1e-5 && (top_left.y - 1.0).abs() < 1e-5);
1236 assert!((bottom_right.x - 1.0).abs() < 1e-5 && (bottom_right.y + 1.0).abs() < 1e-5);
1237 assert!(center.x.abs() < 1e-5 && center.y.abs() < 1e-5);
1238 }
1239
1240 #[test]
1241 fn push_sprite_bridges_snapshot_fields() {
1242 let mut batch = SpriteBatch::new(BatchDesc::default());
1243 batch.push_sprite(&SpriteInstance {
1244 center: glam::Vec2::new(10.0, 20.0),
1245 size: glam::Vec2::new(4.0, 6.0),
1246 uv_min: glam::Vec2::new(0.25, 0.5),
1247 uv_max: glam::Vec2::new(0.5, 0.75),
1248 color: [1.0, 0.5, 0.25, 0.8],
1249 page: 3,
1250 ..Default::default()
1251 });
1252 assert_eq!(batch.len(), 1);
1253 let inst = batch.instances[0];
1254 assert_eq!(inst.uv_min, [0.25, 0.5]);
1255 assert_eq!(inst.uv_max, [0.5, 0.75]);
1256 assert_eq!(inst.tint, [1.0, 0.5, 0.25, 0.8]);
1257 assert_eq!(inst.page, 3.0);
1258 let p = apply((inst.row0, inst.row1), [-0.5, -0.5]);
1260 assert_eq!(p, [8.0, 17.0]);
1261 }
1262
1263 #[test]
1264 fn z_and_blend_sort_into_ranges() {
1265 use super::super::SpriteBlend;
1266 let mut batch = SpriteBatch::with_id("test.sort", BatchDesc::default());
1267 let quad = |z: f32, blend: SpriteBlend| SpriteInstance {
1268 center: glam::Vec2::new(0.0, 0.0),
1269 size: glam::Vec2::new(1.0, 1.0),
1270 z,
1271 blend,
1272 ..Default::default()
1273 };
1274 batch.push_sprite(&quad(5.0, SpriteBlend::Alpha));
1275 batch.push_sprite(&quad(1.0, SpriteBlend::Additive));
1276 batch.push_sprite(&quad(3.0, SpriteBlend::Multiply));
1277 batch.push_sprite(&quad(2.0, SpriteBlend::Alpha));
1278 batch.push_sprite(&quad(0.0, SpriteBlend::Multiply));
1279 let (sorted, alpha, mul_end) = batch.sorted_instances();
1280 assert_eq!((alpha, mul_end, sorted.len() as u32), (2, 4, 5));
1281 assert_eq!([sorted[0].z, sorted[1].z], [2.0, 5.0]);
1282 assert_eq!([sorted[2].z, sorted[3].z], [0.0, 3.0]);
1283 assert_eq!(sorted[4].z, 1.0);
1284 assert_eq!(sorted[4].flags, 1);
1285 }
1286
1287 #[test]
1292 fn same_z_keeps_push_order_across_pages() {
1293 use super::super::SpriteBlend;
1294 let mut batch = SpriteBatch::with_id("test.pages", BatchDesc::default());
1295 let quad = |page: u32| SpriteInstance {
1296 center: glam::Vec2::new(0.0, 0.0),
1297 size: glam::Vec2::new(1.0, 1.0),
1298 z: 0.0,
1299 blend: SpriteBlend::Alpha,
1300 page,
1301 ..Default::default()
1302 };
1303 batch.push_sprite(&quad(1));
1304 batch.push_sprite(&quad(0));
1305 let (sorted, _, _) = batch.sorted_instances();
1306 assert_eq!([sorted[0].page, sorted[1].page], [1.0, 0.0]);
1307 }
1308
1309 #[test]
1310 fn white_upload_is_1x1_white() {
1311 let up = AtlasUpload::white(2, 5, 7);
1312 assert_eq!((up.page, up.x, up.y, up.w, up.h), (2, 5, 7, 1, 1));
1313 assert_eq!(up.rgba, vec![255, 255, 255, 255]);
1314 }
1315
1316 #[test]
1321 fn offscreen_batch_draws_oriented_textured_quads() {
1322 use repose_core::{Color, Rect, Scene, SceneNode};
1323 use repose_render_wgpu::{Callback, offscreen::OffscreenRenderer};
1324
1325 let mut renderer = match OffscreenRenderer::new_blocking(256, 256, 1) {
1326 Ok(r) => r,
1327 Err(e) => {
1328 eprintln!("SKIP offscreen batch test (no GPU): {e}");
1329 return;
1330 }
1331 };
1332 let page0 = vec![
1334 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
1338 ];
1339 let page1 = [255, 0, 255, 255].repeat(4);
1341 let mut batch = SpriteBatch::new(BatchDesc {
1342 layer_size: 2,
1343 layers: 2,
1344 filter: TextureFilter::Nearest,
1345 ..Default::default()
1346 });
1347 batch.upload(AtlasUpload {
1348 page: 0,
1349 x: 0,
1350 y: 0,
1351 w: 2,
1352 h: 2,
1353 rgba: page0,
1354 });
1355 batch.upload(AtlasUpload {
1356 page: 1,
1357 x: 0,
1358 y: 0,
1359 w: 2,
1360 h: 2,
1361 rgba: page1,
1362 });
1363 batch.set_camera(screen_camera([256.0, 256.0]));
1364 batch.push(
1366 [128.0, 128.0],
1367 [128.0, 128.0],
1368 0.0,
1369 [0.5, 0.5],
1370 false,
1371 false,
1372 [0.0, 0.0],
1373 [1.0, 1.0],
1374 [1.0, 1.0, 1.0, 1.0],
1375 0,
1376 );
1377 batch.push(
1379 [224.0, 224.0],
1380 [32.0, 32.0],
1381 0.0,
1382 [0.5, 0.5],
1383 false,
1384 false,
1385 [0.0, 0.0],
1386 [1.0, 1.0],
1387 [1.0, 1.0, 1.0, 1.0],
1388 1,
1389 );
1390 let scene = Scene {
1391 clear_color: Color::from_rgba(0, 0, 0, 255),
1392 nodes: vec![SceneNode::Callback {
1393 rect: Rect {
1394 x: 0.0,
1395 y: 0.0,
1396 w: 256.0,
1397 h: 256.0,
1398 },
1399 payload: Callback::new(batch),
1400 }],
1401 };
1402 let px = renderer
1403 .render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
1404 .expect("offscreen render");
1405 let at = |x: u32, y: u32| -> [u8; 4] {
1406 let i = ((y * 256 + x) * 4) as usize;
1407 [px[i], px[i + 1], px[i + 2], px[i + 3]]
1408 };
1409 assert_eq!(at(64, 64), [255, 0, 0, 255], "top-left red");
1411 assert_eq!(at(191, 64), [0, 255, 0, 255], "top-right green");
1412 assert_eq!(at(64, 191), [0, 0, 255, 255], "bottom-left blue");
1413 assert_eq!(at(191, 191), [255, 255, 255, 255], "bottom-right white");
1414 assert_eq!(at(224, 224), [255, 0, 255, 255], "page 1 magenta");
1415 assert_eq!(at(8, 8), [0, 0, 0, 255], "background clear");
1416
1417 let mut flipped = SpriteBatch::new(BatchDesc {
1419 layer_size: 2,
1420 layers: 2,
1421 filter: TextureFilter::Nearest,
1422 ..Default::default()
1423 });
1424 flipped.set_camera(screen_camera([256.0, 256.0]));
1425 flipped.push(
1426 [128.0, 128.0],
1427 [128.0, 128.0],
1428 0.0,
1429 [0.5, 0.5],
1430 true,
1431 false,
1432 [0.0, 0.0],
1433 [1.0, 1.0],
1434 [1.0, 1.0, 1.0, 1.0],
1435 0,
1436 );
1437 let scene = Scene {
1438 clear_color: Color::from_rgba(0, 0, 0, 255),
1439 nodes: vec![SceneNode::Callback {
1440 rect: Rect {
1441 x: 0.0,
1442 y: 0.0,
1443 w: 256.0,
1444 h: 256.0,
1445 },
1446 payload: Callback::new(flipped),
1447 }],
1448 };
1449 let px = renderer
1450 .render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
1451 .expect("offscreen render");
1452 let at = |x: u32, y: u32| -> [u8; 4] {
1453 let i = ((y * 256 + x) * 4) as usize;
1454 [px[i], px[i + 1], px[i + 2], px[i + 3]]
1455 };
1456 assert_eq!(at(64, 64), [0, 255, 0, 255], "flipped top-left green");
1457 assert_eq!(at(191, 191), [0, 0, 255, 255], "flipped bottom-right blue");
1458 }
1459}