diff --git a/citro3d-sys/bindgen.sh b/citro3d-sys/bindgen.sh deleted file mode 100755 index ebb4ff9..0000000 --- a/citro3d-sys/bindgen.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -cargo run --package bindgen-citro3d > src/bindings.rs diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 9412e59..305a9f8 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -6,6 +6,7 @@ use citro3d::macros::include_shader; use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement}; use citro3d::render::ClearFlags; +use citro3d::texenv; use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -33,20 +34,21 @@ struct Vertex { static VERTICES: &[Vertex] = &[ Vertex { - pos: Vec3::new(0.0, 0.5, 3.0), + pos: Vec3::new(0.0, 0.5, -3.0), color: Vec3::new(1.0, 0.0, 0.0), }, Vertex { - pos: Vec3::new(-0.5, -0.5, 3.0), + pos: Vec3::new(-0.5, -0.5, -3.0), color: Vec3::new(0.0, 1.0, 0.0), }, Vertex { - pos: Vec3::new(0.5, -0.5, 3.0), + pos: Vec3::new(0.5, -0.5, -3.0), color: Vec3::new(0.0, 0.0, 1.0), }, ]; static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); +const CLEAR_COLOR: u32 = 0x68_B0_D8_FF; fn main() { let mut soc = Soc::new().expect("failed to get SOC"); @@ -79,15 +81,22 @@ fn main() { let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap(); let vertex_shader = shader.get(0).unwrap(); - let mut program = shader::Program::new(vertex_shader).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); let mut buf_info = buffer::Info::new(); - let (attr_info, vbo_idx) = prepare_vbos(&mut buf_info, &vbo_data); + let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); - scene_init(&mut program); + // 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(); @@ -100,18 +109,17 @@ fn main() { 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"); - let clear_color: u32 = 0x7F_7F_7F_FF; - target.clear(ClearFlags::ALL, clear_color, 0); - instance.bind_vertex_uniform(projection_uniform_idx, projection); instance.set_attr_info(&attr_info); - instance.draw_arrays(buffer::Primitive::Triangles, vbo_idx); + instance.draw_arrays(buffer::Primitive::Triangles, vbo_data); }; let Projections { @@ -127,15 +135,10 @@ fn main() { } } -// sheeeesh, this sucks to type: -fn prepare_vbos<'buf, 'info, 'vbo>( - buf_info: &'info mut buffer::Info, - vbo_data: &'vbo [Vertex], -) -> (attrib::Info, buffer::Slice<'buf>) -where - 'info: 'buf, - 'vbo: 'buf, -{ +fn prepare_vbos<'a>( + buf_info: &'a mut buffer::Info, + vbo_data: &'a [Vertex], +) -> (attrib::Info, buffer::Slice<'a>) { // Configure attributes for use with the vertex shader let mut attr_info = attrib::Info::new(); @@ -190,23 +193,3 @@ fn calculate_projections() -> Projections { center, } } - -fn scene_init(program: &mut shader::Program) { - // Load the vertex shader, create a shader program and bind it - unsafe { - citro3d_sys::C3D_BindProgram(program.as_raw_mut()); - - // 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 env = citro3d_sys::C3D_GetTexEnv(0); - citro3d_sys::C3D_TexEnvInit(env); - citro3d_sys::C3D_TexEnvSrc( - env, - citro3d_sys::C3D_Both, - ctru_sys::GPU_PRIMARY_COLOR, - 0, - 0, - ); - citro3d_sys::C3D_TexEnvFunc(env, citro3d_sys::C3D_Both, ctru_sys::GPU_REPLACE); - } -} diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index a100e31..c18e6fc 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -15,10 +15,15 @@ pub mod error; pub mod math; pub mod render; pub mod shader; +pub mod texenv; pub mod uniform; +use std::cell::OnceCell; +use std::fmt; + pub use error::{Error, Result}; +use self::texenv::TexEnv; use self::uniform::Uniform; pub mod macros { @@ -30,8 +35,15 @@ pub mod macros { /// should instantiate to use this library. #[non_exhaustive] #[must_use] -#[derive(Debug)] -pub struct Instance; +pub struct Instance { + texenvs: [OnceCell; texenv::TEXENV_COUNT], +} + +impl fmt::Debug for Instance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Instance").finish_non_exhaustive() + } +} impl Instance { /// Initialize the default `citro3d` instance. @@ -51,7 +63,17 @@ impl Instance { #[doc(alias = "C3D_Init")] pub fn with_cmdbuf_size(size: usize) -> Result { if unsafe { citro3d_sys::C3D_Init(size) } { - Ok(Self) + Ok(Self { + texenvs: [ + // thank goodness there's only six of them! + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + OnceCell::new(), + ], + }) } else { Err(Error::FailedToInitialize) } @@ -73,7 +95,8 @@ impl Instance { } /// Render a frame. The passed in function/closure can mutate the instance, - /// such as to [select a render target](Self::select_render_target). + /// 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)) { @@ -125,20 +148,29 @@ impl Instance { /// Render primitives from the current vertex array buffer. #[doc(alias = "C3D_DrawArrays")] - pub fn draw_arrays(&mut self, primitive: buffer::Primitive, index: buffer::Slice) { - self.set_buffer_info(index.info()); + 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, - index.index(), - index.len(), + vbo_data.index(), + vbo_data.len(), ); } } + /// 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()); + } + } + /// Bind a uniform to the given `index` in the vertex shader for the next draw call. /// /// # Example @@ -174,6 +206,27 @@ impl Instance { pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) { uniform.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 Drop for Instance { diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs index 0ec72e5..f694ad6 100644 --- a/citro3d/src/math/projection.rs +++ b/citro3d/src/math/projection.rs @@ -26,14 +26,42 @@ impl Projection { /// Set the coordinate system's orientation for the projection. /// See [`CoordinateOrientation`] for more details. - pub fn coordinates(&mut self, orientation: CoordinateOrientation) -> &mut Self { + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::{Projection, AspectRatio, CoordinateOrientation, Matrix4, ClipPlanes}; + /// let clip_planes = ClipPlanes { + /// near: 0.1, + /// far: 100.0, + /// }; + /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes) + /// .coordinates(CoordinateOrientation::LeftHanded) + /// .into(); + /// ``` + pub fn coordinates(mut self, orientation: CoordinateOrientation) -> Self { self.coordinates = orientation; self } /// Set the screen rotation for the projection. /// See [`ScreenOrientation`] for more details. - pub fn screen(&mut self, orientation: ScreenOrientation) -> &mut Self { + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::{Projection, AspectRatio, ScreenOrientation, Matrix4, ClipPlanes}; + /// let clip_planes = ClipPlanes { + /// near: 0.1, + /// far: 100.0, + /// }; + /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes) + /// .screen(ScreenOrientation::None) + /// .into(); + /// ``` + pub fn screen(mut self, orientation: ScreenOrientation) -> Self { self.rotation = orientation; self } diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index c2042bf..9f1de0b 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -17,6 +17,7 @@ use crate::uniform; /// /// The PICA200 does not support user-programmable fragment shaders. #[doc(alias = "shaderProgram_s")] +#[must_use] pub struct Program { program: ctru_sys::shaderProgram_s, } @@ -102,18 +103,13 @@ impl Program { pub(crate) fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { &self.program } - - // TODO: pub(crate) - pub fn as_raw_mut(&mut self) -> *mut ctru_sys::shaderProgram_s { - &mut self.program - } } impl Drop for Program { #[doc(alias = "shaderProgramFree")] fn drop(&mut self) { unsafe { - let _ = ctru_sys::shaderProgramFree(self.as_raw_mut()); + let _ = ctru_sys::shaderProgramFree(self.as_raw().cast_mut()); } } } diff --git a/citro3d/src/texenv.rs b/citro3d/src/texenv.rs new file mode 100644 index 0000000..055366d --- /dev/null +++ b/citro3d/src/texenv.rs @@ -0,0 +1,132 @@ +//! Texture combiner support. See +//! for more details. + +use bitflags::bitflags; + +/// A texture combiner, also called a "texture environment" (hence the struct name). +/// See also [`texenv.h` documentation](https://oreo639.github.io/citro3d/texenv_8h.html). +#[doc(alias = "C3D_TexEnv")] +pub struct TexEnv(*mut citro3d_sys::C3D_TexEnv); + +// https://oreo639.github.io/citro3d/texenv_8h.html#a9eda91f8e7252c91f873b1d43e3728b6 +pub(crate) const TEXENV_COUNT: usize = 6; + +impl TexEnv { + pub(crate) fn new(stage: Stage) -> Self { + let mut result = unsafe { Self(citro3d_sys::C3D_GetTexEnv(stage.0 as _)) }; + result.reset(); + result + } + + /// Re-initialize the texture combiner to its default state. + pub fn reset(&mut self) { + unsafe { + citro3d_sys::C3D_TexEnvInit(self.0); + } + } + + /// Configure the source values of the texture combiner. + /// + /// # Parameters + /// + /// - `mode`: which [`Mode`]\(s) to set the sourc operand(s) for. + /// - `source0`: the first [`Source`] operand to the texture combiner + /// - `source1` and `source2`: optional additional [`Source`] operands to use + #[doc(alias = "C3D_TexEnvSrc")] + pub fn src( + &mut self, + mode: Mode, + source0: Source, + source1: Option, + source2: Option, + ) -> &mut Self { + unsafe { + citro3d_sys::C3D_TexEnvSrc( + self.0, + mode.bits(), + source0 as _, + source1.unwrap_or(Source::PrimaryColor) as _, + source2.unwrap_or(Source::PrimaryColor) as _, + ); + } + self + } + + /// Configure the texture combination function. + /// + /// # Parameters + /// + /// - `mode`: the [`Mode`]\(s) the combination function will apply to. + /// - `func`: the [`CombineFunc`] used to combine textures. + #[doc(alias = "C3D_TexEnvFunc")] + pub fn func(&mut self, mode: Mode, func: CombineFunc) -> &mut Self { + unsafe { + citro3d_sys::C3D_TexEnvFunc(self.0, mode.bits(), func as _); + } + + self + } +} + +bitflags! { + /// Whether to operate on colors, alpha values, or both. + #[doc(alias = "C3D_TexEnvMode")] + pub struct Mode: citro3d_sys::C3D_TexEnvMode { + #[allow(missing_docs)] + const RGB = citro3d_sys::C3D_RGB; + #[allow(missing_docs)] + const ALPHA = citro3d_sys::C3D_Alpha; + #[allow(missing_docs)] + const BOTH = citro3d_sys::C3D_Both; + } +} + +/// A source operand of a [`TexEnv`]'s texture combination. +#[doc(alias = "GPU_TEVSRC")] +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +#[non_exhaustive] +pub enum Source { + PrimaryColor = ctru_sys::GPU_PRIMARY_COLOR, + FragmentPrimaryColor = ctru_sys::GPU_FRAGMENT_PRIMARY_COLOR, + FragmentSecondaryColor = ctru_sys::GPU_FRAGMENT_SECONDARY_COLOR, + Texture0 = ctru_sys::GPU_TEXTURE0, + Texture1 = ctru_sys::GPU_TEXTURE1, + Texture2 = ctru_sys::GPU_TEXTURE2, + Texture3 = ctru_sys::GPU_TEXTURE3, + PreviousBuffer = ctru_sys::GPU_PREVIOUS_BUFFER, + Constant = ctru_sys::GPU_CONSTANT, + Previous = ctru_sys::GPU_PREVIOUS, +} + +/// The combination function to apply to the [`TexEnv`] operands. +#[doc(alias = "GPU_COMBINEFUNC")] +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +#[non_exhaustive] +pub enum CombineFunc { + Replace = ctru_sys::GPU_REPLACE, + Modulate = ctru_sys::GPU_MODULATE, + Add = ctru_sys::GPU_ADD, + AddSigned = ctru_sys::GPU_ADD_SIGNED, + Interpolate = ctru_sys::GPU_INTERPOLATE, + Subtract = ctru_sys::GPU_SUBTRACT, + Dot3Rgb = ctru_sys::GPU_DOT3_RGB, + // Added in libcrtu 2.3.0: + // Dot3Rgba = ctru_sys::GPU_DOT3_RGBA, +} + +/// A texture combination stage identifier. This index doubles as the order +/// in which texture combinations will be applied. +// (I think?) +#[derive(Copy, Clone, Debug)] +pub struct Stage(pub(crate) usize); + +impl Stage { + /// Get a stage index. Valid indices range from 0 to 5. + pub fn new(index: usize) -> Option { + (index < 6).then_some(Self(index)) + } +}