1use std::collections::{HashMap, HashSet};
33use std::sync::{Arc, Mutex};
34
35use bytemuck::{Pod, Zeroable};
36use glam::Mat4;
37use repose_render_wgpu::{CallbackRenderPass, CallbackResources, ScreenDescriptor, WgpuCallback};
38use wgpu::util::DeviceExt;
39
40use super::{SpriteBlend, SpriteInstance};
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
44pub enum TextureFilter {
45 #[default]
47 Nearest,
48 Linear,
49}
50
51#[derive(Clone, Copy, Debug)]
53pub struct BatchDesc {
54 pub layer_size: u32,
56 pub layers: u32,
58 pub filter: TextureFilter,
59 pub uploads_gen: u64,
61}
62
63impl Default for BatchDesc {
64 fn default() -> Self {
65 Self {
66 layer_size: 2048,
67 layers: 4,
68 filter: TextureFilter::Nearest,
69 uploads_gen: 0,
70 }
71 }
72}
73
74#[derive(Clone, Debug)]
77pub struct AtlasUpload {
78 pub page: u32,
79 pub x: u32,
80 pub y: u32,
81 pub w: u32,
82 pub h: u32,
83 pub rgba: Vec<u8>,
84}
85
86impl AtlasUpload {
87 pub fn from_write(write: &repame_atlas::AtlasWrite, rgba: Vec<u8>) -> Self {
91 Self {
92 page: write.page,
93 x: write.x,
94 y: write.y,
95 w: write.w,
96 h: write.h,
97 rgba,
98 }
99 }
100
101 pub fn white(page: u32, x: u32, y: u32) -> Self {
106 Self {
107 page,
108 x,
109 y,
110 w: 1,
111 h: 1,
112 rgba: vec![255, 255, 255, 255],
113 }
114 }
115
116 pub fn white_for(write: &repame_atlas::AtlasWrite) -> Self {
119 Self {
120 page: write.page,
121 x: write.x,
122 y: write.y,
123 w: write.w.max(1),
124 h: write.h.max(1),
125 rgba: vec![255; write.w.max(1) as usize * write.h.max(1) as usize * 4],
126 }
127 }
128}
129
130pub fn instance_rows(
135 center: [f32; 2],
136 size: [f32; 2],
137 rotation: f32,
138 anchor: [f32; 2],
139 flip_x: bool,
140 flip_y: bool,
141) -> ([f32; 4], [f32; 4]) {
142 let (c, s) = (rotation.cos(), rotation.sin());
143 let sx = if flip_x { -size[0] } else { size[0] };
144 let sy = if flip_y { -size[1] } else { size[1] };
145 let (ax, ay) = (0.5 - anchor[0], 0.5 - anchor[1]);
148 (
149 [c * sx, -s * sy, 0.0, center[0] + c * sx * ax - s * sy * ay],
150 [s * sx, c * sy, 0.0, center[1] + s * sx * ax + c * sy * ay],
151 )
152}
153
154pub fn frame_uv(hframes: u32, vframes: u32, frame: u32) -> ([f32; 2], [f32; 2]) {
176 let hf = hframes.max(1);
177 let vf = vframes.max(1);
178 let f = frame.min(hf * vf - 1);
179 let col = f % hf;
180 let row = f / hf;
181 (
182 [col as f32 / hf as f32, row as f32 / vf as f32],
183 [(col + 1) as f32 / hf as f32, (row + 1) as f32 / vf as f32],
184 )
185}
186
187pub fn sprite_aabb(
205 center: [f32; 2],
206 size: [f32; 2],
207 rotation: f32,
208 anchor: [f32; 2],
209 flip_x: bool,
210 flip_y: bool,
211) -> ([f32; 2], [f32; 2]) {
212 let (row0, row1) = instance_rows(center, size, rotation, anchor, flip_x, flip_y);
213 let mut min = [f32::INFINITY, f32::INFINITY];
214 let mut max = [f32::NEG_INFINITY, f32::NEG_INFINITY];
215 for corner in [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]] {
216 let x = row0[0] * corner[0] + row0[1] * corner[1] + row0[3];
217 let y = row1[0] * corner[0] + row1[1] * corner[1] + row1[3];
218 min[0] = min[0].min(x);
219 min[1] = min[1].min(y);
220 max[0] = max[0].max(x);
221 max[1] = max[1].max(y);
222 }
223 (min, max)
224}
225
226pub fn screen_camera(viewport_px: [f32; 2]) -> Mat4 {
231 glam::camera::rh::proj::directx::orthographic(
233 0.0,
234 viewport_px[0].max(1.0),
235 viewport_px[1].max(1.0),
236 0.0,
237 -1000.0,
238 1000.0,
239 )
240}
241
242#[repr(C)]
243#[derive(Clone, Copy, Pod, Zeroable)]
244struct CameraUniform {
245 view_proj: [[f32; 4]; 4],
246}
247
248#[repr(C)]
249#[derive(Clone, Copy, Pod, Zeroable)]
250struct BatchInstance {
251 row0: [f32; 4],
252 row1: [f32; 4],
253 uv_min: [f32; 2],
254 uv_max: [f32; 2],
255 tint: [f32; 4],
256 page: f32,
257 z: f32,
258 flags: u32,
259 texel: f32,
262 light_mask: u32,
264}
265
266const _: () = assert!(size_of::<BatchInstance>() == 84);
272
273#[repr(C)]
276#[derive(Clone, Copy, Pod, Zeroable)]
277struct LightmapUniform {
278 xform: [f32; 4],
280 modulate: [f32; 4],
282 layer: [u32; 4],
284}
285
286const _: () = assert!(size_of::<LightmapUniform>() == 48);
287
288const IDENTITY_LIGHTMAP_LAYERS: u32 = 2;
292
293pub struct SpriteBatch {
301 id: String,
302 desc: BatchDesc,
303 camera: [[f32; 4]; 4],
304 instances: Vec<BatchInstance>,
305 uploads: Vec<AtlasUpload>,
306 light_source: Option<String>,
307 light_modulate: [f32; 3],
308}
309
310impl SpriteBatch {
311 pub fn new(desc: BatchDesc) -> Self {
312 Self::with_id("sprite_batch.default", desc)
313 }
314
315 pub fn with_id(id: impl Into<String>, desc: BatchDesc) -> Self {
316 Self {
317 id: id.into(),
318 desc,
319 camera: Mat4::IDENTITY.to_cols_array_2d(),
320 instances: Vec::new(),
321 uploads: Vec::new(),
322 light_source: None,
323 light_modulate: [1.0, 1.0, 1.0],
324 }
325 }
326
327 pub fn id(&self) -> &str {
328 &self.id
329 }
330
331 pub fn clear(&mut self) {
332 self.instances.clear();
333 self.uploads.clear();
334 }
335
336 pub fn len(&self) -> usize {
337 self.instances.len()
338 }
339
340 pub fn is_empty(&self) -> bool {
341 self.instances.is_empty()
342 }
343
344 pub fn set_camera(&mut self, view_proj: Mat4) {
347 self.camera = view_proj.to_cols_array_2d();
348 }
349
350 pub fn upload(&mut self, upload: AtlasUpload) {
352 self.uploads.push(upload);
353 }
354
355 pub fn extend_uploads(&mut self, uploads: impl IntoIterator<Item = AtlasUpload>) {
357 self.uploads.extend(uploads);
358 }
359
360 pub fn set_light_source(&mut self, id: impl Into<String>) {
369 self.light_source = Some(id.into());
370 }
371
372 pub fn clear_light_source(&mut self) {
374 self.light_source = None;
375 }
376
377 pub fn set_light_modulate(&mut self, rgb: [f32; 3]) {
383 self.light_modulate =
384 rgb.map(|c| if c.is_finite() { c.max(0.0) } else { 1.0 });
385 }
386
387 #[allow(clippy::too_many_arguments)]
389 pub fn push(
390 &mut self,
391 center: [f32; 2],
392 size: [f32; 2],
393 rotation: f32,
394 anchor: [f32; 2],
395 flip_x: bool,
396 flip_y: bool,
397 uv_min: [f32; 2],
398 uv_max: [f32; 2],
399 tint: [f32; 4],
400 page: u32,
401 ) {
402 self.push_blended(
403 center,
404 size,
405 rotation,
406 anchor,
407 flip_x,
408 flip_y,
409 uv_min,
410 uv_max,
411 tint,
412 page,
413 0.0,
414 SpriteBlend::Alpha,
415 );
416 }
417
418 #[allow(clippy::too_many_arguments)]
420 pub fn push_masked(
421 &mut self,
422 center: [f32; 2],
423 size: [f32; 2],
424 rotation: f32,
425 anchor: [f32; 2],
426 flip_x: bool,
427 flip_y: bool,
428 uv_min: [f32; 2],
429 uv_max: [f32; 2],
430 tint: [f32; 4],
431 page: u32,
432 light_mask: u32,
433 ) {
434 self.push_blended_masked(
435 center,
436 size,
437 rotation,
438 anchor,
439 flip_x,
440 flip_y,
441 uv_min,
442 uv_max,
443 tint,
444 page,
445 0.0,
446 SpriteBlend::Alpha,
447 light_mask,
448 );
449 }
450
451 #[allow(clippy::too_many_arguments)]
454 pub fn push_blended(
455 &mut self,
456 center: [f32; 2],
457 size: [f32; 2],
458 rotation: f32,
459 anchor: [f32; 2],
460 flip_x: bool,
461 flip_y: bool,
462 uv_min: [f32; 2],
463 uv_max: [f32; 2],
464 tint: [f32; 4],
465 page: u32,
466 z: f32,
467 blend: SpriteBlend,
468 ) {
469 self.push_blended_masked(
470 center, size, rotation, anchor, flip_x, flip_y, uv_min, uv_max, tint, page, z, blend,
471 1,
472 );
473 }
474
475 #[allow(clippy::too_many_arguments)]
477 pub fn push_blended_masked(
478 &mut self,
479 center: [f32; 2],
480 size: [f32; 2],
481 rotation: f32,
482 anchor: [f32; 2],
483 flip_x: bool,
484 flip_y: bool,
485 uv_min: [f32; 2],
486 uv_max: [f32; 2],
487 tint: [f32; 4],
488 page: u32,
489 z: f32,
490 blend: SpriteBlend,
491 light_mask: u32,
492 ) {
493 let (row0, row1) = instance_rows(center, size, rotation, anchor, flip_x, flip_y);
494 let flags = match blend {
495 SpriteBlend::Alpha => 0,
496 SpriteBlend::Additive => 1,
497 SpriteBlend::Multiply => 2,
498 };
499 self.instances.push(BatchInstance {
500 row0,
501 row1,
502 uv_min,
503 uv_max,
504 tint,
505 page: page as f32,
506 z,
507 flags,
508 texel: 0.5 / self.desc.layer_size.max(1) as f32,
509 light_mask,
510 });
511 }
512
513 pub fn push_sprite(&mut self, s: &SpriteInstance) {
520 self.push_blended_masked(
521 [s.center.x, s.center.y],
522 [s.size.x, s.size.y],
523 s.rotation,
524 [s.anchor.x, s.anchor.y],
525 s.flip_x,
526 s.flip_y,
527 [s.uv_min.x, s.uv_min.y],
528 [s.uv_max.x, s.uv_max.y],
529 super::rgba8(s.color).to_linear(),
530 s.page,
531 s.z,
532 s.blend,
533 s.light_mask,
534 );
535 }
536}
537
538#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
539struct AtlasDescriptor {
540 layer_size: u32,
541 layers: u32,
542 filter: TextureFilter,
543}
544
545#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
546struct AtlasKey {
547 descriptor: AtlasDescriptor,
548 generation: u64,
549}
550
551struct AtlasLayout {
552 tex_layout: wgpu::BindGroupLayout,
553 sampler: wgpu::Sampler,
554}
555
556struct AtlasResources {
557 texture: wgpu::Texture,
558 tex_bind: wgpu::BindGroup,
559 applied_gen: Arc<Mutex<Option<u64>>>,
560 starved_gen: Arc<Mutex<Option<u64>>>,
563}
564
565struct BatchEntry {
566 key: (wgpu::TextureFormat, u32, u32, u32, TextureFilter),
567 pipeline_alpha: wgpu::RenderPipeline,
568 pipeline_multiply: wgpu::RenderPipeline,
569 pipeline_additive: wgpu::RenderPipeline,
570 corners: wgpu::Buffer,
571 instances: wgpu::Buffer,
572 instance_cap: usize,
573 last_alpha: u32,
576 last_multiply_end: u32,
577 last_total: u32,
578 camera: wgpu::Buffer,
579 cam_bind: wgpu::BindGroup,
580 atlas: Arc<AtlasResources>,
581 atlas_key: AtlasKey,
582 light_uniform: wgpu::Buffer,
584 light_bind: Arc<wgpu::BindGroup>,
587 light_key: Option<(String, u64)>,
590 light_starved: bool,
593}
594
595struct BatchResources {
596 batches: HashMap<String, BatchEntry>,
597 atlases: HashMap<AtlasKey, Arc<AtlasResources>>,
598 layouts: HashMap<AtlasDescriptor, Arc<AtlasLayout>>,
599 light_layout: wgpu::BindGroupLayout,
600 light_sampler: wgpu::Sampler,
601 #[allow(dead_code)]
603 identity_lightmap: wgpu::Texture,
604 identity_bind: Arc<wgpu::BindGroup>,
605}
606
607impl BatchResources {
608 fn prune_atlases(&mut self) {
609 let live: HashSet<AtlasKey> = self.batches.values().map(|entry| entry.atlas_key).collect();
610 self.atlases.retain(|key, _| live.contains(key));
611 }
612}
613
614const CORNERS: &[f32] = &[
615 -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, ];
622
623fn sampler_desc(filter: TextureFilter) -> wgpu::SamplerDescriptor<'static> {
624 let f = match filter {
625 TextureFilter::Nearest => wgpu::FilterMode::Nearest,
626 TextureFilter::Linear => wgpu::FilterMode::Linear,
627 };
628 let m = match filter {
629 TextureFilter::Nearest => wgpu::MipmapFilterMode::Nearest,
630 TextureFilter::Linear => wgpu::MipmapFilterMode::Linear,
631 };
632 wgpu::SamplerDescriptor {
633 label: Some("sprite_batch_sampler"),
634 address_mode_u: wgpu::AddressMode::ClampToEdge,
635 address_mode_v: wgpu::AddressMode::ClampToEdge,
636 address_mode_w: wgpu::AddressMode::ClampToEdge,
637 mag_filter: f,
638 min_filter: f,
639 mipmap_filter: m,
640 lod_min_clamp: 0.0,
641 lod_max_clamp: 1.0,
642 compare: None,
643 anisotropy_clamp: 1,
644 border_color: None,
645 }
646}
647
648pub(crate) fn atlas_shape(layers: u32) -> (wgpu::TextureViewDimension, bool) {
656 if layers <= 1 {
657 (wgpu::TextureViewDimension::D2, false)
658 } else {
659 (wgpu::TextureViewDimension::D2Array, true)
660 }
661}
662
663pub(crate) fn sprite_shader(layers: u32) -> &'static str {
664 if atlas_shape(layers).1 {
665 include_str!("../shaders/sprite.wgsl")
666 } else {
667 include_str!("../shaders/sprite_single.wgsl")
668 }
669}
670
671fn create_light_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
674 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
675 label: Some("sprite_batch_light_bgl"),
676 entries: &[
677 wgpu::BindGroupLayoutEntry {
678 binding: 0,
679 visibility: wgpu::ShaderStages::FRAGMENT,
680 ty: wgpu::BindingType::Texture {
681 sample_type: wgpu::TextureSampleType::Float { filterable: true },
682 view_dimension: wgpu::TextureViewDimension::D2Array,
683 multisampled: false,
684 },
685 count: None,
686 },
687 wgpu::BindGroupLayoutEntry {
688 binding: 1,
689 visibility: wgpu::ShaderStages::FRAGMENT,
690 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
691 count: None,
692 },
693 wgpu::BindGroupLayoutEntry {
694 binding: 2,
695 visibility: wgpu::ShaderStages::FRAGMENT,
696 ty: wgpu::BindingType::Buffer {
697 ty: wgpu::BufferBindingType::Uniform,
698 has_dynamic_offset: false,
699 min_binding_size: None,
700 },
701 count: None,
702 },
703 ],
704 })
705}
706
707fn create_identity_lightmap(
711 device: &wgpu::Device,
712 queue: &wgpu::Queue,
713 layout: &wgpu::BindGroupLayout,
714) -> (wgpu::Texture, Arc<wgpu::BindGroup>) {
715 let texture = device.create_texture(&wgpu::TextureDescriptor {
716 label: Some("sprite_batch_identity_lightmap"),
717 size: wgpu::Extent3d {
718 width: 1,
719 height: 1,
720 depth_or_array_layers: IDENTITY_LIGHTMAP_LAYERS,
721 },
722 mip_level_count: 1,
723 sample_count: 1,
724 dimension: wgpu::TextureDimension::D2,
725 format: wgpu::TextureFormat::Rgba8Unorm,
726 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
727 view_formats: &[],
728 });
729 queue.write_texture(
730 wgpu::TexelCopyTextureInfo {
731 texture: &texture,
732 mip_level: 0,
733 origin: wgpu::Origin3d::ZERO,
734 aspect: wgpu::TextureAspect::All,
735 },
736 &[0, 0, 0, 255, 0, 0, 0, 255],
737 wgpu::TexelCopyBufferLayout {
738 offset: 0,
739 bytes_per_row: Some(4),
740 rows_per_image: Some(1),
741 },
742 wgpu::Extent3d {
743 width: 1,
744 height: 1,
745 depth_or_array_layers: IDENTITY_LIGHTMAP_LAYERS,
746 },
747 );
748 let view = texture.create_view(&wgpu::TextureViewDescriptor {
749 dimension: Some(wgpu::TextureViewDimension::D2Array),
750 ..Default::default()
751 });
752 let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
753 label: Some("sprite_batch_identity_light"),
754 contents: bytemuck::bytes_of(&LightmapUniform {
755 xform: [0.0; 4],
756 modulate: [1.0, 1.0, 1.0, 0.0],
757 layer: [0; 4],
758 }),
759 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
760 });
761 let sampler = device.create_sampler(&sampler_desc(TextureFilter::Nearest));
762 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
763 label: Some("sprite_batch_identity_light_bg"),
764 layout,
765 entries: &[
766 wgpu::BindGroupEntry {
767 binding: 0,
768 resource: wgpu::BindingResource::TextureView(&view),
769 },
770 wgpu::BindGroupEntry {
771 binding: 1,
772 resource: wgpu::BindingResource::Sampler(&sampler),
773 },
774 wgpu::BindGroupEntry {
775 binding: 2,
776 resource: uniform.as_entire_binding(),
777 },
778 ],
779 });
780 (texture, Arc::new(bind))
781}
782
783fn create_atlas_layout(device: &wgpu::Device, filter: TextureFilter, layers: u32) -> AtlasLayout { let (dimension, _) = atlas_shape(layers);
784 let sampler = device.create_sampler(&sampler_desc(filter));
785 let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
786 label: Some("sprite_batch_tex_bgl"),
787 entries: &[
788 wgpu::BindGroupLayoutEntry {
789 binding: 0,
790 visibility: wgpu::ShaderStages::FRAGMENT,
791 ty: wgpu::BindingType::Texture {
792 sample_type: wgpu::TextureSampleType::Float { filterable: true },
793 view_dimension: dimension,
794 multisampled: false,
795 },
796 count: None,
797 },
798 wgpu::BindGroupLayoutEntry {
799 binding: 1,
800 visibility: wgpu::ShaderStages::FRAGMENT,
801 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
802 count: None,
803 },
804 ],
805 });
806 AtlasLayout {
807 tex_layout,
808 sampler,
809 }
810}
811
812fn create_atlas(device: &wgpu::Device, key: AtlasKey, layout: &AtlasLayout) -> AtlasResources {
813 let descriptor = key.descriptor;
814 let texture = device.create_texture(&wgpu::TextureDescriptor {
815 label: Some("sprite_batch_atlas"),
816 size: wgpu::Extent3d {
817 width: descriptor.layer_size,
818 height: descriptor.layer_size,
819 depth_or_array_layers: descriptor.layers,
820 },
821 mip_level_count: 1,
822 sample_count: 1,
823 dimension: wgpu::TextureDimension::D2,
824 format: wgpu::TextureFormat::Rgba8UnormSrgb,
825 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
826 view_formats: &[],
827 });
828 let (dimension, _) = atlas_shape(descriptor.layers);
829 let view = texture.create_view(&wgpu::TextureViewDescriptor {
830 dimension: Some(dimension),
831 ..Default::default()
832 });
833 let tex_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
834 label: Some("sprite_batch_tex_bg"),
835 layout: &layout.tex_layout,
836 entries: &[
837 wgpu::BindGroupEntry {
838 binding: 0,
839 resource: wgpu::BindingResource::TextureView(&view),
840 },
841 wgpu::BindGroupEntry {
842 binding: 1,
843 resource: wgpu::BindingResource::Sampler(&layout.sampler),
844 },
845 ],
846 });
847 AtlasResources {
848 texture,
849 tex_bind,
850 applied_gen: Arc::new(Mutex::new(None)),
851 starved_gen: Arc::new(Mutex::new(None)),
852 }
853}
854
855impl SpriteBatch {
856 fn ensure_resources(
857 &self,
858 device: &wgpu::Device,
859 queue: &wgpu::Queue,
860 screen: &ScreenDescriptor,
861 resources: &mut CallbackResources,
862 ) {
863 let descriptor = AtlasDescriptor {
864 layer_size: self.desc.layer_size,
865 layers: self.desc.layers,
866 filter: self.desc.filter,
867 };
868 let atlas_key = AtlasKey {
869 descriptor,
870 generation: self.desc.uploads_gen,
871 };
872 let key = (
873 screen.target_format,
874 screen.sample_count,
875 self.desc.layer_size,
876 self.desc.layers,
877 self.desc.filter,
878 );
879 if resources.get::<BatchResources>().is_none() {
880 let light_layout = create_light_layout(device);
881 let (identity_lightmap, identity_bind) =
882 create_identity_lightmap(device, queue, &light_layout);
883 resources.insert(BatchResources {
884 batches: HashMap::new(),
885 atlases: HashMap::new(),
886 layouts: HashMap::new(),
887 light_layout,
888 light_sampler: device.create_sampler(&sampler_desc(TextureFilter::Linear)),
889 identity_lightmap,
890 identity_bind,
891 });
892 }
893 let layout = resources
894 .get_mut::<BatchResources>()
895 .expect("batch resources initialized")
896 .layouts
897 .entry(descriptor)
898 .or_insert_with(|| {
899 Arc::new(create_atlas_layout(
900 device,
901 descriptor.filter,
902 descriptor.layers,
903 ))
904 })
905 .clone();
906 let atlas = resources
907 .get_mut::<BatchResources>()
908 .expect("batch resources initialized")
909 .atlases
910 .entry(atlas_key)
911 .or_insert_with(|| Arc::new(create_atlas(device, atlas_key, &layout)))
912 .clone();
913 let needs = resources
914 .get::<BatchResources>()
915 .expect("batch resources initialized")
916 .batches
917 .get(self.id.as_str())
918 .is_none_or(|entry| entry.key != key);
919 if !needs {
920 if let Some(all) = resources.get_mut::<BatchResources>()
921 && let Some(entry) = all.batches.get_mut(self.id.as_str())
922 {
923 entry.atlas = atlas;
924 entry.atlas_key = atlas_key;
925 all.prune_atlases();
926 }
927 return;
928 }
929 if resources
930 .get::<BatchResources>()
931 .is_some_and(|all| all.batches.contains_key(self.id.as_str()))
932 {
933 log::warn!(
934 "sprite_batch[{}]: rebuilding pipeline for format/sample/descriptor change",
935 self.id
936 );
937 }
938 let light_layout = resources
939 .get::<BatchResources>()
940 .expect("batch resources initialized")
941 .light_layout
942 .clone();
943 let light_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
944 label: Some("sprite_batch_light_uniforms"),
945 contents: bytemuck::bytes_of(&LightmapUniform {
946 xform: [0.0; 4],
947 modulate: [1.0, 1.0, 1.0, 0.0],
948 layer: [0; 4],
949 }),
950 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
951 });
952 let light_bind = resources
953 .get::<BatchResources>()
954 .expect("batch resources initialized")
955 .identity_bind
956 .clone();
957 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
958 label: Some("sprite_batch"),
959 source: wgpu::ShaderSource::Wgsl(sprite_shader(self.desc.layers).into()),
960 });
961 let corners = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
962 label: Some("sprite_batch_corners"),
963 contents: bytemuck::cast_slice(CORNERS),
964 usage: wgpu::BufferUsages::VERTEX,
965 });
966 let camera = device.create_buffer(&wgpu::BufferDescriptor {
967 label: Some("sprite_batch_camera"),
968 size: 64,
969 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
970 mapped_at_creation: false,
971 });
972 let instances = device.create_buffer(&wgpu::BufferDescriptor {
973 label: Some("sprite_batch_instances"),
974 size: (self.instances.len().max(1) * size_of::<BatchInstance>()) as u64,
975 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
976 mapped_at_creation: false,
977 });
978 let cam_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
979 label: Some("sprite_batch_cam_bgl"),
980 entries: &[wgpu::BindGroupLayoutEntry {
981 binding: 0,
982 visibility: wgpu::ShaderStages::VERTEX,
983 ty: wgpu::BindingType::Buffer {
984 ty: wgpu::BufferBindingType::Uniform,
985 has_dynamic_offset: false,
986 min_binding_size: None,
987 },
988 count: None,
989 }],
990 });
991 let cam_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
992 label: Some("sprite_batch_cam_bg"),
993 layout: &cam_layout,
994 entries: &[wgpu::BindGroupEntry {
995 binding: 0,
996 resource: camera.as_entire_binding(),
997 }],
998 });
999 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1000 label: Some("sprite_batch_pl"),
1001 bind_group_layouts: &[
1002 Some(&cam_layout),
1003 Some(&layout.tex_layout),
1004 Some(&light_layout),
1005 ],
1006 immediate_size: 0,
1007 });
1008 let vertex_buffers = [
1009 Some(wgpu::VertexBufferLayout {
1010 array_stride: 8,
1011 step_mode: wgpu::VertexStepMode::Vertex,
1012 attributes: &[wgpu::VertexAttribute {
1013 format: wgpu::VertexFormat::Float32x2,
1014 offset: 0,
1015 shader_location: 0,
1016 }],
1017 }),
1018 Some(wgpu::VertexBufferLayout {
1019 array_stride: size_of::<BatchInstance>() as u64,
1020 step_mode: wgpu::VertexStepMode::Instance,
1021 attributes: &[
1022 wgpu::VertexAttribute {
1023 format: wgpu::VertexFormat::Float32x4,
1024 offset: 0,
1025 shader_location: 2,
1026 },
1027 wgpu::VertexAttribute {
1028 format: wgpu::VertexFormat::Float32x4,
1029 offset: 16,
1030 shader_location: 3,
1031 },
1032 wgpu::VertexAttribute {
1033 format: wgpu::VertexFormat::Float32x2,
1034 offset: 32,
1035 shader_location: 4,
1036 },
1037 wgpu::VertexAttribute {
1038 format: wgpu::VertexFormat::Float32x2,
1039 offset: 40,
1040 shader_location: 5,
1041 },
1042 wgpu::VertexAttribute {
1043 format: wgpu::VertexFormat::Float32x4,
1044 offset: 48,
1045 shader_location: 6,
1046 },
1047 wgpu::VertexAttribute {
1048 format: wgpu::VertexFormat::Float32,
1049 offset: 64,
1050 shader_location: 7,
1051 },
1052 wgpu::VertexAttribute {
1053 format: wgpu::VertexFormat::Float32,
1054 offset: 68,
1055 shader_location: 8,
1056 },
1057 wgpu::VertexAttribute {
1058 format: wgpu::VertexFormat::Uint32,
1059 offset: 72,
1060 shader_location: 9,
1061 },
1062 wgpu::VertexAttribute {
1063 format: wgpu::VertexFormat::Float32,
1064 offset: 76,
1065 shader_location: 10,
1066 },
1067 wgpu::VertexAttribute {
1068 format: wgpu::VertexFormat::Uint32,
1069 offset: 80,
1070 shader_location: 11,
1071 },
1072 ],
1073 }),
1074 ];
1075 let mk_pipeline = |label: &'static str, blend: wgpu::BlendState| {
1076 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1077 label: Some(label),
1078 layout: Some(&pipeline_layout),
1079 vertex: wgpu::VertexState {
1080 module: &shader,
1081 entry_point: Some("vs_main"),
1082 buffers: &vertex_buffers,
1083 compilation_options: Default::default(),
1084 },
1085 fragment: Some(wgpu::FragmentState {
1086 module: &shader,
1087 entry_point: Some("fs_main"),
1088 targets: &[Some(wgpu::ColorTargetState {
1089 format: screen.target_format,
1090 blend: Some(blend),
1091 write_mask: wgpu::ColorWrites::ALL,
1092 })],
1093 compilation_options: Default::default(),
1094 }),
1095 primitive: wgpu::PrimitiveState {
1096 topology: wgpu::PrimitiveTopology::TriangleList,
1097 ..Default::default()
1098 },
1099 depth_stencil: Some(wgpu::DepthStencilState {
1100 format: wgpu::TextureFormat::Depth24PlusStencil8,
1101 depth_write_enabled: Some(false),
1102 depth_compare: Some(wgpu::CompareFunction::Always),
1103 stencil: wgpu::StencilState {
1104 front: wgpu::StencilFaceState {
1105 compare: wgpu::CompareFunction::LessEqual,
1106 ..Default::default()
1107 },
1108 back: wgpu::StencilFaceState {
1109 compare: wgpu::CompareFunction::LessEqual,
1110 ..Default::default()
1111 },
1112 ..Default::default()
1113 },
1114 bias: wgpu::DepthBiasState::default(),
1115 }),
1116 multisample: wgpu::MultisampleState {
1117 count: screen.sample_count,
1118 mask: !0,
1119 alpha_to_coverage_enabled: false,
1120 },
1121 multiview_mask: None,
1122 cache: None,
1123 })
1124 };
1125 let pipeline_alpha = mk_pipeline("sprite_batch_pipeline", wgpu::BlendState::ALPHA_BLENDING);
1126 let pipeline_multiply = mk_pipeline(
1127 "sprite_batch_pipeline_mul",
1128 wgpu::BlendState {
1129 color: wgpu::BlendComponent {
1130 src_factor: wgpu::BlendFactor::Dst,
1131 dst_factor: wgpu::BlendFactor::Zero,
1132 operation: wgpu::BlendOperation::Add,
1133 },
1134 alpha: wgpu::BlendComponent::OVER,
1135 },
1136 );
1137 let pipeline_additive = mk_pipeline(
1138 "sprite_batch_pipeline_add",
1139 wgpu::BlendState {
1140 color: wgpu::BlendComponent {
1141 src_factor: wgpu::BlendFactor::SrcAlpha,
1142 dst_factor: wgpu::BlendFactor::One,
1143 operation: wgpu::BlendOperation::Add,
1144 },
1145 alpha: wgpu::BlendComponent {
1146 src_factor: wgpu::BlendFactor::One,
1147 dst_factor: wgpu::BlendFactor::One,
1148 operation: wgpu::BlendOperation::Add,
1149 },
1150 },
1151 );
1152 let entry = BatchEntry {
1153 key,
1154 pipeline_alpha,
1155 pipeline_multiply,
1156 pipeline_additive,
1157 corners,
1158 instances,
1159 instance_cap: self.instances.len().max(1),
1160 last_alpha: 0,
1161 last_multiply_end: 0,
1162 last_total: 0,
1163 camera,
1164 cam_bind,
1165 atlas,
1166 atlas_key,
1167 light_uniform,
1168 light_bind,
1169 light_key: None,
1170 light_starved: false,
1171 };
1172 resources
1173 .get_mut::<BatchResources>()
1174 .expect("batch resources initialized")
1175 .batches
1176 .insert(self.id.clone(), entry);
1177 resources
1178 .get_mut::<BatchResources>()
1179 .expect("batch resources initialized")
1180 .prune_atlases();
1181 }
1182
1183 fn finish_prepare(
1187 &self,
1188 alpha: u32,
1189 multiply_end: u32,
1190 total: u32,
1191 resources: &mut CallbackResources,
1192 ) {
1193 if let Some(all) = resources.get_mut::<BatchResources>()
1194 && let Some(res) = all.batches.get_mut(self.id.as_str())
1195 {
1196 res.last_alpha = alpha;
1197 res.last_multiply_end = multiply_end;
1198 res.last_total = total;
1199 }
1200 }
1201
1202 pub fn resolve_lightmap(
1208 &self,
1209 device: &wgpu::Device,
1210 queue: &wgpu::Queue,
1211 resources: &mut CallbackResources,
1212 ) {
1213 let source = self.light_source.as_deref();
1214 let info = source.and_then(|id| super::lightpass::lookup_lightmap(resources, id));
1215 let Some(all) = resources.get_mut::<BatchResources>() else {
1216 return;
1217 };
1218 let Some(res) = all.batches.get_mut(self.id.as_str()) else {
1219 return;
1220 };
1221 if let Some(id) = source
1222 && info.is_none()
1223 && !res.light_starved
1224 {
1225 res.light_starved = true;
1226 log::warn!(
1227 "sprite_batch[{}]: no light map published by pass '{id}'; drawing unlit. \
1228 The pass must be in the same scene and render into the same target as this batch.",
1229 self.id
1230 );
1231 }
1232 if info.is_some() {
1233 res.light_starved = false;
1234 }
1235 let key = match (&info, source) {
1236 (Some(info), Some(id)) => Some((id.to_string(), info.generation)),
1237 _ => None,
1238 };
1239 if res.light_key != key {
1240 res.light_key = key;
1241 res.light_bind = match &info {
1242 Some(info) => Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
1243 label: Some("sprite_batch_light_bg"),
1244 layout: &all.light_layout,
1245 entries: &[
1246 wgpu::BindGroupEntry {
1247 binding: 0,
1248 resource: wgpu::BindingResource::TextureView(&info.view),
1249 },
1250 wgpu::BindGroupEntry {
1251 binding: 1,
1252 resource: wgpu::BindingResource::Sampler(&all.light_sampler),
1253 },
1254 wgpu::BindGroupEntry {
1255 binding: 2,
1256 resource: res.light_uniform.as_entire_binding(),
1257 },
1258 ],
1259 })),
1260 None => all.identity_bind.clone(),
1261 };
1262 }
1263 let uniform = match &info {
1266 Some(info) => LightmapUniform {
1267 xform: [
1268 1.0 / info.world_rect[2],
1269 1.0 / info.world_rect[3],
1270 -info.world_rect[0] / info.world_rect[2],
1271 -info.world_rect[1] / info.world_rect[3],
1272 ],
1273 modulate: [
1274 self.light_modulate[0],
1275 self.light_modulate[1],
1276 self.light_modulate[2],
1277 0.0,
1278 ],
1279 layer: [info.layer_clamp, 0, 0, 0],
1280 },
1281 None => LightmapUniform {
1282 xform: [0.0; 4],
1283 modulate: [1.0, 1.0, 1.0, 0.0],
1284 layer: [0; 4],
1285 },
1286 };
1287 queue.write_buffer(&res.light_uniform, 0, bytemuck::bytes_of(&uniform));
1288 }
1289
1290 fn blend_group(flags: u32) -> u32 {
1291 match flags {
1292 1 => 2, 2 => 1, _ => 0, }
1296 }
1297
1298 fn sorted_instances(&self) -> (Vec<BatchInstance>, u32, u32) {
1307 let mut v: Vec<(usize, BatchInstance)> =
1308 self.instances.iter().copied().enumerate().collect();
1309 v.sort_by(|(ia, a), (ib, b)| {
1310 Self::blend_group(a.flags)
1311 .cmp(&Self::blend_group(b.flags))
1312 .then_with(|| a.z.total_cmp(&b.z).then_with(|| ia.cmp(ib)))
1313 });
1314 let v: Vec<BatchInstance> = v.into_iter().map(|(_, inst)| inst).collect();
1315 let alpha = v
1316 .iter()
1317 .take_while(|i| Self::blend_group(i.flags) == 0)
1318 .count() as u32;
1319 let multiply_end = alpha
1320 + v[alpha as usize..]
1321 .iter()
1322 .take_while(|i| Self::blend_group(i.flags) == 1)
1323 .count() as u32;
1324 (v, alpha, multiply_end)
1325 }
1326
1327 pub fn prepare_with_uploads(
1330 &self,
1331 device: &wgpu::Device,
1332 queue: &wgpu::Queue,
1333 _encoder: &mut wgpu::CommandEncoder,
1334 screen: &ScreenDescriptor,
1335 resources: &mut CallbackResources,
1336 uploads: &[AtlasUpload],
1337 ) -> Vec<wgpu::CommandBuffer> {
1338 self.ensure_resources(device, queue, screen, resources);
1339 let (sorted, alpha, multiply_end) = self.sorted_instances();
1340 let total = sorted.len() as u32;
1341 let Some(all) = resources.get_mut::<BatchResources>() else {
1342 return Vec::new();
1343 };
1344 let Some(res) = all.batches.get_mut(self.id.as_str()) else {
1345 return Vec::new();
1346 };
1347
1348 if sorted.len() > res.instance_cap {
1349 let new_cap = sorted.len().next_power_of_two().max(64);
1350 res.instances = device.create_buffer(&wgpu::BufferDescriptor {
1351 label: Some("sprite_batch_instances"),
1352 size: (new_cap * size_of::<BatchInstance>()) as u64,
1353 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1354 mapped_at_creation: false,
1355 });
1356 res.instance_cap = new_cap;
1357 }
1358 queue.write_buffer(
1359 &res.camera,
1360 0,
1361 bytemuck::cast_slice(&[CameraUniform {
1362 view_proj: self.camera,
1363 }]),
1364 );
1365 if !sorted.is_empty() {
1366 queue.write_buffer(&res.instances, 0, bytemuck::cast_slice(&sorted));
1367 }
1368 let atlas = res.atlas.clone();
1369 let atlas_behind = atlas
1372 .applied_gen
1373 .lock()
1374 .map(|generation| *generation != Some(self.desc.uploads_gen))
1375 .unwrap_or(true);
1376 if atlas_behind && uploads.is_empty() {
1377 self.report_atlas_starved(&atlas);
1378 self.finish_prepare(alpha, multiply_end, total, resources);
1379 return Vec::new();
1380 }
1381 let mut wrote_any = false;
1382 for up in uploads {
1383 if !atlas_behind {
1384 continue;
1385 }
1386 let expected = up.w as usize * up.h as usize * 4;
1387 if up.page >= self.desc.layers
1388 || up.x + up.w > self.desc.layer_size
1389 || up.y + up.h > self.desc.layer_size
1390 || up.rgba.len() != expected
1391 {
1392 log::warn!(
1393 "sprite_batch: dropping out-of-range upload page={} {}x{}+{}+{} ({} bytes)",
1394 up.page,
1395 up.w,
1396 up.h,
1397 up.x,
1398 up.y,
1399 up.rgba.len()
1400 );
1401 continue;
1402 }
1403 queue.write_texture(
1404 wgpu::TexelCopyTextureInfo {
1405 texture: &atlas.texture,
1406 mip_level: 0,
1407 origin: wgpu::Origin3d {
1408 x: up.x,
1409 y: up.y,
1410 z: up.page,
1411 },
1412 aspect: wgpu::TextureAspect::All,
1413 },
1414 &up.rgba,
1415 wgpu::TexelCopyBufferLayout {
1416 offset: 0,
1417 bytes_per_row: Some(up.w * 4),
1418 rows_per_image: Some(up.h),
1419 },
1420 wgpu::Extent3d {
1421 width: up.w,
1422 height: up.h,
1423 depth_or_array_layers: 1,
1424 },
1425 );
1426 wrote_any = true;
1427 }
1428 if atlas_behind
1429 && wrote_any
1430 && let Ok(mut generation) = atlas.applied_gen.lock()
1431 {
1432 *generation = Some(self.desc.uploads_gen);
1433 }
1434 self.finish_prepare(alpha, multiply_end, total, resources);
1435 Vec::new()
1436 }
1437
1438 fn report_atlas_starved(&self, atlas: &AtlasResources) {
1441 let Ok(mut reported) = atlas.starved_gen.lock() else {
1442 return;
1443 };
1444 if *reported == Some(self.desc.uploads_gen) {
1445 return;
1446 }
1447 *reported = Some(self.desc.uploads_gen);
1448 log::error!(
1449 "sprite_batch[{}]: atlas generation {} is unfilled and no uploads arrived, \
1450 drawing nothing; pass that generation's uploads on every frame until \
1451 the batch applies them, because the first frame carrying them may be \
1452 built before the view is prepared and then discarded",
1453 self.id,
1454 self.desc.uploads_gen
1455 );
1456 }
1457}
1458
1459trait BatchRenderPass {
1460 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]);
1461 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>);
1462 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline);
1463 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>);
1464}
1465
1466impl BatchRenderPass for wgpu::RenderPass<'_> {
1467 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
1468 self.set_bind_group(index, bind_group, offsets);
1469 }
1470
1471 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>) {
1472 self.set_vertex_buffer(slot, buffer);
1473 }
1474
1475 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
1476 self.set_pipeline(pipeline);
1477 }
1478
1479 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
1480 self.draw(vertices, instances);
1481 }
1482}
1483
1484impl BatchRenderPass for CallbackRenderPass<'_, '_> {
1485 fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
1486 self.set_bind_group(index, bind_group, offsets);
1487 }
1488
1489 fn set_vertex_buffer(&mut self, slot: u32, buffer: wgpu::BufferSlice<'_>) {
1490 self.set_vertex_buffer(slot, buffer);
1491 }
1492
1493 fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
1494 self.set_pipeline(pipeline);
1495 }
1496
1497 fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
1498 self.draw(vertices, instances);
1499 }
1500}
1501
1502fn draw_batch_with_pass<P: BatchRenderPass>(
1503 id: &str,
1504 rpass: &mut P,
1505 resources: &CallbackResources,
1506) {
1507 let Some(all) = resources.get::<BatchResources>() else {
1508 return;
1509 };
1510 let Some(res) = all.batches.get(id) else {
1511 return;
1512 };
1513 if res.last_total == 0 {
1514 return;
1515 }
1516 rpass.set_bind_group(0, &res.cam_bind, &[]);
1517 rpass.set_bind_group(1, &res.atlas.tex_bind, &[]);
1518 rpass.set_bind_group(2, res.light_bind.as_ref(), &[]);
1519 rpass.set_vertex_buffer(0, res.corners.slice(..));
1520 rpass.set_vertex_buffer(1, res.instances.slice(..));
1521 if res.last_alpha > 0 {
1522 rpass.set_pipeline(&res.pipeline_alpha);
1523 rpass.draw(0..6, 0..res.last_alpha);
1524 }
1525 if res.last_multiply_end > res.last_alpha {
1526 rpass.set_pipeline(&res.pipeline_multiply);
1527 rpass.draw(0..6, res.last_alpha..res.last_multiply_end);
1528 }
1529 if res.last_total > res.last_multiply_end {
1530 rpass.set_pipeline(&res.pipeline_additive);
1531 rpass.draw(0..6, res.last_multiply_end..res.last_total);
1532 }
1533}
1534
1535pub fn draw_batch_with_id(
1538 id: &str,
1539 rpass: &mut wgpu::RenderPass<'_>,
1540 resources: &CallbackResources,
1541) {
1542 draw_batch_with_pass(id, rpass, resources);
1543}
1544
1545pub(crate) fn draw_batch_with_id_callback(
1546 id: &str,
1547 rpass: &mut CallbackRenderPass<'_, '_>,
1548 resources: &CallbackResources,
1549) {
1550 draw_batch_with_pass(id, rpass, resources);
1551}
1552
1553pub fn draw_batch(rpass: &mut wgpu::RenderPass<'_>, resources: &CallbackResources) {
1555 draw_batch_with_id("sprite_batch.default", rpass, resources);
1556}
1557
1558impl WgpuCallback for SpriteBatch {
1559 fn resource_key(&self) -> Option<&str> {
1560 Some(super::lightpass::SHARED_CALLBACK_KEY)
1561 }
1562
1563 fn resource_type_id(&self) -> std::any::TypeId {
1564 std::any::TypeId::of::<super::lightpass::LightScope>()
1565 }
1566
1567 fn prepare(
1568 &self,
1569 device: &wgpu::Device,
1570 queue: &wgpu::Queue,
1571 encoder: &mut wgpu::CommandEncoder,
1572 screen: &ScreenDescriptor,
1573 resources: &mut CallbackResources,
1574 ) -> Vec<wgpu::CommandBuffer> {
1575 self.prepare_with_uploads(device, queue, encoder, screen, resources, &self.uploads)
1576 }
1577
1578 fn finish_prepare(
1579 &self,
1580 device: &wgpu::Device,
1581 queue: &wgpu::Queue,
1582 _encoder: &mut wgpu::CommandEncoder,
1583 _screen: &ScreenDescriptor,
1584 resources: &mut CallbackResources,
1585 ) -> Vec<wgpu::CommandBuffer> {
1586 self.resolve_lightmap(device, queue, resources);
1587 Vec::new()
1588 }
1589
1590 fn paint(
1591 &self,
1592 _info: repose_core::PaintCallbackInfo,
1593 rpass: &mut CallbackRenderPass<'_, '_>,
1594 resources: &CallbackResources,
1595 ) {
1596 draw_batch_with_id_callback(self.id.as_str(), rpass, resources);
1597 }
1598}
1599
1600pub struct BatchFrame {
1605 batch: SpriteBatch,
1606 uploads: std::sync::Arc<[AtlasUpload]>,
1607}
1608
1609impl BatchFrame {
1610 pub fn new(batch: SpriteBatch, uploads: std::sync::Arc<[AtlasUpload]>) -> Self {
1611 Self { batch, uploads }
1612 }
1613
1614 pub fn node(self, rect: repose_core::Rect) -> repose_core::SceneNode {
1615 repose_core::SceneNode::Callback {
1616 rect,
1617 payload: repose_render_wgpu::Callback::new(self),
1618 }
1619 }
1620}
1621
1622impl WgpuCallback for BatchFrame {
1623 fn resource_key(&self) -> Option<&str> {
1624 Some(super::lightpass::SHARED_CALLBACK_KEY)
1625 }
1626
1627 fn resource_type_id(&self) -> std::any::TypeId {
1628 std::any::TypeId::of::<super::lightpass::LightScope>()
1629 }
1630
1631 fn prepare(
1632 &self,
1633 device: &wgpu::Device,
1634 queue: &wgpu::Queue,
1635 encoder: &mut wgpu::CommandEncoder,
1636 screen: &ScreenDescriptor,
1637 resources: &mut CallbackResources,
1638 ) -> Vec<wgpu::CommandBuffer> {
1639 self.batch
1640 .prepare_with_uploads(device, queue, encoder, screen, resources, &self.uploads)
1641 }
1642
1643 fn finish_prepare(
1644 &self,
1645 device: &wgpu::Device,
1646 queue: &wgpu::Queue,
1647 _encoder: &mut wgpu::CommandEncoder,
1648 _screen: &ScreenDescriptor,
1649 resources: &mut CallbackResources,
1650 ) -> Vec<wgpu::CommandBuffer> {
1651 self.batch.resolve_lightmap(device, queue, resources);
1652 Vec::new()
1653 }
1654
1655 fn paint(
1656 &self,
1657 _info: repose_core::PaintCallbackInfo,
1658 rpass: &mut CallbackRenderPass<'_, '_>,
1659 resources: &CallbackResources,
1660 ) {
1661 draw_batch_with_id_callback(&self.batch.id, rpass, resources);
1662 }
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667 use super::*;
1668
1669 fn apply(rows: ([f32; 4], [f32; 4]), corner: [f32; 2]) -> [f32; 2] {
1670 let (r0, r1) = rows;
1671 [
1672 r0[0] * corner[0] + r0[1] * corner[1] + r0[3],
1673 r1[0] * corner[0] + r1[1] * corner[1] + r1[3],
1674 ]
1675 }
1676
1677 #[test]
1678 fn default_rows_match_canvas_top_left() {
1679 let rows = instance_rows([400.0, 300.0], [64.0, 80.0], 0.0, [0.5, 0.5], false, false);
1680 let tl = apply(rows, [-0.5, -0.5]);
1681 let br = apply(rows, [0.5, 0.5]);
1682 assert_eq!(tl, [368.0, 260.0]);
1683 assert_eq!(br, [432.0, 340.0]);
1684 }
1685
1686 #[test]
1687 fn frame_uv_indexes_row_major() {
1688 let (mn, mx) = frame_uv(4, 2, 5);
1690 assert_eq!(mn, [0.25, 0.5]);
1691 assert_eq!(mx, [0.5, 1.0]);
1692 assert_eq!(frame_uv(4, 2, 0).0, [0.0, 0.0]);
1694 assert_eq!(frame_uv(4, 2, 99), frame_uv(4, 2, 7));
1695 assert_eq!(frame_uv(0, 0, 3), ([0.0, 0.0], [1.0, 1.0]));
1696 }
1697
1698 #[test]
1699 fn sprite_aabb_matches_unrotated_quad() {
1700 let (mn, mx) = sprite_aabb([400.0, 300.0], [64.0, 80.0], 0.0, [0.5, 0.5], false, false);
1701 assert_eq!(mn, [368.0, 260.0]);
1702 assert_eq!(mx, [432.0, 340.0]);
1703 let (mn, mx) = sprite_aabb(
1705 [0.0, 0.0],
1706 [64.0, 80.0],
1707 std::f32::consts::FRAC_PI_2,
1708 [0.5, 0.5],
1709 false,
1710 false,
1711 );
1712 assert!((mn[0] + 40.0).abs() < 1e-4 && (mn[1] + 32.0).abs() < 1e-4);
1713 assert!((mx[0] - 40.0).abs() < 1e-4 && (mx[1] - 32.0).abs() < 1e-4);
1714 }
1715
1716 #[test]
1717 fn quarter_turn_rotates_footprint() {
1718 let rows = instance_rows(
1719 [0.0, 0.0],
1720 [64.0, 80.0],
1721 std::f32::consts::FRAC_PI_2,
1722 [0.5, 0.5],
1723 false,
1724 false,
1725 );
1726 let p = apply(rows, [-0.5, -0.5]);
1728 assert!(
1729 (p[0] - 40.0).abs() < 1e-4 && (p[1] + 32.0).abs() < 1e-4,
1730 "got {p:?}"
1731 );
1732 }
1733
1734 #[test]
1735 fn top_left_anchor_pins_corner() {
1736 let rows = instance_rows([100.0, 100.0], [64.0, 80.0], 0.0, [0.0, 0.0], false, false);
1737 let tl = apply(rows, [-0.5, -0.5]);
1738 assert_eq!(tl, [100.0, 100.0]);
1739 }
1740
1741 #[test]
1742 fn flip_x_mirrors_around_anchor() {
1743 let plain = instance_rows([0.0, 0.0], [64.0, 8.0], 0.0, [0.5, 0.5], false, false);
1744 let flipped = instance_rows([0.0, 0.0], [64.0, 8.0], 0.0, [0.5, 0.5], true, false);
1745 let a = apply(plain, [-0.5, 0.0]);
1746 let b = apply(flipped, [-0.5, 0.0]);
1747 assert_eq!(a, [-32.0, 0.0]);
1748 assert_eq!(b, [32.0, 0.0]);
1749 }
1750
1751 #[test]
1752 fn screen_camera_is_y_down() {
1753 let cam = screen_camera([800.0, 600.0]);
1754 let top_left = cam.project_point3(glam::Vec3::new(0.0, 0.0, 0.0));
1755 let bottom_right = cam.project_point3(glam::Vec3::new(800.0, 600.0, 0.0));
1756 let center = cam.project_point3(glam::Vec3::new(400.0, 300.0, 0.0));
1757 assert!((top_left.x + 1.0).abs() < 1e-5 && (top_left.y - 1.0).abs() < 1e-5);
1759 assert!((bottom_right.x - 1.0).abs() < 1e-5 && (bottom_right.y + 1.0).abs() < 1e-5);
1760 assert!(center.x.abs() < 1e-5 && center.y.abs() < 1e-5);
1761 }
1762
1763 #[test]
1764 fn push_sprite_bridges_snapshot_fields() {
1765 let mut batch = SpriteBatch::new(BatchDesc::default());
1766 batch.push_sprite(&SpriteInstance {
1767 center: glam::Vec2::new(10.0, 20.0),
1768 size: glam::Vec2::new(4.0, 6.0),
1769 uv_min: glam::Vec2::new(0.25, 0.5),
1770 uv_max: glam::Vec2::new(0.5, 0.75),
1771 color: [1.0, 0.5, 0.25, 0.8],
1772 page: 3,
1773 ..Default::default()
1774 });
1775 assert_eq!(batch.len(), 1);
1776 let inst = batch.instances[0];
1777 assert_eq!(inst.uv_min, [0.25, 0.5]);
1778 assert_eq!(inst.uv_max, [0.5, 0.75]);
1779 assert_eq!(
1781 inst.tint,
1782 crate::rgba8([1.0, 0.5, 0.25, 0.8]).to_linear(),
1783 "tint reaches the batch in linear space"
1784 );
1785 assert_eq!(inst.page, 3.0);
1786 let p = apply((inst.row0, inst.row1), [-0.5, -0.5]);
1788 assert_eq!(p, [8.0, 17.0]);
1789 assert_eq!(inst.light_mask, 1, "sprites default to light layer 0");
1790 }
1791
1792 #[test]
1793 fn z_and_blend_sort_into_ranges() {
1794 use super::super::SpriteBlend;
1795 let mut batch = SpriteBatch::with_id("test.sort", BatchDesc::default());
1796 let quad = |z: f32, blend: SpriteBlend| SpriteInstance {
1797 center: glam::Vec2::new(0.0, 0.0),
1798 size: glam::Vec2::new(1.0, 1.0),
1799 z,
1800 blend,
1801 ..Default::default()
1802 };
1803 batch.push_sprite(&quad(5.0, SpriteBlend::Alpha));
1804 batch.push_sprite(&quad(1.0, SpriteBlend::Additive));
1805 batch.push_sprite(&quad(3.0, SpriteBlend::Multiply));
1806 batch.push_sprite(&quad(2.0, SpriteBlend::Alpha));
1807 batch.push_sprite(&quad(0.0, SpriteBlend::Multiply));
1808 let (sorted, alpha, mul_end) = batch.sorted_instances();
1809 assert_eq!((alpha, mul_end, sorted.len() as u32), (2, 4, 5));
1810 assert_eq!([sorted[0].z, sorted[1].z], [2.0, 5.0]);
1811 assert_eq!([sorted[2].z, sorted[3].z], [0.0, 3.0]);
1812 assert_eq!(sorted[4].z, 1.0);
1813 assert_eq!(sorted[4].flags, 1);
1814 }
1815
1816 #[test]
1821 fn same_z_keeps_push_order_across_pages() {
1822 use super::super::SpriteBlend;
1823 let mut batch = SpriteBatch::with_id("test.pages", BatchDesc::default());
1824 let quad = |page: u32| SpriteInstance {
1825 center: glam::Vec2::new(0.0, 0.0),
1826 size: glam::Vec2::new(1.0, 1.0),
1827 z: 0.0,
1828 blend: SpriteBlend::Alpha,
1829 page,
1830 ..Default::default()
1831 };
1832 batch.push_sprite(&quad(1));
1833 batch.push_sprite(&quad(0));
1834 let (sorted, _, _) = batch.sorted_instances();
1835 assert_eq!([sorted[0].page, sorted[1].page], [1.0, 0.0]);
1836 }
1837
1838 #[test]
1839 fn white_upload_is_1x1_white() {
1840 let up = AtlasUpload::white(2, 5, 7);
1841 assert_eq!((up.page, up.x, up.y, up.w, up.h), (2, 5, 7, 1, 1));
1842 assert_eq!(up.rgba, vec![255, 255, 255, 255]);
1843 }
1844
1845 #[test]
1846 fn single_layer_atlas_draws_tinted_sprites() {
1847 assert_eq!(
1853 crate::batch::atlas_shape(1),
1854 (wgpu::TextureViewDimension::D2, false)
1855 );
1856 assert_eq!(
1857 crate::batch::atlas_shape(4),
1858 (wgpu::TextureViewDimension::D2Array, true)
1859 );
1860 assert!(
1861 crate::batch::sprite_shader(1)
1862 .contains("@group(1) @binding(0) var atlas: texture_2d<f32>")
1863 && !crate::batch::sprite_shader(1)
1864 .contains("@group(1) @binding(0) var atlas: texture_2d_array"),
1865 "single-layer shader samples a 2d atlas"
1866 );
1867 assert!(
1868 crate::batch::sprite_shader(4)
1869 .contains("@group(1) @binding(0) var atlas: texture_2d_array"),
1870 "multi-layer shader samples an array atlas"
1871 );
1872 assert!(
1875 crate::batch::sprite_shader(1)
1876 .contains("@group(2) @binding(0) var lightmap: texture_2d_array<f32>"),
1877 "the light map is sampled as an array regardless of atlas shape"
1878 );
1879 }
1880
1881 #[test]
1886 fn offscreen_batch_draws_oriented_textured_quads() {
1887 use repose_core::{Color, Rect, Scene, SceneNode};
1888 use repose_render_wgpu::{Callback, offscreen::OffscreenRenderer};
1889
1890 let mut renderer = match OffscreenRenderer::new_blocking(256, 256, 1) {
1891 Ok(r) => r,
1892 Err(e) => {
1893 eprintln!("SKIP offscreen batch test (no GPU): {e}");
1894 return;
1895 }
1896 };
1897 let page0 = vec![
1899 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
1903 ];
1904 let page1 = [255, 0, 255, 255].repeat(4);
1906 let mut batch = SpriteBatch::new(BatchDesc {
1907 layer_size: 2,
1908 layers: 2,
1909 filter: TextureFilter::Nearest,
1910 ..Default::default()
1911 });
1912 batch.upload(AtlasUpload {
1913 page: 0,
1914 x: 0,
1915 y: 0,
1916 w: 2,
1917 h: 2,
1918 rgba: page0,
1919 });
1920 batch.upload(AtlasUpload {
1921 page: 1,
1922 x: 0,
1923 y: 0,
1924 w: 2,
1925 h: 2,
1926 rgba: page1,
1927 });
1928 batch.set_camera(screen_camera([256.0, 256.0]));
1929 batch.push(
1931 [128.0, 128.0],
1932 [128.0, 128.0],
1933 0.0,
1934 [0.5, 0.5],
1935 false,
1936 false,
1937 [0.0, 0.0],
1938 [1.0, 1.0],
1939 [1.0, 1.0, 1.0, 1.0],
1940 0,
1941 );
1942 batch.push(
1944 [224.0, 224.0],
1945 [32.0, 32.0],
1946 0.0,
1947 [0.5, 0.5],
1948 false,
1949 false,
1950 [0.0, 0.0],
1951 [1.0, 1.0],
1952 [1.0, 1.0, 1.0, 1.0],
1953 1,
1954 );
1955 let scene = Scene {
1956 clear_color: Color::from_rgba(0, 0, 0, 255),
1957 nodes: vec![SceneNode::Callback {
1958 rect: Rect {
1959 x: 0.0,
1960 y: 0.0,
1961 w: 256.0,
1962 h: 256.0,
1963 },
1964 payload: Callback::new(batch),
1965 }],
1966 };
1967 let px = renderer
1968 .render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
1969 .expect("offscreen render");
1970 let at = |x: u32, y: u32| -> [u8; 4] {
1971 let i = ((y * 256 + x) * 4) as usize;
1972 [px[i], px[i + 1], px[i + 2], px[i + 3]]
1973 };
1974 assert_eq!(at(64, 64), [255, 0, 0, 255], "top-left red");
1976 assert_eq!(at(191, 64), [0, 255, 0, 255], "top-right green");
1977 assert_eq!(at(64, 191), [0, 0, 255, 255], "bottom-left blue");
1978 assert_eq!(at(191, 191), [255, 255, 255, 255], "bottom-right white");
1979 assert_eq!(at(224, 224), [255, 0, 255, 255], "page 1 magenta");
1980 assert_eq!(at(8, 8), [0, 0, 0, 255], "background clear");
1981
1982 let mut flipped = SpriteBatch::new(BatchDesc {
1984 layer_size: 2,
1985 layers: 2,
1986 filter: TextureFilter::Nearest,
1987 ..Default::default()
1988 });
1989 flipped.set_camera(screen_camera([256.0, 256.0]));
1990 flipped.push(
1991 [128.0, 128.0],
1992 [128.0, 128.0],
1993 0.0,
1994 [0.5, 0.5],
1995 true,
1996 false,
1997 [0.0, 0.0],
1998 [1.0, 1.0],
1999 [1.0, 1.0, 1.0, 1.0],
2000 0,
2001 );
2002 let scene = Scene {
2003 clear_color: Color::from_rgba(0, 0, 0, 255),
2004 nodes: vec![SceneNode::Callback {
2005 rect: Rect {
2006 x: 0.0,
2007 y: 0.0,
2008 w: 256.0,
2009 h: 256.0,
2010 },
2011 payload: Callback::new(flipped),
2012 }],
2013 };
2014 let px = renderer
2015 .render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
2016 .expect("offscreen render");
2017 let at = |x: u32, y: u32| -> [u8; 4] {
2018 let i = ((y * 256 + x) * 4) as usize;
2019 [px[i], px[i + 1], px[i + 2], px[i + 3]]
2020 };
2021 assert_eq!(at(64, 64), [0, 255, 0, 255], "flipped top-left green");
2022 assert_eq!(at(191, 191), [0, 0, 255, 255], "flipped bottom-right blue");
2023 }
2024
2025 #[test]
2030 fn distinct_batch_ids_compose_in_one_scene() {
2031 use repose_core::{Color, Rect, Scene};
2032 use repose_render_wgpu::offscreen::OffscreenRenderer;
2033 use std::sync::Arc;
2034
2035 let mut renderer = match OffscreenRenderer::new_blocking(256, 256, 1) {
2036 Ok(r) => r,
2037 Err(e) => {
2038 eprintln!("SKIP batch compose test (no GPU): {e}");
2039 return;
2040 }
2041 };
2042 let page = vec![
2043 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
2047 ];
2048 let uploads: Arc<[AtlasUpload]> = Arc::from(vec![AtlasUpload {
2049 page: 0,
2050 x: 0,
2051 y: 0,
2052 w: 2,
2053 h: 2,
2054 rgba: page,
2055 }]);
2056 let desc = BatchDesc {
2057 layer_size: 2,
2058 layers: 1,
2059 filter: TextureFilter::Nearest,
2060 ..Default::default()
2061 };
2062 let mut left = SpriteBatch::with_id("test.compose.left", desc.clone());
2063 left.set_camera(screen_camera([256.0, 256.0]));
2064 left.push(
2065 [64.0, 128.0],
2066 [64.0, 64.0],
2067 0.0,
2068 [0.5, 0.5],
2069 false,
2070 false,
2071 [0.0, 0.0],
2072 [1.0, 1.0],
2073 [1.0, 1.0, 1.0, 1.0],
2074 0,
2075 );
2076 let mut right = SpriteBatch::with_id("test.compose.right", desc.clone());
2077 right.set_camera(screen_camera([256.0, 256.0]));
2078 right.push(
2079 [192.0, 128.0],
2080 [64.0, 64.0],
2081 0.0,
2082 [0.5, 0.5],
2083 false,
2084 false,
2085 [0.0, 0.0],
2086 [1.0, 1.0],
2087 [1.0, 1.0, 1.0, 1.0],
2088 0,
2089 );
2090 let full = Rect {
2091 x: 0.0,
2092 y: 0.0,
2093 w: 256.0,
2094 h: 256.0,
2095 };
2096 let scene = Scene {
2097 clear_color: Color::from_rgba(0, 0, 0, 255),
2098 nodes: vec![
2099 BatchFrame::new(left, uploads.clone()).node(full),
2100 BatchFrame::new(right, uploads.clone()).node(full),
2101 ],
2102 };
2103 let px = renderer
2104 .render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
2105 .expect("offscreen render");
2106 let at = |x: u32, y: u32| -> [u8; 4] {
2107 let i = ((y * 256 + x) * 4) as usize;
2108 [px[i], px[i + 1], px[i + 2], px[i + 3]]
2109 };
2110 assert_eq!(at(40, 104), [255, 0, 0, 255], "left run red");
2111 assert_eq!(at(168, 104), [255, 0, 0, 255], "right run red");
2112 }
2113}