diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 6f3095d..e46171c 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -1,5 +1,5 @@ +use citro3d::{include_aligned_bytes, shader}; use citro3d_sys::C3D_Mtx; -use citro3d_sys::{shaderProgram_s, DVLB_s}; use ctru::gfx::{Gfx, Side}; use ctru::services::apt::Apt; use ctru::services::hid::{Hid, KeyPad}; @@ -44,8 +44,8 @@ const VERTICES: &[Vertex] = &[ }, ]; -static SHADER_BYTES: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/examples/assets/vshader.shbin")); +const SHADER_BYTES: &[u8] = + include_aligned_bytes!(concat!(env!("OUT_DIR"), "/examples/assets/vshader.shbin")); fn main() { ctru::init(); @@ -72,7 +72,12 @@ fn main() { render_target.set_output(&*top_screen, Side::Left); - let (program, uloc_projection, projection, vbo_data, vshader_dvlb) = scene_init(); + 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 (uloc_projection, projection, vbo_data) = scene_init(&mut program); while apt.main_loop() { hid.scan_input(); @@ -93,34 +98,18 @@ fn main() { }); } - scene_exit(vbo_data, program, vshader_dvlb); + scene_exit(vbo_data); } -fn scene_init() -> (shaderProgram_s, i8, C3D_Mtx, *mut libc::c_void, *mut DVLB_s) { +fn scene_init(program: &mut shader::Program) -> (i8, C3D_Mtx, *mut libc::c_void) { // Load the vertex shader, create a shader program and bind it unsafe { - let mut shader_bytes = SHADER_BYTES.to_owned(); - - // Assume the data is aligned properly... - let vshader_dvlb = citro3d_sys::DVLB_ParseFile( - shader_bytes.as_mut_ptr().cast(), - (shader_bytes.len() / 4) - .try_into() - .expect("shader len fits in a u32"), - ); - let mut program = { - let mut program = MaybeUninit::uninit(); - citro3d_sys::shaderProgramInit(program.as_mut_ptr()); - program.assume_init() - }; - - citro3d_sys::shaderProgramSetVsh(&mut program, (*vshader_dvlb).DVLE); - citro3d_sys::C3D_BindProgram(&mut program); + citro3d_sys::C3D_BindProgram(program.as_raw()); // Get the location of the uniforms let projection_name = CStr::from_bytes_with_nul(b"projection\0").unwrap(); let uloc_projection = citro3d_sys::shaderInstanceGetUniformLocation( - program.vertexShader, + (*program.as_raw()).vertexShader, projection_name.as_ptr(), ); @@ -183,13 +172,7 @@ fn scene_init() -> (shaderProgram_s, i8, C3D_Mtx, *mut libc::c_void, *mut DVLB_s ); citro3d_sys::C3D_TexEnvFunc(env, citro3d_sys::C3D_Both, citro3d_sys::GPU_REPLACE); - ( - program, - uloc_projection, - projection, - vbo_data.cast(), - vshader_dvlb, - ) + (uloc_projection, projection, vbo_data.cast()) } } @@ -210,16 +193,8 @@ fn scene_render(uloc_projection: i32, projection: &C3D_Mtx) { } } -fn scene_exit( - vbo_data: *mut libc::c_void, - mut program: shaderProgram_s, - vshader_dvlb: *mut DVLB_s, -) { +fn scene_exit(vbo_data: *mut libc::c_void) { unsafe { citro3d_sys::linearFree(vbo_data); - - citro3d_sys::shaderProgramFree(&mut program); - - citro3d_sys::DVLB_Free(vshader_dvlb); } } diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index 8b13789..520eec1 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -1 +1,166 @@ +//! Functionality for parsing and using PICA200 shaders on the 3DS. This module +//! does not compile shaders, but enables using pre-compiled shaders at runtime. +//! +//! For more details about the PICA200 compiler / shader language, see +//! documentation for . +use std::error::Error; +use std::mem::MaybeUninit; + +pub mod macros; + +/// A PICA200 shader program. It may have one or both of: +/// +/// * A vertex [shader instance](Instance) +/// * A geometry [shader instance](Instance) +/// +/// The PICA200 does not support user-programmable fragment shaders. +pub struct Program { + program: citro3d_sys::shaderProgram_s, +} + +impl Program { + /// Create a new shader program from a vertex shader. + /// + /// # Errors + /// + /// Returns an error if: + /// * the shader program cannot be initialized + /// * the input shader is not a vertex shader or is otherwise invalid + pub fn new(vertex_shader: Entrypoint) -> Result { + let mut program = unsafe { + let mut program = MaybeUninit::uninit(); + let result = citro3d_sys::shaderProgramInit(program.as_mut_ptr()); + if result != 0 { + return Err(ctru::Error::from(result)); + } + program.assume_init() + }; + + let ret = unsafe { citro3d_sys::shaderProgramSetVsh(&mut program, vertex_shader.as_raw()) }; + + if ret == 0 { + Ok(Self { program }) + } else { + Err(ctru::Error::from(ret)) + } + } + + /// Set the geometry shader for a given program. + /// + /// # Errors + /// + /// Returns an error if the input shader is not a geometry shader or is + /// otherwise invalid. + pub fn set_geometry_shader( + &mut self, + geometry_shader: Entrypoint, + stride: u8, + ) -> Result<(), ctru::Error> { + let ret = unsafe { + citro3d_sys::shaderProgramSetGsh(&mut self.program, geometry_shader.as_raw(), stride) + }; + + if ret == 0 { + Ok(()) + } else { + Err(ctru::Error::from(ret)) + } + } + + // TODO: pub(crate) + pub fn as_raw(&mut self) -> *mut citro3d_sys::shaderProgram_s { + &mut self.program + } +} + +impl<'vert, 'geom> Drop for Program { + fn drop(&mut self) { + unsafe { + let _ = citro3d_sys::shaderProgramFree(self.as_raw()); + } + } +} + +/// 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. +/// +/// This is the result of parsing a shader binary (shbin), and the resulting +/// [`Entrypoint`]s can be used as part of a [`Program`]. +pub struct Library(*mut citro3d_sys::DVLB_s); + +impl Library { + /// Parse a new shader library from input bytes. + /// + /// # Errors + /// + /// An error is returned if the input data does not have an alignment of 4 + /// (cannot be safely converted to `&[u32]`). + pub fn from_bytes(bytes: &[u8]) -> Result> { + unsafe { + let (prefix, aligned, suffix) = bytes.align_to::(); + if !prefix.is_empty() || !suffix.is_empty() { + // Align is incorrect, we don't want to drop any data + // TODO fill in error details + return Err("uh oh".into()); + } + + Ok(Self(citro3d_sys::DVLB_ParseFile( + // 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.len().try_into()?, + ))) + } + } + + #[must_use] + pub fn len(&self) -> usize { + unsafe { (*self.0).numDVLE as usize } + } + + #[must_use] + pub fn get(&self, index: usize) -> Option { + if index < self.len() { + Some(Entrypoint { + ptr: unsafe { (*self.0).DVLE.add(index) }, + _library: self, + }) + } else { + None + } + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn as_raw(&mut self) -> *mut citro3d_sys::DVLB_s { + self.0 + } +} + +impl Drop for Library { + fn drop(&mut self) { + unsafe { + citro3d_sys::DVLB_Free(self.as_raw()); + } + } +} + +/// A shader library entrypoint (also called DVLE). This represents either a +/// vertex or a geometry shader. +#[derive(Clone, Copy)] +pub struct Entrypoint<'lib> { + ptr: *mut citro3d_sys::DVLE_s, + _library: &'lib Library, +} + +impl<'lib> Entrypoint<'lib> { + fn as_raw(self) -> *mut citro3d_sys::DVLE_s { + self.ptr + } +} diff --git a/citro3d/src/shader/macros.rs b/citro3d/src/shader/macros.rs new file mode 100644 index 0000000..5a6fc2e --- /dev/null +++ b/citro3d/src/shader/macros.rs @@ -0,0 +1,24 @@ +/// Helper struct to [`include_bytes`] aligned as a specific type. +#[repr(C)] // guarantee 'bytes' comes after '_align' +pub struct AlignedAs { + pub _align: [Align; 0], + pub bytes: Bytes, +} + +/// Helper macro for including a file as bytes that are correctly aligned for +/// use as a [`Library`](super::Library). +#[macro_export] +macro_rules! include_aligned_bytes { + ($path:expr) => {{ + // const block expression to encapsulate the static + use $crate::shader::macros::AlignedAs; + + // this assignment is made possible by CoerceUnsized + const ALIGNED: &AlignedAs = &AlignedAs { + _align: [], + bytes: *include_bytes!($path), + }; + + &ALIGNED.bytes + }}; +}