Skip to main content

goud_engine/sdk/
rendering.rs

1//! # SDK Rendering API
2//!
3//! Provides methods on [`GoudGame`](super::GoudGame) for 2D rendering operations
4//! including frame management, immediate-mode sprite/quad drawing, and render
5//! state control.
6//!
7//! # Availability
8//!
9//! This module requires the `native` feature (desktop platform with OpenGL).
10
11use super::GoudGame;
12use crate::core::error::{GoudError, GoudResult};
13use crate::libs::graphics::backend::RenderBackend;
14
15// Re-export rendering types for SDK users
16pub use crate::libs::graphics::sprite_batch::SpriteBatchConfig;
17
18// Re-export 3D types (they live in rendering_3d but users expect them here)
19pub use crate::libs::graphics::renderer3d::{
20    FogConfig, GridConfig, GridRenderMode, Light, LightType, PrimitiveCreateInfo, PrimitiveType,
21    SkyboxConfig,
22};
23
24// =============================================================================
25// Immediate-Mode Render State
26// =============================================================================
27
28/// GPU resources for immediate-mode sprite and quad rendering.
29///
30/// Created when the OpenGL backend is initialized and stored in GoudGame.
31/// Contains the compiled shader program, vertex/index buffers, VAO, and
32/// cached uniform locations needed by `draw_sprite` and `draw_quad`.
33#[allow(dead_code)]
34pub struct ImmediateRenderState {
35    /// Shader program for sprite rendering
36    pub(crate) shader: crate::libs::graphics::backend::types::ShaderHandle,
37    /// Vertex buffer for quad rendering
38    pub(crate) vertex_buffer: crate::libs::graphics::backend::types::BufferHandle,
39    /// Index buffer for quad rendering (shared)
40    pub(crate) index_buffer: crate::libs::graphics::backend::types::BufferHandle,
41    /// Vertex Array Object (required for macOS Core Profile)
42    pub(crate) vao: u32,
43    /// Cached uniform locations
44    pub(crate) u_projection: i32,
45    pub(crate) u_model: i32,
46    pub(crate) u_color: i32,
47    pub(crate) u_use_texture: i32,
48    pub(crate) u_texture: i32,
49    pub(crate) u_uv_offset: i32,
50    pub(crate) u_uv_scale: i32,
51}
52
53// =============================================================================
54// 2D Rendering -- ECS-based SpriteBatch (not FFI-generated)
55// =============================================================================
56
57impl GoudGame {
58    /// Begins a 2D rendering pass.
59    ///
60    /// Call this before drawing sprites. Must be paired with
61    /// [`end_2d_render`](Self::end_2d_render).
62    pub fn begin_2d_render(&mut self) -> GoudResult<()> {
63        match &mut self.sprite_batch {
64            Some(batch) => {
65                batch.begin();
66                Ok(())
67            }
68            None => Err(GoudError::NotInitialized),
69        }
70    }
71
72    /// Ends the 2D rendering pass and submits batched draw calls to the GPU.
73    pub fn end_2d_render(&mut self) -> GoudResult<()> {
74        match &mut self.sprite_batch {
75            Some(batch) => batch.end(),
76            None => Err(GoudError::NotInitialized),
77        }
78    }
79
80    /// Draws all entities with Sprite + Transform2D components.
81    pub fn draw_sprites(&mut self) -> GoudResult<()> {
82        let asset_server = self
83            .asset_server
84            .as_ref()
85            .ok_or(GoudError::NotInitialized)?;
86        match &mut self.sprite_batch {
87            Some(batch) => batch.draw_sprites(&self.world, asset_server),
88            None => Err(GoudError::NotInitialized),
89        }
90    }
91
92    /// Returns 2D rendering statistics: `(sprite_count, batch_count, batch_ratio)`.
93    #[inline]
94    pub fn render_2d_stats(&self) -> (usize, usize, f32) {
95        match &self.sprite_batch {
96            Some(batch) => batch.stats(),
97            None => (0, 0, 0.0),
98        }
99    }
100
101    /// Returns `true` if a 2D renderer (SpriteBatch) is initialized.
102    #[inline]
103    pub fn has_2d_renderer(&self) -> bool {
104        self.sprite_batch.is_some()
105    }
106}
107
108// =============================================================================
109// Renderer FFI (single annotated impl block for all goud_renderer_* functions)
110// =============================================================================
111
112// NOTE: FFI wrappers are hand-written in ffi/renderer.rs. The `#[goud_api]`
113// attribute is omitted here to avoid duplicate `#[no_mangle]` symbol conflicts.
114impl GoudGame {
115    /// Begins a new rendering frame. Call before any drawing operations.
116    pub fn begin_render(&mut self) -> bool {
117        let backend = match self.render_backend.as_mut() {
118            Some(b) => b,
119            None => return false,
120        };
121        if backend.begin_frame().is_err() {
122            return false;
123        }
124        let (fb_w, fb_h) = self.get_framebuffer_size();
125        // SAFETY: OpenGL viewport call is safe when a context is current.
126        unsafe {
127            gl::Viewport(0, 0, fb_w as i32, fb_h as i32);
128        }
129        true
130    }
131
132    /// Ends the current rendering frame.
133    pub fn end_render(&mut self) -> bool {
134        match self.render_backend.as_mut() {
135            Some(b) => b.end_frame().is_ok(),
136            None => false,
137        }
138    }
139
140    /// Sets the viewport rectangle for rendering.
141    pub fn set_viewport(&mut self, x: i32, y: i32, width: u32, height: u32) {
142        if let Some(backend) = self.render_backend.as_mut() {
143            backend.set_viewport(x, y, width, height);
144        }
145    }
146
147    /// Enables alpha blending for transparent sprites.
148    pub fn enable_blending(&mut self) {
149        if let Some(backend) = self.render_backend.as_mut() {
150            backend.enable_blending();
151        }
152    }
153
154    /// Disables alpha blending.
155    pub fn disable_blending(&mut self) {
156        if let Some(backend) = self.render_backend.as_mut() {
157            backend.disable_blending();
158        }
159    }
160
161    /// Enables depth testing.
162    pub fn enable_depth_test(&mut self) {
163        if let Some(backend) = self.render_backend.as_mut() {
164            backend.enable_depth_test();
165        }
166    }
167
168    /// Disables depth testing.
169    pub fn disable_depth_test(&mut self) {
170        if let Some(backend) = self.render_backend.as_mut() {
171            backend.disable_depth_test();
172        }
173    }
174
175    /// Clears the depth buffer.
176    pub fn clear_depth(&mut self) {
177        if let Some(backend) = self.render_backend.as_mut() {
178            backend.clear_depth();
179        }
180    }
181
182    /// Draws a textured sprite at the given position (immediate mode).
183    #[allow(clippy::too_many_arguments)]
184    pub fn draw_sprite(
185        &mut self,
186        texture: u64,
187        x: f32,
188        y: f32,
189        width: f32,
190        height: f32,
191        rotation: f32,
192        r: f32,
193        g: f32,
194        b: f32,
195        a: f32,
196    ) -> bool {
197        self.draw_sprite_rect(
198            texture, x, y, width, height, rotation, 0.0, 0.0, 1.0, 1.0, r, g, b, a,
199        )
200    }
201
202    /// Draws a textured sprite with a source rectangle for sprite sheet animation.
203    #[allow(clippy::too_many_arguments)]
204    pub fn draw_sprite_rect(
205        &mut self,
206        texture: u64,
207        x: f32,
208        y: f32,
209        width: f32,
210        height: f32,
211        rotation: f32,
212        src_x: f32,
213        src_y: f32,
214        src_w: f32,
215        src_h: f32,
216        r: f32,
217        g: f32,
218        b: f32,
219        a: f32,
220    ) -> bool {
221        use crate::libs::graphics::backend::types::{PrimitiveTopology, TextureHandle};
222
223        let state = match &self.immediate_state {
224            Some(s) => s,
225            None => return false,
226        };
227
228        let (fb_w, fb_h) = self.get_framebuffer_size();
229        let (win_w, win_h) = self.get_window_size();
230
231        // Cache values from immediate state before borrowing backend
232        let (shader, vao, u_proj, u_model, u_color, u_use_tex, u_tex, u_uv_off, u_uv_sc) = (
233            state.shader,
234            state.vao,
235            state.u_projection,
236            state.u_model,
237            state.u_color,
238            state.u_use_texture,
239            state.u_texture,
240            state.u_uv_offset,
241            state.u_uv_scale,
242        );
243
244        let backend = match self.render_backend.as_mut() {
245            Some(b) => b,
246            None => return false,
247        };
248
249        // SAFETY: OpenGL calls require a current context.
250        unsafe {
251            gl::Viewport(0, 0, fb_w as i32, fb_h as i32);
252            gl::BindVertexArray(vao);
253        }
254
255        let projection = ortho_matrix(0.0, win_w as f32, win_h as f32, 0.0);
256        let model = model_matrix(x, y, width, height, rotation);
257
258        let tex_index = (texture & 0xFFFF_FFFF) as u32;
259        let tex_gen = ((texture >> 32) & 0xFFFF_FFFF) as u32;
260        let tex_handle = TextureHandle::new(tex_index, tex_gen);
261
262        if backend.bind_shader(shader).is_err() {
263            return false;
264        }
265        backend.set_uniform_mat4(u_proj, &projection);
266        backend.set_uniform_mat4(u_model, &model);
267        backend.set_uniform_vec4(u_color, r, g, b, a);
268        backend.set_uniform_int(u_use_tex, 1);
269        backend.set_uniform_int(u_tex, 0);
270        backend.set_uniform_vec2(u_uv_off, src_x, src_y);
271        backend.set_uniform_vec2(u_uv_sc, src_w, src_h);
272
273        if backend.bind_texture(tex_handle, 0).is_err() {
274            return false;
275        }
276        backend
277            .draw_indexed(PrimitiveTopology::Triangles, 6, 0)
278            .is_ok()
279    }
280
281    /// Draws a colored quad (no texture) at the given position.
282    #[allow(clippy::too_many_arguments)]
283    pub fn draw_quad(
284        &mut self,
285        x: f32,
286        y: f32,
287        width: f32,
288        height: f32,
289        r: f32,
290        g: f32,
291        b: f32,
292        a: f32,
293    ) -> bool {
294        use crate::libs::graphics::backend::types::PrimitiveTopology;
295
296        let state = match &self.immediate_state {
297            Some(s) => s,
298            None => return false,
299        };
300
301        let (fb_w, fb_h) = self.get_framebuffer_size();
302        let (win_w, win_h) = self.get_window_size();
303
304        let (shader, vao, u_proj, u_model, u_color, u_use_tex) = (
305            state.shader,
306            state.vao,
307            state.u_projection,
308            state.u_model,
309            state.u_color,
310            state.u_use_texture,
311        );
312
313        let backend = match self.render_backend.as_mut() {
314            Some(b) => b,
315            None => return false,
316        };
317
318        // SAFETY: OpenGL calls require a current context.
319        unsafe {
320            gl::Viewport(0, 0, fb_w as i32, fb_h as i32);
321            gl::BindVertexArray(vao);
322        }
323
324        let projection = ortho_matrix(0.0, win_w as f32, win_h as f32, 0.0);
325        let model = model_matrix(x, y, width, height, 0.0);
326
327        if backend.bind_shader(shader).is_err() {
328            return false;
329        }
330        backend.set_uniform_mat4(u_proj, &projection);
331        backend.set_uniform_mat4(u_model, &model);
332        backend.set_uniform_vec4(u_color, r, g, b, a);
333        backend.set_uniform_int(u_use_tex, 0);
334
335        backend
336            .draw_indexed(PrimitiveTopology::Triangles, 6, 0)
337            .is_ok()
338    }
339
340    /// Gets rendering statistics for the current frame.
341    ///
342    /// Writes default statistics to the provided out-pointer.
343    /// Returns `true` on success, `false` if the pointer is null.
344    ///
345    /// # Safety
346    ///
347    /// `out_stats` must be a valid, aligned, writable pointer to a
348    /// `GoudRenderStats` value, or null (in which case this returns `false`).
349    pub unsafe fn get_render_stats(
350        &self,
351        out_stats: *mut crate::ffi::renderer::GoudRenderStats,
352    ) -> bool {
353        if out_stats.is_null() {
354            return false;
355        }
356        // SAFETY: Caller guarantees out_stats is a valid pointer.
357        unsafe {
358            *out_stats = crate::ffi::renderer::GoudRenderStats::default();
359        }
360        true
361    }
362}
363
364// =============================================================================
365// Helper Functions
366// =============================================================================
367
368/// Creates an orthographic projection matrix.
369fn ortho_matrix(left: f32, right: f32, bottom: f32, top: f32) -> [f32; 16] {
370    let near = -1.0f32;
371    let far = 1.0f32;
372    [
373        2.0 / (right - left),
374        0.0,
375        0.0,
376        0.0,
377        0.0,
378        2.0 / (top - bottom),
379        0.0,
380        0.0,
381        0.0,
382        0.0,
383        -2.0 / (far - near),
384        0.0,
385        -(right + left) / (right - left),
386        -(top + bottom) / (top - bottom),
387        -(far + near) / (far - near),
388        1.0,
389    ]
390}
391
392/// Creates a model matrix for sprite transformation.
393fn model_matrix(x: f32, y: f32, width: f32, height: f32, rotation: f32) -> [f32; 16] {
394    let cos_r = rotation.cos();
395    let sin_r = rotation.sin();
396    [
397        width * cos_r,
398        width * sin_r,
399        0.0,
400        0.0,
401        -height * sin_r,
402        height * cos_r,
403        0.0,
404        0.0,
405        0.0,
406        0.0,
407        1.0,
408        0.0,
409        x,
410        y,
411        0.0,
412        1.0,
413    ]
414}
415
416// =============================================================================
417// Tests
418// =============================================================================
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::sdk::GameConfig;
424
425    #[test]
426    fn test_begin_2d_render_headless() {
427        let mut game = GoudGame::new(GameConfig::default()).unwrap();
428        assert!(game.begin_2d_render().is_err());
429    }
430
431    #[test]
432    fn test_end_2d_render_headless() {
433        let mut game = GoudGame::new(GameConfig::default()).unwrap();
434        assert!(game.end_2d_render().is_err());
435    }
436
437    #[test]
438    fn test_draw_sprites_headless() {
439        let mut game = GoudGame::new(GameConfig::default()).unwrap();
440        assert!(game.draw_sprites().is_err());
441    }
442
443    #[test]
444    fn test_render_2d_stats_headless() {
445        let game = GoudGame::new(GameConfig::default()).unwrap();
446        assert_eq!(game.render_2d_stats(), (0, 0, 0.0));
447    }
448
449    #[test]
450    fn test_has_2d_renderer_headless() {
451        let game = GoudGame::new(GameConfig::default()).unwrap();
452        assert!(!game.has_2d_renderer());
453    }
454
455    #[test]
456    fn test_draw_sprite_headless_returns_false() {
457        let mut game = GoudGame::new(GameConfig::default()).unwrap();
458        assert!(!game.draw_sprite(0, 0.0, 0.0, 10.0, 10.0, 0.0, 1.0, 1.0, 1.0, 1.0));
459    }
460
461    #[test]
462    fn test_draw_quad_headless_returns_false() {
463        let mut game = GoudGame::new(GameConfig::default()).unwrap();
464        assert!(!game.draw_quad(0.0, 0.0, 10.0, 10.0, 1.0, 0.0, 0.0, 1.0));
465    }
466
467    #[test]
468    fn test_begin_render_headless() {
469        let mut game = GoudGame::new(GameConfig::default()).unwrap();
470        assert!(!game.begin_render());
471    }
472
473    #[test]
474    fn test_end_render_headless() {
475        let mut game = GoudGame::new(GameConfig::default()).unwrap();
476        assert!(!game.end_render());
477    }
478
479    #[test]
480    fn test_ortho_matrix_identity_like() {
481        let m = ortho_matrix(0.0, 2.0, 0.0, 2.0);
482        assert!((m[0] - 1.0).abs() < 0.001);
483        assert!((m[5] - 1.0).abs() < 0.001);
484    }
485
486    #[test]
487    fn test_model_matrix_no_rotation() {
488        let m = model_matrix(10.0, 20.0, 5.0, 5.0, 0.0);
489        assert!((m[12] - 10.0).abs() < 0.001);
490        assert!((m[13] - 20.0).abs() < 0.001);
491    }
492}