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