From 55dbffe3f0890abe609533bc778034297f6a79b8 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 11:14:55 +0200 Subject: [PATCH 1/8] Require frame-long timelines for all items used during a render pass --- citro3d/examples/triangle.rs | 81 ++++++---- citro3d/src/lib.rs | 234 ++-------------------------- citro3d/src/render.rs | 293 +++++++++++++++++++++++++++++------ citro3d/src/uniform.rs | 8 +- 4 files changed, 319 insertions(+), 297 deletions(-) diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 6559f9c..56faafc 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -5,7 +5,7 @@ use citro3d::macros::include_shader; use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement}; -use citro3d::render::ClearFlags; +use citro3d::render::{ClearFlags, RenderPass}; use citro3d::texenv; use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; @@ -85,7 +85,7 @@ fn main() { let vertex_shader = shader.get(0).unwrap(); let program = shader::Program::new(vertex_shader).unwrap(); - instance.bind_program(&program); + let projection_uniform_idx = program.get_uniform("projection").unwrap(); let mut vbo_data = Vec::with_capacity_in(VERTICES.len(), ctru::linear::LinearAllocator); vbo_data.extend_from_slice(VERTICES); @@ -93,16 +93,6 @@ fn main() { let mut buf_info = buffer::Info::new(); let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); - // Configure the first fragment shading substage to just pass through the vertex color - // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight - let stage0 = texenv::Stage::new(0).unwrap(); - instance - .texenv(stage0) - .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) - .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); - - let projection_uniform_idx = program.get_uniform("projection").unwrap(); - while apt.main_loop() { hid.scan_input(); @@ -110,20 +100,15 @@ fn main() { break; } - instance.render_frame_with(|instance| { - let mut render_to = |target: &mut render::Target, projection| { - target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); - - instance - .select_render_target(target) - .expect("failed to set render target"); + instance.render_frame_with(|mut pass| { + pass.bind_program(&program); - instance.bind_vertex_uniform(projection_uniform_idx, projection); - - instance.set_attr_info(&attr_info); - - instance.draw_arrays(buffer::Primitive::Triangles, vbo_data); - }; + // Configure the first fragment shading substage to just pass through the vertex color + // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight + let stage0 = texenv::Stage::new(0).unwrap(); + pass.texenv(stage0) + .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) + .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); let Projections { left_eye, @@ -131,9 +116,32 @@ fn main() { center, } = calculate_projections(); - render_to(&mut top_left_target, &left_eye); - render_to(&mut top_right_target, &right_eye); - render_to(&mut bottom_target, ¢er); + render_to_target( + &mut pass, + &mut top_left_target, + &left_eye, + projection_uniform_idx, + &attr_info, + vbo_data, + ); + render_to_target( + &mut pass, + &mut top_right_target, + &right_eye, + projection_uniform_idx, + &attr_info, + vbo_data, + ); + render_to_target( + &mut pass, + &mut bottom_target, + ¢er, + projection_uniform_idx, + &attr_info, + vbo_data, + ); + + pass }); } } @@ -161,6 +169,23 @@ fn prepare_vbos<'a>( (attr_info, buf_idx) } +// Repeated render for each target. +fn render_to_target<'pass>( + pass: &mut RenderPass<'pass>, + target: &'pass mut render::Target, + projection: &Matrix4, + projection_uniform_idx: citro3d::uniform::Index, + attr_info: &citro3d::attrib::Info, + vbo_data: citro3d::buffer::Slice<'pass>, +) { + target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); + pass.select_render_target(target) + .expect("failed to set render target"); + pass.bind_vertex_uniform(projection_uniform_idx, projection); + pass.set_attr_info(attr_info); + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); +} + struct Projections { left_eye: Matrix4, right_eye: Matrix4, diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index f7f76cd..be2d7fb 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -30,18 +30,14 @@ pub mod texenv; pub mod texture; pub mod uniform; -use std::cell::{OnceCell, RefMut}; +use std::cell::RefMut; use std::fmt; -use std::pin::Pin; use std::rc::Rc; use ctru::services::gfx::Screen; pub use error::{Error, Result}; -use self::buffer::{Index, Indices}; -use self::light::LightEnv; -use self::texenv::TexEnv; -use self::uniform::Uniform; +use crate::render::RenderPass; pub mod macros { //! Helper macros for working with shaders. @@ -54,22 +50,20 @@ mod private { impl Sealed for u16 {} } +/// Representation of `citro3d`'s internal render queue. This is something that +/// lives in the global context, but it keeps references to resources that are +/// used for rendering, so it's useful for us to have something to represent its +/// lifetime. +struct RenderQueue; + /// The single instance for using `citro3d`. This is the base type that an application /// should instantiate to use this library. #[non_exhaustive] #[must_use] pub struct Instance { - texenvs: [OnceCell; texenv::TEXENV_COUNT], queue: Rc, - light_env: Option>>, } -/// Representation of `citro3d`'s internal render queue. This is something that -/// lives in the global context, but it keeps references to resources that are -/// used for rendering, so it's useful for us to have something to represent its -/// lifetime. -struct RenderQueue; - impl fmt::Debug for Instance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Instance").finish_non_exhaustive() @@ -95,17 +89,7 @@ impl Instance { pub fn with_cmdbuf_size(size: usize) -> Result { if unsafe { citro3d_sys::C3D_Init(size) } { Ok(Self { - texenvs: [ - // thank goodness there's only six of them! - OnceCell::new(), - OnceCell::new(), - OnceCell::new(), - OnceCell::new(), - OnceCell::new(), - OnceCell::new(), - ], queue: Rc::new(RenderQueue), - light_env: None, }) } else { Err(Error::FailedToInitialize) @@ -130,30 +114,15 @@ impl Instance { render::Target::new(width, height, screen, depth_format, Rc::clone(&self.queue)) } - /// Select the given render target for drawing the frame. This must be called - /// as pare of a render call (i.e. within the call to - /// [`render_frame_with`](Self::render_frame_with)). - /// - /// # Errors - /// - /// Fails if the given target cannot be used for drawing, or called outside - /// the context of a frame render. - #[doc(alias = "C3D_FrameDrawOn")] - pub fn select_render_target(&mut self, target: &render::Target<'_>) -> Result<()> { - let _ = self; - if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } { - Ok(()) - } else { - Err(Error::InvalidRenderTarget) - } - } - /// Render a frame. The passed in function/closure can mutate the instance, /// such as to [select a render target](Self::select_render_target) /// or [bind a new shader program](Self::bind_program). #[doc(alias = "C3D_FrameBegin")] #[doc(alias = "C3D_FrameEnd")] - pub fn render_frame_with(&mut self, f: impl FnOnce(&mut Self)) { + pub fn render_frame_with<'istance: 'frame, 'frame>( + &'istance mut self, + f: impl FnOnce(RenderPass<'frame>) -> RenderPass<'frame>, + ) { unsafe { citro3d_sys::C3D_FrameBegin( // TODO: begin + end flags should be configurable @@ -161,187 +130,14 @@ impl Instance { ); } - f(self); + let pass = f(RenderPass::new(self)); unsafe { citro3d_sys::C3D_FrameEnd(0); } - } - - /// Get the buffer info being used, if it exists. Note that the resulting - /// [`buffer::Info`] is copied from the one currently in use. - #[doc(alias = "C3D_GetBufInfo")] - pub fn buffer_info(&self) -> Option { - let raw = unsafe { citro3d_sys::C3D_GetBufInfo() }; - buffer::Info::copy_from(raw) - } - - /// Set the buffer info to use for any following draw calls. - #[doc(alias = "C3D_SetBufInfo")] - pub fn set_buffer_info(&mut self, buffer_info: &buffer::Info) { - let raw: *const _ = &buffer_info.0; - // SAFETY: C3D_SetBufInfo actually copies the pointee instead of mutating it. - unsafe { citro3d_sys::C3D_SetBufInfo(raw.cast_mut()) }; - } - - /// Get the attribute info being used, if it exists. Note that the resulting - /// [`attrib::Info`] is copied from the one currently in use. - #[doc(alias = "C3D_GetAttrInfo")] - pub fn attr_info(&self) -> Option { - let raw = unsafe { citro3d_sys::C3D_GetAttrInfo() }; - attrib::Info::copy_from(raw) - } - /// Set the attribute info to use for any following draw calls. - #[doc(alias = "C3D_SetAttrInfo")] - pub fn set_attr_info(&mut self, attr_info: &attrib::Info) { - let raw: *const _ = &attr_info.0; - // SAFETY: C3D_SetAttrInfo actually copies the pointee instead of mutating it. - unsafe { citro3d_sys::C3D_SetAttrInfo(raw.cast_mut()) }; - } - - /// Render primitives from the current vertex array buffer. - #[doc(alias = "C3D_DrawArrays")] - pub fn draw_arrays(&mut self, primitive: buffer::Primitive, vbo_data: buffer::Slice) { - self.set_buffer_info(vbo_data.info()); - - // TODO: should we also require the attrib info directly here? - unsafe { - citro3d_sys::C3D_DrawArrays( - primitive as ctru_sys::GPU_Primitive_t, - vbo_data.index(), - vbo_data.len(), - ); - } - } - /// Indexed drawing - /// - /// Draws the vertices in `buf` indexed by `indices`. `indices` must be linearly allocated - /// - /// # Safety - // TODO: #41 might be able to solve this: - /// If `indices` goes out of scope before the current frame ends it will cause a - /// use-after-free (possibly by the GPU). - /// - /// # Panics - /// - /// If the given index buffer is too long to have its length converted to `i32`. - #[doc(alias = "C3D_DrawElements")] - pub unsafe fn draw_elements( - &mut self, - primitive: buffer::Primitive, - vbo_data: buffer::Slice, - indices: &Indices<'_, I>, - ) { - self.set_buffer_info(vbo_data.info()); - - let indices = &indices.buffer; - let elements = indices.as_ptr().cast(); - - unsafe { - citro3d_sys::C3D_DrawElements( - primitive as ctru_sys::GPU_Primitive_t, - indices.len().try_into().unwrap(), - // flag bit for short or byte - I::TYPE, - elements, - ); - } - } - - /// Use the given [`shader::Program`] for subsequent draw calls. - pub fn bind_program(&mut self, program: &shader::Program) { - // SAFETY: AFAICT C3D_BindProgram just copies pointers from the given program, - // instead of mutating the pointee in any way that would cause UB - unsafe { - citro3d_sys::C3D_BindProgram(program.as_raw().cast_mut()); - } - } - - /// Binds a new [`LightEnv`], returning the previous one (if present). - pub fn bind_light_env( - &mut self, - new_env: Option>>, - ) -> Option>> { - let old_env = self.light_env.take(); - self.light_env = new_env; - - unsafe { - // setup the light env slot, since this is a pointer copy it will stick around even with we swap - // out light_env later - citro3d_sys::C3D_LightEnvBind( - self.light_env - .as_mut() - .map_or(std::ptr::null_mut(), |env| env.as_mut().as_raw_mut()), - ); - } - - old_env - } - - pub fn light_env(&self) -> Option> { - self.light_env.as_ref().map(|env| env.as_ref()) - } - - pub fn light_env_mut(&mut self) -> Option> { - self.light_env.as_mut().map(|env| env.as_mut()) - } - - /// Bind a uniform to the given `index` in the vertex shader for the next draw call. - /// - /// # Example - /// - /// ``` - /// # let _runner = test_runner::GdbRunner::default(); - /// # use citro3d::uniform; - /// # use citro3d::math::Matrix4; - /// # - /// # let mut instance = citro3d::Instance::new().unwrap(); - /// let idx = uniform::Index::from(0); - /// let mtx = Matrix4::identity(); - /// instance.bind_vertex_uniform(idx, &mtx); - /// ``` - pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Into) { - uniform.into().bind(self, shader::Type::Vertex, index); - } - - /// Bind a uniform to the given `index` in the geometry shader for the next draw call. - /// - /// # Example - /// - /// ``` - /// # let _runner = test_runner::GdbRunner::default(); - /// # use citro3d::uniform; - /// # use citro3d::math::Matrix4; - /// # - /// # let mut instance = citro3d::Instance::new().unwrap(); - /// let idx = uniform::Index::from(0); - /// let mtx = Matrix4::identity(); - /// instance.bind_geometry_uniform(idx, &mtx); - /// ``` - pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Into) { - uniform.into().bind(self, shader::Type::Geometry, index); - } - - /// Retrieve the [`TexEnv`] for the given stage, initializing it first if necessary. - /// - /// # Example - /// - /// ``` - /// # use citro3d::texenv; - /// # let _runner = test_runner::GdbRunner::default(); - /// # let mut instance = citro3d::Instance::new().unwrap(); - /// let stage0 = texenv::Stage::new(0).unwrap(); - /// let texenv0 = instance.texenv(stage0); - /// ``` - #[doc(alias = "C3D_GetTexEnv")] - #[doc(alias = "C3D_TexEnvInit")] - pub fn texenv(&mut self, stage: texenv::Stage) -> &mut texenv::TexEnv { - let texenv = &mut self.texenvs[stage.0]; - texenv.get_or_init(|| TexEnv::new(stage)); - // We have to do this weird unwrap to get a mutable reference, - // since there is no `get_mut_or_init` or equivalent - texenv.get_mut().unwrap() + // Explicit drop after FrameEnd (when the GPU command buffer is flushed). + drop(pass); } } diff --git a/citro3d/src/render.rs b/citro3d/src/render.rs index bbc1f6d..bb26399 100644 --- a/citro3d/src/render.rs +++ b/citro3d/src/render.rs @@ -1,7 +1,9 @@ //! This module provides render target types and options for controlling transfer //! of data to the GPU, including the format of color and depth data to be rendered. -use std::cell::RefMut; +use std::cell::{OnceCell, RefMut}; +use std::marker::PhantomData; +use std::pin::Pin; use std::rc::Rc; use citro3d_sys::{ @@ -11,11 +13,62 @@ use ctru::services::gfx::Screen; use ctru::services::gspgpu::FramebufferFormat; use ctru_sys::{GPU_COLORBUF, GPU_DEPTHBUF}; -use crate::{Error, RenderQueue, Result}; +use crate::{ + Error, Instance, RenderQueue, Result, attrib, + buffer::{self, Index, Indices}, + light::LightEnv, + shader, + texenv::{self, TexEnv}, + uniform::{self, Uniform}, +}; pub mod effect; mod transfer; +bitflags::bitflags! { + /// Indicate whether color, depth buffer, or both values should be cleared. + #[doc(alias = "C3D_ClearBits")] + pub struct ClearFlags: u8 { + /// Clear the color of the render target. + const COLOR = citro3d_sys::C3D_CLEAR_COLOR; + /// Clear the depth buffer value of the render target. + const DEPTH = citro3d_sys::C3D_CLEAR_DEPTH; + /// Clear both color and depth buffer values of the render target. + const ALL = citro3d_sys::C3D_CLEAR_ALL; + } +} + +/// The color format to use when rendering on the GPU. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +#[doc(alias = "GPU_COLORBUF")] +pub enum ColorFormat { + /// 8-bit Red + 8-bit Green + 8-bit Blue + 8-bit Alpha. + RGBA8 = ctru_sys::GPU_RB_RGBA8, + /// 8-bit Red + 8-bit Green + 8-bit Blue. + RGB8 = ctru_sys::GPU_RB_RGB8, + /// 5-bit Red + 5-bit Green + 5-bit Blue + 1-bit Alpha. + RGBA5551 = ctru_sys::GPU_RB_RGBA5551, + /// 5-bit Red + 6-bit Green + 5-bit Blue. + RGB565 = ctru_sys::GPU_RB_RGB565, + /// 4-bit Red + 4-bit Green + 4-bit Blue + 4-bit Alpha. + RGBA4 = ctru_sys::GPU_RB_RGBA4, +} + +/// The depth buffer format to use when rendering. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +#[doc(alias = "GPU_DEPTHBUF")] +#[doc(alias = "C3D_DEPTHTYPE")] +pub enum DepthFormat { + /// 16-bit depth. + Depth16 = ctru_sys::GPU_RB_DEPTH16, + /// 24-bit depth. + Depth24 = ctru_sys::GPU_RB_DEPTH24, + /// 24-bit depth + 8-bit Stencil. + Depth24Stencil8 = ctru_sys::GPU_RB_DEPTH24_STENCIL8, +} + /// A render target for `citro3d`. Frame data will be written to this target /// to be rendered on the GPU and displayed on the screen. #[doc(alias = "C3D_RenderTarget")] @@ -27,13 +80,194 @@ pub struct Target<'screen> { _queue: Rc, } -impl Drop for Target<'_> { - #[doc(alias = "C3D_RenderTargetDelete")] - fn drop(&mut self) { +#[non_exhaustive] +#[must_use] +pub struct RenderPass<'pass> { + texenvs: [OnceCell; texenv::TEXENV_COUNT], + _phantom: PhantomData<&'pass mut Instance>, +} + +impl<'pass> RenderPass<'pass> { + pub(crate) fn new(_istance: &'pass mut Instance) -> Self { + Self { + texenvs: [ + // thank goodness there's only six of them! + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + ], + _phantom: PhantomData, + } + } + + /// Select the given render target for drawing the frame. This must be called + /// as pare of a render call (i.e. within the call to + /// [`render_frame_with`](Self::render_frame_with)). + /// + /// # Errors + /// + /// Fails if the given target cannot be used for drawing, or called outside + /// the context of a frame render. + #[doc(alias = "C3D_FrameDrawOn")] + pub fn select_render_target(&mut self, target: &'pass Target<'_>) -> Result<()> { + let _ = self; + if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } { + Ok(()) + } else { + Err(Error::InvalidRenderTarget) + } + } + + /// Get the buffer info being used, if it exists. Note that the resulting + /// [`buffer::Info`] is copied from the one currently in use. + #[doc(alias = "C3D_GetBufInfo")] + pub fn buffer_info(&self) -> Option { + let raw = unsafe { citro3d_sys::C3D_GetBufInfo() }; + buffer::Info::copy_from(raw) + } + + /// Set the buffer info to use for any following draw calls. + #[doc(alias = "C3D_SetBufInfo")] + pub fn set_buffer_info(&mut self, buffer_info: &buffer::Info) { + let raw: *const _ = &buffer_info.0; + // LIFETIME SAFETY: C3D_SetBufInfo actually copies the pointee instead of mutating it. + unsafe { citro3d_sys::C3D_SetBufInfo(raw.cast_mut()) }; + } + + /// Get the attribute info being used, if it exists. Note that the resulting + /// [`attrib::Info`] is copied from the one currently in use. + #[doc(alias = "C3D_GetAttrInfo")] + pub fn attr_info(&self) -> Option { + let raw = unsafe { citro3d_sys::C3D_GetAttrInfo() }; + attrib::Info::copy_from(raw) + } + + /// Set the attribute info to use for any following draw calls. + #[doc(alias = "C3D_SetAttrInfo")] + pub fn set_attr_info(&mut self, attr_info: &attrib::Info) { + let raw: *const _ = &attr_info.0; + // LIFETIME SAFETY: C3D_SetAttrInfo actually copies the pointee instead of mutating it. + unsafe { citro3d_sys::C3D_SetAttrInfo(raw.cast_mut()) }; + } + + /// Render primitives from the current vertex array buffer. + #[doc(alias = "C3D_DrawArrays")] + pub fn draw_arrays(&mut self, primitive: buffer::Primitive, vbo_data: buffer::Slice<'pass>) { + self.set_buffer_info(vbo_data.info()); + + // TODO: should we also require the attrib info directly here? unsafe { - C3D_RenderTargetDelete(self.raw); + citro3d_sys::C3D_DrawArrays( + primitive as ctru_sys::GPU_Primitive_t, + vbo_data.index(), + vbo_data.len(), + ); + } + } + + /// Indexed drawing. Draws the vertices in `buf` indexed by `indices`. + #[doc(alias = "C3D_DrawElements")] + pub fn draw_elements( + &mut self, + primitive: buffer::Primitive, + vbo_data: buffer::Slice<'pass>, + indices: &Indices<'pass, I>, + ) { + self.set_buffer_info(vbo_data.info()); + + let indices = &indices.buffer; + let elements = indices.as_ptr().cast(); + + unsafe { + citro3d_sys::C3D_DrawElements( + primitive as ctru_sys::GPU_Primitive_t, + indices.len().try_into().unwrap(), + // flag bit for short or byte + I::TYPE, + elements, + ); } } + + /// Use the given [`shader::Program`] for subsequent draw calls. + pub fn bind_program(&mut self, program: &'pass shader::Program) { + // SAFETY: AFAICT C3D_BindProgram just copies pointers from the given program, + // instead of mutating the pointee in any way that would cause UB + unsafe { + citro3d_sys::C3D_BindProgram(program.as_raw().cast_mut()); + } + } + + /// Binds a [`LightEnv`] for the following draw calls. + pub fn bind_light_env(&mut self, env: Option<&'pass mut Pin>>) { + unsafe { + citro3d_sys::C3D_LightEnvBind( + env.map_or(std::ptr::null_mut(), |env| env.as_mut().as_raw_mut()), + ); + } + } + + /// Bind a uniform to the given `index` in the vertex shader for the next draw call. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::uniform; + /// # use citro3d::math::Matrix4; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix4::identity(); + /// instance.bind_vertex_uniform(idx, &mtx); + /// ``` + pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Into) { + // LIFETIME SAFETY: Uniform data is copied into global buffers. + uniform.into().bind(self, shader::Type::Vertex, index); + } + + /// Bind a uniform to the given `index` in the geometry shader for the next draw call. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::uniform; + /// # use citro3d::math::Matrix4; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix4::identity(); + /// instance.bind_geometry_uniform(idx, &mtx); + /// ``` + pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Into) { + // LIFETIME SAFETY: Uniform data is copied into global buffers. + uniform.into().bind(self, shader::Type::Geometry, index); + } + + /// Retrieve the [`TexEnv`] for the given stage, initializing it first if necessary. + /// + /// # Example + /// + /// ``` + /// # use citro3d::texenv; + /// # let _runner = test_runner::GdbRunner::default(); + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let stage0 = texenv::Stage::new(0).unwrap(); + /// let texenv0 = instance.texenv(stage0); + /// ``` + #[doc(alias = "C3D_GetTexEnv")] + #[doc(alias = "C3D_TexEnvInit")] + pub fn texenv(&mut self, stage: texenv::Stage) -> &mut texenv::TexEnv { + let texenv = &mut self.texenvs[stage.0]; + texenv.get_or_init(|| TexEnv::new(stage)); + // We have to do this weird unwrap to get a mutable reference, + // since there is no `get_mut_or_init` or equivalent + texenv.get_mut().unwrap() + } } impl<'screen> Target<'screen> { @@ -98,36 +332,15 @@ impl<'screen> Target<'screen> { } } -bitflags::bitflags! { - /// Indicate whether color, depth buffer, or both values should be cleared. - #[doc(alias = "C3D_ClearBits")] - pub struct ClearFlags: u8 { - /// Clear the color of the render target. - const COLOR = citro3d_sys::C3D_CLEAR_COLOR; - /// Clear the depth buffer value of the render target. - const DEPTH = citro3d_sys::C3D_CLEAR_DEPTH; - /// Clear both color and depth buffer values of the render target. - const ALL = citro3d_sys::C3D_CLEAR_ALL; +impl Drop for Target<'_> { + #[doc(alias = "C3D_RenderTargetDelete")] + fn drop(&mut self) { + unsafe { + C3D_RenderTargetDelete(self.raw); + } } } -/// The color format to use when rendering on the GPU. -#[repr(u8)] -#[derive(Clone, Copy, Debug)] -#[doc(alias = "GPU_COLORBUF")] -pub enum ColorFormat { - /// 8-bit Red + 8-bit Green + 8-bit Blue + 8-bit Alpha. - RGBA8 = ctru_sys::GPU_RB_RGBA8, - /// 8-bit Red + 8-bit Green + 8-bit Blue. - RGB8 = ctru_sys::GPU_RB_RGB8, - /// 5-bit Red + 5-bit Green + 5-bit Blue + 1-bit Alpha. - RGBA5551 = ctru_sys::GPU_RB_RGBA5551, - /// 5-bit Red + 6-bit Green + 5-bit Blue. - RGB565 = ctru_sys::GPU_RB_RGB565, - /// 4-bit Red + 4-bit Green + 4-bit Blue + 4-bit Alpha. - RGBA4 = ctru_sys::GPU_RB_RGBA4, -} - impl From for ColorFormat { fn from(format: FramebufferFormat) -> Self { match format { @@ -141,20 +354,6 @@ impl From for ColorFormat { } } -/// The depth buffer format to use when rendering. -#[repr(u8)] -#[derive(Clone, Copy, Debug)] -#[doc(alias = "GPU_DEPTHBUF")] -#[doc(alias = "C3D_DEPTHTYPE")] -pub enum DepthFormat { - /// 16-bit depth. - Depth16 = ctru_sys::GPU_RB_DEPTH16, - /// 24-bit depth. - Depth24 = ctru_sys::GPU_RB_DEPTH24, - /// 24-bit depth + 8-bit Stencil. - Depth24Stencil8 = ctru_sys::GPU_RB_DEPTH24_STENCIL8, -} - impl DepthFormat { fn as_raw(self) -> C3D_DEPTHTYPE { C3D_DEPTHTYPE { diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs index 8737c47..48d25b5 100644 --- a/citro3d/src/uniform.rs +++ b/citro3d/src/uniform.rs @@ -4,7 +4,7 @@ use std::ops::Range; use crate::math::{FVec4, IVec, Matrix4}; -use crate::{Instance, shader}; +use crate::{RenderPass, shader}; /// The index of a uniform within a [`shader::Program`]. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -73,9 +73,9 @@ impl Uniform { /// Bind a uniform /// - /// Note: `_instance` is here to ensure unique access to the global uniform buffers + /// Note: `_pass` is here to ensure unique access to the global uniform buffers /// otherwise we could race and/or violate aliasing - pub(crate) fn bind(self, _instance: &mut Instance, ty: shader::Type, index: Index) { + pub(crate) fn bind(self, _pass: &mut RenderPass, ty: shader::Type, index: Index) { assert!( self.index_range().contains(&index), "tried to bind uniform to an invalid index (index: {:?}, valid range: {:?})", @@ -89,6 +89,7 @@ impl Uniform { self.len(), self.index_range().end ); + let set_fvs = |fs: &[FVec4]| { for (off, f) in fs.iter().enumerate() { unsafe { @@ -103,6 +104,7 @@ impl Uniform { } } }; + match self { Self::Bool(b) => unsafe { citro3d_sys::C3D_BoolUnifSet(ty.into(), index.into(), b); From e05fe1057df1d317d86c74c88cf8ff033d7eebe7 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 12:00:32 +0200 Subject: [PATCH 2/8] Fix doc links and lints in test --- citro3d/src/lib.rs | 10 +++++----- citro3d/src/render.rs | 28 ++++++++++++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index be2d7fb..4ed3d9f 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -114,9 +114,9 @@ impl Instance { render::Target::new(width, height, screen, depth_format, Rc::clone(&self.queue)) } - /// Render a frame. The passed in function/closure can mutate the instance, - /// such as to [select a render target](Self::select_render_target) - /// or [bind a new shader program](Self::bind_program). + /// Render a frame. + /// + /// The passed in function/closure can access a [`RenderPass`] to emit draw calls. #[doc(alias = "C3D_FrameBegin")] #[doc(alias = "C3D_FrameEnd")] pub fn render_frame_with<'istance: 'frame, 'frame>( @@ -170,8 +170,8 @@ mod tests { let mut instance = Instance::new().unwrap(); let target = instance.render_target(10, 10, screen, None).unwrap(); - instance.render_frame_with(|instance| { - instance.select_render_target(&target).unwrap(); + instance.render_frame_with(|mut pass| { + pass.select_render_target(&target).unwrap(); }); // Check that we don't get a double-free or use-after-free by dropping diff --git a/citro3d/src/render.rs b/citro3d/src/render.rs index bb26399..3a1a659 100644 --- a/citro3d/src/render.rs +++ b/citro3d/src/render.rs @@ -103,14 +103,11 @@ impl<'pass> RenderPass<'pass> { } } - /// Select the given render target for drawing the frame. This must be called - /// as pare of a render call (i.e. within the call to - /// [`render_frame_with`](Self::render_frame_with)). + /// Select the given render target for the following draw calls. /// /// # Errors /// - /// Fails if the given target cannot be used for drawing, or called outside - /// the context of a frame render. + /// Fails if the given target cannot be used for drawing. #[doc(alias = "C3D_FrameDrawOn")] pub fn select_render_target(&mut self, target: &'pass Target<'_>) -> Result<()> { let _ = self; @@ -121,15 +118,18 @@ impl<'pass> RenderPass<'pass> { } } - /// Get the buffer info being used, if it exists. Note that the resulting - /// [`buffer::Info`] is copied from the one currently in use. + /// Get the buffer info being used, if it exists. + /// + /// # Notes + /// + /// The resulting [`buffer::Info`] is copied (and not taken) from the one currently in use. #[doc(alias = "C3D_GetBufInfo")] pub fn buffer_info(&self) -> Option { let raw = unsafe { citro3d_sys::C3D_GetBufInfo() }; buffer::Info::copy_from(raw) } - /// Set the buffer info to use for any following draw calls. + /// Set the buffer info to use for for the following draw calls. #[doc(alias = "C3D_SetBufInfo")] pub fn set_buffer_info(&mut self, buffer_info: &buffer::Info) { let raw: *const _ = &buffer_info.0; @@ -137,8 +137,11 @@ impl<'pass> RenderPass<'pass> { unsafe { citro3d_sys::C3D_SetBufInfo(raw.cast_mut()) }; } - /// Get the attribute info being used, if it exists. Note that the resulting - /// [`attrib::Info`] is copied from the one currently in use. + /// Get the attribute info being used, if it exists. + /// + /// # Notes + /// + /// The resulting [`attrib::Info`] is copied (and not taken) from the one currently in use. #[doc(alias = "C3D_GetAttrInfo")] pub fn attr_info(&self) -> Option { let raw = unsafe { citro3d_sys::C3D_GetAttrInfo() }; @@ -168,7 +171,7 @@ impl<'pass> RenderPass<'pass> { } } - /// Indexed drawing. Draws the vertices in `buf` indexed by `indices`. + /// Draws the vertices in `buf` indexed by `indices`. #[doc(alias = "C3D_DrawElements")] pub fn draw_elements( &mut self, @@ -192,7 +195,7 @@ impl<'pass> RenderPass<'pass> { } } - /// Use the given [`shader::Program`] for subsequent draw calls. + /// Use the given [`shader::Program`] for the following draw calls. pub fn bind_program(&mut self, program: &'pass shader::Program) { // SAFETY: AFAICT C3D_BindProgram just copies pointers from the given program, // instead of mutating the pointee in any way that would cause UB @@ -318,6 +321,7 @@ impl<'screen> Target<'screen> { } /// Clear the render target with the given 32-bit RGBA color and depth buffer value. + /// /// Use `flags` to specify whether color and/or depth should be overwritten. #[doc(alias = "C3D_RenderTargetClear")] pub fn clear(&mut self, flags: ClearFlags, rgba_color: u32, depth: u32) { From 991f46a0f58071b4e07359fd6b84c9301b09face Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 12:22:08 +0200 Subject: [PATCH 3/8] Fragment lighting example with new logic --- citro3d/examples/fragment-light.rs | 102 +++++++++++++++++++---------- citro3d/src/render.rs | 6 +- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/citro3d/examples/fragment-light.rs b/citro3d/examples/fragment-light.rs index 811979a..9143234 100644 --- a/citro3d/examples/fragment-light.rs +++ b/citro3d/examples/fragment-light.rs @@ -6,7 +6,7 @@ use citro3d::{ color::Color, light::{DistanceAttenuation, LightEnv, Lut, LutId, LutInput, Material, Spotlight}, math::{AspectRatio, ClipPlanes, FVec3, Matrix4, Projection, StereoDisplacement}, - render::{self, ClearFlags}, + render::{self, ClearFlags, RenderPass}, shader, texenv, }; use citro3d_macros::include_shader; @@ -294,7 +294,6 @@ fn main() { let vertex_shader = shader.get(0).unwrap(); let program = shader::Program::new(vertex_shader).unwrap(); - instance.bind_program(&program); let mut vbo_data = Vec::with_capacity_in(VERTICES.len(), ctru::linear::LinearAllocator); vbo_data.extend_from_slice(VERTICES); @@ -344,25 +343,10 @@ fn main() { (1.0 / (0.5 * PI * d * d)).min(1.0) // We use a less aggressive attenuation to highlight the spotlight }))); - // Bind the lighting environment for use - instance.bind_light_env(Some(light_env)); - // Setup the rotating view of the cube let mut view = Matrix4::identity(); let model_idx = program.get_uniform("modelView").unwrap(); view.translate(0.0, 0.0, -2.0); - instance.bind_vertex_uniform(model_idx, view); - - let stage0 = texenv::Stage::new(0).unwrap(); - instance - .texenv(stage0) - .src( - texenv::Mode::BOTH, - texenv::Source::FragmentPrimaryColor, - Some(texenv::Source::FragmentSecondaryColor), - None, - ) - .func(texenv::Mode::BOTH, texenv::CombineFunc::Add); let projection_uniform_idx = program.get_uniform("projection").unwrap(); @@ -373,21 +357,19 @@ fn main() { break; } - instance.render_frame_with(|instance| { - let mut render_to = |target: &mut render::Target, projection| { - target.clear(ClearFlags::ALL, 0, 0); - - instance - .select_render_target(target) - .expect("failed to set render target"); - - instance.bind_vertex_uniform(projection_uniform_idx, projection); - instance.bind_vertex_uniform(model_idx, view); + instance.render_frame_with(|mut pass| { + pass.bind_program(&program); + pass.bind_light_env(Some(light_env.as_mut())); - instance.set_attr_info(&attr_info); - - instance.draw_arrays(buffer::Primitive::Triangles, vbo_data); - }; + let stage0 = texenv::Stage::new(0).unwrap(); + pass.texenv(stage0) + .src( + texenv::Mode::BOTH, + texenv::Source::FragmentPrimaryColor, + Some(texenv::Source::FragmentSecondaryColor), + None, + ) + .func(texenv::Mode::BOTH, texenv::CombineFunc::Add); let Projections { left_eye, @@ -395,9 +377,38 @@ fn main() { center, } = calculate_projections(); - render_to(&mut top_left_target, &left_eye); - render_to(&mut top_right_target, &right_eye); - render_to(&mut bottom_target, ¢er); + render_to_target( + &mut pass, + &mut top_left_target, + &left_eye, + projection_uniform_idx, + &view, + model_idx, + &attr_info, + vbo_data, + ); + render_to_target( + &mut pass, + &mut top_right_target, + &right_eye, + projection_uniform_idx, + &view, + model_idx, + &attr_info, + vbo_data, + ); + render_to_target( + &mut pass, + &mut bottom_target, + ¢er, + projection_uniform_idx, + &view, + model_idx, + &attr_info, + vbo_data, + ); + + pass }); // Rotate the modelView @@ -435,6 +446,29 @@ fn prepare_vbos<'a>( (attr_info, buf_idx) } +// Repeated render for each target. +fn render_to_target<'pass>( + pass: &mut RenderPass<'pass>, + target: &'pass mut render::Target, + projection: &Matrix4, + projection_uniform_idx: citro3d::uniform::Index, + model_view: &Matrix4, + model_uniform_idx: citro3d::uniform::Index, + attr_info: &citro3d::attrib::Info, + vbo_data: citro3d::buffer::Slice<'pass>, +) { + target.clear(ClearFlags::ALL, 0, 0); + pass.select_render_target(target) + .expect("failed to set render target"); + + pass.bind_vertex_uniform(projection_uniform_idx, projection); + pass.bind_vertex_uniform(model_uniform_idx, model_view); + + pass.set_attr_info(&attr_info); + + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); +} + struct Projections { left_eye: Matrix4, right_eye: Matrix4, diff --git a/citro3d/src/render.rs b/citro3d/src/render.rs index 3a1a659..553f3bd 100644 --- a/citro3d/src/render.rs +++ b/citro3d/src/render.rs @@ -205,11 +205,9 @@ impl<'pass> RenderPass<'pass> { } /// Binds a [`LightEnv`] for the following draw calls. - pub fn bind_light_env(&mut self, env: Option<&'pass mut Pin>>) { + pub fn bind_light_env(&mut self, env: Option>) { unsafe { - citro3d_sys::C3D_LightEnvBind( - env.map_or(std::ptr::null_mut(), |env| env.as_mut().as_raw_mut()), - ); + citro3d_sys::C3D_LightEnvBind(env.map_or(std::ptr::null_mut(), |env| env.as_raw_mut())); } } From 7ab60985244944f51a2bd6a605b82893f20d8799 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 12:30:02 +0200 Subject: [PATCH 4/8] Use Frame struct to represent FrameBegin and FrameEnd calls --- citro3d/src/lib.rs | 13 +------------ citro3d/src/render.rs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 4ed3d9f..b374a0d 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -123,20 +123,9 @@ impl Instance { &'istance mut self, f: impl FnOnce(RenderPass<'frame>) -> RenderPass<'frame>, ) { - unsafe { - citro3d_sys::C3D_FrameBegin( - // TODO: begin + end flags should be configurable - citro3d_sys::C3D_FRAME_SYNCDRAW, - ); - } - let pass = f(RenderPass::new(self)); - unsafe { - citro3d_sys::C3D_FrameEnd(0); - } - - // Explicit drop after FrameEnd (when the GPU command buffer is flushed). + // Explicit drop for FrameEnd (when the GPU command buffer is flushed). drop(pass); } } diff --git a/citro3d/src/render.rs b/citro3d/src/render.rs index 553f3bd..e3bb6b0 100644 --- a/citro3d/src/render.rs +++ b/citro3d/src/render.rs @@ -80,10 +80,13 @@ pub struct Target<'screen> { _queue: Rc, } +struct Frame; + #[non_exhaustive] #[must_use] pub struct RenderPass<'pass> { texenvs: [OnceCell; texenv::TEXENV_COUNT], + _active_frame: Frame, _phantom: PhantomData<&'pass mut Instance>, } @@ -99,6 +102,7 @@ impl<'pass> RenderPass<'pass> { OnceCell::new(), OnceCell::new(), ], + _active_frame: Frame::new(), _phantom: PhantomData, } } @@ -334,6 +338,27 @@ impl<'screen> Target<'screen> { } } +impl Frame { + fn new() -> Self { + unsafe { + citro3d_sys::C3D_FrameBegin( + // TODO: begin + end flags should be configurable + citro3d_sys::C3D_FRAME_SYNCDRAW, + ) + }; + + Self {} + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { + citro3d_sys::C3D_FrameEnd(0); + } + } +} + impl Drop for Target<'_> { #[doc(alias = "C3D_RenderTargetDelete")] fn drop(&mut self) { From 600c9b7c5e703b8c65423fe3c85e5423dfcd9a43 Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 14:58:56 +0200 Subject: [PATCH 5/8] Use cast_lifetime_to_closure trick to force renderpass lifetime --- citro3d/examples/fragment-light.rs | 78 ++++++++++-------------------- citro3d/examples/triangle.rs | 68 +++++++++++--------------- 2 files changed, 52 insertions(+), 94 deletions(-) diff --git a/citro3d/examples/fragment-light.rs b/citro3d/examples/fragment-light.rs index 9143234..850a6fe 100644 --- a/citro3d/examples/fragment-light.rs +++ b/citro3d/examples/fragment-light.rs @@ -358,6 +358,28 @@ fn main() { } instance.render_frame_with(|mut pass| { + fn cast_lifetime_to_closure<'pass, T>(x: T) -> T + where + T: Fn(&mut RenderPass<'pass>, &'pass mut render::Target<'_>, &Matrix4), + { + x + } + + let render_to = cast_lifetime_to_closure( + |pass: &mut RenderPass, target: &mut render::Target, projection| { + target.clear(ClearFlags::ALL, 0, 0); + pass.select_render_target(target) + .expect("failed to set render target"); + + pass.bind_vertex_uniform(projection_uniform_idx, projection); + pass.bind_vertex_uniform(model_idx, view); + + pass.set_attr_info(&attr_info); + + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); + }, + ); + pass.bind_program(&program); pass.bind_light_env(Some(light_env.as_mut())); @@ -377,36 +399,9 @@ fn main() { center, } = calculate_projections(); - render_to_target( - &mut pass, - &mut top_left_target, - &left_eye, - projection_uniform_idx, - &view, - model_idx, - &attr_info, - vbo_data, - ); - render_to_target( - &mut pass, - &mut top_right_target, - &right_eye, - projection_uniform_idx, - &view, - model_idx, - &attr_info, - vbo_data, - ); - render_to_target( - &mut pass, - &mut bottom_target, - ¢er, - projection_uniform_idx, - &view, - model_idx, - &attr_info, - vbo_data, - ); + render_to(&mut pass, &mut top_left_target, &left_eye); + render_to(&mut pass, &mut top_right_target, &right_eye); + render_to(&mut pass, &mut bottom_target, ¢er); pass }); @@ -446,29 +441,6 @@ fn prepare_vbos<'a>( (attr_info, buf_idx) } -// Repeated render for each target. -fn render_to_target<'pass>( - pass: &mut RenderPass<'pass>, - target: &'pass mut render::Target, - projection: &Matrix4, - projection_uniform_idx: citro3d::uniform::Index, - model_view: &Matrix4, - model_uniform_idx: citro3d::uniform::Index, - attr_info: &citro3d::attrib::Info, - vbo_data: citro3d::buffer::Slice<'pass>, -) { - target.clear(ClearFlags::ALL, 0, 0); - pass.select_render_target(target) - .expect("failed to set render target"); - - pass.bind_vertex_uniform(projection_uniform_idx, projection); - pass.bind_vertex_uniform(model_uniform_idx, model_view); - - pass.set_attr_info(&attr_info); - - pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); -} - struct Projections { left_eye: Matrix4, right_eye: Matrix4, diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 56faafc..981fa6a 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -101,6 +101,30 @@ fn main() { } instance.render_frame_with(|mut pass| { + // Sadly closures can't have lifetime specifiers, + // so we wrap `render_to` in this function to force the borrow checker rules. + fn cast_lifetime_to_closure<'pass, T>(x: T) -> T + where + T: Fn(&mut RenderPass<'pass>, &'pass mut render::Target<'_>, &Matrix4), + { + x + } + + let render_to = cast_lifetime_to_closure( + |pass: &mut RenderPass, target: &mut render::Target, projection| { + target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); + + pass.select_render_target(target) + .expect("failed to set render target"); + pass.bind_vertex_uniform(projection_uniform_idx, projection); + + pass.set_attr_info(&attr_info); + + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); + }, + ); + + // We bind the vertex shader. pass.bind_program(&program); // Configure the first fragment shading substage to just pass through the vertex color @@ -116,30 +140,9 @@ fn main() { center, } = calculate_projections(); - render_to_target( - &mut pass, - &mut top_left_target, - &left_eye, - projection_uniform_idx, - &attr_info, - vbo_data, - ); - render_to_target( - &mut pass, - &mut top_right_target, - &right_eye, - projection_uniform_idx, - &attr_info, - vbo_data, - ); - render_to_target( - &mut pass, - &mut bottom_target, - ¢er, - projection_uniform_idx, - &attr_info, - vbo_data, - ); + render_to(&mut pass, &mut top_left_target, &left_eye); + render_to(&mut pass, &mut top_right_target, &right_eye); + render_to(&mut pass, &mut bottom_target, ¢er); pass }); @@ -169,23 +172,6 @@ fn prepare_vbos<'a>( (attr_info, buf_idx) } -// Repeated render for each target. -fn render_to_target<'pass>( - pass: &mut RenderPass<'pass>, - target: &'pass mut render::Target, - projection: &Matrix4, - projection_uniform_idx: citro3d::uniform::Index, - attr_info: &citro3d::attrib::Info, - vbo_data: citro3d::buffer::Slice<'pass>, -) { - target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); - pass.select_render_target(target) - .expect("failed to set render target"); - pass.bind_vertex_uniform(projection_uniform_idx, projection); - pass.set_attr_info(attr_info); - pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); -} - struct Projections { left_eye: Matrix4, right_eye: Matrix4, From 6c8d998a0208014f30b5b07e84b611468892a39b Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 15:06:11 +0200 Subject: [PATCH 6/8] Cube example using RenderPass --- citro3d/examples/cube.rs | 54 +++++++++++++++++------------- citro3d/examples/fragment-light.rs | 40 ++++++++-------------- citro3d/examples/triangle.rs | 24 ++++++------- 3 files changed, 55 insertions(+), 63 deletions(-) diff --git a/citro3d/examples/cube.rs b/citro3d/examples/cube.rs index 48c2634..2f63e21 100644 --- a/citro3d/examples/cube.rs +++ b/citro3d/examples/cube.rs @@ -6,8 +6,8 @@ use citro3d::macros::include_shader; use citro3d::math::{ AspectRatio, ClipPlanes, CoordinateOrientation, FVec3, Matrix4, Projection, StereoDisplacement, }; -use citro3d::render::ClearFlags; -use citro3d::{attrib, buffer, render, shader, texenv}; +use citro3d::render::{ClearFlags, RenderPass, Target}; +use citro3d::{attrib, buffer, shader, texenv}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -104,7 +104,7 @@ fn main() { let vertex_shader = shader.get(0).unwrap(); let program = shader::Program::new(vertex_shader).unwrap(); - instance.bind_program(&program); + let mut vbo_data = Vec::with_capacity_in(VERTS.len(), ctru::linear::LinearAllocator); for vert in VERTS.iter().enumerate().map(|(i, v)| Vertex { pos: Vec3 { @@ -126,14 +126,6 @@ fn main() { let mut buf_info = buffer::Info::new(); let vbo_slice = buf_info.add(&vbo_data, &attr_info).unwrap(); - // Configure the first fragment shading substage to just pass through the vertex color - // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight - let stage0 = texenv::Stage::new(0).unwrap(); - instance - .texenv(stage0) - .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) - .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); - let projection_uniform_idx = program.get_uniform("projection").unwrap(); let camera_transform = Matrix4::looking_at( FVec3::new(1.8, 1.8, 1.8), @@ -158,21 +150,33 @@ fn main() { break; } - instance.render_frame_with(|instance| { - let mut render_to = |target: &mut render::Target, projection| { + instance.render_frame_with(|mut pass| { + fn cast_lifetime_to_closure<'pass, T>(x: T) -> T + where + T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), + { + x + } + + let render_to = cast_lifetime_to_closure(|pass, target, projection| { target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); - instance - .select_render_target(target) + pass.select_render_target(target) .expect("failed to set render target"); - instance.bind_vertex_uniform(projection_uniform_idx, projection * camera_transform); + pass.bind_vertex_uniform(projection_uniform_idx, projection * camera_transform); + + pass.set_attr_info(&attr_info); - instance.set_attr_info(&attr_info); - unsafe { - instance.draw_elements(buffer::Primitive::Triangles, vbo_slice, &index_buffer); - } - }; + pass.draw_elements(buffer::Primitive::Triangles, vbo_slice, &index_buffer); + }); + + pass.bind_program(&program); + + let stage0 = texenv::Stage::new(0).unwrap(); + pass.texenv(stage0) + .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) + .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); let Projections { left_eye, @@ -180,9 +184,11 @@ fn main() { center, } = calculate_projections(); - render_to(&mut top_left_target, &left_eye); - render_to(&mut top_right_target, &right_eye); - render_to(&mut bottom_target, ¢er); + render_to(&mut pass, &mut top_left_target, &left_eye); + render_to(&mut pass, &mut top_right_target, &right_eye); + render_to(&mut pass, &mut bottom_target, ¢er); + + pass }); } } diff --git a/citro3d/examples/fragment-light.rs b/citro3d/examples/fragment-light.rs index 850a6fe..15e238d 100644 --- a/citro3d/examples/fragment-light.rs +++ b/citro3d/examples/fragment-light.rs @@ -6,7 +6,7 @@ use citro3d::{ color::Color, light::{DistanceAttenuation, LightEnv, Lut, LutId, LutInput, Material, Spotlight}, math::{AspectRatio, ClipPlanes, FVec3, Matrix4, Projection, StereoDisplacement}, - render::{self, ClearFlags, RenderPass}, + render::{ClearFlags, DepthFormat, RenderPass, Target}, shader, texenv, }; use citro3d_macros::include_shader; @@ -260,22 +260,12 @@ fn main() { let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer(); let mut top_left_target = instance - .render_target( - width, - height, - top_left, - Some(render::DepthFormat::Depth24Stencil8), - ) + .render_target(width, height, top_left, Some(DepthFormat::Depth24Stencil8)) .expect("failed to create render target"); let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); let mut top_right_target = instance - .render_target( - width, - height, - top_right, - Some(render::DepthFormat::Depth24Stencil8), - ) + .render_target(width, height, top_right, Some(DepthFormat::Depth24Stencil8)) .expect("failed to create render target"); let mut bottom_screen = gfx.bottom_screen.borrow_mut(); @@ -286,7 +276,7 @@ fn main() { width, height, bottom_screen, - Some(render::DepthFormat::Depth24Stencil8), + Some(DepthFormat::Depth24Stencil8), ) .expect("failed to create bottom screen render target"); @@ -360,25 +350,23 @@ fn main() { instance.render_frame_with(|mut pass| { fn cast_lifetime_to_closure<'pass, T>(x: T) -> T where - T: Fn(&mut RenderPass<'pass>, &'pass mut render::Target<'_>, &Matrix4), + T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), { x } - let render_to = cast_lifetime_to_closure( - |pass: &mut RenderPass, target: &mut render::Target, projection| { - target.clear(ClearFlags::ALL, 0, 0); - pass.select_render_target(target) - .expect("failed to set render target"); + let render_to = cast_lifetime_to_closure(|pass, target, projection| { + target.clear(ClearFlags::ALL, 0, 0); + pass.select_render_target(target) + .expect("failed to set render target"); - pass.bind_vertex_uniform(projection_uniform_idx, projection); - pass.bind_vertex_uniform(model_idx, view); + pass.bind_vertex_uniform(projection_uniform_idx, projection); + pass.bind_vertex_uniform(model_idx, view); - pass.set_attr_info(&attr_info); + pass.set_attr_info(&attr_info); - pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); - }, - ); + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); + }); pass.bind_program(&program); pass.bind_light_env(Some(light_env.as_mut())); diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 981fa6a..ebd66a6 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -5,9 +5,9 @@ use citro3d::macros::include_shader; use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement}; -use citro3d::render::{ClearFlags, RenderPass}; +use citro3d::render::{ClearFlags, RenderPass, Target}; use citro3d::texenv; -use citro3d::{attrib, buffer, render, shader}; +use citro3d::{attrib, buffer, shader}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -105,24 +105,22 @@ fn main() { // so we wrap `render_to` in this function to force the borrow checker rules. fn cast_lifetime_to_closure<'pass, T>(x: T) -> T where - T: Fn(&mut RenderPass<'pass>, &'pass mut render::Target<'_>, &Matrix4), + T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), { x } - let render_to = cast_lifetime_to_closure( - |pass: &mut RenderPass, target: &mut render::Target, projection| { - target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); + let render_to = cast_lifetime_to_closure(|pass, target, projection| { + target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); - pass.select_render_target(target) - .expect("failed to set render target"); - pass.bind_vertex_uniform(projection_uniform_idx, projection); + pass.select_render_target(target) + .expect("failed to set render target"); + pass.bind_vertex_uniform(projection_uniform_idx, projection); - pass.set_attr_info(&attr_info); + pass.set_attr_info(&attr_info); - pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); - }, - ); + pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); + }); // We bind the vertex shader. pass.bind_program(&program); From e5fd46a0187e3687c2993af6dc7a7c3f1948eb2a Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Sun, 31 Aug 2025 15:10:42 +0200 Subject: [PATCH 7/8] Return pass in lib test --- citro3d/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index b374a0d..9d0d39d 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -161,6 +161,8 @@ mod tests { instance.render_frame_with(|mut pass| { pass.select_render_target(&target).unwrap(); + + pass }); // Check that we don't get a double-free or use-after-free by dropping From f0bd5b0b6b035688bcd90babb45c009bbd792d5c Mon Sep 17 00:00:00 2001 From: Andrea Ciliberti Date: Tue, 2 Sep 2025 10:55:40 +0200 Subject: [PATCH 8/8] Add non-exhaustive Drop impl for RenderPass --- citro3d/src/render.rs | 121 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/citro3d/src/render.rs b/citro3d/src/render.rs index e3bb6b0..995ae66 100644 --- a/citro3d/src/render.rs +++ b/citro3d/src/render.rs @@ -87,11 +87,17 @@ struct Frame; pub struct RenderPass<'pass> { texenvs: [OnceCell; texenv::TEXENV_COUNT], _active_frame: Frame, + + // It is not valid behaviour to bind anything but a correct shader program. + // Instead of binding NULL, we simply force the user to have a shader program bound again + // before any draw calls. + is_program_bound: bool, + _phantom: PhantomData<&'pass mut Instance>, } impl<'pass> RenderPass<'pass> { - pub(crate) fn new(_istance: &'pass mut Instance) -> Self { + pub(crate) fn new(_instance: &'pass mut Instance) -> Self { Self { texenvs: [ // thank goodness there's only six of them! @@ -103,6 +109,7 @@ impl<'pass> RenderPass<'pass> { OnceCell::new(), ], _active_frame: Frame::new(), + is_program_bound: false, _phantom: PhantomData, } } @@ -161,8 +168,17 @@ impl<'pass> RenderPass<'pass> { } /// Render primitives from the current vertex array buffer. + /// + /// # Panics + /// + /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). #[doc(alias = "C3D_DrawArrays")] pub fn draw_arrays(&mut self, primitive: buffer::Primitive, vbo_data: buffer::Slice<'pass>) { + // TODO: Decide whether it's worth returning an `Error` instead of panicking. + if !self.is_program_bound { + panic!("tried todraw arrays when no shader program is bound"); + } + self.set_buffer_info(vbo_data.info()); // TODO: should we also require the attrib info directly here? @@ -176,6 +192,10 @@ impl<'pass> RenderPass<'pass> { } /// Draws the vertices in `buf` indexed by `indices`. + /// + /// # Panics + /// + /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). #[doc(alias = "C3D_DrawElements")] pub fn draw_elements( &mut self, @@ -183,6 +203,10 @@ impl<'pass> RenderPass<'pass> { vbo_data: buffer::Slice<'pass>, indices: &Indices<'pass, I>, ) { + if !self.is_program_bound { + panic!("tried to draw elements when no shader program is bound"); + } + self.set_buffer_info(vbo_data.info()); let indices = &indices.buffer; @@ -206,6 +230,8 @@ impl<'pass> RenderPass<'pass> { unsafe { citro3d_sys::C3D_BindProgram(program.as_raw().cast_mut()); } + + self.is_program_bound = true; } /// Binds a [`LightEnv`] for the following draw calls. @@ -217,6 +243,10 @@ impl<'pass> RenderPass<'pass> { /// Bind a uniform to the given `index` in the vertex shader for the next draw call. /// + /// # Panics + /// + /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). + /// /// # Example /// /// ``` @@ -230,12 +260,20 @@ impl<'pass> RenderPass<'pass> { /// instance.bind_vertex_uniform(idx, &mtx); /// ``` pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Into) { + if !self.is_program_bound { + panic!("tried to bind vertex uniform when no shader program is bound"); + } + // LIFETIME SAFETY: Uniform data is copied into global buffers. uniform.into().bind(self, shader::Type::Vertex, index); } /// Bind a uniform to the given `index` in the geometry shader for the next draw call. /// + /// # Panics + /// + /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). + /// /// # Example /// /// ``` @@ -249,6 +287,10 @@ impl<'pass> RenderPass<'pass> { /// instance.bind_geometry_uniform(idx, &mtx); /// ``` pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Into) { + if !self.is_program_bound { + panic!("tried to bind geometry uniform when no shader program is bound"); + } + // LIFETIME SAFETY: Uniform data is copied into global buffers. uniform.into().bind(self, shader::Type::Geometry, index); } @@ -388,3 +430,80 @@ impl DepthFormat { } } } + +impl Drop for RenderPass<'_> { + fn drop(&mut self) { + unsafe { + // TODO: substitute as many as possible with safe wrappers. + // These resets are derived from the implementation of `C3D_Init` and by studying the `C3D_Context` struct. + citro3d_sys::C3D_DepthMap(true, -1.0, 0.0); + citro3d_sys::C3D_CullFace(ctru_sys::GPU_CULL_BACK_CCW); + citro3d_sys::C3D_StencilTest(false, ctru_sys::GPU_ALWAYS, 0x00, 0xFF, 0x00); + citro3d_sys::C3D_StencilOp( + ctru_sys::GPU_STENCIL_KEEP, + ctru_sys::GPU_STENCIL_KEEP, + ctru_sys::GPU_STENCIL_KEEP, + ); + citro3d_sys::C3D_BlendingColor(0); + citro3d_sys::C3D_EarlyDepthTest(false, ctru_sys::GPU_EARLYDEPTH_GREATER, 0); + citro3d_sys::C3D_DepthTest(true, ctru_sys::GPU_GREATER, ctru_sys::GPU_WRITE_ALL); + citro3d_sys::C3D_AlphaTest(false, ctru_sys::GPU_ALWAYS, 0x00); + citro3d_sys::C3D_AlphaBlend( + ctru_sys::GPU_BLEND_ADD, + ctru_sys::GPU_BLEND_ADD, + ctru_sys::GPU_SRC_ALPHA, + ctru_sys::GPU_ONE_MINUS_SRC_ALPHA, + ctru_sys::GPU_SRC_ALPHA, + ctru_sys::GPU_ONE_MINUS_SRC_ALPHA, + ); + citro3d_sys::C3D_FragOpMode(ctru_sys::GPU_FRAGOPMODE_GL); + citro3d_sys::C3D_FragOpShadow(0.0, 1.0); + + // The texCoordId has no importance since we are binding NULL + citro3d_sys::C3D_ProcTexBind(0, std::ptr::null_mut()); + + // ctx->texConfig = BIT(12); I have not found a way to replicate this one yet (maybe not necessary because of texenv's unbinding). + + // ctx->texShadow = BIT(0); + citro3d_sys::C3D_TexShadowParams(true, 0.0); + + // ctx->texEnvBuf = 0; I have not found a way to replicate this one yet (maybe not necessary because of texenv's unbinding). + + // ctx->texEnvBufClr = 0xFFFFFFFF; + citro3d_sys::C3D_TexEnvBufColor(0xFFFFFFFF); + // ctx->fogClr = 0; + citro3d_sys::C3D_FogColor(0); + //ctx->fogLut = NULL; + citro3d_sys::C3D_FogLutBind(std::ptr::null_mut()); + + // We don't need to unbind programs (and in citro3D you can't), + // since the user is forced to bind them again before drawing next time they render. + + self.bind_light_env(None); + + // TODO: C3D_TexBind doesn't work for NULL + // https://github.com/devkitPro/citro3d/blob/9f21cf7b380ce6f9e01a0420f19f0763e5443ca7/source/texture.c#L222 + /*for i in 0..3 { + citro3d_sys::C3D_TexBind(i, std::ptr::null_mut()); + }*/ + + for i in 0..6 { + self.texenv(texenv::Stage::new(i).unwrap()).reset(); + } + + // Unbind attribute information (can't use NULL pointer, so we use an empty attrib::Info instead). + // + // TODO: Drawing nothing actually hangs the GPU, so this code is never really helpful (also, not used since the flag makes it a non-issue). + // Is it worth keeping? Could hanging be considered better than an ARM exception? + let empty_info = attrib::Info::default(); + self.set_attr_info(&empty_info); + + // ctx->fixedAttribDirty = 0; + // ctx->fixedAttribEverDirty = 0; + for i in 0..12 { + let vec = citro3d_sys::C3D_FixedAttribGetWritePtr(i); + (*vec).c = [0.0, 0.0, 0.0, 0.0]; + } + } + } +}