Browse Source

Merge pull request #27 from rust3ds/feature/safe-math-wrappers

Start porting <c3d/maths.h>, vectors and matrices
pull/29/head
Ian Chamberlain 1 year ago committed by GitHub
parent
commit
6ce6c96c79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      citro3d-macros/src/lib.rs
  2. 110
      citro3d/examples/triangle.rs
  3. 1
      citro3d/src/buffer.rs
  4. 11
      citro3d/src/error.rs
  5. 43
      citro3d/src/lib.rs
  6. 185
      citro3d/src/math.rs
  7. 264
      citro3d/src/math/projection.rs
  8. 57
      citro3d/src/shader.rs
  9. 44
      citro3d/src/uniform.rs
  10. 3
      rustfmt.toml

2
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. // we're already nightly-only so might as well use unstable proc macro APIs.
#![feature(proc_macro_span)] #![feature(proc_macro_span)]

110
citro3d/examples/triangle.rs

@ -3,13 +3,10 @@
#![feature(allocator_api)] #![feature(allocator_api)]
use std::ffi::CStr;
use std::mem::MaybeUninit;
use citro3d::macros::include_shader; use citro3d::macros::include_shader;
use citro3d::render::{self, ClearFlags}; use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement};
use citro3d::{attrib, buffer, shader}; use citro3d::render::ClearFlags;
use citro3d_sys::C3D_Mtx; use citro3d::{attrib, buffer, render, shader};
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D};
@ -92,7 +89,10 @@ fn main() {
let mut buf_info = buffer::Info::new(); let mut buf_info = buffer::Info::new();
let (attr_info, vbo_idx) = prepare_vbos(&mut buf_info, &vbo_data); 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() { while apt.main_loop() {
hid.scan_input(); hid.scan_input();
@ -109,14 +109,7 @@ fn main() {
let clear_color: u32 = 0x7F_7F_7F_FF; let clear_color: u32 = 0x7F_7F_7F_FF;
target.clear(ClearFlags::ALL, clear_color, 0); target.clear(ClearFlags::ALL, clear_color, 0);
unsafe { instance.bind_vertex_uniform(projection_uniform_idx, projection);
// Update the uniforms
citro3d_sys::C3D_FVUnifMtx4x4(
ctru_sys::GPU_VERTEX_SHADER,
projection_uniform_idx.into(),
projection,
);
}
instance.set_attr_info(&attr_info); instance.set_attr_info(&attr_info);
@ -124,13 +117,13 @@ fn main() {
}; };
let Projections { let Projections {
left, left_eye,
right, right_eye,
center, center,
} = calculate_projections(); } = calculate_projections();
render_to(&mut top_left_target, &left); render_to(&mut top_left_target, &left_eye);
render_to(&mut top_right_target, &right); render_to(&mut top_right_target, &right_eye);
render_to(&mut bottom_target, &center); render_to(&mut bottom_target, &center);
}); });
} }
@ -165,70 +158,45 @@ where
} }
struct Projections { struct Projections {
left: C3D_Mtx, left_eye: Matrix,
right: C3D_Mtx, right_eye: Matrix,
center: C3D_Mtx, center: Matrix,
} }
fn calculate_projections() -> Projections { 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 // TODO: it would be cool to allow playing around with these parameters on
// the fly with D-pad, etc. // the fly with D-pad, etc.
let slider_val = unsafe { ctru_sys::osGet3DSliderState() }; 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 vertical_fov = 40.0_f32.to_radians();
let far = 100.0; let screen_depth = 2.0;
let fovy = 40.0_f32.to_radians();
let screen = 2.0;
unsafe { let clip_planes = ClipPlanes {
citro3d_sys::Mtx_PerspStereoTilt( near: 0.01,
left_eye.as_mut_ptr(), far: 100.0,
fovy, };
citro3d_sys::C3D_AspectRatioTop as f32,
near,
far,
-iod,
screen,
true,
);
citro3d_sys::Mtx_PerspStereoTilt( let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth);
right_eye.as_mut_ptr(),
fovy,
citro3d_sys::C3D_AspectRatioTop as f32,
near,
far,
iod,
screen,
true,
);
citro3d_sys::Mtx_PerspTilt( let (left_eye, right_eye) =
center.as_mut_ptr(), Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes)
fovy, .stereo_matrices(left, right);
citro3d_sys::C3D_AspectRatioBot as f32,
near,
far,
true,
);
Projections { let center =
left: left_eye.assume_init(), Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into();
right: right_eye.assume_init(),
center: center.assume_init(), 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 // Load the vertex shader, create a shader program and bind it
unsafe { 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 // 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 // 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, 0,
); );
citro3d_sys::C3D_TexEnvFunc(env, citro3d_sys::C3D_Both, ctru_sys::GPU_REPLACE); 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(),
)
} }
} }

1
citro3d/src/buffer.rs

@ -100,7 +100,6 @@ impl Info {
/// ///
/// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator /// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator
/// * if the maximum number (12) of VBOs are already registered /// * if the maximum number (12) of VBOs are already registered
///
pub fn add<'this, 'vbo, 'idx, T>( pub fn add<'this, 'vbo, 'idx, T>(
&'this mut self, &'this mut self,
vbo_data: &'vbo [T], vbo_data: &'vbo [T],

11
citro3d/src/error.rs

@ -1,5 +1,6 @@
//! General-purpose error and result types returned by public APIs of this crate. //! General-purpose error and result types returned by public APIs of this crate.
use std::ffi::NulError;
use std::num::TryFromIntError; use std::num::TryFromIntError;
use std::sync::TryLockError; use std::sync::TryLockError;
@ -31,6 +32,10 @@ pub enum Error {
/// The given memory could not be converted to a physical address for sharing /// The given memory could not be converted to a physical address for sharing
/// with the GPU. Data should be allocated with [`ctru::linear`]. /// with the GPU. Data should be allocated with [`ctru::linear`].
InvalidMemoryLocation, InvalidMemoryLocation,
/// The given name was not valid for the requested purpose.
InvalidName,
/// The requested resource could not be found.
NotFound,
} }
impl From<TryFromIntError> for Error { impl From<TryFromIntError> for Error {
@ -44,3 +49,9 @@ impl<T> From<TryLockError<T>> for Error {
Self::LockHeld Self::LockHeld
} }
} }
impl From<NulError> for Error {
fn from(_: NulError) -> Self {
Self::InvalidName
}
}

43
citro3d/src/lib.rs

@ -5,12 +5,15 @@
pub mod attrib; pub mod attrib;
pub mod buffer; pub mod buffer;
pub mod error; pub mod error;
pub mod math;
pub mod render; pub mod render;
pub mod shader; pub mod shader;
pub mod uniform;
use citro3d_sys::C3D_FrameDrawOn;
pub use error::{Error, Result}; pub use error::{Error, Result};
use self::uniform::Uniform;
pub mod macros { pub mod macros {
//! Helper macros for working with shaders. //! Helper macros for working with shaders.
pub use citro3d_macros::*; pub use citro3d_macros::*;
@ -53,7 +56,7 @@ impl Instance {
/// Fails if the given target cannot be used for drawing. /// Fails if the given target cannot be used for drawing.
pub fn select_render_target(&mut self, target: &render::Target<'_>) -> Result<()> { pub fn select_render_target(&mut self, target: &render::Target<'_>) -> Result<()> {
let _ = self; let _ = self;
if unsafe { C3D_FrameDrawOn(target.as_raw()) } { if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } {
Ok(()) Ok(())
} else { } else {
Err(Error::InvalidRenderTarget) 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 { impl Drop for Instance {

185
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<AspectRatio> 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

264
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<Kind> {
coordinates: CoordinateOrientation,
rotation: ScreenOrientation,
inner: Kind,
}
impl<Kind> Projection<Kind> {
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<StereoDisplacement>,
}
impl Projection<Perspective> {
/// 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<Projection<Perspective>> for Matrix {
fn from(projection: Projection<Perspective>) -> 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<f32>,
clip_planes_y: Range<f32>,
clip_planes_z: ClipPlanes,
}
impl Projection<Orthographic> {
/// 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<f32>,
clip_planes_y: Range<f32>,
clip_planes_z: ClipPlanes,
) -> Self {
Self::new(Orthographic {
clip_planes_x,
clip_planes_y,
clip_planes_z,
})
}
}
impl From<Projection<Orthographic>> for Matrix {
fn from(projection: Projection<Orthographic>) -> 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())
}
}
}

57
citro3d/src/shader.rs

@ -5,12 +5,15 @@
//! documentation for <https://github.com/devkitPro/picasso>. //! documentation for <https://github.com/devkitPro/picasso>.
use std::error::Error; use std::error::Error;
use std::ffi::CString;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use crate::uniform;
/// A PICA200 shader program. It may have one or both of: /// A PICA200 shader program. It may have one or both of:
/// ///
/// * A vertex shader [`Library`] /// * A [vertex](Type::Vertex) shader [`Library`]
/// * A geometry shader [`Library`] /// * A [geometry](Type::Geometry) shader [`Library`]
/// ///
/// The PICA200 does not support user-programmable fragment shaders. /// The PICA200 does not support user-programmable fragment shaders.
pub struct Program { 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<uniform::Index> {
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) // 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 &mut self.program
} }
} }
@ -75,11 +107,26 @@ impl Program {
impl Drop for Program { impl Drop for Program {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { 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<Type> for u32 {
fn from(value: Type) -> Self {
value as u32
}
}
/// A PICA200 Shader Library (commonly called DVLB). This can be comprised of /// 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 /// one or more [`Entrypoint`]s, but most commonly has one vertex shader and an
/// optional geometry shader. /// optional geometry shader.
@ -102,7 +149,7 @@ impl Library {
// SAFETY: we're trusting the parse implementation doesn't mutate // SAFETY: we're trusting the parse implementation doesn't mutate
// the contents of the data. From a quick read it looks like that's // 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. // 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()?, aligned.len().try_into()?,
) )
})) }))

44
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<i8> for Index {
fn from(value: i8) -> Self {
Self(value)
}
}
impl From<Index> 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()) }
}
}

3
rustfmt.toml

@ -0,0 +1,3 @@
unstable_features = true
format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
Loading…
Cancel
Save