Implement --input-tileset (#1464)

As discussed in https://github.com/gbdev/rgbds/issues/575#issuecomment-1991456862
This commit is contained in:
Eldred Habert
2024-09-04 21:20:01 +02:00
committed by GitHub
parent 1283b0b6a6
commit 80d37f9988
18 changed files with 168 additions and 23 deletions

View File

@@ -108,7 +108,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
}
// Short options
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvx:Z";
static char const *optstring = "-Aa:b:Cc:Dd:Ffhi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvx:Z";
/*
* Equivalent long options
@@ -127,6 +127,7 @@ static option const longopts[] = {
{"color-curve", no_argument, nullptr, 'C'},
{"colors", required_argument, nullptr, 'c'},
{"depth", required_argument, nullptr, 'd'},
{"input-tileset", required_argument, nullptr, 'i'},
{"slice", required_argument, nullptr, 'L'},
{"mirror-tiles", no_argument, nullptr, 'm'},
{"nb-tiles", required_argument, nullptr, 'N'},
@@ -154,9 +155,10 @@ static option const longopts[] = {
static void printUsage() {
fputs(
"Usage: rgbgfx [-r stride] [-CmOuVXYZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-L <slice>] [-N <nb_tiles>]\n"
" [-n <nb_pals>] [-o <out_file>] [-p <pal_file> | -P] [-q <pal_map> | -Q]\n"
" [-s <nb_colors>] [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-i <tileset_file>]\n"
" [-L <slice>] [-N <nb_tiles>] [-n <nb_pals>] [-o <out_file>]\n"
" [-p <pal_file> | -P] [-q <pal_map> | -Q] [-s <nb_colors>]\n"
" [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
"Useful options:\n"
" -m, --mirror-tiles optimize out mirrored tiles\n"
" -o, --output <path> output the tile data to this path\n"
@@ -427,6 +429,11 @@ static char *parseArgv(int argc, char *argv[]) {
options.bitDepth = 2;
}
break;
case 'i':
if (!options.inputTileset.empty())
warning("Overriding input tileset file %s", options.inputTileset.c_str());
options.inputTileset = musl_optarg;
break;
case 'L':
options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
if (options.inputSlice.left > INT16_MAX) {

View File

@@ -692,7 +692,7 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
if (!options.palettes.empty()) {
File output;
if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) {
fatal("Failed to open \"%s\": %s", output.c_str(options.palettes), strerror(errno));
fatal("Failed to create \"%s\": %s", output.c_str(options.palettes), strerror(errno));
}
for (Palette const &palette : palettes) {
@@ -706,6 +706,17 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
}
}
static void hashBitplanes(uint16_t bitplanes, uint16_t &hash) {
hash ^= bitplanes;
if (options.allowMirroringX) {
// Count the line itself as mirrored, which ensures the same hash as the tile's horizontal
// flip; vertical mirroring is already taken care of because the symmetric line will be
// XOR'd the same way. (This can trivially create some collisions, but real-world tile data
// generally doesn't trigger them.)
hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF];
}
}
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
@@ -736,23 +747,23 @@ public:
return row;
}
TileData(std::array<uint8_t, 16> &&raw) : _data(raw), _hash(0) {
for (uint8_t y = 0; y < 8; ++y) {
uint16_t bitplanes = _data[y * 2] | _data[y * 2 + 1] << 8;
hashBitplanes(bitplanes, _hash);
}
}
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);
hashBitplanes(bitplanes, _hash);
_data[writeIndex++] = bitplanes & 0xFF;
if (options.bitDepth == 2) {
_data[writeIndex++] = bitplanes >> 8;
}
// Update the hash
_hash ^= bitplanes;
if (options.allowMirroringX) {
// Count the line itself as mirrorred horizontally; vertical mirroring is already
// taken care of because the symmetric line will be XOR'd the same way.
// (This reduces the hash's efficiency, but seems benign with most real-world data.)
_hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF];
}
}
}
@@ -836,7 +847,7 @@ static void outputTileData(
) {
File output;
if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
fatal("Failed to open \"%s\": %s", output.c_str(options.output), strerror(errno));
fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno));
}
uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8;
@@ -875,7 +886,7 @@ static void outputMaps(
if (!path.empty()) {
file.emplace();
if (!file->open(path, std::ios_base::out | std::ios_base::binary)) {
fatal("Failed to open \"%s\": %s", file->c_str(options.tilemap), strerror(errno));
fatal("Failed to create \"%s\": %s", file->c_str(options.tilemap), strerror(errno));
}
}
};
@@ -923,12 +934,10 @@ struct UniqueTiles {
/*
* 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);
std::tuple<uint16_t, TileData::MatchType> addTile(TileData newTile) {
auto [tileData, inserted] = tileset.insert(newTile);
TileData::MatchType matchType = TileData::EXACT;
TileData::MatchType matchType = TileData::NOPE;
if (inserted) {
// Give the new tile the next available unique ID
tileData->tileID = static_cast<uint16_t>(tiles.size());
@@ -963,8 +972,57 @@ static UniqueTiles dedupTiles(
// by caching the full tile data anyway, so we might as well.)
UniqueTiles tiles;
if (!options.inputTileset.empty()) {
File inputTileset;
if (!inputTileset.open(options.inputTileset, std::ios::in | std::ios::binary)) {
fatal("Failed to open \"%s\": %s", options.inputTileset.c_str(), strerror(errno));
}
std::array<uint8_t, 16> tile;
size_t const tileSize = options.bitDepth * 8;
for (;;) {
// It's okay to cast between character types.
size_t len = inputTileset->sgetn(reinterpret_cast<char *>(tile.data()), tileSize);
if (len == 0) { // EOF!
break;
} else if (len != tileSize) {
fatal(
"\"%s\" does not contain a multiple of %zu bytes; is it actually tile data?",
options.inputTileset.c_str(),
tileSize
);
} else if (len == 8) {
// Expand the tile data to 2bpp.
for (size_t i = 8; i--;) {
tile[i * 2 + 1] = 0;
tile[i * 2] = tile[i];
}
}
auto [tileID, matchType] = tiles.addTile(std::move(tile));
if (matchType != TileData::NOPE) {
error(
"The input tileset's tile #%hu was deduplicated; please check that your "
"deduplication flags (`-u`, `-m`) are consistent with what was used to "
"generate the input tileset",
tileID
);
}
}
}
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[attr.protoPaletteID]]);
auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]});
if (matchType == TileData::NOPE && options.output.empty()) {
error(
"Tile at (%" PRIu32 ", %" PRIu32
") is not within the input tileset, and `-o` was not given!",
tile.x,
tile.y
);
}
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
@@ -1186,6 +1244,12 @@ continue_visiting_tiles:;
);
}
// I currently cannot figure out useful semantics for this combination of flags.
if (!options.inputTileset.empty()) {
fatal("Input tilesets are not supported without `-u`\nPlease consider explaining your "
"use case to RGBDS' developers!");
}
if (!options.output.empty()) {
options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
unoptimized::outputTileData(png, attrmap, palettes, mappings);