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_sorting.o \
src/gfx/proto_palette.o \
src/gfx/rgba.o \
src/extern/getopt.o \
src/error.o

View File

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

View File

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

View File

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

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.
.Pq If you want a given color to appear more than once in a palette, you should specify the palettes explicitly instead using Fl TODO .
.It
Otherwise, if the PNG only contains shades of gray, they will be categorized into 4
Otherwise, if the PNG only contains shades of gray, they will be categorized into as many
.Dq bins
.Pq white, light gray, dark gray, and black in this order ,
and the palette is set to these four bins.
(TODO: how does this interact with 1bpp? With more than 1 palette?)
If more than one grey ends up in the same bin, the RGB method below is used instead.
as there are colors per palette, and the palette is set to these bins.
The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
If two distinct grays end up in the same bin, the RGB method is used instead.
.It
If none of the above apply, colors are sorted from lightest to darkest.
(This is what the old documentation claimed, but the definition of luminance that was used wasn't quite right.
It is kept for compatibility.)
.EQ
delim $$
.EN
The definition of luminance that
.Nm
uses is
.Do
$2126 times red + 7152 times green + 722 times blue$
.Dc .
.EQ
delim off
.EN
.El
.Pp
Note that the

View File

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

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

View File

@@ -78,7 +78,7 @@ void Options::verbosePrint(char const *fmt, ...) const {
}
// Short options
static char const *optstring = "Aa:CDd:Ffhmo:Pp:Tt:uVvx:";
static char const *optstring = "Aa:Cc:Dd:Ffhmo:Pp:Tt:uVvx:";
/*
* Equivalent long options
@@ -94,6 +94,7 @@ static struct option const longopts[] = {
{"output-attr-map", no_argument, NULL, 'A'},
{"attr-map", required_argument, NULL, 'a'},
{"color-curve", no_argument, NULL, 'C'},
{"colors", required_argument, NULL, 'c'},
{"debug", no_argument, NULL, 'D'},
{"depth", required_argument, NULL, 'd'},
{"fix", no_argument, NULL, 'f'},
@@ -112,8 +113,8 @@ static struct option const longopts[] = {
{NULL, no_argument, NULL, 0 }
};
static void print_usage(void) {
fputs("Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
static void printUsage(void) {
fputs("Usage: rgbgfx [-CcDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
" [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n"
" [-x <tiles>] <file>\n"
"Useful options:\n"
@@ -135,6 +136,22 @@ void fputsv(std::string_view const &view, FILE *f) {
}
}
void parsePaletteSpec(char *arg) {
if (arg[0] == '#') {
// List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons
options.palSpecType = Options::EXPLICIT;
// TODO
} else if (strcasecmp(arg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
} else {
// `fmt:path`, parse the file according to the given format
// TODO: split both parts, error out if malformed or file not found
options.palSpecType = Options::EXPLICIT;
// TODO
}
}
int main(int argc, char *argv[]) {
int opt;
bool autoAttrmap = false, autoTilemap = false, autoPalettes = false;
@@ -166,6 +183,9 @@ int main(int argc, char *argv[]) {
case 'C':
options.useColorCurve = true;
break;
case 'c':
parsePaletteSpec(musl_optarg);
break;
case 'd':
if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1
&& options.bitDepth != 2) {
@@ -177,9 +197,9 @@ int main(int argc, char *argv[]) {
options.fixInput = true;
break;
case 'h':
warning("`-h` is deprecated, use `-???` instead");
warning("`-h` is deprecated, use `-Z` instead");
[[fallthrough]];
case '?': // TODO
case 'Z':
options.columnMajor = true;
break;
case 'm':
@@ -219,7 +239,7 @@ int main(int argc, char *argv[]) {
warning("Ignoring option '%c'", musl_optopt);
break;
default:
print_usage();
printUsage();
exit(1);
}
}
@@ -233,11 +253,11 @@ int main(int argc, char *argv[]) {
if (musl_optind == argc) {
fputs("FATAL: No input image specified\n", stderr);
print_usage();
printUsage();
exit(1);
} else if (argc - musl_optind != 1) {
fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind);
print_usage();
printUsage();
exit(1);
}
@@ -259,7 +279,7 @@ int main(int argc, char *argv[]) {
if (options.fixInput)
fputs("\tConvert input to indexed\n", stderr);
if (options.columnMajor)
fputs("\tOutput {tile,attr}map in column-major order\n", stderr);
fputs("\tVisit image in column-major order\n", stderr);
if (options.allowMirroring)
fputs("\tAllow mirroring tiles\n", stderr);
if (options.allowDedup)
@@ -267,7 +287,8 @@ int main(int argc, char *argv[]) {
if (options.useColorCurve)
fputs("\tUse color curve\n", stderr);
fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
if (options.trim != 0)
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
options.baseTileIDs[1]);
fprintf(stderr, "\tMax number of tiles: [%" PRIu16 ", %" PRIu16 "]\n",

View File

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

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;
}