diff --git a/citro3d-macros/src/lib.rs b/citro3d-macros/src/lib.rs index 3d77b3f..4e52c88 100644 --- a/citro3d-macros/src/lib.rs +++ b/citro3d-macros/src/lib.rs @@ -1,3 +1,5 @@ +//! Procedural macro helpers for `citro3d`. + // we're already nightly-only so might as well use unstable proc macro APIs. #![feature(proc_macro_span)] diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 8f3522f..b367906 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -3,13 +3,10 @@ #![feature(allocator_api)] -use std::ffi::CStr; -use std::mem::MaybeUninit; - use citro3d::macros::include_shader; -use citro3d::render::{self, ClearFlags}; -use citro3d::{attrib, buffer, shader}; -use citro3d_sys::C3D_Mtx; +use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement}; +use citro3d::render::ClearFlags; +use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -92,7 +89,10 @@ fn main() { let mut buf_info = buffer::Info::new(); let (attr_info, vbo_idx) = prepare_vbos(&mut buf_info, &vbo_data); - let projection_uniform_idx = scene_init(&mut program); + scene_init(&mut program); + + let projection_uniform_idx = program.get_uniform("projection").unwrap(); + while apt.main_loop() { hid.scan_input(); @@ -109,14 +109,7 @@ fn main() { let clear_color: u32 = 0x7F_7F_7F_FF; target.clear(ClearFlags::ALL, clear_color, 0); - unsafe { - // Update the uniforms - citro3d_sys::C3D_FVUnifMtx4x4( - ctru_sys::GPU_VERTEX_SHADER, - projection_uniform_idx.into(), - projection, - ); - } + instance.bind_vertex_uniform(projection_uniform_idx, projection); instance.set_attr_info(&attr_info); @@ -124,13 +117,13 @@ fn main() { }; let Projections { - left, - right, + left_eye, + right_eye, center, } = calculate_projections(); - render_to(&mut top_left_target, &left); - render_to(&mut top_right_target, &right); + render_to(&mut top_left_target, &left_eye); + render_to(&mut top_right_target, &right_eye); render_to(&mut bottom_target, ¢er); }); } @@ -165,70 +158,45 @@ where } struct Projections { - left: C3D_Mtx, - right: C3D_Mtx, - center: C3D_Mtx, + left_eye: Matrix, + right_eye: Matrix, + center: Matrix, } fn calculate_projections() -> Projections { - let mut left_eye = MaybeUninit::uninit(); - let mut right_eye = MaybeUninit::uninit(); - let mut center = MaybeUninit::uninit(); - // TODO: it would be cool to allow playing around with these parameters on // the fly with D-pad, etc. let slider_val = unsafe { ctru_sys::osGet3DSliderState() }; - let iod = slider_val / 4.0; + let interocular_distance = slider_val / 2.0; - let near = 0.01; - let far = 100.0; - let fovy = 40.0_f32.to_radians(); - let screen = 2.0; + let vertical_fov = 40.0_f32.to_radians(); + let screen_depth = 2.0; - unsafe { - citro3d_sys::Mtx_PerspStereoTilt( - left_eye.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioTop as f32, - near, - far, - -iod, - screen, - true, - ); + let clip_planes = ClipPlanes { + near: 0.01, + far: 100.0, + }; - citro3d_sys::Mtx_PerspStereoTilt( - right_eye.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioTop as f32, - near, - far, - iod, - screen, - true, - ); + let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); - citro3d_sys::Mtx_PerspTilt( - center.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioBot as f32, - near, - far, - true, - ); + let (left_eye, right_eye) = + Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) + .stereo_matrices(left, right); - Projections { - left: left_eye.assume_init(), - right: right_eye.assume_init(), - center: center.assume_init(), - } + let center = + Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); + + Projections { + left_eye, + right_eye, + center, } } -fn scene_init(program: &mut shader::Program) -> i8 { +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()); + 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 @@ -242,13 +210,5 @@ fn scene_init(program: &mut shader::Program) -> i8 { 0, ); citro3d_sys::C3D_TexEnvFunc(env, citro3d_sys::C3D_Both, ctru_sys::GPU_REPLACE); - - // Get the location of the uniforms - let projection_name = CStr::from_bytes_with_nul(b"projection\0").unwrap(); - - ctru_sys::shaderInstanceGetUniformLocation( - (*program.as_raw()).vertexShader, - projection_name.as_ptr(), - ) } } diff --git a/citro3d/src/buffer.rs b/citro3d/src/buffer.rs index 534a487..b86be01 100644 --- a/citro3d/src/buffer.rs +++ b/citro3d/src/buffer.rs @@ -100,7 +100,6 @@ impl Info { /// /// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator /// * if the maximum number (12) of VBOs are already registered - /// pub fn add<'this, 'vbo, 'idx, T>( &'this mut self, vbo_data: &'vbo [T], diff --git a/citro3d/src/error.rs b/citro3d/src/error.rs index 29acd73..9a99089 100644 --- a/citro3d/src/error.rs +++ b/citro3d/src/error.rs @@ -1,5 +1,6 @@ //! General-purpose error and result types returned by public APIs of this crate. +use std::ffi::NulError; use std::num::TryFromIntError; use std::sync::TryLockError; @@ -31,6 +32,10 @@ pub enum Error { /// The given memory could not be converted to a physical address for sharing /// with the GPU. Data should be allocated with [`ctru::linear`]. InvalidMemoryLocation, + /// The given name was not valid for the requested purpose. + InvalidName, + /// The requested resource could not be found. + NotFound, } impl From for Error { @@ -44,3 +49,9 @@ impl From> for Error { Self::LockHeld } } + +impl From for Error { + fn from(_: NulError) -> Self { + Self::InvalidName + } +} diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index c7dbe0e..0ba340d 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -5,12 +5,15 @@ pub mod attrib; pub mod buffer; pub mod error; +pub mod math; pub mod render; pub mod shader; +pub mod uniform; -use citro3d_sys::C3D_FrameDrawOn; pub use error::{Error, Result}; +use self::uniform::Uniform; + pub mod macros { //! Helper macros for working with shaders. pub use citro3d_macros::*; @@ -53,7 +56,7 @@ impl Instance { /// Fails if the given target cannot be used for drawing. pub fn select_render_target(&mut self, target: &render::Target<'_>) -> Result<()> { let _ = self; - if unsafe { C3D_FrameDrawOn(target.as_raw()) } { + if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } { Ok(()) } else { Err(Error::InvalidRenderTarget) @@ -121,6 +124,42 @@ impl Instance { ); } } + + /// 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::Matrix; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix::identity(); + /// instance.bind_vertex_uniform(idx, &mtx); + /// ``` + pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) { + uniform.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::Matrix; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix::identity(); + /// instance.bind_geometry_uniform(idx, &mtx); + /// ``` + pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) { + uniform.bind(self, shader::Type::Geometry, index); + } } impl Drop for Instance { diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs new file mode 100644 index 0000000..479ef71 --- /dev/null +++ b/citro3d/src/math.rs @@ -0,0 +1,185 @@ +//! Safe wrappers for working with matrix and vector types provided by `citro3d`. + +use std::mem::MaybeUninit; + +mod projection; + +pub use projection::{Orthographic, Perspective, Projection}; + +/// A 4-vector of `u8`s. +#[doc(alias = "C3D_IVec")] +pub struct IVec(citro3d_sys::C3D_IVec); + +/// A 4-vector of `f32`s. +#[doc(alias = "C3D_FVec")] +pub struct FVec(citro3d_sys::C3D_FVec); + +/// A quaternion, internally represented the same way as [`FVec`]. +#[doc(alias = "C3D_FQuat")] +pub struct FQuat(citro3d_sys::C3D_FQuat); + +/// A 4x4 row-major matrix of `f32`s. +#[doc(alias = "C3D_Mtx")] +pub struct Matrix(citro3d_sys::C3D_Mtx); + +impl Matrix { + /// Construct the zero matrix. + #[doc(alias = "Mtx_Zeros")] + pub fn zero() -> Self { + // TODO: should this also be Default::default()? + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Zeros(out.as_mut_ptr()); + Self(out.assume_init()) + } + } + + /// Construct the identity matrix. + #[doc(alias = "Mtx_Identity")] + pub fn identity() -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Identity(out.as_mut_ptr()); + Self(out.assume_init()) + } + } + + pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { + &self.0 + } +} + +// region: Projection configuration +// +// TODO: maybe move into `mod projection`, or hoist `projection::*` into here. +// it will probably mostly depend on how big all the matrices/vec impls get. +// Also worth considering is whether `mod projection` should be pub. + +/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry)) +/// (or "handedness") of the coordinate system. Coordinates are always +Y-up, +/// +X-right. +#[derive(Clone, Copy, Debug)] +pub enum CoordinateOrientation { + /// A left-handed coordinate system. +Z points into the screen. + LeftHanded, + /// A right-handed coordinate system. +Z points out of the screen. + RightHanded, +} + +impl CoordinateOrientation { + pub(crate) fn is_left_handed(self) -> bool { + matches!(self, Self::LeftHanded) + } +} + +impl Default for CoordinateOrientation { + /// This is an opinionated default, but [`RightHanded`](Self::RightHanded) + /// seems to be the preferred coordinate system for most + /// [examples](https://github.com/devkitPro/3ds-examples) + /// from upstream, and is also fairly common in other applications. + fn default() -> Self { + Self::RightHanded + } +} + +/// Whether to rotate a projection to account for the 3DS screen orientation. +/// Both screens on the 3DS are oriented such that the "top-left" of the screen +/// in framebuffer coordinates is the physical bottom-left of the screen +/// (i.e. the "width" is smaller than the "height"). +#[derive(Clone, Copy, Debug)] +pub enum ScreenOrientation { + /// Rotate 90° clockwise to account for the 3DS screen rotation. Most + /// applications will use this variant. + Rotated, + /// Do not apply any extra rotation to the projection. + None, +} + +impl Default for ScreenOrientation { + fn default() -> Self { + Self::Rotated + } +} + +/// Configuration for calculating stereoscopic projections. +// TODO: not totally happy with this name + API yet, but it works for now. +#[derive(Clone, Copy, Debug)] +pub struct StereoDisplacement { + /// The horizontal offset of the eye from center. Negative values + /// correspond to the left eye, and positive values to the right eye. + pub displacement: f32, + /// The position of the screen, which determines the focal length. Objects + /// closer than this depth will appear to pop out of the screen, and objects + /// further than this will appear inside the screen. + pub screen_depth: f32, +} + +impl StereoDisplacement { + /// Construct displacement for the left and right eyes simulataneously. + /// The given `interocular_distance` describes the distance between the two + /// rendered "eyes". A negative value will be treated the same as a positive + /// value of the same magnitude. + /// + /// See struct documentation for details about the + /// [`screen_depth`](Self::screen_depth) parameter. + pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) { + let displacement = interocular_distance.abs() / 2.0; + + let left_eye = Self { + displacement: -displacement, + screen_depth, + }; + let right_eye = Self { + displacement, + screen_depth, + }; + + (left_eye, right_eye) + } +} + +/// Configuration for the clipping planes of a projection. +/// +/// For [`Perspective`] projections, this is used for the near and far clip planes +/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum). +/// +/// For [`Orthographic`] projections, this is used for the Z clipping planes of +/// the projection. +/// +/// Note that the `near` value should always be less than `far`, regardless of +/// [`CoordinateOrientation`]. In other words, these values will be negated +/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded) +/// orientation. +#[derive(Clone, Copy, Debug)] +pub struct ClipPlanes { + /// The Z-depth of the near clip plane, usually close or equal to zero. + pub near: f32, + /// The Z-depth of the far clip plane, usually greater than zero. + pub far: f32, +} + +/// The aspect ratio of a projection plane. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum AspectRatio { + /// The aspect ratio of the 3DS' top screen (per-eye). + #[doc(alias = "C3D_AspectRatioTop")] + TopScreen, + /// The aspect ratio of the 3DS' bottom screen. + #[doc(alias = "C3D_AspectRatioBot")] + BottomScreen, + /// A custom aspect ratio (should be calcualted as `width / height`). + Other(f32), +} + +impl From for f32 { + fn from(ratio: AspectRatio) -> Self { + match ratio { + AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32, + AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32, + AspectRatio::Other(ratio) => ratio, + } + } +} + +// endregion diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs new file mode 100644 index 0000000..6833fb9 --- /dev/null +++ b/citro3d/src/math/projection.rs @@ -0,0 +1,264 @@ +use std::mem::MaybeUninit; +use std::ops::Range; + +use super::{ + AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement, +}; + +/// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection). +/// See specific `Kind` implementations for constructors, e.g. +/// [`Projection::perspective`] and [`Projection::orthographic`]. +/// +/// To use the resulting projection, convert it to a [`Matrix`] with [`From`]/[`Into`]. +#[derive(Clone, Debug)] +pub struct Projection { + coordinates: CoordinateOrientation, + rotation: ScreenOrientation, + inner: Kind, +} + +impl Projection { + fn new(inner: Kind) -> Self { + Self { + coordinates: CoordinateOrientation::default(), + rotation: ScreenOrientation::default(), + inner, + } + } + + /// Set the coordinate system's orientation for the projection. + /// See [`CoordinateOrientation`] for more details. + pub fn coordinates(&mut self, orientation: CoordinateOrientation) -> &mut 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 { + self.rotation = orientation; + self + } +} + +/// See [`Projection::perspective`]. +#[derive(Clone, Debug)] +pub struct Perspective { + vertical_fov_radians: f32, + aspect_ratio: AspectRatio, + clip_planes: ClipPlanes, + stereo: Option, +} + +impl Projection { + /// Construct a projection matrix suitable for projecting 3D world space onto + /// the 3DS screens. + /// + /// # Parameters + /// + /// * `vertical_fov`: the vertical field of view, measured in radians + /// * `aspect_ratio`: the aspect ratio of the projection + /// * `clip_planes`: the near and far clip planes of the view frustum. + /// [`ClipPlanes`] are always defined by near and far values, regardless + /// of the projection's [`CoordinateOrientation`]. + /// + /// # Examples + /// + /// ``` + /// # use citro3d::math::*; + /// # use std::f32::consts::PI; + /// # + /// # let _runner = test_runner::GdbRunner::default(); + /// # + /// let clip_planes = ClipPlanes { + /// near: 0.01, + /// far: 100.0, + /// }; + /// + /// let bottom: Matrix = + /// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into(); + /// + /// let top: Matrix = Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into(); + /// ``` + #[doc(alias = "Mtx_Persp")] + #[doc(alias = "Mtx_PerspTilt")] + pub fn perspective( + vertical_fov_radians: f32, + aspect_ratio: AspectRatio, + clip_planes: ClipPlanes, + ) -> Self { + Self::new(Perspective { + vertical_fov_radians, + aspect_ratio, + clip_planes, + stereo: None, + }) + } + + /// Helper function to build both eyes' perspective projection matrices + /// at once. See [`StereoDisplacement`] for details on how to configure + /// stereoscopy. + /// + /// ``` + /// # use std::f32::consts::PI; + /// # use citro3d::math::*; + /// # + /// # let _runner = test_runner::GdbRunner::default(); + /// # + /// let (left, right) = StereoDisplacement::new(0.5, 2.0); + /// let (left_eye, right_eye) = Projection::perspective( + /// PI / 4.0, + /// AspectRatio::TopScreen, + /// ClipPlanes { + /// near: 0.01, + /// far: 100.0, + /// }, + /// ) + /// .stereo_matrices(left, right); + /// ``` + #[doc(alias = "Mtx_PerspStereo")] + #[doc(alias = "Mtx_PerspStereoTilt")] + pub fn stereo_matrices( + self, + left_eye: StereoDisplacement, + right_eye: StereoDisplacement, + ) -> (Matrix, Matrix) { + // TODO: we might be able to avoid this clone if there was a conversion + // from &Self to Matrix instead of Self... but it's probably fine for now + let left = self.clone().stereo(left_eye); + let right = self.stereo(right_eye); + // Also, we could consider just returning (Self, Self) here? idk + (left.into(), right.into()) + } + + fn stereo(mut self, displacement: StereoDisplacement) -> Self { + self.inner.stereo = Some(displacement); + self + } +} + +impl From> for Matrix { + fn from(projection: Projection) -> Self { + let Perspective { + vertical_fov_radians, + aspect_ratio, + clip_planes, + stereo, + } = projection.inner; + + let mut result = MaybeUninit::uninit(); + + if let Some(stereo) = stereo { + let make_mtx = match projection.rotation { + ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspStereoTilt, + ScreenOrientation::None => citro3d_sys::Mtx_PerspStereo, + }; + unsafe { + make_mtx( + result.as_mut_ptr(), + vertical_fov_radians, + aspect_ratio.into(), + clip_planes.near, + clip_planes.far, + stereo.displacement, + stereo.screen_depth, + projection.coordinates.is_left_handed(), + ); + } + } else { + let make_mtx = match projection.rotation { + ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspTilt, + ScreenOrientation::None => citro3d_sys::Mtx_Persp, + }; + unsafe { + make_mtx( + result.as_mut_ptr(), + vertical_fov_radians, + aspect_ratio.into(), + clip_planes.near, + clip_planes.far, + projection.coordinates.is_left_handed(), + ); + } + } + + unsafe { Self(result.assume_init()) } + } +} + +/// See [`Projection::orthographic`]. +#[derive(Clone, Debug)] +pub struct Orthographic { + clip_planes_x: Range, + clip_planes_y: Range, + clip_planes_z: ClipPlanes, +} + +impl Projection { + /// Construct an orthographic projection. The X and Y clip planes are passed + /// as ranges because their coordinates are always oriented the same way + /// (+X right, +Y up). + /// + /// The Z [`ClipPlanes`], however, are always defined by + /// near and far values, regardless of the projection's [`CoordinateOrientation`]. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::{Projection, ClipPlanes, Matrix}; + /// # + /// let mtx: Matrix = Projection::orthographic( + /// 0.0..240.0, + /// 0.0..400.0, + /// ClipPlanes { + /// near: 0.0, + /// far: 100.0, + /// }, + /// ) + /// .into(); + /// ``` + #[doc(alias = "Mtx_Ortho")] + #[doc(alias = "Mtx_OrthoTilt")] + pub fn orthographic( + clip_planes_x: Range, + clip_planes_y: Range, + clip_planes_z: ClipPlanes, + ) -> Self { + Self::new(Orthographic { + clip_planes_x, + clip_planes_y, + clip_planes_z, + }) + } +} + +impl From> for Matrix { + fn from(projection: Projection) -> Self { + let make_mtx = match projection.rotation { + ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt, + ScreenOrientation::None => citro3d_sys::Mtx_Ortho, + }; + + let Orthographic { + clip_planes_x, + clip_planes_y, + clip_planes_z, + } = projection.inner; + + let mut out = MaybeUninit::uninit(); + unsafe { + make_mtx( + out.as_mut_ptr(), + clip_planes_x.start, + clip_planes_x.end, + clip_planes_y.start, + clip_planes_y.end, + clip_planes_z.near, + clip_planes_z.far, + projection.coordinates.is_left_handed(), + ); + Self(out.assume_init()) + } + } +} diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index d3c0060..39b9aa8 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -5,12 +5,15 @@ //! documentation for . use std::error::Error; +use std::ffi::CString; use std::mem::MaybeUninit; +use crate::uniform; + /// A PICA200 shader program. It may have one or both of: /// -/// * A vertex shader [`Library`] -/// * A geometry shader [`Library`] +/// * A [vertex](Type::Vertex) shader [`Library`] +/// * A [geometry](Type::Geometry) shader [`Library`] /// /// The PICA200 does not support user-programmable fragment shaders. pub struct Program { @@ -66,8 +69,37 @@ impl Program { } } + /// Get the index of a uniform by name. + /// + /// # Errors + /// + /// * If the given `name` contains a null byte + /// * If a uniform with the given `name` could not be found + pub fn get_uniform(&self, name: &str) -> crate::Result { + let vertex_instance = unsafe { (*self.as_raw()).vertexShader }; + assert!( + !vertex_instance.is_null(), + "vertex shader should never be null!" + ); + + let name = CString::new(name)?; + + let idx = + unsafe { ctru_sys::shaderInstanceGetUniformLocation(vertex_instance, name.as_ptr()) }; + + if idx < 0 { + Err(crate::Error::NotFound) + } else { + Ok(idx.into()) + } + } + + pub(crate) fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { + &self.program + } + // TODO: pub(crate) - pub fn as_raw(&mut self) -> *mut ctru_sys::shaderProgram_s { + pub fn as_raw_mut(&mut self) -> *mut ctru_sys::shaderProgram_s { &mut self.program } } @@ -75,11 +107,26 @@ impl Program { impl Drop for Program { fn drop(&mut self) { unsafe { - let _ = ctru_sys::shaderProgramFree(self.as_raw()); + let _ = ctru_sys::shaderProgramFree(self.as_raw_mut()); } } } +/// The type of a shader. +#[repr(u32)] +pub enum Type { + /// A vertex shader. + Vertex = ctru_sys::GPU_VERTEX_SHADER, + /// A geometry shader. + Geometry = ctru_sys::GPU_GEOMETRY_SHADER, +} + +impl From for u32 { + fn from(value: Type) -> Self { + value as u32 + } +} + /// A PICA200 Shader Library (commonly called DVLB). This can be comprised of /// one or more [`Entrypoint`]s, but most commonly has one vertex shader and an /// optional geometry shader. @@ -102,7 +149,7 @@ impl Library { // SAFETY: we're trusting the parse implementation doesn't mutate // the contents of the data. From a quick read it looks like that's // correct and it should just take a const arg in the API. - aligned.as_ptr() as *mut _, + aligned.as_ptr().cast_mut(), aligned.len().try_into()?, ) })) diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs new file mode 100644 index 0000000..c6a1d75 --- /dev/null +++ b/citro3d/src/uniform.rs @@ -0,0 +1,44 @@ +//! Common definitions for binding uniforms to shaders. This is primarily +//! done by implementing the [`Uniform`] trait for a given type. + +use crate::math::Matrix; +use crate::{shader, Instance}; + +/// The index of a uniform within a [`shader::Program`]. +#[derive(Copy, Clone, Debug)] +pub struct Index(i8); + +impl From for Index { + fn from(value: i8) -> Self { + Self(value) + } +} + +impl From for i32 { + fn from(value: Index) -> Self { + value.0.into() + } +} + +mod private { + use crate::math::Matrix; + + pub trait Sealed {} + impl Sealed for &Matrix {} +} + +/// A shader uniform. This trait is implemented for types that can be bound to +/// shaders to be used as a uniform input to the shader. +pub trait Uniform: private::Sealed { + /// Bind the uniform to the given shader index for the given shader type. + /// An [`Instance`] is required to prevent concurrent binding of different + /// uniforms to the same index. + fn bind(self, instance: &mut Instance, shader_type: shader::Type, index: Index); +} + +impl Uniform for &Matrix { + #[doc(alias = "C3D_FVUnifMtx4x4")] + fn bind(self, _instance: &mut Instance, type_: shader::Type, index: Index) { + unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) } + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e8c7ca3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +unstable_features = true +format_code_in_doc_comments = true +group_imports = "StdExternalCrate"