diff --git a/.gitignore b/.gitignore index f13002d..a22ed31 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,9 @@ rust-toolchain.toml # Pica200 output files *.shbin + +# Various dev tools +.idea +.bacon-locations +bacon.toml +justfile \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 31f011c..84c3343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,11 @@ [workspace] -members = [ - "citro3d", - "citro3d-sys", - "citro3d-macros", -] +members = ["citro3d", "citro3d-sys", "citro3d-macros", "citro2d-sys", "citro2d"] default-members = [ "citro3d", "citro3d-sys", "citro3d-macros", + "citro2d-sys", + "citro2d", ] resolver = "2" @@ -15,3 +13,5 @@ resolver = "2" citro3d = { path = "citro3d" } citro3d-sys = { path = "citro3d-sys" } citro3d-macros = { path = "citro3d-macros" } +citro2d-sys = { path = "citro2d-sys" } +citro2d = { path = "citro2d" } diff --git a/README.md b/README.md index 5a5bf5a..bcd5b06 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ⚠️ WIP ⚠️ Rust bindings and safe wrapper to the [citro3d](https://github.com/devkitPro/citro3d) -library, to write homebrew graphical programs for the Nintendo 3DS. +and [citro2d](https://github.com/devkitPro/citro2d) library, to write homebrew graphical programs for the Nintendo 3DS. ## Crates @@ -15,5 +15,5 @@ library, to write homebrew graphical programs for the Nintendo 3DS. ## License -* `citro3d-sys` is licensed under Zlib +* `citro3d-sys` and `citro2d-sys` is licensed under Zlib * `citro3d` and `citro3d-macros` are dual-licensed under MIT or Apache-2.0 diff --git a/citro2d-sys/Cargo.toml b/citro2d-sys/Cargo.toml new file mode 100644 index 0000000..198fb37 --- /dev/null +++ b/citro2d-sys/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "citro2d-sys" +version = "0.1.0" +authors = ["Rust3DS Org", "Bailey Townsend"] +edition = "2021" +license = "Zlib" +links = "citro2d" + +[dependencies] +libc = "0.2.116" +ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } +citro3d-sys = { path = "../citro3d-sys" } + +[build-dependencies] +bindgen = { version = "0.68.1", features = ["experimental"] } +cc = "1.0.83" +doxygen-rs = "0.4.2" + +[dev-dependencies] +shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } diff --git a/citro2d-sys/LICENSE b/citro2d-sys/LICENSE new file mode 100644 index 0000000..da0cd15 --- /dev/null +++ b/citro2d-sys/LICENSE @@ -0,0 +1,18 @@ +As with the original citro3d, this library is licensed under zlib. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any + damages arising from the use of this software. + + Permission is granted to anyone to use this software for any + purpose, including commercial applications, and to alter it and + redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you use + this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + 3. This notice may not be removed or altered from any source + distribution. \ No newline at end of file diff --git a/citro2d-sys/README.md b/citro2d-sys/README.md new file mode 100644 index 0000000..a583c03 --- /dev/null +++ b/citro2d-sys/README.md @@ -0,0 +1,10 @@ +# citro2d-sys + +Rust bindings to [`citro2d`](https://github.com/devkitPro/citro2d). +Bindings are generated at build time using the locally-installed devkitPro. + +[Documentation](https://rust3ds.github.io/citro3d-rs/crates/citro2d_sys) is generated from the +`main` branch, and should generally be up to date with the latest devkitPro. +This will be more useful than [docs.rs](https://docs.rs/crates/citro2d), since +the bindings are generated at build time and `docs.rs`' build environment does not +have a copy of devkitPro to generate bindings from. diff --git a/citro2d-sys/build.rs b/citro2d-sys/build.rs new file mode 100644 index 0000000..9d03fc4 --- /dev/null +++ b/citro2d-sys/build.rs @@ -0,0 +1,167 @@ +//! This build script generates bindings from `citro2d` on the fly at compilation +//! time into `OUT_DIR`, from which they can be included into `lib.rs`. + +use std::env; +use std::iter::FromIterator; +use std::path::{Path, PathBuf}; + +use bindgen::callbacks::{DeriveTrait, ImplementsTrait, ParseCallbacks}; +use bindgen::{Builder, RustTarget}; + +fn main() { + let devkitpro = env::var("DEVKITPRO").expect("DEVKITPRO not set in environment"); + println!("cargo:rerun-if-env-changed=DEVKITPRO"); + + let devkitarm = std::env::var("DEVKITARM").expect("DEVKITARM not set in environment"); + println!("cargo:rerun-if-env-changed=DEVKITARM"); + + let debug_symbols = env::var("DEBUG").unwrap(); + println!("cargo:rerun-if-env-changed=DEBUG"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + println!("cargo:rerun-if-env-changed=OUT_DIR"); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rustc-link-search=native={devkitpro}/libctru/lib"); + println!( + "cargo:rustc-link-lib=static={}", + match debug_symbols.as_str() { + // Based on valid values described in + // https://doc.rust-lang.org/cargo/reference/profiles.html#debug + "0" | "false" | "none" => "citro2d", + _ => "citro2dd", + } + ); + + println!( + "cargo:rustc-link-lib=static={}", + match debug_symbols.as_str() { + // Based on valid values described in + // https://doc.rust-lang.org/cargo/reference/profiles.html#debug + "0" | "false" | "none" => "citro3d", + _ => "citro3dd", + } + ); + + let include_path = PathBuf::from_iter([devkitpro.as_str(), "libctru", "include"]); + let citro2d_h = include_path.join("citro2d.h"); + let three_ds_h = include_path.join("3ds.h"); + + let sysroot = Path::new(devkitarm.as_str()).join("arm-none-eabi"); + let system_include = sysroot.join("include"); + let static_fns_path = Path::new("citro2d_statics_wrapper"); + + let gcc_dir = PathBuf::from_iter([devkitarm.as_str(), "lib", "gcc", "arm-none-eabi"]); + + let gcc_include = gcc_dir + .read_dir() + .unwrap() + // Assuming that there is only one gcc version of libs under the devkitARM dir + .next() + .unwrap() + .unwrap() + .path() + .join("include"); + + let bindings = Builder::default() + .header(three_ds_h.to_str().unwrap()) + .header(citro2d_h.to_str().unwrap()) + .rust_target(RustTarget::Nightly) + .use_core() + .trust_clang_mangling(false) + .layout_tests(false) + .ctypes_prefix("::libc") + .prepend_enum_name(false) + .fit_macro_constants(true) + .raw_line("use ctru_sys::*;") + .raw_line("use libc::FILE;") + .must_use_type("Result") + .blocklist_type("u(8|16|32|64)") + .blocklist_type("FILE") + .opaque_type("(GPU|GFX)_.*") + .opaque_type("float24Uniform_s") + .allowlist_file(".*/c2d/.*[.]h") + .blocklist_file(".*/3ds/.*[.]h") + .blocklist_file(".*/sys/.*[.]h") + .wrap_static_fns(true) + .wrap_static_fns_path(out_dir.join(static_fns_path)) + .clang_args([ + "--target=arm-none-eabi", + "--sysroot", + sysroot.to_str().unwrap(), + "-isystem", + system_include.to_str().unwrap(), + "-isystem", + gcc_include.to_str().unwrap(), + "-I", + include_path.to_str().unwrap(), + "-mfloat-abi=hard", + "-march=armv6k", + "-mtune=mpcore", + "-mfpu=vfp", + "-DARM11 ", + "-D_3DS ", + "-D__3DS__ ", + "-fshort-enums", + ]) + .parse_callbacks(Box::new(CustomCallbacks)) + .generate() + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_dir.join("bindings.rs")) + .expect("failed to write bindings"); + + // Compile static inline fns wrapper + let cc = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-gcc"); + let ar = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-ar"); + + cc::Build::new() + .compiler(cc) + .archiver(ar) + .include(&include_path) + .file(out_dir.join(static_fns_path.with_extension("c"))) + .flag("-march=armv6k") + .flag("-mtune=mpcore") + .flag("-mfloat-abi=hard") + .flag("-mfpu=vfp") + .flag("-mtp=soft") + .flag("-Wno-deprecated-declarations") + .compile("citro2d_statics_wrapper"); +} + +/// Custom callback struct to allow us to mark some "known good types" as +/// [`Copy`], which in turn allows using Rust `union` instead of bindgen union types. See +/// +/// for more info. +/// +/// We do the same for [`Debug`] just for the convenience of derived Debug impls +/// on some `citro2d` types. +/// +/// Finally, we use [`doxygen_rs`] to transform the doc comments into something +/// easier to read in the generated documentation / hover documentation. +#[derive(Debug)] +struct CustomCallbacks; + +impl ParseCallbacks for CustomCallbacks { + fn process_comment(&self, comment: &str) -> Option { + Some(doxygen_rs::transform(comment)) + } + + fn blocklisted_type_implements_trait( + &self, + name: &str, + derive_trait: DeriveTrait, + ) -> Option { + if let DeriveTrait::Copy | DeriveTrait::Debug = derive_trait { + match name { + "u64_" | "u32_" | "u16_" | "u8_" | "u64" | "u32" | "u16" | "u8" | "gfxScreen_t" + | "gfx3dSide_t" => Some(ImplementsTrait::Yes), + _ if name.starts_with("GPU_") => Some(ImplementsTrait::Yes), + _ => None, + } + } else { + None + } + } +} diff --git a/citro2d-sys/src/lib.rs b/citro2d-sys/src/lib.rs new file mode 100644 index 0000000..59575fd --- /dev/null +++ b/citro2d-sys/src/lib.rs @@ -0,0 +1,19 @@ +#![no_std] +#![allow(non_snake_case)] +#![allow(warnings)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(clippy::all)] +#![doc(html_root_url = "https://rust3ds.github.io/citro3d-rs/crates")] +#![doc( + html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" +)] +#![doc( + html_logo_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" +)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +// Prevent linking errors from the standard `test` library when running `cargo 3ds test --lib`. +#[cfg(test)] +extern crate shim_3ds; diff --git a/citro2d/Cargo.toml b/citro2d/Cargo.toml new file mode 100644 index 0000000..8e92878 --- /dev/null +++ b/citro2d/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "citro2d" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +document-features = "0.2.7" +ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" } +ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } +citro2d-sys = { path = "../citro2d-sys" } +citro3d = { version = "0.1.0", path = "../citro3d" } +citro3d-sys = { version = "0.1.0", path = "../citro3d-sys" } + +[dev-dependencies] +test-runner = { git = "https://github.com/rust3ds/ctru-rs.git" } + +[package.metadata.docs.rs] +all-features = true +default-target = "armv6k-nintendo-3ds" +targs = [] +cargo-args = ["-Z", "build-std"] diff --git a/citro2d/LICENSE-APACHE b/citro2d/LICENSE-APACHE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/citro2d/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/citro2d/LICENSE-MIT b/citro2d/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/citro2d/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/citro2d/examples/2d_shapes.rs b/citro2d/examples/2d_shapes.rs new file mode 100644 index 0000000..613f5e4 --- /dev/null +++ b/citro2d/examples/2d_shapes.rs @@ -0,0 +1,130 @@ +//! This example demonstrates the most basic usage of `citro2d`: rendering shapes +//! on the top screen of the 3DS. +//! This is an exact copy of 2d_shapes from the devkitPro examples, but in Rust. +//! https://github.com/devkitPro/3ds-examples/blob/master/graphics/gpu/2d_shapes/source/main.c +#![feature(allocator_api)] + +use citro2d::Point; +use citro2d::render::{Color, Target}; +use citro2d::shapes::{Circle, CircleSolid, Ellipse, MultiColor, Rectangle, Triangle}; +use ctru::{prelude::*, services::gfx::TopScreen3D}; + +const SCREEN_WIDTH: u16 = 400; +const SCREEN_HEIGHT: u16 = 240; + +fn main() { + let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); + let mut hid = Hid::new().expect("Couldn't obtain HID controller"); + let apt = Apt::new().expect("Couldn't obtain APT controller"); + + let mut citro2d_instance = citro2d::Instance::new().expect("Couldn't obtain citro2d instance"); + let top_screen = TopScreen3D::from(&gfx.top_screen); + let (top_left, _) = top_screen.split_mut(); + let mut top_target = Target::new(top_left).expect("failed to create render target"); + + let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut()); + let clr_white = Color::new(255, 255, 255); + let clr_green = Color::new(0, 255, 0); + let clr_red = Color::new(255, 0, 0); + let clr_blue = Color::new(0, 0, 255); + let clr_circle1 = Color::new(255, 0, 255); + let clr_circle2 = Color::new(255, 255, 0); + let clr_circle3 = Color::new(0, 255, 255); + let clr_solid_circle = Color::new(104, 176, 216); + let clr_tri1 = Color::new(255, 21, 0); + let clr_tri2 = Color::new(39, 105, 229); + let clr_rec1 = Color::new(154, 108, 185); + let clr_rec2 = Color::new(255, 255, 44); + let clr_rec3 = Color::new(216, 246, 15); + let clr_rec4 = Color::new(64, 234, 135); + let clr_clear = Color::new_with_alpha(255, 216, 176, 104); + + while apt.main_loop() { + hid.scan_input(); + + citro2d_instance.render_target(&mut top_target, |_instance, render_target| { + render_target.clear(clr_clear); + + render_target.render_2d_shape(&Triangle { + top: (25.0, 190.0).into(), + top_color: clr_white, + left: (0.0, SCREEN_HEIGHT as f32).into(), + left_color: clr_tri1, + right: (50.0, SCREEN_HEIGHT as f32).into(), + right_color: clr_tri2, + depth: 0.0, + }); + + render_target.render_2d_shape(&Rectangle { + point: Point::new(350.0, 0.0, 0.0), + size: (50.0, 50.0).into(), + multi_color: MultiColor { + top_left: clr_rec1, + top_right: clr_rec2, + bottom_left: clr_rec3, + bottom_right: clr_rec4, + }, + }); + + // Circles require a state change (an expensive operation) within citro2d's internals, so draw them last. + // Although it is possible to draw them in the middle of drawing non-circular objects + // (sprites, images, triangles, rectangles, etc.) this is not recommended. They should either + // be drawn before all non-circular objects, or afterwards. + + render_target.render_2d_shape(&Ellipse { + point: Point::new(0.0, 0.0, 0.0), + size: (SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32).into(), + multi_color: MultiColor { + top_left: clr_circle1, + top_right: clr_circle2, + bottom_left: clr_circle3, + bottom_right: clr_white, + }, + }); + + render_target.render_2d_shape(&Circle { + point: Point::new((SCREEN_WIDTH / 2) as f32, (SCREEN_HEIGHT / 2) as f32, 0.0), + radius: 50.0, + multi_color: MultiColor { + top_left: clr_circle3, + top_right: clr_white, + bottom_left: clr_circle1, + bottom_right: clr_circle2, + }, + }); + + render_target.render_2d_shape(&Circle { + point: Point::new(25.0, 25.0, 0.0), + radius: 25.0, + multi_color: MultiColor { + top_left: clr_red, + top_right: clr_blue, + bottom_left: clr_green, + bottom_right: clr_white, + }, + }); + + render_target.render_2d_shape(&CircleSolid { + x: (SCREEN_WIDTH - 25) as f32, + y: (SCREEN_HEIGHT - 25) as f32, + z: 0.0, + radius: 25.0, + color: clr_solid_circle, + }); + }); + + let stats = citro2d_instance.get_3d_stats(); + bottom_screen.select(); + println!("\x1b[1;1HSimple Rusty citro2d shapes example"); + println!("\x1b[2;1HCPU: {:6.2}%", stats.processing_time * 6.0); + println!("\x1b[3;1HGPU: {:6.2}%", stats.drawing_time * 6.0); + println!("\x1b[4;1HCmdBuf: {:6.2}%", stats.cmd_buf_usage * 100.0); + + if hid.keys_down().contains(KeyPad::START) { + break; + } + + //Uncomment to cap fps + // gfx.wait_for_vblank(); + } +} diff --git a/citro2d/examples/breakout.rs b/citro2d/examples/breakout.rs new file mode 100644 index 0000000..f2b4292 --- /dev/null +++ b/citro2d/examples/breakout.rs @@ -0,0 +1,254 @@ +//! This example demonstrates a simple 2d game of the classic game Breakout +//! Very simple implementation with bugs, but to show case a simple 2d game +#![feature(allocator_api)] + +use citro2d::render::{Color, Target}; +use citro2d::shapes::{CircleSolid, RectangleSolid}; +use citro2d::{Point, Size}; +use ctru::{prelude::*, services::gfx::TopScreen3D}; + +const TOP_SCREEN_WIDTH: u16 = 400; +const TOP_SCREEN_HEIGHT: u16 = 240; + +const BOTTOM_SCREEN_WIDTH: u16 = 320; +const BOTTOM_SCREEN_HEIGHT: u16 = 240; + +fn main() { + let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); + let mut hid = Hid::new().expect("Couldn't obtain HID controller"); + let apt = Apt::new().expect("Couldn't obtain APT controller"); + + let mut citro2d_instance = citro2d::Instance::new().expect("Couldn't obtain citro2d instance"); + let top_screen = TopScreen3D::from(&gfx.top_screen); + let (top_left, _) = top_screen.split_mut(); + let mut top_target = Target::new(top_left).expect("failed to create render target"); + + let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut()); + + let white = Color::new(255, 255, 255); + let black = Color::new(0, 0, 0); + + let mut paddle = Paddle { + position: Point::new( + BOTTOM_SCREEN_WIDTH as f32 / 2.0, + BOTTOM_SCREEN_HEIGHT as f32 - 10.0, + 0.0, + ), + size: (75.0, 10.0).into(), + color: white, + }; + + let mut ball = Ball { + position: Point::new( + BOTTOM_SCREEN_WIDTH as f32 / 2.0, + (BOTTOM_SCREEN_HEIGHT - 15) as f32, + 0.0, + ), + radius: 5.0, + color: white, + velocity: Point::new(2.0, -2.0, 0.0), + }; + let collors_of_rainbow = [ + Color::new(255, 0, 0), + Color::new(255, 127, 0), + Color::new(255, 255, 0), + Color::new(0, 255, 0), + Color::new(0, 0, 255), + Color::new(75, 0, 130), + Color::new(148, 0, 211), + ]; + let mut bricks = Vec::new(); + for row in 0..7 { + for column in 0..12 { + bricks.push(Brick { + position: Point::new(column as f32 * 32.0, row as f32 * 16.0, 0.0), + size: (30.0, 15.0).into(), + color: collors_of_rainbow[row], + is_alive: true, + }); + } + } + + while apt.main_loop() { + hid.scan_input(); + if hid.keys_down().contains(KeyPad::START) { + break; + } + + if hid.keys_held().contains(KeyPad::LEFT) || hid.keys_held().contains(KeyPad::DPAD_LEFT) { + paddle.move_left(); + } + + if hid.keys_held().contains(KeyPad::RIGHT) || hid.keys_held().contains(KeyPad::DPAD_RIGHT) { + paddle.move_right(); + } + + citro2d_instance.render_target(&mut top_target, |_instance, render_target| { + render_target.clear(black); + + paddle.render(render_target); + + ball.bounce(&paddle); + for brick in &mut bricks { + if brick.is_alive { + brick.live_or_die(&mut ball); + brick.render(render_target); + } + } + //circles are better to render last for performance reasons + ball.render(render_target); + }); + + let stats = citro2d_instance.get_3d_stats(); + bottom_screen.select(); + println!("\x1b[1;1HSimple Rusty citro2d shapes example"); + println!("\x1b[2;1HCPU: {:6.2}%", stats.processing_time * 6.0); + println!("\x1b[3;1HGPU: {:6.2}%", stats.drawing_time * 6.0); + println!("\x1b[4;1HCmdBuf: {:6.2}%", stats.cmd_buf_usage * 100.0); + + //Uncomment to cap fps + // gfx.wait_for_vblank(); + } +} + +struct Paddle { + pub position: Point, + pub size: Size, + pub color: Color, +} + +impl Paddle { + fn render(&self, render_target: &mut Target) { + render_target.render_2d_shape(&RectangleSolid { + point: self.position, + size: self.size, + color: self.color, + }); + } + + fn move_left(&mut self) { + if self.position.x > 0.0 { + self.position.x -= 2.0; + } + } + + fn move_right(&mut self) { + if self.position.x <= BOTTOM_SCREEN_WIDTH as f32 { + self.position.x += 2.0; + } + } +} + +struct Ball { + pub position: Point, + pub radius: f32, + pub color: Color, + pub velocity: Point, +} + +impl Ball { + fn render(&self, render_target: &mut Target) { + render_target.render_2d_shape(&CircleSolid { + x: self.position.x, + y: self.position.y, + z: self.position.z, + radius: self.radius, + color: self.color, + }); + } + + fn bounce(&mut self, paddle: &Paddle) { + self.position.x += self.velocity.x; + self.position.y += self.velocity.y; + + // Check for collision with the walls + if self.position.x - self.radius <= 0.0 + || self.position.x + self.radius >= TOP_SCREEN_WIDTH as f32 + { + self.velocity.x = -self.velocity.x; + } + + if self.position.y - self.radius <= 0.0 { + self.velocity.y = -self.velocity.y; + } + + // Check for collision with the paddle + if self.position.y + self.radius >= paddle.position.y + && self.position.x >= paddle.position.x + && self.position.x <= paddle.position.x + paddle.size.width + { + self.velocity.y = -self.velocity.y; + } + + // Check if the ball hits the bottom of the screen + if self.position.y + self.radius >= TOP_SCREEN_WIDTH as f32 { + // Reset the ball + self.position = Point::new( + TOP_SCREEN_WIDTH as f32 / 2.0, + TOP_SCREEN_HEIGHT as f32 / 2.0, + 0.0, + ); + self.velocity = Point::new(2.0, -2.0, 0.0); + } + } +} + +struct Brick { + pub position: Point, + pub size: Size, + pub color: Color, + pub is_alive: bool, +} + +impl Brick { + fn render(&self, render_target: &mut Target) { + if self.is_alive { + render_target.render_2d_shape(&RectangleSolid { + point: self.position, + size: self.size, + color: self.color, + }); + } + } + + fn check_collision(&self, ball: &mut Ball) -> bool { + let brick_left = self.position.x; + let brick_right = self.position.x + self.size.width; + let brick_top = self.position.y; + let brick_bottom = self.position.y + self.size.height; + + let ball_left = ball.position.x - ball.radius; + let ball_right = ball.position.x + ball.radius; + let ball_top = ball.position.y - ball.radius; + let ball_bottom = ball.position.y + ball.radius; + + if ball_left < brick_right + && ball_right > brick_left + && ball_top < brick_bottom + && ball_bottom > brick_top + { + // Determine the side of the collision and bounce the ball accordingly + if ball.velocity.x > 0.0 && ball_left < brick_right && ball_right > brick_left { + ball.velocity.x = -ball.velocity.x; + } else if ball.velocity.x < 0.0 && ball_right > brick_left && ball_left < brick_right { + ball.velocity.x = -ball.velocity.x; + } + + if ball.velocity.y > 0.0 && ball_top < brick_bottom && ball_bottom > brick_top { + ball.velocity.y = -ball.velocity.y; + } else if ball.velocity.y < 0.0 && ball_bottom > brick_top && ball_top < brick_bottom { + ball.velocity.y = -ball.velocity.y; + } + + return true; + } + + false + } + + fn live_or_die(&mut self, ball: &mut Ball) { + if self.check_collision(ball) { + self.is_alive = false; + } + } +} diff --git a/citro2d/src/error.rs b/citro2d/src/error.rs new file mode 100644 index 0000000..35c4c5a --- /dev/null +++ b/citro2d/src/error.rs @@ -0,0 +1,12 @@ +//! General-purpose error and result types returned by public APIs of this crate. + +/// The common result type returned by `citro2d` functions. +pub type Result = std::result::Result; + +/// The common error type that may be returned by `citro3d` functions. +#[non_exhaustive] +#[derive(Debug)] +pub enum Error { + /// A C2D object or context could not be initialized. + FailedToInitialize, +} diff --git a/citro2d/src/lib.rs b/citro2d/src/lib.rs new file mode 100644 index 0000000..5266027 --- /dev/null +++ b/citro2d/src/lib.rs @@ -0,0 +1,161 @@ +#![feature(custom_test_frameworks)] +#![test_runner(test_runner::run_gdb)] +#![feature(doc_cfg)] +#![feature(doc_auto_cfg)] +#![doc(html_root_url = "https://rust3ds.github.io/citro2d-rs/crates")] +#![doc( + html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" +)] +#![doc( + html_logo_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" +)] + +//! Safe Rust bindings to `citro2d`. This crate wraps `citro2d-sys` to provide +//! safer APIs for graphics programs targeting the 3DS. +//! +//! ## Feature flags +#![doc = document_features::document_features!()] + +pub mod error; +pub mod render; +pub mod shapes; +use citro2d_sys::C2D_DEFAULT_MAX_OBJECTS; +pub use error::{Error, Result}; +use render::Target; + +/// The single instance for using `citro2d`. This is the base type that an application +/// should instantiate to use this library. +#[non_exhaustive] +#[must_use] +pub struct Instance { + pub citro3d_instance: citro3d::Instance, +} + +impl Instance { + /// Create a new instance of `citro2d`. + /// This also initializes `citro3d` since it is required for `citro2d`. + pub fn new() -> Result { + let citro3d_instance = citro3d::Instance::new().expect("failed to initialize Citro3D"); + let citro2d = Self::with_max_objects( + C2D_DEFAULT_MAX_OBJECTS.try_into().unwrap(), + citro3d_instance, + ); + + citro2d + } + + /// You have to initialize citro3d before using citro2d, but some cases you may + /// Have initialized citro3d already, so you can use this function to initialize + /// You pass in the citro3d instance you already initialized to ensure it's lifetime is the same as citro2d + /// **Note** The above statement may not work, and may not be able to switch between the two without api changes + /// but currently working on that assumption and to allow for flexibility for the developer + pub fn new_without_c3d_init(citro3d_instance: citro3d::Instance) -> Result { + Self::with_max_objects( + C2D_DEFAULT_MAX_OBJECTS.try_into().unwrap(), + citro3d_instance, + ) + } + + /// Create a new instance of `citro2d` with a custom maximum number of objects. + #[doc(alias = "C2D_Init")] + #[doc(alias = "C2D_Prepare")] + pub fn with_max_objects( + max_objects: usize, + citro3d_instance: citro3d::Instance, + ) -> Result { + let new_citro_2d = match unsafe { citro2d_sys::C2D_Init(max_objects) } { + true => Ok(Self { + citro3d_instance: citro3d_instance, + }), + false => Err(Error::FailedToInitialize), + }; + unsafe { citro2d_sys::C2D_Prepare() }; + new_citro_2d + } + + /// Render 2d graphics to a selected [Target] + #[doc(alias = "C3D_FrameBegin")] + #[doc(alias = "C2D_SceneBegin")] + #[doc(alias = "C3D_FrameEnd")] + pub fn render_target(&mut self, target: &mut Target<'_>, f: F) + where + F: FnOnce(&Self, &mut Target<'_>), + { + unsafe { + citro3d_sys::C3D_FrameBegin(citro3d_sys::C3D_FRAME_SYNCDRAW); + citro2d_sys::C2D_SceneBegin(target.raw); + f(self, target); + citro3d_sys::C3D_FrameEnd(0); + } + } + + /// Returns some stats about the 3Ds's graphics + /// TODO this may be more appropriate in citro3d + pub fn get_3d_stats(&self) -> Citro3DStats { + //TODO should i check for NaN? + let processing_time_f32 = unsafe { citro3d_sys::C3D_GetProcessingTime() }; + let drawing_time_f32 = unsafe { citro3d_sys::C3D_GetDrawingTime() }; + let cmd_buf_usage_f32 = unsafe { citro3d_sys::C3D_GetCmdBufUsage() }; + Citro3DStats { + processing_time: processing_time_f32, + drawing_time: drawing_time_f32, + cmd_buf_usage: cmd_buf_usage_f32, + } + } +} + +/// Stats about the 3Ds's graphics +pub struct Citro3DStats { + pub processing_time: f32, + pub drawing_time: f32, + pub cmd_buf_usage: f32, +} + +/// A 2D point in space. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Point { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Point { + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + + pub const fn new_no_z(x: f32, y: f32) -> Self { + Self { x, y, z: 0.0 } + } +} + +impl From<(f32, f32, f32)> for Point { + fn from((x, y, z): (f32, f32, f32)) -> Self { + Self { x, y, z } + } +} + +impl From<(f32, f32)> for Point { + fn from((x, y): (f32, f32)) -> Self { + Self { x, y, z: 0.0 } + } +} + +/// Size of a 2D object. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Size { + pub width: f32, + pub height: f32, +} + +impl Size { + pub const fn new(width: f32, height: f32) -> Self { + Self { width, height } + } +} + +impl From<(f32, f32)> for Size { + fn from((width, height): (f32, f32)) -> Self { + Self { width, height } + } +} diff --git a/citro2d/src/render.rs b/citro2d/src/render.rs new file mode 100644 index 0000000..d8f434c --- /dev/null +++ b/citro2d/src/render.rs @@ -0,0 +1,79 @@ +//! Safe bindings to render 2d graphics to a [Target] +use std::cell::RefMut; + +use ctru::services::gfx::Screen; + +use crate::{Error, Result, shapes::Shape}; + +/// A color in RGBA format. The color is stored as a 32-bit integer +#[derive(Debug, Clone, Copy)] +pub struct Color { + pub inner: u32, +} + +impl Color { + /// Create a new color with the given RGB values. Alpha is set to 255 (fully opaque). + pub fn new(r: u8, g: u8, b: u8) -> Self { + Self::new_with_alpha(r, g, b, 255) + } + + /// Create a new color with the given RGBA values. + pub fn new_with_alpha(r: u8, g: u8, b: u8, a: u8) -> Self { + let inner = r as u32 | (g as u32) << 8 | (b as u32) << 16 | (a as u32) << 24; + Self { inner } + } +} + +impl Into for u32 { + fn into(self) -> Color { + Color { inner: self } + } +} + +impl From for u32 { + fn from(color: Color) -> u32 { + color.inner + } +} + +/// HACK A 2D target, which technically is a 3D target, but we use it for 2D rendering. +/// There is a chance that this can be combined with the 3D target in the future. +#[doc(alias = "C3D_RenderTarget")] +pub struct Target<'screen> { + pub raw: *mut citro2d_sys::C3D_RenderTarget_tag, + // This is unused after construction, but ensures unique access to the + // screen this target writes to during rendering + _phantom_screen: RefMut<'screen, dyn Screen>, +} + +impl<'screen> Target<'screen> { + ///Creates a 2D [Target] for rendering. Even though it returns a C3D_RenderTarget_tag, it is required to use the C2D_CreateScreenTarget method + pub fn new(screen: RefMut<'screen, dyn Screen>) -> Result { + let raw = + unsafe { citro2d_sys::C2D_CreateScreenTarget(screen.as_raw(), screen.side().into()) }; + + if raw.is_null() { + return Err(Error::FailedToInitialize); + } + + Ok(Self { + raw, + _phantom_screen: screen, + }) + } + + /// Clears the screen to a selected color + pub fn clear(&mut self, color: Color) { + unsafe { + citro2d_sys::C2D_TargetClear(self.raw, color.inner); + } + } + + /// Renders a 2d shape to the [Target] + pub fn render_2d_shape(&self, shape: &S) + where + S: Shape, + { + shape.render(); + } +} diff --git a/citro2d/src/shapes.rs b/citro2d/src/shapes.rs new file mode 100644 index 0000000..9a6fdc4 --- /dev/null +++ b/citro2d/src/shapes.rs @@ -0,0 +1,224 @@ +//! Safe bindings to shapes supported by citro2d +use crate::{Point, Size, render::Color}; + +/// Holds information for rendering multi colored shapes +/// most shapes have a 'solid' +pub struct MultiColor { + pub top_left: Color, + pub top_right: Color, + pub bottom_left: Color, + pub bottom_right: Color, +} + +/// A trait to help render all 2D shapes supported by citro2d +pub trait Shape { + //TODO possibly return Option. + fn render(&self) -> bool; +} + +/// Holds information for rendering a C2D_DrawRectangle +pub struct Rectangle { + pub point: Point, + pub size: Size, + pub multi_color: MultiColor, +} + +impl Shape for Rectangle { + /// Draws a multi color rectangle + #[doc(alias = "C2D_DrawRectangle")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawRectangle( + self.point.x, + self.point.y, + self.point.z, + self.size.width, + self.size.height, + self.multi_color.top_left.into(), + self.multi_color.top_right.into(), + self.multi_color.bottom_left.into(), + self.multi_color.bottom_right.into(), + ) + } + } +} + +/// Holds the information needed to draw a solid color Rectangle +pub struct RectangleSolid { + pub point: Point, + pub size: Size, + pub color: Color, +} + +impl Shape for RectangleSolid { + /// Draws a single colored Rectangle + #[doc(alias = "C2D_DrawRectSolid")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawRectSolid( + self.point.x, + self.point.y, + self.point.z, + self.size.width, + self.size.height, + self.color.into(), + ) + } + } +} + +/// Holds the information needed to draw a solid color Triangle +pub struct Triangle { + pub top: Point, + pub top_color: Color, + pub left: Point, + pub left_color: Color, + pub right: Point, + pub right_color: Color, + pub depth: f32, +} + +impl Shape for Triangle { + /// Draws a multi color Triangle + #[doc(alias = "C2D_DrawTriangle")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawTriangle( + self.top.x, + self.top.y, + self.top_color.into(), + self.left.x, + self.left.y, + self.left_color.into(), + self.right.x, + self.right.y, + self.right_color.into(), + self.depth, + ) + } + } +} + +/// Holds the information needed to draw a Ellipse +pub struct Ellipse { + pub point: Point, + pub size: Size, + pub multi_color: MultiColor, +} + +impl Shape for Ellipse { + /// Draws a multi color Ellipse + #[doc(alias = "C2D_DrawEllipse")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawEllipse( + self.point.x, + self.point.y, + self.point.z, + self.size.width, + self.size.height, + self.multi_color.top_left.into(), + self.multi_color.top_right.into(), + self.multi_color.bottom_left.into(), + self.multi_color.bottom_right.into(), + ) + } + } +} + +/// Holds the information needed to draw a solid color Triangle +pub struct EllipseSolid { + pub point: Point, + pub size: Size, + pub color: Color, +} + +impl Shape for EllipseSolid { + ///Draws a solid color Ellipse + #[doc(alias = "C2D_DrawEllipseSolid")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawEllipseSolid( + self.point.x, + self.point.y, + self.point.z, + self.size.width, + self.size.height, + self.color.into(), + ) + } + } +} +/// Holds the information needed to draw a multi colored circle +pub struct Circle { + pub point: Point, + pub radius: f32, + pub multi_color: MultiColor, +} + +impl Shape for Circle { + /// Draws a multi color Ellipse + #[doc(alias = "C2D_DrawCircle")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawCircle( + self.point.x, + self.point.y, + self.point.z, + self.radius, + self.multi_color.top_left.into(), + self.multi_color.top_right.into(), + self.multi_color.bottom_left.into(), + self.multi_color.bottom_right.into(), + ) + } + } +} + +/// Holds the information needed to draw a solid color Circle +pub struct CircleSolid { + pub x: f32, + pub y: f32, + pub z: f32, + pub radius: f32, + pub color: Color, +} + +impl Shape for CircleSolid { + /// Renders a solid Circle + #[doc(alias = "C2D_DrawCircleSolid")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawCircleSolid(self.x, self.y, self.z, self.radius, self.color.into()) + } + } +} + +/// Holds the information needed to draw a solid color Circle +pub struct Line { + pub start: Point, + pub end: Point, + pub start_color: Color, + pub end_color: Color, + pub thickness: f32, + pub depth: f32, +} + +impl Shape for Line { + /// Renders a line + #[doc(alias = "C2D_DrawLine")] + fn render(&self) -> bool { + unsafe { + citro2d_sys::C2D_DrawLine( + self.start.x, + self.start.y, + self.start_color.into(), + self.end.x, + self.end.y, + self.end_color.into(), + self.thickness, + self.depth, + ) + } + } +} diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index b96ca1e..256e47b 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -11,7 +11,7 @@ approx = { version = "0.5.1", optional = true } bitflags = "1.3.2" bytemuck = { version = "1.10.0", features = ["extern_crate_std"] } citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } -citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" } +citro3d-sys = { path = "../citro3d-sys" } ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" } ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } document-features = "0.2.7"