Switch to a proc macro for generating the image data

This commit is contained in:
Gwilym Kuiper 2021-07-21 22:07:09 +01:00
parent 47c5c0f86e
commit 8713f514be
12 changed files with 132 additions and 110 deletions

View file

@ -13,6 +13,7 @@ name = "agb_image_converter"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"image", "image",
"litrs",
"serde", "serde",
"toml", "toml",
"typed-builder", "typed-builder",
@ -88,6 +89,15 @@ dependencies = [
"png", "png",
] ]
[[package]]
name = "litrs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.3.7" version = "0.3.7"

View file

@ -6,8 +6,12 @@ edition = "2018"
license = "MPL-2.0" license = "MPL-2.0"
description = "Library for converting graphics for use on the Game Boy Advance" description = "Library for converting graphics for use on the Game Boy Advance"
[lib]
proc-macro = true
[dependencies] [dependencies]
image = { version = "0.23.14", default-features = false, features = [ "png", "bmp" ] } image = { version = "0.23.14", default-features = false, features = [ "png", "bmp" ] }
typed-builder = "0.9.0" typed-builder = "0.9.0"
toml = "0.5.8" toml = "0.5.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
litrs = "0.2.3"

View file

@ -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(),
);
}

View file

@ -4,8 +4,8 @@ use std::fs;
use crate::{TileSize, Colour}; use crate::{TileSize, Colour};
pub fn parse(filename: &str) -> Box<dyn Config> { pub(crate) fn parse(filename: &str) -> Box<dyn Config> {
let config_toml = fs::read_to_string(filename).expect("Failed to read file"); let config_toml = fs::read_to_string(filename).expect(&format!("Failed to read file {}", filename));
let config: ConfigV1 = toml::from_str(&config_toml).expect("Failed to parse file"); let config: ConfigV1 = toml::from_str(&config_toml).expect("Failed to parse file");
@ -16,12 +16,12 @@ pub fn parse(filename: &str) -> Box<dyn Config> {
Box::new(config) Box::new(config)
} }
pub trait Config { pub(crate) trait Config {
fn crate_prefix(&self) -> String; fn crate_prefix(&self) -> String;
fn images(&self) -> HashMap<String, &dyn Image>; fn images(&self) -> HashMap<String, &dyn Image>;
} }
pub trait Image { pub(crate) trait Image {
fn filename(&self) -> String; fn filename(&self) -> String;
fn transparent_colour(&self) -> Option<Colour>; fn transparent_colour(&self) -> Option<Colour>;
fn tilesize(&self) -> TileSize; fn tilesize(&self) -> TileSize;
@ -85,12 +85,15 @@ impl Image for ImageV1 {
pub enum TileSizeV1 { pub enum TileSizeV1 {
#[serde(rename = "8x8")] #[serde(rename = "8x8")]
Tile8, Tile8,
#[serde(rename = "16x16")]
Tile16,
} }
impl Into<TileSize> for TileSizeV1 { impl Into<TileSize> for TileSizeV1 {
fn into(self) -> TileSize { fn into(self) -> TileSize {
match self { match self {
TileSizeV1::Tile8 => TileSize::Tile8, TileSizeV1::Tile8 => TileSize::Tile8,
TileSizeV1::Tile16 => TileSize::Tile16,
} }
} }
} }

View file

@ -1,8 +1,9 @@
use std::fs::File; use proc_macro::TokenStream;
use std::io::BufWriter; use litrs::StringLit;
use std::path::PathBuf;
use typed_builder::TypedBuilder; use std::path::Path;
use std::convert::TryFrom;
use std::fmt::Write;
mod colour; mod colour;
mod image_loader; mod image_loader;
@ -12,10 +13,10 @@ mod config;
use image_loader::Image; use image_loader::Image;
pub use colour::Colour; use colour::Colour;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum TileSize { pub(crate) enum TileSize {
Tile8, Tile8,
Tile16, Tile16,
} }
@ -29,41 +30,62 @@ impl TileSize {
} }
} }
#[derive(TypedBuilder)] #[proc_macro]
pub struct ImageConverterConfig { pub fn include_gfx(input: TokenStream) -> TokenStream {
#[builder(default, setter(strip_option))] let first_token = input.into_iter().next().expect("no input");
transparent_colour: Option<Colour>,
tile_size: TileSize,
input_image: PathBuf,
output_file: PathBuf,
#[builder(default, setter(strip_option))] let filename = match StringLit::try_from(first_token) {
crate_prefix: Option<String>, Err(e) => return e.to_compile_error(),
Ok(filename) => filename.into_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 = path.file_stem().expect("Expected a file stem");
let mut output = String::new();
writeln!(&mut output, "mod {} {{", module_name.to_string_lossy()).unwrap();
writeln!(&mut output, "const _: &[u8] = include_bytes!(\"{}\");", path.to_string_lossy()).unwrap();
for (image_name, image) in config.images() {
writeln!(&mut output, "{}", convert_image(image, parent, &image_name, &config.crate_prefix())).unwrap();
}
writeln!(&mut output, "}}").unwrap();
output.parse().expect("Failed to generate valid rust code")
} }
pub fn convert_image(settings: ImageConverterConfig) { fn convert_image(settings: &dyn config::Image, parent: &Path, variable_name: &str, crate_prefix: &str) -> String {
let image = Image::load_from_file(&settings.input_image); 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 { if image.width % tile_size != 0 || image.height % tile_size != 0 {
panic!("Image size not a multiple of tile size"); panic!("Image size not a multiple of tile size");
} }
let optimiser = optimiser_for_image(&image, tile_size); let optimiser = optimiser_for_image(&image, tile_size);
let optimisation_results = optimiser.optimise_palettes(settings.transparent_colour); 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 = String::new();
let mut writer = BufWriter::new(output_file);
rust_generator::generate_code( rust_generator::generate_code(
&mut writer, &mut writer,
"test", variable_name,
&optimisation_results, &optimisation_results,
&image, &image,
settings.tile_size, &image_filename.to_string_lossy(),
settings.crate_prefix.unwrap_or("agb".to_owned()), settings.tilesize(),
) crate_prefix.to_owned(),
.expect("Failed to write data"); );
writer
} }
fn optimiser_for_image(image: &Image, tile_size: usize) -> palette16::Palette16Optimiser { fn optimiser_for_image(image: &Image, tile_size: usize) -> palette16::Palette16Optimiser {

View file

@ -1,5 +1,4 @@
use std::io; use std::fmt::Write;
use std::io::Write;
use crate::image_loader::Image; use crate::image_loader::Image;
use crate::palette16::Palette16OptimisationResults; use crate::palette16::Palette16OptimisationResults;
@ -10,40 +9,43 @@ pub(crate) fn generate_code(
output_variable_name: &str, output_variable_name: &str,
results: &Palette16OptimisationResults, results: &Palette16OptimisationResults,
image: &Image, image: &Image,
image_filename: &str,
tile_size: TileSize, tile_size: TileSize,
crate_prefix: String, crate_prefix: String,
) -> io::Result<()> { ) {
writeln!(output, "#[allow(non_upper_case_globals)]")?; writeln!(output, "#[allow(non_upper_case_globals)]").unwrap();
writeln!(output, "pub const {}: {}::display::tile_data::TileData = {{", output_variable_name, crate_prefix)?; writeln!(output, "pub const {}: {}::display::tile_data::TileData = {{", output_variable_name, crate_prefix).unwrap();
writeln!(output, "const _: &[u8] = include_bytes!(\"{}\");", image_filename).unwrap();
writeln!( writeln!(
output, output,
"const PALETTE_DATA: &[{}::display::palette16::Palette16] = &[", "const PALETTE_DATA: &[{}::display::palette16::Palette16] = &[",
crate_prefix, crate_prefix,
)?; ).unwrap();
for palette in &results.optimised_palettes { for palette in &results.optimised_palettes {
write!( write!(
output, output,
" {}::display::palette16::Palette16::new([", " {}::display::palette16::Palette16::new([",
crate_prefix crate_prefix
)?; ).unwrap();
for colour in palette.clone() { for colour in palette.clone() {
write!(output, "0x{:08x}, ", colour.to_rgb15())?; write!(output, "0x{:08x}, ", colour.to_rgb15()).unwrap();
} }
for _ in palette.clone().into_iter().len()..16 { for _ in palette.clone().into_iter().len()..16 {
write!(output, "0x00000000, ")?; write!(output, "0x00000000, ").unwrap();
} }
writeln!(output, "]),")?; writeln!(output, "]),").unwrap();
} }
writeln!(output, "];")?; writeln!(output, "];").unwrap();
writeln!(output)?; writeln!(output).unwrap();
writeln!(output, "const TILE_DATA: &[u32] = &[",)?; writeln!(output, "const TILE_DATA: &[u32] = &[",).unwrap();
let tile_size = tile_size.to_size(); let tile_size = tile_size.to_size();
@ -58,46 +60,44 @@ pub(crate) fn generate_code(
output, output,
" /* {}, {} (palette index {}) */", " /* {}, {} (palette index {}) */",
x, y, palette_index x, y, palette_index
)?; ).unwrap();
for inner_y in 0..tile_size / 8 { for inner_y in 0..tile_size / 8 {
write!(output, " ")?; write!(output, " ").unwrap();
for inner_x in 0..tile_size / 8 { for inner_x in 0..tile_size / 8 {
for j in inner_y * 8..inner_y * 8 + 8 { for j in inner_y * 8..inner_y * 8 + 8 {
write!(output, "0x")?; write!(output, "0x").unwrap();
for i in (inner_x * 8..inner_x * 8 + 8).rev() { 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 = image.colour(x * tile_size + i, y * tile_size + j);
let colour_index = palette.colour_index(colour); let colour_index = palette.colour_index(colour);
write!(output, "{:x}", colour_index)?; write!(output, "{:x}", colour_index).unwrap();
} }
write!(output, ", ")?; write!(output, ", ").unwrap();
} }
} }
} }
writeln!(output)?; writeln!(output).unwrap();
} }
} }
writeln!(output, "];")?; writeln!(output, "];").unwrap();
writeln!(output)?; writeln!(output).unwrap();
write!(output, "const PALETTE_ASSIGNMENT: &[u8] = &[")?; write!(output, "const PALETTE_ASSIGNMENT: &[u8] = &[").unwrap();
for (i, assignment) in results.assignments.iter().enumerate() { for (i, assignment) in results.assignments.iter().enumerate() {
if i % 16 == 0 { if i % 16 == 0 {
write!(output, "\n ")?; write!(output, "\n ").unwrap();
} }
write!(output, "{}, ", assignment)?; write!(output, "{}, ", assignment).unwrap();
} }
writeln!(output, "\n];")?; writeln!(output, "\n];").unwrap();
writeln!(output, "{}::display::tile_data::TileData::new(PALETTE_DATA, TILE_DATA, PALETTE_ASSIGNMENT)\n}};", crate_prefix)?; writeln!(output, "{}::display::tile_data::TileData::new(PALETTE_DATA, TILE_DATA, PALETTE_ASSIGNMENT)\n}};", crate_prefix).unwrap();
Ok(())
} }

10
agb/Cargo.lock generated
View file

@ -21,6 +21,7 @@ name = "agb_image_converter"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"image", "image",
"litrs",
"serde", "serde",
"toml", "toml",
"typed-builder", "typed-builder",
@ -96,6 +97,15 @@ dependencies = [
"png", "png",
] ]
[[package]]
name = "litrs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.3.7" version = "0.3.7"

View file

@ -18,8 +18,6 @@ lto = true
[dependencies] [dependencies]
bitflags = "1.2" bitflags = "1.2"
[build-dependencies]
agb_image_converter = { version = "0.4.0", path = "../agb-image-converter" } agb_image_converter = { version = "0.4.0", path = "../agb-image-converter" }
[package.metadata.docs.rs] [package.metadata.docs.rs]

View file

@ -1,5 +1,3 @@
use agb_image_converter::{convert_image, Colour, ImageConverterConfig, TileSize};
fn main() { fn main() {
println!("cargo:rerun-if-changed=crt0.s"); println!("cargo:rerun-if-changed=crt0.s");
println!("cargo:rerun-if-changed=gba_mb.ld"); println!("cargo:rerun-if-changed=gba_mb.ld");
@ -24,14 +22,4 @@ fn main() {
} }
println!("cargo:rustc-link-search={}", out_dir); 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(),
);
} }

View file

@ -10,19 +10,7 @@ pub fn main() -> ! {
let mut gba = agb::Gba::new(); let mut gba = agb::Gba::new();
let mut gfx = gba.display.video.tiled0(); let mut gfx = gba.display.video.tiled0();
gfx.set_background_palettes(example_logo::test.palettes); example_logo::display_logo(&mut gfx);
gfx.set_background_tilemap(0, example_logo::test.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 = example_logo::test.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();
loop {} loop {}
} }

9
agb/gfx/agb_logo.toml Normal file
View file

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

View file

@ -1,22 +1,29 @@
include!(concat!(env!("OUT_DIR"), "/test_logo.rs")); use agb_image_converter::include_gfx;
use crate::display::tiled0::Tiled0;
#[test_case] include_gfx!("gfx/agb_logo.toml");
fn logo_display(gba: &mut crate::Gba) {
let mut gfx = gba.display.video.tiled0();
gfx.set_background_palettes(test.palettes); pub fn display_logo(gfx: &mut Tiled0) {
gfx.set_background_tilemap(0, test.tiles); 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 back = gfx.get_background().unwrap();
let mut entries: [u16; 30 * 20] = [0; 30 * 20]; let mut entries: [u16; 30 * 20] = [0; 30 * 20];
for tile_id in 0..(30 * 20) { for tile_id in 0..(30 * 20) {
let palette_entry = test.palette_assignments[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); entries[tile_id as usize] = tile_id | (palette_entry << 12);
} }
back.draw_full_map(&entries, (30_u32, 20_u32).into(), 0); back.draw_full_map(&entries, (30_u32, 20_u32).into(), 0);
back.show(); 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"); crate::assert_image_output("gfx/test_logo.png");
} }