diff --git a/Makefile b/Makefile index 8d48354a..88924824 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,7 @@ rgbgfx_obj := \ 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 diff --git a/include/gfx/convert.hpp b/include/gfx/convert.hpp index 0dc42fb3..8c2dfb98 100644 --- 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 -#include - -#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(val) << shift; }; - return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0); - } - bool operator!=(Rgba const &other) const { - return static_cast(*this) != static_cast(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 */ diff --git a/include/gfx/main.hpp b/include/gfx/main.hpp index 9a34cc34..6f97eafb 100644 --- a/include/gfx/main.hpp +++ b/include/gfx/main.hpp @@ -13,22 +13,33 @@ #include #include #include +#include #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 baseTileIDs{0, 0}; // TODO - std::array 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> palSpec{}; + std::array unitSize{1, 1}; // -u (in tiles) + std::array inputSlice; // -L + std::array baseTileIDs{0, 0}; // -b + std::array 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 @@ struct Options { 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 @@ struct Palette { 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(); diff --git a/include/gfx/pal_sorting.hpp b/include/gfx/pal_sorting.hpp index 5c6a24cc..8acd1edb 100644 --- a/include/gfx/pal_sorting.hpp +++ b/include/gfx/pal_sorting.hpp @@ -9,16 +9,22 @@ #ifndef RGBDS_GFX_PAL_SORTING_HPP #define RGBDS_GFX_PAL_SORTING_HPP +#include +#include +#include #include #include +#include "gfx/rgba.hpp" + class Palette; namespace sorting { void indexed(std::vector &palettes, int palSize, png_color const *palRGB, png_byte *palAlpha); -void grayscale(std::vector &palettes); +void grayscale(std::vector &palettes, + std::array, 0x8001> const &colors); void rgb(std::vector &palettes); } diff --git a/include/gfx/rgba.hpp b/include/gfx/rgba.hpp new file mode 100644 index 00000000..06dc77e0 --- /dev/null +++ b/include/gfx/rgba.hpp @@ -0,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 + +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(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 */ diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index c65ab94b..f2f2923c 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -129,16 +129,25 @@ PNG), then colors in each output palette will be sorted according to their order 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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a17d139d..a41b2214 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,6 +75,7 @@ set(rgbgfx_src "gfx/pal_packing.cpp" "gfx/pal_sorting.cpp" "gfx/proto_palette.cpp" + "gfx/rgba.cpp" "extern/getopt.c" "error.c" ) diff --git a/src/gfx/convert.cpp b/src/gfx/convert.cpp index 2f6d12fb..0a0f84e6 100644 --- a/src/gfx/convert.cpp +++ b/src/gfx/convert.cpp @@ -49,10 +49,15 @@ public: } } - 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 @@ class Png { // These are cached for speed uint32_t width, height; - DefaultInitVec pixels; + std::vector 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_get_error_ptr(png)); @@ -110,11 +114,38 @@ public: 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 @@ public: 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,16 +329,15 @@ public: 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; } } } } - 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: 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: }; 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 @@ struct AttrmapEntry { bool xFlip; }; +static std::tuple, std::vector> + generatePalettes(std::vector 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 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, std::vector> + makePalsAsSpecified(std::vector 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 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 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 const &palettes) { std::filebuf output; output.open(options.palettes, std::ios_base::out | std::ios_base::binary); @@ -421,13 +537,19 @@ static void outputPalettes(std::vector const &palettes) { 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 const &attrmap, std::vector const &palettes, DefaultInitVec const &mappings) { 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 @@ static void outputTileData(Png const &png, DefaultInitVec const &a 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 @@ static void outputTileData(Png const &png, DefaultInitVec const &a } } ++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 const &attrmap, @@ -485,6 +613,7 @@ static void outputMaps(Png const &png, DefaultInitVec const &attrm } ++tileID; } + assert(iter == attrmap.end()); } } // namespace unoptimized @@ -513,7 +642,7 @@ public: 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 @@ static UniqueTiles dedupTiles(Png const &png, DefaultInitVec &attr } 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 @@ static void outputTileData(UniqueTiles const &tiles) { 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(tile->data().data()), options.bitDepth * 8); @@ -705,7 +835,7 @@ void process() { 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,42 +863,14 @@ contained:; 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]); - } - } - - // TODO: optionally, "decant" the result - - // Generate the actual palettes from the mappings - std::vector 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); - } + auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC + ? generatePalettes(protoPalettes, png) + : makePalsAsSpecified(protoPalettes, png); if (options.beVerbose) { for (auto &&palette : palettes) { @@ -796,13 +898,11 @@ contained:; 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 @@ contained:; 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); } } diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index 7bea50c1..96a1fdfa 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -78,7 +78,7 @@ void Options::verbosePrint(char const *fmt, ...) const { } // 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 @@ static struct option const longopts[] = { {"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 @@ static struct option const longopts[] = { {NULL, no_argument, NULL, 0 } }; -static void print_usage(void) { - fputs("Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a | -A] [-d ]\n" +static void printUsage(void) { + fputs("Usage: rgbgfx [-CcDhmuVv] [-f | -F] [-a | -A] [-d ]\n" " [-o ] [-p | -P] [-t | -T]\n" " [-x ] \n" "Useful options:\n" @@ -135,6 +136,22 @@ void fputsv(std::string_view const &view, FILE *f) { } } +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 @@ int main(int argc, char *argv[]) { 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 @@ int main(int argc, char *argv[]) { 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 @@ int main(int argc, char *argv[]) { warning("Ignoring option '%c'", musl_optopt); break; default: - print_usage(); + printUsage(); exit(1); } } @@ -233,11 +253,11 @@ int main(int argc, char *argv[]) { 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 @@ int main(int argc, char *argv[]) { 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 @@ int main(int argc, char *argv[]) { 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", diff --git a/src/gfx/pal_packing.cpp b/src/gfx/pal_packing.cpp index c01edde8..399f3e54 100644 --- a/src/gfx/pal_packing.cpp +++ b/src/gfx/pal_packing.cpp @@ -66,12 +66,12 @@ class AssignedProtos { // We leave room for emptied slots to avoid copying the structs around on removal std::vector> _assigned; // For resolving proto-palette indices - std::vector const &_protoPals; + std::vector const *_protoPals; public: template - AssignedProtos(decltype(_protoPals) protoPals, Ts &&...elems) - : _assigned{std::forward(elems)...}, _protoPals{protoPals} {} + AssignedProtos(std::vector const &protoPals, Ts &&...elems) + : _assigned{std::forward(elems)...}, _protoPals{&protoPals} {} private: template typename Constness> @@ -93,7 +93,7 @@ private: skipEmpty(); } void skipEmpty() { - while (_iter != _array->end() && !(*_iter).has_value()) { + while (_iter != _array->end() && !_iter->has_value()) { ++_iter; } } @@ -139,7 +139,7 @@ public: * Args are passed to the `ProtoPalAttrs`'s constructor */ template - auto assign(Ts &&...args) { + void assign(Ts &&...args) { auto freeSlot = std::find_if_not( _assigned.begin(), _assigned.end(), [](std::optional const &slot) { return slot.has_value(); }); @@ -147,34 +147,24 @@ public: if (freeSlot == _assigned.end()) { // We are full, use a new slot _assigned.emplace_back(std::forward(args)...); } else { // Reuse a free slot - (*freeSlot).emplace(std::forward(args)...); + freeSlot->emplace(std::forward(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 &colors, AssignedProtos const &pal) { + for (ProtoPalAttrs const &attrs : pal) { + for (uint16_t color : (*pal._protoPals)[attrs.palIndex]) { + colors.insert(color); + } + } + } std::unordered_set &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 @@ private: // 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 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 @@ public: 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 &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 &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, size_t> overloadAndRemove(std::vector const &protoPalettes) { options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n"); @@ -256,7 +342,7 @@ std::tuple, size_t> 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,8 +416,12 @@ std::tuple, size_t> } 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) { @@ -341,7 +431,7 @@ std::tuple, size_t> options.verbosePrint("%04" PRIx16 ", ", colorIndex); } } - options.verbosePrint("} (%zu)\n", assignment.volume()); + options.verbosePrint("} (volume = %zu)\n", assignment.volume()); } } diff --git a/src/gfx/pal_sorting.cpp b/src/gfx/pal_sorting.cpp index d22aae61..756746ee 100644 --- a/src/gfx/pal_sorting.cpp +++ b/src/gfx/pal_sorting.cpp @@ -39,11 +39,21 @@ void indexed(std::vector &palettes, int palSize, png_color const *palRG } } -void grayscale(std::vector &palettes) { - options.verbosePrint("Sorting grayscale-only palettes...\n"); +void grayscale(std::vector &palettes, + std::array, 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(); } } diff --git a/src/gfx/rgba.cpp b/src/gfx/rgba.cpp new file mode 100644 index 00000000..63b5b5aa --- /dev/null +++ b/src/gfx/rgba.cpp @@ -0,0 +1,24 @@ + +#include "gfx/rgba.hpp" + +#include +#include + +#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(255 - red) * options.maxPalSize() / 256; +}