Implement enough functionality to compile & match pokecrystal

This commit is contained in:
ISSOtm
2022-03-04 23:49:55 +01:00
committed by Eldred Habert
parent 6e406b22bb
commit 3fa1854332
12 changed files with 460 additions and 183 deletions

View File

@@ -110,6 +110,7 @@ rgbgfx_obj := \
src/gfx/pal_packing.o \ src/gfx/pal_packing.o \
src/gfx/pal_sorting.o \ src/gfx/pal_sorting.o \
src/gfx/proto_palette.o \ src/gfx/proto_palette.o \
src/gfx/rgba.o \
src/extern/getopt.o \ src/extern/getopt.o \
src/error.o src/error.o

View File

@@ -9,59 +9,6 @@
#ifndef RGBDS_GFX_CONVERT_HPP #ifndef RGBDS_GFX_CONVERT_HPP
#define RGBDS_GFX_CONVERT_HPP #define RGBDS_GFX_CONVERT_HPP
#include <assert.h>
#include <stdint.h>
#include "gfx/main.hpp"
struct Rgba {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
Rgba(uint32_t rgba) : red(rgba), green(rgba >> 8), blue(rgba >> 16), alpha(rgba >> 24) {}
operator uint32_t() const { return toCSS(); }
/**
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
* representation
*/
uint32_t toCSS() const {
auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
bool operator!=(Rgba const &other) const {
return static_cast<uint32_t>(*this) != static_cast<uint32_t>(other);
}
bool isGray() const { return red == green && green == blue; }
/**
* CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
* Since the rest of the bits don't matter then, we return 0x8000 exactly.
*/
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
/**
* All alpha values strictly below this will be considered transparent
*/
static constexpr uint8_t opacity_threshold = 0xF0; // TODO: adjust this
/**
* Computes the equivalent CGB color, respects the color curve depending on options
*/
uint16_t cgbColor() const {
if (alpha < opacity_threshold)
return transparent;
if (options.useColorCurve) {
assert(!"TODO");
} else {
return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
}
}
};
void process(); void process();
#endif /* RGBDS_GFX_CONVERT_HPP */ #endif /* RGBDS_GFX_CONVERT_HPP */

View File

@@ -13,22 +13,33 @@
#include <filesystem> #include <filesystem>
#include <limits.h> #include <limits.h>
#include <stdint.h> #include <stdint.h>
#include <vector>
#include "helpers.h" #include "helpers.h"
#include "gfx/rgba.hpp"
struct Options { struct Options {
bool beVerbose = false; // -v bool beVerbose = false; // -v
bool fixInput = false; // -f 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 allowMirroring = false; // -m
bool allowDedup = false; // -u bool allowDedup = false; // -u
bool useColorCurve = false; // -C bool useColorCurve = false; // -C
uint8_t bitDepth = 2; // -d uint8_t bitDepth = 2; // -d
uint64_t trim = 0; // -x uint64_t trim = 0; // -x
uint8_t nbPalettes = 8; // TODO uint8_t nbPalettes = 8; // -n
uint8_t nbColorsPerPal = 0; // TODO; 0 means "auto" = 1 << bitDepth; uint8_t nbColorsPerPal = 0; // -s; 0 means "auto" = 1 << bitDepth;
std::array<uint8_t, 2> baseTileIDs{0, 0}; // TODO enum {
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // TODO NO_SPEC,
EXPLICIT,
EMBEDDED,
} palSpecType = NO_SPEC; // -c
std::vector<std::array<Rgba, 4>> palSpec{};
std::array<uint16_t, 2> unitSize{1, 1}; // -u (in tiles)
std::array<uint32_t, 4> inputSlice; // -L
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
std::filesystem::path tilemap{}; // -t, -T std::filesystem::path tilemap{}; // -t, -T
std::filesystem::path attrmap{}; // -a, -A std::filesystem::path attrmap{}; // -a, -A
std::filesystem::path palettes{}; // -p, -P std::filesystem::path palettes{}; // -p, -P
@@ -37,8 +48,8 @@ struct Options {
format_(printf, 2, 3) void verbosePrint(char const *fmt, ...) const; format_(printf, 2, 3) void verbosePrint(char const *fmt, ...) const;
uint8_t maxPalSize() const { uint8_t maxPalSize() const {
return nbColorsPerPal; return nbColorsPerPal; // TODO: minus 1 when transparency is active
} // TODO: minus 1 when transparency is active }
}; };
extern Options options; extern Options options;
@@ -53,6 +64,8 @@ struct Palette {
void addColor(uint16_t color); void addColor(uint16_t color);
uint8_t indexOf(uint16_t color) const; 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 begin();
decltype(colors)::iterator end(); decltype(colors)::iterator end();

View File

@@ -9,16 +9,22 @@
#ifndef RGBDS_GFX_PAL_SORTING_HPP #ifndef RGBDS_GFX_PAL_SORTING_HPP
#define RGBDS_GFX_PAL_SORTING_HPP #define RGBDS_GFX_PAL_SORTING_HPP
#include <array>
#include <assert.h>
#include <optional>
#include <png.h> #include <png.h>
#include <vector> #include <vector>
#include "gfx/rgba.hpp"
class Palette; class Palette;
namespace sorting { namespace sorting {
void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB, void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
png_byte *palAlpha); png_byte *palAlpha);
void grayscale(std::vector<Palette> &palettes); void grayscale(std::vector<Palette> &palettes,
std::array<std::optional<Rgba>, 0x8001> const &colors);
void rgb(std::vector<Palette> &palettes); void rgb(std::vector<Palette> &palettes);
} }

58
include/gfx/rgba.hpp Normal file
View File

@@ -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 <stdint.h>
struct Rgba {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
/**
* Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
*/
explicit Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
/**
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
* representation
*/
uint32_t toCSS() const {
auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
friend bool operator!=(Rgba const &lhs, Rgba const &rhs) { return lhs.toCSS() != rhs.toCSS(); }
/**
* CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
* Since the rest of the bits don't matter then, we return 0x8000 exactly.
*/
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
/**
* All alpha values strictly below this will be considered transparent
*/
static constexpr uint8_t opacity_threshold = 0xF0; // TODO: adjust this
// TODO: also a transparency threshold, and error out on "middle" values
bool isTransparent() const { return alpha < opacity_threshold; }
/**
* Computes the equivalent CGB color, respects the color curve depending on options
*/
uint16_t cgbColor() const;
bool isGray() const { return red == green && green == blue; }
uint8_t grayIndex() const;
};
#endif /* RGBDS_GFX_RGBA_HPP */

View File

@@ -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. 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 . .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 .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 .Dq bins
.Pq white, light gray, dark gray, and black in this order , as there are colors per palette, and the palette is set to these bins.
and the palette is set to these four bins. The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
(TODO: how does this interact with 1bpp? With more than 1 palette?) If two distinct grays end up in the same bin, the RGB method is used instead.
If more than one grey ends up in the same bin, the RGB method below is used instead.
.It .It
If none of the above apply, colors are sorted from lightest to darkest. 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. .EQ
It is kept for compatibility.) 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 .El
.Pp .Pp
Note that the Note that the

View File

@@ -75,6 +75,7 @@ set(rgbgfx_src
"gfx/pal_packing.cpp" "gfx/pal_packing.cpp"
"gfx/pal_sorting.cpp" "gfx/pal_sorting.cpp"
"gfx/proto_palette.cpp" "gfx/proto_palette.cpp"
"gfx/rgba.cpp"
"extern/getopt.c" "extern/getopt.c"
"error.c" "error.c"
) )

View File

@@ -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 begin() const { return _colors.begin(); }
auto end() const { return _colors.end(); } auto end() const { return _colors.end(); }
}; };
@@ -64,13 +69,12 @@ class Png {
// These are cached for speed // These are cached for speed
uint32_t width, height; uint32_t width, height;
DefaultInitVec<uint16_t> pixels; std::vector<Rgba> pixels;
ImagePalette colors; ImagePalette colors;
int colorType; int colorType;
int nbColors; int nbColors;
png_colorp embeddedPal = nullptr; png_colorp embeddedPal = nullptr;
png_bytep transparencyPal = nullptr; png_bytep transparencyPal = nullptr;
bool isGrayOnly = true;
[[noreturn]] static void handleError(png_structp png, char const *msg) { [[noreturn]] static void handleError(png_structp png, char const *msg) {
struct Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png)); struct Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
@@ -110,11 +114,38 @@ public:
uint32_t getHeight() const { return height; } 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 * 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]); Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
colors.registerColor(rgba); colors.registerColor(rgba);
pixel(x, y) = rgba.cgbColor(); pixel(x, y) = rgba;
isGrayOnly &= rgba.isGray();
} }
} }
} else { } else {
@@ -299,16 +329,15 @@ public:
Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]); Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
colors.registerColor(rgba); colors.registerColor(rgba);
pixel(x, y) = rgba.cgbColor(); pixel(x, y) = rgba;
isGrayOnly &= rgba.isGray();
ptr += 4; ptr += 4;
} }
} }
} }
} }
png_read_end(png, // We don't care about chunks after the image data (comments, etc.)
nullptr); // 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); } ~Png() { png_destroy_read_struct(&png, &info, nullptr); }
@@ -330,7 +359,7 @@ public:
public: public:
Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {} 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); return _png.pixel(_x + xOfs, _y + yOfs);
} }
}; };
@@ -362,10 +391,10 @@ public:
}; };
public: public:
iterator begin() const { return {*this, _width, 0, 0}; } iterator begin() const { return {*this, _limit, 0, 0}; }
iterator end() const { iterator end() const {
iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one...
return ++it; // Now one-past-last return ++it; // ...now one-past-last!
} }
}; };
public: public:
@@ -407,6 +436,93 @@ struct AttrmapEntry {
bool xFlip; bool xFlip;
}; };
static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
// Run a "pagination" problem solver
// TODO: allow picking one of several solvers?
auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes);
assert(mappings.size() == protoPalettes.size());
if (options.beVerbose) {
options.verbosePrint("Proto-palette mappings: (%zu palette%s)\n", nbPalettes,
nbPalettes != 1 ? "s" : "");
for (size_t i = 0; i < mappings.size(); ++i) {
options.verbosePrint("%zu -> %zu\n", i, mappings[i]);
}
}
std::vector<Palette> palettes(nbPalettes);
// Generate the actual palettes from the mappings
for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
auto &pal = palettes[mappings[protoPalID]];
for (uint16_t color : protoPalettes[protoPalID]) {
pal.addColor(color);
}
}
// "Sort" colors in the generated palettes, see the man page for the flowchart
auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
if (embPalRGB != nullptr) {
sorting::indexed(palettes, embPalSize, embPalRGB, embPalAlpha);
} else if (png.isSuitableForGrayscale()) {
sorting::grayscale(palettes, png.getColors().raw());
} else {
sorting::rgb(palettes);
}
return {mappings, palettes};
}
static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
makePalsAsSpecified(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
if (options.palSpecType == Options::EMBEDDED) {
// Generate a palette spec from the first few colors in the embedded palette
auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
if (embPalRGB == nullptr) {
fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
}
// Fill in the palette spec
options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent)
assert(options.palSpec.size() == 1);
// TODO: abort if ignored colors are being used; do it now for a friendlier error
// message
if (embPalSize > options.maxPalSize()) { // Ignore extraneous colors if they are unused
embPalSize = options.maxPalSize();
}
for (int i = 0; i < embPalSize; ++i) {
options.palSpec[0][i] = Rgba(embPalRGB[i].red, embPalRGB[i].green, embPalRGB[i].blue,
embPalAlpha ? embPalAlpha[i] : 0xFF);
}
}
// Convert the palette spec to actual palettes
std::vector<Palette> palettes(options.palSpec.size());
auto palIter = palettes.begin(); // TODO: `zip`
for (auto const &spec : options.palSpec) {
for (size_t i = 0; i < options.maxPalSize(); ++i) {
(*palIter)[i] = spec[i].cgbColor();
}
++palIter;
}
// Iterate through proto-palettes, and try mapping them to the specified palettes
DefaultInitVec<size_t> mappings(protoPalettes.size());
for (size_t i = 0; i < protoPalettes.size(); ++i) {
ProtoPalette const &protoPal = protoPalettes[i];
// Find the palette...
auto iter = std::find_if(palettes.begin(), palettes.end(), [&protoPal](Palette const &pal) {
// ...which contains all colors in this proto-pal
return std::all_of(protoPal.begin(), protoPal.end(), [&pal](uint16_t color) {
return std::find(pal.begin(), pal.end(), color) != pal.end();
});
});
assert(iter != palettes.end()); // TODO: produce a proper error message
mappings[i] = iter - palettes.begin();
}
return {mappings, palettes};
}
static void outputPalettes(std::vector<Palette> const &palettes) { static void outputPalettes(std::vector<Palette> const &palettes) {
std::filebuf output; std::filebuf output;
output.open(options.palettes, std::ios_base::out | std::ios_base::binary); output.open(options.palettes, std::ios_base::out | std::ios_base::binary);
@@ -421,13 +537,19 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
namespace unoptimized { namespace unoptimized {
// TODO: this is very redundant with `TileData`; try merging both? // TODO: this is very redundant with `TileData::TileData`; try merging both?
static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap, static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
std::vector<Palette> const &palettes, std::vector<Palette> const &palettes,
DefaultInitVec<size_t> const &mappings) { DefaultInitVec<size_t> const &mappings) {
std::filebuf output; std::filebuf output;
output.open(options.output, std::ios_base::out | std::ios_base::binary); 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(); auto iter = attrmap.begin();
for (auto tile : png.visitAsTiles(options.columnMajor)) { for (auto tile : png.visitAsTiles(options.columnMajor)) {
Palette const &palette = palettes[mappings[iter->protoPaletteID]]; Palette const &palette = palettes[mappings[iter->protoPaletteID]];
@@ -435,7 +557,7 @@ static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &a
uint16_t row = 0; uint16_t row = 0;
for (uint32_t x = 0; x < 8; ++x) { for (uint32_t x = 0; x < 8; ++x) {
row <<= 1; row <<= 1;
uint8_t index = palette.indexOf(tile.pixel(x, y)); uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
if (index & 1) { if (index & 1) {
row |= 0x001; row |= 0x001;
} }
@@ -449,8 +571,14 @@ static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &a
} }
} }
++iter; ++iter;
--remainingTiles;
if (remainingTiles == 0) {
break;
} }
assert(iter == attrmap.end()); }
assert(remainingTiles == 0);
assert(iter + options.trim == attrmap.end());
} }
static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap, static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
@@ -485,6 +613,7 @@ static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrm
} }
++tileID; ++tileID;
} }
assert(iter == attrmap.end());
} }
} // namespace unoptimized } // namespace unoptimized
@@ -513,7 +642,7 @@ public:
uint16_t bitplanes = 0; uint16_t bitplanes = 0;
for (uint32_t x = 0; x < 8; ++x) { for (uint32_t x = 0; x < 8; ++x) {
bitplanes <<= 1; bitplanes <<= 1;
uint8_t index = palette.indexOf(tile.pixel(x, y)); uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
if (index & 1) { if (index & 1) {
bitplanes |= 1; bitplanes |= 1;
} }
@@ -626,8 +755,8 @@ static UniqueTiles dedupTiles(Png const &png, DefaultInitVec<AttrmapEntry> &attr
} }
assert(iter == attrmap.end()); assert(iter == attrmap.end());
return tiles; // Copy elision should prevent the contained `unordered_set` from being // Copy elision should prevent the contained `unordered_set` from being re-constructed
// re-constructed return tiles;
} }
static void outputTileData(UniqueTiles const &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); output.open(options.output, std::ios_base::out | std::ios_base::binary);
uint16_t tileID = 0; 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); assert(tile->tileID == tileID);
++tileID; ++tileID;
output.sputn(reinterpret_cast<char const *>(tile->data().data()), options.bitDepth * 8); output.sputn(reinterpret_cast<char const *>(tile->data().data()), options.bitDepth * 8);
@@ -705,7 +835,7 @@ void process() {
for (uint32_t y = 0; y < 8; ++y) { for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) { 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" : ""); protoPalettes.size() != 1 ? "s" : "");
// Sort the proto-palettes by size, which improves the packing algorithm's efficiency // 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( std::sort(
protoPalettes.begin(), protoPalettes.end(), protoPalettes.begin(), protoPalettes.end(),
[](ProtoPalette const &lhs, ProtoPalette const &rhs) { return lhs.size() < rhs.size(); }); [](ProtoPalette const &lhs, ProtoPalette const &rhs) { return lhs.size() < rhs.size(); });
// Run a "pagination" problem solver auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
// TODO: allow picking one of several solvers? ? generatePalettes(protoPalettes, png)
auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes); : makePalsAsSpecified(protoPalettes, png);
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<Palette> palettes(nbPalettes);
for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
auto &pal = palettes[mappings[protoPalID]];
for (uint16_t color : protoPalettes[protoPalID]) {
pal.addColor(color);
}
}
// "Sort" colors in the generated palettes, see the man page for the flowchart
auto [palSize, palRGB, palAlpha] = png.getEmbeddedPal();
if (palRGB) {
sorting::indexed(palettes, palSize, palRGB, palAlpha);
} else if (png.hasNonGray()) {
sorting::rgb(palettes);
} else {
sorting::grayscale(palettes);
}
if (options.beVerbose) { if (options.beVerbose) {
for (auto &&palette : palettes) { for (auto &&palette : palettes) {
@@ -796,13 +898,11 @@ contained:;
if (!options.output.empty()) { if (!options.output.empty()) {
options.verbosePrint("Generating unoptimized tile data...\n"); options.verbosePrint("Generating unoptimized tile data...\n");
unoptimized::outputTileData(png, attrmap, palettes, mappings); unoptimized::outputTileData(png, attrmap, palettes, mappings);
} }
if (!options.tilemap.empty() || !options.attrmap.empty()) { if (!options.tilemap.empty() || !options.attrmap.empty()) {
options.verbosePrint("Generating unoptimized tilemap and/or attrmap...\n"); options.verbosePrint("Generating unoptimized tilemap and/or attrmap...\n");
unoptimized::outputMaps(png, attrmap, mappings); unoptimized::outputMaps(png, attrmap, mappings);
} }
} else { } else {
@@ -817,19 +917,16 @@ contained:;
if (!options.output.empty()) { if (!options.output.empty()) {
options.verbosePrint("Generating optimized tile data...\n"); options.verbosePrint("Generating optimized tile data...\n");
optimized::outputTileData(tiles); optimized::outputTileData(tiles);
} }
if (!options.tilemap.empty()) { if (!options.tilemap.empty()) {
options.verbosePrint("Generating optimized tilemap...\n"); options.verbosePrint("Generating optimized tilemap...\n");
optimized::outputTilemap(attrmap); optimized::outputTilemap(attrmap);
} }
if (!options.attrmap.empty()) { if (!options.attrmap.empty()) {
options.verbosePrint("Generating optimized attrmap...\n"); options.verbosePrint("Generating optimized attrmap...\n");
optimized::outputAttrmap(attrmap, mappings); optimized::outputAttrmap(attrmap, mappings);
} }
} }

View File

@@ -78,7 +78,7 @@ void Options::verbosePrint(char const *fmt, ...) const {
} }
// Short options // 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 * Equivalent long options
@@ -94,6 +94,7 @@ static struct option const longopts[] = {
{"output-attr-map", no_argument, NULL, 'A'}, {"output-attr-map", no_argument, NULL, 'A'},
{"attr-map", required_argument, NULL, 'a'}, {"attr-map", required_argument, NULL, 'a'},
{"color-curve", no_argument, NULL, 'C'}, {"color-curve", no_argument, NULL, 'C'},
{"colors", required_argument, NULL, 'c'},
{"debug", no_argument, NULL, 'D'}, {"debug", no_argument, NULL, 'D'},
{"depth", required_argument, NULL, 'd'}, {"depth", required_argument, NULL, 'd'},
{"fix", no_argument, NULL, 'f'}, {"fix", no_argument, NULL, 'f'},
@@ -112,8 +113,8 @@ static struct option const longopts[] = {
{NULL, no_argument, NULL, 0 } {NULL, no_argument, NULL, 0 }
}; };
static void print_usage(void) { static void printUsage(void) {
fputs("Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n" fputs("Usage: rgbgfx [-CcDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
" [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n" " [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n"
" [-x <tiles>] <file>\n" " [-x <tiles>] <file>\n"
"Useful options:\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 main(int argc, char *argv[]) {
int opt; int opt;
bool autoAttrmap = false, autoTilemap = false, autoPalettes = false; bool autoAttrmap = false, autoTilemap = false, autoPalettes = false;
@@ -166,6 +183,9 @@ int main(int argc, char *argv[]) {
case 'C': case 'C':
options.useColorCurve = true; options.useColorCurve = true;
break; break;
case 'c':
parsePaletteSpec(musl_optarg);
break;
case 'd': case 'd':
if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1 if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1
&& options.bitDepth != 2) { && options.bitDepth != 2) {
@@ -177,9 +197,9 @@ int main(int argc, char *argv[]) {
options.fixInput = true; options.fixInput = true;
break; break;
case 'h': case 'h':
warning("`-h` is deprecated, use `-???` instead"); warning("`-h` is deprecated, use `-Z` instead");
[[fallthrough]]; [[fallthrough]];
case '?': // TODO case 'Z':
options.columnMajor = true; options.columnMajor = true;
break; break;
case 'm': case 'm':
@@ -219,7 +239,7 @@ int main(int argc, char *argv[]) {
warning("Ignoring option '%c'", musl_optopt); warning("Ignoring option '%c'", musl_optopt);
break; break;
default: default:
print_usage(); printUsage();
exit(1); exit(1);
} }
} }
@@ -233,11 +253,11 @@ int main(int argc, char *argv[]) {
if (musl_optind == argc) { if (musl_optind == argc) {
fputs("FATAL: No input image specified\n", stderr); fputs("FATAL: No input image specified\n", stderr);
print_usage(); printUsage();
exit(1); exit(1);
} else if (argc - musl_optind != 1) { } else if (argc - musl_optind != 1) {
fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind); fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind);
print_usage(); printUsage();
exit(1); exit(1);
} }
@@ -259,7 +279,7 @@ int main(int argc, char *argv[]) {
if (options.fixInput) if (options.fixInput)
fputs("\tConvert input to indexed\n", stderr); fputs("\tConvert input to indexed\n", stderr);
if (options.columnMajor) 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) if (options.allowMirroring)
fputs("\tAllow mirroring tiles\n", stderr); fputs("\tAllow mirroring tiles\n", stderr);
if (options.allowDedup) if (options.allowDedup)
@@ -267,6 +287,7 @@ int main(int argc, char *argv[]) {
if (options.useColorCurve) if (options.useColorCurve)
fputs("\tUse color curve\n", stderr); fputs("\tUse color curve\n", stderr);
fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth); fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
if (options.trim != 0)
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim); fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0], fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
options.baseTileIDs[1]); options.baseTileIDs[1]);

View File

@@ -66,12 +66,12 @@ class AssignedProtos {
// We leave room for emptied slots to avoid copying the structs around on removal // We leave room for emptied slots to avoid copying the structs around on removal
std::vector<std::optional<ProtoPalAttrs>> _assigned; std::vector<std::optional<ProtoPalAttrs>> _assigned;
// For resolving proto-palette indices // For resolving proto-palette indices
std::vector<ProtoPalette> const &_protoPals; std::vector<ProtoPalette> const *_protoPals;
public: public:
template<typename... Ts> template<typename... Ts>
AssignedProtos(decltype(_protoPals) protoPals, Ts &&...elems) AssignedProtos(std::vector<ProtoPalette> const &protoPals, Ts &&...elems)
: _assigned{std::forward<Ts>(elems)...}, _protoPals{protoPals} {} : _assigned{std::forward<Ts>(elems)...}, _protoPals{&protoPals} {}
private: private:
template<typename Inner, template<typename> typename Constness> template<typename Inner, template<typename> typename Constness>
@@ -93,7 +93,7 @@ private:
skipEmpty(); skipEmpty();
} }
void skipEmpty() { void skipEmpty() {
while (_iter != _array->end() && !(*_iter).has_value()) { while (_iter != _array->end() && !_iter->has_value()) {
++_iter; ++_iter;
} }
} }
@@ -139,7 +139,7 @@ public:
* Args are passed to the `ProtoPalAttrs`'s constructor * Args are passed to the `ProtoPalAttrs`'s constructor
*/ */
template<typename... Ts> template<typename... Ts>
auto assign(Ts &&...args) { void assign(Ts &&...args) {
auto freeSlot = std::find_if_not( auto freeSlot = std::find_if_not(
_assigned.begin(), _assigned.end(), _assigned.begin(), _assigned.end(),
[](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); }); [](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); });
@@ -147,34 +147,24 @@ public:
if (freeSlot == _assigned.end()) { // We are full, use a new slot if (freeSlot == _assigned.end()) { // We are full, use a new slot
_assigned.emplace_back(std::forward<Ts>(args)...); _assigned.emplace_back(std::forward<Ts>(args)...);
} else { // Reuse a free slot } else { // Reuse a free slot
(*freeSlot).emplace(std::forward<Ts>(args)...); freeSlot->emplace(std::forward<Ts>(args)...);
} }
return freeSlot;
} }
void remove(iterator const &iter) { 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(); } void clear() { _assigned.clear(); }
/** bool empty() const { return std::distance(begin(), end()) == 0; }
* 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();
}));
});
}
private: private:
static void addUniqueColors(std::unordered_set<uint16_t> &colors, AssignedProtos const &pal) {
for (ProtoPalAttrs const &attrs : pal) {
for (uint16_t color : (*pal._protoPals)[attrs.palIndex]) {
colors.insert(color);
}
}
}
std::unordered_set<uint16_t> &uniqueColors() const { std::unordered_set<uint16_t> &uniqueColors() const {
// We check for *distinct* colors by stuffing them into a `set`; this should be // We check for *distinct* colors by stuffing them into a `set`; this should be
// faster than "back-checking" on every element (O(n²)) // 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 // TODO: calc84maniac suggested another approach; try implementing it, see if it
// performs better: // performs better:
// > So basically you make a priority queue that takes iterators into each of your sets // > 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 // > (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, // > values pointed to by each iterator
// optionally add one to your count, increment the iterator and push it back into the queue // > Then each iteration you pop from the queue,
// if it didn't reach the end > and you do this until the priority queue is empty // > optionally add one to your count, increment the iterator and push it back into the
// > queue if it didn't reach the end
// > And you do this until the priority queue is empty
static std::unordered_set<uint16_t> colors; static std::unordered_set<uint16_t> colors;
colors.clear(); colors.clear();
for (ProtoPalAttrs const &attrs : *this) { addUniqueColors(colors, *this);
for (uint16_t color : _protoPals[attrs.palIndex]) {
colors.insert(color);
}
}
return colors; return colors;
} }
public: public:
@@ -206,8 +194,106 @@ public:
colors.insert(protoPal.begin(), protoPal.end()); colors.insert(protoPal.begin(), protoPal.end());
return colors.size() <= options.maxPalSize(); return colors.size() <= options.maxPalSize();
} }
public:
/**
* Computes the "relative size" of a proto-palette on this palette
*/
double relSizeOf(ProtoPalette const &protoPal) const {
// NOTE: this function must not call `uniqueColors`, or one of its callers will break
return std::transform_reduce(
protoPal.begin(), protoPal.end(), 0.0, std::plus<>(), [this](uint16_t color) {
// NOTE: The paper and the associated code disagree on this: the code has
// this `1 +`, whereas the paper does not; its lack causes a division by 0
// if the symbol is not found anywhere, so I'm assuming the paper is wrong.
return 1.
/ (1
+ std::count_if(
begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
ProtoPalette const &pal = (*_protoPals)[attrs.palIndex];
return std::find(pal.begin(), pal.end(), color) != pal.end();
}));
});
}
/**
* Computes the "relative size" of a palette on this one
*/
double combinedVolume(AssignedProtos const &pal) const {
auto &colors = uniqueColors();
addUniqueColors(colors, pal);
return colors.size();
}
}; };
static void removeEmptyPals(std::vector<AssignedProtos> &assignments) {
// We do this by plucking "replacement" palettes from the end of the vector, so as to minimize
// the amount of moves performed. We can afford this because we don't care about their order,
// unlike `std::remove_if`, which permits less moves and thus better performance.
for (size_t i = 0; i != assignments.size(); ++i) {
if (assignments[i].empty()) {
// Hinting the compiler that the `return;` can only be reached if entering the loop
// produces better assembly
if (assignments.back().empty()) {
do {
assignments.pop_back();
assert(assignments.size() != 0);
} while (assignments.back().empty());
// Worst case, the loop ended on `assignments[i - 1]` (since every slot before `i`
// is known to be non-empty).
// (This could be a problem if `i` was 0, but we know there must be at least one
// color, so we're safe from that. The assertion in the loop checks it to be sure.)
// However, if it did stop at `i - 1`, then `i` no longer points to a valid slot,
// and we must end.
if (i == assignments.size()) {
break;
}
}
assert(i < assignments.size());
assignments[i] = std::move(assignments.back());
assignments.pop_back();
}
}
}
static void decant(std::vector<AssignedProtos> &assignments) {
// "Decanting" is the process of moving all *things* that can fit in a lower index there
auto decantOn = [&assignments](auto const &move) {
// No need to attempt decanting on palette #0, as there are no palettes to decant to
for (size_t from = assignments.size(); --from;) {
// Scan all palettes before this one
for (size_t to = 0; to < from; ++to) {
move(assignments[to], assignments[from]);
}
}
};
// Decant on palettes
decantOn([](AssignedProtos &to, AssignedProtos &from) {
// If the entire palettes can be merged, move all of `from`'s proto-palettes
if (to.combinedVolume(from) <= options.maxPalSize()) {
for (ProtoPalAttrs &protoPal : from) {
to.assign(std::move(protoPal));
}
from.clear();
}
});
// Decant on "components" (= proto-pals sharing colors)
decantOn([](AssignedProtos &to, AssignedProtos &from) {
// TODO
(void)to;
(void)from;
});
// Decant on proto-palettes
decantOn([](AssignedProtos &to, AssignedProtos &from) {
// TODO
(void)to;
(void)from;
});
}
std::tuple<DefaultInitVec<size_t>, size_t> std::tuple<DefaultInitVec<size_t>, size_t>
overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) { overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n"); options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n");
@@ -256,7 +342,7 @@ std::tuple<DefaultInitVec<size_t>, size_t>
continue; 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()); assignments[i].relSizeOf(protoPal), protoPal.size());
if (assignments[i].relSizeOf(protoPal) < bestRelSize) { if (assignments[i].relSizeOf(protoPal) < bestRelSize) {
bestPalIndex = i; bestPalIndex = i;
@@ -330,8 +416,12 @@ std::tuple<DefaultInitVec<size_t>, size_t>
} }
queue.pop(); 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) { if (options.beVerbose) {
for (auto &&assignment : assignments) { for (auto &&assignment : assignments) {
@@ -341,7 +431,7 @@ std::tuple<DefaultInitVec<size_t>, size_t>
options.verbosePrint("%04" PRIx16 ", ", colorIndex); options.verbosePrint("%04" PRIx16 ", ", colorIndex);
} }
} }
options.verbosePrint("} (%zu)\n", assignment.volume()); options.verbosePrint("} (volume = %zu)\n", assignment.volume());
} }
} }

View File

@@ -39,11 +39,21 @@ void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRG
} }
} }
void grayscale(std::vector<Palette> &palettes) { void grayscale(std::vector<Palette> &palettes,
options.verbosePrint("Sorting grayscale-only palettes...\n"); std::array<std::optional<Rgba>, 0x8001> const &colors) {
options.verbosePrint("Sorting grayscale-only palette...\n");
for (Palette &pal : palettes) { // This method is only applicable if there are at most as many colors as colors per palette, so
(void)pal; // TODO // 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();
} }
} }

24
src/gfx/rgba.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include "gfx/rgba.hpp"
#include <assert.h>
#include <stdint.h>
#include "gfx/main.hpp" // options
uint16_t Rgba::cgbColor() const {
if (isTransparent()) {
return transparent;
}
if (options.useColorCurve) {
assert(!"TODO");
} else {
return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
}
}
uint8_t Rgba::grayIndex() const {
assert(isGray());
// Convert from [0; 256[ to [0; maxPalSize[
return static_cast<uint16_t>(255 - red) * options.maxPalSize() / 256;
}