|
| 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