From 7d5cd29c5ee524a5bcedd370dc1cbc75d4adcc0c Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 5 Aug 2023 13:39:31 -0400 Subject: [PATCH 1/4] Add pica200::include_shader macro using picasso --- Cargo.toml | 4 +- citro3d/Cargo.toml | 3 +- citro3d/build.rs | 39 ------------ citro3d/examples/triangle.rs | 13 ++-- citro3d/src/lib.rs | 1 + citro3d/src/shader.rs | 5 -- citro3d/src/shader/macros.rs | 27 -------- pica200/Cargo.toml | 13 ++++ pica200/build.rs | 8 +++ pica200/src/lib.rs | 115 +++++++++++++++++++++++++++++++++++ 10 files changed, 146 insertions(+), 82 deletions(-) delete mode 100644 citro3d/build.rs delete mode 100644 citro3d/src/shader/macros.rs create mode 100644 pica200/Cargo.toml create mode 100644 pica200/build.rs create mode 100644 pica200/src/lib.rs 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() +} From 4752c65f416bf3ec2edcccb63ea758ef5d610e34 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 23 Sep 2023 13:34:28 -0400 Subject: [PATCH 2/4] Rename to citro3d-macros Export as pub mod macros, add tests, and refactor error handling a bit to make the logic simpler. --- .gitignore | 3 ++ Cargo.toml | 13 ++++- {pica200 => citro3d-macros}/Cargo.toml | 2 +- {pica200 => citro3d-macros}/build.rs | 0 {pica200 => citro3d-macros}/src/lib.rs | 66 ++++++++++++++++---------- citro3d-macros/tests/integration.pica | 14 ++++++ citro3d-macros/tests/integration.rs | 15 ++++++ citro3d/Cargo.toml | 2 +- citro3d/src/lib.rs | 6 ++- 9 files changed, 91 insertions(+), 30 deletions(-) rename {pica200 => citro3d-macros}/Cargo.toml (89%) rename {pica200 => citro3d-macros}/build.rs (100%) rename {pica200 => citro3d-macros}/src/lib.rs (65%) create mode 100644 citro3d-macros/tests/integration.pica create mode 100644 citro3d-macros/tests/integration.rs diff --git a/.gitignore b/.gitignore index 4a87a85..f13002d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ Cargo.lock rust-toolchain rust-toolchain.toml .cargo/ + +# Pica200 output files +*.shbin diff --git a/Cargo.toml b/Cargo.toml index f8955b4..c2c3e84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,15 @@ [workspace] -members = ["citro3d-sys", "citro3d", "bindgen-citro3d", "pica200"] -default-members = ["citro3d", "citro3d-sys", "pica200"] +members = [ + "bindgen-citro3d", + "citro3d-macros", + "citro3d-sys", + "citro3d", +] +default-members = [ + "citro3d", + "citro3d-sys", + "citro3d-macros", +] resolver = "2" [patch."https://github.com/rust3ds/citro3d-rs.git"] diff --git a/pica200/Cargo.toml b/citro3d-macros/Cargo.toml similarity index 89% rename from pica200/Cargo.toml rename to citro3d-macros/Cargo.toml index bc7c0e3..968366e 100644 --- a/pica200/Cargo.toml +++ b/citro3d-macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pica200" +name = "citro3d-macros" version = "0.1.0" edition = "2021" authors = ["Rust3DS Org"] diff --git a/pica200/build.rs b/citro3d-macros/build.rs similarity index 100% rename from pica200/build.rs rename to citro3d-macros/build.rs diff --git a/pica200/src/lib.rs b/citro3d-macros/src/lib.rs similarity index 65% rename from pica200/src/lib.rs rename to citro3d-macros/src/lib.rs index 77f195e..6fbcc3a 100644 --- a/pica200/src/lib.rs +++ b/citro3d-macros/src/lib.rs @@ -1,10 +1,13 @@ // we're already nightly-only so might as well use unstable proc macro APIs. #![feature(proc_macro_span)] +use std::error::Error; +use std::fs::DirBuilder; use std::path::PathBuf; use std::{env, process}; use litrs::StringLit; +use proc_macro::TokenStream; use quote::quote; /// Compiles the given PICA200 shader using [`picasso`](https://github.com/devkitPro/picasso) @@ -23,22 +26,30 @@ use quote::quote; /// # Example /// /// ```no_run -/// # use pica200::include_shader; +/// # use citro3d_macros::include_shader; /// static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); /// ``` #[proc_macro] pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + match include_shader_impl(input) { + Ok(tokens) => tokens, + Err(err) => { + let err_str = err.to_string(); + quote! { compile_error!( #err_str ) }.into() + } + } +} + +fn include_shader_impl(input: TokenStream) -> Result> { 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(); + return Err(format!("expected exactly one input token, got {}", tokens.len()).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, + Err(err) => return Ok(err.to_compile_error()), }; // The cwd can change depending on whether this is running in a doctest or not: @@ -46,46 +57,51 @@ pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream // // 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"); + let span = tokens[0].span(); + let invoking_source_file = span.source_file().path(); + let Some(invoking_source_dir) = invoking_source_file.parent() else { + return Err("unable to find parent directory of invoking source file".into()); + }; // 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 shader_out_file: PathBuf = shader_source_file.with_extension("shbin"); let out_dir = PathBuf::from(env!("OUT_DIR")); - let out_path = out_dir.join(shader_out_file); + // This might be overkill, but it ensures we get a unique path if different + // shaders with the same relative path are used within one program + let relative_out_path = shader_out_file.canonicalize()?; + + let out_path = out_dir.join( + relative_out_path + .strip_prefix("/") + .unwrap_or(&shader_out_file), + ); + + let parent_dir = out_path.parent().ok_or("invalid input filename")?; + DirBuilder::new().recursive(true).create(parent_dir)?; 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(); + .output()?; 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}: {}", + return Err(format!( + "failed to compile shader: exit status from `picasso` was {code}: {}", String::from_utf8_lossy(&output.stderr), - ); - - return quote! { compile_error!(#msg) }.into(); + ) + .into()); } } - let bytes = std::fs::read(out_path).unwrap(); + let bytes = std::fs::read(out_path)?; let source_file_path = shader_source_file.to_string_lossy(); let result = quote! { @@ -111,5 +127,5 @@ pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream } }; - result.into() + Ok(result.into()) } diff --git a/citro3d-macros/tests/integration.pica b/citro3d-macros/tests/integration.pica new file mode 100644 index 0000000..a47bfde --- /dev/null +++ b/citro3d-macros/tests/integration.pica @@ -0,0 +1,14 @@ +; Trivial vertex shader + +.out outpos position +.out outclr color + +.alias inpos v1 +.alias inclr v0 + +.proc main + mov outpos, inpos + mov outclr, inclr + + end +.end diff --git a/citro3d-macros/tests/integration.rs b/citro3d-macros/tests/integration.rs new file mode 100644 index 0000000..c67a52f --- /dev/null +++ b/citro3d-macros/tests/integration.rs @@ -0,0 +1,15 @@ +use citro3d_macros::include_shader; + +#[test] +fn includes_shader_static() { + static SHADER_BYTES: &[u8] = include_shader!("test.pica"); + + assert_eq!(SHADER_BYTES.len() % 4, 0); +} + +#[test] +fn includes_shader_const() { + const SHADER_BYTES: &[u8] = include_shader!("test.pica"); + + assert_eq!(SHADER_BYTES.len() % 4, 0); +} diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index 8d94e9f..99b340c 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" edition = "2021" [dependencies] -pica200 = { version = "0.1.0", path = "../pica200" } +citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } 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/src/lib.rs b/citro3d/src/lib.rs index cab6a59..5176de6 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -8,7 +8,11 @@ pub mod shader; use citro3d_sys::C3D_FrameDrawOn; pub use error::{Error, Result}; -pub use pica200::include_shader; + +pub mod macros { + //! Helper macros for working with shaders. + pub use citro3d_macros::*; +} /// The single instance for using `citro3d`. This is the base type that an application /// should instantiate to use this library. From c19876af66ec95e8235f78fe782e69fbbae08154 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 23 Sep 2023 14:04:09 -0400 Subject: [PATCH 3/4] Even more error handling improvements Use map_err everywhere to add context / file paths when possible. Also add some basic compile_fail doctests for missing / bad syntax shader sources. --- citro3d-macros/src/lib.rs | 98 +++++++++++++++++++--------- citro3d-macros/tests/bad-shader.pica | 11 ++++ citro3d-macros/tests/integration.rs | 4 +- 3 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 citro3d-macros/tests/bad-shader.pica diff --git a/citro3d-macros/src/lib.rs b/citro3d-macros/src/lib.rs index 6fbcc3a..932f177 100644 --- a/citro3d-macros/src/lib.rs +++ b/citro3d-macros/src/lib.rs @@ -25,9 +25,25 @@ use quote::quote; /// /// # Example /// -/// ```no_run +/// ``` +/// use citro3d_macros::include_shader; +/// +/// static SHADER_BYTES: &[u8] = include_shader!("../tests/integration.pica"); +/// ``` +/// +/// # Errors +/// +/// The macro will fail to compile if the `.pica` file cannot be found, or contains +/// `picasso` syntax errors. +/// +/// ```compile_fail +/// # use citro3d_macros::include_shader; +/// static _ERROR: &[u8] = include_shader!("../tests/nonexistent.pica"); +/// ``` +/// +/// ```compile_fail /// # use citro3d_macros::include_shader; -/// static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); +/// static _ERROR: &[u8] = include_shader!("../tests/bad-shader.pica"); /// ``` #[proc_macro] pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -47,7 +63,9 @@ fn include_shader_impl(input: TokenStream) -> Result return Err(format!("expected exactly one input token, got {}", tokens.len()).into()); } - let string_lit = match StringLit::try_from(&tokens[0]) { + let shader_source_filename = &tokens[0]; + + let string_lit = match StringLit::try_from(shader_source_filename) { Ok(lit) => lit, Err(err) => return Ok(err.to_compile_error()), }; @@ -56,52 +74,70 @@ fn include_shader_impl(input: TokenStream) -> Result // 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 span = tokens[0].span(); - let invoking_source_file = span.source_file().path(); + let cwd = env::current_dir() + .map_err(|err| format!("unable to determine current directory: {err}"))?; + + let invoking_source_file = shader_source_filename.span().source_file().path(); let Some(invoking_source_dir) = invoking_source_file.parent() else { - return Err("unable to find parent directory of invoking source file".into()); + return Ok(quote! { + compile_error!( + concat!( + "unable to find parent directory of current source file \"", + file!(), + "\"" + ) + ) + } + .into()); }; // 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_source_file = cwd + .join(invoking_source_dir) + .join(string_lit.value()) + // This might be overkill, but it ensures we get a unique path if different + // shaders with the same relative path are used within one program + .canonicalize() + .map_err(|err| format!("unable to resolve absolute path of shader source: {err}"))?; + let shader_out_file: PathBuf = shader_source_file.with_extension("shbin"); let out_dir = PathBuf::from(env!("OUT_DIR")); - // This might be overkill, but it ensures we get a unique path if different - // shaders with the same relative path are used within one program - let relative_out_path = shader_out_file.canonicalize()?; - let out_path = out_dir.join( - relative_out_path - .strip_prefix("/") - .unwrap_or(&shader_out_file), - ); + let out_path = out_dir.join(shader_out_file.components().skip(1).collect::()); + // UNWRAP: we already canonicalized the source path, so it should have a parent. + let out_parent = out_path.parent().unwrap(); - let parent_dir = out_path.parent().ok_or("invalid input filename")?; - DirBuilder::new().recursive(true).create(parent_dir)?; + DirBuilder::new() + .recursive(true) + .create(out_parent) + .map_err(|err| format!("unable to create output directory {out_parent:?}: {err}"))?; let devkitpro = PathBuf::from(env!("DEVKITPRO")); + let picasso = devkitpro.join("tools/bin/picasso"); - let output = process::Command::new(devkitpro.join("tools/bin/picasso")) + let output = process::Command::new(&picasso) .arg("--out") .args([&out_path, &shader_source_file]) - .output()?; + .output() + .map_err(|err| format!("unable to run {picasso:?}: {err}"))?; - match output.status.code() { - Some(0) => {} - code => { - let code = code.map_or_else(|| String::from("unknown"), |c| c.to_string()); + let error_code = match output.status.code() { + Some(0) => None, + code => Some(code.map_or_else(|| String::from(""), |c| c.to_string())), + }; - return Err(format!( - "failed to compile shader: exit status from `picasso` was {code}: {}", - String::from_utf8_lossy(&output.stderr), - ) - .into()); - } + if let Some(code) = error_code { + return Err(format!( + "failed to compile shader: `picasso` exited with status {code}: {}", + String::from_utf8_lossy(&output.stderr), + ) + .into()); } - let bytes = std::fs::read(out_path)?; + let bytes = std::fs::read(&out_path) + .map_err(|err| format!("unable to read output file {out_path:?}: {err}"))?; + let source_file_path = shader_source_file.to_string_lossy(); let result = quote! { diff --git a/citro3d-macros/tests/bad-shader.pica b/citro3d-macros/tests/bad-shader.pica new file mode 100644 index 0000000..360bbc0 --- /dev/null +++ b/citro3d-macros/tests/bad-shader.pica @@ -0,0 +1,11 @@ +; Vertex shader that won't compile + +.out outpos position +.out outclr color + +.proc main + mov outpos, 1 + mov outclr, 0 + + end +.end diff --git a/citro3d-macros/tests/integration.rs b/citro3d-macros/tests/integration.rs index c67a52f..68addcc 100644 --- a/citro3d-macros/tests/integration.rs +++ b/citro3d-macros/tests/integration.rs @@ -2,14 +2,14 @@ use citro3d_macros::include_shader; #[test] fn includes_shader_static() { - static SHADER_BYTES: &[u8] = include_shader!("test.pica"); + static SHADER_BYTES: &[u8] = include_shader!("integration.pica"); assert_eq!(SHADER_BYTES.len() % 4, 0); } #[test] fn includes_shader_const() { - const SHADER_BYTES: &[u8] = include_shader!("test.pica"); + const SHADER_BYTES: &[u8] = include_shader!("integration.pica"); assert_eq!(SHADER_BYTES.len() % 4, 0); } From 4e5c9167820686184cdad637fec08be796fe449b Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 23 Sep 2023 14:15:40 -0400 Subject: [PATCH 4/4] Fix doc link to stdlib include_bytes --- citro3d-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citro3d-macros/src/lib.rs b/citro3d-macros/src/lib.rs index 932f177..3d77b3f 100644 --- a/citro3d-macros/src/lib.rs +++ b/citro3d-macros/src/lib.rs @@ -13,7 +13,7 @@ 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 +/// This is similar to the standard library's [`include_bytes!`](std::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`.