From 7b6556b3814d3cbfcabec31988ed7b28ab5b812f Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 25 Sep 2024 11:33:51 +0100 Subject: [PATCH] Use a better optimisation method --- agb-image-converter/Cargo.toml | 1 + agb-image-converter/src/lib.rs | 8 +- agb-image-converter/src/palette16.rs | 137 ++++++++++++++------------- 3 files changed, 79 insertions(+), 67 deletions(-) diff --git a/agb-image-converter/Cargo.toml b/agb-image-converter/Cargo.toml index ce55029c..7e78a931 100644 --- a/agb-image-converter/Cargo.toml +++ b/agb-image-converter/Cargo.toml @@ -20,6 +20,7 @@ proc-macro2 = "1" quote = "1" asefile = "0.3.8" fontdue = "0.9" +pagination-packing = "2.1.0" [dev-dependencies] quickcheck = "1" diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index 0c31b95a..48e1a134 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -244,7 +244,9 @@ fn include_gfx_from_config( } } - let optimisation_results = optimiser.optimise_palettes(); + let optimisation_results = optimiser + .optimise_palettes() + .expect("Failed to optimised palettes"); let optimisation_results = palette256.extend_results(&optimisation_results); let mut image_code = vec![]; @@ -377,7 +379,9 @@ pub fn include_aseprite_inner(input: TokenStream) -> TokenStream { } } - let optimised_results = optimiser.optimise_palettes(); + let optimised_results = optimiser + .optimise_palettes() + .expect("Failed to optimise palettes"); let (palette_data, tile_data, assignments) = palette_tile_data(&optimised_results, &images); diff --git a/agb-image-converter/src/palette16.rs b/agb-image-converter/src/palette16.rs index 19592f8c..cc5f33c8 100644 --- a/agb-image-converter/src/palette16.rs +++ b/agb-image-converter/src/palette16.rs @@ -1,5 +1,5 @@ use crate::colour::Colour; -use std::collections::HashSet; +use std::{collections::HashSet, fmt}; const MAX_COLOURS: usize = 256; const MAX_COLOURS_PER_PALETTE: usize = 16; @@ -87,6 +87,20 @@ impl IntoIterator for Palette16 { } } +impl<'a, T> From for Palette16 +where + T: IntoIterator, +{ + fn from(value: T) -> Self { + let mut palette = Palette16::new(); + for colour in value.into_iter() { + palette.add_colour(*colour); + } + + palette + } +} + pub(crate) struct Palette16Optimiser { palettes: Vec, colours: Vec, @@ -125,28 +139,41 @@ impl Palette16Optimiser { } } - pub fn optimise_palettes(&self) -> Palette16OptimisationResults { - let mut assignments = vec![0; self.palettes.len()]; - let mut optimised_palettes = vec![]; - - let mut unsatisfied_palettes = self + pub fn optimise_palettes(&self) -> Result { + let palettes_to_optimise = self .palettes .iter() .cloned() - .collect::>(); + .map(|mut palette| { + // ensure each palette we're creating the covering for has the transparent colour in it + palette.add_colour( + self.transparent_colour + .unwrap_or_else(|| Colour::from_rgb(255, 0, 255, 0)), + ); + palette + }) + .collect::>() + .into_iter() + .map(|palette| palette.colours) + .collect::>(); - while !unsatisfied_palettes.is_empty() { - let palette = self.find_maximal_palette_for(&unsatisfied_palettes); + let packed_palettes = + pagination_packing::overload_and_remove::<_, _, Vec<_>>(&palettes_to_optimise, 16); - unsatisfied_palettes.retain(|test_palette| !test_palette.is_satisfied_by(&palette)); - - optimised_palettes.push(palette); - - if optimised_palettes.len() == MAX_COLOURS / MAX_COLOURS_PER_PALETTE { - panic!("Failed to find covering palettes"); - } + if packed_palettes.len() > 16 { + return Err(DoesNotFitError(packed_palettes.len())); } + let optimised_palettes = packed_palettes + .iter() + .map(|packed_palette| { + let colours = packed_palette.unique_symbols(&palettes_to_optimise); + Palette16::from(colours) + }) + .collect::>(); + + let mut assignments = vec![0; self.palettes.len()]; + for (i, overall_palette) in self.palettes.iter().enumerate() { assignments[i] = optimised_palettes .iter() @@ -154,60 +181,29 @@ impl Palette16Optimiser { .unwrap(); } - Palette16OptimisationResults { + Ok(Palette16OptimisationResults { optimised_palettes, assignments, transparent_colour: self.transparent_colour, - } + }) } +} - fn find_maximal_palette_for(&self, unsatisfied_palettes: &HashSet) -> Palette16 { - let mut palette = Palette16::new(); +pub struct DoesNotFitError(pub usize); - palette.add_colour( - self.transparent_colour - .unwrap_or_else(|| Colour::from_rgb(255, 0, 255, 0)), - ); +impl fmt::Display for DoesNotFitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Could not fit colours into palette, needed {} bins but can have at most 16", + self.0 + ) + } +} - loop { - let mut colour_usage = vec![0; MAX_COLOURS]; - let mut a_colour_is_used = false; - - for current_palette in unsatisfied_palettes { - if palette.union_length(current_palette) > MAX_COLOURS_PER_PALETTE { - continue; - } - - for colour in ¤t_palette.colours { - if palette.colours.contains(colour) { - continue; - } - - if let Some(colour_index) = self.colours.iter().position(|c| c == colour) { - colour_usage[colour_index] += 1; - a_colour_is_used = true; - } - } - } - - if !a_colour_is_used { - return palette; - } - - let best_index = colour_usage - .iter() - .enumerate() - .max_by(|(_, usage1), (_, usage2)| usage1.cmp(usage2)) - .unwrap() - .0; - - let best_colour = self.colours[best_index]; - - palette.add_colour(best_colour); - if palette.colours.len() == MAX_COLOURS_PER_PALETTE { - return palette; - } - } +impl fmt::Debug for DoesNotFitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") } } @@ -218,13 +214,24 @@ mod test { use super::*; quickcheck! { - fn less_than_256_colours_always_fits(palettes: Vec) -> () { + fn less_than_256_colours_always_fits(palettes: Vec) -> bool { let mut optimiser = Palette16Optimiser::new(None); for palette in palettes.clone().into_iter().take(16) { optimiser.add_palette(palette); } - optimiser.optimise_palettes(); + let Ok(optimisation_results) = optimiser.optimise_palettes() else { + return false + }; + + for (i, palette) in palettes.into_iter().take(16).enumerate() { + let optimised_palette = &optimisation_results.optimised_palettes[optimisation_results.assignments[i]]; + if !palette.is_satisfied_by(optimised_palette) { + return false; + } + } + + true } }