ref: 188027bccce01fc744a845c0626c7c6c8559966c
parent: 79adcdb7eac7e7ea22e2a65f33092f2c764f550a
author: ISSOtm <eldredhabert0@gmail.com>
date: Tue Mar 22 14:42:33 EDT 2022
Rename `convert` to `process` More consistent with its "main" function's name
--- a/Makefile
+++ b/Makefile
@@ -105,10 +105,10 @@
src/error.o
rgbgfx_obj := \
- src/gfx/convert.o \
src/gfx/main.o \
src/gfx/pal_packing.o \
src/gfx/pal_sorting.o \
+ src/gfx/process.o \
src/gfx/proto_palette.o \
src/gfx/rgba.o \
src/extern/getopt.o \
--- a/include/gfx/convert.hpp
+++ /dev/null
@@ -1,14 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_CONVERT_HPP
-#define RGBDS_GFX_CONVERT_HPP
-
-void process();
-
-#endif /* RGBDS_GFX_CONVERT_HPP */
--- /dev/null
+++ b/include/gfx/process.hpp
@@ -1,0 +1,14 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_CONVERT_HPP
+#define RGBDS_GFX_CONVERT_HPP
+
+void process();
+
+#endif /* RGBDS_GFX_CONVERT_HPP */
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -70,10 +70,10 @@
)
set(rgbgfx_src
- "gfx/convert.cpp"
"gfx/main.cpp"
"gfx/pal_packing.cpp"
"gfx/pal_sorting.cpp"
+ "gfx/process.cpp"
"gfx/proto_palette.cpp"
"gfx/rgba.cpp"
"extern/getopt.c"
--- a/src/gfx/convert.cpp
+++ /dev/null
@@ -1,1025 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/convert.hpp"
-
-#include <algorithm>
-#include <assert.h>
-#include <cinttypes>
-#include <errno.h>
-#include <fstream>
-#include <memory>
-#include <optional>
-#include <png.h>
-#include <setjmp.h>
-#include <string.h>
-#include <tuple>
-#include <unordered_set>
-#include <utility>
-#include <vector>
-
-#include "defaultinitalloc.hpp"
-#include "helpers.h"
-
-#include "gfx/main.hpp"
-#include "gfx/pal_packing.hpp"
-#include "gfx/pal_sorting.hpp"
-#include "gfx/proto_palette.hpp"
-
-class ImagePalette {- // Use as many slots as there are CGB colors (plus transparency)
- std::array<std::optional<Rgba>, 0x8001> _colors;
-
-public:
- ImagePalette() = default;
-
- void registerColor(Rgba const &rgba) {- decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
-
- if (rgba.cgbColor() == Rgba::transparent) {- options.hasTransparentPixels = true;
- }
-
- if (!slot.has_value()) {- slot.emplace(rgba);
- } else if (*slot != rgba) {- warning("Different colors melded together (#%08x into #%08x as %04x)", rgba.toCSS(),- slot->toCSS(), rgba.cgbColor()); // TODO: indicate position
- }
- }
-
- 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(); }-};
-
-class Png {- std::string const &path;
- std::filebuf file{};- png_structp png = nullptr;
- png_infop info = nullptr;
-
- // These are cached for speed
- uint32_t width, height;
- DefaultInitVec<Rgba> pixels;
- ImagePalette colors;
- int colorType;
- int nbColors;
- png_colorp embeddedPal = nullptr;
- png_bytep transparencyPal = nullptr;
-
- [[noreturn]] static void handleError(png_structp png, char const *msg) {- Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
-
- fatal("Error reading input image (\"%s\"): %s", self->path.c_str(), msg);- }
-
- static void handleWarning(png_structp png, char const *msg) {- Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
-
- warning("In input image (\"%s\"): %s", self->path.c_str(), msg);- }
-
- static void readData(png_structp png, png_bytep data, size_t length) {- Png *self = reinterpret_cast<Png *>(png_get_io_ptr(png));
- std::streamsize expectedLen = length;
- std::streamsize nbBytesRead = self->file.sgetn(reinterpret_cast<char *>(data), expectedLen);
-
- if (nbBytesRead != expectedLen) {- fatal("Error reading input image (\"%s\"): file too short (expected at least %zd more "- "bytes after reading %lld)",
- self->path.c_str(), length - nbBytesRead,
- self->file.pubseekoff(0, std::ios_base::cur));
- }
- }
-
-public:
- ImagePalette const &getColors() const { return colors; }-
- int getColorType() const { return colorType; }-
- std::tuple<int, png_const_colorp, png_bytep> getEmbeddedPal() const {- return {nbColors, embeddedPal, transparencyPal};- }
-
- uint32_t getWidth() const { return width; }-
- uint32_t getHeight() const { return height; }-
- Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }-
- Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }-
- bool isSuitableForGrayscale() const {- // Check that all of the grays don't fall into the same "bin"
- if (colors.size() > options.maxOpaqueColors()) { // Apply the Pigeonhole Principle- options.verbosePrint(Options::VERB_DEBUG,
- "Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n",
- colors.size(), options.maxOpaqueColors());
- return false;
- }
- uint8_t bins = 0;
- for (auto const &color : colors) {- if (color->isTransparent()) {- continue;
- }
- if (!color->isGray()) {- options.verbosePrint(Options::VERB_DEBUG,
- "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(
- Options::VERB_DEBUG,
- "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
- *
- * This code is more complicated than strictly necessary, but that's because of the API
- * being used: the "high-level" interface doesn't provide all the transformations we need,
- * so we use the "lower-level" one instead.
- * We also use that occasion to only read the PNG one line at a time, since we store all of
- * the pixel data in `pixels`, which saves on memory allocations.
- */
- explicit Png(std::string const &filePath) : path(filePath), colors() {- if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {- fatal("Failed to open input image (\"%s\"): %s", path.c_str(), strerror(errno));- }
-
- options.verbosePrint(Options::VERB_LOG_ACT, "Opened input file\n");
-
- std::array<unsigned char, 8> pngHeader;
-
- if (file.sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
- != static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
- || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {- fatal("Input file (\"%s\") is not a PNG image!", path.c_str());- }
-
- options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
-
- png = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)this, handleError,
- handleWarning);
- if (!png) {- fatal("Failed to allocate PNG structure: %s", strerror(errno));- }
-
- info = png_create_info_struct(png);
- if (!info) {- png_destroy_read_struct(&png, nullptr, nullptr);
- fatal("Failed to allocate PNG info structure: %s", strerror(errno));- }
-
- png_set_read_fn(png, this, readData);
- png_set_sig_bytes(png, pngHeader.size());
-
- // TODO: png_set_crc_action(png, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
-
- // Skipping chunks we don't use should improve performance
- // TODO: png_set_keep_unknown_chunks(png, ...);
-
- // Process all chunks up to but not including the image data
- png_read_info(png, info);
-
- int bitDepth, interlaceType; //, compressionType, filterMethod;
-
- png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr,
- nullptr);
-
- if (width % 8 != 0) {- fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);- }
- if (height % 8 != 0) {- fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);- }
-
- pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
-
- auto colorTypeName = [this]() {- switch (colorType) {- case PNG_COLOR_TYPE_GRAY:
- return "grayscale";
- case PNG_COLOR_TYPE_GRAY_ALPHA:
- return "grayscale + alpha";
- case PNG_COLOR_TYPE_PALETTE:
- return "palette";
- case PNG_COLOR_TYPE_RGB:
- return "RGB";
- case PNG_COLOR_TYPE_RGB_ALPHA:
- return "RGB + alpha";
- default:
- fatal("Unknown color type %d", colorType);- }
- };
- auto interlaceTypeName = [&interlaceType]() {- switch (interlaceType) {- case PNG_INTERLACE_NONE:
- return "not interlaced";
- case PNG_INTERLACE_ADAM7:
- return "interlaced (Adam7)";
- default:
- fatal("Unknown interlace type %d", interlaceType);- }
- };
- options.verbosePrint(Options::VERB_INTERM,
- "Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", height,
- width, bitDepth, colorTypeName(), interlaceTypeName());
-
- if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {- int nbTransparentEntries;
- if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {- assert(nbTransparentEntries == nbColors);
- }
-
- options.verbosePrint(Options::VERB_INTERM, "Embedded palette has %d colors: [",
- nbColors);
- for (int i = 0; i < nbColors; ++i) {- auto const &color = embeddedPal[i];
- options.verbosePrint(
- Options::VERB_INTERM, "#%02x%02x%02x%02x%s", color.red, color.green, color.blue,
- transparencyPal ? transparencyPal[i] : 0xFF, i != nbColors - 1 ? ", " : "]\n");
- }
- } else {- options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n");
- }
-
- // Set up transformations; to turn everything into RGBA888
- // TODO: it's not necessary to uniformize the pixel data (in theory), and not doing
- // so *might* improve performance, and should reduce memory usage.
-
- // Convert grayscale to RGB
- switch (colorType & ~PNG_COLOR_MASK_ALPHA) {- case PNG_COLOR_TYPE_GRAY:
- png_set_gray_to_rgb(png); // This also converts tRNS to alpha
- break;
- case PNG_COLOR_TYPE_PALETTE:
- png_set_palette_to_rgb(png);
- break;
- }
-
- if (png_get_valid(png, info, PNG_INFO_tRNS)) {- // If we read a tRNS chunk, convert it to alpha
- png_set_tRNS_to_alpha(png);
- } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {- // Otherwise, if we lack an alpha channel, default to full opacity
- png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
- }
-
- // Scale 16bpp back to 8 (we don't need all of that precision anyway)
- if (bitDepth == 16) {- png_set_scale_16(png);
- } else if (bitDepth < 8) {- png_set_packing(png);
- }
-
- // Set interlace handling (MUST be done before `png_read_update_info`)
- int nbPasses = png_set_interlace_handling(png);
-
- // Update `info` with the transformations
- png_read_update_info(png, info);
- // These shouldn't have changed
- assert(png_get_image_width(png, info) == width);
- assert(png_get_image_height(png, info) == height);
- // These should have changed, however
- assert(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
- assert(png_get_bit_depth(png, info) == 8);
-
- // Now that metadata has been read, we can process the image data
-
- std::vector<png_byte> row(png_get_rowbytes(png, info));
-
- if (interlaceType == PNG_INTERLACE_NONE) {- for (png_uint_32 y = 0; y < height; ++y) {- png_read_row(png, row.data(), nullptr);
-
- for (png_uint_32 x = 0; x < width; ++x) {- Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
- colors.registerColor(rgba);
- pixel(x, y) = rgba;
- }
- }
- } else {- // For interlace to work properly, we must read the image `nbPasses` times
- for (int pass = 0; pass < nbPasses; ++pass) {- // The interlacing pass must be skipped if its width or height is reported as zero
- if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {- continue;
- }
-
- png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
- png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
-
- for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {- png_bytep ptr = row.data();
- png_read_row(png, ptr, nullptr);
-
- for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {- Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
- colors.registerColor(rgba);
- pixel(x, y) = rgba;
- ptr += 4;
- }
- }
- }
- }
-
- // 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); }-
- class TilesVisitor {- Png const &_png;
- bool const _columnMajor;
- uint32_t const _width, _height;
- uint32_t const _limit = _columnMajor ? _height : _width;
-
- public:
- TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height)
- : _png(png), _columnMajor(columnMajor), _width(width), _height(height) {}-
- class Tile {- Png const &_png;
- uint32_t const _x, _y;
-
- public:
- Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {}-
- Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {- return _png.pixel(_x + xOfs, _y + yOfs);
- }
- };
-
- private:
- struct iterator {- TilesVisitor const &parent;
- uint32_t const limit;
- uint32_t x, y;
-
- std::pair<uint32_t, uint32_t> coords() const { return {x, y}; }- Tile operator*() const { return {parent._png, x, y}; }-
- iterator &operator++() {- auto [major, minor] = parent._columnMajor ? std::tie(y, x) : std::tie(x, y);
- major += 8;
- if (major == limit) {- minor += 8;
- major = 0;
- }
- return *this;
- }
-
- bool operator!=(iterator const &rhs) const {- return coords() != rhs.coords(); // Compare the returned coord pairs
- }
- };
-
- public:
- 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!
- }
- };
-public:
- TilesVisitor visitAsTiles(bool columnMajor) const {- return {*this, columnMajor, width, height};- }
-};
-
-class RawTiles {- /**
- * A tile which only contains indices into the image's global palette
- */
- class RawTile {- std::array<std::array<size_t, 8>, 8> _pixelIndices{};-
- public:
- // Not super clean, but it's closer to matrix notation
- size_t &operator()(size_t x, size_t y) { return _pixelIndices[y][x]; }- };
-
-private:
- std::vector<RawTile> _tiles;
-
-public:
- /**
- * Creates a new raw tile, and returns a reference to it so it can be filled in
- */
- RawTile &newTile() {- _tiles.emplace_back();
- return _tiles.back();
- }
-};
-
-struct AttrmapEntry {- size_t protoPaletteID; // Only this field is used when outputting "unoptimized" data
- uint8_t tileID; // This is the ID as it will be output to the tilemap
- bool bank;
- bool yFlip;
- bool xFlip;
-
- static constexpr decltype(protoPaletteID) transparent = SIZE_MAX;
-};
-
-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.verbosity >= Options::VERB_INTERM) {- fprintf(stderr, "Proto-palette mappings: (%zu palette%s)\n", nbPalettes,
- nbPalettes != 1 ? "s" : "");
- for (size_t i = 0; i < mappings.size(); ++i) {- fprintf(stderr, "%zu -> %zu\n", i, mappings[i]);
- }
- }
-
- std::vector<Palette> palettes(nbPalettes);
- // If the image contains at least one transparent pixel, force transparency in the first slot of
- // all palettes
- if (options.hasTransparentPixels) {- for (Palette &pal : palettes) {- pal.colors[0] = Rgba::transparent;
- }
- }
- // 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.maxOpaqueColors()) { // Ignore extraneous colors if they are unused- embPalSize = options.maxOpaqueColors();
- }
- 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.nbColorsPerPal; ++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);
-
- for (Palette const &palette : palettes) {- for (uint8_t i = 0; i < options.nbColorsPerPal; ++i) {- uint16_t color = palette.colors[i]; // Will return `UINT16_MAX` for unused slots
- output.sputc(color & 0xFF);
- output.sputc(color >> 8);
- }
- }
-}
-
-static uint8_t flip(uint8_t byte) {- // To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
- byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
- byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
- byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
- return byte;
-}
-
-class TileData {- std::array<uint8_t, 16> _data;
- // The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
- // if horizontal mirroring is in effect. It should still be a reasonable tie-breaker in
- // non-pathological cases.
- uint16_t _hash;
-public:
- // This is an index within the "global" pool; no bank info is encoded here
- // It's marked as `mutable` so that it can be modified even on a `const` object;
- // this is necessary because the `set` in which it's inserted refuses any modification for fear
- // of altering the element's hash, but the tile ID is not part of it.
- mutable uint16_t tileID;
-
- static uint16_t rowBitplanes(Png::TilesVisitor::Tile const &tile, Palette const &palette,
- uint32_t y) {- uint16_t row = 0;
- for (uint32_t x = 0; x < 8; ++x) {- row <<= 1;
- uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
- if (index & 1) {- row |= 1;
- }
- if (index & 2) {- row |= 0x100;
- }
- }
- return row;
- }
-
- TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {- size_t writeIndex = 0;
- for (uint32_t y = 0; y < 8; ++y) {- uint16_t bitplanes = rowBitplanes(tile, palette, y);
- _data[writeIndex++] = bitplanes & 0xFF;
- if (options.bitDepth == 2) {- _data[writeIndex++] = bitplanes >> 8;
- }
-
- // Update the hash
- _hash ^= bitplanes;
- if (options.allowMirroring) {- // Count the line itself as mirrorred; vertical mirroring is
- // already taken care of because the symmetric line will be XOR'd
- // the same way. (...which is a problem, but probably benign.)
- _hash ^= flip(bitplanes >> 8) << 8 | flip(bitplanes & 0xFF);
- }
- }
- }
-
- auto const &data() const { return _data; }- uint16_t hash() const { return _hash; }-
- enum MatchType {- NOPE,
- EXACT,
- HFLIP,
- VFLIP,
- VHFLIP,
- };
- MatchType tryMatching(TileData const &other) const {- // Check for strict equality first, as that can typically be optimized, and it allows
- // hoisting the mirroring check out of the loop
- if (_data == other._data) {- return MatchType::EXACT;
- }
-
- if (!options.allowMirroring) {- return MatchType::NOPE;
- }
-
- // Check if we have horizontal mirroring, which scans the array forward again
- if (std::equal(_data.begin(), _data.end(), other._data.begin(),
- [](uint8_t lhs, uint8_t rhs) { return lhs == flip(rhs); })) {- return MatchType::HFLIP;
- }
-
- // Check if we have vertical or vertical+horizontal mirroring, for which we have to read
- // bitplane *pairs* backwards
- bool hasVFlip = true, hasVHFlip = true;
- for (uint8_t i = 0; i < _data.size(); ++i) {- // Flip the bottom bit to get the corresponding row's bitplane 0/1
- // (This works because the array size is even)
- uint8_t lhs = _data[i], rhs = other._data[(15 - i) ^ 1];
- if (lhs != rhs) {- hasVFlip = false;
- }
- if (lhs != flip(rhs)) {- hasVHFlip = false;
- }
- if (!hasVFlip && !hasVHFlip) {- return MatchType::NOPE; // If both have been eliminated, all hope is lost!
- }
- }
-
- // If we have both (i.e. we have symmetry), default to vflip only
- assert(hasVFlip || hasVHFlip);
- return hasVFlip ? MatchType::VFLIP : MatchType::VHFLIP;
- }
- friend bool operator==(TileData const &lhs, TileData const &rhs) {- return lhs.tryMatching(rhs) != MatchType::NOPE;
- }
-};
-
-template<>
-struct std::hash<TileData> {- std::size_t operator()(TileData const &tile) const { return tile.hash(); }-};
-
-namespace unoptimized {-
-// 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) {- 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)) {- size_t protoPaletteID = iter->protoPaletteID;
- // If the tile is fully transparent, default to palette 0
- Palette const &palette = palettes[protoPaletteID != AttrmapEntry::transparent ? mappings[protoPaletteID] : 0];
- for (uint32_t y = 0; y < 8; ++y) {- uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y);
- output.sputc(bitplanes & 0xFF);
- if (options.bitDepth == 2) {- output.sputc(bitplanes >> 8);
- }
- }
- ++iter;
-
- --remainingTiles;
- if (remainingTiles == 0) {- break;
- }
- }
- assert(remainingTiles == 0);
- assert(iter + options.trim == attrmap.end());
-}
-
-static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
- DefaultInitVec<size_t> const &mappings) {- std::optional<std::filebuf> tilemapOutput, attrmapOutput;
- if (!options.tilemap.empty()) {- tilemapOutput.emplace();
- tilemapOutput->open(options.tilemap, std::ios_base::out | std::ios_base::binary);
- }
- if (!options.attrmap.empty()) {- attrmapOutput.emplace();
- attrmapOutput->open(options.attrmap, std::ios_base::out | std::ios_base::binary);
- }
-
- uint8_t tileID = 0;
- uint8_t bank = 0;
- auto iter = attrmap.begin();
- for ([[maybe_unused]] auto tile : png.visitAsTiles(options.columnMajor)) {- if (tileID == options.maxNbTiles[bank]) {- assert(bank == 0);
- bank = 1;
- tileID = 0;
- }
-
- if (tilemapOutput.has_value()) {- tilemapOutput->sputc(tileID + options.baseTileIDs[bank]);
- }
- if (attrmapOutput.has_value()) {- uint8_t palID = mappings[iter->protoPaletteID] & 7;
- attrmapOutput->sputc(palID | bank << 3); // The other flags are all 0
- ++iter;
- }
- ++tileID;
- }
- assert(iter == attrmap.end());
-}
-
-} // namespace unoptimized
-
-namespace optimized {-
-struct UniqueTiles {- std::unordered_set<TileData> tileset;
- std::vector<TileData const *> tiles;
-
- UniqueTiles() = default;
- // Copies are likely to break pointers, so we really don't want those.
- // Copy elision should be relied on to be more sure that refs won't be invalidated, too!
- UniqueTiles(UniqueTiles const &) = delete;
- UniqueTiles(UniqueTiles &&) = default;
-
- /**
- * Adds a tile to the collection, and returns its ID
- */
- std::tuple<uint16_t, TileData::MatchType> addTile(Png::TilesVisitor::Tile const &tile,
- Palette const &palette) {- TileData newTile(tile, palette);
- auto [tileData, inserted] = tileset.insert(newTile);
-
- TileData::MatchType matchType = TileData::EXACT;
- if (inserted) {- // Give the new tile the next available unique ID
- tileData->tileID = static_cast<uint16_t>(tiles.size());
- // Pointers are never invalidated!
- tiles.emplace_back(&*tileData);
- } else {- matchType = tileData->tryMatching(newTile);
- }
- return {tileData->tileID, matchType};- }
-
- auto size() const { return tiles.size(); }-
- auto begin() const { return tiles.begin(); }- auto end() const { return tiles.end(); }-};
-
-/**
- * Generate tile data while deduplicating unique tiles (via mirroring if enabled)
- * Additionally, while we have the info handy, convert from the 16-bit "global" tile IDs to
- * 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially
- * twice)
- */
-static UniqueTiles dedupTiles(Png const &png, DefaultInitVec<AttrmapEntry> &attrmap,
- std::vector<Palette> const &palettes,
- DefaultInitVec<size_t> const &mappings) {- // Iterate throughout the image, generating tile data as we go
- // (We don't need the full tile data to be able to dedup tiles, but we don't lose anything
- // by caching the full tile data anyway, so we might as well.)
- UniqueTiles tiles;
-
- auto iter = attrmap.begin();
- for (auto tile : png.visitAsTiles(options.columnMajor)) {- auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[iter->protoPaletteID]]);
-
- iter->xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
- iter->yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
- iter->bank = tileID >= options.maxNbTiles[0];
- iter->tileID = (iter->bank ? tileID - options.maxNbTiles[0] : tileID)
- + options.baseTileIDs[iter->bank];
-
- ++iter;
- }
- assert(iter == attrmap.end());
-
- // Copy elision should prevent the contained `unordered_set` from being re-constructed
- return tiles;
-}
-
-static void outputTileData(UniqueTiles const &tiles) {- std::filebuf output;
- output.open(options.output, std::ios_base::out | std::ios_base::binary);
-
- uint16_t tileID = 0;
- 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);
- }
-}
-
-static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {- std::filebuf output;
- output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
-
- for (AttrmapEntry const &entry : attrmap) {- output.sputc(entry.tileID); // The tile ID has already been converted
- }
-}
-
-static void outputAttrmap(DefaultInitVec<AttrmapEntry> const &attrmap,
- DefaultInitVec<size_t> const &mappings) {- std::filebuf output;
- output.open(options.attrmap, std::ios_base::out | std::ios_base::binary);
-
- for (AttrmapEntry const &entry : attrmap) {- uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6;
- attr |= entry.bank << 3;
- attr |= mappings[entry.protoPaletteID] & 7;
- output.sputc(attr);
- }
-}
-
-} // namespace optimized
-
-void process() {- options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
-
- options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
- Png png(options.input); // This also sets `hasTransparentPixels` as a side effect
- ImagePalette const &colors = png.getColors();
-
- // Now, we have all the image's colors in `colors`
- // The next step is to order the palette
-
- if (options.verbosity >= Options::VERB_INTERM) {- fputs("Image colors: [ ", stderr);- for (auto const &slot : colors) {- if (!slot.has_value()) {- continue;
- }
- fprintf(stderr, "#%08x, ", slot->toCSS());
- }
- fputs("]\n", stderr);- }
-
- // Now, iterate through the tiles, generating proto-palettes as we go
- // We do this unconditionally because this performs the image validation (which we want to
- // perform even if no output is requested), and because it's necessary to generate any
- // output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.)
- std::vector<ProtoPalette> protoPalettes;
- DefaultInitVec<AttrmapEntry> attrmap{};-
- for (auto tile : png.visitAsTiles(options.columnMajor)) {- ProtoPalette tileColors;
- AttrmapEntry &attrs = attrmap.emplace_back();
-
- for (uint32_t y = 0; y < 8; ++y) {- for (uint32_t x = 0; x < 8; ++x) {- Rgba color = tile.pixel(x, y);
- if (!color.isTransparent()) { // Do not count transparency in for packing- tileColors.add(color.cgbColor());
- }
- }
- }
-
- if (tileColors.empty()) {- // "Empty" proto-palettes screw with the packing process, so discard those
- attrs.protoPaletteID = AttrmapEntry::transparent;
- continue;
- }
-
- // Insert the proto-palette, making sure to avoid overlaps
- for (size_t n = 0; n < protoPalettes.size(); ++n) {- switch (tileColors.compare(protoPalettes[n])) {- case ProtoPalette::WE_BIGGER:
- protoPalettes[n] = tileColors; // Override them
- // Remove any other proto-palettes that we encompass
- // (Example [(0, 1), (0, 2)], inserting (0, 1, 2))
- /* The following code does its job, except that references to the removed
- * proto-palettes are not updated, causing issues.
- * TODO: overlap might not be detrimental to the packing algorithm.
- * Investigation is necessary, especially if pathological cases are found.
-
- for (size_t i = protoPalettes.size(); --i != n;) {- if (tileColors.compare(protoPalettes[i]) == ProtoPalette::WE_BIGGER) {- protoPalettes.erase(protoPalettes.begin() + i);
- }
- }
- */
- [[fallthrough]];
-
- case ProtoPalette::THEY_BIGGER:
- // Do nothing, they already contain us
- attrs.protoPaletteID = n;
- goto contained;
-
- case ProtoPalette::NEITHER:
- break; // Keep going
- }
- }
- attrs.protoPaletteID = protoPalettes.size();
- if (protoPalettes.size() == AttrmapEntry::transparent) {- abort(); // TODO: nice error message
- }
- protoPalettes.push_back(tileColors);
-contained:;
- }
-
- options.verbosePrint(Options::VERB_INTERM, "Image contains %zu proto-palette%s\n",
- protoPalettes.size(), protoPalettes.size() != 1 ? "s" : "");
- if (options.verbosity >= Options::VERB_INTERM) {- for (auto const &protoPal : protoPalettes) {- fputs("[ ", stderr);- for (uint16_t color : protoPal) {- fprintf(stderr, "$%04x, ", color);
- }
- fputs("]\n", stderr);- }
- }
-
- // Sort the proto-palettes by size, which improves the packing algorithm's efficiency
- // 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(); });-
- auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
- ? generatePalettes(protoPalettes, png)
- : makePalsAsSpecified(protoPalettes, png);
-
- if (options.verbosity >= Options::VERB_INTERM) {- for (auto &&palette : palettes) {- fputs("{ ", stderr);- for (uint16_t colorIndex : palette) {- fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
- }
- fputs("}\n", stderr);- }
- }
-
- if (palettes.size() > options.nbPalettes) {- // If the palette generation is wrong, other (dependee) operations are likely to be
- // nonsensical, so fatal-error outright
- fatal("Generated %zu palettes, over the maximum of %" PRIu8, palettes.size(),- options.nbPalettes);
- }
-
- if (!options.palettes.empty()) {- outputPalettes(palettes);
- }
-
- // If deduplication is not happening, we just need to output the tile data and/or maps as-is
- if (!options.allowDedup) {- uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8;
-
- // Check the tile count
- if (nbTilesW * nbTilesH > options.maxNbTiles[0] + options.maxNbTiles[1]) {- fatal("Image contains %" PRIu32 " tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,- nbTilesW * nbTilesH, options.maxNbTiles[0], options.maxNbTiles[1]);
- }
-
- if (!options.output.empty()) {- options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
- unoptimized::outputTileData(png, attrmap, palettes, mappings);
- }
-
- if (!options.tilemap.empty() || !options.attrmap.empty()) {- options.verbosePrint(Options::VERB_LOG_ACT,
- "Generating unoptimized tilemap and/or attrmap...\n");
- unoptimized::outputMaps(png, attrmap, mappings);
- }
- } else {- // All of these require the deduplication process to be performed to be output
- options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
- optimized::UniqueTiles tiles = optimized::dedupTiles(png, attrmap, palettes, mappings);
-
- if (tiles.size() > options.maxNbTiles[0] + options.maxNbTiles[1]) {- fatal("Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,- tiles.size(), options.maxNbTiles[0], options.maxNbTiles[1]);
- }
-
- if (!options.output.empty()) {- options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tile data...\n");
- optimized::outputTileData(tiles);
- }
-
- if (!options.tilemap.empty()) {- options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tilemap...\n");
- optimized::outputTilemap(attrmap);
- }
-
- if (!options.attrmap.empty()) {- options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized attrmap...\n");
- optimized::outputAttrmap(attrmap, mappings);
- }
- }
-}
--- a/src/gfx/main.cpp
+++ b/src/gfx/main.cpp
@@ -24,7 +24,7 @@
#include "platform.h"
#include "version.h"
-#include "gfx/convert.hpp"
+#include "gfx/process.hpp"
using namespace std::literals::string_view_literals;
--- a/src/gfx/pal_sorting.cpp
+++ b/src/gfx/pal_sorting.cpp
@@ -7,8 +7,8 @@
#include "helpers.h"
-#include "gfx/convert.hpp"
#include "gfx/main.hpp"
+#include "gfx/process.hpp"
namespace sorting {--- /dev/null
+++ b/src/gfx/process.cpp
@@ -1,0 +1,1026 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "gfx/process.hpp"
+
+#include <algorithm>
+#include <assert.h>
+#include <cinttypes>
+#include <errno.h>
+#include <fstream>
+#include <memory>
+#include <optional>
+#include <png.h>
+#include <setjmp.h>
+#include <string.h>
+#include <tuple>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "defaultinitalloc.hpp"
+#include "helpers.h"
+
+#include "gfx/main.hpp"
+#include "gfx/pal_packing.hpp"
+#include "gfx/pal_sorting.hpp"
+#include "gfx/proto_palette.hpp"
+
+class ImagePalette {+ // Use as many slots as there are CGB colors (plus transparency)
+ std::array<std::optional<Rgba>, 0x8001> _colors;
+
+public:
+ ImagePalette() = default;
+
+ void registerColor(Rgba const &rgba) {+ decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
+
+ if (rgba.cgbColor() == Rgba::transparent) {+ options.hasTransparentPixels = true;
+ }
+
+ if (!slot.has_value()) {+ slot.emplace(rgba);
+ } else if (*slot != rgba) {+ warning("Different colors melded together (#%08x into #%08x as %04x)", rgba.toCSS(),+ slot->toCSS(), rgba.cgbColor()); // TODO: indicate position
+ }
+ }
+
+ 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(); }+};
+
+class Png {+ std::string const &path;
+ std::filebuf file{};+ png_structp png = nullptr;
+ png_infop info = nullptr;
+
+ // These are cached for speed
+ uint32_t width, height;
+ DefaultInitVec<Rgba> pixels;
+ ImagePalette colors;
+ int colorType;
+ int nbColors;
+ png_colorp embeddedPal = nullptr;
+ png_bytep transparencyPal = nullptr;
+
+ [[noreturn]] static void handleError(png_structp png, char const *msg) {+ Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
+
+ fatal("Error reading input image (\"%s\"): %s", self->path.c_str(), msg);+ }
+
+ static void handleWarning(png_structp png, char const *msg) {+ Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
+
+ warning("In input image (\"%s\"): %s", self->path.c_str(), msg);+ }
+
+ static void readData(png_structp png, png_bytep data, size_t length) {+ Png *self = reinterpret_cast<Png *>(png_get_io_ptr(png));
+ std::streamsize expectedLen = length;
+ std::streamsize nbBytesRead = self->file.sgetn(reinterpret_cast<char *>(data), expectedLen);
+
+ if (nbBytesRead != expectedLen) {+ fatal("Error reading input image (\"%s\"): file too short (expected at least %zd more "+ "bytes after reading %lld)",
+ self->path.c_str(), length - nbBytesRead,
+ self->file.pubseekoff(0, std::ios_base::cur));
+ }
+ }
+
+public:
+ ImagePalette const &getColors() const { return colors; }+
+ int getColorType() const { return colorType; }+
+ std::tuple<int, png_const_colorp, png_bytep> getEmbeddedPal() const {+ return {nbColors, embeddedPal, transparencyPal};+ }
+
+ uint32_t getWidth() const { return width; }+
+ uint32_t getHeight() const { return height; }+
+ Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }+
+ Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }+
+ bool isSuitableForGrayscale() const {+ // Check that all of the grays don't fall into the same "bin"
+ if (colors.size() > options.maxOpaqueColors()) { // Apply the Pigeonhole Principle+ options.verbosePrint(Options::VERB_DEBUG,
+ "Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n",
+ colors.size(), options.maxOpaqueColors());
+ return false;
+ }
+ uint8_t bins = 0;
+ for (auto const &color : colors) {+ if (color->isTransparent()) {+ continue;
+ }
+ if (!color->isGray()) {+ options.verbosePrint(Options::VERB_DEBUG,
+ "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(
+ Options::VERB_DEBUG,
+ "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
+ *
+ * This code is more complicated than strictly necessary, but that's because of the API
+ * being used: the "high-level" interface doesn't provide all the transformations we need,
+ * so we use the "lower-level" one instead.
+ * We also use that occasion to only read the PNG one line at a time, since we store all of
+ * the pixel data in `pixels`, which saves on memory allocations.
+ */
+ explicit Png(std::string const &filePath) : path(filePath), colors() {+ if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {+ fatal("Failed to open input image (\"%s\"): %s", path.c_str(), strerror(errno));+ }
+
+ options.verbosePrint(Options::VERB_LOG_ACT, "Opened input file\n");
+
+ std::array<unsigned char, 8> pngHeader;
+
+ if (file.sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
+ != static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
+ || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {+ fatal("Input file (\"%s\") is not a PNG image!", path.c_str());+ }
+
+ options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
+
+ png = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)this, handleError,
+ handleWarning);
+ if (!png) {+ fatal("Failed to allocate PNG structure: %s", strerror(errno));+ }
+
+ info = png_create_info_struct(png);
+ if (!info) {+ png_destroy_read_struct(&png, nullptr, nullptr);
+ fatal("Failed to allocate PNG info structure: %s", strerror(errno));+ }
+
+ png_set_read_fn(png, this, readData);
+ png_set_sig_bytes(png, pngHeader.size());
+
+ // TODO: png_set_crc_action(png, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
+
+ // Skipping chunks we don't use should improve performance
+ // TODO: png_set_keep_unknown_chunks(png, ...);
+
+ // Process all chunks up to but not including the image data
+ png_read_info(png, info);
+
+ int bitDepth, interlaceType; //, compressionType, filterMethod;
+
+ png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr,
+ nullptr);
+
+ if (width % 8 != 0) {+ fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);+ }
+ if (height % 8 != 0) {+ fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);+ }
+
+ pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
+
+ auto colorTypeName = [this]() {+ switch (colorType) {+ case PNG_COLOR_TYPE_GRAY:
+ return "grayscale";
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
+ return "grayscale + alpha";
+ case PNG_COLOR_TYPE_PALETTE:
+ return "palette";
+ case PNG_COLOR_TYPE_RGB:
+ return "RGB";
+ case PNG_COLOR_TYPE_RGB_ALPHA:
+ return "RGB + alpha";
+ default:
+ fatal("Unknown color type %d", colorType);+ }
+ };
+ auto interlaceTypeName = [&interlaceType]() {+ switch (interlaceType) {+ case PNG_INTERLACE_NONE:
+ return "not interlaced";
+ case PNG_INTERLACE_ADAM7:
+ return "interlaced (Adam7)";
+ default:
+ fatal("Unknown interlace type %d", interlaceType);+ }
+ };
+ options.verbosePrint(Options::VERB_INTERM,
+ "Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", height,
+ width, bitDepth, colorTypeName(), interlaceTypeName());
+
+ if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {+ int nbTransparentEntries;
+ if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {+ assert(nbTransparentEntries == nbColors);
+ }
+
+ options.verbosePrint(Options::VERB_INTERM, "Embedded palette has %d colors: [",
+ nbColors);
+ for (int i = 0; i < nbColors; ++i) {+ auto const &color = embeddedPal[i];
+ options.verbosePrint(
+ Options::VERB_INTERM, "#%02x%02x%02x%02x%s", color.red, color.green, color.blue,
+ transparencyPal ? transparencyPal[i] : 0xFF, i != nbColors - 1 ? ", " : "]\n");
+ }
+ } else {+ options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n");
+ }
+
+ // Set up transformations; to turn everything into RGBA888
+ // TODO: it's not necessary to uniformize the pixel data (in theory), and not doing
+ // so *might* improve performance, and should reduce memory usage.
+
+ // Convert grayscale to RGB
+ switch (colorType & ~PNG_COLOR_MASK_ALPHA) {+ case PNG_COLOR_TYPE_GRAY:
+ png_set_gray_to_rgb(png); // This also converts tRNS to alpha
+ break;
+ case PNG_COLOR_TYPE_PALETTE:
+ png_set_palette_to_rgb(png);
+ break;
+ }
+
+ if (png_get_valid(png, info, PNG_INFO_tRNS)) {+ // If we read a tRNS chunk, convert it to alpha
+ png_set_tRNS_to_alpha(png);
+ } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {+ // Otherwise, if we lack an alpha channel, default to full opacity
+ png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
+ }
+
+ // Scale 16bpp back to 8 (we don't need all of that precision anyway)
+ if (bitDepth == 16) {+ png_set_scale_16(png);
+ } else if (bitDepth < 8) {+ png_set_packing(png);
+ }
+
+ // Set interlace handling (MUST be done before `png_read_update_info`)
+ int nbPasses = png_set_interlace_handling(png);
+
+ // Update `info` with the transformations
+ png_read_update_info(png, info);
+ // These shouldn't have changed
+ assert(png_get_image_width(png, info) == width);
+ assert(png_get_image_height(png, info) == height);
+ // These should have changed, however
+ assert(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
+ assert(png_get_bit_depth(png, info) == 8);
+
+ // Now that metadata has been read, we can process the image data
+
+ std::vector<png_byte> row(png_get_rowbytes(png, info));
+
+ if (interlaceType == PNG_INTERLACE_NONE) {+ for (png_uint_32 y = 0; y < height; ++y) {+ png_read_row(png, row.data(), nullptr);
+
+ for (png_uint_32 x = 0; x < width; ++x) {+ Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
+ colors.registerColor(rgba);
+ pixel(x, y) = rgba;
+ }
+ }
+ } else {+ // For interlace to work properly, we must read the image `nbPasses` times
+ for (int pass = 0; pass < nbPasses; ++pass) {+ // The interlacing pass must be skipped if its width or height is reported as zero
+ if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {+ continue;
+ }
+
+ png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
+ png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
+
+ for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {+ png_bytep ptr = row.data();
+ png_read_row(png, ptr, nullptr);
+
+ for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {+ Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
+ colors.registerColor(rgba);
+ pixel(x, y) = rgba;
+ ptr += 4;
+ }
+ }
+ }
+ }
+
+ // 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); }+
+ class TilesVisitor {+ Png const &_png;
+ bool const _columnMajor;
+ uint32_t const _width, _height;
+ uint32_t const _limit = _columnMajor ? _height : _width;
+
+ public:
+ TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height)
+ : _png(png), _columnMajor(columnMajor), _width(width), _height(height) {}+
+ class Tile {+ Png const &_png;
+ uint32_t const _x, _y;
+
+ public:
+ Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {}+
+ Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {+ return _png.pixel(_x + xOfs, _y + yOfs);
+ }
+ };
+
+ private:
+ struct iterator {+ TilesVisitor const &parent;
+ uint32_t const limit;
+ uint32_t x, y;
+
+ std::pair<uint32_t, uint32_t> coords() const { return {x, y}; }+ Tile operator*() const { return {parent._png, x, y}; }+
+ iterator &operator++() {+ auto [major, minor] = parent._columnMajor ? std::tie(y, x) : std::tie(x, y);
+ major += 8;
+ if (major == limit) {+ minor += 8;
+ major = 0;
+ }
+ return *this;
+ }
+
+ bool operator!=(iterator const &rhs) const {+ return coords() != rhs.coords(); // Compare the returned coord pairs
+ }
+ };
+
+ public:
+ 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!
+ }
+ };
+public:
+ TilesVisitor visitAsTiles(bool columnMajor) const {+ return {*this, columnMajor, width, height};+ }
+};
+
+class RawTiles {+ /**
+ * A tile which only contains indices into the image's global palette
+ */
+ class RawTile {+ std::array<std::array<size_t, 8>, 8> _pixelIndices{};+
+ public:
+ // Not super clean, but it's closer to matrix notation
+ size_t &operator()(size_t x, size_t y) { return _pixelIndices[y][x]; }+ };
+
+private:
+ std::vector<RawTile> _tiles;
+
+public:
+ /**
+ * Creates a new raw tile, and returns a reference to it so it can be filled in
+ */
+ RawTile &newTile() {+ _tiles.emplace_back();
+ return _tiles.back();
+ }
+};
+
+struct AttrmapEntry {+ size_t protoPaletteID; // Only this field is used when outputting "unoptimized" data
+ uint8_t tileID; // This is the ID as it will be output to the tilemap
+ bool bank;
+ bool yFlip;
+ bool xFlip;
+
+ static constexpr decltype(protoPaletteID) transparent = SIZE_MAX;
+};
+
+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.verbosity >= Options::VERB_INTERM) {+ fprintf(stderr, "Proto-palette mappings: (%zu palette%s)\n", nbPalettes,
+ nbPalettes != 1 ? "s" : "");
+ for (size_t i = 0; i < mappings.size(); ++i) {+ fprintf(stderr, "%zu -> %zu\n", i, mappings[i]);
+ }
+ }
+
+ std::vector<Palette> palettes(nbPalettes);
+ // If the image contains at least one transparent pixel, force transparency in the first slot of
+ // all palettes
+ if (options.hasTransparentPixels) {+ for (Palette &pal : palettes) {+ pal.colors[0] = Rgba::transparent;
+ }
+ }
+ // 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.maxOpaqueColors()) { // Ignore extraneous colors if they are unused+ embPalSize = options.maxOpaqueColors();
+ }
+ 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.nbColorsPerPal; ++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);
+
+ for (Palette const &palette : palettes) {+ for (uint8_t i = 0; i < options.nbColorsPerPal; ++i) {+ uint16_t color = palette.colors[i]; // Will return `UINT16_MAX` for unused slots
+ output.sputc(color & 0xFF);
+ output.sputc(color >> 8);
+ }
+ }
+}
+
+static uint8_t flip(uint8_t byte) {+ // To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
+ byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
+ byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
+ byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
+ return byte;
+}
+
+class TileData {+ std::array<uint8_t, 16> _data;
+ // The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
+ // if horizontal mirroring is in effect. It should still be a reasonable tie-breaker in
+ // non-pathological cases.
+ uint16_t _hash;
+public:
+ // This is an index within the "global" pool; no bank info is encoded here
+ // It's marked as `mutable` so that it can be modified even on a `const` object;
+ // this is necessary because the `set` in which it's inserted refuses any modification for fear
+ // of altering the element's hash, but the tile ID is not part of it.
+ mutable uint16_t tileID;
+
+ static uint16_t rowBitplanes(Png::TilesVisitor::Tile const &tile, Palette const &palette,
+ uint32_t y) {+ uint16_t row = 0;
+ for (uint32_t x = 0; x < 8; ++x) {+ row <<= 1;
+ uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
+ if (index & 1) {+ row |= 1;
+ }
+ if (index & 2) {+ row |= 0x100;
+ }
+ }
+ return row;
+ }
+
+ TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {+ size_t writeIndex = 0;
+ for (uint32_t y = 0; y < 8; ++y) {+ uint16_t bitplanes = rowBitplanes(tile, palette, y);
+ _data[writeIndex++] = bitplanes & 0xFF;
+ if (options.bitDepth == 2) {+ _data[writeIndex++] = bitplanes >> 8;
+ }
+
+ // Update the hash
+ _hash ^= bitplanes;
+ if (options.allowMirroring) {+ // Count the line itself as mirrorred; vertical mirroring is
+ // already taken care of because the symmetric line will be XOR'd
+ // the same way. (...which is a problem, but probably benign.)
+ _hash ^= flip(bitplanes >> 8) << 8 | flip(bitplanes & 0xFF);
+ }
+ }
+ }
+
+ auto const &data() const { return _data; }+ uint16_t hash() const { return _hash; }+
+ enum MatchType {+ NOPE,
+ EXACT,
+ HFLIP,
+ VFLIP,
+ VHFLIP,
+ };
+ MatchType tryMatching(TileData const &other) const {+ // Check for strict equality first, as that can typically be optimized, and it allows
+ // hoisting the mirroring check out of the loop
+ if (_data == other._data) {+ return MatchType::EXACT;
+ }
+
+ if (!options.allowMirroring) {+ return MatchType::NOPE;
+ }
+
+ // Check if we have horizontal mirroring, which scans the array forward again
+ if (std::equal(_data.begin(), _data.end(), other._data.begin(),
+ [](uint8_t lhs, uint8_t rhs) { return lhs == flip(rhs); })) {+ return MatchType::HFLIP;
+ }
+
+ // Check if we have vertical or vertical+horizontal mirroring, for which we have to read
+ // bitplane *pairs* backwards
+ bool hasVFlip = true, hasVHFlip = true;
+ for (uint8_t i = 0; i < _data.size(); ++i) {+ // Flip the bottom bit to get the corresponding row's bitplane 0/1
+ // (This works because the array size is even)
+ uint8_t lhs = _data[i], rhs = other._data[(15 - i) ^ 1];
+ if (lhs != rhs) {+ hasVFlip = false;
+ }
+ if (lhs != flip(rhs)) {+ hasVHFlip = false;
+ }
+ if (!hasVFlip && !hasVHFlip) {+ return MatchType::NOPE; // If both have been eliminated, all hope is lost!
+ }
+ }
+
+ // If we have both (i.e. we have symmetry), default to vflip only
+ assert(hasVFlip || hasVHFlip);
+ return hasVFlip ? MatchType::VFLIP : MatchType::VHFLIP;
+ }
+ friend bool operator==(TileData const &lhs, TileData const &rhs) {+ return lhs.tryMatching(rhs) != MatchType::NOPE;
+ }
+};
+
+template<>
+struct std::hash<TileData> {+ std::size_t operator()(TileData const &tile) const { return tile.hash(); }+};
+
+namespace unoptimized {+
+// 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) {+ 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)) {+ size_t protoPaletteID = iter->protoPaletteID;
+ // If the tile is fully transparent, default to palette 0
+ Palette const &palette =
+ palettes[protoPaletteID != AttrmapEntry::transparent ? mappings[protoPaletteID] : 0];
+ for (uint32_t y = 0; y < 8; ++y) {+ uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y);
+ output.sputc(bitplanes & 0xFF);
+ if (options.bitDepth == 2) {+ output.sputc(bitplanes >> 8);
+ }
+ }
+ ++iter;
+
+ --remainingTiles;
+ if (remainingTiles == 0) {+ break;
+ }
+ }
+ assert(remainingTiles == 0);
+ assert(iter + options.trim == attrmap.end());
+}
+
+static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
+ DefaultInitVec<size_t> const &mappings) {+ std::optional<std::filebuf> tilemapOutput, attrmapOutput;
+ if (!options.tilemap.empty()) {+ tilemapOutput.emplace();
+ tilemapOutput->open(options.tilemap, std::ios_base::out | std::ios_base::binary);
+ }
+ if (!options.attrmap.empty()) {+ attrmapOutput.emplace();
+ attrmapOutput->open(options.attrmap, std::ios_base::out | std::ios_base::binary);
+ }
+
+ uint8_t tileID = 0;
+ uint8_t bank = 0;
+ auto iter = attrmap.begin();
+ for ([[maybe_unused]] auto tile : png.visitAsTiles(options.columnMajor)) {+ if (tileID == options.maxNbTiles[bank]) {+ assert(bank == 0);
+ bank = 1;
+ tileID = 0;
+ }
+
+ if (tilemapOutput.has_value()) {+ tilemapOutput->sputc(tileID + options.baseTileIDs[bank]);
+ }
+ if (attrmapOutput.has_value()) {+ uint8_t palID = mappings[iter->protoPaletteID] & 7;
+ attrmapOutput->sputc(palID | bank << 3); // The other flags are all 0
+ ++iter;
+ }
+ ++tileID;
+ }
+ assert(iter == attrmap.end());
+}
+
+} // namespace unoptimized
+
+namespace optimized {+
+struct UniqueTiles {+ std::unordered_set<TileData> tileset;
+ std::vector<TileData const *> tiles;
+
+ UniqueTiles() = default;
+ // Copies are likely to break pointers, so we really don't want those.
+ // Copy elision should be relied on to be more sure that refs won't be invalidated, too!
+ UniqueTiles(UniqueTiles const &) = delete;
+ UniqueTiles(UniqueTiles &&) = default;
+
+ /**
+ * Adds a tile to the collection, and returns its ID
+ */
+ std::tuple<uint16_t, TileData::MatchType> addTile(Png::TilesVisitor::Tile const &tile,
+ Palette const &palette) {+ TileData newTile(tile, palette);
+ auto [tileData, inserted] = tileset.insert(newTile);
+
+ TileData::MatchType matchType = TileData::EXACT;
+ if (inserted) {+ // Give the new tile the next available unique ID
+ tileData->tileID = static_cast<uint16_t>(tiles.size());
+ // Pointers are never invalidated!
+ tiles.emplace_back(&*tileData);
+ } else {+ matchType = tileData->tryMatching(newTile);
+ }
+ return {tileData->tileID, matchType};+ }
+
+ auto size() const { return tiles.size(); }+
+ auto begin() const { return tiles.begin(); }+ auto end() const { return tiles.end(); }+};
+
+/**
+ * Generate tile data while deduplicating unique tiles (via mirroring if enabled)
+ * Additionally, while we have the info handy, convert from the 16-bit "global" tile IDs to
+ * 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially
+ * twice)
+ */
+static UniqueTiles dedupTiles(Png const &png, DefaultInitVec<AttrmapEntry> &attrmap,
+ std::vector<Palette> const &palettes,
+ DefaultInitVec<size_t> const &mappings) {+ // Iterate throughout the image, generating tile data as we go
+ // (We don't need the full tile data to be able to dedup tiles, but we don't lose anything
+ // by caching the full tile data anyway, so we might as well.)
+ UniqueTiles tiles;
+
+ auto iter = attrmap.begin();
+ for (auto tile : png.visitAsTiles(options.columnMajor)) {+ auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[iter->protoPaletteID]]);
+
+ iter->xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
+ iter->yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
+ iter->bank = tileID >= options.maxNbTiles[0];
+ iter->tileID = (iter->bank ? tileID - options.maxNbTiles[0] : tileID)
+ + options.baseTileIDs[iter->bank];
+
+ ++iter;
+ }
+ assert(iter == attrmap.end());
+
+ // Copy elision should prevent the contained `unordered_set` from being re-constructed
+ return tiles;
+}
+
+static void outputTileData(UniqueTiles const &tiles) {+ std::filebuf output;
+ output.open(options.output, std::ios_base::out | std::ios_base::binary);
+
+ uint16_t tileID = 0;
+ 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);
+ }
+}
+
+static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {+ std::filebuf output;
+ output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
+
+ for (AttrmapEntry const &entry : attrmap) {+ output.sputc(entry.tileID); // The tile ID has already been converted
+ }
+}
+
+static void outputAttrmap(DefaultInitVec<AttrmapEntry> const &attrmap,
+ DefaultInitVec<size_t> const &mappings) {+ std::filebuf output;
+ output.open(options.attrmap, std::ios_base::out | std::ios_base::binary);
+
+ for (AttrmapEntry const &entry : attrmap) {+ uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6;
+ attr |= entry.bank << 3;
+ attr |= mappings[entry.protoPaletteID] & 7;
+ output.sputc(attr);
+ }
+}
+
+} // namespace optimized
+
+void process() {+ options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
+
+ options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
+ Png png(options.input); // This also sets `hasTransparentPixels` as a side effect
+ ImagePalette const &colors = png.getColors();
+
+ // Now, we have all the image's colors in `colors`
+ // The next step is to order the palette
+
+ if (options.verbosity >= Options::VERB_INTERM) {+ fputs("Image colors: [ ", stderr);+ for (auto const &slot : colors) {+ if (!slot.has_value()) {+ continue;
+ }
+ fprintf(stderr, "#%08x, ", slot->toCSS());
+ }
+ fputs("]\n", stderr);+ }
+
+ // Now, iterate through the tiles, generating proto-palettes as we go
+ // We do this unconditionally because this performs the image validation (which we want to
+ // perform even if no output is requested), and because it's necessary to generate any
+ // output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.)
+ std::vector<ProtoPalette> protoPalettes;
+ DefaultInitVec<AttrmapEntry> attrmap{};+
+ for (auto tile : png.visitAsTiles(options.columnMajor)) {+ ProtoPalette tileColors;
+ AttrmapEntry &attrs = attrmap.emplace_back();
+
+ for (uint32_t y = 0; y < 8; ++y) {+ for (uint32_t x = 0; x < 8; ++x) {+ Rgba color = tile.pixel(x, y);
+ if (!color.isTransparent()) { // Do not count transparency in for packing+ tileColors.add(color.cgbColor());
+ }
+ }
+ }
+
+ if (tileColors.empty()) {+ // "Empty" proto-palettes screw with the packing process, so discard those
+ attrs.protoPaletteID = AttrmapEntry::transparent;
+ continue;
+ }
+
+ // Insert the proto-palette, making sure to avoid overlaps
+ for (size_t n = 0; n < protoPalettes.size(); ++n) {+ switch (tileColors.compare(protoPalettes[n])) {+ case ProtoPalette::WE_BIGGER:
+ protoPalettes[n] = tileColors; // Override them
+ // Remove any other proto-palettes that we encompass
+ // (Example [(0, 1), (0, 2)], inserting (0, 1, 2))
+ /* The following code does its job, except that references to the removed
+ * proto-palettes are not updated, causing issues.
+ * TODO: overlap might not be detrimental to the packing algorithm.
+ * Investigation is necessary, especially if pathological cases are found.
+
+ for (size_t i = protoPalettes.size(); --i != n;) {+ if (tileColors.compare(protoPalettes[i]) == ProtoPalette::WE_BIGGER) {+ protoPalettes.erase(protoPalettes.begin() + i);
+ }
+ }
+ */
+ [[fallthrough]];
+
+ case ProtoPalette::THEY_BIGGER:
+ // Do nothing, they already contain us
+ attrs.protoPaletteID = n;
+ goto contained;
+
+ case ProtoPalette::NEITHER:
+ break; // Keep going
+ }
+ }
+ attrs.protoPaletteID = protoPalettes.size();
+ if (protoPalettes.size() == AttrmapEntry::transparent) {+ abort(); // TODO: nice error message
+ }
+ protoPalettes.push_back(tileColors);
+contained:;
+ }
+
+ options.verbosePrint(Options::VERB_INTERM, "Image contains %zu proto-palette%s\n",
+ protoPalettes.size(), protoPalettes.size() != 1 ? "s" : "");
+ if (options.verbosity >= Options::VERB_INTERM) {+ for (auto const &protoPal : protoPalettes) {+ fputs("[ ", stderr);+ for (uint16_t color : protoPal) {+ fprintf(stderr, "$%04x, ", color);
+ }
+ fputs("]\n", stderr);+ }
+ }
+
+ // Sort the proto-palettes by size, which improves the packing algorithm's efficiency
+ // 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(); });+
+ auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
+ ? generatePalettes(protoPalettes, png)
+ : makePalsAsSpecified(protoPalettes, png);
+
+ if (options.verbosity >= Options::VERB_INTERM) {+ for (auto &&palette : palettes) {+ fputs("{ ", stderr);+ for (uint16_t colorIndex : palette) {+ fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
+ }
+ fputs("}\n", stderr);+ }
+ }
+
+ if (palettes.size() > options.nbPalettes) {+ // If the palette generation is wrong, other (dependee) operations are likely to be
+ // nonsensical, so fatal-error outright
+ fatal("Generated %zu palettes, over the maximum of %" PRIu8, palettes.size(),+ options.nbPalettes);
+ }
+
+ if (!options.palettes.empty()) {+ outputPalettes(palettes);
+ }
+
+ // If deduplication is not happening, we just need to output the tile data and/or maps as-is
+ if (!options.allowDedup) {+ uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8;
+
+ // Check the tile count
+ if (nbTilesW * nbTilesH > options.maxNbTiles[0] + options.maxNbTiles[1]) {+ fatal("Image contains %" PRIu32 " tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,+ nbTilesW * nbTilesH, options.maxNbTiles[0], options.maxNbTiles[1]);
+ }
+
+ if (!options.output.empty()) {+ options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
+ unoptimized::outputTileData(png, attrmap, palettes, mappings);
+ }
+
+ if (!options.tilemap.empty() || !options.attrmap.empty()) {+ options.verbosePrint(Options::VERB_LOG_ACT,
+ "Generating unoptimized tilemap and/or attrmap...\n");
+ unoptimized::outputMaps(png, attrmap, mappings);
+ }
+ } else {+ // All of these require the deduplication process to be performed to be output
+ options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
+ optimized::UniqueTiles tiles = optimized::dedupTiles(png, attrmap, palettes, mappings);
+
+ if (tiles.size() > options.maxNbTiles[0] + options.maxNbTiles[1]) {+ fatal("Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,+ tiles.size(), options.maxNbTiles[0], options.maxNbTiles[1]);
+ }
+
+ if (!options.output.empty()) {+ options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tile data...\n");
+ optimized::outputTileData(tiles);
+ }
+
+ if (!options.tilemap.empty()) {+ options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tilemap...\n");
+ optimized::outputTilemap(attrmap);
+ }
+
+ if (!options.attrmap.empty()) {+ options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized attrmap...\n");
+ optimized::outputAttrmap(attrmap, mappings);
+ }
+ }
+}
--
⑨