1use crate::assets::{loaders::TextureAsset, AssetHandle, AssetServer};
39use crate::core::error::{GoudError, GoudResult};
40use crate::core::math::{Color, Rect, Vec2};
41use crate::ecs::components::{Mat3x3, Sprite, Transform2D};
42use crate::ecs::query::Query;
43use crate::ecs::{Entity, World};
44use crate::libs::graphics::backend::types::{
45 BufferHandle, BufferType, BufferUsage, PrimitiveTopology, ShaderHandle, TextureHandle,
46 VertexAttribute, VertexAttributeType, VertexLayout,
47};
48use crate::libs::graphics::backend::RenderBackend;
49use std::collections::HashMap;
50
51#[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq)]
58pub struct SpriteBatchConfig {
59 pub initial_capacity: usize,
61
62 pub max_batch_size: usize,
64
65 pub enable_z_sorting: bool,
67
68 pub enable_batching: bool,
70}
71
72impl Default for SpriteBatchConfig {
73 fn default() -> Self {
74 Self {
75 initial_capacity: 1024, max_batch_size: 10000, enable_z_sorting: true, enable_batching: true, }
80 }
81}
82
83#[allow(dead_code)] #[repr(C)]
93#[derive(Debug, Clone, Copy)]
94struct SpriteVertex {
95 position: Vec2,
97 tex_coords: Vec2,
99 color: Color,
101}
102
103impl SpriteVertex {
104 #[allow(dead_code)] fn layout() -> VertexLayout {
107 VertexLayout::new(std::mem::size_of::<Self>() as u32)
108 .with_attribute(VertexAttribute {
109 location: 0,
110 attribute_type: VertexAttributeType::Float2,
111 offset: 0,
112 normalized: false,
113 })
114 .with_attribute(VertexAttribute {
115 location: 1,
116 attribute_type: VertexAttributeType::Float2,
117 offset: 8,
118 normalized: false,
119 })
120 .with_attribute(VertexAttribute {
121 location: 2,
122 attribute_type: VertexAttributeType::Float4,
123 offset: 16,
124 normalized: false,
125 })
126 }
127}
128
129#[allow(dead_code)] #[derive(Debug, Clone)]
136struct SpriteInstance {
137 entity: Entity,
139 texture: AssetHandle<TextureAsset>,
141 transform: Mat3x3,
143 color: Color,
145 source_rect: Option<Rect>,
147 size: Vec2,
149 z_layer: f32,
151 flip_x: bool,
153 flip_y: bool,
154}
155
156#[allow(dead_code)] #[derive(Debug)]
163struct SpriteBatchEntry {
164 texture_handle: AssetHandle<TextureAsset>,
166 gpu_texture: Option<TextureHandle>,
168 vertex_start: usize,
170 vertex_count: usize,
172}
173
174#[allow(dead_code)] pub struct SpriteBatch<B: RenderBackend> {
192 backend: B,
194 config: SpriteBatchConfig,
196 vertex_buffer: Option<BufferHandle>,
198 index_buffer: Option<BufferHandle>,
200 vertex_capacity: usize,
202 shader: Option<ShaderHandle>,
204 sprites: Vec<SpriteInstance>,
206 vertices: Vec<SpriteVertex>,
208 batches: Vec<SpriteBatchEntry>,
210 texture_cache: HashMap<AssetHandle<TextureAsset>, TextureHandle>,
212 frame_count: u64,
214}
215
216#[allow(dead_code)] impl<B: RenderBackend> SpriteBatch<B> {
218 pub fn new(backend: B, config: SpriteBatchConfig) -> GoudResult<Self> {
224 Ok(Self {
225 backend,
226 config,
227 vertex_buffer: None,
228 index_buffer: None,
229 vertex_capacity: 0,
230 shader: None,
231 sprites: Vec::with_capacity(config.initial_capacity),
232 vertices: Vec::with_capacity(config.initial_capacity * 4),
233 batches: Vec::with_capacity(128),
234 texture_cache: HashMap::new(),
235 frame_count: 0,
236 })
237 }
238
239 pub fn begin(&mut self) {
243 self.sprites.clear();
244 self.vertices.clear();
245 self.batches.clear();
246 self.frame_count += 1;
247 }
248
249 pub fn end(&mut self) -> GoudResult<()> {
251 Ok(())
254 }
255
256 pub fn draw_sprites(&mut self, world: &World, asset_server: &AssetServer) -> GoudResult<()> {
268 self.gather_sprites(world)?;
270
271 if self.config.enable_z_sorting {
273 self.sort_sprites();
274 }
275
276 self.generate_batches(asset_server)?;
278
279 self.render_batches()?;
281
282 Ok(())
283 }
284
285 fn gather_sprites(&mut self, world: &World) -> GoudResult<()> {
291 self.sprites.clear();
293
294 let query: Query<(Entity, &Sprite, &Transform2D)> = Query::new(world);
297
298 for (entity, sprite, transform) in query.iter(world) {
300 let matrix = transform.matrix();
302
303 let size = if let Some(custom_size) = sprite.custom_size {
305 custom_size
306 } else if let Some(ref source_rect) = sprite.source_rect {
307 Vec2::new(source_rect.width, source_rect.height)
308 } else {
309 Vec2::new(64.0, 64.0)
312 };
313
314 let z_layer = transform.position.y;
317
318 let instance = SpriteInstance {
320 entity,
321 transform: matrix,
322 texture: sprite.texture,
323 color: sprite.color,
324 source_rect: sprite.source_rect,
325 size,
326 flip_x: sprite.flip_x,
327 flip_y: sprite.flip_y,
328 z_layer,
329 };
330
331 self.sprites.push(instance);
332 }
333
334 Ok(())
335 }
336
337 fn sort_sprites(&mut self) {
343 if !self.config.enable_batching {
344 self.sprites.sort_by(|a, b| {
346 a.z_layer
347 .partial_cmp(&b.z_layer)
348 .unwrap_or(std::cmp::Ordering::Equal)
349 });
350 } else {
351 self.sprites.sort_by(|a, b| {
353 match a.z_layer.partial_cmp(&b.z_layer) {
354 Some(std::cmp::Ordering::Equal) | None => {
355 a.texture.cmp(&b.texture)
357 }
358 Some(ord) => ord,
359 }
360 });
361 }
362 }
363
364 fn generate_batches(&mut self, asset_server: &AssetServer) -> GoudResult<()> {
370 if self.sprites.is_empty() {
371 return Ok(());
372 }
373
374 let sprite_count = self.sprites.len();
375 let mut current_texture = self.sprites[0].texture;
376 let mut batch_start = 0;
377
378 let mut batch_ranges = Vec::new();
380
381 for i in 0..sprite_count {
382 let sprite_texture = self.sprites[i].texture;
383
384 let new_batch = sprite_texture != current_texture
386 || (i - batch_start) >= self.config.max_batch_size;
387
388 if new_batch && i > batch_start {
389 batch_ranges.push((current_texture, batch_start, i));
391
392 current_texture = sprite_texture;
394 batch_start = i;
395 }
396 }
397
398 batch_ranges.push((current_texture, batch_start, sprite_count));
400
401 for (texture_handle, start_idx, end_idx) in batch_ranges {
403 self.finalize_batch(texture_handle, start_idx, end_idx, asset_server)?;
404 }
405
406 Ok(())
407 }
408
409 fn finalize_batch(
411 &mut self,
412 texture_handle: AssetHandle<TextureAsset>,
413 start_idx: usize,
414 end_idx: usize,
415 asset_server: &AssetServer,
416 ) -> GoudResult<()> {
417 let vertex_start = self.vertices.len();
418
419 let texture_size = self.get_texture_size(texture_handle, asset_server);
421
422 let sprites_to_process = self.sprites[start_idx..end_idx].to_vec();
424
425 for sprite in sprites_to_process {
427 self.generate_sprite_vertices(&sprite, texture_size)?;
428 }
429
430 let vertex_count = self.vertices.len() - vertex_start;
431
432 let gpu_texture = self.resolve_texture(texture_handle, asset_server)?;
434
435 self.batches.push(SpriteBatchEntry {
437 texture_handle,
438 gpu_texture: Some(gpu_texture),
439 vertex_start,
440 vertex_count,
441 });
442
443 Ok(())
444 }
445
446 fn generate_sprite_vertices(
448 &mut self,
449 sprite: &SpriteInstance,
450 texture_size: Vec2,
451 ) -> GoudResult<()> {
452 let half_size = sprite.size * 0.5;
454 let local_corners = [
455 Vec2::new(-half_size.x, -half_size.y), Vec2::new(half_size.x, -half_size.y), Vec2::new(half_size.x, half_size.y), Vec2::new(-half_size.x, half_size.y), ];
460
461 let uv_rect = if let Some(source) = sprite.source_rect {
463 Rect::new(
465 source.x / texture_size.x,
466 source.y / texture_size.y,
467 source.width / texture_size.x,
468 source.height / texture_size.y,
469 )
470 } else {
471 Rect::new(0.0, 0.0, 1.0, 1.0)
473 };
474
475 let u_min = if sprite.flip_x {
477 uv_rect.x + uv_rect.width
478 } else {
479 uv_rect.x
480 };
481 let u_max = if sprite.flip_x {
482 uv_rect.x
483 } else {
484 uv_rect.x + uv_rect.width
485 };
486 let v_min = if sprite.flip_y {
487 uv_rect.y + uv_rect.height
488 } else {
489 uv_rect.y
490 };
491 let v_max = if sprite.flip_y {
492 uv_rect.y
493 } else {
494 uv_rect.y + uv_rect.height
495 };
496
497 let uv_corners = [
498 Vec2::new(u_min, v_min), Vec2::new(u_max, v_min), Vec2::new(u_max, v_max), Vec2::new(u_min, v_max), ];
503
504 for i in 0..4 {
506 let world_pos = sprite.transform.transform_point(local_corners[i]);
507 self.vertices.push(SpriteVertex {
508 position: world_pos,
509 tex_coords: uv_corners[i],
510 color: sprite.color,
511 });
512 }
513
514 Ok(())
515 }
516
517 fn render_batches(&mut self) -> GoudResult<()> {
523 if self.batches.is_empty() {
524 return Ok(());
525 }
526
527 self.ensure_resources()?;
529
530 self.upload_vertices()?;
532
533 if let Some(shader) = self.shader {
535 self.backend.bind_shader(shader)?;
536 }
538
539 if let Some(vbo) = self.vertex_buffer {
541 self.backend.bind_buffer(vbo)?;
542 self.backend.set_vertex_attributes(&SpriteVertex::layout());
543 }
544
545 if let Some(ibo) = self.index_buffer {
547 self.backend.bind_buffer(ibo)?;
548 }
549
550 for batch in &self.batches {
552 if let Some(gpu_tex) = batch.gpu_texture {
554 self.backend.bind_texture(gpu_tex, 0)?;
555 }
556
557 let sprite_count = batch.vertex_count / 4;
559 let index_start = (batch.vertex_start / 4) * 6;
560 let index_count = sprite_count * 6;
561
562 self.backend.draw_indexed(
564 PrimitiveTopology::Triangles,
565 index_count as u32,
566 index_start,
567 )?;
568 }
569
570 Ok(())
571 }
572
573 fn ensure_resources(&mut self) -> GoudResult<()> {
579 if self.vertex_buffer.is_none() || self.vertices.len() > self.vertex_capacity * 4 {
581 self.create_vertex_buffer()?;
582 }
583
584 if self.index_buffer.is_none() {
586 self.create_index_buffer()?;
587 }
588
589 if self.shader.is_none() {
591 self.create_shader()?;
592 }
593
594 Ok(())
595 }
596
597 fn create_vertex_buffer(&mut self) -> GoudResult<()> {
599 let required_sprites = self.vertices.len().div_ceil(4);
601 let new_capacity = if required_sprites > self.vertex_capacity {
602 (required_sprites * 2).max(self.config.initial_capacity)
603 } else {
604 self.config.initial_capacity
605 };
606
607 let buffer_size = new_capacity * 4 * std::mem::size_of::<SpriteVertex>();
608
609 if let Some(old_buffer) = self.vertex_buffer {
611 let _ = self.backend.destroy_buffer(old_buffer);
612 }
613
614 let empty_data = vec![0u8; buffer_size];
616 let buffer =
617 self.backend
618 .create_buffer(BufferType::Vertex, BufferUsage::Dynamic, &empty_data)?;
619
620 self.vertex_buffer = Some(buffer);
621 self.vertex_capacity = new_capacity;
622
623 Ok(())
624 }
625
626 fn create_index_buffer(&mut self) -> GoudResult<()> {
628 let quad_count = self.config.max_batch_size;
630 let mut indices = Vec::with_capacity(quad_count * 6);
631
632 for i in 0..quad_count {
633 let base = (i * 4) as u32;
634 indices.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]);
636 }
637
638 let buffer_size = indices.len() * std::mem::size_of::<u32>();
639 let buffer_data =
640 unsafe { std::slice::from_raw_parts(indices.as_ptr() as *const u8, buffer_size) };
641
642 let buffer =
643 self.backend
644 .create_buffer(BufferType::Index, BufferUsage::Static, buffer_data)?;
645
646 self.index_buffer = Some(buffer);
647
648 Ok(())
649 }
650
651 fn create_shader(&mut self) -> GoudResult<()> {
653 Err(GoudError::NotImplemented(
656 "Sprite shader creation".to_string(),
657 ))
658 }
659
660 fn upload_vertices(&mut self) -> GoudResult<()> {
662 if self.vertices.is_empty() {
663 return Ok(());
664 }
665
666 let buffer = self
667 .vertex_buffer
668 .ok_or_else(|| GoudError::InvalidState("Vertex buffer not created".to_string()))?;
669
670 let data_size = self.vertices.len() * std::mem::size_of::<SpriteVertex>();
671 let data_ptr = self.vertices.as_ptr() as *const u8;
672 let data_slice = unsafe { std::slice::from_raw_parts(data_ptr, data_size) };
673
674 self.backend.update_buffer(buffer, 0, data_slice)?;
675
676 Ok(())
677 }
678
679 fn resolve_texture(
681 &mut self,
682 asset_handle: AssetHandle<TextureAsset>,
683 asset_server: &AssetServer,
684 ) -> GoudResult<TextureHandle> {
685 if let Some(&gpu_handle) = self.texture_cache.get(&asset_handle) {
687 return Ok(gpu_handle);
688 }
689
690 let _texture_asset = asset_server.get(&asset_handle).ok_or_else(|| {
692 GoudError::ResourceNotFound(format!("Texture asset {:?}", asset_handle))
693 })?;
694
695 Err(GoudError::NotImplemented("Texture upload".to_string()))
698 }
699
700 fn get_texture_size(
702 &self,
703 asset_handle: AssetHandle<TextureAsset>,
704 asset_server: &AssetServer,
705 ) -> Vec2 {
706 if let Some(texture) = asset_server.get(&asset_handle) {
707 Vec2::new(texture.width as f32, texture.height as f32)
708 } else {
709 Vec2::one() }
711 }
712
713 pub fn sprite_count(&self) -> usize {
719 self.sprites.len()
720 }
721
722 pub fn batch_count(&self) -> usize {
724 self.batches.len()
725 }
726
727 pub fn frame_count(&self) -> u64 {
729 self.frame_count
730 }
731
732 pub fn batch_ratio(&self) -> f32 {
734 if self.batches.is_empty() {
735 0.0
736 } else {
737 self.sprites.len() as f32 / self.batches.len() as f32
738 }
739 }
740
741 pub fn stats(&self) -> (usize, usize, f32) {
753 (self.sprite_count(), self.batch_count(), self.batch_ratio())
754 }
755}
756
757#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::ecs::World;
765 use crate::libs::graphics::backend::opengl::OpenGLBackend;
766
767 #[test]
768 fn test_sprite_batch_config_default() {
769 let config = SpriteBatchConfig::default();
770 assert_eq!(config.initial_capacity, 1024);
771 assert_eq!(config.max_batch_size, 10000);
772 assert!(config.enable_z_sorting);
773 assert!(config.enable_batching);
774 }
775
776 #[test]
777 fn test_sprite_vertex_layout() {
778 let layout = SpriteVertex::layout();
779 assert_eq!(layout.stride, std::mem::size_of::<SpriteVertex>() as u32);
780 assert_eq!(layout.attributes.len(), 3);
781 }
782
783 #[test]
784 #[ignore] fn test_sprite_batch_new() {
786 let backend = OpenGLBackend::new().unwrap();
787 let config = SpriteBatchConfig::default();
788 let batch = SpriteBatch::new(backend, config);
789 assert!(batch.is_ok());
790 }
791
792 #[test]
793 #[ignore] fn test_sprite_batch_begin_end() {
795 let backend = OpenGLBackend::new().unwrap();
796 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
797
798 batch.begin();
799 assert_eq!(batch.sprite_count(), 0);
800 assert_eq!(batch.batch_count(), 0);
801
802 let result = batch.end();
803 assert!(result.is_ok());
804 }
805
806 #[test]
807 #[ignore] fn test_sprite_batch_gather_empty_world() {
809 let backend = OpenGLBackend::new().unwrap();
810 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
811 let world = World::new();
812
813 batch.begin();
814 let result = batch.gather_sprites(&world);
815 assert!(result.is_ok());
816 assert_eq!(batch.sprite_count(), 0);
817 }
818
819 #[test]
820 #[ignore] fn test_sprite_batch_sort_z_layer() {
822 let backend = OpenGLBackend::new().unwrap();
823 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
824
825 batch.sprites = vec![
827 SpriteInstance {
828 entity: Entity::new(0, 0),
829 texture: AssetHandle::new(0, 0),
830 transform: Mat3x3::IDENTITY,
831 color: Color::WHITE,
832 source_rect: None,
833 size: Vec2::one(),
834 z_layer: 10.0,
835 flip_x: false,
836 flip_y: false,
837 },
838 SpriteInstance {
839 entity: Entity::new(1, 0),
840 texture: AssetHandle::new(0, 0),
841 transform: Mat3x3::IDENTITY,
842 color: Color::WHITE,
843 source_rect: None,
844 size: Vec2::one(),
845 z_layer: 5.0,
846 flip_x: false,
847 flip_y: false,
848 },
849 ];
850
851 batch.sort_sprites();
852 assert_eq!(batch.sprites[0].z_layer, 5.0);
853 assert_eq!(batch.sprites[1].z_layer, 10.0);
854 }
855
856 #[test]
857 #[ignore] fn test_sprite_batch_statistics() {
859 let backend = OpenGLBackend::new().unwrap();
860 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
861
862 assert_eq!(batch.frame_count(), 0);
863 batch.begin();
864 assert_eq!(batch.frame_count(), 1);
865
866 assert_eq!(batch.batch_ratio(), 0.0);
867 }
868
869 #[test]
870 fn test_sprite_instance_creation() {
871 let instance = SpriteInstance {
872 entity: Entity::new(42, 1),
873 texture: AssetHandle::new(1, 1),
874 transform: Mat3x3::IDENTITY,
875 color: Color::RED,
876 source_rect: Some(Rect::new(0.0, 0.0, 32.0, 32.0)),
877 size: Vec2::new(64.0, 64.0),
878 z_layer: 100.0,
879 flip_x: true,
880 flip_y: false,
881 };
882
883 assert_eq!(instance.entity.index(), 42);
884 assert_eq!(instance.color, Color::RED);
885 assert!(instance.flip_x);
886 assert!(!instance.flip_y);
887 }
888
889 #[test]
890 fn test_sprite_batch_entry_creation() {
891 let entry = SpriteBatchEntry {
892 texture_handle: AssetHandle::new(1, 1),
893 gpu_texture: None,
894 vertex_start: 0,
895 vertex_count: 24,
896 };
897
898 assert_eq!(entry.vertex_start, 0);
899 assert_eq!(entry.vertex_count, 24);
900 assert!(entry.gpu_texture.is_none());
901 }
902
903 #[test]
908 #[ignore] fn test_texture_batching_single_texture() {
910 let backend = OpenGLBackend::new().unwrap();
911 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
912
913 let texture = AssetHandle::new(1, 1);
915 for i in 0..5 {
916 batch.sprites.push(SpriteInstance {
917 entity: Entity::new(i, 0),
918 texture,
919 transform: Mat3x3::IDENTITY,
920 color: Color::WHITE,
921 source_rect: None,
922 size: Vec2::one(),
923 z_layer: 0.0,
924 flip_x: false,
925 flip_y: false,
926 });
927 }
928
929 assert_eq!(batch.sprites.len(), 5);
931
932 for sprite in &batch.sprites {
934 assert_eq!(sprite.texture, texture);
935 }
936 }
937
938 #[test]
939 #[ignore] fn test_texture_batching_multiple_textures() {
941 let backend = OpenGLBackend::new().unwrap();
942 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
943
944 let tex1 = AssetHandle::new(1, 1);
946 let tex2 = AssetHandle::new(2, 1);
947 let tex3 = AssetHandle::new(3, 1);
948
949 batch.sprites = vec![
950 SpriteInstance {
951 entity: Entity::new(0, 0),
952 texture: tex1,
953 transform: Mat3x3::IDENTITY,
954 color: Color::WHITE,
955 source_rect: None,
956 size: Vec2::one(),
957 z_layer: 0.0,
958 flip_x: false,
959 flip_y: false,
960 },
961 SpriteInstance {
962 entity: Entity::new(1, 0),
963 texture: tex2,
964 transform: Mat3x3::IDENTITY,
965 color: Color::WHITE,
966 source_rect: None,
967 size: Vec2::one(),
968 z_layer: 0.0,
969 flip_x: false,
970 flip_y: false,
971 },
972 SpriteInstance {
973 entity: Entity::new(2, 0),
974 texture: tex3,
975 transform: Mat3x3::IDENTITY,
976 color: Color::WHITE,
977 source_rect: None,
978 size: Vec2::one(),
979 z_layer: 0.0,
980 flip_x: false,
981 flip_y: false,
982 },
983 ];
984
985 assert_eq!(batch.sprites.len(), 3);
987 assert_ne!(batch.sprites[0].texture, batch.sprites[1].texture);
988 assert_ne!(batch.sprites[1].texture, batch.sprites[2].texture);
989 assert_ne!(batch.sprites[0].texture, batch.sprites[2].texture);
990 }
991
992 #[test]
993 #[ignore] fn test_texture_batching_sort_by_texture() {
995 let backend = OpenGLBackend::new().unwrap();
996 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
997
998 let tex1 = AssetHandle::new(1, 1);
1000 let tex2 = AssetHandle::new(2, 1);
1001
1002 batch.sprites = vec![
1003 SpriteInstance {
1004 entity: Entity::new(0, 0),
1005 texture: tex2, transform: Mat3x3::IDENTITY,
1007 color: Color::WHITE,
1008 source_rect: None,
1009 size: Vec2::one(),
1010 z_layer: 0.0,
1011 flip_x: false,
1012 flip_y: false,
1013 },
1014 SpriteInstance {
1015 entity: Entity::new(1, 0),
1016 texture: tex1, transform: Mat3x3::IDENTITY,
1018 color: Color::WHITE,
1019 source_rect: None,
1020 size: Vec2::one(),
1021 z_layer: 0.0,
1022 flip_x: false,
1023 flip_y: false,
1024 },
1025 SpriteInstance {
1026 entity: Entity::new(2, 0),
1027 texture: tex2, transform: Mat3x3::IDENTITY,
1029 color: Color::WHITE,
1030 source_rect: None,
1031 size: Vec2::one(),
1032 z_layer: 0.0,
1033 flip_x: false,
1034 flip_y: false,
1035 },
1036 ];
1037
1038 batch.sort_sprites();
1040
1041 assert_eq!(batch.sprites[0].texture, tex1);
1044 assert_eq!(batch.sprites[1].texture, tex2);
1045 assert_eq!(batch.sprites[2].texture, tex2);
1046 }
1047
1048 #[test]
1049 #[ignore] fn test_texture_batching_with_z_layers() {
1051 let backend = OpenGLBackend::new().unwrap();
1052 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
1053
1054 let tex1 = AssetHandle::new(1, 1);
1055 let tex2 = AssetHandle::new(2, 1);
1056
1057 batch.sprites = vec![
1058 SpriteInstance {
1059 entity: Entity::new(0, 0),
1060 texture: tex2,
1061 transform: Mat3x3::IDENTITY,
1062 color: Color::WHITE,
1063 source_rect: None,
1064 size: Vec2::one(),
1065 z_layer: 10.0, flip_x: false,
1067 flip_y: false,
1068 },
1069 SpriteInstance {
1070 entity: Entity::new(1, 0),
1071 texture: tex1,
1072 transform: Mat3x3::IDENTITY,
1073 color: Color::WHITE,
1074 source_rect: None,
1075 size: Vec2::one(),
1076 z_layer: 5.0, flip_x: false,
1078 flip_y: false,
1079 },
1080 ];
1081
1082 batch.sort_sprites();
1084
1085 assert_eq!(batch.sprites[0].z_layer, 5.0);
1087 assert_eq!(batch.sprites[1].z_layer, 10.0);
1088 }
1089
1090 #[test]
1091 #[ignore] fn test_texture_batching_same_z_different_texture() {
1093 let backend = OpenGLBackend::new().unwrap();
1094 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
1095
1096 let tex1 = AssetHandle::new(1, 1);
1097 let tex2 = AssetHandle::new(2, 1);
1098
1099 batch.sprites = vec![
1100 SpriteInstance {
1101 entity: Entity::new(0, 0),
1102 texture: tex2,
1103 transform: Mat3x3::IDENTITY,
1104 color: Color::WHITE,
1105 source_rect: None,
1106 size: Vec2::one(),
1107 z_layer: 5.0,
1108 flip_x: false,
1109 flip_y: false,
1110 },
1111 SpriteInstance {
1112 entity: Entity::new(1, 0),
1113 texture: tex1,
1114 transform: Mat3x3::IDENTITY,
1115 color: Color::WHITE,
1116 source_rect: None,
1117 size: Vec2::one(),
1118 z_layer: 5.0, flip_x: false,
1120 flip_y: false,
1121 },
1122 SpriteInstance {
1123 entity: Entity::new(2, 0),
1124 texture: tex1,
1125 transform: Mat3x3::IDENTITY,
1126 color: Color::WHITE,
1127 source_rect: None,
1128 size: Vec2::one(),
1129 z_layer: 5.0, flip_x: false,
1131 flip_y: false,
1132 },
1133 ];
1134
1135 batch.sort_sprites();
1137
1138 assert_eq!(batch.sprites[0].texture, tex1);
1140 assert_eq!(batch.sprites[1].texture, tex1);
1141 assert_eq!(batch.sprites[2].texture, tex2);
1142 }
1143
1144 #[test]
1145 #[ignore] fn test_texture_batching_disabled() {
1147 let backend = OpenGLBackend::new().unwrap();
1148 let config = SpriteBatchConfig {
1149 initial_capacity: 1024,
1150 max_batch_size: 10000,
1151 enable_z_sorting: true,
1152 enable_batching: false, };
1154 let mut batch = SpriteBatch::new(backend, config).unwrap();
1155
1156 let tex1 = AssetHandle::new(1, 1);
1157 let tex2 = AssetHandle::new(2, 1);
1158
1159 batch.sprites = vec![
1160 SpriteInstance {
1161 entity: Entity::new(0, 0),
1162 texture: tex2,
1163 transform: Mat3x3::IDENTITY,
1164 color: Color::WHITE,
1165 source_rect: None,
1166 size: Vec2::one(),
1167 z_layer: 5.0,
1168 flip_x: false,
1169 flip_y: false,
1170 },
1171 SpriteInstance {
1172 entity: Entity::new(1, 0),
1173 texture: tex1,
1174 transform: Mat3x3::IDENTITY,
1175 color: Color::WHITE,
1176 source_rect: None,
1177 size: Vec2::one(),
1178 z_layer: 10.0,
1179 flip_x: false,
1180 flip_y: false,
1181 },
1182 ];
1183
1184 batch.sort_sprites();
1186
1187 assert_eq!(batch.sprites[0].z_layer, 5.0);
1189 assert_eq!(batch.sprites[1].z_layer, 10.0);
1190 }
1191
1192 #[test]
1193 #[ignore] fn test_texture_batching_stress_test() {
1195 let backend = OpenGLBackend::new().unwrap();
1196 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
1197
1198 for texture_id in 0..10 {
1200 let texture = AssetHandle::new(texture_id, 1);
1201 for sprite_id in 0..10 {
1202 batch.sprites.push(SpriteInstance {
1203 entity: Entity::new((texture_id * 10 + sprite_id) as u32, 0),
1204 texture,
1205 transform: Mat3x3::IDENTITY,
1206 color: Color::WHITE,
1207 source_rect: None,
1208 size: Vec2::one(),
1209 z_layer: 0.0,
1210 flip_x: false,
1211 flip_y: false,
1212 });
1213 }
1214 }
1215
1216 assert_eq!(batch.sprites.len(), 100);
1217
1218 batch.sort_sprites();
1220
1221 for i in 0..10 {
1223 let start = i * 10;
1224 let end = start + 10;
1225 let texture = batch.sprites[start].texture;
1226
1227 for j in start..end {
1228 assert_eq!(
1229 batch.sprites[j].texture, texture,
1230 "Sprite {} should have texture {:?}",
1231 j, texture
1232 );
1233 }
1234 }
1235 }
1236
1237 #[test]
1238 #[ignore] fn test_max_batch_size_enforcement() {
1240 let backend = OpenGLBackend::new().unwrap();
1241 let config = SpriteBatchConfig {
1242 initial_capacity: 1024,
1243 max_batch_size: 5, enable_z_sorting: true,
1245 enable_batching: true,
1246 };
1247 let mut batch = SpriteBatch::new(backend, config).unwrap();
1248
1249 let texture = AssetHandle::new(1, 1);
1251 for i in 0..10 {
1252 batch.sprites.push(SpriteInstance {
1253 entity: Entity::new(i, 0),
1254 texture,
1255 transform: Mat3x3::IDENTITY,
1256 color: Color::WHITE,
1257 source_rect: None,
1258 size: Vec2::one(),
1259 z_layer: 0.0,
1260 flip_x: false,
1261 flip_y: false,
1262 });
1263 }
1264
1265 assert_eq!(batch.sprites.len(), 10);
1266
1267 }
1271
1272 #[test]
1273 #[ignore] fn test_interleaved_textures_batching() {
1275 let backend = OpenGLBackend::new().unwrap();
1276 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default()).unwrap();
1277
1278 let tex1 = AssetHandle::new(1, 1);
1279 let tex2 = AssetHandle::new(2, 1);
1280
1281 for i in 0..6 {
1283 let texture = if i % 2 == 0 { tex1 } else { tex2 };
1284 batch.sprites.push(SpriteInstance {
1285 entity: Entity::new(i, 0),
1286 texture,
1287 transform: Mat3x3::IDENTITY,
1288 color: Color::WHITE,
1289 source_rect: None,
1290 size: Vec2::one(),
1291 z_layer: 0.0,
1292 flip_x: false,
1293 flip_y: false,
1294 });
1295 }
1296
1297 assert_eq!(batch.sprites[0].texture, tex1);
1299 assert_eq!(batch.sprites[1].texture, tex2);
1300 assert_eq!(batch.sprites[2].texture, tex1);
1301
1302 batch.sort_sprites();
1304
1305 let first_texture = batch.sprites[0].texture;
1308 let mut found_second = false;
1309 let mut second_texture = first_texture;
1310
1311 for sprite in &batch.sprites {
1312 if sprite.texture != first_texture {
1313 if !found_second {
1314 found_second = true;
1315 second_texture = sprite.texture;
1316 } else {
1317 assert_eq!(sprite.texture, second_texture);
1319 }
1320 }
1321 }
1322
1323 assert!(found_second);
1325 }
1326
1327 #[test]
1332 #[ignore] fn test_gather_sprites_from_world() {
1334 use crate::assets::AssetServer;
1335 use crate::ecs::components::{Sprite, Transform2D};
1336
1337 let mut world = World::new();
1338 let mut asset_server = AssetServer::new();
1339
1340 let texture = asset_server.load::<crate::assets::loaders::TextureAsset>("test.png");
1342
1343 let e1 = world.spawn_empty();
1344 world.insert(e1, Transform2D::from_position(Vec2::new(10.0, 20.0)));
1345 world.insert(e1, Sprite::new(texture));
1346
1347 let e2 = world.spawn_empty();
1348 world.insert(e2, Transform2D::from_position(Vec2::new(30.0, 40.0)));
1349 world.insert(e2, Sprite::new(texture).with_color(Color::RED));
1350
1351 let e3 = world.spawn_empty();
1352 world.insert(e3, Transform2D::from_position(Vec2::new(50.0, 60.0)));
1353 world.insert(
1354 e3,
1355 Sprite::new(texture).with_source_rect(Rect::new(0.0, 0.0, 32.0, 32.0)),
1356 );
1357
1358 let e4 = world.spawn_empty();
1360 world.insert(e4, Transform2D::from_position(Vec2::new(70.0, 80.0)));
1361
1362 let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
1364 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default())
1365 .expect("Failed to create sprite batch");
1366
1367 batch
1369 .gather_sprites(&world)
1370 .expect("Failed to gather sprites");
1371
1372 assert_eq!(batch.sprite_count(), 3);
1374
1375 let entities: Vec<Entity> = batch.sprites.iter().map(|s| s.entity).collect();
1377 assert!(entities.contains(&e1));
1378 assert!(entities.contains(&e2));
1379 assert!(entities.contains(&e3));
1380 assert!(!entities.contains(&e4));
1381
1382 let sprite1 = batch.sprites.iter().find(|s| s.entity == e1).unwrap();
1384 assert_eq!(sprite1.color, Color::WHITE);
1385
1386 let sprite2 = batch.sprites.iter().find(|s| s.entity == e2).unwrap();
1387 assert_eq!(sprite2.color, Color::RED);
1388
1389 let sprite3 = batch.sprites.iter().find(|s| s.entity == e3).unwrap();
1391 assert!(sprite3.source_rect.is_some());
1392 let source = sprite3.source_rect.unwrap();
1393 assert_eq!(source.x, 0.0);
1394 assert_eq!(source.y, 0.0);
1395 assert_eq!(source.width, 32.0);
1396 assert_eq!(source.height, 32.0);
1397
1398 assert_eq!(sprite1.z_layer, 20.0);
1400 assert_eq!(sprite2.z_layer, 40.0);
1401 assert_eq!(sprite3.z_layer, 60.0);
1402 }
1403
1404 #[test]
1405 #[ignore] fn test_gather_sprites_empty_world() {
1407 use crate::assets::AssetServer;
1408
1409 let world = World::new();
1410 let _asset_server = AssetServer::new();
1411
1412 let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
1413 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default())
1414 .expect("Failed to create sprite batch");
1415
1416 batch
1418 .gather_sprites(&world)
1419 .expect("Failed to gather sprites");
1420 assert_eq!(batch.sprite_count(), 0);
1421 }
1422
1423 #[test]
1424 #[ignore] fn test_gather_sprites_clears_previous_frame() {
1426 use crate::assets::AssetServer;
1427 use crate::ecs::components::{Sprite, Transform2D};
1428
1429 let mut world = World::new();
1430 let mut asset_server = AssetServer::new();
1431
1432 let texture = asset_server.load::<crate::assets::loaders::TextureAsset>("test.png");
1433
1434 let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
1435 let mut batch = SpriteBatch::new(backend, SpriteBatchConfig::default())
1436 .expect("Failed to create sprite batch");
1437
1438 let e1 = world.spawn_empty();
1440 world.insert(e1, Transform2D::from_position(Vec2::new(10.0, 20.0)));
1441 world.insert(e1, Sprite::new(texture));
1442
1443 let e2 = world.spawn_empty();
1444 world.insert(e2, Transform2D::from_position(Vec2::new(30.0, 40.0)));
1445 world.insert(e2, Sprite::new(texture));
1446
1447 batch
1448 .gather_sprites(&world)
1449 .expect("Failed to gather sprites");
1450 assert_eq!(batch.sprite_count(), 2);
1451
1452 world.despawn(e2);
1454
1455 batch
1456 .gather_sprites(&world)
1457 .expect("Failed to gather sprites");
1458 assert_eq!(batch.sprite_count(), 1);
1460 }
1461}