Skip to main content

goud_engine/rendering/
render_system.rs

1//! Rendering systems for 2D sprite rendering.
2//!
3//! This module provides ECS systems for rendering sprites using the SpriteBatch renderer.
4//!
5//! # Architecture
6//!
7//! The rendering pipeline:
8//! 1. Query entities with Sprite + Transform2D components
9//! 2. Sort by Z-layer for correct draw order
10//! 3. Batch by texture to minimize draw calls
11//! 4. Submit to GPU via backend
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use goud_engine::ecs::systems::SpriteRenderSystem;
17//! use goud_engine::ecs::World;
18//! use goud_engine::graphics::backend::OpenGLBackend;
19//!
20//! let backend = OpenGLBackend::new()?;
21//! let mut render_system = SpriteRenderSystem::new(backend)?;
22//!
23//! // Each frame
24//! render_system.run(&mut world)?;
25//! ```
26
27use super::sprite_batch::{SpriteBatch, SpriteBatchConfig};
28use crate::assets::AssetServer;
29use crate::core::error::GoudResult;
30use crate::ecs::World;
31use crate::libs::graphics::backend::RenderBackend;
32
33/// System for rendering 2D sprites using batched rendering.
34///
35/// This system:
36/// - Queries all entities with Sprite + Transform2D components
37/// - Batches sprites by texture to minimize draw calls
38/// - Sorts by Z-layer for correct rendering order
39/// - Submits draw calls to the GPU backend
40///
41/// # Performance
42///
43/// Target: <100 draw calls for 10,000 sprites (100:1 batch ratio)
44pub struct SpriteRenderSystem<B: RenderBackend> {
45    sprite_batch: SpriteBatch<B>,
46}
47
48impl<B: RenderBackend> SpriteRenderSystem<B> {
49    /// Creates a new sprite render system with the given backend.
50    pub fn new(backend: B) -> GoudResult<Self> {
51        let sprite_batch = SpriteBatch::new(backend, SpriteBatchConfig::default())?;
52        Ok(Self { sprite_batch })
53    }
54
55    /// Creates a new sprite render system with custom configuration.
56    pub fn with_config(backend: B, config: SpriteBatchConfig) -> GoudResult<Self> {
57        let sprite_batch = SpriteBatch::new(backend, config)?;
58        Ok(Self { sprite_batch })
59    }
60
61    /// Runs the sprite rendering system.
62    ///
63    /// This should be called once per frame, after all game logic has been updated.
64    ///
65    /// # Arguments
66    ///
67    /// * `world` - The ECS world containing sprite entities
68    /// * `asset_server` - Asset server for loading textures
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if:
73    /// - Shader compilation fails
74    /// - Buffer allocation fails
75    /// - GPU operations fail
76    pub fn run(&mut self, world: &World, asset_server: &AssetServer) -> GoudResult<()> {
77        self.sprite_batch.begin();
78        self.sprite_batch.draw_sprites(world, asset_server)?;
79        self.sprite_batch.end()?;
80        Ok(())
81    }
82
83    /// Gets rendering statistics from the last frame.
84    ///
85    /// Returns (sprite_count, batch_count, batch_ratio) tuple.
86    pub fn stats(&self) -> (usize, usize, f32) {
87        self.sprite_batch.stats()
88    }
89
90    /// Gets a reference to the underlying sprite batch.
91    pub fn sprite_batch(&self) -> &SpriteBatch<B> {
92        &self.sprite_batch
93    }
94
95    /// Gets a mutable reference to the underlying sprite batch.
96    pub fn sprite_batch_mut(&mut self) -> &mut SpriteBatch<B> {
97        &mut self.sprite_batch
98    }
99}
100
101impl<B: RenderBackend> std::fmt::Debug for SpriteRenderSystem<B> {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("SpriteRenderSystem")
104            .field("sprite_batch", &"SpriteBatch { ... }")
105            .finish()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::assets::loaders::{TextureAsset, TextureFormat};
113    use crate::assets::AssetStorage;
114    use crate::core::math::{Color, Vec2};
115    use crate::ecs::components::{Sprite, Transform2D};
116    use crate::ecs::World;
117    use crate::libs::graphics::backend::opengl::OpenGLBackend;
118
119    #[test]
120    fn test_sprite_render_system_new() {
121        // This test requires OpenGL context, so we just verify the type exists
122        // Real testing would require OpenGL initialization
123    }
124
125    #[test]
126    fn test_sprite_render_system_debug() {
127        // Debug formatting doesn't require OpenGL
128    }
129
130    #[test]
131    #[ignore] // Requires OpenGL context
132    fn test_sprite_render_system_run() {
133        // Setup
134        let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
135        let mut render_system =
136            SpriteRenderSystem::new(backend).expect("Failed to create render system");
137
138        // Create world with sprite entities
139        let mut world = World::new();
140        let asset_server = AssetServer::new();
141
142        // Create a test texture asset
143        let texture_data = vec![255u8; 64 * 64 * 4]; // 64x64 RGBA white texture
144        let texture_asset = TextureAsset::new(texture_data, 64, 64, TextureFormat::Png);
145        let mut storage = AssetStorage::new();
146        let texture_handle = storage.insert(texture_asset);
147
148        // Spawn sprite entity
149        let entity = world.spawn_empty();
150        world
151            .insert(entity, Sprite::new(texture_handle).with_color(Color::WHITE))
152            .expect("Failed to add Sprite");
153        world
154            .insert(entity, Transform2D::from_position(Vec2::new(100.0, 100.0)))
155            .expect("Failed to add Transform2D");
156
157        // Run render system
158        let result = render_system.run(&world, &asset_server);
159        assert!(result.is_ok(), "Render system should run successfully");
160
161        // Check stats
162        let (sprite_count, batch_count, _ratio) = render_system.stats();
163        assert_eq!(sprite_count, 1, "Should have rendered 1 sprite");
164        assert!(batch_count > 0, "Should have at least 1 batch");
165    }
166
167    #[test]
168    #[ignore] // Requires OpenGL context
169    fn test_sprite_render_system_multiple_sprites() {
170        let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
171        let mut render_system =
172            SpriteRenderSystem::new(backend).expect("Failed to create render system");
173
174        let mut world = World::new();
175        let asset_server = AssetServer::new();
176
177        // Create texture
178        let texture_data = vec![255u8; 64 * 64 * 4];
179        let texture_asset = TextureAsset::new(texture_data, 64, 64, TextureFormat::Png);
180        let mut storage = AssetStorage::new();
181        let texture_handle = storage.insert(texture_asset);
182
183        // Spawn 10 sprites
184        for i in 0..10 {
185            let entity = world.spawn_empty();
186            world
187                .insert(entity, Sprite::new(texture_handle).with_color(Color::WHITE))
188                .expect("Failed to add Sprite");
189            world
190                .insert(
191                    entity,
192                    Transform2D::from_position(Vec2::new(i as f32 * 50.0, 100.0)),
193                )
194                .expect("Failed to add Transform2D");
195        }
196
197        // Run render system
198        render_system
199            .run(&world, &asset_server)
200            .expect("Failed to run render system");
201
202        // Check stats
203        let (sprite_count, batch_count, ratio) = render_system.stats();
204        assert_eq!(sprite_count, 10, "Should have rendered 10 sprites");
205        assert!(batch_count > 0, "Should have at least 1 batch");
206        assert!(
207            ratio > 1.0,
208            "Should have good batching ratio with same texture"
209        );
210    }
211
212    #[test]
213    #[ignore] // Requires OpenGL context
214    fn test_sprite_render_system_empty_world() {
215        let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
216        let mut render_system =
217            SpriteRenderSystem::new(backend).expect("Failed to create render system");
218
219        let world = World::new();
220        let asset_server = AssetServer::new();
221
222        // Run on empty world
223        let result = render_system.run(&world, &asset_server);
224        assert!(result.is_ok(), "Should handle empty world gracefully");
225
226        let (sprite_count, batch_count, _ratio) = render_system.stats();
227        assert_eq!(sprite_count, 0, "Should have 0 sprites");
228        assert_eq!(batch_count, 0, "Should have 0 batches");
229    }
230
231    #[test]
232    fn test_sprite_render_system_accessors() {
233        // Test that accessors are available (no OpenGL required)
234        // In real usage, you'd create with OpenGL backend
235    }
236
237    #[test]
238    #[ignore] // Requires OpenGL context
239    fn test_sprite_render_system_z_sorting() {
240        let backend = OpenGLBackend::new().expect("Failed to create OpenGL backend");
241        let mut render_system =
242            SpriteRenderSystem::new(backend).expect("Failed to create render system");
243
244        let mut world = World::new();
245        let asset_server = AssetServer::new();
246
247        // Create texture
248        let texture_data = vec![255u8; 64 * 64 * 4];
249        let texture_asset = TextureAsset::new(texture_data, 64, 64, TextureFormat::Png);
250        let mut storage = AssetStorage::new();
251        let texture_handle = storage.insert(texture_asset);
252
253        // Spawn sprites with different Y positions (Z-layer)
254        for i in 0..5 {
255            let entity = world.spawn_empty();
256            world
257                .insert(entity, Sprite::new(texture_handle).with_color(Color::WHITE))
258                .expect("Failed to add Sprite");
259            world
260                .insert(
261                    entity,
262                    Transform2D::from_position(Vec2::new(100.0, i as f32 * 50.0)),
263                )
264                .expect("Failed to add Transform2D");
265        }
266
267        // Run render system (should sort by Y position)
268        render_system
269            .run(&world, &asset_server)
270            .expect("Failed to run render system");
271
272        let (sprite_count, _batch_count, _ratio) = render_system.stats();
273        assert_eq!(sprite_count, 5, "Should have rendered 5 sprites");
274    }
275}