mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 18:22:07 +00:00
Implement --input-tileset (#1464)
As discussed in https://github.com/gbdev/rgbds/issues/575#issuecomment-1991456862
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user