├── _config.yml ├── rustfmt.toml ├── citro3d ├── README.md ├── src │ ├── color.rs │ ├── math.rs │ ├── render │ │ ├── transfer.rs │ │ └── effect.rs │ ├── error.rs │ ├── fog.rs │ ├── texenv.rs │ ├── attrib.rs │ ├── lib.rs │ ├── uniform.rs │ ├── shader.rs │ ├── math │ │ ├── matrix.rs │ │ ├── ops.rs │ │ ├── fvec.rs │ │ └── projection.rs │ ├── buffer.rs │ └── render.rs ├── examples │ ├── assets │ │ ├── vshader.pica │ │ └── frag-shader.pica │ ├── triangle.rs │ ├── cube.rs │ └── fragment-light.rs ├── LICENSE-MIT ├── Cargo.toml └── LICENSE-APACHE ├── citro3d-macros ├── tests │ ├── bad-shader.pica │ ├── integration.pica │ └── integration.rs ├── README.md ├── Cargo.toml ├── build.rs ├── LICENSE-MIT ├── src │ └── lib.rs └── LICENSE-APACHE ├── assets └── css │ └── style.scss ├── Cargo.toml ├── citro3d-sys ├── Cargo.toml ├── README.md ├── src │ ├── lib.rs │ └── gx.rs ├── LICENSE └── build.rs ├── .gitignore ├── README.md └── .github └── workflows ├── docs.yml └── ci.yml /_config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for GitHub Pages (Jekyll) 2 | theme: jekyll-theme-midnight 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | format_code_in_doc_comments = true 3 | group_imports = "StdExternalCrate" 4 | -------------------------------------------------------------------------------- /citro3d/README.md: -------------------------------------------------------------------------------- 1 | # citro3d 2 | 3 | Safe wrappers around [`citro3d-sys`](../citro3d-sys). 4 | 5 | [Documentation](https://rust3ds.github.io/citro3d-rs/crates/citro3d) is generated from the 6 | `main` branch. 7 | -------------------------------------------------------------------------------- /citro3d-macros/tests/bad-shader.pica: -------------------------------------------------------------------------------- 1 | ; Vertex shader that won't compile 2 | 3 | .out outpos position 4 | .out outclr color 5 | 6 | .proc main 7 | mov outpos, 1 8 | mov outclr, 0 9 | 10 | end 11 | .end 12 | -------------------------------------------------------------------------------- /citro3d-macros/README.md: -------------------------------------------------------------------------------- 1 | # citro3d-macros 2 | 3 | Proc-macro helpers for [`citro3d`](../citro3d). For now, the only available 4 | macro is `include_shader!` which can be used to embed compiled PICA200 shaders 5 | into an application. 6 | -------------------------------------------------------------------------------- /citro3d-macros/tests/integration.pica: -------------------------------------------------------------------------------- 1 | ; Trivial vertex shader 2 | 3 | .out outpos position 4 | .out outclr color 5 | 6 | .alias inpos v1 7 | .alias inclr v0 8 | 9 | .proc main 10 | mov outpos, inpos 11 | mov outclr, inclr 12 | 13 | end 14 | .end 15 | -------------------------------------------------------------------------------- /citro3d-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "citro3d-macros" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Rust3DS Org"] 6 | license = "MIT OR Apache-2.0" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | litrs = { version = "0.5.1", default-features = false } 13 | quote = "1.0.40" 14 | -------------------------------------------------------------------------------- /citro3d-macros/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script mainly exists just to ensure `OUT_DIR` is set for the macro, 2 | //! but we can also use it to force a re-evaluation if `DEVKITPRO` changes. 3 | 4 | fn main() { 5 | for var in ["OUT_DIR", "DEVKITPRO"] { 6 | println!("cargo:rerun-if-env-changed={var}"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | // https://github.com/pages-themes/midnight#stylesheet 5 | @import "{{ site.theme }}"; 6 | 7 | // Give code links a color that matches regular links 8 | a code { 9 | color: inherit; 10 | } 11 | 12 | // Remove weird extra padding and spaces from inline code blocks 13 | code { 14 | padding: 0; 15 | } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "citro3d", 4 | "citro3d-sys", 5 | "citro3d-macros", 6 | ] 7 | default-members = [ 8 | "citro3d", 9 | "citro3d-sys", 10 | "citro3d-macros", 11 | ] 12 | resolver = "2" 13 | 14 | [patch."https://github.com/rust3ds/citro3d-rs.git"] 15 | citro3d = { path = "citro3d" } 16 | citro3d-sys = { path = "citro3d-sys" } 17 | citro3d-macros = { path = "citro3d-macros" } 18 | -------------------------------------------------------------------------------- /citro3d-macros/tests/integration.rs: -------------------------------------------------------------------------------- 1 | use citro3d_macros::include_shader; 2 | 3 | #[test] 4 | fn includes_shader_static() { 5 | static SHADER_BYTES: &[u8] = include_shader!("integration.pica"); 6 | 7 | assert_eq!(SHADER_BYTES.len() % 4, 0); 8 | } 9 | 10 | #[test] 11 | fn includes_shader_const() { 12 | const SHADER_BYTES: &[u8] = include_shader!("integration.pica"); 13 | 14 | assert_eq!(SHADER_BYTES.len() % 4, 0); 15 | } 16 | -------------------------------------------------------------------------------- /citro3d-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "citro3d-sys" 3 | version = "0.1.0" 4 | authors = ["Rust3DS Org", "panicbit "] 5 | edition = "2024" 6 | license = "Zlib" 7 | links = "citro3d" 8 | 9 | [dependencies] 10 | libc = "0.2.175" 11 | ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } 12 | 13 | [build-dependencies] 14 | bindgen = { version = "0.72", features = ["experimental"] } 15 | cc = "1.0" 16 | doxygen-rs = "0.4.2" 17 | 18 | [dev-dependencies] 19 | shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } 20 | -------------------------------------------------------------------------------- /citro3d-sys/README.md: -------------------------------------------------------------------------------- 1 | # citro3d-sys 2 | 3 | Rust bindings to [`citro3d`](https://github.com/devkitPro/citro3d). 4 | Bindings are generated at build time using the locally-installed devkitPro. 5 | 6 | [Documentation](https://rust3ds.github.io/citro3d-rs/crates/citro3d_sys) is generated from the 7 | `main` branch, and should generally be up to date with the latest devkitPro. 8 | This will be more useful than [docs.rs](https://docs.rs/crates/citro3d), since 9 | the bindings are generated at build time and `docs.rs`' build environment does not 10 | have a copy of devkitPro to generate bindings from. 11 | -------------------------------------------------------------------------------- /citro3d/src/color.rs: -------------------------------------------------------------------------------- 1 | //! Color manipulation module. 2 | 3 | /// RGB color in linear space ([0, 1]). 4 | #[derive(Debug, Default, Clone, Copy)] 5 | pub struct Color { 6 | pub r: f32, 7 | pub g: f32, 8 | pub b: f32, 9 | } 10 | 11 | impl Color { 12 | pub fn new(r: f32, g: f32, b: f32) -> Self { 13 | Self { r, g, b } 14 | } 15 | 16 | /// Splits the color into RGB ordered parts. 17 | pub fn to_parts_rgb(self) -> [f32; 3] { 18 | [self.r, self.g, self.b] 19 | } 20 | 21 | /// Splits the color into BGR ordered parts. 22 | pub fn to_parts_bgr(self) -> [f32; 3] { 23 | [self.b, self.g, self.r] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE files 2 | .zed 3 | 4 | # Generated by Cargo 5 | # will have compiled files and executables 6 | debug/ 7 | target/ 8 | 9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 11 | Cargo.lock 12 | 13 | # These are backup files generated by rustfmt 14 | **/*.rs.bk 15 | 16 | # MSVC Windows builds of rustc generate these, which store debugging information 17 | *.pdb 18 | 19 | # Can be used for local development to set a custom toolchain 20 | rust-toolchain 21 | rust-toolchain.toml 22 | .cargo/ 23 | 24 | # Pica200 output files 25 | *.shbin 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # citro3d-rs 2 | 3 | ⚠️ WIP ⚠️ 4 | 5 | Rust bindings and safe wrapper to the [citro3d](https://github.com/devkitPro/citro3d) 6 | library, to write homebrew graphical programs for the Nintendo 3DS. 7 | 8 | ## Crates 9 | 10 | * [`citro3d-sys`](./citro3d-sys) - C bindings to `libcitro3d` 11 | ([docs](https://rust3ds.github.io/citro3d-rs/crates/citro3d_sys)) 12 | * [`citro3d`](./citro3d) - safe Rust wrappers for `citro3d-sys` (WIP) 13 | ([docs](https://rust3ds.github.io/citro3d-rs/crates/citro3d)) 14 | * [`citro3d-macros`](./citro3d-macros/) – helper proc-macros for `citro3d` 15 | 16 | ## License 17 | 18 | * `citro3d-sys` is licensed under Zlib 19 | * `citro3d` and `citro3d-macros` are dual-licensed under MIT or Apache-2.0 20 | -------------------------------------------------------------------------------- /citro3d/examples/assets/vshader.pica: -------------------------------------------------------------------------------- 1 | ; Basic PICA200 vertex shader 2 | 3 | ; Uniforms 4 | .fvec projection[4] 5 | 6 | ; Constants 7 | .constf ones(1.0, 1.0, 1.0, 1.0) 8 | 9 | ; Outputs 10 | .out outpos position 11 | .out outclr color 12 | 13 | ; Inputs (defined as aliases for convenience) 14 | .alias inpos v0 15 | .alias inclr v1 16 | 17 | .proc main 18 | ; Force the w component of inpos to be 1.0 19 | mov r0.xyz, inpos 20 | mov r0.w, ones 21 | 22 | ; outpos = projectionMatrix * inpos 23 | dp4 outpos.x, projection[0], r0 24 | dp4 outpos.y, projection[1], r0 25 | dp4 outpos.z, projection[2], r0 26 | dp4 outpos.w, projection[3], r0 27 | 28 | ; outclr = inclr 29 | mov outclr, inclr 30 | 31 | ; We're finished 32 | end 33 | .end 34 | -------------------------------------------------------------------------------- /citro3d-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(non_snake_case)] 3 | #![allow(warnings)] 4 | #![allow(non_upper_case_globals)] 5 | #![allow(non_camel_case_types)] 6 | #![allow(clippy::all)] 7 | #![doc(html_root_url = "https://rust3ds.github.io/citro3d-rs/crates")] 8 | #![doc( 9 | html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" 10 | )] 11 | #![doc( 12 | html_logo_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" 13 | )] 14 | 15 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 16 | 17 | pub mod gx; 18 | pub use gx::*; 19 | 20 | // Prevent linking errors from the standard `test` library when running `cargo 3ds test --lib`. 21 | #[cfg(test)] 22 | extern crate shim_3ds; 23 | -------------------------------------------------------------------------------- /citro3d-sys/src/gx.rs: -------------------------------------------------------------------------------- 1 | //! Helper functions based on `<3ds/gpu/gx.h>`. Bindgen doesn't work on these 2 | //! function-like macros so we just reimplement them as `#[inline]` here. 3 | 4 | use ctru_sys::{GX_TRANSFER_FORMAT, GX_TRANSFER_SCALE}; 5 | 6 | #[inline] 7 | pub const fn GX_TRANSFER_FLIP_VERT(flip: bool) -> u32 { 8 | flip as u32 9 | } 10 | 11 | #[inline] 12 | pub const fn GX_TRANSFER_OUT_TILED(tiled: bool) -> u32 { 13 | (tiled as u32) << 1 14 | } 15 | 16 | #[inline] 17 | pub const fn GX_TRANSFER_RAW_COPY(raw_copy: bool) -> u32 { 18 | (raw_copy as u32) << 3 19 | } 20 | 21 | #[inline] 22 | pub const fn GX_TRANSFER_IN_FORMAT(format: GX_TRANSFER_FORMAT) -> u32 { 23 | (format as u32) << 8 24 | } 25 | 26 | #[inline] 27 | pub const fn GX_TRANSFER_OUT_FORMAT(format: GX_TRANSFER_FORMAT) -> u32 { 28 | (format as u32) << 12 29 | } 30 | 31 | #[inline] 32 | pub const fn GX_TRANSFER_SCALING(scale: GX_TRANSFER_SCALE) -> u32 { 33 | (scale as u32) << 24 34 | } 35 | -------------------------------------------------------------------------------- /citro3d-sys/LICENSE: -------------------------------------------------------------------------------- 1 | As with the original citro3d, this library is licensed under zlib. 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any 5 | damages arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any 8 | purpose, including commercial applications, and to alter it and 9 | redistribute it freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you 12 | must not claim that you wrote the original software. If you use 13 | this software in a product, an acknowledgment in the product 14 | documentation would be appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and 16 | must not be misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source 18 | distribution. -------------------------------------------------------------------------------- /citro3d/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /citro3d-macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /citro3d/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "citro3d" 3 | authors = ["Rust3DS Org"] 4 | license = "MIT OR Apache-2.0" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [dependencies] 9 | glam = { version = "0.30.5", optional = true } 10 | approx = { version = "0.5.1", optional = true } 11 | bitflags = "2.9.1" 12 | bytemuck = { version = "1.23.2", features = ["extern_crate_std"] } 13 | citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } 14 | citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" } 15 | ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" } 16 | ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } 17 | document-features = "0.2.11" 18 | libc = "0.2.175" 19 | pin_array = "0.1.2" 20 | 21 | [features] 22 | default = ["glam"] 23 | ## Enable this feature to use the `approx` crate for comparing vectors and matrices. 24 | approx = ["dep:approx"] 25 | ## Enable for glam support in uniforms 26 | glam = ["dep:glam"] 27 | 28 | [dev-dependencies] 29 | test-runner = { git = "https://github.com/rust3ds/ctru-rs.git" } 30 | 31 | [dev-dependencies.citro3d] 32 | # Basically, this works like `cargo 3ds test --features ...` for building tests 33 | # https://github.com/rust-lang/cargo/issues/2911#issuecomment-749580481 34 | path = "." 35 | features = ["approx"] 36 | 37 | [package.metadata.docs.rs] 38 | all-features = true 39 | default-target = "armv6k-nintendo-3ds" 40 | targs = [] 41 | cargo-args = ["-Z", "build-std"] 42 | -------------------------------------------------------------------------------- /citro3d/src/math.rs: -------------------------------------------------------------------------------- 1 | //! Safe wrappers for working with matrix and vector types provided by `citro3d`. 2 | 3 | // TODO: bench FFI calls into `inline statics` generated by bindgen, vs 4 | // reimplementing some of those calls. Many of them are pretty trivial impls 5 | 6 | mod fvec; 7 | mod matrix; 8 | mod ops; 9 | mod projection; 10 | 11 | pub use fvec::{FVec, FVec3, FVec4}; 12 | pub use matrix::Matrix4; 13 | pub use projection::{ 14 | AspectRatio, ClipPlanes, CoordinateOrientation, Orthographic, Perspective, Projection, 15 | ScreenOrientation, StereoDisplacement, 16 | }; 17 | 18 | /// A 4-vector of `u8`s. 19 | /// 20 | /// # Layout 21 | /// Uses the PICA layout of WZYX 22 | #[doc(alias = "C3D_IVec")] 23 | #[repr(transparent)] 24 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 25 | pub struct IVec(citro3d_sys::C3D_IVec); 26 | 27 | impl IVec { 28 | pub fn new(x: u8, y: u8, z: u8, w: u8) -> Self { 29 | Self(unsafe { citro3d_sys::IVec_Pack(x, y, z, w) }) 30 | } 31 | pub fn as_raw(&self) -> &citro3d_sys::C3D_IVec { 32 | &self.0 33 | } 34 | pub fn x(self) -> u8 { 35 | self.0 as u8 36 | } 37 | pub fn y(self) -> u8 { 38 | (self.0 >> 8) as u8 39 | } 40 | pub fn z(self) -> u8 { 41 | (self.0 >> 16) as u8 42 | } 43 | pub fn w(self) -> u8 { 44 | (self.0 >> 24) as u8 45 | } 46 | } 47 | 48 | /// A quaternion, internally represented the same way as [`FVec`]. 49 | #[allow(dead_code)] 50 | #[doc(alias = "C3D_FQuat")] 51 | pub struct FQuat(citro3d_sys::C3D_FQuat); 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::IVec; 56 | 57 | #[test] 58 | fn ivec_getters_work() { 59 | let iv = IVec::new(1, 2, 3, 4); 60 | assert_eq!(iv.x(), 1); 61 | assert_eq!(iv.y(), 2); 62 | assert_eq!(iv.z(), 3); 63 | assert_eq!(iv.w(), 4); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | container: devkitpro/devkitarm 14 | steps: 15 | - name: Checkout branch 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Pages 19 | uses: actions/configure-pages@v3 20 | 21 | - name: Build with Jekyll 22 | uses: actions/jekyll-build-pages@v1 23 | 24 | - uses: rust3ds/test-runner/setup@v1 25 | with: 26 | toolchain: nightly-2025-07-25 27 | 28 | - name: Build workspace docs 29 | run: cargo 3ds --verbose doc --verbose --no-deps --workspace 30 | env: 31 | RUSTDOCFLAGS: --enable-index-page 32 | 33 | # https://github.com/actions/upload-pages-artifact#file-permissions 34 | - name: Fix file permissions 35 | run: | 36 | chmod -c -R +rX "target/armv6k-nintendo-3ds/doc" | while read line; do 37 | echo "::warning title=Invalid file permissions automatically fixed::$line" 38 | done 39 | 40 | - name: Copy generated docs to _site 41 | # Note: this won't include proc-macro crate, but macros are re-exported 42 | # by the crate docs so there will still be some documentation. 43 | run: cp -R ./target/armv6k-nintendo-3ds/doc ./_site/crates 44 | 45 | - name: Upload docs 46 | uses: actions/upload-pages-artifact@v3 47 | 48 | deploy: 49 | runs-on: ubuntu-latest 50 | needs: build 51 | if: github.ref_name == 'main' 52 | permissions: 53 | pages: write 54 | id-token: write 55 | 56 | environment: 57 | name: github-pages 58 | url: ${{ steps.deployment.outputs.page_url }} 59 | 60 | steps: 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v4 64 | -------------------------------------------------------------------------------- /citro3d/examples/assets/frag-shader.pica: -------------------------------------------------------------------------------- 1 | ; modified version of https://github.com/devkitPro/3ds-examples/blob/ea519187782397c279609da80310e0f8c7e80f09/graphics/gpu/fragment_light/source/vshader.v.pica 2 | ; Example PICA200 vertex shader 3 | 4 | ; Uniforms 5 | .fvec projection[4], modelView[4] 6 | 7 | ; Constants 8 | .constf myconst(0.0, 1.0, -1.0, 0.5) 9 | .alias zeros myconst.xxxx ; Vector full of zeros 10 | .alias ones myconst.yyyy ; Vector full of ones 11 | .alias half myconst.wwww 12 | 13 | ; Outputs 14 | .out outpos position 15 | .out outtex texcoord0 16 | .out outclr color 17 | .out outview view 18 | .out outnq normalquat 19 | 20 | ; Inputs (defined as aliases for convenience) 21 | .in inpos 22 | .in innrm 23 | .in intex 24 | 25 | .proc main 26 | ; Force the w component of inpos to be 1.0 27 | mov r0.xyz, inpos 28 | mov r0.w, ones 29 | 30 | ; r1 = modelView * inpos 31 | dp4 r1.x, modelView[0], r0 32 | dp4 r1.y, modelView[1], r0 33 | dp4 r1.z, modelView[2], r0 34 | dp4 r1.w, modelView[3], r0 35 | 36 | ; outview = -r1 37 | mov outview, -r1 38 | 39 | ; outpos = projection * r1 40 | dp4 outpos.x, projection[0], r1 41 | dp4 outpos.y, projection[1], r1 42 | dp4 outpos.z, projection[2], r1 43 | dp4 outpos.w, projection[3], r1 44 | 45 | ; outtex = intex 46 | mov outtex, intex 47 | 48 | ; Transform the normal vector with the modelView matrix 49 | ; TODO: use a separate normal matrix that is the transpose of the inverse of modelView 50 | dp3 r14.x, modelView[0], innrm 51 | dp3 r14.y, modelView[1], innrm 52 | dp3 r14.z, modelView[2], innrm 53 | dp3 r6.x, r14, r14 54 | rsq r6.x, r6.x 55 | mul r14.xyz, r14.xyz, r6.x 56 | 57 | mov r0, myconst.yxxx 58 | add r4, ones, r14.z 59 | mul r4, half, r4 60 | cmp zeros, ge, ge, r4.x 61 | rsq r4, r4.x 62 | mul r5, half, r14 63 | jmpc cmp.x, degenerate 64 | 65 | rcp r0.z, r4.x 66 | mul r0.xy, r5, r4 67 | 68 | degenerate: 69 | mov outnq, r0 70 | mov outclr, ones 71 | 72 | ; We're finished 73 | end 74 | .end -------------------------------------------------------------------------------- /citro3d/src/render/transfer.rs: -------------------------------------------------------------------------------- 1 | use citro3d_sys::{GX_TRANSFER_IN_FORMAT, GX_TRANSFER_OUT_FORMAT}; 2 | use ctru_sys::GX_TRANSFER_FORMAT; 3 | 4 | use super::ColorFormat; 5 | 6 | /// Control flags for a GX data transfer. 7 | #[derive(Default, Clone, Copy)] 8 | pub struct Flags(u32); 9 | 10 | impl Flags { 11 | /// Set the input format of the data transfer. 12 | #[must_use] 13 | pub fn in_format(self, fmt: Format) -> Self { 14 | Self(self.0 | GX_TRANSFER_IN_FORMAT(fmt as GX_TRANSFER_FORMAT)) 15 | } 16 | 17 | /// Set the output format of the data transfer. 18 | #[must_use] 19 | pub fn out_format(self, fmt: Format) -> Self { 20 | Self(self.0 | GX_TRANSFER_OUT_FORMAT(fmt as GX_TRANSFER_FORMAT)) 21 | } 22 | 23 | #[must_use] 24 | pub fn bits(self) -> u32 { 25 | self.0 26 | } 27 | } 28 | 29 | /// The color format to use when transferring data to/from the GPU. 30 | /// 31 | /// NOTE: this a distinct type from [`ColorFormat`] because they are not implicitly 32 | /// convertible to one another. Use [`From::from`] to get the [`Format`] corresponding 33 | /// to a given [`ColorFormat`]. 34 | #[repr(u8)] 35 | #[doc(alias = "GX_TRANSFER_FORMAT")] 36 | pub enum Format { 37 | /// 8-bit Red + 8-bit Green + 8-bit Blue + 8-bit Alpha. 38 | RGBA8 = ctru_sys::GX_TRANSFER_FMT_RGBA8, 39 | /// 8-bit Red + 8-bit Green + 8-bit Blue. 40 | RGB8 = ctru_sys::GX_TRANSFER_FMT_RGB8, 41 | /// 5-bit Red + 5-bit Green + 5-bit Blue + 1-bit Alpha. 42 | RGB565 = ctru_sys::GX_TRANSFER_FMT_RGB565, 43 | /// 5-bit Red + 6-bit Green + 5-bit Blue. 44 | RGB5A1 = ctru_sys::GX_TRANSFER_FMT_RGB5A1, 45 | /// 4-bit Red + 4-bit Green + 4-bit Blue + 4-bit Alpha. 46 | RGBA4 = ctru_sys::GX_TRANSFER_FMT_RGBA4, 47 | } 48 | 49 | impl From for Format { 50 | fn from(color_fmt: ColorFormat) -> Self { 51 | match color_fmt { 52 | ColorFormat::RGBA8 => Self::RGBA8, 53 | ColorFormat::RGB8 => Self::RGB8, 54 | ColorFormat::RGBA5551 => Self::RGB5A1, 55 | ColorFormat::RGB565 => Self::RGB565, 56 | ColorFormat::RGBA4 => Self::RGBA4, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /citro3d/src/error.rs: -------------------------------------------------------------------------------- 1 | //! General-purpose error and result types returned by public APIs of this crate. 2 | 3 | use std::ffi::NulError; 4 | use std::num::TryFromIntError; 5 | use std::sync::TryLockError; 6 | 7 | /// The common result type returned by `citro3d` functions. 8 | pub type Result = std::result::Result; 9 | 10 | // TODO probably want a similar type to ctru::Result to make it easier to convert 11 | // nonzero result codes to errors. 12 | 13 | /// The common error type that may be returned by `citro3d` functions. 14 | #[non_exhaustive] 15 | #[derive(Debug)] 16 | pub enum Error { 17 | /// C3D error code. 18 | System(libc::c_int), 19 | /// A C3D object or context could not be initialized. 20 | FailedToInitialize, 21 | /// A size parameter was specified that cannot be converted to the proper type. 22 | InvalidSize, 23 | /// Failed to select the given render target for drawing to. 24 | InvalidRenderTarget, 25 | /// Indicates that a reference could not be obtained because a lock is already 26 | /// held on the requested object. 27 | LockHeld, 28 | /// Indicates that too many vertex attributes were registered (max 12 supported). 29 | TooManyAttributes, 30 | /// Indicates that too many vertex buffer objects were registered (max 12 supported). 31 | TooManyBuffers, 32 | /// The given memory could not be converted to a physical address for sharing 33 | /// with the GPU. Data should be allocated with [`ctru::linear`]. 34 | InvalidMemoryLocation, 35 | /// The given name was not valid for the requested purpose. 36 | InvalidName, 37 | /// The requested resource could not be found. 38 | NotFound, 39 | /// Attempted to use an index that was out of bounds. 40 | IndexOutOfBounds { 41 | /// The index used. 42 | idx: libc::c_int, 43 | /// The length of the collection. 44 | len: libc::c_int, 45 | }, 46 | } 47 | 48 | impl From for Error { 49 | fn from(_: TryFromIntError) -> Self { 50 | Self::InvalidSize 51 | } 52 | } 53 | 54 | impl From> for Error { 55 | fn from(_: TryLockError) -> Self { 56 | Self::LockHeld 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(_: NulError) -> Self { 62 | Self::InvalidName 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | toolchain: 16 | # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date 17 | - nightly-2025-07-25 18 | # Check for breakage on latest nightly 19 | - nightly 20 | 21 | # But if latest nightly fails, allow the workflow to continue 22 | continue-on-error: ${{ matrix.toolchain == 'nightly' }} 23 | runs-on: ubuntu-latest 24 | container: devkitpro/devkitarm 25 | steps: 26 | - name: Checkout branch 27 | uses: actions/checkout@v4 28 | 29 | - uses: rust3ds/test-runner/setup@v1 30 | with: 31 | toolchain: ${{ matrix.toolchain }} 32 | 33 | # https://github.com/actions/runner/issues/504 34 | # Removing the matchers won't keep the job from failing if there are errors, 35 | # but will at least declutter pull request annotations (especially for warnings). 36 | - name: Hide duplicate annotations from nightly 37 | if: ${{ matrix.toolchain == 'nightly' }} 38 | run: | 39 | echo "::remove-matcher owner=clippy::" 40 | echo "::remove-matcher owner=rustfmt::" 41 | 42 | - name: Check formatting 43 | run: cargo fmt --all --verbose -- --check 44 | 45 | - name: Run linting 46 | run: cargo 3ds clippy --color=always --verbose --all-targets 47 | 48 | test: 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | toolchain: 53 | - nightly-2025-07-25 54 | - nightly 55 | continue-on-error: ${{ matrix.toolchain == 'nightly' }} 56 | runs-on: ubuntu-latest 57 | container: devkitpro/devkitarm 58 | steps: 59 | - name: Checkout branch 60 | uses: actions/checkout@v4 61 | 62 | - uses: rust3ds/test-runner/setup@v1 63 | with: 64 | toolchain: ${{ matrix.toolchain }} 65 | 66 | - name: Hide duplicated warnings from lint job 67 | run: echo "::remove-matcher owner=clippy::" 68 | 69 | - name: Build and run macro tests 70 | run: cargo test --package citro3d-macros 71 | 72 | - name: Build and run lib and integration tests 73 | uses: rust3ds/test-runner/run-tests@v1 74 | with: 75 | args: --tests --package citro3d 76 | 77 | - name: Build and run doc tests 78 | uses: rust3ds/test-runner/run-tests@v1 79 | with: 80 | args: --doc --package citro3d 81 | 82 | - name: Upload citra logs and capture videos 83 | uses: actions/upload-artifact@v4 84 | if: success() || failure() # always run unless the workflow was cancelled 85 | with: 86 | name: citra-logs-${{ matrix.toolchain }} 87 | path: | 88 | target/armv6k-nintendo-3ds/debug/deps/*.txt 89 | target/armv6k-nintendo-3ds/debug/deps/*.webm 90 | -------------------------------------------------------------------------------- /citro3d/src/fog.rs: -------------------------------------------------------------------------------- 1 | //! Fog/Gas unit configuration. 2 | 3 | /// Fog modes. 4 | #[repr(u8)] 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | #[doc(alias = "GPU_FOGMODE")] 7 | pub enum FogMode { 8 | /// Fog/Gas unit disabled. 9 | #[doc(alias = "GPU_NO_FOG")] 10 | NoFog = ctru_sys::GPU_NO_FOG, 11 | 12 | /// Fog/Gas unit configured in Fog mode. 13 | #[doc(alias = "GPU_FOG")] 14 | Fog = ctru_sys::GPU_FOG, 15 | 16 | /// Fog/Gas unit configured in Gas mode. 17 | #[doc(alias = "GPU_GAS")] 18 | Gas = ctru_sys::GPU_GAS, 19 | } 20 | 21 | impl TryFrom for FogMode { 22 | type Error = String; 23 | fn try_from(value: u8) -> Result { 24 | match value { 25 | ctru_sys::GPU_NO_FOG => Ok(FogMode::NoFog), 26 | ctru_sys::GPU_FOG => Ok(FogMode::Fog), 27 | ctru_sys::GPU_GAS => Ok(FogMode::Gas), 28 | _ => Err("invalid value for FogMode".to_string()), 29 | } 30 | } 31 | } 32 | 33 | /// Gas shading density source values. 34 | #[repr(u8)] 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 36 | #[doc(alias = "GPU_GASMODE")] 37 | pub enum GasMode { 38 | /// Plain density. 39 | #[doc(alias = "GPU_PLAIN_DENSITY")] 40 | PlainDensity = ctru_sys::GPU_PLAIN_DENSITY, 41 | 42 | /// Depth density. 43 | #[doc(alias = "GPU_DEPTH_DENSITY")] 44 | DepthDensity = ctru_sys::GPU_DEPTH_DENSITY, 45 | } 46 | 47 | impl TryFrom for GasMode { 48 | type Error = String; 49 | fn try_from(value: u8) -> Result { 50 | match value { 51 | ctru_sys::GPU_PLAIN_DENSITY => Ok(GasMode::PlainDensity), 52 | ctru_sys::GPU_DEPTH_DENSITY => Ok(GasMode::DepthDensity), 53 | _ => Err("invalid value for GasMode".to_string()), 54 | } 55 | } 56 | } 57 | 58 | /// Gas color LUT inputs. 59 | #[repr(u8)] 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 61 | #[doc(alias = "GPU_GASLUTINPUT")] 62 | pub enum GasLutInput { 63 | /// Gas density used as input. 64 | #[doc(alias = "GPU_GAS_DENSITY")] 65 | Density = ctru_sys::GPU_GAS_DENSITY, 66 | 67 | /// Light factor used as input. 68 | #[doc(alias = "GPU_GAS_LIGHT_FACTOR")] 69 | LightFactor = ctru_sys::GPU_GAS_LIGHT_FACTOR, 70 | } 71 | 72 | impl TryFrom for GasLutInput { 73 | type Error = String; 74 | fn try_from(value: u8) -> Result { 75 | match value { 76 | ctru_sys::GPU_GAS_DENSITY => Ok(GasLutInput::Density), 77 | ctru_sys::GPU_GAS_LIGHT_FACTOR => Ok(GasLutInput::LightFactor), 78 | _ => Err("invalid value for GasLutInput".to_string()), 79 | } 80 | } 81 | } 82 | 83 | /// Gas depth functions. 84 | #[repr(u8)] 85 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 86 | #[doc(alias = "GPU_GASDEPTHFUNC")] 87 | pub enum GasDepthFunction { 88 | /// Never pass (0). 89 | #[doc(alias = "GPU_GAS_NEVER")] 90 | Never = ctru_sys::GPU_GAS_NEVER, 91 | 92 | /// Always pass (1). 93 | #[doc(alias = "GPU_GAS_ALWAYS")] 94 | Always = ctru_sys::GPU_GAS_ALWAYS, 95 | 96 | /// Pass if greater than (1-X). 97 | #[doc(alias = "GPU_GAS_GREATER")] 98 | Greater = ctru_sys::GPU_GAS_GREATER, 99 | 100 | /// Pass if less than (X). 101 | #[doc(alias = "GPU_GAS_LESS")] 102 | Less = ctru_sys::GPU_GAS_LESS, 103 | } 104 | 105 | impl TryFrom for GasDepthFunction { 106 | type Error = String; 107 | 108 | fn try_from(value: u8) -> Result { 109 | match value { 110 | ctru_sys::GPU_GAS_NEVER => Ok(GasDepthFunction::Never), 111 | ctru_sys::GPU_GAS_ALWAYS => Ok(GasDepthFunction::Always), 112 | ctru_sys::GPU_GAS_GREATER => Ok(GasDepthFunction::Greater), 113 | ctru_sys::GPU_GAS_LESS => Ok(GasDepthFunction::Less), 114 | _ => Err("invalid value for GasDepthFunction".to_string()), 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /citro3d/src/texenv.rs: -------------------------------------------------------------------------------- 1 | //! Texture combiner support. See 2 | //! for more details. 3 | 4 | use bitflags::bitflags; 5 | 6 | /// A texture combiner, also called a "texture environment" (hence the struct name). 7 | /// See also [`texenv.h` documentation](https://oreo639.github.io/citro3d/texenv_8h.html). 8 | #[doc(alias = "C3D_TexEnv")] 9 | pub struct TexEnv(*mut citro3d_sys::C3D_TexEnv); 10 | 11 | // https://oreo639.github.io/citro3d/texenv_8h.html#a9eda91f8e7252c91f873b1d43e3728b6 12 | pub(crate) const TEXENV_COUNT: usize = 6; 13 | 14 | impl TexEnv { 15 | pub(crate) fn new(stage: Stage) -> Self { 16 | let mut result = unsafe { Self(citro3d_sys::C3D_GetTexEnv(stage.0 as _)) }; 17 | result.reset(); 18 | result 19 | } 20 | 21 | /// Re-initialize the texture combiner to its default state. 22 | pub fn reset(&mut self) { 23 | unsafe { 24 | citro3d_sys::C3D_TexEnvInit(self.0); 25 | } 26 | } 27 | 28 | /// Configure the source values of the texture combiner. 29 | /// 30 | /// # Parameters 31 | /// 32 | /// - `mode`: which [`Mode`]\(s) to set the sourc operand(s) for. 33 | /// - `source0`: the first [`Source`] operand to the texture combiner 34 | /// - `source1` and `source2`: optional additional [`Source`] operands to use 35 | #[doc(alias = "C3D_TexEnvSrc")] 36 | pub fn src( 37 | &mut self, 38 | mode: Mode, 39 | source0: Source, 40 | source1: Option, 41 | source2: Option, 42 | ) -> &mut Self { 43 | unsafe { 44 | citro3d_sys::C3D_TexEnvSrc( 45 | self.0, 46 | mode.bits(), 47 | source0 as _, 48 | source1.unwrap_or(Source::PrimaryColor) as _, 49 | source2.unwrap_or(Source::PrimaryColor) as _, 50 | ); 51 | } 52 | self 53 | } 54 | 55 | /// Configure the texture combination function. 56 | /// 57 | /// # Parameters 58 | /// 59 | /// - `mode`: the [`Mode`]\(s) the combination function will apply to. 60 | /// - `func`: the [`CombineFunc`] used to combine textures. 61 | #[doc(alias = "C3D_TexEnvFunc")] 62 | pub fn func(&mut self, mode: Mode, func: CombineFunc) -> &mut Self { 63 | unsafe { 64 | citro3d_sys::C3D_TexEnvFunc(self.0, mode.bits(), func as _); 65 | } 66 | 67 | self 68 | } 69 | } 70 | 71 | bitflags! { 72 | /// Whether to operate on colors, alpha values, or both. 73 | #[doc(alias = "C3D_TexEnvMode")] 74 | pub struct Mode: citro3d_sys::C3D_TexEnvMode { 75 | #[allow(missing_docs)] 76 | const RGB = citro3d_sys::C3D_RGB; 77 | #[allow(missing_docs)] 78 | const ALPHA = citro3d_sys::C3D_Alpha; 79 | #[allow(missing_docs)] 80 | const BOTH = citro3d_sys::C3D_Both; 81 | } 82 | } 83 | 84 | /// A source operand of a [`TexEnv`]'s texture combination. 85 | #[doc(alias = "GPU_TEVSRC")] 86 | #[allow(missing_docs)] 87 | #[derive(Debug, Clone, Copy)] 88 | #[repr(u8)] 89 | #[non_exhaustive] 90 | pub enum Source { 91 | PrimaryColor = ctru_sys::GPU_PRIMARY_COLOR, 92 | FragmentPrimaryColor = ctru_sys::GPU_FRAGMENT_PRIMARY_COLOR, 93 | FragmentSecondaryColor = ctru_sys::GPU_FRAGMENT_SECONDARY_COLOR, 94 | Texture0 = ctru_sys::GPU_TEXTURE0, 95 | Texture1 = ctru_sys::GPU_TEXTURE1, 96 | Texture2 = ctru_sys::GPU_TEXTURE2, 97 | Texture3 = ctru_sys::GPU_TEXTURE3, 98 | PreviousBuffer = ctru_sys::GPU_PREVIOUS_BUFFER, 99 | Constant = ctru_sys::GPU_CONSTANT, 100 | Previous = ctru_sys::GPU_PREVIOUS, 101 | } 102 | 103 | /// The combination function to apply to the [`TexEnv`] operands. 104 | #[doc(alias = "GPU_COMBINEFUNC")] 105 | #[allow(missing_docs)] 106 | #[derive(Debug, Clone, Copy)] 107 | #[repr(u8)] 108 | #[non_exhaustive] 109 | pub enum CombineFunc { 110 | Replace = ctru_sys::GPU_REPLACE, 111 | Modulate = ctru_sys::GPU_MODULATE, 112 | Add = ctru_sys::GPU_ADD, 113 | AddSigned = ctru_sys::GPU_ADD_SIGNED, 114 | Interpolate = ctru_sys::GPU_INTERPOLATE, 115 | Subtract = ctru_sys::GPU_SUBTRACT, 116 | Dot3Rgb = ctru_sys::GPU_DOT3_RGB, 117 | // Added in libcrtu 2.3.0: 118 | // Dot3Rgba = ctru_sys::GPU_DOT3_RGBA, 119 | } 120 | 121 | /// A texture combination stage identifier. This index doubles as the order 122 | /// in which texture combinations will be applied. 123 | // (I think?) 124 | #[derive(Copy, Clone, Debug)] 125 | pub struct Stage(pub(crate) usize); 126 | 127 | impl Stage { 128 | /// Get a stage index. Valid indices range from 0 to 5. 129 | pub fn new(index: usize) -> Option { 130 | (index < 6).then_some(Self(index)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /citro3d/src/attrib.rs: -------------------------------------------------------------------------------- 1 | //! Configure vertex attributes. 2 | //! 3 | //! This module has types and helpers for describing the shape/structure of vertex 4 | //! data to be sent to the GPU. 5 | //! 6 | //! See the [`buffer`](crate::buffer) module to use the vertex data itself. 7 | 8 | use std::mem::MaybeUninit; 9 | 10 | /// Vertex attribute info. This struct describes how vertex buffers are 11 | /// layed out and used (i.e. the shape of the vertex data). 12 | #[derive(Debug)] 13 | #[doc(alias = "C3D_AttrInfo")] 14 | pub struct Info(pub(crate) citro3d_sys::C3D_AttrInfo); 15 | 16 | /// A shader input register, usually corresponding to a single vertex attribute 17 | /// (e.g. position or color). These are called `v0`, `v1`, ... `v15` in the 18 | /// [picasso](https://github.com/devkitPro/picasso/blob/master/Manual.md) 19 | /// shader language. 20 | #[derive(Debug, Clone, Copy)] 21 | pub struct Register(libc::c_int); 22 | 23 | impl Register { 24 | /// Get a register corresponding to the given index. 25 | /// 26 | /// # Errors 27 | /// 28 | /// Returns an error for `n >= 16`. 29 | pub fn new(n: u16) -> crate::Result { 30 | if n < 16 { 31 | Ok(Self(n.into())) 32 | } else { 33 | Err(crate::Error::TooManyAttributes) 34 | } 35 | } 36 | } 37 | 38 | /// An attribute index. This is the attribute's actual index in the input buffer, 39 | /// and may correspond to any [`Register`] (or multiple) as input in the shader 40 | /// program. 41 | #[allow(dead_code)] 42 | #[derive(Debug, Clone, Copy)] 43 | pub struct Index(u8); 44 | 45 | /// The data format of an attribute. 46 | #[repr(u8)] 47 | #[derive(Debug, Clone, Copy)] 48 | #[doc(alias = "GPU_FORMATS")] 49 | pub enum Format { 50 | /// A signed byte, i.e. [`i8`]. 51 | Byte = ctru_sys::GPU_BYTE, 52 | /// An unsigned byte, i.e. [`u8`]. 53 | UnsignedByte = ctru_sys::GPU_UNSIGNED_BYTE, 54 | /// A float, i.e. [`f32`]. 55 | Float = ctru_sys::GPU_FLOAT, 56 | /// A short integer, i.e. [`i16`]. 57 | Short = ctru_sys::GPU_SHORT, 58 | } 59 | 60 | impl From for u8 { 61 | fn from(value: Format) -> Self { 62 | value as u8 63 | } 64 | } 65 | 66 | // SAFETY: the RWLock ensures unique access when mutating the global struct, and 67 | // we trust citro3d to Do The Right Thing™ and not mutate it otherwise. 68 | unsafe impl Sync for Info {} 69 | unsafe impl Send for Info {} 70 | 71 | impl Default for Info { 72 | #[doc(alias = "AttrInfo_Init")] 73 | fn default() -> Self { 74 | let mut raw = MaybeUninit::zeroed(); 75 | let raw = unsafe { 76 | citro3d_sys::AttrInfo_Init(raw.as_mut_ptr()); 77 | raw.assume_init() 78 | }; 79 | Self(raw) 80 | } 81 | } 82 | 83 | impl Info { 84 | /// Construct a new attribute info structure with no attributes. 85 | pub fn new() -> Self { 86 | Self::default() 87 | } 88 | 89 | pub(crate) fn copy_from(raw: *const citro3d_sys::C3D_AttrInfo) -> Option { 90 | if raw.is_null() { 91 | None 92 | } else { 93 | // This is less efficient than returning a pointer or something, but it's 94 | // safer since we don't know the lifetime of the pointee 95 | Some(Self(unsafe { *raw })) 96 | } 97 | } 98 | 99 | /// Add an attribute loader to the attribute info. The resulting attribute index 100 | /// indicates the registration order of the attributes. 101 | /// 102 | /// # Parameters 103 | /// 104 | /// * `register`: the shader program input register for this attribute. 105 | /// * `format`: the data format of this attribute. 106 | /// * `count`: the number of elements in each attribute (up to 4, corresponding 107 | /// to `xyzw` / `rgba` / `stpq`). 108 | /// 109 | /// # Errors 110 | /// 111 | /// * If `count > 4` 112 | /// * If this attribute info already has the maximum number of attributes. 113 | #[doc(alias = "AttrInfo_AddLoader")] 114 | pub fn add_loader( 115 | &mut self, 116 | register: Register, 117 | format: Format, 118 | count: u8, 119 | ) -> crate::Result { 120 | if count > 4 { 121 | return Err(crate::Error::InvalidSize); 122 | } 123 | 124 | // SAFETY: the &mut self.0 reference is only used to access fields in 125 | // the attribute info, not stored somewhere for later use 126 | let ret = unsafe { 127 | citro3d_sys::AttrInfo_AddLoader(&mut self.0, register.0, format.into(), count.into()) 128 | }; 129 | 130 | let Ok(idx) = ret.try_into() else { 131 | return Err(crate::Error::TooManyAttributes); 132 | }; 133 | 134 | Ok(Index(idx)) 135 | } 136 | 137 | pub(crate) fn permutation(&self) -> u64 { 138 | self.0.permutation 139 | } 140 | 141 | /// Get the number of registered attributes. 142 | pub fn attr_count(&self) -> libc::c_int { 143 | self.0.attrCount 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /citro3d/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(custom_test_frameworks)] 2 | #![test_runner(test_runner::run_gdb)] 3 | #![feature(allocator_api)] 4 | #![feature(doc_cfg)] 5 | #![feature(doc_auto_cfg)] 6 | #![doc(html_root_url = "https://rust3ds.github.io/citro3d-rs/crates")] 7 | #![doc( 8 | html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" 9 | )] 10 | #![doc( 11 | html_logo_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" 12 | )] 13 | 14 | //! Safe Rust bindings to `citro3d`. This crate wraps `citro3d-sys` to provide 15 | //! safer APIs for graphics programs targeting the 3DS. 16 | //! 17 | //! ## Feature flags 18 | #![doc = document_features::document_features!()] 19 | 20 | pub mod attrib; 21 | pub mod buffer; 22 | pub mod color; 23 | pub mod error; 24 | pub mod fog; 25 | pub mod light; 26 | pub mod math; 27 | pub mod render; 28 | pub mod shader; 29 | pub mod texenv; 30 | pub mod texture; 31 | pub mod uniform; 32 | 33 | use std::cell::RefMut; 34 | use std::fmt; 35 | use std::rc::Rc; 36 | 37 | use ctru::services::gfx::Screen; 38 | pub use error::{Error, Result}; 39 | 40 | use crate::render::RenderPass; 41 | 42 | pub mod macros { 43 | //! Helper macros for working with shaders. 44 | pub use citro3d_macros::*; 45 | } 46 | 47 | mod private { 48 | pub trait Sealed {} 49 | impl Sealed for u8 {} 50 | impl Sealed for u16 {} 51 | } 52 | 53 | /// Representation of `citro3d`'s internal render queue. This is something that 54 | /// lives in the global context, but it keeps references to resources that are 55 | /// used for rendering, so it's useful for us to have something to represent its 56 | /// lifetime. 57 | struct RenderQueue; 58 | 59 | /// The single instance for using `citro3d`. This is the base type that an application 60 | /// should instantiate to use this library. 61 | #[non_exhaustive] 62 | #[must_use] 63 | pub struct Instance { 64 | queue: Rc, 65 | } 66 | 67 | impl fmt::Debug for Instance { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | f.debug_struct("Instance").finish_non_exhaustive() 70 | } 71 | } 72 | 73 | impl Instance { 74 | /// Initialize the default `citro3d` instance. 75 | /// 76 | /// # Errors 77 | /// 78 | /// Fails if `citro3d` cannot be initialized. 79 | pub fn new() -> Result { 80 | Self::with_cmdbuf_size(citro3d_sys::C3D_DEFAULT_CMDBUF_SIZE.try_into().unwrap()) 81 | } 82 | 83 | /// Initialize the instance with a specified command buffer size. 84 | /// 85 | /// # Errors 86 | /// 87 | /// Fails if `citro3d` cannot be initialized. 88 | #[doc(alias = "C3D_Init")] 89 | pub fn with_cmdbuf_size(size: usize) -> Result { 90 | if unsafe { citro3d_sys::C3D_Init(size) } { 91 | Ok(Self { 92 | queue: Rc::new(RenderQueue), 93 | }) 94 | } else { 95 | Err(Error::FailedToInitialize) 96 | } 97 | } 98 | 99 | /// Create a new render target with the specified size, color format, 100 | /// and depth format. 101 | /// 102 | /// # Errors 103 | /// 104 | /// Fails if the target could not be created with the given parameters. 105 | #[doc(alias = "C3D_RenderTargetCreate")] 106 | #[doc(alias = "C3D_RenderTargetSetOutput")] 107 | pub fn render_target<'screen>( 108 | &self, 109 | width: usize, 110 | height: usize, 111 | screen: RefMut<'screen, dyn Screen>, 112 | depth_format: Option, 113 | ) -> Result> { 114 | render::Target::new(width, height, screen, depth_format, Rc::clone(&self.queue)) 115 | } 116 | 117 | /// Render a frame. 118 | /// 119 | /// The passed in function/closure can access a [`RenderPass`] to emit draw calls. 120 | #[doc(alias = "C3D_FrameBegin")] 121 | #[doc(alias = "C3D_FrameEnd")] 122 | pub fn render_frame_with<'istance: 'frame, 'frame>( 123 | &'istance mut self, 124 | f: impl FnOnce(RenderPass<'frame>) -> RenderPass<'frame>, 125 | ) { 126 | let pass = f(RenderPass::new(self)); 127 | 128 | // Explicit drop for FrameEnd (when the GPU command buffer is flushed). 129 | drop(pass); 130 | } 131 | } 132 | 133 | // This only exists to be an alias, which admittedly is kinda silly. The default 134 | // impl should be equivalent though, since RenderQueue has a drop impl too. 135 | impl Drop for Instance { 136 | #[doc(alias = "C3D_Fini")] 137 | fn drop(&mut self) {} 138 | } 139 | 140 | impl Drop for RenderQueue { 141 | fn drop(&mut self) { 142 | unsafe { 143 | citro3d_sys::C3D_Fini(); 144 | } 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use ctru::services::gfx::Gfx; 151 | 152 | use super::*; 153 | 154 | #[test] 155 | fn select_render_target() { 156 | let gfx = Gfx::new().unwrap(); 157 | let screen = gfx.top_screen.borrow_mut(); 158 | 159 | let mut instance = Instance::new().unwrap(); 160 | let target = instance.render_target(10, 10, screen, None).unwrap(); 161 | 162 | instance.render_frame_with(|mut pass| { 163 | pass.select_render_target(&target).unwrap(); 164 | 165 | pass 166 | }); 167 | 168 | // Check that we don't get a double-free or use-after-free by dropping 169 | // the global instance before dropping the target. 170 | drop(instance); 171 | drop(target); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /citro3d/src/uniform.rs: -------------------------------------------------------------------------------- 1 | //! Common definitions for binding uniforms to shaders. This is primarily 2 | //! done by implementing the [`Uniform`] trait for a given type. 3 | 4 | use std::ops::Range; 5 | 6 | use crate::math::{FVec4, IVec, Matrix4}; 7 | use crate::{RenderPass, shader}; 8 | 9 | /// The index of a uniform within a [`shader::Program`]. 10 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 11 | pub struct Index(u8); 12 | 13 | impl From for Index { 14 | fn from(value: u8) -> Self { 15 | Self(value) 16 | } 17 | } 18 | 19 | impl From for i32 { 20 | fn from(value: Index) -> Self { 21 | value.0.into() 22 | } 23 | } 24 | 25 | /// A uniform which may be bound as input to a shader program 26 | #[non_exhaustive] 27 | #[derive(Debug, PartialEq, Clone, Copy)] 28 | pub enum Uniform { 29 | /// Single float uniform (`.fvec name`) 30 | #[doc(alias = "C3D_FVUnifSet")] 31 | Float(FVec4), 32 | /// Two element float uniform (`.fvec name[2]`) 33 | #[doc(alias = "C3D_FVUnifMtx2x4")] 34 | Float2([FVec4; 2]), 35 | /// Three element float uniform (`.fvec name [3]`) 36 | #[doc(alias = "C3D_FVUnifMtx3x4")] 37 | Float3([FVec4; 3]), 38 | /// Matrix/4 element float uniform (`.fvec name[4]`) 39 | #[doc(alias = "C3D_FVUnifMtx4x4")] 40 | Float4(Matrix4), 41 | /// Bool uniform (`.bool name`) 42 | #[doc(alias = "C3D_BoolUnifSet")] 43 | Bool(bool), 44 | /// Integer uniform (`.ivec name`) 45 | #[doc(alias = "C3D_IVUnifSet")] 46 | Int(IVec), 47 | } 48 | impl Uniform { 49 | /// Get range of valid indexes for this uniform to bind to 50 | pub fn index_range(&self) -> Range { 51 | // these indexes are from the uniform table in the shader see: https://www.3dbrew.org/wiki/SHBIN#Uniform_Table_Entry 52 | // the input registers then are excluded by libctru, see: https://github.com/devkitPro/libctru/blob/0da8705527f03b4b08ff7fee4dd1b7f28df37905/libctru/source/gpu/shbin.c#L93 53 | match self { 54 | Self::Float(_) | Self::Float2(_) | Self::Float3(_) | Self::Float4(_) => { 55 | Index(0)..Index(0x60) 56 | } 57 | Self::Int(_) => Index(0x60)..Index(0x64), 58 | // this gap is intentional 59 | Self::Bool(_) => Index(0x68)..Index(0x79), 60 | } 61 | } 62 | /// Get length of uniform, i.e. how many registers it will write to 63 | #[allow(clippy::len_without_is_empty)] // is_empty doesn't make sense here 64 | pub fn len(&self) -> usize { 65 | match self { 66 | Self::Float(_) => 1, 67 | Self::Float2(_) => 2, 68 | Self::Float3(_) => 3, 69 | Self::Float4(_) => 4, 70 | Self::Bool(_) | Uniform::Int(_) => 1, 71 | } 72 | } 73 | 74 | /// Bind a uniform 75 | /// 76 | /// Note: `_pass` is here to ensure unique access to the global uniform buffers 77 | /// otherwise we could race and/or violate aliasing 78 | pub(crate) fn bind(self, _pass: &mut RenderPass, ty: shader::Type, index: Index) { 79 | assert!( 80 | self.index_range().contains(&index), 81 | "tried to bind uniform to an invalid index (index: {:?}, valid range: {:?})", 82 | index, 83 | self.index_range(), 84 | ); 85 | assert!( 86 | self.index_range().end.0 as usize >= self.len() + index.0 as usize, 87 | "tried to bind a uniform that would overflow the uniform buffer. index was {:?}, size was {} max is {:?}", 88 | index, 89 | self.len(), 90 | self.index_range().end 91 | ); 92 | 93 | let set_fvs = |fs: &[FVec4]| { 94 | for (off, f) in fs.iter().enumerate() { 95 | unsafe { 96 | citro3d_sys::C3D_FVUnifSet( 97 | ty.into(), 98 | (index.0 as usize + off) as i32, 99 | f.x(), 100 | f.y(), 101 | f.z(), 102 | f.w(), 103 | ); 104 | } 105 | } 106 | }; 107 | 108 | match self { 109 | Self::Bool(b) => unsafe { 110 | citro3d_sys::C3D_BoolUnifSet(ty.into(), index.into(), b); 111 | }, 112 | Self::Int(i) => unsafe { 113 | citro3d_sys::C3D_IVUnifSet( 114 | ty.into(), 115 | index.into(), 116 | i.x() as i32, 117 | i.y() as i32, 118 | i.z() as i32, 119 | i.w() as i32, 120 | ); 121 | }, 122 | Self::Float(f) => set_fvs(&[f]), 123 | Self::Float2(fs) => { 124 | set_fvs(&fs); 125 | } 126 | Self::Float3(fs) => set_fvs(&fs), 127 | Self::Float4(m) => { 128 | set_fvs(&m.rows_wzyx()); 129 | } 130 | } 131 | } 132 | } 133 | 134 | impl From for Uniform { 135 | fn from(value: Matrix4) -> Self { 136 | Self::Float4(value) 137 | } 138 | } 139 | impl From<[FVec4; 3]> for Uniform { 140 | fn from(value: [FVec4; 3]) -> Self { 141 | Self::Float3(value) 142 | } 143 | } 144 | 145 | impl From<[FVec4; 2]> for Uniform { 146 | fn from(value: [FVec4; 2]) -> Self { 147 | Self::Float2(value) 148 | } 149 | } 150 | impl From for Uniform { 151 | fn from(value: FVec4) -> Self { 152 | Self::Float(value) 153 | } 154 | } 155 | impl From for Uniform { 156 | fn from(value: IVec) -> Self { 157 | Self::Int(value) 158 | } 159 | } 160 | impl From for Uniform { 161 | fn from(value: bool) -> Self { 162 | Self::Bool(value) 163 | } 164 | } 165 | impl From<&Matrix4> for Uniform { 166 | fn from(value: &Matrix4) -> Self { 167 | (*value).into() 168 | } 169 | } 170 | 171 | #[cfg(feature = "glam")] 172 | impl From for Uniform { 173 | fn from(value: glam::Vec4) -> Self { 174 | Self::Float(value.into()) 175 | } 176 | } 177 | 178 | #[cfg(feature = "glam")] 179 | impl From for Uniform { 180 | fn from(value: glam::Mat4) -> Self { 181 | Self::Float4(value.into()) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /citro3d-sys/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script generates bindings from `citro3d` on the fly at compilation 2 | //! time into `OUT_DIR`, from which they can be included into `lib.rs`. 3 | 4 | use std::env; 5 | use std::iter::FromIterator; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use bindgen::callbacks::{DeriveTrait, ImplementsTrait, ParseCallbacks}; 9 | use bindgen::{Builder, RustTarget}; 10 | 11 | fn main() { 12 | let devkitpro = env::var("DEVKITPRO").expect("DEVKITPRO not set in environment"); 13 | println!("cargo:rerun-if-env-changed=DEVKITPRO"); 14 | 15 | let devkitarm = std::env::var("DEVKITARM").expect("DEVKITARM not set in environment"); 16 | println!("cargo:rerun-if-env-changed=DEVKITARM"); 17 | 18 | let debug_symbols = env::var("DEBUG").unwrap(); 19 | println!("cargo:rerun-if-env-changed=DEBUG"); 20 | 21 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 22 | println!("cargo:rerun-if-env-changed=OUT_DIR"); 23 | 24 | println!("cargo:rerun-if-changed=build.rs"); 25 | println!("cargo:rustc-link-search=native={devkitpro}/libctru/lib"); 26 | println!( 27 | "cargo:rustc-link-lib=static={}", 28 | match debug_symbols.as_str() { 29 | // Based on valid values described in 30 | // https://doc.rust-lang.org/cargo/reference/profiles.html#debug 31 | "0" | "false" | "none" => "citro3d", 32 | _ => "citro3dd", 33 | } 34 | ); 35 | 36 | let include_path = PathBuf::from_iter([devkitpro.as_str(), "libctru", "include"]); 37 | let tex3ds_h = include_path.join("tex3ds.h"); 38 | let citro3d_h = include_path.join("citro3d.h"); 39 | let three_ds_h = include_path.join("3ds.h"); 40 | 41 | let sysroot = Path::new(devkitarm.as_str()).join("arm-none-eabi"); 42 | let system_include = sysroot.join("include"); 43 | let static_fns_path = Path::new("citro3d_statics_wrapper"); 44 | 45 | let gcc_dir = PathBuf::from_iter([devkitarm.as_str(), "lib", "gcc", "arm-none-eabi"]); 46 | 47 | let gcc_include = gcc_dir 48 | .read_dir() 49 | .unwrap() 50 | // Assuming that there is only one gcc version of libs under the devkitARM dir 51 | .next() 52 | .unwrap() 53 | .unwrap() 54 | .path() 55 | .join("include"); 56 | 57 | let bindings = Builder::default() 58 | .header(three_ds_h.to_str().unwrap()) 59 | .header(citro3d_h.to_str().unwrap()) 60 | .header(tex3ds_h.to_str().unwrap()) 61 | .rust_target(RustTarget::nightly()) 62 | .use_core() 63 | .trust_clang_mangling(false) 64 | .layout_tests(false) 65 | .ctypes_prefix("::libc") 66 | .prepend_enum_name(false) 67 | .fit_macro_constants(true) 68 | .raw_line("use ctru_sys::*;") 69 | .raw_line("use libc::FILE;") 70 | .must_use_type("Result") 71 | .blocklist_type("u(8|16|32|64)") 72 | .blocklist_type("FILE") 73 | .opaque_type("(GPU|GFX)_.*") 74 | .opaque_type("float24Uniform_s") 75 | .allowlist_file(".*/c3d/.*[.]h") 76 | .allowlist_file(".*/tex3ds[.]h") 77 | .blocklist_file(".*/3ds/.*[.]h") 78 | .blocklist_file(".*/sys/.*[.]h") 79 | .wrap_static_fns(true) 80 | .wrap_static_fns_path(out_dir.join(static_fns_path)) 81 | .clang_args([ 82 | "--target=arm-none-eabi", 83 | "--sysroot", 84 | sysroot.to_str().unwrap(), 85 | "-isystem", 86 | system_include.to_str().unwrap(), 87 | "-isystem", 88 | gcc_include.to_str().unwrap(), 89 | "-I", 90 | include_path.to_str().unwrap(), 91 | "-mfloat-abi=hard", 92 | "-march=armv6k", 93 | "-mtune=mpcore", 94 | "-mfpu=vfp", 95 | "-DARM11 ", 96 | "-D_3DS ", 97 | "-D__3DS__ ", 98 | "-fshort-enums", 99 | ]) 100 | .parse_callbacks(Box::new(CustomCallbacks)) 101 | .generate() 102 | .expect("Unable to generate bindings"); 103 | 104 | bindings 105 | .write_to_file(out_dir.join("bindings.rs")) 106 | .expect("failed to write bindings"); 107 | 108 | // Compile static inline fns wrapper 109 | let cc = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-gcc"); 110 | let ar = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-ar"); 111 | 112 | cc::Build::new() 113 | .compiler(cc) 114 | .archiver(ar) 115 | .include(&include_path) 116 | .file(out_dir.join(static_fns_path.with_extension("c"))) 117 | .flag("-march=armv6k") 118 | .flag("-mtune=mpcore") 119 | .flag("-mfloat-abi=hard") 120 | .flag("-mfpu=vfp") 121 | .flag("-mtp=soft") 122 | .flag("-Wno-deprecated-declarations") 123 | .compile("citro3d_statics_wrapper"); 124 | } 125 | 126 | /// Custom callback struct to allow us to mark some "known good types" as 127 | /// [`Copy`], which in turn allows using Rust `union` instead of bindgen union types. See 128 | /// 129 | /// for more info. 130 | /// 131 | /// We do the same for [`Debug`] just for the convenience of derived Debug impls 132 | /// on some `citro3d` types. 133 | /// 134 | /// Finally, we use [`doxygen_rs`] to transform the doc comments into something 135 | /// easier to read in the generated documentation / hover documentation. 136 | #[derive(Debug)] 137 | struct CustomCallbacks; 138 | 139 | impl ParseCallbacks for CustomCallbacks { 140 | fn process_comment(&self, comment: &str) -> Option { 141 | Some(doxygen_rs::transform(comment)) 142 | } 143 | 144 | fn blocklisted_type_implements_trait( 145 | &self, 146 | name: &str, 147 | derive_trait: DeriveTrait, 148 | ) -> Option { 149 | if let DeriveTrait::Copy | DeriveTrait::Debug = derive_trait { 150 | match name { 151 | "u64_" | "u32_" | "u16_" | "u8_" | "u64" | "u32" | "u16" | "u8" | "gfxScreen_t" 152 | | "gfx3dSide_t" => Some(ImplementsTrait::Yes), 153 | _ if name.starts_with("GPU_") => Some(ImplementsTrait::Yes), 154 | _ => None, 155 | } 156 | } else { 157 | None 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /citro3d-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macro helpers for `citro3d`. 2 | 3 | // we're already nightly-only so might as well use unstable proc macro APIs. 4 | #![feature(proc_macro_span)] 5 | 6 | use std::error::Error; 7 | use std::fs::DirBuilder; 8 | use std::path::PathBuf; 9 | use std::{env, process}; 10 | 11 | use litrs::StringLit; 12 | use proc_macro::TokenStream; 13 | use quote::quote; 14 | 15 | /// Compiles the given PICA200 shader using [`picasso`](https://github.com/devkitPro/picasso) 16 | /// and returns the compiled bytes directly as a `&[u8]` slice. 17 | /// 18 | /// This is similar to the standard library's [`include_bytes!`](std::include_bytes) macro, for which 19 | /// file paths are relative to the source file where the macro is invoked. 20 | /// 21 | /// The compiled shader binary will be saved in the caller's `$OUT_DIR`. 22 | /// 23 | /// # Errors 24 | /// 25 | /// This macro will fail to compile if the input is not a single string literal. 26 | /// In other words, inputs like `concat!("foo", "/bar")` are not supported. 27 | /// 28 | /// # Example 29 | /// 30 | /// ``` 31 | /// use citro3d_macros::include_shader; 32 | /// 33 | /// static SHADER_BYTES: &[u8] = include_shader!("../tests/integration.pica"); 34 | /// ``` 35 | /// 36 | /// # Errors 37 | /// 38 | /// The macro will fail to compile if the `.pica` file cannot be found, or contains 39 | /// `picasso` syntax errors. 40 | /// 41 | /// ```compile_fail 42 | /// # use citro3d_macros::include_shader; 43 | /// static _ERROR: &[u8] = include_shader!("../tests/nonexistent.pica"); 44 | /// ``` 45 | /// 46 | /// ```compile_fail 47 | /// # use citro3d_macros::include_shader; 48 | /// static _ERROR: &[u8] = include_shader!("../tests/bad-shader.pica"); 49 | /// ``` 50 | #[proc_macro] 51 | pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 52 | match include_shader_impl(input) { 53 | Ok(tokens) => tokens, 54 | Err(err) => { 55 | let err_str = err.to_string(); 56 | quote! { compile_error!( #err_str ) }.into() 57 | } 58 | } 59 | } 60 | 61 | fn include_shader_impl(input: TokenStream) -> Result> { 62 | let tokens: Vec<_> = input.into_iter().collect(); 63 | 64 | if tokens.len() != 1 { 65 | return Err(format!("expected exactly one input token, got {}", tokens.len()).into()); 66 | } 67 | 68 | let shader_source_filename = &tokens[0]; 69 | 70 | let string_lit = match StringLit::try_from(shader_source_filename) { 71 | Ok(lit) => lit, 72 | Err(err) => return Ok(err.to_compile_error()), 73 | }; 74 | 75 | // The cwd can change depending on whether this is running in a doctest or not: 76 | // https://users.rust-lang.org/t/which-directory-does-a-proc-macro-run-from/71917 77 | // 78 | // But the span's `source_file()` seems to always be relative to the cwd. 79 | let cwd = env::current_dir() 80 | .map_err(|err| format!("unable to determine current directory: {err}"))?; 81 | 82 | let invoking_source_file = shader_source_filename 83 | .span() 84 | .local_file() 85 | .expect("source file not found"); 86 | let Some(invoking_source_dir) = invoking_source_file.parent() else { 87 | return Ok(quote! { 88 | compile_error!( 89 | concat!( 90 | "unable to find parent directory of current source file \"", 91 | file!(), 92 | "\"" 93 | ) 94 | ) 95 | } 96 | .into()); 97 | }; 98 | 99 | // By joining these three pieces, we arrive at approximately the same behavior as `include_bytes!` 100 | let shader_source_file = cwd 101 | .join(invoking_source_dir) 102 | .join(string_lit.value()) 103 | // This might be overkill, but it ensures we get a unique path if different 104 | // shaders with the same relative path are used within one program 105 | .canonicalize() 106 | .map_err(|err| format!("unable to resolve absolute path of shader source: {err}"))?; 107 | 108 | let shader_out_file: PathBuf = shader_source_file.with_extension("shbin"); 109 | 110 | let out_dir = PathBuf::from(env!("OUT_DIR")); 111 | 112 | let out_path = out_dir.join(shader_out_file.components().skip(1).collect::()); 113 | // UNWRAP: we already canonicalized the source path, so it should have a parent. 114 | let out_parent = out_path.parent().unwrap(); 115 | 116 | DirBuilder::new() 117 | .recursive(true) 118 | .create(out_parent) 119 | .map_err(|err| format!("unable to create output directory {out_parent:?}: {err}"))?; 120 | 121 | let devkitpro = PathBuf::from(env!("DEVKITPRO")); 122 | let picasso = devkitpro.join("tools/bin/picasso"); 123 | 124 | let output = process::Command::new(&picasso) 125 | .arg("--out") 126 | .args([&out_path, &shader_source_file]) 127 | .output() 128 | .map_err(|err| format!("unable to run {picasso:?}: {err}"))?; 129 | 130 | let error_code = match output.status.code() { 131 | Some(0) => None, 132 | code => Some(code.map_or_else(|| String::from(""), |c| c.to_string())), 133 | }; 134 | 135 | if let Some(code) = error_code { 136 | return Err(format!( 137 | "failed to compile shader: `picasso` exited with status {code}: {}", 138 | String::from_utf8_lossy(&output.stderr), 139 | ) 140 | .into()); 141 | } 142 | 143 | let bytes = std::fs::read(&out_path) 144 | .map_err(|err| format!("unable to read output file {out_path:?}: {err}"))?; 145 | 146 | let source_file_path = shader_source_file.to_string_lossy(); 147 | 148 | let result = quote! { 149 | { 150 | // ensure the source is re-evaluted if the input file changes 151 | const _SOURCE: &[u8] = include_bytes! ( #source_file_path ); 152 | 153 | // https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049/2 154 | #[repr(C)] 155 | struct AlignedAsU32 { 156 | _align: [u32; 0], 157 | bytes: Bytes, 158 | } 159 | 160 | // this assignment is made possible by CoerceUnsized 161 | const ALIGNED: &AlignedAsU32<[u8]> = &AlignedAsU32 { 162 | _align: [], 163 | // emits a token stream like `[10u8, 11u8, ... ]` 164 | bytes: [ #(#bytes),* ] 165 | }; 166 | 167 | &ALIGNED.bytes 168 | } 169 | }; 170 | 171 | Ok(result.into()) 172 | } 173 | -------------------------------------------------------------------------------- /citro3d/src/shader.rs: -------------------------------------------------------------------------------- 1 | //! Functionality for parsing and using PICA200 shaders on the 3DS. This module 2 | //! does not compile shaders, but enables using pre-compiled shaders at runtime. 3 | //! 4 | //! For more details about the PICA200 compiler / shader language, see 5 | //! documentation for . 6 | 7 | use std::error::Error; 8 | use std::ffi::CString; 9 | use std::mem::MaybeUninit; 10 | 11 | use crate::uniform; 12 | 13 | /// A PICA200 shader program. It may have one or both of: 14 | /// 15 | /// * A [vertex](Type::Vertex) shader [`Library`] 16 | /// * A [geometry](Type::Geometry) shader [`Library`] 17 | /// 18 | /// The PICA200 does not support user-programmable fragment shaders. 19 | #[doc(alias = "shaderProgram_s")] 20 | #[must_use] 21 | pub struct Program { 22 | program: ctru_sys::shaderProgram_s, 23 | } 24 | 25 | impl Program { 26 | /// Create a new shader program from a vertex shader. 27 | /// 28 | /// # Errors 29 | /// 30 | /// Returns an error if: 31 | /// * the shader program cannot be initialized 32 | /// * the input shader is not a vertex shader or is otherwise invalid 33 | #[doc(alias = "shaderProgramInit")] 34 | #[doc(alias = "shaderProgramSetVsh")] 35 | pub fn new(vertex_shader: Entrypoint) -> Result { 36 | let mut program = unsafe { 37 | let mut program = MaybeUninit::uninit(); 38 | let result = ctru_sys::shaderProgramInit(program.as_mut_ptr()); 39 | if result != 0 { 40 | return Err(ctru::Error::from(result)); 41 | } 42 | program.assume_init() 43 | }; 44 | 45 | let ret = unsafe { ctru_sys::shaderProgramSetVsh(&mut program, vertex_shader.as_raw()) }; 46 | 47 | if ret == 0 { 48 | Ok(Self { program }) 49 | } else { 50 | Err(ctru::Error::from(ret)) 51 | } 52 | } 53 | 54 | /// Set the geometry shader for a given program. 55 | /// 56 | /// # Errors 57 | /// 58 | /// Returns an error if the input shader is not a geometry shader or is 59 | /// otherwise invalid. 60 | #[doc(alias = "shaderProgramSetGsh")] 61 | pub fn set_geometry_shader( 62 | &mut self, 63 | geometry_shader: Entrypoint, 64 | stride: u8, 65 | ) -> Result<(), ctru::Error> { 66 | let ret = unsafe { 67 | ctru_sys::shaderProgramSetGsh(&mut self.program, geometry_shader.as_raw(), stride) 68 | }; 69 | 70 | if ret == 0 { 71 | Ok(()) 72 | } else { 73 | Err(ctru::Error::from(ret)) 74 | } 75 | } 76 | 77 | /// Get the index of a uniform by name. 78 | /// 79 | /// # Errors 80 | /// 81 | /// * If the given `name` contains a null byte 82 | /// * If a uniform with the given `name` could not be found 83 | #[doc(alias = "shaderInstanceGetUniformLocation")] 84 | pub fn get_uniform(&self, name: &str) -> crate::Result { 85 | let vertex_instance = unsafe { (*self.as_raw()).vertexShader }; 86 | assert!( 87 | !vertex_instance.is_null(), 88 | "vertex shader should never be null!" 89 | ); 90 | 91 | let name = CString::new(name)?; 92 | 93 | let idx = 94 | unsafe { ctru_sys::shaderInstanceGetUniformLocation(vertex_instance, name.as_ptr()) }; 95 | 96 | if idx < 0 { 97 | Err(crate::Error::NotFound) 98 | } else { 99 | Ok((idx as u8).into()) 100 | } 101 | } 102 | 103 | pub(crate) fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { 104 | &self.program 105 | } 106 | } 107 | 108 | impl Drop for Program { 109 | #[doc(alias = "shaderProgramFree")] 110 | fn drop(&mut self) { 111 | unsafe { 112 | let _ = ctru_sys::shaderProgramFree(self.as_raw().cast_mut()); 113 | } 114 | } 115 | } 116 | 117 | /// The type of a shader. 118 | #[repr(u8)] 119 | #[derive(Clone, Copy)] 120 | pub enum Type { 121 | /// A vertex shader. 122 | Vertex = ctru_sys::GPU_VERTEX_SHADER, 123 | /// A geometry shader. 124 | Geometry = ctru_sys::GPU_GEOMETRY_SHADER, 125 | } 126 | 127 | impl From for u8 { 128 | fn from(value: Type) -> Self { 129 | value as u8 130 | } 131 | } 132 | 133 | /// A PICA200 Shader Library (commonly called DVLB). This can be comprised of 134 | /// one or more [`Entrypoint`]s, but most commonly has one vertex shader and an 135 | /// optional geometry shader. 136 | /// 137 | /// This is the result of parsing a shader binary (`.shbin`), and the resulting 138 | /// [`Entrypoint`]s can be used as part of a [`Program`]. 139 | #[doc(alias = "DVLB_s")] 140 | pub struct Library(*mut ctru_sys::DVLB_s); 141 | 142 | impl Library { 143 | /// Parse a new shader library from input bytes. 144 | /// 145 | /// # Errors 146 | /// 147 | /// An error is returned if the input data does not have an alignment of 4 148 | /// (cannot be safely converted to `&[u32]`). 149 | #[doc(alias = "DVLB_ParseFile")] 150 | pub fn from_bytes(bytes: &[u8]) -> Result> { 151 | let aligned: &[u32] = bytemuck::try_cast_slice(bytes)?; 152 | Ok(Self(unsafe { 153 | ctru_sys::DVLB_ParseFile( 154 | // SAFETY: we're trusting the parse implementation doesn't mutate 155 | // the contents of the data. From a quick read it looks like that's 156 | // correct and it should just take a const arg in the API. 157 | aligned.as_ptr().cast_mut(), 158 | aligned.len().try_into()?, 159 | ) 160 | })) 161 | } 162 | 163 | /// Get the number of [`Entrypoint`]s in this shader library. 164 | #[must_use] 165 | #[doc(alias = "numDVLE")] 166 | pub fn len(&self) -> usize { 167 | unsafe { (*self.0).numDVLE as usize } 168 | } 169 | 170 | /// Whether the library has any [`Entrypoint`]s or not. 171 | #[must_use] 172 | pub fn is_empty(&self) -> bool { 173 | self.len() == 0 174 | } 175 | 176 | /// Get the [`Entrypoint`] at the given index, if present. 177 | #[must_use] 178 | pub fn get(&self, index: usize) -> Option> { 179 | if index < self.len() { 180 | Some(Entrypoint { 181 | ptr: unsafe { (*self.0).DVLE.add(index) }, 182 | _library: self, 183 | }) 184 | } else { 185 | None 186 | } 187 | } 188 | 189 | fn as_raw(&mut self) -> *mut ctru_sys::DVLB_s { 190 | self.0 191 | } 192 | } 193 | 194 | impl Drop for Library { 195 | #[doc(alias = "DVLB_Free")] 196 | fn drop(&mut self) { 197 | unsafe { 198 | ctru_sys::DVLB_Free(self.as_raw()); 199 | } 200 | } 201 | } 202 | 203 | /// A shader library entrypoint (also called DVLE). This represents either a 204 | /// vertex or a geometry shader. 205 | #[derive(Clone, Copy)] 206 | pub struct Entrypoint<'lib> { 207 | ptr: *mut ctru_sys::DVLE_s, 208 | _library: &'lib Library, 209 | } 210 | 211 | impl<'lib> Entrypoint<'lib> { 212 | fn as_raw(self) -> *mut ctru_sys::DVLE_s { 213 | self.ptr 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /citro3d/src/math/matrix.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | 3 | use super::{CoordinateOrientation, FVec3, FVec4}; 4 | 5 | /// A 4x4 row-major matrix of `f32`s. 6 | /// 7 | /// # Layout details 8 | /// Rows are actually stored as WZYX in memory. There are helper functions 9 | /// for accessing the rows in XYZW form. The `Debug` implementation prints 10 | /// the shows in WZYX form 11 | /// 12 | /// It is also guaranteed to have the same layout as [`citro3d_sys::C3D_Mtx`] 13 | #[doc(alias = "C3D_Mtx")] 14 | #[derive(Clone, Copy)] 15 | #[repr(transparent)] 16 | pub struct Matrix4(citro3d_sys::C3D_Mtx); 17 | 18 | impl Matrix4 { 19 | /// Construct a Matrix4 from the cells 20 | /// 21 | /// # Note 22 | /// This expects rows to be in WZYX order 23 | pub fn from_cells_wzyx(cells: [f32; 16]) -> Self { 24 | Self(citro3d_sys::C3D_Mtx { m: cells }) 25 | } 26 | /// Construct a Matrix4 from its rows 27 | pub fn from_rows(rows: [FVec4; 4]) -> Self { 28 | Self(citro3d_sys::C3D_Mtx { 29 | r: rows.map(|r| r.0), 30 | }) 31 | } 32 | /// Create a new matrix from a raw citro3d_sys one 33 | pub fn from_raw(value: citro3d_sys::C3D_Mtx) -> Self { 34 | Self(value) 35 | } 36 | 37 | pub fn as_raw(&self) -> &citro3d_sys::C3D_Mtx { 38 | &self.0 39 | } 40 | 41 | pub fn as_raw_mut(&mut self) -> &mut citro3d_sys::C3D_Mtx { 42 | &mut self.0 43 | } 44 | 45 | pub fn into_raw(self) -> citro3d_sys::C3D_Mtx { 46 | self.0 47 | } 48 | 49 | /// Get the rows in raw (WZYX) form 50 | pub fn rows_wzyx(self) -> [FVec4; 4] { 51 | unsafe { self.0.r }.map(FVec4::from_raw) 52 | } 53 | 54 | /// Get the rows in XYZW form 55 | pub fn rows_xyzw(self) -> [[f32; 4]; 4] { 56 | self.rows_wzyx().map(|r| [r.x(), r.y(), r.z(), r.w()]) 57 | } 58 | /// Construct the zero matrix. 59 | #[doc(alias = "Mtx_Zeros")] 60 | pub fn zero() -> Self { 61 | // TODO: should this also be Default::default()? 62 | let mut out = MaybeUninit::uninit(); 63 | unsafe { 64 | citro3d_sys::Mtx_Zeros(out.as_mut_ptr()); 65 | Self::from_raw(out.assume_init()) 66 | } 67 | } 68 | 69 | /// Transpose the matrix, swapping rows and columns. 70 | #[doc(alias = "Mtx_Transpose")] 71 | pub fn transpose(mut self) -> Matrix4 { 72 | unsafe { 73 | citro3d_sys::Mtx_Transpose(self.as_raw_mut()); 74 | } 75 | Matrix4::from_raw(self.into_raw()) 76 | } 77 | 78 | // region: Matrix transformations 79 | // 80 | // NOTE: the `bRightSide` arg common to many of these APIs flips the order of 81 | // operations so that a transformation occurs as self(T) instead of T(self). 82 | // For now I'm not sure if that's a common use case, but if needed we could 83 | // probably have some kinda wrapper type that does transformations in the 84 | // opposite order, or an enum arg for these APIs or something. 85 | 86 | /// Translate a transformation matrix by the given amounts in the X, Y, and Z 87 | /// directions. 88 | #[doc(alias = "Mtx_Translate")] 89 | pub fn translate(&mut self, x: f32, y: f32, z: f32) { 90 | unsafe { citro3d_sys::Mtx_Translate(self.as_raw_mut(), x, y, z, false) } 91 | } 92 | 93 | /// Scale a transformation matrix by the given amounts in the X, Y, and Z directions. 94 | #[doc(alias = "Mtx_Scale")] 95 | pub fn scale(&mut self, x: f32, y: f32, z: f32) { 96 | unsafe { citro3d_sys::Mtx_Scale(self.as_raw_mut(), x, y, z) } 97 | } 98 | 99 | /// Rotate a transformation matrix by the given angle around the given axis. 100 | #[doc(alias = "Mtx_Rotate")] 101 | pub fn rotate(&mut self, axis: FVec3, angle: f32) { 102 | unsafe { citro3d_sys::Mtx_Rotate(self.as_raw_mut(), axis.0, angle, false) } 103 | } 104 | 105 | /// Rotate a transformation matrix by the given angle around the X axis. 106 | #[doc(alias = "Mtx_RotateX")] 107 | pub fn rotate_x(&mut self, angle: f32) { 108 | unsafe { citro3d_sys::Mtx_RotateX(self.as_raw_mut(), angle, false) } 109 | } 110 | 111 | /// Rotate a transformation matrix by the given angle around the Y axis. 112 | #[doc(alias = "Mtx_RotateY")] 113 | pub fn rotate_y(&mut self, angle: f32) { 114 | unsafe { citro3d_sys::Mtx_RotateY(self.as_raw_mut(), angle, false) } 115 | } 116 | 117 | /// Rotate a transformation matrix by the given angle around the Z axis. 118 | #[doc(alias = "Mtx_RotateZ")] 119 | pub fn rotate_z(&mut self, angle: f32) { 120 | unsafe { citro3d_sys::Mtx_RotateZ(self.as_raw_mut(), angle, false) } 121 | } 122 | 123 | /// Find the inverse of the matrix. 124 | /// 125 | /// # Errors 126 | /// 127 | /// If the matrix has no inverse, it will be returned unchanged as an [`Err`]. 128 | #[doc(alias = "Mtx_Inverse")] 129 | pub fn inverse(mut self) -> Result { 130 | let determinant = unsafe { citro3d_sys::Mtx_Inverse(self.as_raw_mut()) }; 131 | if determinant == 0.0 { 132 | Err(self) 133 | } else { 134 | Ok(self) 135 | } 136 | } 137 | 138 | /// Construct the identity matrix. 139 | #[doc(alias = "Mtx_Identity")] 140 | pub fn identity() -> Self { 141 | let mut out = MaybeUninit::uninit(); 142 | unsafe { 143 | citro3d_sys::Mtx_Identity(out.as_mut_ptr()); 144 | Self::from_raw(out.assume_init()) 145 | } 146 | } 147 | 148 | /// Construct a 4x4 matrix with the given values on the diagonal. 149 | #[doc(alias = "Mtx_Diagonal")] 150 | pub fn diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { 151 | let mut out = MaybeUninit::uninit(); 152 | unsafe { 153 | citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, w); 154 | Self::from_raw(out.assume_init()) 155 | } 156 | } 157 | 158 | /// Construct a 3D transformation matrix for a camera, given its position, 159 | /// target, and upward direction. 160 | #[doc(alias = "Mtx_LookAt")] 161 | pub fn looking_at( 162 | camera_position: FVec3, 163 | camera_target: FVec3, 164 | camera_up: FVec3, 165 | coordinates: CoordinateOrientation, 166 | ) -> Self { 167 | let mut out = MaybeUninit::uninit(); 168 | unsafe { 169 | citro3d_sys::Mtx_LookAt( 170 | out.as_mut_ptr(), 171 | camera_position.0, 172 | camera_target.0, 173 | camera_up.0, 174 | coordinates.is_left_handed(), 175 | ); 176 | Self::from_raw(out.assume_init()) 177 | } 178 | } 179 | } 180 | 181 | impl core::fmt::Debug for Matrix4 { 182 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 183 | f.debug_tuple("Matrix4").field(&self.rows_wzyx()).finish() 184 | } 185 | } 186 | 187 | #[cfg(feature = "glam")] 188 | impl From for Matrix4 { 189 | fn from(mat: glam::Mat4) -> Self { 190 | Matrix4::from_rows(core::array::from_fn(|i| mat.row(i).into())) 191 | } 192 | } 193 | 194 | #[cfg(feature = "glam")] 195 | impl From for glam::Mat4 { 196 | fn from(mat: Matrix4) -> Self { 197 | glam::Mat4::from_cols_array_2d(&mat.rows_xyzw()).transpose() 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /citro3d/examples/triangle.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates the most basic usage of `citro3d`: rendering a simple 2 | //! RGB triangle (sometimes called a "Hello triangle") to the 3DS screen. 3 | 4 | #![feature(allocator_api)] 5 | 6 | use citro3d::macros::include_shader; 7 | use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement}; 8 | use citro3d::render::{ClearFlags, RenderPass, Target}; 9 | use citro3d::texenv; 10 | use citro3d::{attrib, buffer, shader}; 11 | use ctru::prelude::*; 12 | use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; 13 | 14 | #[repr(C)] 15 | #[derive(Copy, Clone)] 16 | struct Vec3 { 17 | x: f32, 18 | y: f32, 19 | z: f32, 20 | } 21 | 22 | impl Vec3 { 23 | const fn new(x: f32, y: f32, z: f32) -> Self { 24 | Self { x, y, z } 25 | } 26 | } 27 | 28 | #[repr(C)] 29 | #[derive(Copy, Clone)] 30 | struct Vertex { 31 | pos: Vec3, 32 | color: Vec3, 33 | } 34 | 35 | static VERTICES: &[Vertex] = &[ 36 | Vertex { 37 | pos: Vec3::new(0.0, 0.5, -3.0), 38 | color: Vec3::new(1.0, 0.0, 0.0), 39 | }, 40 | Vertex { 41 | pos: Vec3::new(-0.5, -0.5, -3.0), 42 | color: Vec3::new(0.0, 1.0, 0.0), 43 | }, 44 | Vertex { 45 | pos: Vec3::new(0.5, -0.5, -3.0), 46 | color: Vec3::new(0.0, 0.0, 1.0), 47 | }, 48 | ]; 49 | 50 | static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); 51 | const CLEAR_COLOR: u32 = 0x68_B0_D8_FF; 52 | 53 | fn main() { 54 | let mut soc = Soc::new().expect("failed to get SOC"); 55 | drop(soc.redirect_to_3dslink(true, true)); 56 | 57 | let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); 58 | let mut hid = Hid::new().expect("Couldn't obtain HID controller"); 59 | let apt = Apt::new().expect("Couldn't obtain APT controller"); 60 | 61 | let mut instance = citro3d::Instance::new().expect("failed to initialize Citro3D"); 62 | 63 | let top_screen = TopScreen3D::from(&gfx.top_screen); 64 | 65 | let (mut top_left, mut top_right) = top_screen.split_mut(); 66 | 67 | let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer(); 68 | let mut top_left_target = instance 69 | .render_target(width, height, top_left, None) 70 | .expect("failed to create render target"); 71 | 72 | let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); 73 | let mut top_right_target = instance 74 | .render_target(width, height, top_right, None) 75 | .expect("failed to create render target"); 76 | 77 | let mut bottom_screen = gfx.bottom_screen.borrow_mut(); 78 | let RawFrameBuffer { width, height, .. } = bottom_screen.raw_framebuffer(); 79 | 80 | let mut bottom_target = instance 81 | .render_target(width, height, bottom_screen, None) 82 | .expect("failed to create bottom screen render target"); 83 | 84 | let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap(); 85 | let vertex_shader = shader.get(0).unwrap(); 86 | 87 | let program = shader::Program::new(vertex_shader).unwrap(); 88 | let projection_uniform_idx = program.get_uniform("projection").unwrap(); 89 | 90 | let mut vbo_data = Vec::with_capacity_in(VERTICES.len(), ctru::linear::LinearAllocator); 91 | vbo_data.extend_from_slice(VERTICES); 92 | 93 | let mut buf_info = buffer::Info::new(); 94 | let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); 95 | 96 | while apt.main_loop() { 97 | hid.scan_input(); 98 | 99 | if hid.keys_down().contains(KeyPad::START) { 100 | break; 101 | } 102 | 103 | instance.render_frame_with(|mut pass| { 104 | // Sadly closures can't have lifetime specifiers, 105 | // so we wrap `render_to` in this function to force the borrow checker rules. 106 | fn cast_lifetime_to_closure<'pass, T>(x: T) -> T 107 | where 108 | T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), 109 | { 110 | x 111 | } 112 | 113 | let render_to = cast_lifetime_to_closure(|pass, target, projection| { 114 | target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); 115 | 116 | pass.select_render_target(target) 117 | .expect("failed to set render target"); 118 | pass.bind_vertex_uniform(projection_uniform_idx, projection); 119 | 120 | pass.set_attr_info(&attr_info); 121 | 122 | pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); 123 | }); 124 | 125 | // We bind the vertex shader. 126 | pass.bind_program(&program); 127 | 128 | // Configure the first fragment shading substage to just pass through the vertex color 129 | // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight 130 | let stage0 = texenv::Stage::new(0).unwrap(); 131 | pass.texenv(stage0) 132 | .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) 133 | .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); 134 | 135 | let Projections { 136 | left_eye, 137 | right_eye, 138 | center, 139 | } = calculate_projections(); 140 | 141 | render_to(&mut pass, &mut top_left_target, &left_eye); 142 | render_to(&mut pass, &mut top_right_target, &right_eye); 143 | render_to(&mut pass, &mut bottom_target, ¢er); 144 | 145 | pass 146 | }); 147 | } 148 | } 149 | 150 | fn prepare_vbos<'a>( 151 | buf_info: &'a mut buffer::Info, 152 | vbo_data: &'a [Vertex], 153 | ) -> (attrib::Info, buffer::Slice<'a>) { 154 | // Configure attributes for use with the vertex shader 155 | let mut attr_info = attrib::Info::new(); 156 | 157 | let reg0 = attrib::Register::new(0).unwrap(); 158 | let reg1 = attrib::Register::new(1).unwrap(); 159 | 160 | attr_info 161 | .add_loader(reg0, attrib::Format::Float, 3) 162 | .unwrap(); 163 | 164 | attr_info 165 | .add_loader(reg1, attrib::Format::Float, 3) 166 | .unwrap(); 167 | 168 | let buf_idx = buf_info.add(vbo_data, &attr_info).unwrap(); 169 | 170 | (attr_info, buf_idx) 171 | } 172 | 173 | struct Projections { 174 | left_eye: Matrix4, 175 | right_eye: Matrix4, 176 | center: Matrix4, 177 | } 178 | 179 | fn calculate_projections() -> Projections { 180 | // TODO: it would be cool to allow playing around with these parameters on 181 | // the fly with D-pad, etc. 182 | let slider_val = ctru::os::current_3d_slider_state(); 183 | let interocular_distance = slider_val / 2.0; 184 | 185 | let vertical_fov = 40.0_f32.to_radians(); 186 | let screen_depth = 2.0; 187 | 188 | let clip_planes = ClipPlanes { 189 | near: 0.01, 190 | far: 100.0, 191 | }; 192 | 193 | let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); 194 | 195 | let (left_eye, right_eye) = 196 | Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) 197 | .stereo_matrices(left, right); 198 | 199 | let center = 200 | Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); 201 | 202 | Projections { 203 | left_eye, 204 | right_eye, 205 | center, 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /citro3d/src/math/ops.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::ops::{Add, Div, Mul, Neg, Sub}; 3 | 4 | #[cfg(feature = "approx")] 5 | use approx::AbsDiffEq; 6 | 7 | use super::{FVec, FVec3, FVec4, Matrix4}; 8 | 9 | // region: FVec4 math operators 10 | 11 | impl Add for FVec4 { 12 | type Output = Self; 13 | 14 | #[doc(alias = "FVec4_Add")] 15 | fn add(self, rhs: Self) -> Self::Output { 16 | Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.0) }) 17 | } 18 | } 19 | 20 | impl Sub for FVec4 { 21 | type Output = Self; 22 | 23 | #[doc(alias = "FVec4_Subtract")] 24 | fn sub(self, rhs: Self) -> Self::Output { 25 | Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.0) }) 26 | } 27 | } 28 | 29 | impl Neg for FVec4 { 30 | type Output = Self; 31 | 32 | #[doc(alias = "FVec4_Negate")] 33 | fn neg(self) -> Self::Output { 34 | Self(unsafe { citro3d_sys::FVec4_Negate(self.0) }) 35 | } 36 | } 37 | 38 | impl Mul for FVec4 { 39 | type Output = Self; 40 | 41 | #[doc(alias = "FVec4_Scale")] 42 | fn mul(self, rhs: f32) -> Self::Output { 43 | Self(unsafe { citro3d_sys::FVec4_Scale(self.0, rhs) }) 44 | } 45 | } 46 | 47 | // endregion 48 | 49 | // region: FVec3 math operators 50 | 51 | impl Add for FVec3 { 52 | type Output = Self; 53 | 54 | #[doc(alias = "FVec3_Add")] 55 | fn add(self, rhs: Self) -> Self::Output { 56 | Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.0) }) 57 | } 58 | } 59 | 60 | impl Sub for FVec3 { 61 | type Output = Self; 62 | 63 | #[doc(alias = "FVec3_Subtract")] 64 | fn sub(self, rhs: Self) -> Self::Output { 65 | Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.0) }) 66 | } 67 | } 68 | 69 | impl Neg for FVec3 { 70 | type Output = Self; 71 | 72 | #[doc(alias = "FVec3_Negate")] 73 | fn neg(self) -> Self::Output { 74 | Self(unsafe { citro3d_sys::FVec3_Negate(self.0) }) 75 | } 76 | } 77 | 78 | impl Mul for FVec3 { 79 | type Output = Self; 80 | 81 | #[doc(alias = "FVec3_Scale")] 82 | fn mul(self, rhs: f32) -> Self::Output { 83 | Self(unsafe { citro3d_sys::FVec3_Scale(self.0, rhs) }) 84 | } 85 | } 86 | 87 | // endregion 88 | 89 | impl Div for FVec 90 | where 91 | FVec: Mul, 92 | { 93 | type Output = >::Output; 94 | 95 | fn div(self, rhs: f32) -> Self::Output { 96 | self * (1.0 / rhs) 97 | } 98 | } 99 | 100 | impl PartialEq for FVec { 101 | fn eq(&self, other: &Self) -> bool { 102 | let range = (4 - N)..; 103 | unsafe { self.0.c[range.clone()] == other.0.c[range] } 104 | } 105 | } 106 | 107 | impl Eq for FVec {} 108 | 109 | #[cfg(feature = "approx")] 110 | impl AbsDiffEq for FVec { 111 | type Epsilon = f32; 112 | 113 | fn default_epsilon() -> Self::Epsilon { 114 | // See https://docs.rs/almost/latest/almost/#why-another-crate 115 | // for rationale of using this over just EPSILON 116 | f32::EPSILON.sqrt() 117 | } 118 | 119 | fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 120 | let range = (4 - N)..; 121 | let (lhs, rhs) = unsafe { (&self.0.c[range.clone()], &other.0.c[range]) }; 122 | lhs.abs_diff_eq(rhs, epsilon) 123 | } 124 | } 125 | 126 | // region: Matrix math operators 127 | 128 | impl Add for Matrix4 { 129 | type Output = Matrix4; 130 | 131 | #[doc(alias = "Mtx_Add")] 132 | fn add(self, rhs: Matrix4) -> Self::Output { 133 | let mut out = MaybeUninit::uninit(); 134 | unsafe { 135 | citro3d_sys::Mtx_Add(out.as_mut_ptr(), self.as_raw(), rhs.as_raw()); 136 | Matrix4::from_raw(out.assume_init()) 137 | } 138 | } 139 | } 140 | 141 | impl Sub for Matrix4 { 142 | type Output = Matrix4; 143 | 144 | #[doc(alias = "Mtx_Subtract")] 145 | fn sub(self, rhs: Matrix4) -> Self::Output { 146 | let mut out = MaybeUninit::uninit(); 147 | unsafe { 148 | citro3d_sys::Mtx_Subtract(out.as_mut_ptr(), self.as_raw(), rhs.as_raw()); 149 | Matrix4::from_raw(out.assume_init()) 150 | } 151 | } 152 | } 153 | 154 | impl Mul for Matrix4 { 155 | type Output = Matrix4; 156 | 157 | #[doc(alias = "Mtx_Multiply")] 158 | fn mul(self, rhs: Matrix4) -> Self::Output { 159 | let mut out = MaybeUninit::uninit(); 160 | unsafe { 161 | citro3d_sys::Mtx_Multiply(out.as_mut_ptr(), self.as_raw(), rhs.as_raw()); 162 | Matrix4::from_raw(out.assume_init()) 163 | } 164 | } 165 | } 166 | 167 | impl Mul for &Matrix4 { 168 | type Output = Matrix4; 169 | 170 | fn mul(self, rhs: Matrix4) -> Self::Output { 171 | *self * rhs 172 | } 173 | } 174 | 175 | impl Mul for &Matrix4 { 176 | type Output = FVec4; 177 | 178 | #[doc(alias = "Mtx_MultiplyFVec4")] 179 | fn mul(self, rhs: FVec4) -> Self::Output { 180 | FVec(unsafe { citro3d_sys::Mtx_MultiplyFVec4(self.as_raw(), rhs.0) }) 181 | } 182 | } 183 | 184 | impl Mul for &Matrix4 { 185 | type Output = FVec4; 186 | 187 | #[doc(alias = "Mtx_MultiplyFVecH")] 188 | fn mul(self, rhs: FVec3) -> Self::Output { 189 | FVec(unsafe { citro3d_sys::Mtx_MultiplyFVecH(self.as_raw(), rhs.0) }) 190 | } 191 | } 192 | 193 | impl PartialEq for Matrix4 { 194 | fn eq(&self, other: &Matrix4) -> bool { 195 | self.rows_wzyx() == other.rows_wzyx() 196 | } 197 | } 198 | 199 | // endregion 200 | 201 | #[cfg(feature = "approx")] 202 | #[doc(cfg(feature = "approx"))] 203 | impl AbsDiffEq for Matrix4 { 204 | type Epsilon = f32; 205 | 206 | fn default_epsilon() -> Self::Epsilon { 207 | // See https://docs.rs/almost/latest/almost/#why-another-crate 208 | // for rationale of using this over just EPSILON 209 | f32::EPSILON.sqrt() 210 | } 211 | 212 | fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 213 | self.rows_wzyx() 214 | .into_iter() 215 | .zip(other.rows_wzyx()) 216 | .all(|(l, r)| l.abs_diff_eq(&r, epsilon)) 217 | } 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use approx::assert_abs_diff_eq; 223 | 224 | use super::*; 225 | 226 | #[test] 227 | fn fvec3() { 228 | let l = FVec3::splat(1.0); 229 | let r = FVec3::splat(2.0); 230 | 231 | assert_abs_diff_eq!(l + r, FVec3::splat(3.0)); 232 | assert_abs_diff_eq!(l - r, FVec3::splat(-1.0)); 233 | assert_abs_diff_eq!(-l, FVec3::splat(-1.0)); 234 | assert_abs_diff_eq!(l * 1.5, FVec3::splat(1.5)); 235 | assert_abs_diff_eq!(l / 2.0, FVec3::splat(0.5)); 236 | } 237 | 238 | #[test] 239 | fn fvec4() { 240 | let l = FVec4::splat(1.0); 241 | let r = FVec4::splat(2.0); 242 | 243 | assert_abs_diff_eq!(l + r, FVec4::splat(3.0)); 244 | assert_abs_diff_eq!(l - r, FVec4::splat(-1.0)); 245 | assert_abs_diff_eq!(-l, FVec4::splat(-1.0)); 246 | assert_abs_diff_eq!(l * 1.5, FVec4::splat(1.5)); 247 | assert_abs_diff_eq!(l / 2.0, FVec4::splat(0.5)); 248 | } 249 | 250 | #[test] 251 | fn matrix4() { 252 | let l = Matrix4::diagonal(1.0, 2.0, 3.0, 4.0); 253 | let r = Matrix4::identity(); 254 | 255 | assert_abs_diff_eq!(l * r, l); 256 | assert_abs_diff_eq!(l + r, Matrix4::diagonal(2.0, 3.0, 4.0, 5.0)); 257 | assert_abs_diff_eq!(l - r, Matrix4::diagonal(0.0, 1.0, 2.0, 3.0)); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /citro3d/src/buffer.rs: -------------------------------------------------------------------------------- 1 | //! Configure vertex buffer objects to be sent to the GPU for rendering. 2 | //! 3 | //! See the [`attrib`] module for details on how to describe the shape and type 4 | //! of the VBO data. 5 | 6 | use std::mem::MaybeUninit; 7 | 8 | use ctru::linear::LinearAllocator; 9 | 10 | use crate::Error; 11 | use crate::attrib; 12 | 13 | /// Vertex buffer info. This struct is used to describe the shape of the buffer 14 | /// data to be sent to the GPU for rendering. 15 | #[derive(Debug)] 16 | #[doc(alias = "C3D_BufInfo")] 17 | pub struct Info(pub(crate) citro3d_sys::C3D_BufInfo); 18 | 19 | /// A slice of buffer data. This borrows the buffer data and can be thought of 20 | /// as similar to `&[T]` obtained by slicing a `Vec`. 21 | #[derive(Debug, Clone, Copy)] 22 | pub struct Slice<'buf> { 23 | index: libc::c_int, 24 | size: libc::c_int, 25 | buf_info: &'buf Info, 26 | // TODO: should we encapsulate the primitive here too, and require it when the 27 | // slice is registered? Could there ever be a use case to draw different primitives 28 | // using the same backing data??? 29 | } 30 | 31 | impl Slice<'_> { 32 | /// Get the index into the buffer for this slice. 33 | pub fn index(&self) -> libc::c_int { 34 | self.index 35 | } 36 | 37 | /// Get the length of the slice. 38 | #[must_use] 39 | pub fn len(&self) -> libc::c_int { 40 | self.size 41 | } 42 | 43 | /// Return whether or not the slice has any elements. 44 | pub fn is_empty(&self) -> bool { 45 | self.len() <= 0 46 | } 47 | 48 | /// Get the buffer info this slice is associated with. 49 | pub fn info(&self) -> &Info { 50 | self.buf_info 51 | } 52 | 53 | /// Get an index buffer for this slice using the given indices. 54 | /// 55 | /// # Errors 56 | /// 57 | /// Returns an error if: 58 | /// - any of the given indices are out of bounds. 59 | /// - the given slice is too long for its length to fit in a `libc::c_int`. 60 | pub fn index_buffer(&self, indices: &[I]) -> Result, Error> 61 | where 62 | I: Index + Copy + Into, 63 | { 64 | if libc::c_int::try_from(indices.len()).is_err() { 65 | return Err(Error::InvalidSize); 66 | } 67 | 68 | for &idx in indices { 69 | let idx = idx.into(); 70 | let len = self.len(); 71 | if idx >= len { 72 | return Err(Error::IndexOutOfBounds { idx, len }); 73 | } 74 | } 75 | 76 | Ok(unsafe { self.index_buffer_unchecked(indices) }) 77 | } 78 | 79 | /// Get an index buffer for this slice using the given indices without 80 | /// bounds checking. 81 | /// 82 | /// # Safety 83 | /// 84 | /// If any indices are outside this buffer it can cause an invalid access by the GPU 85 | /// (this crashes citra). 86 | pub unsafe fn index_buffer_unchecked(&self, indices: &[I]) -> Indices<'_, I> { 87 | let mut buffer = Vec::with_capacity_in(indices.len(), LinearAllocator); 88 | buffer.extend_from_slice(indices); 89 | Indices { 90 | buffer, 91 | _slice: *self, 92 | } 93 | } 94 | } 95 | 96 | /// An index buffer for indexed drawing. See [`Slice::index_buffer`] to obtain one. 97 | pub struct Indices<'buf, I> { 98 | pub(crate) buffer: Vec, 99 | _slice: Slice<'buf>, 100 | } 101 | 102 | /// A type that can be used as an index for indexed drawing. 103 | pub trait Index: crate::private::Sealed { 104 | /// The data type of the index, as used by [`citro3d_sys::C3D_DrawElements`]'s `type_` parameter. 105 | const TYPE: libc::c_int; 106 | } 107 | 108 | impl Index for u8 { 109 | const TYPE: libc::c_int = citro3d_sys::C3D_UNSIGNED_BYTE as _; 110 | } 111 | 112 | impl Index for u16 { 113 | const TYPE: libc::c_int = citro3d_sys::C3D_UNSIGNED_SHORT as _; 114 | } 115 | 116 | /// The geometric primitive to draw (i.e. what shapes the buffer data describes). 117 | #[repr(u16)] 118 | #[derive(Debug, Clone, Copy)] 119 | #[doc(alias = "GPU_Primitive_t")] 120 | pub enum Primitive { 121 | /// Draw triangles (3 vertices per triangle). 122 | Triangles = ctru_sys::GPU_TRIANGLES, 123 | /// Draw a triangle strip (each vertex shared by 1-3 triangles). 124 | TriangleStrip = ctru_sys::GPU_TRIANGLE_STRIP, 125 | /// Draw a triangle fan (first vertex shared by all triangles). 126 | TriangleFan = ctru_sys::GPU_TRIANGLE_FAN, 127 | /// Geometry primitive. Can be used for more complex use cases like geometry 128 | /// shaders that output custom primitives. 129 | GeometryPrim = ctru_sys::GPU_GEOMETRY_PRIM, 130 | } 131 | 132 | impl Default for Info { 133 | #[doc(alias = "BufInfo_Init")] 134 | fn default() -> Self { 135 | let mut info = MaybeUninit::zeroed(); 136 | let info = unsafe { 137 | citro3d_sys::BufInfo_Init(info.as_mut_ptr()); 138 | info.assume_init() 139 | }; 140 | Self(info) 141 | } 142 | } 143 | 144 | impl Info { 145 | /// Construct buffer info without any registered data. 146 | pub fn new() -> Self { 147 | Self::default() 148 | } 149 | 150 | pub(crate) fn copy_from(raw: *const citro3d_sys::C3D_BufInfo) -> Option { 151 | if raw.is_null() { 152 | None 153 | } else { 154 | // This is less efficient than returning a pointer or something, but it's 155 | // safer since we don't know the lifetime of the pointee 156 | Some(Self(unsafe { *raw })) 157 | } 158 | } 159 | 160 | /// Register vertex buffer object data. The resulting [`Slice`] will have its 161 | /// lifetime tied to both this [`Info`] and the passed-in VBO. `vbo_data` is 162 | /// assumed to use one `T` per drawn primitive, and its layout is assumed to 163 | /// match the given `attrib_info` 164 | /// 165 | /// # Errors 166 | /// 167 | /// Registering VBO data may fail: 168 | /// 169 | /// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator 170 | /// * if the maximum number (12) of VBOs are already registered 171 | #[doc(alias = "BufInfo_Add")] 172 | pub fn add<'this, 'vbo, 'idx, T>( 173 | &'this mut self, 174 | vbo_data: &'vbo [T], 175 | attrib_info: &attrib::Info, 176 | ) -> crate::Result> 177 | where 178 | 'this: 'idx, 179 | 'vbo: 'idx, 180 | { 181 | let stride = std::mem::size_of::().try_into()?; 182 | 183 | // SAFETY: the lifetime of the VBO data is encapsulated in the return value's 184 | // 'vbo lifetime, and the pointer to &mut self.0 is used to access values 185 | // in the BufInfo, not copied to be used later. 186 | let res = unsafe { 187 | citro3d_sys::BufInfo_Add( 188 | &mut self.0, 189 | vbo_data.as_ptr().cast(), 190 | stride, 191 | attrib_info.attr_count(), 192 | attrib_info.permutation(), 193 | ) 194 | }; 195 | 196 | // Error codes from 197 | match res { 198 | ..=-3 => Err(crate::Error::System(res)), 199 | -2 => Err(crate::Error::InvalidMemoryLocation), 200 | -1 => Err(crate::Error::TooManyBuffers), 201 | _ => Ok(Slice { 202 | index: res, 203 | size: vbo_data.len().try_into()?, 204 | buf_info: self, 205 | }), 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /citro3d/examples/cube.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates drawing a coloured cube using indexed rendering. 2 | 3 | #![feature(allocator_api)] 4 | 5 | use citro3d::macros::include_shader; 6 | use citro3d::math::{ 7 | AspectRatio, ClipPlanes, CoordinateOrientation, FVec3, Matrix4, Projection, StereoDisplacement, 8 | }; 9 | use citro3d::render::{ClearFlags, RenderPass, Target}; 10 | use citro3d::{attrib, buffer, shader, texenv}; 11 | use ctru::prelude::*; 12 | use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; 13 | 14 | #[repr(C)] 15 | #[derive(Copy, Clone)] 16 | struct Vec3 { 17 | x: f32, 18 | y: f32, 19 | z: f32, 20 | } 21 | 22 | impl Vec3 { 23 | const fn new(x: f32, y: f32, z: f32) -> Self { 24 | Self { x, y, z } 25 | } 26 | } 27 | 28 | #[repr(C)] 29 | #[derive(Copy, Clone)] 30 | struct Vertex { 31 | pos: Vec3, 32 | color: Vec3, 33 | } 34 | 35 | // borrowed from https://bevyengine.org/examples/3D%20Rendering/generate-custom-mesh/ 36 | const VERTS: &[[f32; 3]] = &[ 37 | // top (facing towards +y) 38 | [-0.5, 0.5, -0.5], // vertex with index 0 39 | [0.5, 0.5, -0.5], // vertex with index 1 40 | [0.5, 0.5, 0.5], // etc. until 23 41 | [-0.5, 0.5, 0.5], 42 | // bottom (-y) 43 | [-0.5, -0.5, -0.5], 44 | [0.5, -0.5, -0.5], 45 | [0.5, -0.5, 0.5], 46 | [-0.5, -0.5, 0.5], 47 | // right (+x) 48 | [0.5, -0.5, -0.5], 49 | [0.5, -0.5, 0.5], 50 | [0.5, 0.5, 0.5], // This vertex is at the same position as vertex with index 2, but they'll have different UV and normal 51 | [0.5, 0.5, -0.5], 52 | // left (-x) 53 | [-0.5, -0.5, -0.5], 54 | [-0.5, -0.5, 0.5], 55 | [-0.5, 0.5, 0.5], 56 | [-0.5, 0.5, -0.5], 57 | // back (+z) 58 | [-0.5, -0.5, 0.5], 59 | [-0.5, 0.5, 0.5], 60 | [0.5, 0.5, 0.5], 61 | [0.5, -0.5, 0.5], 62 | // forward (-z) 63 | [-0.5, -0.5, -0.5], 64 | [-0.5, 0.5, -0.5], 65 | [0.5, 0.5, -0.5], 66 | [0.5, -0.5, -0.5], 67 | ]; 68 | 69 | static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); 70 | const CLEAR_COLOR: u32 = 0x68_B0_D8_FF; 71 | 72 | fn main() { 73 | let mut soc = Soc::new().expect("failed to get SOC"); 74 | drop(soc.redirect_to_3dslink(true, true)); 75 | 76 | let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); 77 | let mut hid = Hid::new().expect("Couldn't obtain HID controller"); 78 | let apt = Apt::new().expect("Couldn't obtain APT controller"); 79 | 80 | let mut instance = citro3d::Instance::new().expect("failed to initialize Citro3D"); 81 | 82 | let top_screen = TopScreen3D::from(&gfx.top_screen); 83 | 84 | let (mut top_left, mut top_right) = top_screen.split_mut(); 85 | 86 | let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer(); 87 | let mut top_left_target = instance 88 | .render_target(width, height, top_left, None) 89 | .expect("failed to create render target"); 90 | 91 | let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); 92 | let mut top_right_target = instance 93 | .render_target(width, height, top_right, None) 94 | .expect("failed to create render target"); 95 | 96 | let mut bottom_screen = gfx.bottom_screen.borrow_mut(); 97 | let RawFrameBuffer { width, height, .. } = bottom_screen.raw_framebuffer(); 98 | 99 | let mut bottom_target = instance 100 | .render_target(width, height, bottom_screen, None) 101 | .expect("failed to create bottom screen render target"); 102 | 103 | let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap(); 104 | let vertex_shader = shader.get(0).unwrap(); 105 | 106 | let program = shader::Program::new(vertex_shader).unwrap(); 107 | 108 | let mut vbo_data = Vec::with_capacity_in(VERTS.len(), ctru::linear::LinearAllocator); 109 | for vert in VERTS.iter().enumerate().map(|(i, v)| Vertex { 110 | pos: Vec3 { 111 | x: v[0], 112 | y: v[1], 113 | z: v[2], 114 | }, 115 | color: { 116 | // Give each vertex a slightly different color just to highlight edges/corners 117 | let value = i as f32 / VERTS.len() as f32; 118 | Vec3::new(1.0, 0.7 * value, 0.5) 119 | }, 120 | }) { 121 | vbo_data.push(vert); 122 | } 123 | 124 | let attr_info = build_attrib_info(); 125 | 126 | let mut buf_info = buffer::Info::new(); 127 | let vbo_slice = buf_info.add(&vbo_data, &attr_info).unwrap(); 128 | 129 | let projection_uniform_idx = program.get_uniform("projection").unwrap(); 130 | let camera_transform = Matrix4::looking_at( 131 | FVec3::new(1.8, 1.8, 1.8), 132 | FVec3::new(0.0, 0.0, 0.0), 133 | FVec3::new(0.0, 1.0, 0.0), 134 | CoordinateOrientation::RightHanded, 135 | ); 136 | let indices: &[u8] = &[ 137 | 0, 3, 1, 1, 3, 2, // triangles making up the top (+y) facing side. 138 | 4, 5, 7, 5, 6, 7, // bottom (-y) 139 | 8, 11, 9, 9, 11, 10, // right (+x) 140 | 12, 13, 15, 13, 14, 15, // left (-x) 141 | 16, 19, 17, 17, 19, 18, // back (+z) 142 | 20, 21, 23, 21, 22, 23, // forward (-z) 143 | ]; 144 | let index_buffer = vbo_slice.index_buffer(indices).unwrap(); 145 | 146 | while apt.main_loop() { 147 | hid.scan_input(); 148 | 149 | if hid.keys_down().contains(KeyPad::START) { 150 | break; 151 | } 152 | 153 | instance.render_frame_with(|mut pass| { 154 | fn cast_lifetime_to_closure<'pass, T>(x: T) -> T 155 | where 156 | T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), 157 | { 158 | x 159 | } 160 | 161 | let render_to = cast_lifetime_to_closure(|pass, target, projection| { 162 | target.clear(ClearFlags::ALL, CLEAR_COLOR, 0); 163 | 164 | pass.select_render_target(target) 165 | .expect("failed to set render target"); 166 | 167 | pass.bind_vertex_uniform(projection_uniform_idx, projection * camera_transform); 168 | 169 | pass.set_attr_info(&attr_info); 170 | 171 | pass.draw_elements(buffer::Primitive::Triangles, vbo_slice, &index_buffer); 172 | }); 173 | 174 | pass.bind_program(&program); 175 | 176 | let stage0 = texenv::Stage::new(0).unwrap(); 177 | pass.texenv(stage0) 178 | .src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None) 179 | .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); 180 | 181 | let Projections { 182 | left_eye, 183 | right_eye, 184 | center, 185 | } = calculate_projections(); 186 | 187 | render_to(&mut pass, &mut top_left_target, &left_eye); 188 | render_to(&mut pass, &mut top_right_target, &right_eye); 189 | render_to(&mut pass, &mut bottom_target, ¢er); 190 | 191 | pass 192 | }); 193 | } 194 | } 195 | 196 | fn build_attrib_info() -> attrib::Info { 197 | // Configure attributes for use with the vertex shader 198 | let mut attr_info = attrib::Info::new(); 199 | 200 | let reg0 = attrib::Register::new(0).unwrap(); 201 | let reg1 = attrib::Register::new(1).unwrap(); 202 | 203 | attr_info 204 | .add_loader(reg0, attrib::Format::Float, 3) 205 | .unwrap(); 206 | 207 | attr_info 208 | .add_loader(reg1, attrib::Format::Float, 3) 209 | .unwrap(); 210 | 211 | attr_info 212 | } 213 | 214 | struct Projections { 215 | left_eye: Matrix4, 216 | right_eye: Matrix4, 217 | center: Matrix4, 218 | } 219 | 220 | fn calculate_projections() -> Projections { 221 | // TODO: it would be cool to allow playing around with these parameters on 222 | // the fly with D-pad, etc. 223 | let slider_val = ctru::os::current_3d_slider_state(); 224 | let interocular_distance = slider_val / 2.0; 225 | 226 | let vertical_fov = 40.0_f32.to_radians(); 227 | let screen_depth = 2.0; 228 | 229 | let clip_planes = ClipPlanes { 230 | near: 0.01, 231 | far: 100.0, 232 | }; 233 | 234 | let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); 235 | 236 | let (left_eye, right_eye) = 237 | Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) 238 | .stereo_matrices(left, right); 239 | 240 | let center = 241 | Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); 242 | 243 | Projections { 244 | left_eye, 245 | right_eye, 246 | center, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /citro3d/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /citro3d-macros/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /citro3d/src/math/fvec.rs: -------------------------------------------------------------------------------- 1 | //! Floating-point vectors. 2 | 3 | use std::fmt; 4 | 5 | /// A vector of `f32`s. 6 | /// 7 | /// # Layout 8 | /// Note that this matches the PICA layout so is actually WZYX, this means using it 9 | /// in vertex data as an attribute it will be reversed 10 | /// 11 | /// It is guaranteed to have the same layout as [`citro3d_sys::C3D_FVec`] in memory 12 | #[derive(Clone, Copy)] 13 | #[doc(alias = "C3D_FVec")] 14 | #[repr(transparent)] 15 | pub struct FVec(pub(crate) citro3d_sys::C3D_FVec); 16 | 17 | /// A 3-vector of `f32`s. 18 | pub type FVec3 = FVec<3>; 19 | 20 | /// A 4-vector of `f32`s. 21 | pub type FVec4 = FVec<4>; 22 | 23 | impl fmt::Debug for FVec { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | let inner = unsafe { self.0.__bindgen_anon_1 }; 26 | let type_name = std::any::type_name::().split("::").last().unwrap(); 27 | f.debug_tuple(type_name).field(&inner).finish() 28 | } 29 | } 30 | 31 | impl FVec { 32 | /// The vector's `x` component (also called the `i` component of `ijk[r]`). 33 | #[doc(alias = "i")] 34 | pub fn x(self) -> f32 { 35 | unsafe { self.0.__bindgen_anon_1.x } 36 | } 37 | 38 | /// The vector's `y` component (also called the `j` component of `ijk[r]`). 39 | #[doc(alias = "j")] 40 | pub fn y(self) -> f32 { 41 | unsafe { self.0.__bindgen_anon_1.y } 42 | } 43 | 44 | /// The vector's `i` component (also called the `k` component of `ijk[r]`). 45 | #[doc(alias = "k")] 46 | pub fn z(self) -> f32 { 47 | unsafe { self.0.__bindgen_anon_1.z } 48 | } 49 | } 50 | 51 | impl FVec4 { 52 | /// The vector's `w` component (also called `r` for the real component of `ijk[r]`). 53 | #[doc(alias = "r")] 54 | pub fn w(self) -> f32 { 55 | unsafe { self.0.__bindgen_anon_1.w } 56 | } 57 | 58 | /// Wrap a raw [`citro3d_sys::C3D_FVec`] 59 | pub fn from_raw(raw: citro3d_sys::C3D_FVec) -> Self { 60 | Self(raw) 61 | } 62 | 63 | /// Create a new [`FVec4`] from its components. 64 | /// 65 | /// # Example 66 | /// ``` 67 | /// # let _runner = test_runner::GdbRunner::default(); 68 | /// # use citro3d::math::FVec4; 69 | /// let v = FVec4::new(1.0, 2.0, 3.0, 4.0); 70 | /// ``` 71 | #[doc(alias = "FVec4_New")] 72 | pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { 73 | Self(unsafe { citro3d_sys::FVec4_New(x, y, z, w) }) 74 | } 75 | 76 | /// Create a new [`FVec4`], setting each component to `v`. 77 | /// 78 | /// # Example 79 | /// ``` 80 | /// # let _runner = test_runner::GdbRunner::default(); 81 | /// # use citro3d::math::FVec4; 82 | /// # use approx::assert_abs_diff_eq; 83 | /// let v = FVec4::splat(1.0); 84 | /// assert_abs_diff_eq!(v, FVec4::new(1.0, 1.0, 1.0, 1.0)); 85 | /// ``` 86 | pub fn splat(v: f32) -> Self { 87 | Self::new(v, v, v, v) 88 | } 89 | 90 | /// Divide the vector's XYZ components by its W component. 91 | /// 92 | /// # Example 93 | /// ``` 94 | /// # let _runner = test_runner::GdbRunner::default(); 95 | /// # use citro3d::math::FVec4; 96 | /// # use approx::assert_abs_diff_eq; 97 | /// let v = FVec4::new(2.0, 4.0, 6.0, 2.0); 98 | /// assert_abs_diff_eq!(v.perspective_divide(), FVec4::new(1.0, 2.0, 3.0, 1.0)); 99 | /// ``` 100 | #[doc(alias = "FVec4_PerspDivide")] 101 | pub fn perspective_divide(self) -> Self { 102 | Self(unsafe { citro3d_sys::FVec4_PerspDivide(self.0) }) 103 | } 104 | 105 | /// The dot product of two vectors. 106 | /// 107 | /// # Example 108 | /// ``` 109 | /// # let _runner = test_runner::GdbRunner::default(); 110 | /// # use citro3d::math::FVec4; 111 | /// # use approx::assert_abs_diff_eq; 112 | /// let v1 = FVec4::new(1.0, 2.0, 3.0, 4.0); 113 | /// let v2 = FVec4::new(1.0, 0.5, 1.0, 0.5); 114 | /// assert_abs_diff_eq!(v1.dot(v2), 7.0); 115 | /// ``` 116 | #[doc(alias = "FVec4_Dot")] 117 | pub fn dot(self, rhs: Self) -> f32 { 118 | unsafe { citro3d_sys::FVec4_Dot(self.0, rhs.0) } 119 | } 120 | 121 | /// The magnitude of the vector. 122 | /// 123 | /// # Example 124 | /// ``` 125 | /// # let _runner = test_runner::GdbRunner::default(); 126 | /// # use citro3d::math::FVec4; 127 | /// # use approx::assert_abs_diff_eq; 128 | /// let v = FVec4::splat(1.0); 129 | /// assert_abs_diff_eq!(v.magnitude(), 2.0); 130 | /// ``` 131 | #[doc(alias = "FVec4_Magnitude")] 132 | pub fn magnitude(self) -> f32 { 133 | unsafe { citro3d_sys::FVec4_Magnitude(self.0) } 134 | } 135 | 136 | /// Normalize the vector to a magnitude of `1.0`. 137 | /// 138 | /// # Example 139 | /// ``` 140 | /// # let _runner = test_runner::GdbRunner::default(); 141 | /// # use citro3d::math::FVec4; 142 | /// # use approx::assert_abs_diff_eq; 143 | /// let v = FVec4::new(1.0, 2.0, 2.0, 4.0); 144 | /// assert_abs_diff_eq!(v.normalize(), FVec4::new(0.2, 0.4, 0.4, 0.8)); 145 | /// ``` 146 | #[doc(alias = "FVec4_Normalize")] 147 | pub fn normalize(self) -> Self { 148 | Self(unsafe { citro3d_sys::FVec4_Normalize(self.0) }) 149 | } 150 | } 151 | 152 | impl FVec3 { 153 | /// Create a new [`FVec3`] from its components. 154 | /// 155 | /// # Example 156 | /// ``` 157 | /// # let _runner = test_runner::GdbRunner::default(); 158 | /// # use citro3d::math::FVec3; 159 | /// let v = FVec3::new(1.0, 2.0, 3.0); 160 | /// ``` 161 | #[doc(alias = "FVec3_New")] 162 | pub fn new(x: f32, y: f32, z: f32) -> Self { 163 | Self(unsafe { citro3d_sys::FVec3_New(x, y, z) }) 164 | } 165 | 166 | /// Create a new [`FVec3`], setting each component to the given `v`. 167 | /// 168 | /// # Example 169 | /// ``` 170 | /// # let _runner = test_runner::GdbRunner::default(); 171 | /// # use citro3d::math::FVec3; 172 | /// let v = FVec3::splat(1.0); 173 | /// ``` 174 | pub fn splat(v: f32) -> Self { 175 | Self::new(v, v, v) 176 | } 177 | 178 | /// The distance between two points in 3D space. 179 | /// 180 | /// # Example 181 | /// ``` 182 | /// # let _runner = test_runner::GdbRunner::default(); 183 | /// # use citro3d::math::FVec3; 184 | /// # use approx::assert_abs_diff_eq; 185 | /// let l = FVec3::new(1.0, 3.0, 4.0); 186 | /// let r = FVec3::new(0.0, 1.0, 2.0); 187 | /// 188 | /// assert_abs_diff_eq!(l.distance(r), 3.0); 189 | /// ``` 190 | #[doc(alias = "FVec3_Distance")] 191 | pub fn distance(self, rhs: Self) -> f32 { 192 | unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) } 193 | } 194 | 195 | /// The cross product of two 3D vectors. 196 | /// 197 | /// # Example 198 | /// ``` 199 | /// # let _runner = test_runner::GdbRunner::default(); 200 | /// # use citro3d::math::FVec3; 201 | /// # use approx::assert_abs_diff_eq; 202 | /// let l = FVec3::new(1.0, 0.0, 0.0); 203 | /// let r = FVec3::new(0.0, 1.0, 0.0); 204 | /// assert_abs_diff_eq!(l.cross(r), FVec3::new(0.0, 0.0, 1.0)); 205 | /// ``` 206 | #[doc(alias = "FVec3_Cross")] 207 | pub fn cross(self, rhs: Self) -> Self { 208 | Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) }) 209 | } 210 | 211 | /// The dot product of two vectors. 212 | /// 213 | /// # Example 214 | /// ``` 215 | /// # let _runner = test_runner::GdbRunner::default(); 216 | /// # use citro3d::math::FVec3; 217 | /// # use approx::assert_abs_diff_eq; 218 | /// let l = FVec3::new(1.0, 2.0, 3.0); 219 | /// let r = FVec3::new(3.0, 2.0, 1.0); 220 | /// assert_abs_diff_eq!(l.dot(r), 10.0); 221 | /// ``` 222 | #[doc(alias = "FVec3_Dot")] 223 | pub fn dot(self, rhs: Self) -> f32 { 224 | unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } 225 | } 226 | 227 | /// The magnitude of the vector. 228 | /// 229 | /// # Example 230 | /// ``` 231 | /// # let _runner = test_runner::GdbRunner::default(); 232 | /// # use citro3d::math::FVec3; 233 | /// # use approx::assert_abs_diff_eq; 234 | /// let v = FVec3::splat(3.0f32.sqrt()); 235 | /// assert_abs_diff_eq!(v.magnitude(), 3.0); 236 | /// ``` 237 | #[doc(alias = "FVec3_Magnitude")] 238 | pub fn magnitude(self) -> f32 { 239 | unsafe { citro3d_sys::FVec3_Magnitude(self.0) } 240 | } 241 | 242 | /// Normalize the vector to a magnitude of `1.0`. 243 | /// 244 | /// # Example 245 | /// ``` 246 | /// # let _runner = test_runner::GdbRunner::default(); 247 | /// # use citro3d::math::FVec3; 248 | /// # use approx::assert_abs_diff_eq; 249 | /// let v = FVec3::splat(1.0); 250 | /// assert_abs_diff_eq!(v.normalize(), FVec3::splat(1.0 / 3.0_f32.sqrt())); 251 | /// ``` 252 | #[doc(alias = "FVec3_Normalize")] 253 | pub fn normalize(self) -> Self { 254 | Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) 255 | } 256 | } 257 | 258 | #[cfg(feature = "glam")] 259 | impl From for FVec4 { 260 | fn from(value: glam::Vec4) -> Self { 261 | Self::new(value.x, value.y, value.z, value.w) 262 | } 263 | } 264 | #[cfg(feature = "glam")] 265 | impl From for FVec3 { 266 | fn from(value: glam::Vec3) -> Self { 267 | Self::new(value.x, value.y, value.z) 268 | } 269 | } 270 | #[cfg(feature = "glam")] 271 | impl From for glam::Vec4 { 272 | fn from(value: FVec4) -> Self { 273 | glam::Vec4::new(value.x(), value.y(), value.z(), value.w()) 274 | } 275 | } 276 | 277 | #[cfg(feature = "glam")] 278 | impl From for glam::Vec3 { 279 | fn from(value: FVec3) -> Self { 280 | glam::Vec3::new(value.x(), value.y(), value.z()) 281 | } 282 | } 283 | 284 | #[cfg(test)] 285 | mod tests { 286 | use approx::assert_abs_diff_eq; 287 | 288 | use super::*; 289 | 290 | #[test] 291 | fn fvec4() { 292 | let v = FVec4::new(1.0, 2.0, 3.0, 4.0); 293 | let actual = [v.x(), v.y(), v.z(), v.w()]; 294 | let expected = [1.0, 2.0, 3.0, 4.0]; 295 | assert_abs_diff_eq!(&actual[..], &expected[..]); 296 | } 297 | 298 | #[test] 299 | fn fvec3() { 300 | let v = FVec3::new(1.0, 2.0, 3.0); 301 | let actual = [v.x(), v.y(), v.z()]; 302 | let expected = [1.0, 2.0, 3.0]; 303 | assert_abs_diff_eq!(&actual[..], &expected[..]); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /citro3d/examples/fragment-light.rs: -------------------------------------------------------------------------------- 1 | #![feature(allocator_api)] 2 | use std::f32::consts::PI; 3 | 4 | use citro3d::{ 5 | attrib, buffer, 6 | color::Color, 7 | light::{DistanceAttenuation, LightEnv, Lut, LutId, LutInput, Material, Spotlight}, 8 | math::{AspectRatio, ClipPlanes, FVec3, Matrix4, Projection, StereoDisplacement}, 9 | render::{ClearFlags, DepthFormat, RenderPass, Target}, 10 | shader, texenv, 11 | }; 12 | use citro3d_macros::include_shader; 13 | use ctru::services::{ 14 | apt::Apt, 15 | gfx::{Gfx, RawFrameBuffer, Screen, TopScreen3D}, 16 | hid::{Hid, KeyPad}, 17 | }; 18 | 19 | #[repr(C)] 20 | #[derive(Copy, Clone)] 21 | struct Vec3 { 22 | x: f32, 23 | y: f32, 24 | z: f32, 25 | } 26 | 27 | impl Vec3 { 28 | const fn new(x: f32, y: f32, z: f32) -> Self { 29 | Self { x, y, z } 30 | } 31 | } 32 | #[derive(Copy, Clone)] 33 | #[repr(C)] 34 | struct Vec2 { 35 | x: f32, 36 | y: f32, 37 | } 38 | 39 | impl Vec2 { 40 | const fn new(x: f32, y: f32) -> Self { 41 | Self { x, y } 42 | } 43 | } 44 | 45 | #[repr(C)] 46 | #[derive(Copy, Clone)] 47 | struct Vertex { 48 | pos: Vec3, 49 | normal: Vec3, 50 | uv: Vec2, 51 | } 52 | 53 | impl Vertex { 54 | const fn new(pos: Vec3, normal: Vec3, uv: Vec2) -> Self { 55 | Self { pos, normal, uv } 56 | } 57 | } 58 | 59 | static SHADER_BYTES: &[u8] = include_shader!("assets/frag-shader.pica"); 60 | 61 | const VERTICES: &[Vertex] = &[ 62 | Vertex::new( 63 | Vec3::new(-0.5, -0.5, 0.5), 64 | Vec3::new(0.0, 0.0, 1.0), 65 | Vec2::new(0.0, 0.0), 66 | ), 67 | Vertex::new( 68 | Vec3::new(0.5, -0.5, 0.5), 69 | Vec3::new(0.0, 0.0, 1.0), 70 | Vec2::new(1.0, 0.0), 71 | ), 72 | Vertex::new( 73 | Vec3::new(0.5, 0.5, 0.5), 74 | Vec3::new(0.0, 0.0, 1.0), 75 | Vec2::new(1.0, 1.0), 76 | ), 77 | Vertex::new( 78 | Vec3::new(0.5, 0.5, 0.5), 79 | Vec3::new(0.0, 0.0, 1.0), 80 | Vec2::new(1.0, 1.0), 81 | ), 82 | Vertex::new( 83 | Vec3::new(-0.5, 0.5, 0.5), 84 | Vec3::new(0.0, 0.0, 1.0), 85 | Vec2::new(0.0, 1.0), 86 | ), 87 | Vertex::new( 88 | Vec3::new(-0.5, -0.5, 0.5), 89 | Vec3::new(0.0, 0.0, 1.0), 90 | Vec2::new(0.0, 0.0), 91 | ), 92 | Vertex::new( 93 | Vec3::new(-0.5, -0.5, -0.5), 94 | Vec3::new(0.0, 0.0, -1.0), 95 | Vec2::new(0.0, 0.0), 96 | ), 97 | Vertex::new( 98 | Vec3::new(-0.5, 0.5, -0.5), 99 | Vec3::new(0.0, 0.0, -1.0), 100 | Vec2::new(1.0, 0.0), 101 | ), 102 | Vertex::new( 103 | Vec3::new(0.5, 0.5, -0.5), 104 | Vec3::new(0.0, 0.0, -1.0), 105 | Vec2::new(1.0, 1.0), 106 | ), 107 | Vertex::new( 108 | Vec3::new(0.5, 0.5, -0.5), 109 | Vec3::new(0.0, 0.0, -1.0), 110 | Vec2::new(1.0, 1.0), 111 | ), 112 | Vertex::new( 113 | Vec3::new(0.5, -0.5, -0.5), 114 | Vec3::new(0.0, 0.0, -1.0), 115 | Vec2::new(0.0, 1.0), 116 | ), 117 | Vertex::new( 118 | Vec3::new(-0.5, -0.5, -0.5), 119 | Vec3::new(0.0, 0.0, -1.0), 120 | Vec2::new(0.0, 0.0), 121 | ), 122 | Vertex::new( 123 | Vec3::new(0.5, -0.5, -0.5), 124 | Vec3::new(1.0, 0.0, 0.0), 125 | Vec2::new(0.0, 0.0), 126 | ), 127 | Vertex::new( 128 | Vec3::new(0.5, 0.5, -0.5), 129 | Vec3::new(1.0, 0.0, 0.0), 130 | Vec2::new(1.0, 0.0), 131 | ), 132 | Vertex::new( 133 | Vec3::new(0.5, 0.5, 0.5), 134 | Vec3::new(1.0, 0.0, 0.0), 135 | Vec2::new(1.0, 1.0), 136 | ), 137 | Vertex::new( 138 | Vec3::new(0.5, 0.5, 0.5), 139 | Vec3::new(1.0, 0.0, 0.0), 140 | Vec2::new(1.0, 1.0), 141 | ), 142 | Vertex::new( 143 | Vec3::new(0.5, -0.5, 0.5), 144 | Vec3::new(1.0, 0.0, 0.0), 145 | Vec2::new(0.0, 1.0), 146 | ), 147 | Vertex::new( 148 | Vec3::new(0.5, -0.5, -0.5), 149 | Vec3::new(1.0, 0.0, 0.0), 150 | Vec2::new(0.0, 0.0), 151 | ), 152 | Vertex::new( 153 | Vec3::new(-0.5, -0.5, -0.5), 154 | Vec3::new(-1.0, 0.0, 0.0), 155 | Vec2::new(0.0, 0.0), 156 | ), 157 | Vertex::new( 158 | Vec3::new(-0.5, -0.5, 0.5), 159 | Vec3::new(-1.0, 0.0, 0.0), 160 | Vec2::new(1.0, 0.0), 161 | ), 162 | Vertex::new( 163 | Vec3::new(-0.5, 0.5, 0.5), 164 | Vec3::new(-1.0, 0.0, 0.0), 165 | Vec2::new(1.0, 1.0), 166 | ), 167 | Vertex::new( 168 | Vec3::new(-0.5, 0.5, 0.5), 169 | Vec3::new(-1.0, 0.0, 0.0), 170 | Vec2::new(1.0, 1.0), 171 | ), 172 | Vertex::new( 173 | Vec3::new(-0.5, 0.5, -0.5), 174 | Vec3::new(-1.0, 0.0, 0.0), 175 | Vec2::new(0.0, 1.0), 176 | ), 177 | Vertex::new( 178 | Vec3::new(-0.5, -0.5, -0.5), 179 | Vec3::new(-1.0, 0.0, 0.0), 180 | Vec2::new(0.0, 0.0), 181 | ), 182 | Vertex::new( 183 | Vec3::new(-0.5, 0.5, -0.5), 184 | Vec3::new(0.0, 1.0, 0.0), 185 | Vec2::new(0.0, 0.0), 186 | ), 187 | Vertex::new( 188 | Vec3::new(-0.5, 0.5, 0.5), 189 | Vec3::new(0.0, 1.0, 0.0), 190 | Vec2::new(1.0, 0.0), 191 | ), 192 | Vertex::new( 193 | Vec3::new(0.5, 0.5, 0.5), 194 | Vec3::new(0.0, 1.0, 0.0), 195 | Vec2::new(1.0, 1.0), 196 | ), 197 | Vertex::new( 198 | Vec3::new(0.5, 0.5, 0.5), 199 | Vec3::new(0.0, 1.0, 0.0), 200 | Vec2::new(1.0, 1.0), 201 | ), 202 | Vertex::new( 203 | Vec3::new(0.5, 0.5, -0.5), 204 | Vec3::new(0.0, 1.0, 0.0), 205 | Vec2::new(0.0, 1.0), 206 | ), 207 | Vertex::new( 208 | Vec3::new(-0.5, 0.5, -0.5), 209 | Vec3::new(0.0, 1.0, 0.0), 210 | Vec2::new(0.0, 0.0), 211 | ), 212 | Vertex::new( 213 | Vec3::new(-0.5, -0.5, -0.5), 214 | Vec3::new(0.0, -1.0, 0.0), 215 | Vec2::new(0.0, 0.0), 216 | ), 217 | Vertex::new( 218 | Vec3::new(0.5, -0.5, -0.5), 219 | Vec3::new(0.0, -1.0, 0.0), 220 | Vec2::new(1.0, 0.0), 221 | ), 222 | Vertex::new( 223 | Vec3::new(0.5, -0.5, 0.5), 224 | Vec3::new(0.0, -1.0, 0.0), 225 | Vec2::new(1.0, 1.0), 226 | ), 227 | Vertex::new( 228 | Vec3::new(0.5, -0.5, 0.5), 229 | Vec3::new(0.0, -1.0, 0.0), 230 | Vec2::new(1.0, 1.0), 231 | ), 232 | Vertex::new( 233 | Vec3::new(-0.5, -0.5, 0.5), 234 | Vec3::new(0.0, -1.0, 0.0), 235 | Vec2::new(0.0, 1.0), 236 | ), 237 | Vertex::new( 238 | Vec3::new(-0.5, -0.5, -0.5), 239 | Vec3::new(0.0, -1.0, 0.0), 240 | Vec2::new(0.0, 0.0), 241 | ), 242 | ]; 243 | 244 | fn main() { 245 | ctru::set_panic_hook(true); 246 | 247 | let gfx = Gfx::with_formats_shared( 248 | ctru::services::gspgpu::FramebufferFormat::Rgba8, 249 | ctru::services::gspgpu::FramebufferFormat::Rgba8, 250 | ) 251 | .expect("Couldn't obtain GFX controller"); 252 | let mut hid = Hid::new().expect("Couldn't obtain HID controller"); 253 | let apt = Apt::new().expect("Couldn't obtain APT controller"); 254 | 255 | let mut instance = citro3d::Instance::new().expect("failed to initialize Citro3D"); 256 | 257 | let top_screen = TopScreen3D::from(&gfx.top_screen); 258 | 259 | let (mut top_left, mut top_right) = top_screen.split_mut(); 260 | 261 | let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer(); 262 | let mut top_left_target = instance 263 | .render_target(width, height, top_left, Some(DepthFormat::Depth24Stencil8)) 264 | .expect("failed to create render target"); 265 | 266 | let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); 267 | let mut top_right_target = instance 268 | .render_target(width, height, top_right, Some(DepthFormat::Depth24Stencil8)) 269 | .expect("failed to create render target"); 270 | 271 | let mut bottom_screen = gfx.bottom_screen.borrow_mut(); 272 | let RawFrameBuffer { width, height, .. } = bottom_screen.raw_framebuffer(); 273 | 274 | let mut bottom_target = instance 275 | .render_target( 276 | width, 277 | height, 278 | bottom_screen, 279 | Some(DepthFormat::Depth24Stencil8), 280 | ) 281 | .expect("failed to create bottom screen render target"); 282 | 283 | let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap(); 284 | let vertex_shader = shader.get(0).unwrap(); 285 | 286 | let program = shader::Program::new(vertex_shader).unwrap(); 287 | 288 | let mut vbo_data = Vec::with_capacity_in(VERTICES.len(), ctru::linear::LinearAllocator); 289 | vbo_data.extend_from_slice(VERTICES); 290 | let mut buf_info = buffer::Info::new(); 291 | let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); 292 | 293 | // Setup the global lighting environment, using an exponential lookup-table. 294 | let mut light_env = LightEnv::new_pinned(); 295 | light_env.as_mut().connect_lut( 296 | LutId::D0, 297 | LutInput::LightNormal, 298 | Lut::from_fn(|v| v.powf(20.0), false), 299 | ); 300 | light_env.as_mut().set_material(Material { 301 | ambient: Some(Color::new(0.2, 0.2, 0.2)), 302 | diffuse: Some(Color::new(1.0, 0.4, 1.0)), 303 | specular0: Some(Color::new(0.8, 0.8, 0.8)), 304 | ..Default::default() 305 | }); 306 | 307 | // Create a new light instance. 308 | let light = light_env.as_mut().create_light().unwrap(); 309 | let mut light = light_env.as_mut().light_mut(light).unwrap(); 310 | light.as_mut().set_color(Color::new(1.0, 1.0, 1.0)); // White color 311 | light.as_mut().set_position(FVec3::new(0.0, 0.0, -1.0)); // Approximately emitting from the camera 312 | // Set how the light attenuates over distance. 313 | // This particular LUT is optimized to work between 0 and 10 units of distance from the light point. 314 | light 315 | .as_mut() 316 | .set_distance_attenutation(Some(DistanceAttenuation::new(0.0..10.0, |d| { 317 | (1.0 / (2.0 * PI * d * d)).min(1.0) 318 | }))); 319 | 320 | // Subtle spotlight pointed at the top of the cube. 321 | let light = light_env.as_mut().create_light().unwrap(); 322 | let mut light = light_env.as_mut().light_mut(light).unwrap(); 323 | light.as_mut().set_color(Color::new(0.5, 0.5, 0.5)); 324 | light 325 | .as_mut() 326 | .set_spotlight(Some(Spotlight::with_cutoff(PI / 8.0))); // Spotlight angle of PI/6 327 | light 328 | .as_mut() 329 | .set_spotlight_direction(FVec3::new(0.0, 0.4, -1.0)); // Slightly tilted upwards 330 | light 331 | .as_mut() 332 | .set_distance_attenutation(Some(DistanceAttenuation::new(0.0..10.0, |d| { 333 | (1.0 / (0.5 * PI * d * d)).min(1.0) // We use a less aggressive attenuation to highlight the spotlight 334 | }))); 335 | 336 | // Setup the rotating view of the cube 337 | let mut view = Matrix4::identity(); 338 | let model_idx = program.get_uniform("modelView").unwrap(); 339 | view.translate(0.0, 0.0, -2.0); 340 | 341 | let projection_uniform_idx = program.get_uniform("projection").unwrap(); 342 | 343 | while apt.main_loop() { 344 | hid.scan_input(); 345 | 346 | if hid.keys_down().contains(KeyPad::START) { 347 | break; 348 | } 349 | 350 | instance.render_frame_with(|mut pass| { 351 | fn cast_lifetime_to_closure<'pass, T>(x: T) -> T 352 | where 353 | T: Fn(&mut RenderPass<'pass>, &'pass mut Target<'_>, &Matrix4), 354 | { 355 | x 356 | } 357 | 358 | let render_to = cast_lifetime_to_closure(|pass, target, projection| { 359 | target.clear(ClearFlags::ALL, 0, 0); 360 | pass.select_render_target(target) 361 | .expect("failed to set render target"); 362 | 363 | pass.bind_vertex_uniform(projection_uniform_idx, projection); 364 | pass.bind_vertex_uniform(model_idx, view); 365 | 366 | pass.set_attr_info(&attr_info); 367 | 368 | pass.draw_arrays(buffer::Primitive::Triangles, vbo_data); 369 | }); 370 | 371 | pass.bind_program(&program); 372 | pass.bind_light_env(Some(light_env.as_mut())); 373 | 374 | let stage0 = texenv::Stage::new(0).unwrap(); 375 | pass.texenv(stage0) 376 | .src( 377 | texenv::Mode::BOTH, 378 | texenv::Source::FragmentPrimaryColor, 379 | Some(texenv::Source::FragmentSecondaryColor), 380 | None, 381 | ) 382 | .func(texenv::Mode::BOTH, texenv::CombineFunc::Add); 383 | 384 | let Projections { 385 | left_eye, 386 | right_eye, 387 | center, 388 | } = calculate_projections(); 389 | 390 | render_to(&mut pass, &mut top_left_target, &left_eye); 391 | render_to(&mut pass, &mut top_right_target, &right_eye); 392 | render_to(&mut pass, &mut bottom_target, ¢er); 393 | 394 | pass 395 | }); 396 | 397 | // Rotate the modelView 398 | view.translate(0.0, 0.0, 2.0); 399 | view.rotate_y(1.0f32.to_radians()); 400 | view.translate(0.0, 0.0, -2.0); 401 | } 402 | } 403 | 404 | fn prepare_vbos<'a>( 405 | buf_info: &'a mut buffer::Info, 406 | vbo_data: &'a [Vertex], 407 | ) -> (attrib::Info, buffer::Slice<'a>) { 408 | // Configure attributes for use with the vertex shader 409 | let mut attr_info = attrib::Info::new(); 410 | 411 | let reg0 = attrib::Register::new(0).unwrap(); 412 | let reg1 = attrib::Register::new(1).unwrap(); 413 | let reg2 = attrib::Register::new(2).unwrap(); 414 | 415 | attr_info 416 | .add_loader(reg0, attrib::Format::Float, 3) 417 | .unwrap(); 418 | 419 | attr_info 420 | .add_loader(reg1, attrib::Format::Float, 3) 421 | .unwrap(); 422 | 423 | attr_info 424 | .add_loader(reg2, attrib::Format::Float, 2) 425 | .unwrap(); 426 | 427 | let buf_idx = buf_info.add(vbo_data, &attr_info).unwrap(); 428 | 429 | (attr_info, buf_idx) 430 | } 431 | 432 | struct Projections { 433 | left_eye: Matrix4, 434 | right_eye: Matrix4, 435 | center: Matrix4, 436 | } 437 | 438 | fn calculate_projections() -> Projections { 439 | let slider_val = ctru::os::current_3d_slider_state(); 440 | let interocular_distance = slider_val / 2.0; 441 | 442 | let vertical_fov = 40.0_f32.to_radians(); 443 | let screen_depth = 2.0; 444 | 445 | let clip_planes = ClipPlanes { 446 | near: 0.01, 447 | far: 100.0, 448 | }; 449 | 450 | let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); 451 | 452 | let (left_eye, right_eye) = 453 | Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) 454 | .stereo_matrices(left, right); 455 | 456 | let center = 457 | Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); 458 | 459 | Projections { 460 | left_eye, 461 | right_eye, 462 | center, 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /citro3d/src/math/projection.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::ops::Range; 3 | 4 | use super::Matrix4; 5 | 6 | /// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection). 7 | /// See specific `Kind` implementations for constructors, e.g. 8 | /// [`Projection::perspective`] and [`Projection::orthographic`]. 9 | /// 10 | /// To use the resulting projection, convert it to a [`Matrix4`] with [`From`]/[`Into`]. 11 | #[derive(Clone, Debug)] 12 | pub struct Projection { 13 | coordinates: CoordinateOrientation, 14 | rotation: ScreenOrientation, 15 | inner: Kind, 16 | } 17 | 18 | impl Projection { 19 | fn new(inner: Kind) -> Self { 20 | Self { 21 | coordinates: CoordinateOrientation::default(), 22 | rotation: ScreenOrientation::default(), 23 | inner, 24 | } 25 | } 26 | 27 | /// Set the coordinate system's orientation for the projection. 28 | /// See [`CoordinateOrientation`] for more details. 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// # let _runner = test_runner::GdbRunner::default(); 34 | /// # use citro3d::math::{Projection, AspectRatio, CoordinateOrientation, Matrix4, ClipPlanes}; 35 | /// let clip_planes = ClipPlanes { 36 | /// near: 0.1, 37 | /// far: 100.0, 38 | /// }; 39 | /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes) 40 | /// .coordinates(CoordinateOrientation::LeftHanded) 41 | /// .into(); 42 | /// ``` 43 | pub fn coordinates(mut self, orientation: CoordinateOrientation) -> Self { 44 | self.coordinates = orientation; 45 | self 46 | } 47 | 48 | /// Set the screen rotation for the projection. 49 | /// See [`ScreenOrientation`] for more details. 50 | /// 51 | /// # Example 52 | /// 53 | /// ``` 54 | /// # let _runner = test_runner::GdbRunner::default(); 55 | /// # use citro3d::math::{Projection, AspectRatio, ScreenOrientation, Matrix4, ClipPlanes}; 56 | /// let clip_planes = ClipPlanes { 57 | /// near: 0.1, 58 | /// far: 100.0, 59 | /// }; 60 | /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes) 61 | /// .screen(ScreenOrientation::None) 62 | /// .into(); 63 | /// ``` 64 | pub fn screen(mut self, orientation: ScreenOrientation) -> Self { 65 | self.rotation = orientation; 66 | self 67 | } 68 | } 69 | 70 | /// See [`Projection::perspective`]. 71 | #[derive(Clone, Debug)] 72 | pub struct Perspective { 73 | vertical_fov_radians: f32, 74 | aspect_ratio: AspectRatio, 75 | clip_planes: ClipPlanes, 76 | stereo: Option, 77 | } 78 | 79 | impl Projection { 80 | /// Construct a projection matrix suitable for projecting 3D world space onto 81 | /// the 3DS screens. 82 | /// 83 | /// # Parameters 84 | /// 85 | /// * `vertical_fov`: the vertical field of view, measured in radians 86 | /// * `aspect_ratio`: the aspect ratio of the projection 87 | /// * `clip_planes`: the near and far clip planes of the view frustum. 88 | /// [`ClipPlanes`] are always defined by near and far values, regardless 89 | /// of the projection's [`CoordinateOrientation`]. 90 | /// 91 | /// # Examples 92 | /// 93 | /// ``` 94 | /// # use citro3d::math::*; 95 | /// # use std::f32::consts::PI; 96 | /// # 97 | /// # let _runner = test_runner::GdbRunner::default(); 98 | /// # 99 | /// let clip_planes = ClipPlanes { 100 | /// near: 0.01, 101 | /// far: 100.0, 102 | /// }; 103 | /// 104 | /// let bottom: Matrix4 = 105 | /// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into(); 106 | /// 107 | /// let top: Matrix4 = 108 | /// Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into(); 109 | /// ``` 110 | #[doc(alias = "Mtx_Persp")] 111 | #[doc(alias = "Mtx_PerspTilt")] 112 | pub fn perspective( 113 | vertical_fov_radians: f32, 114 | aspect_ratio: AspectRatio, 115 | clip_planes: ClipPlanes, 116 | ) -> Self { 117 | Self::new(Perspective { 118 | vertical_fov_radians, 119 | aspect_ratio, 120 | clip_planes, 121 | stereo: None, 122 | }) 123 | } 124 | 125 | /// Helper function to build both eyes' perspective projection matrices 126 | /// at once. See [`StereoDisplacement`] for details on how to configure 127 | /// stereoscopy. 128 | /// 129 | /// ``` 130 | /// # use std::f32::consts::PI; 131 | /// # use citro3d::math::*; 132 | /// # 133 | /// # let _runner = test_runner::GdbRunner::default(); 134 | /// # 135 | /// let (left, right) = StereoDisplacement::new(0.5, 2.0); 136 | /// let (left_eye, right_eye) = Projection::perspective( 137 | /// PI / 4.0, 138 | /// AspectRatio::TopScreen, 139 | /// ClipPlanes { 140 | /// near: 0.01, 141 | /// far: 100.0, 142 | /// }, 143 | /// ) 144 | /// .stereo_matrices(left, right); 145 | /// ``` 146 | #[doc(alias = "Mtx_PerspStereo")] 147 | #[doc(alias = "Mtx_PerspStereoTilt")] 148 | pub fn stereo_matrices( 149 | self, 150 | left_eye: StereoDisplacement, 151 | right_eye: StereoDisplacement, 152 | ) -> (Matrix4, Matrix4) { 153 | // TODO: we might be able to avoid this clone if there was a conversion 154 | // from &Self to Matrix4 instead of Self... but it's probably fine for now 155 | let left = self.clone().stereo(left_eye); 156 | let right = self.stereo(right_eye); 157 | // Also, we could consider just returning (Self, Self) here? idk 158 | (left.into(), right.into()) 159 | } 160 | 161 | fn stereo(mut self, displacement: StereoDisplacement) -> Self { 162 | self.inner.stereo = Some(displacement); 163 | self 164 | } 165 | } 166 | 167 | impl From> for Matrix4 { 168 | fn from(projection: Projection) -> Self { 169 | let Perspective { 170 | vertical_fov_radians, 171 | aspect_ratio, 172 | clip_planes, 173 | stereo, 174 | } = projection.inner; 175 | 176 | let mut result = MaybeUninit::uninit(); 177 | 178 | if let Some(stereo) = stereo { 179 | let make_mtx = match projection.rotation { 180 | ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspStereoTilt, 181 | ScreenOrientation::None => citro3d_sys::Mtx_PerspStereo, 182 | }; 183 | unsafe { 184 | make_mtx( 185 | result.as_mut_ptr(), 186 | vertical_fov_radians, 187 | aspect_ratio.into(), 188 | clip_planes.near, 189 | clip_planes.far, 190 | stereo.displacement, 191 | stereo.screen_depth, 192 | projection.coordinates.is_left_handed(), 193 | ); 194 | } 195 | } else { 196 | let make_mtx = match projection.rotation { 197 | ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspTilt, 198 | ScreenOrientation::None => citro3d_sys::Mtx_Persp, 199 | }; 200 | unsafe { 201 | make_mtx( 202 | result.as_mut_ptr(), 203 | vertical_fov_radians, 204 | aspect_ratio.into(), 205 | clip_planes.near, 206 | clip_planes.far, 207 | projection.coordinates.is_left_handed(), 208 | ); 209 | } 210 | } 211 | 212 | unsafe { Self::from_raw(result.assume_init()) } 213 | } 214 | } 215 | 216 | /// See [`Projection::orthographic`]. 217 | #[derive(Clone, Debug)] 218 | pub struct Orthographic { 219 | clip_planes_x: Range, 220 | clip_planes_y: Range, 221 | clip_planes_z: ClipPlanes, 222 | } 223 | 224 | impl Projection { 225 | /// Construct an orthographic projection. The X and Y clip planes are passed 226 | /// as ranges because their coordinates are always oriented the same way 227 | /// (+X right, +Y up). 228 | /// 229 | /// The Z [`ClipPlanes`], however, are always defined by 230 | /// near and far values, regardless of the projection's [`CoordinateOrientation`]. 231 | /// 232 | /// # Example 233 | /// 234 | /// ``` 235 | /// # let _runner = test_runner::GdbRunner::default(); 236 | /// # use citro3d::math::{Projection, ClipPlanes, Matrix4}; 237 | /// # 238 | /// let mtx: Matrix4 = Projection::orthographic( 239 | /// 0.0..240.0, 240 | /// 0.0..400.0, 241 | /// ClipPlanes { 242 | /// near: 0.0, 243 | /// far: 100.0, 244 | /// }, 245 | /// ) 246 | /// .into(); 247 | /// ``` 248 | #[doc(alias = "Mtx_Ortho")] 249 | #[doc(alias = "Mtx_OrthoTilt")] 250 | pub fn orthographic( 251 | clip_planes_x: Range, 252 | clip_planes_y: Range, 253 | clip_planes_z: ClipPlanes, 254 | ) -> Self { 255 | Self::new(Orthographic { 256 | clip_planes_x, 257 | clip_planes_y, 258 | clip_planes_z, 259 | }) 260 | } 261 | } 262 | 263 | impl From> for Matrix4 { 264 | fn from(projection: Projection) -> Self { 265 | let make_mtx = match projection.rotation { 266 | ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt, 267 | ScreenOrientation::None => citro3d_sys::Mtx_Ortho, 268 | }; 269 | 270 | let Orthographic { 271 | clip_planes_x, 272 | clip_planes_y, 273 | clip_planes_z, 274 | } = projection.inner; 275 | 276 | let mut out = MaybeUninit::uninit(); 277 | unsafe { 278 | make_mtx( 279 | out.as_mut_ptr(), 280 | clip_planes_x.start, 281 | clip_planes_x.end, 282 | clip_planes_y.start, 283 | clip_planes_y.end, 284 | clip_planes_z.near, 285 | clip_planes_z.far, 286 | projection.coordinates.is_left_handed(), 287 | ); 288 | Self::from_raw(out.assume_init()) 289 | } 290 | } 291 | } 292 | 293 | // region: Projection configuration 294 | 295 | /// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry)) 296 | /// (or "handedness") of the coordinate system. Coordinates are always +Y-up, 297 | /// +X-right. 298 | #[derive(Clone, Copy, Debug)] 299 | pub enum CoordinateOrientation { 300 | /// A left-handed coordinate system. +Z points into the screen. 301 | LeftHanded, 302 | /// A right-handed coordinate system. +Z points out of the screen. 303 | RightHanded, 304 | } 305 | 306 | impl CoordinateOrientation { 307 | pub(crate) fn is_left_handed(self) -> bool { 308 | matches!(self, Self::LeftHanded) 309 | } 310 | } 311 | 312 | impl Default for CoordinateOrientation { 313 | /// This is an opinionated default, but [`RightHanded`](Self::RightHanded) 314 | /// seems to be the preferred coordinate system for most 315 | /// [examples](https://github.com/devkitPro/3ds-examples) 316 | /// from upstream, and is also fairly common in other applications. 317 | fn default() -> Self { 318 | Self::RightHanded 319 | } 320 | } 321 | 322 | /// Whether to rotate a projection to account for the 3DS screen orientation. 323 | /// Both screens on the 3DS are oriented such that the "top-left" of the screen 324 | /// in framebuffer coordinates is the physical bottom-left of the screen 325 | /// (i.e. the "width" is smaller than the "height"). 326 | #[derive(Clone, Copy, Debug)] 327 | pub enum ScreenOrientation { 328 | /// Rotate 90° clockwise to account for the 3DS screen rotation. Most 329 | /// applications will use this variant. 330 | Rotated, 331 | /// Do not apply any extra rotation to the projection. 332 | None, 333 | } 334 | 335 | impl Default for ScreenOrientation { 336 | fn default() -> Self { 337 | Self::Rotated 338 | } 339 | } 340 | 341 | /// Configuration for calculating stereoscopic projections. 342 | // TODO: not totally happy with this name + API yet, but it works for now. 343 | #[derive(Clone, Copy, Debug)] 344 | pub struct StereoDisplacement { 345 | /// The horizontal offset of the eye from center. Negative values 346 | /// correspond to the left eye, and positive values to the right eye. 347 | pub displacement: f32, 348 | /// The position of the screen, which determines the focal length. Objects 349 | /// closer than this depth will appear to pop out of the screen, and objects 350 | /// further than this will appear inside the screen. 351 | pub screen_depth: f32, 352 | } 353 | 354 | impl StereoDisplacement { 355 | /// Construct displacement for the left and right eyes simulataneously. 356 | /// The given `interocular_distance` describes the distance between the two 357 | /// rendered "eyes". A negative value will be treated the same as a positive 358 | /// value of the same magnitude. 359 | /// 360 | /// See struct documentation for details about the 361 | /// [`screen_depth`](Self::screen_depth) parameter. 362 | pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) { 363 | let displacement = interocular_distance.abs() / 2.0; 364 | 365 | let left_eye = Self { 366 | displacement: -displacement, 367 | screen_depth, 368 | }; 369 | let right_eye = Self { 370 | displacement, 371 | screen_depth, 372 | }; 373 | 374 | (left_eye, right_eye) 375 | } 376 | } 377 | 378 | /// Configuration for the clipping planes of a projection. 379 | /// 380 | /// For [`Perspective`] projections, this is used for the near and far clip planes 381 | /// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum). 382 | /// 383 | /// For [`Orthographic`] projections, this is used for the Z clipping planes of 384 | /// the projection. 385 | /// 386 | /// Note that the `near` value should always be less than `far`, regardless of 387 | /// [`CoordinateOrientation`]. In other words, these values will be negated 388 | /// when used with a [`RightHanded`](CoordinateOrientation::RightHanded) 389 | /// orientation. 390 | #[derive(Clone, Copy, Debug)] 391 | pub struct ClipPlanes { 392 | /// The Z-depth of the near clip plane, usually close or equal to zero. 393 | pub near: f32, 394 | /// The Z-depth of the far clip plane, usually greater than zero. 395 | pub far: f32, 396 | } 397 | 398 | /// The aspect ratio of a projection plane. 399 | #[derive(Clone, Copy, Debug)] 400 | #[non_exhaustive] 401 | #[doc(alias = "C3D_AspectRatioTop")] 402 | #[doc(alias = "C3D_AspectRatioBot")] 403 | pub enum AspectRatio { 404 | /// The aspect ratio of the 3DS' top screen (per-eye). 405 | #[doc(alias = "C3D_AspectRatioTop")] 406 | TopScreen, 407 | /// The aspect ratio of the 3DS' bottom screen. 408 | #[doc(alias = "C3D_AspectRatioBot")] 409 | BottomScreen, 410 | /// A custom aspect ratio (should be calcualted as `width / height`). 411 | Other(f32), 412 | } 413 | 414 | impl From for f32 { 415 | fn from(ratio: AspectRatio) -> Self { 416 | match ratio { 417 | AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32, 418 | AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32, 419 | AspectRatio::Other(ratio) => ratio, 420 | } 421 | } 422 | } 423 | 424 | // endregion 425 | -------------------------------------------------------------------------------- /citro3d/src/render/effect.rs: -------------------------------------------------------------------------------- 1 | //! Render effects and behaviour used by the GPU. 2 | 3 | /// Test functions. 4 | #[repr(u8)] 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | #[doc(alias = "GPU_TESTFUNC")] 7 | pub enum TestFunction { 8 | /// Never pass. 9 | #[doc(alias = "GPU_NEVER")] 10 | Never = ctru_sys::GPU_NEVER, 11 | 12 | /// Always pass. 13 | #[doc(alias = "GPU_ALWAYS")] 14 | Always = ctru_sys::GPU_ALWAYS, 15 | 16 | /// Pass if equal. 17 | #[doc(alias = "GPU_EQUAL")] 18 | Equal = ctru_sys::GPU_EQUAL, 19 | 20 | /// Pass if not equal. 21 | #[doc(alias = "GPU_NOTEQUAL")] 22 | NotEqual = ctru_sys::GPU_NOTEQUAL, 23 | 24 | /// Pass if less than. 25 | #[doc(alias = "GPU_LESS")] 26 | Less = ctru_sys::GPU_LESS, 27 | 28 | /// Pass if less than or equal. 29 | #[doc(alias = "GPU_LEQUAL")] 30 | LessOrEqual = ctru_sys::GPU_LEQUAL, 31 | 32 | /// Pass if greater than. 33 | #[doc(alias = "GPU_GREATER")] 34 | Greater = ctru_sys::GPU_GREATER, 35 | 36 | /// Pass if greater than or equal. 37 | #[doc(alias = "GPU_GEQUAL")] 38 | GreaterOrEqual = ctru_sys::GPU_GEQUAL, 39 | } 40 | 41 | impl TryFrom for TestFunction { 42 | type Error = String; 43 | 44 | fn try_from(value: u8) -> Result { 45 | match value { 46 | ctru_sys::GPU_NEVER => Ok(Self::Never), 47 | ctru_sys::GPU_ALWAYS => Ok(Self::Always), 48 | ctru_sys::GPU_EQUAL => Ok(Self::Equal), 49 | ctru_sys::GPU_NOTEQUAL => Ok(Self::NotEqual), 50 | ctru_sys::GPU_LESS => Ok(Self::Less), 51 | ctru_sys::GPU_LEQUAL => Ok(Self::LessOrEqual), 52 | ctru_sys::GPU_GREATER => Ok(Self::Greater), 53 | ctru_sys::GPU_GEQUAL => Ok(Self::GreaterOrEqual), 54 | _ => Err("invalid value for TestFunction".to_string()), 55 | } 56 | } 57 | } 58 | /// Early depth test functions. 59 | #[repr(u8)] 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 61 | #[doc(alias = "GPU_EARLYDEPTHFUNC")] 62 | pub enum EarlyDepthFunction { 63 | /// Pass if greater than or equal. 64 | #[doc(alias = "GPU_EARLYDEPTH_GEQUAL")] 65 | GreaterOrEqual = ctru_sys::GPU_EARLYDEPTH_GEQUAL, 66 | 67 | /// Pass if greater than. 68 | #[doc(alias = "GPU_EARLYDEPTH_GREATER")] 69 | Greater = ctru_sys::GPU_EARLYDEPTH_GREATER, 70 | 71 | /// Pass if less than or equal. 72 | #[doc(alias = "GPU_EARLYDEPTH_LEQUAL")] 73 | LessOrEqual = ctru_sys::GPU_EARLYDEPTH_LEQUAL, 74 | 75 | /// Pass if less than. 76 | #[doc(alias = "GPU_EARLYDEPTH_LESS")] 77 | Less = ctru_sys::GPU_EARLYDEPTH_LESS, 78 | } 79 | 80 | impl TryFrom for EarlyDepthFunction { 81 | type Error = String; 82 | 83 | fn try_from(value: u8) -> Result { 84 | match value { 85 | ctru_sys::GPU_EARLYDEPTH_GEQUAL => Ok(Self::GreaterOrEqual), 86 | ctru_sys::GPU_EARLYDEPTH_GREATER => Ok(Self::Greater), 87 | ctru_sys::GPU_EARLYDEPTH_LEQUAL => Ok(Self::LessOrEqual), 88 | ctru_sys::GPU_EARLYDEPTH_LESS => Ok(Self::Less), 89 | _ => Err("invalid value for EarlyDepthFunction".to_string()), 90 | } 91 | } 92 | } 93 | 94 | /// Scissor test modes. 95 | #[repr(u8)] 96 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 97 | #[doc(alias = "GPU_SCISSORMODE")] 98 | pub enum ScissorMode { 99 | /// Disable. 100 | #[doc(alias = "GPU_SCISSOR_DISABLE")] 101 | Disable = ctru_sys::GPU_SCISSOR_DISABLE, 102 | 103 | /// Exclude pixels inside the scissor box. 104 | #[doc(alias = "GPU_SCISSOR_INVERT")] 105 | Invert = ctru_sys::GPU_SCISSOR_INVERT, 106 | 107 | /// Exclude pixels outside of the scissor box. 108 | #[doc(alias = "GPU_SCISSOR_NORMAL")] 109 | Normal = ctru_sys::GPU_SCISSOR_NORMAL, 110 | } 111 | 112 | impl TryFrom for ScissorMode { 113 | type Error = String; 114 | fn try_from(value: u8) -> Result { 115 | match value { 116 | ctru_sys::GPU_SCISSOR_DISABLE => Ok(Self::Disable), 117 | ctru_sys::GPU_SCISSOR_INVERT => Ok(Self::Invert), 118 | ctru_sys::GPU_SCISSOR_NORMAL => Ok(Self::Normal), 119 | _ => Err("invalid value for ScissorMode".to_string()), 120 | } 121 | } 122 | } 123 | 124 | /// Stencil operations. 125 | #[repr(u8)] 126 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 127 | #[doc(alias = "GPU_STENCILOP")] 128 | pub enum StencilOperation { 129 | /// Keep old value. (old_stencil) 130 | #[doc(alias = "GPU_STENCIL_KEEP")] 131 | Keep = ctru_sys::GPU_STENCIL_KEEP, 132 | 133 | /// Zero. (0) 134 | #[doc(alias = "GPU_STENCIL_ZERO")] 135 | Zero = ctru_sys::GPU_STENCIL_ZERO, 136 | 137 | /// Replace value. (ref) 138 | #[doc(alias = "GPU_STENCIL_REPLACE")] 139 | Replace = ctru_sys::GPU_STENCIL_REPLACE, 140 | 141 | /// Increment value. (old_stencil + 1 saturated to [0, 255]) 142 | #[doc(alias = "GPU_STENCIL_INCR")] 143 | Increment = ctru_sys::GPU_STENCIL_INCR, 144 | 145 | /// Decrement value. (old_stencil - 1 saturated to [0, 255]) 146 | #[doc(alias = "GPU_STENCIL_DECR")] 147 | Decrement = ctru_sys::GPU_STENCIL_DECR, 148 | 149 | /// Invert value. (~old_stencil) 150 | #[doc(alias = "GPU_STENCIL_INVERT")] 151 | Invert = ctru_sys::GPU_STENCIL_INVERT, 152 | 153 | /// Increment value. (old_stencil + 1) 154 | #[doc(alias = "GPU_STENCIL_INCR_WRAP")] 155 | IncrementWrap = ctru_sys::GPU_STENCIL_INCR_WRAP, 156 | 157 | /// Decrement value. (old_stencil - 1) 158 | #[doc(alias = "GPU_STENCIL_DECR_WRAP")] 159 | DecrementWrap = ctru_sys::GPU_STENCIL_DECR_WRAP, 160 | } 161 | 162 | impl TryFrom for StencilOperation { 163 | type Error = String; 164 | fn try_from(value: u8) -> Result { 165 | match value { 166 | ctru_sys::GPU_STENCIL_KEEP => Ok(Self::Keep), 167 | ctru_sys::GPU_STENCIL_ZERO => Ok(Self::Zero), 168 | ctru_sys::GPU_STENCIL_REPLACE => Ok(Self::Replace), 169 | ctru_sys::GPU_STENCIL_INCR => Ok(Self::Increment), 170 | ctru_sys::GPU_STENCIL_DECR => Ok(Self::Decrement), 171 | ctru_sys::GPU_STENCIL_INVERT => Ok(Self::Invert), 172 | ctru_sys::GPU_STENCIL_INCR_WRAP => Ok(Self::IncrementWrap), 173 | ctru_sys::GPU_STENCIL_DECR_WRAP => Ok(Self::DecrementWrap), 174 | _ => Err("invalid value for StencilOperation".to_string()), 175 | } 176 | } 177 | } 178 | 179 | /// Pixel write mask. 180 | #[repr(u8)] 181 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 182 | #[doc(alias = "GPU_WRITEMASK")] 183 | pub enum WriteMask { 184 | /// Write red. 185 | #[doc(alias = "GPU_WRITE_RED")] 186 | Red = ctru_sys::GPU_WRITE_RED, 187 | 188 | /// Write green. 189 | #[doc(alias = "GPU_WRITE_GREEN")] 190 | Green = ctru_sys::GPU_WRITE_GREEN, 191 | 192 | /// Write blue. 193 | #[doc(alias = "GPU_WRITE_BLUE")] 194 | Blue = ctru_sys::GPU_WRITE_BLUE, 195 | 196 | /// Write alpha. 197 | #[doc(alias = "GPU_WRITE_ALPHA")] 198 | Alpha = ctru_sys::GPU_WRITE_ALPHA, 199 | 200 | /// Write depth. 201 | #[doc(alias = "GPU_WRITE_DEPTH")] 202 | Depth = ctru_sys::GPU_WRITE_DEPTH, 203 | 204 | /// Write all color components. 205 | #[doc(alias = "GPU_WRITE_COLOR")] 206 | Color = ctru_sys::GPU_WRITE_COLOR, 207 | 208 | /// Write all components. 209 | #[doc(alias = "GPU_WRITE_ALL")] 210 | All = ctru_sys::GPU_WRITE_ALL, 211 | } 212 | 213 | impl TryFrom for WriteMask { 214 | type Error = String; 215 | fn try_from(value: u8) -> Result { 216 | match value { 217 | ctru_sys::GPU_WRITE_RED => Ok(Self::Red), 218 | ctru_sys::GPU_WRITE_GREEN => Ok(Self::Green), 219 | ctru_sys::GPU_WRITE_BLUE => Ok(Self::Blue), 220 | ctru_sys::GPU_WRITE_ALPHA => Ok(Self::Alpha), 221 | ctru_sys::GPU_WRITE_DEPTH => Ok(Self::Depth), 222 | ctru_sys::GPU_WRITE_COLOR => Ok(Self::Color), 223 | ctru_sys::GPU_WRITE_ALL => Ok(Self::All), 224 | _ => Err("invalid value for WriteMask".to_string()), 225 | } 226 | } 227 | } 228 | 229 | /// Blend modes. 230 | #[repr(u8)] 231 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 232 | #[doc(alias = "GPU_BLENDEQUATION")] 233 | pub enum BlendEquation { 234 | /// Add colors. 235 | #[doc(alias = "GPU_BLEND_ADD")] 236 | Add = ctru_sys::GPU_BLEND_ADD, 237 | 238 | /// Subtract colors. 239 | #[doc(alias = "GPU_BLEND_SUBTRACT")] 240 | Subtract = ctru_sys::GPU_BLEND_SUBTRACT, 241 | 242 | /// Reverse-subtract colors. 243 | #[doc(alias = "GPU_BLEND_REVERSE_SUBTRACT")] 244 | ReverseSubtract = ctru_sys::GPU_BLEND_REVERSE_SUBTRACT, 245 | 246 | /// Use the minimum color. 247 | #[doc(alias = "GPU_BLEND_MIN")] 248 | Min = ctru_sys::GPU_BLEND_MIN, 249 | 250 | /// Use the maximum color. 251 | #[doc(alias = "GPU_BLEND_MAX")] 252 | Max = ctru_sys::GPU_BLEND_MAX, 253 | } 254 | 255 | impl TryFrom for BlendEquation { 256 | type Error = String; 257 | fn try_from(value: u8) -> Result { 258 | match value { 259 | ctru_sys::GPU_BLEND_ADD => Ok(Self::Add), 260 | ctru_sys::GPU_BLEND_SUBTRACT => Ok(Self::Subtract), 261 | ctru_sys::GPU_BLEND_REVERSE_SUBTRACT => Ok(Self::ReverseSubtract), 262 | ctru_sys::GPU_BLEND_MIN => Ok(Self::Min), 263 | ctru_sys::GPU_BLEND_MAX => Ok(Self::Max), 264 | _ => Err("invalid value for BlendEquation".to_string()), 265 | } 266 | } 267 | } 268 | 269 | /// Blend factors. 270 | #[repr(u8)] 271 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 272 | #[doc(alias = "GPU_BLENDFACTOR")] 273 | pub enum BlendFactor { 274 | /// Zero. 275 | #[doc(alias = "GPU_ZERO")] 276 | Zero = ctru_sys::GPU_ZERO, 277 | 278 | /// One. 279 | #[doc(alias = "GPU_ONE")] 280 | One = ctru_sys::GPU_ONE, 281 | 282 | /// Source color. 283 | #[doc(alias = "GPU_SRC_COLOR")] 284 | SrcColor = ctru_sys::GPU_SRC_COLOR, 285 | 286 | /// Source color - 1. 287 | #[doc(alias = "GPU_ONE_MINUS_SRC_COLOR")] 288 | OneMinusSrcColor = ctru_sys::GPU_ONE_MINUS_SRC_COLOR, 289 | 290 | /// Destination color. 291 | #[doc(alias = "GPU_DST_COLOR")] 292 | DstColor = ctru_sys::GPU_DST_COLOR, 293 | 294 | /// Destination color - 1. 295 | #[doc(alias = "GPU_ONE_MINUS_DST_COLOR")] 296 | OneMinusDstColor = ctru_sys::GPU_ONE_MINUS_DST_COLOR, 297 | 298 | /// Source alpha. 299 | #[doc(alias = "GPU_SRC_ALPHA")] 300 | SrcAlpha = ctru_sys::GPU_SRC_ALPHA, 301 | 302 | /// Source alpha - 1. 303 | #[doc(alias = "GPU_ONE_MINUS_SRC_ALPHA")] 304 | OneMinusSrcAlpha = ctru_sys::GPU_ONE_MINUS_SRC_ALPHA, 305 | 306 | /// Destination alpha. 307 | #[doc(alias = "GPU_DST_ALPHA")] 308 | DstAlpha = ctru_sys::GPU_DST_ALPHA, 309 | 310 | /// Destination alpha - 1. 311 | #[doc(alias = "GPU_ONE_MINUS_DST_ALPHA")] 312 | OneMinusDstAlpha = ctru_sys::GPU_ONE_MINUS_DST_ALPHA, 313 | 314 | /// Constant color. 315 | #[doc(alias = "GPU_CONSTANT_COLOR")] 316 | ConstantColor = ctru_sys::GPU_CONSTANT_COLOR, 317 | 318 | /// Constant color - 1. 319 | #[doc(alias = "GPU_ONE_MINUS_CONSTANT_COLOR")] 320 | OneMinusConstantColor = ctru_sys::GPU_ONE_MINUS_CONSTANT_COLOR, 321 | 322 | /// Constant alpha. 323 | #[doc(alias = "GPU_CONSTANT_ALPHA")] 324 | ConstantAlpha = ctru_sys::GPU_CONSTANT_ALPHA, 325 | 326 | /// Constant alpha - 1. 327 | #[doc(alias = "GPU_ONE_MINUS_CONSTANT_ALPHA")] 328 | OneMinusConstantAlpha = ctru_sys::GPU_ONE_MINUS_CONSTANT_ALPHA, 329 | 330 | /// Saturated alpha. 331 | #[doc(alias = "GPU_SRC_ALPHA_SATURATE")] 332 | SrcAlphaSaturate = ctru_sys::GPU_SRC_ALPHA_SATURATE, 333 | } 334 | 335 | impl TryFrom for BlendFactor { 336 | type Error = String; 337 | fn try_from(value: u8) -> Result { 338 | match value { 339 | ctru_sys::GPU_ZERO => Ok(Self::Zero), 340 | ctru_sys::GPU_ONE => Ok(Self::One), 341 | ctru_sys::GPU_SRC_COLOR => Ok(Self::SrcColor), 342 | ctru_sys::GPU_ONE_MINUS_SRC_COLOR => Ok(Self::OneMinusSrcColor), 343 | ctru_sys::GPU_DST_COLOR => Ok(Self::DstColor), 344 | ctru_sys::GPU_ONE_MINUS_DST_COLOR => Ok(Self::OneMinusDstColor), 345 | ctru_sys::GPU_SRC_ALPHA => Ok(Self::SrcAlpha), 346 | ctru_sys::GPU_ONE_MINUS_SRC_ALPHA => Ok(Self::OneMinusSrcAlpha), 347 | ctru_sys::GPU_DST_ALPHA => Ok(Self::DstAlpha), 348 | ctru_sys::GPU_ONE_MINUS_DST_ALPHA => Ok(Self::OneMinusDstAlpha), 349 | ctru_sys::GPU_CONSTANT_COLOR => Ok(Self::ConstantColor), 350 | ctru_sys::GPU_ONE_MINUS_CONSTANT_COLOR => Ok(Self::OneMinusConstantColor), 351 | ctru_sys::GPU_CONSTANT_ALPHA => Ok(Self::ConstantAlpha), 352 | ctru_sys::GPU_ONE_MINUS_CONSTANT_ALPHA => Ok(Self::OneMinusConstantAlpha), 353 | ctru_sys::GPU_SRC_ALPHA_SATURATE => Ok(Self::SrcAlphaSaturate), 354 | _ => Err("invalid value for BlendFactor".to_string()), 355 | } 356 | } 357 | } 358 | 359 | /// Logical operations. 360 | #[repr(u8)] 361 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 362 | #[doc(alias = "GPU_LOGICOP")] 363 | pub enum LogicOperation { 364 | /// Clear. 365 | #[doc(alias = "GPU_LOGICOP_CLEAR")] 366 | Clear = ctru_sys::GPU_LOGICOP_CLEAR, 367 | 368 | /// Bitwise AND. 369 | #[doc(alias = "GPU_LOGICOP_AND")] 370 | And = ctru_sys::GPU_LOGICOP_AND, 371 | 372 | /// Reverse bitwise AND. 373 | #[doc(alias = "GPU_LOGICOP_AND_REVERSE")] 374 | AndReverse = ctru_sys::GPU_LOGICOP_AND_REVERSE, 375 | 376 | /// Copy. 377 | #[doc(alias = "GPU_LOGICOP_COPY")] 378 | Copy = ctru_sys::GPU_LOGICOP_COPY, 379 | 380 | /// Set. 381 | #[doc(alias = "GPU_LOGICOP_SET")] 382 | Set = ctru_sys::GPU_LOGICOP_SET, 383 | 384 | /// Inverted copy. 385 | #[doc(alias = "GPU_LOGICOP_COPY_INVERTED")] 386 | CopyInverted = ctru_sys::GPU_LOGICOP_COPY_INVERTED, 387 | 388 | /// No operation. 389 | #[doc(alias = "GPU_LOGICOP_NOOP")] 390 | Noop = ctru_sys::GPU_LOGICOP_NOOP, 391 | 392 | /// Invert. 393 | #[doc(alias = "GPU_LOGICOP_INVERT")] 394 | Invert = ctru_sys::GPU_LOGICOP_INVERT, 395 | 396 | /// Bitwise NAND. 397 | #[doc(alias = "GPU_LOGICOP_NAND")] 398 | Nand = ctru_sys::GPU_LOGICOP_NAND, 399 | 400 | /// Bitwise OR. 401 | #[doc(alias = "GPU_LOGICOP_OR")] 402 | Or = ctru_sys::GPU_LOGICOP_OR, 403 | 404 | /// Bitwise NOR. 405 | #[doc(alias = "GPU_LOGICOP_NOR")] 406 | Nor = ctru_sys::GPU_LOGICOP_NOR, 407 | 408 | /// Bitwise XOR. 409 | #[doc(alias = "GPU_LOGICOP_XOR")] 410 | Xor = ctru_sys::GPU_LOGICOP_XOR, 411 | 412 | /// Equivalent. 413 | #[doc(alias = "GPU_LOGICOP_EQUIV")] 414 | Equiv = ctru_sys::GPU_LOGICOP_EQUIV, 415 | 416 | /// Inverted bitwise AND. 417 | #[doc(alias = "GPU_LOGICOP_AND_INVERTED")] 418 | AndInverted = ctru_sys::GPU_LOGICOP_AND_INVERTED, 419 | 420 | /// Reverse bitwise OR. 421 | #[doc(alias = "GPU_LOGICOP_OR_REVERSE")] 422 | OrReverse = ctru_sys::GPU_LOGICOP_OR_REVERSE, 423 | 424 | /// Inverted bitwise OR. 425 | #[doc(alias = "GPU_LOGICOP_OR_INVERTED")] 426 | OrInverted = ctru_sys::GPU_LOGICOP_OR_INVERTED, 427 | } 428 | 429 | impl TryFrom for LogicOperation { 430 | type Error = String; 431 | fn try_from(value: u8) -> Result { 432 | match value { 433 | ctru_sys::GPU_LOGICOP_CLEAR => Ok(Self::Clear), 434 | ctru_sys::GPU_LOGICOP_AND => Ok(Self::And), 435 | ctru_sys::GPU_LOGICOP_AND_REVERSE => Ok(Self::AndReverse), 436 | ctru_sys::GPU_LOGICOP_COPY => Ok(Self::Copy), 437 | ctru_sys::GPU_LOGICOP_SET => Ok(Self::Set), 438 | ctru_sys::GPU_LOGICOP_COPY_INVERTED => Ok(Self::CopyInverted), 439 | ctru_sys::GPU_LOGICOP_NOOP => Ok(Self::Noop), 440 | ctru_sys::GPU_LOGICOP_INVERT => Ok(Self::Invert), 441 | ctru_sys::GPU_LOGICOP_NAND => Ok(Self::Nand), 442 | ctru_sys::GPU_LOGICOP_OR => Ok(Self::Or), 443 | ctru_sys::GPU_LOGICOP_NOR => Ok(Self::Nor), 444 | ctru_sys::GPU_LOGICOP_XOR => Ok(Self::Xor), 445 | ctru_sys::GPU_LOGICOP_EQUIV => Ok(Self::Equiv), 446 | ctru_sys::GPU_LOGICOP_AND_INVERTED => Ok(Self::AndInverted), 447 | ctru_sys::GPU_LOGICOP_OR_REVERSE => Ok(Self::OrReverse), 448 | ctru_sys::GPU_LOGICOP_OR_INVERTED => Ok(Self::OrInverted), 449 | _ => Err("invalid value for LogicOperation".to_string()), 450 | } 451 | } 452 | } 453 | 454 | /// Cull modes. 455 | #[repr(u8)] 456 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 457 | #[doc(alias = "GPU_CULLMODE")] 458 | pub enum CullMode { 459 | /// Disabled. 460 | #[doc(alias = "GPU_CULL_NONE")] 461 | None = ctru_sys::GPU_CULL_NONE, 462 | 463 | /// Front, counter-clockwise. 464 | #[doc(alias = "GPU_CULL_FRONT_CCW")] 465 | FrontCounterClockwise = ctru_sys::GPU_CULL_FRONT_CCW, 466 | 467 | /// Back, counter-clockwise. 468 | #[doc(alias = "GPU_CULL_BACK_CCW")] 469 | BackCounterClockwise = ctru_sys::GPU_CULL_BACK_CCW, 470 | } 471 | 472 | impl TryFrom for CullMode { 473 | type Error = String; 474 | fn try_from(value: u8) -> Result { 475 | match value { 476 | ctru_sys::GPU_CULL_NONE => Ok(Self::None), 477 | ctru_sys::GPU_CULL_FRONT_CCW => Ok(Self::FrontCounterClockwise), 478 | ctru_sys::GPU_CULL_BACK_CCW => Ok(Self::BackCounterClockwise), 479 | _ => Err("invalid value for CullMode".to_string()), 480 | } 481 | } 482 | } 483 | 484 | /// Fragment operation modes. 485 | #[repr(u8)] 486 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 487 | #[doc(alias = "GPU_FRAGOPMODE")] 488 | pub enum FragmentOperationMode { 489 | /// OpenGL mode. 490 | #[doc(alias = "GPU_FRAGOPMODE_GL")] 491 | Gl = ctru_sys::GPU_FRAGOPMODE_GL, 492 | 493 | /// Gas mode (?). 494 | #[doc(alias = "GPU_FRAGOPMODE_GAS_ACC")] 495 | GasAcc = ctru_sys::GPU_FRAGOPMODE_GAS_ACC, 496 | 497 | /// Shadow mode (?). 498 | #[doc(alias = "GPU_FRAGOPMODE_SHADOW")] 499 | Shadow = ctru_sys::GPU_FRAGOPMODE_SHADOW, 500 | } 501 | 502 | impl TryFrom for FragmentOperationMode { 503 | type Error = String; 504 | fn try_from(value: u8) -> Result { 505 | match value { 506 | ctru_sys::GPU_FRAGOPMODE_GL => Ok(Self::Gl), 507 | ctru_sys::GPU_FRAGOPMODE_GAS_ACC => Ok(Self::GasAcc), 508 | ctru_sys::GPU_FRAGOPMODE_SHADOW => Ok(Self::Shadow), 509 | _ => Err("invalid value for FragmentOperationMode".to_string()), 510 | } 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /citro3d/src/render.rs: -------------------------------------------------------------------------------- 1 | //! This module provides render target types and options for controlling transfer 2 | //! of data to the GPU, including the format of color and depth data to be rendered. 3 | 4 | use std::cell::{OnceCell, RefMut}; 5 | use std::marker::PhantomData; 6 | use std::pin::Pin; 7 | use std::rc::Rc; 8 | 9 | use citro3d_sys::{ 10 | C3D_DEPTHTYPE, C3D_RenderTarget, C3D_RenderTargetCreate, C3D_RenderTargetDelete, 11 | }; 12 | use ctru::services::gfx::Screen; 13 | use ctru::services::gspgpu::FramebufferFormat; 14 | use ctru_sys::{GPU_COLORBUF, GPU_DEPTHBUF}; 15 | 16 | use crate::{ 17 | Error, Instance, RenderQueue, Result, attrib, 18 | buffer::{self, Index, Indices}, 19 | light::LightEnv, 20 | shader, 21 | texenv::{self, TexEnv}, 22 | uniform::{self, Uniform}, 23 | }; 24 | 25 | pub mod effect; 26 | mod transfer; 27 | 28 | bitflags::bitflags! { 29 | /// Indicate whether color, depth buffer, or both values should be cleared. 30 | #[doc(alias = "C3D_ClearBits")] 31 | pub struct ClearFlags: u8 { 32 | /// Clear the color of the render target. 33 | const COLOR = citro3d_sys::C3D_CLEAR_COLOR; 34 | /// Clear the depth buffer value of the render target. 35 | const DEPTH = citro3d_sys::C3D_CLEAR_DEPTH; 36 | /// Clear both color and depth buffer values of the render target. 37 | const ALL = citro3d_sys::C3D_CLEAR_ALL; 38 | } 39 | } 40 | 41 | /// The color format to use when rendering on the GPU. 42 | #[repr(u8)] 43 | #[derive(Clone, Copy, Debug)] 44 | #[doc(alias = "GPU_COLORBUF")] 45 | pub enum ColorFormat { 46 | /// 8-bit Red + 8-bit Green + 8-bit Blue + 8-bit Alpha. 47 | RGBA8 = ctru_sys::GPU_RB_RGBA8, 48 | /// 8-bit Red + 8-bit Green + 8-bit Blue. 49 | RGB8 = ctru_sys::GPU_RB_RGB8, 50 | /// 5-bit Red + 5-bit Green + 5-bit Blue + 1-bit Alpha. 51 | RGBA5551 = ctru_sys::GPU_RB_RGBA5551, 52 | /// 5-bit Red + 6-bit Green + 5-bit Blue. 53 | RGB565 = ctru_sys::GPU_RB_RGB565, 54 | /// 4-bit Red + 4-bit Green + 4-bit Blue + 4-bit Alpha. 55 | RGBA4 = ctru_sys::GPU_RB_RGBA4, 56 | } 57 | 58 | /// The depth buffer format to use when rendering. 59 | #[repr(u8)] 60 | #[derive(Clone, Copy, Debug)] 61 | #[doc(alias = "GPU_DEPTHBUF")] 62 | #[doc(alias = "C3D_DEPTHTYPE")] 63 | pub enum DepthFormat { 64 | /// 16-bit depth. 65 | Depth16 = ctru_sys::GPU_RB_DEPTH16, 66 | /// 24-bit depth. 67 | Depth24 = ctru_sys::GPU_RB_DEPTH24, 68 | /// 24-bit depth + 8-bit Stencil. 69 | Depth24Stencil8 = ctru_sys::GPU_RB_DEPTH24_STENCIL8, 70 | } 71 | 72 | /// A render target for `citro3d`. Frame data will be written to this target 73 | /// to be rendered on the GPU and displayed on the screen. 74 | #[doc(alias = "C3D_RenderTarget")] 75 | pub struct Target<'screen> { 76 | raw: *mut citro3d_sys::C3D_RenderTarget, 77 | // This is unused after construction, but ensures unique access to the 78 | // screen this target writes to during rendering 79 | _screen: RefMut<'screen, dyn Screen>, 80 | _queue: Rc, 81 | } 82 | 83 | struct Frame; 84 | 85 | #[non_exhaustive] 86 | #[must_use] 87 | pub struct RenderPass<'pass> { 88 | texenvs: [OnceCell; texenv::TEXENV_COUNT], 89 | _active_frame: Frame, 90 | 91 | // It is not valid behaviour to bind anything but a correct shader program. 92 | // Instead of binding NULL, we simply force the user to have a shader program bound again 93 | // before any draw calls. 94 | is_program_bound: bool, 95 | 96 | _phantom: PhantomData<&'pass mut Instance>, 97 | } 98 | 99 | impl<'pass> RenderPass<'pass> { 100 | pub(crate) fn new(_instance: &'pass mut Instance) -> Self { 101 | Self { 102 | texenvs: [ 103 | // thank goodness there's only six of them! 104 | OnceCell::new(), 105 | OnceCell::new(), 106 | OnceCell::new(), 107 | OnceCell::new(), 108 | OnceCell::new(), 109 | OnceCell::new(), 110 | ], 111 | _active_frame: Frame::new(), 112 | is_program_bound: false, 113 | _phantom: PhantomData, 114 | } 115 | } 116 | 117 | /// Select the given render target for the following draw calls. 118 | /// 119 | /// # Errors 120 | /// 121 | /// Fails if the given target cannot be used for drawing. 122 | #[doc(alias = "C3D_FrameDrawOn")] 123 | pub fn select_render_target(&mut self, target: &'pass Target<'_>) -> Result<()> { 124 | let _ = self; 125 | if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } { 126 | Ok(()) 127 | } else { 128 | Err(Error::InvalidRenderTarget) 129 | } 130 | } 131 | 132 | /// Get the buffer info being used, if it exists. 133 | /// 134 | /// # Notes 135 | /// 136 | /// The resulting [`buffer::Info`] is copied (and not taken) from the one currently in use. 137 | #[doc(alias = "C3D_GetBufInfo")] 138 | pub fn buffer_info(&self) -> Option { 139 | let raw = unsafe { citro3d_sys::C3D_GetBufInfo() }; 140 | buffer::Info::copy_from(raw) 141 | } 142 | 143 | /// Set the buffer info to use for for the following draw calls. 144 | #[doc(alias = "C3D_SetBufInfo")] 145 | pub fn set_buffer_info(&mut self, buffer_info: &buffer::Info) { 146 | let raw: *const _ = &buffer_info.0; 147 | // LIFETIME SAFETY: C3D_SetBufInfo actually copies the pointee instead of mutating it. 148 | unsafe { citro3d_sys::C3D_SetBufInfo(raw.cast_mut()) }; 149 | } 150 | 151 | /// Get the attribute info being used, if it exists. 152 | /// 153 | /// # Notes 154 | /// 155 | /// The resulting [`attrib::Info`] is copied (and not taken) from the one currently in use. 156 | #[doc(alias = "C3D_GetAttrInfo")] 157 | pub fn attr_info(&self) -> Option { 158 | let raw = unsafe { citro3d_sys::C3D_GetAttrInfo() }; 159 | attrib::Info::copy_from(raw) 160 | } 161 | 162 | /// Set the attribute info to use for any following draw calls. 163 | #[doc(alias = "C3D_SetAttrInfo")] 164 | pub fn set_attr_info(&mut self, attr_info: &attrib::Info) { 165 | let raw: *const _ = &attr_info.0; 166 | // LIFETIME SAFETY: C3D_SetAttrInfo actually copies the pointee instead of mutating it. 167 | unsafe { citro3d_sys::C3D_SetAttrInfo(raw.cast_mut()) }; 168 | } 169 | 170 | /// Render primitives from the current vertex array buffer. 171 | /// 172 | /// # Panics 173 | /// 174 | /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). 175 | #[doc(alias = "C3D_DrawArrays")] 176 | pub fn draw_arrays(&mut self, primitive: buffer::Primitive, vbo_data: buffer::Slice<'pass>) { 177 | // TODO: Decide whether it's worth returning an `Error` instead of panicking. 178 | if !self.is_program_bound { 179 | panic!("tried todraw arrays when no shader program is bound"); 180 | } 181 | 182 | self.set_buffer_info(vbo_data.info()); 183 | 184 | // TODO: should we also require the attrib info directly here? 185 | unsafe { 186 | citro3d_sys::C3D_DrawArrays( 187 | primitive as ctru_sys::GPU_Primitive_t, 188 | vbo_data.index(), 189 | vbo_data.len(), 190 | ); 191 | } 192 | } 193 | 194 | /// Draws the vertices in `buf` indexed by `indices`. 195 | /// 196 | /// # Panics 197 | /// 198 | /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). 199 | #[doc(alias = "C3D_DrawElements")] 200 | pub fn draw_elements( 201 | &mut self, 202 | primitive: buffer::Primitive, 203 | vbo_data: buffer::Slice<'pass>, 204 | indices: &Indices<'pass, I>, 205 | ) { 206 | if !self.is_program_bound { 207 | panic!("tried to draw elements when no shader program is bound"); 208 | } 209 | 210 | self.set_buffer_info(vbo_data.info()); 211 | 212 | let indices = &indices.buffer; 213 | let elements = indices.as_ptr().cast(); 214 | 215 | unsafe { 216 | citro3d_sys::C3D_DrawElements( 217 | primitive as ctru_sys::GPU_Primitive_t, 218 | indices.len().try_into().unwrap(), 219 | // flag bit for short or byte 220 | I::TYPE, 221 | elements, 222 | ); 223 | } 224 | } 225 | 226 | /// Use the given [`shader::Program`] for the following draw calls. 227 | pub fn bind_program(&mut self, program: &'pass shader::Program) { 228 | // SAFETY: AFAICT C3D_BindProgram just copies pointers from the given program, 229 | // instead of mutating the pointee in any way that would cause UB 230 | unsafe { 231 | citro3d_sys::C3D_BindProgram(program.as_raw().cast_mut()); 232 | } 233 | 234 | self.is_program_bound = true; 235 | } 236 | 237 | /// Binds a [`LightEnv`] for the following draw calls. 238 | pub fn bind_light_env(&mut self, env: Option>) { 239 | unsafe { 240 | citro3d_sys::C3D_LightEnvBind(env.map_or(std::ptr::null_mut(), |env| env.as_raw_mut())); 241 | } 242 | } 243 | 244 | /// Bind a uniform to the given `index` in the vertex shader for the next draw call. 245 | /// 246 | /// # Panics 247 | /// 248 | /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). 249 | /// 250 | /// # Example 251 | /// 252 | /// ``` 253 | /// # let _runner = test_runner::GdbRunner::default(); 254 | /// # use citro3d::uniform; 255 | /// # use citro3d::math::Matrix4; 256 | /// # 257 | /// # let mut instance = citro3d::Instance::new().unwrap(); 258 | /// let idx = uniform::Index::from(0); 259 | /// let mtx = Matrix4::identity(); 260 | /// instance.bind_vertex_uniform(idx, &mtx); 261 | /// ``` 262 | pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Into) { 263 | if !self.is_program_bound { 264 | panic!("tried to bind vertex uniform when no shader program is bound"); 265 | } 266 | 267 | // LIFETIME SAFETY: Uniform data is copied into global buffers. 268 | uniform.into().bind(self, shader::Type::Vertex, index); 269 | } 270 | 271 | /// Bind a uniform to the given `index` in the geometry shader for the next draw call. 272 | /// 273 | /// # Panics 274 | /// 275 | /// Panics if no shader program was bound (see [`RenderPass::bind_program`]). 276 | /// 277 | /// # Example 278 | /// 279 | /// ``` 280 | /// # let _runner = test_runner::GdbRunner::default(); 281 | /// # use citro3d::uniform; 282 | /// # use citro3d::math::Matrix4; 283 | /// # 284 | /// # let mut instance = citro3d::Instance::new().unwrap(); 285 | /// let idx = uniform::Index::from(0); 286 | /// let mtx = Matrix4::identity(); 287 | /// instance.bind_geometry_uniform(idx, &mtx); 288 | /// ``` 289 | pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Into) { 290 | if !self.is_program_bound { 291 | panic!("tried to bind geometry uniform when no shader program is bound"); 292 | } 293 | 294 | // LIFETIME SAFETY: Uniform data is copied into global buffers. 295 | uniform.into().bind(self, shader::Type::Geometry, index); 296 | } 297 | 298 | /// Retrieve the [`TexEnv`] for the given stage, initializing it first if necessary. 299 | /// 300 | /// # Example 301 | /// 302 | /// ``` 303 | /// # use citro3d::texenv; 304 | /// # let _runner = test_runner::GdbRunner::default(); 305 | /// # let mut instance = citro3d::Instance::new().unwrap(); 306 | /// let stage0 = texenv::Stage::new(0).unwrap(); 307 | /// let texenv0 = instance.texenv(stage0); 308 | /// ``` 309 | #[doc(alias = "C3D_GetTexEnv")] 310 | #[doc(alias = "C3D_TexEnvInit")] 311 | pub fn texenv(&mut self, stage: texenv::Stage) -> &mut texenv::TexEnv { 312 | let texenv = &mut self.texenvs[stage.0]; 313 | texenv.get_or_init(|| TexEnv::new(stage)); 314 | // We have to do this weird unwrap to get a mutable reference, 315 | // since there is no `get_mut_or_init` or equivalent 316 | texenv.get_mut().unwrap() 317 | } 318 | } 319 | 320 | impl<'screen> Target<'screen> { 321 | /// Create a new render target with the given parameters. This takes a 322 | /// [`RenderQueue`] parameter to make sure this [`Target`] doesn't outlive 323 | /// the render queue. 324 | pub(crate) fn new( 325 | width: usize, 326 | height: usize, 327 | screen: RefMut<'screen, dyn Screen>, 328 | depth_format: Option, 329 | queue: Rc, 330 | ) -> Result { 331 | let color_format: ColorFormat = screen.framebuffer_format().into(); 332 | 333 | let raw = unsafe { 334 | C3D_RenderTargetCreate( 335 | width.try_into()?, 336 | height.try_into()?, 337 | color_format as GPU_COLORBUF, 338 | depth_format.map_or(C3D_DEPTHTYPE { __i: -1 }, DepthFormat::as_raw), 339 | ) 340 | }; 341 | 342 | if raw.is_null() { 343 | return Err(Error::FailedToInitialize); 344 | } 345 | 346 | // Set the render target to actually output to the given screen 347 | let flags = transfer::Flags::default() 348 | .in_format(color_format.into()) 349 | .out_format(color_format.into()); 350 | 351 | unsafe { 352 | citro3d_sys::C3D_RenderTargetSetOutput( 353 | raw, 354 | screen.as_raw(), 355 | screen.side().into(), 356 | flags.bits(), 357 | ); 358 | } 359 | 360 | Ok(Self { 361 | raw, 362 | _screen: screen, 363 | _queue: queue, 364 | }) 365 | } 366 | 367 | /// Clear the render target with the given 32-bit RGBA color and depth buffer value. 368 | /// 369 | /// Use `flags` to specify whether color and/or depth should be overwritten. 370 | #[doc(alias = "C3D_RenderTargetClear")] 371 | pub fn clear(&mut self, flags: ClearFlags, rgba_color: u32, depth: u32) { 372 | unsafe { 373 | citro3d_sys::C3D_RenderTargetClear(self.raw, flags.bits(), rgba_color, depth); 374 | } 375 | } 376 | 377 | /// Return the underlying `citro3d` render target for this target. 378 | pub(crate) fn as_raw(&self) -> *mut C3D_RenderTarget { 379 | self.raw 380 | } 381 | } 382 | 383 | impl Frame { 384 | fn new() -> Self { 385 | unsafe { 386 | citro3d_sys::C3D_FrameBegin( 387 | // TODO: begin + end flags should be configurable 388 | citro3d_sys::C3D_FRAME_SYNCDRAW, 389 | ) 390 | }; 391 | 392 | Self {} 393 | } 394 | } 395 | 396 | impl Drop for Frame { 397 | fn drop(&mut self) { 398 | unsafe { 399 | citro3d_sys::C3D_FrameEnd(0); 400 | } 401 | } 402 | } 403 | 404 | impl Drop for Target<'_> { 405 | #[doc(alias = "C3D_RenderTargetDelete")] 406 | fn drop(&mut self) { 407 | unsafe { 408 | C3D_RenderTargetDelete(self.raw); 409 | } 410 | } 411 | } 412 | 413 | impl From for ColorFormat { 414 | fn from(format: FramebufferFormat) -> Self { 415 | match format { 416 | FramebufferFormat::Rgba8 => Self::RGBA8, 417 | FramebufferFormat::Rgb565 => Self::RGB565, 418 | FramebufferFormat::Rgb5A1 => Self::RGBA5551, 419 | FramebufferFormat::Rgba4 => Self::RGBA4, 420 | // this one seems unusual, but it appears to work fine: 421 | FramebufferFormat::Bgr8 => Self::RGB8, 422 | } 423 | } 424 | } 425 | 426 | impl DepthFormat { 427 | fn as_raw(self) -> C3D_DEPTHTYPE { 428 | C3D_DEPTHTYPE { 429 | __e: self as GPU_DEPTHBUF, 430 | } 431 | } 432 | } 433 | 434 | impl Drop for RenderPass<'_> { 435 | fn drop(&mut self) { 436 | unsafe { 437 | // TODO: substitute as many as possible with safe wrappers. 438 | // These resets are derived from the implementation of `C3D_Init` and by studying the `C3D_Context` struct. 439 | citro3d_sys::C3D_DepthMap(true, -1.0, 0.0); 440 | citro3d_sys::C3D_CullFace(ctru_sys::GPU_CULL_BACK_CCW); 441 | citro3d_sys::C3D_StencilTest(false, ctru_sys::GPU_ALWAYS, 0x00, 0xFF, 0x00); 442 | citro3d_sys::C3D_StencilOp( 443 | ctru_sys::GPU_STENCIL_KEEP, 444 | ctru_sys::GPU_STENCIL_KEEP, 445 | ctru_sys::GPU_STENCIL_KEEP, 446 | ); 447 | citro3d_sys::C3D_BlendingColor(0); 448 | citro3d_sys::C3D_EarlyDepthTest(false, ctru_sys::GPU_EARLYDEPTH_GREATER, 0); 449 | citro3d_sys::C3D_DepthTest(true, ctru_sys::GPU_GREATER, ctru_sys::GPU_WRITE_ALL); 450 | citro3d_sys::C3D_AlphaTest(false, ctru_sys::GPU_ALWAYS, 0x00); 451 | citro3d_sys::C3D_AlphaBlend( 452 | ctru_sys::GPU_BLEND_ADD, 453 | ctru_sys::GPU_BLEND_ADD, 454 | ctru_sys::GPU_SRC_ALPHA, 455 | ctru_sys::GPU_ONE_MINUS_SRC_ALPHA, 456 | ctru_sys::GPU_SRC_ALPHA, 457 | ctru_sys::GPU_ONE_MINUS_SRC_ALPHA, 458 | ); 459 | citro3d_sys::C3D_FragOpMode(ctru_sys::GPU_FRAGOPMODE_GL); 460 | citro3d_sys::C3D_FragOpShadow(0.0, 1.0); 461 | 462 | // The texCoordId has no importance since we are binding NULL 463 | citro3d_sys::C3D_ProcTexBind(0, std::ptr::null_mut()); 464 | 465 | // ctx->texConfig = BIT(12); I have not found a way to replicate this one yet (maybe not necessary because of texenv's unbinding). 466 | 467 | // ctx->texShadow = BIT(0); 468 | citro3d_sys::C3D_TexShadowParams(true, 0.0); 469 | 470 | // ctx->texEnvBuf = 0; I have not found a way to replicate this one yet (maybe not necessary because of texenv's unbinding). 471 | 472 | // ctx->texEnvBufClr = 0xFFFFFFFF; 473 | citro3d_sys::C3D_TexEnvBufColor(0xFFFFFFFF); 474 | // ctx->fogClr = 0; 475 | citro3d_sys::C3D_FogColor(0); 476 | //ctx->fogLut = NULL; 477 | citro3d_sys::C3D_FogLutBind(std::ptr::null_mut()); 478 | 479 | // We don't need to unbind programs (and in citro3D you can't), 480 | // since the user is forced to bind them again before drawing next time they render. 481 | 482 | self.bind_light_env(None); 483 | 484 | // TODO: C3D_TexBind doesn't work for NULL 485 | // https://github.com/devkitPro/citro3d/blob/9f21cf7b380ce6f9e01a0420f19f0763e5443ca7/source/texture.c#L222 486 | /*for i in 0..3 { 487 | citro3d_sys::C3D_TexBind(i, std::ptr::null_mut()); 488 | }*/ 489 | 490 | for i in 0..6 { 491 | self.texenv(texenv::Stage::new(i).unwrap()).reset(); 492 | } 493 | 494 | // Unbind attribute information (can't use NULL pointer, so we use an empty attrib::Info instead). 495 | // 496 | // 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). 497 | // Is it worth keeping? Could hanging be considered better than an ARM exception? 498 | let empty_info = attrib::Info::default(); 499 | self.set_attr_info(&empty_info); 500 | 501 | // ctx->fixedAttribDirty = 0; 502 | // ctx->fixedAttribEverDirty = 0; 503 | for i in 0..12 { 504 | let vec = citro3d_sys::C3D_FixedAttribGetWritePtr(i); 505 | (*vec).c = [0.0, 0.0, 0.0, 0.0]; 506 | } 507 | } 508 | } 509 | } 510 | --------------------------------------------------------------------------------