diff --git a/agb-image-converter/Cargo.lock b/agb-image-converter/Cargo.lock index 80b05ad7..a0dfd7c4 100644 --- a/agb-image-converter/Cargo.lock +++ b/agb-image-converter/Cargo.lock @@ -13,7 +13,11 @@ name = "agb_image_converter" version = "0.4.0" dependencies = [ "image", - "typed-builder", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", ] [[package]] @@ -166,6 +170,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.73" @@ -178,14 +202,12 @@ dependencies = [ ] [[package]] -name = "typed-builder" -version = "0.9.0" +name = "toml" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345426c7406aa355b60c5007c79a2d1f5b605540072795222f17f6443e6a9c6f" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ - "proc-macro2", - "quote", - "syn", + "serde", ] [[package]] diff --git a/agb-image-converter/Cargo.toml b/agb-image-converter/Cargo.toml index 8e6889c4..8add75bb 100644 --- a/agb-image-converter/Cargo.toml +++ b/agb-image-converter/Cargo.toml @@ -6,6 +6,13 @@ edition = "2018" license = "MPL-2.0" description = "Library for converting graphics for use on the Game Boy Advance" +[lib] +proc-macro = true + [dependencies] image = { version = "0.23.14", default-features = false, features = [ "png", "bmp" ] } -typed-builder = "0.9.0" \ No newline at end of file +toml = "0.5.8" +serde = { version = "1.0", features = ["derive"] } +syn = "1.0.73" +proc-macro2 = "1.0.27" +quote = "1.0.9" \ No newline at end of file diff --git a/agb-image-converter/src/bin/convert.rs b/agb-image-converter/src/bin/convert.rs deleted file mode 100644 index 366fd492..00000000 --- a/agb-image-converter/src/bin/convert.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::env; - -use agb_image_converter::{convert_image, ImageConverterConfig, TileSize}; - -fn main() { - let args: Vec<_> = env::args().collect(); - - let file_path = &args[1]; - let output_path = &args[2]; - convert_image( - ImageConverterConfig::builder() - .tile_size(TileSize::Tile8) - .input_image(file_path.into()) - .output_file(output_path.into()) - .build(), - ); -} diff --git a/agb-image-converter/src/config.rs b/agb-image-converter/src/config.rs new file mode 100644 index 00000000..1c2c62da --- /dev/null +++ b/agb-image-converter/src/config.rs @@ -0,0 +1,105 @@ +use serde::Deserialize; +use std::collections::HashMap; +use std::fs; + +use crate::{Colour, TileSize}; + +pub(crate) fn parse(filename: &str) -> Box { + let config_toml = + fs::read_to_string(filename).unwrap_or_else(|_| panic!("Failed to read file {}", filename)); + + let config: ConfigV1 = toml::from_str(&config_toml).expect("Failed to parse file"); + + if config.version != "1.0" { + panic!( + "Expected version of {} to be 1.0, got {}", + filename, config.version + ); + } + + Box::new(config) +} + +pub(crate) trait Config { + fn crate_prefix(&self) -> String; + fn images(&self) -> HashMap; +} + +pub(crate) trait Image { + fn filename(&self) -> String; + fn transparent_colour(&self) -> Option; + fn tilesize(&self) -> TileSize; +} + +#[derive(Deserialize)] +pub struct ConfigV1 { + version: String, + crate_prefix: Option, + + image: HashMap, +} + +impl Config for ConfigV1 { + fn crate_prefix(&self) -> String { + self.crate_prefix + .clone() + .unwrap_or_else(|| "agb".to_owned()) + } + + fn images(&self) -> HashMap { + self.image + .iter() + .map(|(filename, image)| (filename.clone(), image as &dyn Image)) + .collect() + } +} + +#[derive(Deserialize)] +pub struct ImageV1 { + filename: String, + transparent_colour: Option, + tile_size: TileSizeV1, +} + +impl Image for ImageV1 { + fn filename(&self) -> String { + self.filename.clone() + } + + fn transparent_colour(&self) -> Option { + if let Some(colour) = &self.transparent_colour { + if colour.len() != 6 { + panic!("Expected colour to be 6 characters, got {}", colour); + } + + let r = u8::from_str_radix(&colour[0..2], 16).unwrap(); + let g = u8::from_str_radix(&colour[2..4], 16).unwrap(); + let b = u8::from_str_radix(&colour[4..6], 16).unwrap(); + + return Some(Colour::from_rgb(r, g, b)); + } + + None + } + + fn tilesize(&self) -> TileSize { + self.tile_size.into() + } +} + +#[derive(Deserialize, Clone, Copy)] +pub enum TileSizeV1 { + #[serde(rename = "8x8")] + Tile8, + #[serde(rename = "16x16")] + Tile16, +} + +impl From for TileSize { + fn from(item: TileSizeV1) -> Self { + match item { + TileSizeV1::Tile8 => TileSize::Tile8, + TileSizeV1::Tile16 => TileSize::Tile16, + } + } +} diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index e13ce861..02eddcf4 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -1,20 +1,22 @@ -use std::fs::File; -use std::io::BufWriter; -use std::path::PathBuf; +use proc_macro::TokenStream; +use syn::parse_macro_input; -use typed_builder::TypedBuilder; +use std::path::Path; + +use quote::{format_ident, quote}; mod colour; +mod config; mod image_loader; mod palette16; mod rust_generator; use image_loader::Image; -pub use colour::Colour; +use colour::Colour; #[derive(Debug, Clone, Copy)] -pub enum TileSize { +pub(crate) enum TileSize { Tile8, Tile16, } @@ -28,40 +30,69 @@ impl TileSize { } } -#[derive(TypedBuilder)] -pub struct ImageConverterConfig { - #[builder(default, setter(strip_option))] - transparent_colour: Option, - tile_size: TileSize, - input_image: PathBuf, - output_file: PathBuf, +#[proc_macro] +pub fn include_gfx(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::LitStr); - #[builder(default, setter(strip_option))] - crate_prefix: Option, + let filename = input.value(); + + let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); + let path = Path::new(&root).join(&*filename); + let parent = path + .parent() + .expect("Expected a parent directory for the path"); + + let config = config::parse(&path.to_string_lossy()); + + let module_name = format_ident!( + "{}", + path.file_stem() + .expect("Expected a file stem") + .to_string_lossy() + ); + let include_path = path.to_string_lossy(); + + let images = config.images(); + let image_code = images.iter().map(|(image_name, &image)| { + convert_image(image, parent, &image_name, &config.crate_prefix()) + }); + + let module = quote! { + pub mod #module_name { + const _: &[u8] = include_bytes!(#include_path); + + #(#image_code)* + } + }; + + TokenStream::from(module) } -pub fn convert_image(settings: ImageConverterConfig) { - let image = Image::load_from_file(&settings.input_image); +fn convert_image( + settings: &dyn config::Image, + parent: &Path, + variable_name: &str, + crate_prefix: &str, +) -> proc_macro2::TokenStream { + let image_filename = &parent.join(&settings.filename()); + let image = Image::load_from_file(image_filename); - let tile_size = settings.tile_size.to_size(); + let tile_size = settings.tilesize().to_size(); if image.width % tile_size != 0 || image.height % tile_size != 0 { panic!("Image size not a multiple of tile size"); } let optimiser = optimiser_for_image(&image, tile_size); - let optimisation_results = optimiser.optimise_palettes(settings.transparent_colour); - - let output_file = File::create(&settings.output_file).expect("Failed to create file"); - let mut writer = BufWriter::new(output_file); + let optimisation_results = optimiser.optimise_palettes(settings.transparent_colour()); rust_generator::generate_code( - &mut writer, + variable_name, &optimisation_results, &image, - settings.tile_size, - settings.crate_prefix.unwrap_or("agb".to_owned()), + &image_filename.to_string_lossy(), + settings.tilesize(), + crate_prefix.to_owned(), ) - .expect("Failed to write data"); } fn optimiser_for_image(image: &Image, tile_size: usize) -> palette16::Palette16Optimiser { diff --git a/agb-image-converter/src/rust_generator.rs b/agb-image-converter/src/rust_generator.rs index 27c49021..f3fafa83 100644 --- a/agb-image-converter/src/rust_generator.rs +++ b/agb-image-converter/src/rust_generator.rs @@ -1,97 +1,87 @@ -use std::io; -use std::io::Write; - use crate::image_loader::Image; use crate::palette16::Palette16OptimisationResults; use crate::TileSize; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use std::iter; + pub(crate) fn generate_code( - output: &mut dyn Write, + output_variable_name: &str, results: &Palette16OptimisationResults, image: &Image, + image_filename: &str, tile_size: TileSize, crate_prefix: String, -) -> io::Result<()> { - writeln!( - output, - "pub const PALETTE_DATA: &[{}::display::palette16::Palette16] = &[", - crate_prefix, - )?; +) -> TokenStream { + let crate_prefix = format_ident!("{}", crate_prefix); + let output_variable_name = format_ident!("{}", output_variable_name); - for palette in &results.optimised_palettes { - write!( - output, - " {}::display::palette16::Palette16::new([", - crate_prefix - )?; + let palette_data = results.optimised_palettes.iter().map(|palette| { + let colours = palette + .clone() + .into_iter() + .map(|colour| colour.to_rgb15()) + .chain(iter::repeat(0)) + .take(16) + .map(|colour| colour as u16); - for colour in palette.clone() { - write!(output, "0x{:08x}, ", colour.to_rgb15())?; + quote! { + #crate_prefix::display::palette16::Palette16::new([ + #(#colours),* + ]) } - - for _ in palette.clone().into_iter().len()..16 { - write!(output, "0x00000000, ")?; - } - - writeln!(output, "]),")?; - } - - writeln!(output, "];")?; - writeln!(output)?; - - writeln!(output, "pub const TILE_DATA: &[u32] = &[",)?; + }); let tile_size = tile_size.to_size(); let tiles_x = image.width / tile_size; let tiles_y = image.height / tile_size; + let mut tile_data = vec![]; + for y in 0..tiles_y { for x in 0..tiles_x { let palette_index = results.assignments[y * tiles_x + x]; let palette = &results.optimised_palettes[palette_index]; - writeln!( - output, - " /* {}, {} (palette index {}) */", - x, y, palette_index - )?; - + for inner_y in 0..tile_size / 8 { - write!(output, " ")?; - for inner_x in 0..tile_size / 8 { - for j in inner_y * 8..inner_y * 8 + 8 { - write!(output, "0x")?; - + for j in inner_y * 8..inner_y * 8 + 8 { for i in (inner_x * 8..inner_x * 8 + 8).rev() { let colour = image.colour(x * tile_size + i, y * tile_size + j); - let colour_index = palette.colour_index(colour); - - write!(output, "{:x}", colour_index)?; + tile_data.push(palette.colour_index(colour)); } - - write!(output, ", ")?; } } } - - writeln!(output)?; } } - writeln!(output, "];")?; - writeln!(output)?; + let tile_data = tile_data.chunks(8) + .map(|chunk| chunk.iter().fold(0u32, |acc, &x| (acc << 4) | (x as u32))); - write!(output, "pub const PALETTE_ASSIGNMENT: &[u8] = &[")?; + let assignments = results.assignments.iter().map(|&x| x as u8); - for (i, assignment) in results.assignments.iter().enumerate() { - if i % 16 == 0 { - write!(output, "\n ")?; - } - write!(output, "{}, ", assignment)?; + quote! { + #[allow(non_upper_case_globals)] + pub const #output_variable_name: #crate_prefix::display::tile_data::TileData = { + const _: &[u8] = include_bytes!(#image_filename); + + const PALETTE_DATA: &[#crate_prefix::display::palette16::Palette16] = &[ + #(#palette_data),* + ]; + + const TILE_DATA: &[u32] = &[ + #(#tile_data),* + ]; + + const PALETTE_ASSIGNMENT: &[u8] = &[ + #(#assignments),* + ]; + + #crate_prefix::display::tile_data::TileData::new(PALETTE_DATA, TILE_DATA, PALETTE_ASSIGNMENT) + }; } - - writeln!(output, "\n];")?; - - Ok(()) } diff --git a/agb/Cargo.lock b/agb/Cargo.lock index d3189c30..67d021c9 100644 --- a/agb/Cargo.lock +++ b/agb/Cargo.lock @@ -19,11 +19,13 @@ dependencies = [ [[package]] name = "agb_image_converter" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c48ea2b7f5bb3c4605234f6355218401a840055ce703602b8266076a1e6f30" dependencies = [ "image", - "typed-builder", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", ] [[package]] @@ -176,6 +178,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.73" @@ -188,14 +210,12 @@ dependencies = [ ] [[package]] -name = "typed-builder" -version = "0.9.0" +name = "toml" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345426c7406aa355b60c5007c79a2d1f5b605540072795222f17f6443e6a9c6f" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ - "proc-macro2", - "quote", - "syn", + "serde", ] [[package]] diff --git a/agb/Cargo.toml b/agb/Cargo.toml index a48fc8ef..887cfb7e 100644 --- a/agb/Cargo.toml +++ b/agb/Cargo.toml @@ -18,9 +18,7 @@ lto = true [dependencies] bitflags = "1.2" - -[build-dependencies] -agb_image_converter = "0.4.0" +agb_image_converter = { version = "0.4.0", path = "../agb-image-converter" } [package.metadata.docs.rs] default-target = "thumbv6m-none-eabi" diff --git a/agb/build.rs b/agb/build.rs index e0b833cc..217ba164 100644 --- a/agb/build.rs +++ b/agb/build.rs @@ -1,5 +1,3 @@ -use agb_image_converter::{convert_image, Colour, ImageConverterConfig, TileSize}; - fn main() { println!("cargo:rerun-if-changed=crt0.s"); println!("cargo:rerun-if-changed=gba_mb.ld"); @@ -24,14 +22,4 @@ fn main() { } println!("cargo:rustc-link-search={}", out_dir); - - convert_image( - ImageConverterConfig::builder() - .transparent_colour(Colour::from_rgb(1, 1, 1)) - .tile_size(TileSize::Tile8) - .input_image("gfx/test_logo.png".into()) - .output_file(format!("{}/test_logo.rs", out_dir).into()) - .crate_prefix("crate".to_owned()) - .build(), - ); } diff --git a/agb/examples/test_logo.rs b/agb/examples/test_logo.rs index 888e04d5..6eddc02b 100644 --- a/agb/examples/test_logo.rs +++ b/agb/examples/test_logo.rs @@ -10,19 +10,7 @@ pub fn main() -> ! { let mut gba = agb::Gba::new(); let mut gfx = gba.display.video.tiled0(); - gfx.set_background_palettes(example_logo::PALETTE_DATA); - gfx.set_background_tilemap(0, example_logo::TILE_DATA); - - let mut back = gfx.get_background().unwrap(); - - let mut entries: [u16; 30 * 20] = [0; 30 * 20]; - for tile_id in 0..(30 * 20) { - let palette_entry = example_logo::PALETTE_ASSIGNMENT[tile_id as usize] as u16; - entries[tile_id as usize] = tile_id | (palette_entry << 12); - } - - back.draw_full_map(&entries, (30_u32, 20_u32).into(), 0); - back.show(); + example_logo::display_logo(&mut gfx); loop {} } diff --git a/agb/gfx/agb_logo.toml b/agb/gfx/agb_logo.toml new file mode 100644 index 00000000..eb1f3304 --- /dev/null +++ b/agb/gfx/agb_logo.toml @@ -0,0 +1,9 @@ +version = "1.0" + +# Only needed for within the agb crate +crate_prefix = "crate" + +[image.test_logo] +filename = "test_logo.png" +transparent_colour = "010101" +tile_size = "8x8" diff --git a/agb/src/display/example_logo.rs b/agb/src/display/example_logo.rs index 9adc2b3d..7b9c520d 100644 --- a/agb/src/display/example_logo.rs +++ b/agb/src/display/example_logo.rs @@ -1,22 +1,28 @@ -include!(concat!(env!("OUT_DIR"), "/test_logo.rs")); +use crate::display::tiled0::Tiled0; -#[test_case] -fn logo_display(gba: &mut crate::Gba) { - let mut gfx = gba.display.video.tiled0(); +crate::include_gfx!("gfx/agb_logo.toml"); - gfx.set_background_palettes(PALETTE_DATA); - gfx.set_background_tilemap(0, TILE_DATA); +pub fn display_logo(gfx: &mut Tiled0) { + gfx.set_background_palettes(agb_logo::test_logo.palettes); + gfx.set_background_tilemap(0, agb_logo::test_logo.tiles); let mut back = gfx.get_background().unwrap(); let mut entries: [u16; 30 * 20] = [0; 30 * 20]; for tile_id in 0..(30 * 20) { - let palette_entry = PALETTE_ASSIGNMENT[tile_id as usize] as u16; + let palette_entry = agb_logo::test_logo.palette_assignments[tile_id as usize] as u16; entries[tile_id as usize] = tile_id | (palette_entry << 12); } back.draw_full_map(&entries, (30_u32, 20_u32).into(), 0); back.show(); +} + +#[test_case] +fn logo_display(gba: &mut crate::Gba) { + let mut gfx = gba.display.video.tiled0(); + + display_logo(&mut gfx); crate::assert_image_output("gfx/test_logo.png"); } diff --git a/agb/src/display/mod.rs b/agb/src/display/mod.rs index afd81570..b3d68bad 100644 --- a/agb/src/display/mod.rs +++ b/agb/src/display/mod.rs @@ -16,6 +16,8 @@ pub mod example_logo; pub mod object; /// Palette type. pub mod palette16; +/// Data produced by agb-image-converter +pub mod tile_data; /// Graphics mode 0. Four regular backgrounds. pub mod tiled0; /// Syscall for waiting for vblank. diff --git a/agb/src/display/tile_data.rs b/agb/src/display/tile_data.rs new file mode 100644 index 00000000..a8698e82 --- /dev/null +++ b/agb/src/display/tile_data.rs @@ -0,0 +1,21 @@ +use crate::display::palette16::Palette16; + +pub struct TileData { + pub palettes: &'static [Palette16], + pub tiles: &'static [u32], + pub palette_assignments: &'static [u8], +} + +impl TileData { + pub const fn new( + palettes: &'static [Palette16], + tiles: &'static [u32], + palette_assignments: &'static [u8], + ) -> Self { + TileData { + palettes, + tiles, + palette_assignments, + } + } +} diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 286b0076..dc7a73ab 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -20,6 +20,8 @@ pub mod input; /// Implements sound output. pub mod sound; +pub use agb_image_converter::include_gfx; + mod bitarray; mod interrupt; mod memory_mapped;