shithub: rgbds

Download patch

ref: 3fa1854332735f4817bd43eee748b44cdc811c06
parent: 6e406b22bbe3e5f66193a94364114aafacc8b8da
author: ISSOtm <[email protected]>
date: Fri Mar 4 18:49:55 EST 2022

Implement enough functionality to compile & match pokecrystal

--- a/Makefile
+++ b/Makefile
@@ -110,6 +110,7 @@
 	src/gfx/pal_packing.o \
 	src/gfx/pal_sorting.o \
 	src/gfx/proto_palette.o \
+	src/gfx/rgba.o \
 	src/extern/getopt.o \
 	src/error.o
 
--- a/include/gfx/convert.hpp
+++ b/include/gfx/convert.hpp
@@ -9,59 +9,6 @@
 #ifndef RGBDS_GFX_CONVERT_HPP
 #define RGBDS_GFX_CONVERT_HPP
 
-#include <assert.h>
-#include <stdint.h>
-
-#include "gfx/main.hpp"
-
-struct Rgba {
-	uint8_t red;
-	uint8_t green;
-	uint8_t blue;
-	uint8_t alpha;
-
-	Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
-	Rgba(uint32_t rgba) : red(rgba), green(rgba >> 8), blue(rgba >> 16), alpha(rgba >> 24) {}
-
-	operator uint32_t() const { return toCSS(); }
-	/**
-	 * Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
-	 * representation
-	 */
-	uint32_t toCSS() const {
-		auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
-		return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
-	}
-	bool operator!=(Rgba const &other) const {
-		return static_cast<uint32_t>(*this) != static_cast<uint32_t>(other);
-	}
-
-	bool isGray() const { return red == green && green == blue; }
-
-	/**
-	 * CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
-	 * Since the rest of the bits don't matter then, we return 0x8000 exactly.
-	 */
-	static constexpr uint16_t transparent = 0b1'00000'00000'00000;
-
-	/**
-	 * All alpha values strictly below this will be considered transparent
-	 */
-	static constexpr uint8_t opacity_threshold = 0xF0; // TODO: adjust this
-	/**
-	 * Computes the equivalent CGB color, respects the color curve depending on options
-	 */
-	uint16_t cgbColor() const {
-		if (alpha < opacity_threshold)
-			return transparent;
-		if (options.useColorCurve) {
-			assert(!"TODO");
-		} else {
-			return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
-		}
-	}
-};
-
 void process();
 
 #endif /* RGBDS_GFX_CONVERT_HPP */
--- a/include/gfx/main.hpp
+++ b/include/gfx/main.hpp
@@ -13,22 +13,33 @@
 #include <filesystem>
 #include <limits.h>
 #include <stdint.h>
+#include <vector>
 
 #include "helpers.h"
 
+#include "gfx/rgba.hpp"
+
 struct Options {
 	bool beVerbose = false; // -v
 	bool fixInput = false; // -f
-	bool columnMajor = false; // -h; whether to output the tilemap in columns instead of rows
+	bool columnMajor = false; // -Z, previously -h
 	bool allowMirroring = false; // -m
 	bool allowDedup = false; // -u
 	bool useColorCurve = false; // -C
 	uint8_t bitDepth = 2; // -d
 	uint64_t trim = 0; // -x
-	uint8_t nbPalettes = 8; // TODO
-	uint8_t nbColorsPerPal = 0; // TODO; 0 means "auto" = 1 << bitDepth;
-	std::array<uint8_t, 2> baseTileIDs{0, 0}; // TODO
-	std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // TODO
+	uint8_t nbPalettes = 8; // -n
+	uint8_t nbColorsPerPal = 0; // -s; 0 means "auto" = 1 << bitDepth;
+	enum {
+		NO_SPEC,
+		EXPLICIT,
+		EMBEDDED,
+	} palSpecType = NO_SPEC; // -c
+	std::vector<std::array<Rgba, 4>> palSpec{};
+	std::array<uint16_t, 2> unitSize{1, 1}; // -u (in tiles)
+	std::array<uint32_t, 4> inputSlice; // -L
+	std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
+	std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
 	std::filesystem::path tilemap{}; // -t, -T
 	std::filesystem::path attrmap{}; // -a, -A
 	std::filesystem::path palettes{}; // -p, -P
@@ -37,8 +48,8 @@
 
 	format_(printf, 2, 3) void verbosePrint(char const *fmt, ...) const;
 	uint8_t maxPalSize() const {
-		return nbColorsPerPal;
-	} // TODO: minus 1 when transparency is active
+		return nbColorsPerPal; // TODO: minus 1 when transparency is active
+	}
 };
 
 extern Options options;
@@ -53,6 +64,8 @@
 
 	void addColor(uint16_t color);
 	uint8_t indexOf(uint16_t color) const;
+	uint16_t &operator[](size_t index) { return colors[index]; }
+	uint16_t const &operator[](size_t index) const { return colors[index]; }
 
 	decltype(colors)::iterator begin();
 	decltype(colors)::iterator end();
--- a/include/gfx/pal_sorting.hpp
+++ b/include/gfx/pal_sorting.hpp
@@ -9,9 +9,14 @@
 #ifndef RGBDS_GFX_PAL_SORTING_HPP
 #define RGBDS_GFX_PAL_SORTING_HPP
 
+#include <array>
+#include <assert.h>
+#include <optional>
 #include <png.h>
 #include <vector>
 
+#include "gfx/rgba.hpp"
+
 class Palette;
 
 namespace sorting {
@@ -18,7 +23,8 @@
 
 void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
              png_byte *palAlpha);
-void grayscale(std::vector<Palette> &palettes);
+void grayscale(std::vector<Palette> &palettes,
+               std::array<std::optional<Rgba>, 0x8001> const &colors);
 void rgb(std::vector<Palette> &palettes);
 
 }
--- /dev/null
+++ b/include/gfx/rgba.hpp
@@ -1,0 +1,58 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_RGBA_HPP
+#define RGBDS_GFX_RGBA_HPP
+
+#include <stdint.h>
+
+struct Rgba {
+	uint8_t red;
+	uint8_t green;
+	uint8_t blue;
+	uint8_t alpha;
+
+	Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
+	/**
+	 * Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
+	 */
+	explicit Rgba(uint32_t rgba = 0)
+		: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
+
+	/**
+	 * Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
+	 * representation
+	 */
+	uint32_t toCSS() const {
+		auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
+		return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
+	}
+	friend bool operator!=(Rgba const &lhs, Rgba const &rhs) { return lhs.toCSS() != rhs.toCSS(); }
+
+	/**
+	 * CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
+	 * Since the rest of the bits don't matter then, we return 0x8000 exactly.
+	 */
+	static constexpr uint16_t transparent = 0b1'00000'00000'00000;
+
+	/**
+	 * All alpha values strictly below this will be considered transparent
+	 */
+	static constexpr uint8_t opacity_threshold = 0xF0; // TODO: adjust this
+	// TODO: also a transparency threshold, and error out on "middle" values
+	bool isTransparent() const { return alpha < opacity_threshold; }
+	/**
+	 * Computes the equivalent CGB color, respects the color curve depending on options
+	 */
+	uint16_t cgbColor() const;
+
+	bool isGray() const { return red == green && green == blue; }
+	uint8_t grayIndex() const;
+};
+
+#endif /* RGBDS_GFX_RGBA_HPP */
--- a/man/rgbgfx.1
+++ b/man/rgbgfx.1
@@ -129,16 +129,25 @@
 Any unused entries will be ignored, and only the first entry is considered if there any duplicates.
 .Pq If you want a given color to appear more than once in a palette, you should specify the palettes explicitly instead using Fl TODO .
 .It
-Otherwise, if the PNG only contains shades of gray, they will be categorized into 4
+Otherwise, if the PNG only contains shades of gray, they will be categorized into as many
 .Dq bins
-.Pq white, light gray, dark gray, and black in this order ,
-and the palette is set to these four bins.
-(TODO: how does this interact with 1bpp? With more than 1 palette?)
-If more than one grey ends up in the same bin, the RGB method below is used instead.
+as there are colors per palette, and the palette is set to these bins.
+The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
+If two distinct grays end up in the same bin, the RGB method is used instead.
 .It
 If none of the above apply, colors are sorted from lightest to darkest.
-(This is what the old documentation claimed, but the definition of luminance that was used wasn't quite right.
-It is kept for compatibility.)
+.EQ
+delim $$
+.EN
+The definition of luminance that
+.Nm
+uses is
+.Do
+$2126 times red + 7152 times green + 722 times blue$
+.Dc .
+.EQ
+delim off
+.EN
 .El
 .Pp
 Note that the
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -75,6 +75,7 @@
     "gfx/pal_packing.cpp"
     "gfx/pal_sorting.cpp"
     "gfx/proto_palette.cpp"
+    "gfx/rgba.cpp"
     "extern/getopt.c"
     "error.c"
     )
--- a/src/gfx/convert.cpp
+++ b/src/gfx/convert.cpp
@@ -49,10 +49,15 @@
 		}
 	}
 
-	size_t size() const { return _colors.size(); }
+	size_t size() const {
+		return std::count_if(_colors.begin(), _colors.end(),
+		                     [](decltype(_colors)::value_type const &slot) {
+								 return slot.has_value() && !slot->isTransparent();
+							 });
+	}
+	decltype(_colors) const &raw() const { return _colors; }
 
 	auto begin() const { return _colors.begin(); }
-
 	auto end() const { return _colors.end(); }
 };
 
@@ -64,13 +69,12 @@
 
 	// These are cached for speed
 	uint32_t width, height;
-	DefaultInitVec<uint16_t> pixels;
+	std::vector<Rgba> pixels;
 	ImagePalette colors;
 	int colorType;
 	int nbColors;
 	png_colorp embeddedPal = nullptr;
 	png_bytep transparencyPal = nullptr;
-	bool isGrayOnly = true;
 
 	[[noreturn]] static void handleError(png_structp png, char const *msg) {
 		struct Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
@@ -110,11 +114,38 @@
 
 	uint32_t getHeight() const { return height; }
 
-	uint16_t &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
+	Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
 
-	uint16_t const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
+	Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
 
-	bool hasNonGray() const { return !isGrayOnly; }
+	bool isSuitableForGrayscale() const {
+		// Check that all of the grays don't fall into the same "bin"
+		if (colors.size() > options.maxPalSize()) { // Apply the Pigeonhole Principle
+			options.verbosePrint("Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n",
+			                     colors.size(), options.maxPalSize());
+			return false;
+		}
+		uint8_t bins = 0;
+		for (auto const &color : colors) {
+			if (color->isTransparent()) {
+				continue;
+			}
+			if (!color->isGray()) {
+				options.verbosePrint("Found non-gray color #%08x, not using grayscale sorting\n",
+				                     color->toCSS());
+				return false;
+			}
+			uint8_t mask = 1 << color->grayIndex();
+			if (bins & mask) { // Two in the same bin!
+				options.verbosePrint(
+					"Color #%08x conflicts with another one, not using grayscale sorting\n",
+					color->toCSS());
+				return false;
+			}
+			bins |= mask;
+		}
+		return true;
+	}
 
 	/**
 	 * Reads a PNG and notes all of its colors
@@ -276,8 +307,7 @@
 					Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
 
 					colors.registerColor(rgba);
-					pixel(x, y) = rgba.cgbColor();
-					isGrayOnly &= rgba.isGray();
+					pixel(x, y) = rgba;
 				}
 			}
 		} else {
@@ -299,8 +329,7 @@
 						Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
 
 						colors.registerColor(rgba);
-						pixel(x, y) = rgba.cgbColor();
-						isGrayOnly &= rgba.isGray();
+						pixel(x, y) = rgba;
 						ptr += 4;
 					}
 				}
@@ -307,8 +336,8 @@
 			}
 		}
 
-		png_read_end(png,
-		             nullptr); // We don't care about chunks after the image data (comments, etc.)
+		// We don't care about chunks after the image data (comments, etc.)
+		png_read_end(png, nullptr);
 	}
 
 	~Png() { png_destroy_read_struct(&png, &info, nullptr); }
@@ -330,7 +359,7 @@
 		public:
 			Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {}
 
-			uint16_t pixel(uint32_t xOfs, uint32_t yOfs) const {
+			Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {
 				return _png.pixel(_x + xOfs, _y + yOfs);
 			}
 		};
@@ -362,10 +391,10 @@
 		};
 
 	public:
-		iterator begin() const { return {*this, _width, 0, 0}; }
+		iterator begin() const { return {*this, _limit, 0, 0}; }
 		iterator end() const {
-			iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one
-			return ++it; // Now one-past-last
+			iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one...
+			return ++it; // ...now one-past-last!
 		}
 	};
 public:
@@ -407,6 +436,93 @@
 	bool xFlip;
 };
 
+static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
+	generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
+	// Run a "pagination" problem solver
+	// TODO: allow picking one of several solvers?
+	auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes);
+	assert(mappings.size() == protoPalettes.size());
+
+	if (options.beVerbose) {
+		options.verbosePrint("Proto-palette mappings: (%zu palette%s)\n", nbPalettes,
+		                     nbPalettes != 1 ? "s" : "");
+		for (size_t i = 0; i < mappings.size(); ++i) {
+			options.verbosePrint("%zu -> %zu\n", i, mappings[i]);
+		}
+	}
+
+	std::vector<Palette> palettes(nbPalettes);
+	// Generate the actual palettes from the mappings
+	for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
+		auto &pal = palettes[mappings[protoPalID]];
+		for (uint16_t color : protoPalettes[protoPalID]) {
+			pal.addColor(color);
+		}
+	}
+
+	// "Sort" colors in the generated palettes, see the man page for the flowchart
+	auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
+	if (embPalRGB != nullptr) {
+		sorting::indexed(palettes, embPalSize, embPalRGB, embPalAlpha);
+	} else if (png.isSuitableForGrayscale()) {
+		sorting::grayscale(palettes, png.getColors().raw());
+	} else {
+		sorting::rgb(palettes);
+	}
+	return {mappings, palettes};
+}
+
+static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
+	makePalsAsSpecified(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
+	if (options.palSpecType == Options::EMBEDDED) {
+		// Generate a palette spec from the first few colors in the embedded palette
+		auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
+		if (embPalRGB == nullptr) {
+			fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
+		}
+
+		// Fill in the palette spec
+		options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent)
+		assert(options.palSpec.size() == 1);
+		// TODO: abort if ignored colors are being used; do it now for a friendlier error
+		// message
+		if (embPalSize > options.maxPalSize()) { // Ignore extraneous colors if they are unused
+			embPalSize = options.maxPalSize();
+		}
+		for (int i = 0; i < embPalSize; ++i) {
+			options.palSpec[0][i] = Rgba(embPalRGB[i].red, embPalRGB[i].green, embPalRGB[i].blue,
+			                             embPalAlpha ? embPalAlpha[i] : 0xFF);
+		}
+	}
+
+	// Convert the palette spec to actual palettes
+	std::vector<Palette> palettes(options.palSpec.size());
+	auto palIter = palettes.begin(); // TODO: `zip`
+	for (auto const &spec : options.palSpec) {
+		for (size_t i = 0; i < options.maxPalSize(); ++i) {
+			(*palIter)[i] = spec[i].cgbColor();
+		}
+		++palIter;
+	}
+
+	// Iterate through proto-palettes, and try mapping them to the specified palettes
+	DefaultInitVec<size_t> mappings(protoPalettes.size());
+	for (size_t i = 0; i < protoPalettes.size(); ++i) {
+		ProtoPalette const &protoPal = protoPalettes[i];
+		// Find the palette...
+		auto iter = std::find_if(palettes.begin(), palettes.end(), [&protoPal](Palette const &pal) {
+			// ...which contains all colors in this proto-pal
+			return std::all_of(protoPal.begin(), protoPal.end(), [&pal](uint16_t color) {
+				return std::find(pal.begin(), pal.end(), color) != pal.end();
+			});
+		});
+		assert(iter != palettes.end()); // TODO: produce a proper error message
+		mappings[i] = iter - palettes.begin();
+	}
+
+	return {mappings, palettes};
+}
+
 static void outputPalettes(std::vector<Palette> const &palettes) {
 	std::filebuf output;
 	output.open(options.palettes, std::ios_base::out | std::ios_base::binary);
@@ -421,7 +537,7 @@
 
 namespace unoptimized {
 
-// TODO: this is very redundant with `TileData`; try merging both?
+// TODO: this is very redundant with `TileData::TileData`; try merging both?
 static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
                            std::vector<Palette> const &palettes,
                            DefaultInitVec<size_t> const &mappings) {
@@ -428,6 +544,12 @@
 	std::filebuf output;
 	output.open(options.output, std::ios_base::out | std::ios_base::binary);
 
+	uint64_t remainingTiles = (png.getWidth() / 8) * (png.getHeight() / 8);
+	if (remainingTiles <= options.trim) {
+		return;
+	}
+	remainingTiles -= options.trim;
+
 	auto iter = attrmap.begin();
 	for (auto tile : png.visitAsTiles(options.columnMajor)) {
 		Palette const &palette = palettes[mappings[iter->protoPaletteID]];
@@ -435,7 +557,7 @@
 			uint16_t row = 0;
 			for (uint32_t x = 0; x < 8; ++x) {
 				row <<= 1;
-				uint8_t index = palette.indexOf(tile.pixel(x, y));
+				uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
 				if (index & 1) {
 					row |= 0x001;
 				}
@@ -449,8 +571,14 @@
 			}
 		}
 		++iter;
+
+		--remainingTiles;
+		if (remainingTiles == 0) {
+			break;
+		}
 	}
-	assert(iter == attrmap.end());
+	assert(remainingTiles == 0);
+	assert(iter + options.trim == attrmap.end());
 }
 
 static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
@@ -485,6 +613,7 @@
 		}
 		++tileID;
 	}
+	assert(iter == attrmap.end());
 }
 
 } // namespace unoptimized
@@ -513,7 +642,7 @@
 			uint16_t bitplanes = 0;
 			for (uint32_t x = 0; x < 8; ++x) {
 				bitplanes <<= 1;
-				uint8_t index = palette.indexOf(tile.pixel(x, y));
+				uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
 				if (index & 1) {
 					bitplanes |= 1;
 				}
@@ -626,8 +755,8 @@
 	}
 	assert(iter == attrmap.end());
 
-	return tiles; // Copy elision should prevent the contained `unordered_set` from being
-	              // re-constructed
+	// Copy elision should prevent the contained `unordered_set` from being re-constructed
+	return tiles;
 }
 
 static void outputTileData(UniqueTiles const &tiles) {
@@ -635,7 +764,8 @@
 	output.open(options.output, std::ios_base::out | std::ios_base::binary);
 
 	uint16_t tileID = 0;
-	for (TileData const *tile : tiles) {
+	for (auto iter = tiles.begin(), end = tiles.end() - options.trim; iter != end; ++iter) {
+		TileData const *tile = *iter;
 		assert(tile->tileID == tileID);
 		++tileID;
 		output.sputn(reinterpret_cast<char const *>(tile->data().data()), options.bitDepth * 8);
@@ -705,7 +835,7 @@
 
 		for (uint32_t y = 0; y < 8; ++y) {
 			for (uint32_t x = 0; x < 8; ++x) {
-				tileColors.add(tile.pixel(x, y));
+				tileColors.add(tile.pixel(x, y).cgbColor());
 			}
 		}
 
@@ -733,43 +863,15 @@
 	                     protoPalettes.size() != 1 ? "s" : "");
 
 	// Sort the proto-palettes by size, which improves the packing algorithm's efficiency
-	// TODO: try keeping the palettes stored while inserting them instead, might perform better
+	// We sort after all insertions to avoid moving items: https://stackoverflow.com/a/2710332
 	std::sort(
 		protoPalettes.begin(), protoPalettes.end(),
 		[](ProtoPalette const &lhs, ProtoPalette const &rhs) { return lhs.size() < rhs.size(); });
 
-	// Run a "pagination" problem solver
-	// TODO: allow picking one of several solvers?
-	auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes);
-	assert(mappings.size() == protoPalettes.size());
-	if (options.beVerbose) {
-		options.verbosePrint("Proto-palette mappings: (%zu palettes)\n", nbPalettes);
-		for (size_t i = 0; i < mappings.size(); ++i) {
-			options.verbosePrint("%zu -> %zu\n", i, mappings[i]);
-		}
-	}
+	auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
+	                                ? generatePalettes(protoPalettes, png)
+	                                : makePalsAsSpecified(protoPalettes, png);
 
-	// TODO: optionally, "decant" the result
-
-	// Generate the actual palettes from the mappings
-	std::vector<Palette> palettes(nbPalettes);
-	for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
-		auto &pal = palettes[mappings[protoPalID]];
-		for (uint16_t color : protoPalettes[protoPalID]) {
-			pal.addColor(color);
-		}
-	}
-
-	// "Sort" colors in the generated palettes, see the man page for the flowchart
-	auto [palSize, palRGB, palAlpha] = png.getEmbeddedPal();
-	if (palRGB) {
-		sorting::indexed(palettes, palSize, palRGB, palAlpha);
-	} else if (png.hasNonGray()) {
-		sorting::rgb(palettes);
-	} else {
-		sorting::grayscale(palettes);
-	}
-
 	if (options.beVerbose) {
 		for (auto &&palette : palettes) {
 			options.verbosePrint("{ ");
@@ -796,13 +898,11 @@
 
 		if (!options.output.empty()) {
 			options.verbosePrint("Generating unoptimized tile data...\n");
-
 			unoptimized::outputTileData(png, attrmap, palettes, mappings);
 		}
 
 		if (!options.tilemap.empty() || !options.attrmap.empty()) {
 			options.verbosePrint("Generating unoptimized tilemap and/or attrmap...\n");
-
 			unoptimized::outputMaps(png, attrmap, mappings);
 		}
 	} else {
@@ -817,19 +917,16 @@
 
 		if (!options.output.empty()) {
 			options.verbosePrint("Generating optimized tile data...\n");
-
 			optimized::outputTileData(tiles);
 		}
 
 		if (!options.tilemap.empty()) {
 			options.verbosePrint("Generating optimized tilemap...\n");
-
 			optimized::outputTilemap(attrmap);
 		}
 
 		if (!options.attrmap.empty()) {
 			options.verbosePrint("Generating optimized attrmap...\n");
-
 			optimized::outputAttrmap(attrmap, mappings);
 		}
 	}
--- a/src/gfx/main.cpp
+++ b/src/gfx/main.cpp
@@ -78,7 +78,7 @@
 }
 
 // Short options
-static char const *optstring = "Aa:CDd:Ffhmo:Pp:Tt:uVvx:";
+static char const *optstring = "Aa:Cc:Dd:Ffhmo:Pp:Tt:uVvx:";
 
 /*
  * Equivalent long options
@@ -94,6 +94,7 @@
 	{"output-attr-map", no_argument,       NULL, 'A'},
 	{"attr-map",        required_argument, NULL, 'a'},
 	{"color-curve",     no_argument,       NULL, 'C'},
+	{"colors",          required_argument, NULL, 'c'},
 	{"debug",           no_argument,       NULL, 'D'},
 	{"depth",           required_argument, NULL, 'd'},
 	{"fix",             no_argument,       NULL, 'f'},
@@ -112,8 +113,8 @@
 	{NULL,              no_argument,       NULL, 0  }
 };
 
-static void print_usage(void) {
-	fputs("Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
+static void printUsage(void) {
+	fputs("Usage: rgbgfx [-CcDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
 	      "	      [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n"
 	      "	      [-x <tiles>] <file>\n"
 	      "Useful options:\n"
@@ -135,6 +136,22 @@
 	}
 }
 
+void parsePaletteSpec(char *arg) {
+	if (arg[0] == '#') {
+		// List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons
+		options.palSpecType = Options::EXPLICIT;
+		// TODO
+	} else if (strcasecmp(arg, "embedded") == 0) {
+		// Use PLTE, error out if missing
+		options.palSpecType = Options::EMBEDDED;
+	} else {
+		// `fmt:path`, parse the file according to the given format
+		// TODO: split both parts, error out if malformed or file not found
+		options.palSpecType = Options::EXPLICIT;
+		// TODO
+	}
+}
+
 int main(int argc, char *argv[]) {
 	int opt;
 	bool autoAttrmap = false, autoTilemap = false, autoPalettes = false;
@@ -166,6 +183,9 @@
 		case 'C':
 			options.useColorCurve = true;
 			break;
+		case 'c':
+			parsePaletteSpec(musl_optarg);
+			break;
 		case 'd':
 			if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1
 			    && options.bitDepth != 2) {
@@ -177,9 +197,9 @@
 			options.fixInput = true;
 			break;
 		case 'h':
-			warning("`-h` is deprecated, use `-???` instead");
+			warning("`-h` is deprecated, use `-Z` instead");
 			[[fallthrough]];
-		case '?': // TODO
+		case 'Z':
 			options.columnMajor = true;
 			break;
 		case 'm':
@@ -219,7 +239,7 @@
 			warning("Ignoring option '%c'", musl_optopt);
 			break;
 		default:
-			print_usage();
+			printUsage();
 			exit(1);
 		}
 	}
@@ -233,11 +253,11 @@
 
 	if (musl_optind == argc) {
 		fputs("FATAL: No input image specified\n", stderr);
-		print_usage();
+		printUsage();
 		exit(1);
 	} else if (argc - musl_optind != 1) {
 		fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind);
-		print_usage();
+		printUsage();
 		exit(1);
 	}
 
@@ -259,7 +279,7 @@
 		if (options.fixInput)
 			fputs("\tConvert input to indexed\n", stderr);
 		if (options.columnMajor)
-			fputs("\tOutput {tile,attr}map in column-major order\n", stderr);
+			fputs("\tVisit image in column-major order\n", stderr);
 		if (options.allowMirroring)
 			fputs("\tAllow mirroring tiles\n", stderr);
 		if (options.allowDedup)
@@ -267,7 +287,8 @@
 		if (options.useColorCurve)
 			fputs("\tUse color curve\n", stderr);
 		fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
-		fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
+		if (options.trim != 0)
+			fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
 		fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
 		        options.baseTileIDs[1]);
 		fprintf(stderr, "\tMax number of tiles: [%" PRIu16 ", %" PRIu16 "]\n",
--- a/src/gfx/pal_packing.cpp
+++ b/src/gfx/pal_packing.cpp
@@ -66,12 +66,12 @@
 	// We leave room for emptied slots to avoid copying the structs around on removal
 	std::vector<std::optional<ProtoPalAttrs>> _assigned;
 	// For resolving proto-palette indices
-	std::vector<ProtoPalette> const &_protoPals;
+	std::vector<ProtoPalette> const *_protoPals;
 
 public:
 	template<typename... Ts>
-	AssignedProtos(decltype(_protoPals) protoPals, Ts &&...elems)
-		: _assigned{std::forward<Ts>(elems)...}, _protoPals{protoPals} {}
+	AssignedProtos(std::vector<ProtoPalette> const &protoPals, Ts &&...elems)
+		: _assigned{std::forward<Ts>(elems)...}, _protoPals{&protoPals} {}
 
 private:
 	template<typename Inner, template<typename> typename Constness>
@@ -93,7 +93,7 @@
 			skipEmpty();
 		}
 		void skipEmpty() {
-			while (_iter != _array->end() && !(*_iter).has_value()) {
+			while (_iter != _array->end() && !_iter->has_value()) {
 				++_iter;
 			}
 		}
@@ -139,7 +139,7 @@
 	 * Args are passed to the `ProtoPalAttrs`'s constructor
 	 */
 	template<typename... Ts>
-	auto assign(Ts &&...args) {
+	void assign(Ts &&...args) {
 		auto freeSlot = std::find_if_not(
 			_assigned.begin(), _assigned.end(),
 			[](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); });
@@ -147,34 +147,24 @@
 		if (freeSlot == _assigned.end()) { // We are full, use a new slot
 			_assigned.emplace_back(std::forward<Ts>(args)...);
 		} else { // Reuse a free slot
-			(*freeSlot).emplace(std::forward<Ts>(args)...);
+			freeSlot->emplace(std::forward<Ts>(args)...);
 		}
-		return freeSlot;
 	}
 	void remove(iterator const &iter) {
-		(*iter._iter).reset(); // This time, we want to access the `optional` itself
+		iter._iter->reset(); // This time, we want to access the `optional` itself
 	}
 	void clear() { _assigned.clear(); }
 
-	/**
-	 * Computes the "relative size" of a proto-palette on this palette
-	 */
-	double relSizeOf(ProtoPalette const &protoPal) const {
-		return std::transform_reduce(
-			protoPal.begin(), protoPal.end(), .0, std::plus<>(), [this](uint16_t color) {
-				// NOTE: The paper and the associated code disagree on this: the code has
-			    // this `1 +`, whereas the paper does not; its lack causes a division by 0
-			    // if the symbol is not found anywhere, so I'm assuming the paper is wrong.
-				return 1.
-			           / (1
-			              + std::count_if(
-							  begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
-								  ProtoPalette const &pal = _protoPals[attrs.palIndex];
-								  return std::find(pal.begin(), pal.end(), color) != pal.end();
-							  }));
-			});
-	}
+	bool empty() const { return std::distance(begin(), end()) == 0; }
+
 private:
+	static void addUniqueColors(std::unordered_set<uint16_t> &colors, AssignedProtos const &pal) {
+		for (ProtoPalAttrs const &attrs : pal) {
+			for (uint16_t color : (*pal._protoPals)[attrs.palIndex]) {
+				colors.insert(color);
+			}
+		}
+	}
 	std::unordered_set<uint16_t> &uniqueColors() const {
 		// We check for *distinct* colors by stuffing them into a `set`; this should be
 		// faster than "back-checking" on every element (O(n²))
@@ -182,18 +172,16 @@
 		// TODO: calc84maniac suggested another approach; try implementing it, see if it
 		// performs better:
 		// > So basically you make a priority queue that takes iterators into each of your sets
-		// (paired with end iterators so you'll know where to stop), and the comparator tests the
-		// values pointed to by each iterator > Then each iteration you pop from the queue,
-		// optionally add one to your count, increment the iterator and push it back into the queue
-		// if it didn't reach the end > and you do this until the priority queue is empty
+		// > (paired with end iterators so you'll know where to stop), and the comparator tests the
+		// > values pointed to by each iterator
+		// > Then each iteration you pop from the queue,
+		// > optionally add one to your count, increment the iterator and push it back into the
+		// > queue if it didn't reach the end
+		// > And you do this until the priority queue is empty
 		static std::unordered_set<uint16_t> colors;
 
 		colors.clear();
-		for (ProtoPalAttrs const &attrs : *this) {
-			for (uint16_t color : _protoPals[attrs.palIndex]) {
-				colors.insert(color);
-			}
-		}
+		addUniqueColors(colors, *this);
 		return colors;
 	}
 public:
@@ -206,8 +194,106 @@
 		colors.insert(protoPal.begin(), protoPal.end());
 		return colors.size() <= options.maxPalSize();
 	}
+
+public:
+	/**
+	 * Computes the "relative size" of a proto-palette on this palette
+	 */
+	double relSizeOf(ProtoPalette const &protoPal) const {
+		// NOTE: this function must not call `uniqueColors`, or one of its callers will break
+		return std::transform_reduce(
+			protoPal.begin(), protoPal.end(), 0.0, std::plus<>(), [this](uint16_t color) {
+				// NOTE: The paper and the associated code disagree on this: the code has
+			    // this `1 +`, whereas the paper does not; its lack causes a division by 0
+			    // if the symbol is not found anywhere, so I'm assuming the paper is wrong.
+				return 1.
+			           / (1
+			              + std::count_if(
+							  begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
+								  ProtoPalette const &pal = (*_protoPals)[attrs.palIndex];
+								  return std::find(pal.begin(), pal.end(), color) != pal.end();
+							  }));
+			});
+	}
+
+	/**
+	 * Computes the "relative size" of a palette on this one
+	 */
+	double combinedVolume(AssignedProtos const &pal) const {
+		auto &colors = uniqueColors();
+		addUniqueColors(colors, pal);
+		return colors.size();
+	}
 };
 
+static void removeEmptyPals(std::vector<AssignedProtos> &assignments) {
+	// We do this by plucking "replacement" palettes from the end of the vector, so as to minimize
+	// the amount of moves performed. We can afford this because we don't care about their order,
+	// unlike `std::remove_if`, which permits less moves and thus better performance.
+	for (size_t i = 0; i != assignments.size(); ++i) {
+		if (assignments[i].empty()) {
+			// Hinting the compiler that the `return;` can only be reached if entering the loop
+			// produces better assembly
+			if (assignments.back().empty()) {
+				do {
+					assignments.pop_back();
+					assert(assignments.size() != 0);
+				} while (assignments.back().empty());
+				// Worst case, the loop ended on `assignments[i - 1]` (since every slot before `i`
+				// is known to be non-empty).
+				// (This could be a problem if `i` was 0, but we know there must be at least one
+				// color, so we're safe from that. The assertion in the loop checks it to be sure.)
+				// However, if it did stop at `i - 1`, then `i` no longer points to a valid slot,
+				// and we must end.
+				if (i == assignments.size()) {
+					break;
+				}
+			}
+			assert(i < assignments.size());
+			assignments[i] = std::move(assignments.back());
+			assignments.pop_back();
+		}
+	}
+}
+
+static void decant(std::vector<AssignedProtos> &assignments) {
+	// "Decanting" is the process of moving all *things* that can fit in a lower index there
+	auto decantOn = [&assignments](auto const &move) {
+		// No need to attempt decanting on palette #0, as there are no palettes to decant to
+		for (size_t from = assignments.size(); --from;) {
+			// Scan all palettes before this one
+			for (size_t to = 0; to < from; ++to) {
+				move(assignments[to], assignments[from]);
+			}
+		}
+	};
+
+	// Decant on palettes
+	decantOn([](AssignedProtos &to, AssignedProtos &from) {
+		// If the entire palettes can be merged, move all of `from`'s proto-palettes
+		if (to.combinedVolume(from) <= options.maxPalSize()) {
+			for (ProtoPalAttrs &protoPal : from) {
+				to.assign(std::move(protoPal));
+			}
+			from.clear();
+		}
+	});
+
+	// Decant on "components" (= proto-pals sharing colors)
+	decantOn([](AssignedProtos &to, AssignedProtos &from) {
+		// TODO
+		(void)to;
+		(void)from;
+	});
+
+	// Decant on proto-palettes
+	decantOn([](AssignedProtos &to, AssignedProtos &from) {
+		// TODO
+		(void)to;
+		(void)from;
+	});
+}
+
 std::tuple<DefaultInitVec<size_t>, size_t>
 	overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
 	options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n");
@@ -256,7 +342,7 @@
 				continue;
 			}
 
-			options.verbosePrint("%zu: Rel size: %f (size = %zu)\n", i,
+			options.verbosePrint("%zu/%zu: Rel size: %f (size = %zu)\n", i, assignments.size(),
 			                     assignments[i].relSizeOf(protoPal), protoPal.size());
 			if (assignments[i].relSizeOf(protoPal) < bestRelSize) {
 				bestPalIndex = i;
@@ -330,9 +416,13 @@
 		}
 		queue.pop();
 	}
-	// Deal with any empty palettes left over from the "un-overloading" step
-	// TODO (can there be any?)
 
+	// "Decant" the result
+	decant(assignments);
+
+	// Remove all empty palettes, filling the gaps created.
+	removeEmptyPals(assignments);
+
 	if (options.beVerbose) {
 		for (auto &&assignment : assignments) {
 			options.verbosePrint("{ ");
@@ -341,7 +431,7 @@
 					options.verbosePrint("%04" PRIx16 ", ", colorIndex);
 				}
 			}
-			options.verbosePrint("} (%zu)\n", assignment.volume());
+			options.verbosePrint("} (volume = %zu)\n", assignment.volume());
 		}
 	}
 
--- a/src/gfx/pal_sorting.cpp
+++ b/src/gfx/pal_sorting.cpp
@@ -39,11 +39,21 @@
 	}
 }
 
-void grayscale(std::vector<Palette> &palettes) {
-	options.verbosePrint("Sorting grayscale-only palettes...\n");
+void grayscale(std::vector<Palette> &palettes,
+               std::array<std::optional<Rgba>, 0x8001> const &colors) {
+	options.verbosePrint("Sorting grayscale-only palette...\n");
 
-	for (Palette &pal : palettes) {
-		(void)pal; // TODO
+	// This method is only applicable if there are at most as many colors as colors per palette, so
+	// we should only have a single palette.
+	assert(palettes.size() == 1);
+
+	Palette &palette = palettes[0];
+	std::fill(palette.begin(), palette.end(), Rgba::transparent);
+	for (auto const &slot : colors) {
+		if (!slot.has_value() || slot->isTransparent()) {
+			continue;
+		}
+		palette[slot->grayIndex()] = slot->cgbColor();
 	}
 }
 
--- /dev/null
+++ b/src/gfx/rgba.cpp
@@ -1,0 +1,24 @@
+
+#include "gfx/rgba.hpp"
+
+#include <assert.h>
+#include <stdint.h>
+
+#include "gfx/main.hpp" // options
+
+uint16_t Rgba::cgbColor() const {
+	if (isTransparent()) {
+		return transparent;
+	}
+	if (options.useColorCurve) {
+		assert(!"TODO");
+	} else {
+		return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
+	}
+}
+
+uint8_t Rgba::grayIndex() const {
+	assert(isGray());
+	// Convert from [0; 256[ to [0; maxPalSize[
+	return static_cast<uint16_t>(255 - red) * options.maxPalSize() / 256;
+}