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