diff --git a/Cargo.toml b/Cargo.toml index d59cf00..f8955b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["citro3d-sys", "citro3d", "bindgen-citro3d"] -default-members = ["citro3d", "citro3d-sys"] +members = ["citro3d-sys", "citro3d", "bindgen-citro3d", "pica200"] +default-members = ["citro3d", "citro3d-sys", "pica200"] resolver = "2" [patch."https://github.com/rust3ds/citro3d-rs.git"] diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index 0ac2fa7..8d94e9f 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "citro3d" -authors = [ "Rust3DS Org" ] +authors = ["Rust3DS Org"] license = "MIT OR Apache-2.0" version = "0.1.0" edition = "2021" [dependencies] +pica200 = { version = "0.1.0", path = "../pica200" } bitflags = "1.3.2" bytemuck = { version = "1.10.0", features = ["extern_crate_std"] } citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" } diff --git a/citro3d/build.rs b/citro3d/build.rs deleted file mode 100644 index b6935f0..0000000 --- a/citro3d/build.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::ffi::OsStr; -use std::path::PathBuf; -use std::process::Command; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=examples/assets"); - - let mut asset_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); - asset_dir.push("examples"); - asset_dir.push("assets"); - - println!("Checking dir {:?}", asset_dir.display()); - - for entry in asset_dir.read_dir().unwrap().flatten() { - println!("Checking {:?}", entry.path().display()); - if let Some("pica") = entry.path().extension().and_then(OsStr::to_str) { - println!("cargo:rerun-if-changed={}", entry.path().display()); - - let mut out_path = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); - out_path.push("examples"); - out_path.push("assets"); - out_path.push(entry.path().with_extension("shbin").file_name().unwrap()); - - std::fs::create_dir_all(out_path.parent().unwrap()).unwrap(); - - println!("Compiling {:?}", out_path.display()); - - let mut cmd = Command::new("picasso"); - cmd.arg(entry.path()).arg("--out").arg(out_path); - - let status = cmd.spawn().unwrap().wait().unwrap(); - assert!( - status.success(), - "Command {cmd:#?} failed with code {status:?}" - ); - } - } -} diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 6bfc1fa..1a10ac2 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -3,17 +3,15 @@ #![feature(allocator_api)] -use citro3d::attrib::{self}; -use citro3d::buffer::{self}; +use std::ffi::CStr; +use std::mem::MaybeUninit; + use citro3d::render::{ClearFlags, Target}; -use citro3d::{include_aligned_bytes, shader}; +use citro3d::{attrib, buffer, shader}; use citro3d_sys::C3D_Mtx; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; -use std::ffi::CStr; -use std::mem::MaybeUninit; - #[repr(C)] #[derive(Copy, Clone)] struct Vec3 { @@ -50,8 +48,7 @@ static VERTICES: &[Vertex] = &[ }, ]; -static SHADER_BYTES: &[u8] = - include_aligned_bytes!(concat!(env!("OUT_DIR"), "/examples/assets/vshader.shbin")); +static SHADER_BYTES: &[u8] = citro3d::include_shader!("assets/vshader.pica"); fn main() { ctru::use_panic_handler(); diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index f3f79fd..cab6a59 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -8,6 +8,7 @@ pub mod shader; use citro3d_sys::C3D_FrameDrawOn; pub use error::{Error, Result}; +pub use pica200::include_shader; /// The single instance for using `citro3d`. This is the base type that an application /// should instantiate to use this library. diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index a1bd264..d3c0060 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -7,11 +7,6 @@ use std::error::Error; use std::mem::MaybeUninit; -// Macros get exported at the crate root, so no reason to document this module. -// It still needs to be `pub` for the helper struct it exports. -#[doc(hidden)] -pub mod macros; - /// A PICA200 shader program. It may have one or both of: /// /// * A vertex shader [`Library`] diff --git a/citro3d/src/shader/macros.rs b/citro3d/src/shader/macros.rs deleted file mode 100644 index fb61eb5..0000000 --- a/citro3d/src/shader/macros.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Helper macros for working with shader data. - -/// Helper macro for including a file as bytes that are correctly aligned for -/// use as a [`Library`](super::Library). -#[macro_export] -macro_rules! include_aligned_bytes { - ($path:expr) => {{ - // const block expression to encapsulate the static - use $crate::shader::macros::AlignedAs; - - // this assignment is made possible by CoerceUnsized - const ALIGNED: &AlignedAs = &AlignedAs { - _align: [], - bytes: *include_bytes!($path), - }; - - &ALIGNED.bytes - }}; -} - -/// Helper struct to [`include_bytes`] aligned as a specific type. -#[repr(C)] // guarantee 'bytes' comes after '_align' -#[doc(hidden)] -pub struct AlignedAs { - pub _align: [Align; 0], - pub bytes: Bytes, -} diff --git a/pica200/Cargo.toml b/pica200/Cargo.toml new file mode 100644 index 0000000..bc7c0e3 --- /dev/null +++ b/pica200/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pica200" +version = "0.1.0" +edition = "2021" +authors = ["Rust3DS Org"] +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +litrs = { version = "0.4.0", default-features = false } +quote = "1.0.32" diff --git a/pica200/build.rs b/pica200/build.rs new file mode 100644 index 0000000..2b03120 --- /dev/null +++ b/pica200/build.rs @@ -0,0 +1,8 @@ +//! This build script mainly exists just to ensure `OUT_DIR` is set for the macro, +//! but we can also use it to force a re-evaluation if `DEVKITPRO` changes. + +fn main() { + for var in ["OUT_DIR", "DEVKITPRO"] { + println!("cargo:rerun-if-env-changed={var}"); + } +} diff --git a/pica200/src/lib.rs b/pica200/src/lib.rs new file mode 100644 index 0000000..77f195e --- /dev/null +++ b/pica200/src/lib.rs @@ -0,0 +1,115 @@ +// we're already nightly-only so might as well use unstable proc macro APIs. +#![feature(proc_macro_span)] + +use std::path::PathBuf; +use std::{env, process}; + +use litrs::StringLit; +use quote::quote; + +/// Compiles the given PICA200 shader using [`picasso`](https://github.com/devkitPro/picasso) +/// and returns the compiled bytes directly as a `&[u8]` slice. +/// +/// This is similar to the standard library's [`include_bytes!`] macro, for which +/// file paths are relative to the source file where the macro is invoked. +/// +/// The compiled shader binary will be saved in the caller's `$OUT_DIR`. +/// +/// # Errors +/// +/// This macro will fail to compile if the input is not a single string literal. +/// In other words, inputs like `concat!("foo", "/bar")` are not supported. +/// +/// # Example +/// +/// ```no_run +/// # use pica200::include_shader; +/// static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); +/// ``` +#[proc_macro] +pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let tokens: Vec<_> = input.into_iter().collect(); + + if tokens.len() != 1 { + let msg = format!("expected exactly one input token, got {}", tokens.len()); + return quote! { compile_error!(#msg) }.into(); + } + + let string_lit = match StringLit::try_from(&tokens[0]) { + // Error if the token is not a string literal + Err(e) => return e.to_compile_error(), + Ok(lit) => lit, + }; + + // The cwd can change depending on whether this is running in a doctest or not: + // https://users.rust-lang.org/t/which-directory-does-a-proc-macro-run-from/71917 + // + // But the span's `source_file()` seems to always be relative to the cwd. + let cwd = env::current_dir().expect("unable to determine working directory"); + let invoking_source_file = tokens[0].span().source_file().path(); + let invoking_source_dir = invoking_source_file + .parent() + .expect("unable to find parent directory of invoking source file"); + + // By joining these three pieces, we arrive at approximately the same behavior as `include_bytes!` + let shader_source_file = cwd.join(invoking_source_dir).join(string_lit.value()); + let shader_out_file = shader_source_file.with_extension("shbin"); + + let Some(shader_out_file) = shader_out_file.file_name() else { + let msg = format!("invalid input file name {shader_source_file:?}"); + return quote! { compile_error!(#msg) }.into(); + }; + + let out_dir = PathBuf::from(env!("OUT_DIR")); + let out_path = out_dir.join(shader_out_file); + + let devkitpro = PathBuf::from(env!("DEVKITPRO")); + + let output = process::Command::new(devkitpro.join("tools/bin/picasso")) + .arg("--out") + .args([&out_path, &shader_source_file]) + .output() + .unwrap(); + + match output.status.code() { + Some(0) => {} + code => { + let code = code.map_or_else(|| String::from("unknown"), |c| c.to_string()); + + let msg = format!( + "failed to compile shader {shader_source_file:?}: exit status {code}: {}", + String::from_utf8_lossy(&output.stderr), + ); + + return quote! { compile_error!(#msg) }.into(); + } + } + + let bytes = std::fs::read(out_path).unwrap(); + let source_file_path = shader_source_file.to_string_lossy(); + + let result = quote! { + { + // ensure the source is re-evaluted if the input file changes + const _SOURCE: &[u8] = include_bytes! ( #source_file_path ); + + // https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049/2 + #[repr(C)] + struct AlignedAsU32 { + _align: [u32; 0], + bytes: Bytes, + } + + // this assignment is made possible by CoerceUnsized + const ALIGNED: &AlignedAsU32<[u8]> = &AlignedAsU32 { + _align: [], + // emits a token stream like `[10u8, 11u8, ... ]` + bytes: [ #(#bytes),* ] + }; + + &ALIGNED.bytes + } + }; + + result.into() +}