Skip to content

Commit 80d37f9

Browse files
authored
Implement --input-tileset (#1464)
As discussed in #575 (comment)
1 parent 1283b0b commit 80d37f9

18 files changed

+168
-23
lines changed

contrib/bash_compl/_rgbgfx.bash

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ _rgbgfx_completions() {
2121
[b]="base-tiles:unk"
2222
[c]="colors:unk"
2323
[d]="depth:unk"
24+
[i]="input-tileset:glob-*.2bpp"
2425
[L]="slice:unk"
2526
[N]="nb-tiles:unk"
2627
[n]="nb-palettes:unk"

contrib/zsh_compl/_rgbgfx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ local args=(
3030
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
3131
'(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:'
3232
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'
33+
'(-i --input-tileset)'{-i,--input-tileset}'+[Use specific tiles]:tileset file:_files -g "*.2bpp"'
3334
'(-L --slice)'{-L,--slice}'+[Only process a portion of the image]:input slice:'
3435
'(-N --nb-tiles)'{-N,--nb-tiles}'+[Limit number of tiles]:tile count:'
3536
'(-n --nb-palettes)'{-n,--nb-palettes}'+[Limit number of palettes]:palette count:'

include/gfx/main.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ struct Options {
2828
EMBEDDED,
2929
} palSpecType = NO_SPEC; // -c
3030
std::vector<std::array<std::optional<Rgba>, 4>> palSpec{};
31-
uint8_t bitDepth = 2; // -d
31+
uint8_t bitDepth = 2; // -d
32+
std::string inputTileset{}; // -i
3233
struct {
3334
uint16_t left;
3435
uint16_t top;

man/rgbgfx.1

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
.Op Fl b Ar base_ids
1717
.Op Fl c Ar pal_spec
1818
.Op Fl d Ar depth
19+
.Op Fl i Ar input_tiles
1920
.Op Fl L Ar slice
2021
.Op Fl N Ar nb_tiles
2122
.Op Fl n Ar nb_pals
@@ -164,6 +165,57 @@ for a list of formats and their descriptions.
164165
.It Fl d Ar depth , Fl \-depth Ar depth
165166
Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default).
166167
This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively).
168+
.It Fl i Ar input_tiles , Fl \-input-tileset Ar input_tiles
169+
Use the specified input tiles in addition to having
170+
.Nm
171+
automatically determine some.
172+
The input tiles will always be first in the
173+
.Fl o
174+
image output, and will always get the first IDs in the
175+
.Fl t
176+
tilemap output.
177+
.Ar input_tiles
178+
must contain 1bpp or 2bpp tile data
179+
.Pq whichever matches the Fl d No option used here ,
180+
as could be previously generated with the
181+
.Fl o
182+
option.
183+
.Pp
184+
If the
185+
.Fl o
186+
option is also specified, then the input tiles will be assigned the first tile IDs, and any tiles from the input image that are not in the input tileset will be assigned subsequent IDs.
187+
But if the
188+
.Fl o
189+
option is
190+
.Em not
191+
specified, then the tile map can
192+
.Em only
193+
use tiles from the input tileset.
194+
Using
195+
.Fl o
196+
with
197+
.Fl i
198+
is useful if you want to precisely control the tile IDs of its tile map.
199+
Using
200+
.Fl i
201+
alone is more useful if you want several images to use a subset of shared tiles.
202+
.Pp
203+
If the image will use more than one color palette, it is
204+
.Em strongly
205+
advised to generate the palette set along with the input tile data, and pass
206+
.Fl c Cm gbc: Ns Ar input_palette
207+
along with
208+
.Fl i Ar input_tiles .
209+
This is because
210+
.Nm
211+
might not generate the same palette set for this image as it did for its input tileset.
212+
.Pp
213+
See
214+
.Sx EXAMPLES
215+
for examples of how to use this option.
216+
.Pp
217+
This option is ignored in
218+
.Sx REVERSE MODE .
167219
.It Fl L Ar slice , Fl \-slice Ar slice
168220
Only process a given rectangle of the image.
169221
This is useful for example if the input image is a sheet of some sort, and you want to convert each cel individually.
@@ -637,7 +689,13 @@ without needing an input image.
637689
.Pp
638690
.Dl $ rgbgfx -c '#fff,#ff0,#f80,#000' -p colors.pal
639691
.Pp
640-
TODO: more examples.
692+
The following will convert two level images using the same tileset, and error out if any of them contain tiles not in the tileset.
693+
.Pp
694+
.Bd -literal -offset Ds
695+
$ rgbgfx tileset.png -o tileset.2bpp -O -P
696+
$ rgbgfx level1.png -i tileset.2bpp -c gbc:tileset.pal -t level1.tilemap -a level1.attrmap
697+
$ rgbgfx level2.png -i tileset.2bpp -c gbc:tileset.pal -t level2.tilemap -a level2.attrmap
698+
.Ed
641699
.Sh BUGS
642700
Please report bugs and mistakes in this man page on
643701
.Lk https://github.com/gbdev/rgbds/issues GitHub .

src/gfx/main.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
108108
}
109109

110110
// Short options
111-
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvx:Z";
111+
static char const *optstring = "-Aa:b:Cc:Dd:Ffhi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvx:Z";
112112

113113
/*
114114
* Equivalent long options
@@ -127,6 +127,7 @@ static option const longopts[] = {
127127
{"color-curve", no_argument, nullptr, 'C'},
128128
{"colors", required_argument, nullptr, 'c'},
129129
{"depth", required_argument, nullptr, 'd'},
130+
{"input-tileset", required_argument, nullptr, 'i'},
130131
{"slice", required_argument, nullptr, 'L'},
131132
{"mirror-tiles", no_argument, nullptr, 'm'},
132133
{"nb-tiles", required_argument, nullptr, 'N'},
@@ -154,9 +155,10 @@ static option const longopts[] = {
154155
static void printUsage() {
155156
fputs(
156157
"Usage: rgbgfx [-r stride] [-CmOuVXYZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
157-
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-L <slice>] [-N <nb_tiles>]\n"
158-
" [-n <nb_pals>] [-o <out_file>] [-p <pal_file> | -P] [-q <pal_map> | -Q]\n"
159-
" [-s <nb_colors>] [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
158+
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-i <tileset_file>]\n"
159+
" [-L <slice>] [-N <nb_tiles>] [-n <nb_pals>] [-o <out_file>]\n"
160+
" [-p <pal_file> | -P] [-q <pal_map> | -Q] [-s <nb_colors>]\n"
161+
" [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
160162
"Useful options:\n"
161163
" -m, --mirror-tiles optimize out mirrored tiles\n"
162164
" -o, --output <path> output the tile data to this path\n"
@@ -427,6 +429,11 @@ static char *parseArgv(int argc, char *argv[]) {
427429
options.bitDepth = 2;
428430
}
429431
break;
432+
case 'i':
433+
if (!options.inputTileset.empty())
434+
warning("Overriding input tileset file %s", options.inputTileset.c_str());
435+
options.inputTileset = musl_optarg;
436+
break;
430437
case 'L':
431438
options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
432439
if (options.inputSlice.left > INT16_MAX) {

src/gfx/process.cpp

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
692692
if (!options.palettes.empty()) {
693693
File output;
694694
if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) {
695-
fatal("Failed to open \"%s\": %s", output.c_str(options.palettes), strerror(errno));
695+
fatal("Failed to create \"%s\": %s", output.c_str(options.palettes), strerror(errno));
696696
}
697697

698698
for (Palette const &palette : palettes) {
@@ -706,6 +706,17 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
706706
}
707707
}
708708

709+
static void hashBitplanes(uint16_t bitplanes, uint16_t &hash) {
710+
hash ^= bitplanes;
711+
if (options.allowMirroringX) {
712+
// Count the line itself as mirrored, which ensures the same hash as the tile's horizontal
713+
// flip; vertical mirroring is already taken care of because the symmetric line will be
714+
// XOR'd the same way. (This can trivially create some collisions, but real-world tile data
715+
// generally doesn't trigger them.)
716+
hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF];
717+
}
718+
}
719+
709720
class TileData {
710721
std::array<uint8_t, 16> _data;
711722
// The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
@@ -736,23 +747,23 @@ class TileData {
736747
return row;
737748
}
738749

750+
TileData(std::array<uint8_t, 16> &&raw) : _data(raw), _hash(0) {
751+
for (uint8_t y = 0; y < 8; ++y) {
752+
uint16_t bitplanes = _data[y * 2] | _data[y * 2 + 1] << 8;
753+
hashBitplanes(bitplanes, _hash);
754+
}
755+
}
756+
739757
TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
740758
size_t writeIndex = 0;
741759
for (uint32_t y = 0; y < 8; ++y) {
742760
uint16_t bitplanes = rowBitplanes(tile, palette, y);
761+
hashBitplanes(bitplanes, _hash);
762+
743763
_data[writeIndex++] = bitplanes & 0xFF;
744764
if (options.bitDepth == 2) {
745765
_data[writeIndex++] = bitplanes >> 8;
746766
}
747-
748-
// Update the hash
749-
_hash ^= bitplanes;
750-
if (options.allowMirroringX) {
751-
// Count the line itself as mirrorred horizontally; vertical mirroring is already
752-
// taken care of because the symmetric line will be XOR'd the same way.
753-
// (This reduces the hash's efficiency, but seems benign with most real-world data.)
754-
_hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF];
755-
}
756767
}
757768
}
758769

@@ -836,7 +847,7 @@ static void outputTileData(
836847
) {
837848
File output;
838849
if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
839-
fatal("Failed to open \"%s\": %s", output.c_str(options.output), strerror(errno));
850+
fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno));
840851
}
841852

842853
uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8;
@@ -875,7 +886,7 @@ static void outputMaps(
875886
if (!path.empty()) {
876887
file.emplace();
877888
if (!file->open(path, std::ios_base::out | std::ios_base::binary)) {
878-
fatal("Failed to open \"%s\": %s", file->c_str(options.tilemap), strerror(errno));
889+
fatal("Failed to create \"%s\": %s", file->c_str(options.tilemap), strerror(errno));
879890
}
880891
}
881892
};
@@ -923,12 +934,10 @@ struct UniqueTiles {
923934
/*
924935
* Adds a tile to the collection, and returns its ID
925936
*/
926-
std::tuple<uint16_t, TileData::MatchType>
927-
addTile(Png::TilesVisitor::Tile const &tile, Palette const &palette) {
928-
TileData newTile(tile, palette);
937+
std::tuple<uint16_t, TileData::MatchType> addTile(TileData newTile) {
929938
auto [tileData, inserted] = tileset.insert(newTile);
930939

931-
TileData::MatchType matchType = TileData::EXACT;
940+
TileData::MatchType matchType = TileData::NOPE;
932941
if (inserted) {
933942
// Give the new tile the next available unique ID
934943
tileData->tileID = static_cast<uint16_t>(tiles.size());
@@ -963,8 +972,57 @@ static UniqueTiles dedupTiles(
963972
// by caching the full tile data anyway, so we might as well.)
964973
UniqueTiles tiles;
965974

975+
if (!options.inputTileset.empty()) {
976+
File inputTileset;
977+
if (!inputTileset.open(options.inputTileset, std::ios::in | std::ios::binary)) {
978+
fatal("Failed to open \"%s\": %s", options.inputTileset.c_str(), strerror(errno));
979+
}
980+
981+
std::array<uint8_t, 16> tile;
982+
size_t const tileSize = options.bitDepth * 8;
983+
for (;;) {
984+
// It's okay to cast between character types.
985+
size_t len = inputTileset->sgetn(reinterpret_cast<char *>(tile.data()), tileSize);
986+
if (len == 0) { // EOF!
987+
break;
988+
} else if (len != tileSize) {
989+
fatal(
990+
"\"%s\" does not contain a multiple of %zu bytes; is it actually tile data?",
991+
options.inputTileset.c_str(),
992+
tileSize
993+
);
994+
} else if (len == 8) {
995+
// Expand the tile data to 2bpp.
996+
for (size_t i = 8; i--;) {
997+
tile[i * 2 + 1] = 0;
998+
tile[i * 2] = tile[i];
999+
}
1000+
}
1001+
1002+
auto [tileID, matchType] = tiles.addTile(std::move(tile));
1003+
1004+
if (matchType != TileData::NOPE) {
1005+
error(
1006+
"The input tileset's tile #%hu was deduplicated; please check that your "
1007+
"deduplication flags (`-u`, `-m`) are consistent with what was used to "
1008+
"generate the input tileset",
1009+
tileID
1010+
);
1011+
}
1012+
}
1013+
}
1014+
9661015
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
967-
auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[attr.protoPaletteID]]);
1016+
auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]});
1017+
1018+
if (matchType == TileData::NOPE && options.output.empty()) {
1019+
error(
1020+
"Tile at (%" PRIu32 ", %" PRIu32
1021+
") is not within the input tileset, and `-o` was not given!",
1022+
tile.x,
1023+
tile.y
1024+
);
1025+
}
9681026

9691027
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
9701028
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
@@ -1186,6 +1244,12 @@ continue_visiting_tiles:;
11861244
);
11871245
}
11881246

1247+
// I currently cannot figure out useful semantics for this combination of flags.
1248+
if (!options.inputTileset.empty()) {
1249+
fatal("Input tilesets are not supported without `-u`\nPlease consider explaining your "
1250+
"use case to RGBDS' developers!");
1251+
}
1252+
11891253
if (!options.output.empty()) {
11901254
options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
11911255
unoptimized::outputTileData(png, attrmap, palettes, mappings);

test/gfx/input_tileset.flags

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-i input_tileset.in.2bpp
2+
-u

test/gfx/input_tileset.in.2bpp

272 Bytes
Binary file not shown.

test/gfx/input_tileset.out.2bpp

320 Bytes
Binary file not shown.

test/gfx/input_tileset.png

691 Bytes
Loading

0 commit comments

Comments
 (0)