Skip to content

Commit 7d5cd29

Browse files
Add pica200::include_shader macro using picasso
1 parent 795a9d9 commit 7d5cd29

File tree

10 files changed

+146
-82
lines changed

10 files changed

+146
-82
lines changed

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
2-
members = ["citro3d-sys", "citro3d", "bindgen-citro3d"]
3-
default-members = ["citro3d", "citro3d-sys"]
2+
members = ["citro3d-sys", "citro3d", "bindgen-citro3d", "pica200"]
3+
default-members = ["citro3d", "citro3d-sys", "pica200"]
44
resolver = "2"
55

66
[patch."https://github.com/rust3ds/citro3d-rs.git"]

citro3d/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
[package]
22
name = "citro3d"
3-
authors = [ "Rust3DS Org" ]
3+
authors = ["Rust3DS Org"]
44
license = "MIT OR Apache-2.0"
55
version = "0.1.0"
66
edition = "2021"
77

88
[dependencies]
9+
pica200 = { version = "0.1.0", path = "../pica200" }
910
bitflags = "1.3.2"
1011
bytemuck = { version = "1.10.0", features = ["extern_crate_std"] }
1112
citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" }

citro3d/build.rs

-39
This file was deleted.

citro3d/examples/triangle.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
44
#![feature(allocator_api)]
55

6-
use citro3d::attrib::{self};
7-
use citro3d::buffer::{self};
6+
use std::ffi::CStr;
7+
use std::mem::MaybeUninit;
8+
89
use citro3d::render::{ClearFlags, Target};
9-
use citro3d::{include_aligned_bytes, shader};
10+
use citro3d::{attrib, buffer, shader};
1011
use citro3d_sys::C3D_Mtx;
1112
use ctru::prelude::*;
1213
use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D};
1314

14-
use std::ffi::CStr;
15-
use std::mem::MaybeUninit;
16-
1715
#[repr(C)]
1816
#[derive(Copy, Clone)]
1917
struct Vec3 {
@@ -50,8 +48,7 @@ static VERTICES: &[Vertex] = &[
5048
},
5149
];
5250

53-
static SHADER_BYTES: &[u8] =
54-
include_aligned_bytes!(concat!(env!("OUT_DIR"), "/examples/assets/vshader.shbin"));
51+
static SHADER_BYTES: &[u8] = citro3d::include_shader!("assets/vshader.pica");
5552

5653
fn main() {
5754
ctru::use_panic_handler();

citro3d/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod shader;
88

99
use citro3d_sys::C3D_FrameDrawOn;
1010
pub use error::{Error, Result};
11+
pub use pica200::include_shader;
1112

1213
/// The single instance for using `citro3d`. This is the base type that an application
1314
/// should instantiate to use this library.

citro3d/src/shader.rs

-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
use std::error::Error;
88
use std::mem::MaybeUninit;
99

10-
// Macros get exported at the crate root, so no reason to document this module.
11-
// It still needs to be `pub` for the helper struct it exports.
12-
#[doc(hidden)]
13-
pub mod macros;
14-
1510
/// A PICA200 shader program. It may have one or both of:
1611
///
1712
/// * A vertex shader [`Library`]

citro3d/src/shader/macros.rs

-27
This file was deleted.

pica200/Cargo.toml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "pica200"
3+
version = "0.1.0"
4+
edition = "2021"
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.4.0", default-features = false }
13+
quote = "1.0.32"

pica200/build.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
}

pica200/src/lib.rs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// we're already nightly-only so might as well use unstable proc macro APIs.
2+
#![feature(proc_macro_span)]
3+
4+
use std::path::PathBuf;
5+
use std::{env, process};
6+
7+
use litrs::StringLit;
8+
use quote::quote;
9+
10+
/// Compiles the given PICA200 shader using [`picasso`](https://github.com/devkitPro/picasso)
11+
/// and returns the compiled bytes directly as a `&[u8]` slice.
12+
///
13+
/// This is similar to the standard library's [`include_bytes!`] macro, for which
14+
/// file paths are relative to the source file where the macro is invoked.
15+
///
16+
/// The compiled shader binary will be saved in the caller's `$OUT_DIR`.
17+
///
18+
/// # Errors
19+
///
20+
/// This macro will fail to compile if the input is not a single string literal.
21+
/// In other words, inputs like `concat!("foo", "/bar")` are not supported.
22+
///
23+
/// # Example
24+
///
25+
/// ```no_run
26+
/// # use pica200::include_shader;
27+
/// static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica");
28+
/// ```
29+
#[proc_macro]
30+
pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
31+
let tokens: Vec<_> = input.into_iter().collect();
32+
33+
if tokens.len() != 1 {
34+
let msg = format!("expected exactly one input token, got {}", tokens.len());
35+
return quote! { compile_error!(#msg) }.into();
36+
}
37+
38+
let string_lit = match StringLit::try_from(&tokens[0]) {
39+
// Error if the token is not a string literal
40+
Err(e) => return e.to_compile_error(),
41+
Ok(lit) => lit,
42+
};
43+
44+
// The cwd can change depending on whether this is running in a doctest or not:
45+
// https://users.rust-lang.org/t/which-directory-does-a-proc-macro-run-from/71917
46+
//
47+
// But the span's `source_file()` seems to always be relative to the cwd.
48+
let cwd = env::current_dir().expect("unable to determine working directory");
49+
let invoking_source_file = tokens[0].span().source_file().path();
50+
let invoking_source_dir = invoking_source_file
51+
.parent()
52+
.expect("unable to find parent directory of invoking source file");
53+
54+
// By joining these three pieces, we arrive at approximately the same behavior as `include_bytes!`
55+
let shader_source_file = cwd.join(invoking_source_dir).join(string_lit.value());
56+
let shader_out_file = shader_source_file.with_extension("shbin");
57+
58+
let Some(shader_out_file) = shader_out_file.file_name() else {
59+
let msg = format!("invalid input file name {shader_source_file:?}");
60+
return quote! { compile_error!(#msg) }.into();
61+
};
62+
63+
let out_dir = PathBuf::from(env!("OUT_DIR"));
64+
let out_path = out_dir.join(shader_out_file);
65+
66+
let devkitpro = PathBuf::from(env!("DEVKITPRO"));
67+
68+
let output = process::Command::new(devkitpro.join("tools/bin/picasso"))
69+
.arg("--out")
70+
.args([&out_path, &shader_source_file])
71+
.output()
72+
.unwrap();
73+
74+
match output.status.code() {
75+
Some(0) => {}
76+
code => {
77+
let code = code.map_or_else(|| String::from("unknown"), |c| c.to_string());
78+
79+
let msg = format!(
80+
"failed to compile shader {shader_source_file:?}: exit status {code}: {}",
81+
String::from_utf8_lossy(&output.stderr),
82+
);
83+
84+
return quote! { compile_error!(#msg) }.into();
85+
}
86+
}
87+
88+
let bytes = std::fs::read(out_path).unwrap();
89+
let source_file_path = shader_source_file.to_string_lossy();
90+
91+
let result = quote! {
92+
{
93+
// ensure the source is re-evaluted if the input file changes
94+
const _SOURCE: &[u8] = include_bytes! ( #source_file_path );
95+
96+
// https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049/2
97+
#[repr(C)]
98+
struct AlignedAsU32<Bytes: ?Sized> {
99+
_align: [u32; 0],
100+
bytes: Bytes,
101+
}
102+
103+
// this assignment is made possible by CoerceUnsized
104+
const ALIGNED: &AlignedAsU32<[u8]> = &AlignedAsU32 {
105+
_align: [],
106+
// emits a token stream like `[10u8, 11u8, ... ]`
107+
bytes: [ #(#bytes),* ]
108+
};
109+
110+
&ALIGNED.bytes
111+
}
112+
};
113+
114+
result.into()
115+
}

0 commit comments

Comments
 (0)