diff --git a/Makefile b/Makefile index 10e022c5..9c8f0adb 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,9 @@ rgbfix: ${rgbfix_obj} rgbgfx: ${rgbgfx_obj} $Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} -x c++ src/version.c ${PNGLDLIBS} +test/randtilegen: test/randtilegen.c + $Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCFLAGS} -Wno-vla ${PNGCFLAGS} ${PNGLDLIBS} + # Rules to process files # We want the Bison invocation to pass through our rules, not default ones diff --git a/test/.gitignore b/test/.gitignore index 32dd40f7..ecf476f6 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,3 +1 @@ -/pokecrystal/ -/pokered/ -/ucity/ +/randtilegen diff --git a/test/randtilegen.c b/test/randtilegen.c new file mode 100644 index 00000000..2eccf6b2 --- /dev/null +++ b/test/randtilegen.c @@ -0,0 +1,240 @@ +/* + * This file is part of RGBDS. + * + * Copyright (c) 2022, Eldred Habert and RGBDS contributors. + * + * SPDX-License-Identifier: MIT + * + * Originally: + * // This program is hereby released to the public domain. + * // ~aaaaaa123456789, released 2022-03-15 + * https://gist.github.com/aaaaaa123456789/3feccf085ab4f82d144d9a47fb1b4bdf + * + * This was modified to use libpng instead of libplum, as well as comments and style changes. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +uint32_t randBits = 0; // Storage for bits read from the input stream but not yet used +uint8_t randCount = 0; // How many bits are currently stored in the above +size_t nbRandBytes = 0; + +static uint32_t getRandomBits(uint8_t count) { + // Trying to read one more byte with `randCount` at least this high will drop some bits + // If the count is no higher than that limit, then the loop will exit without reading more bytes + assert(count <= sizeof(randBits) * 8 + 1); + + // Read bytes until we have enough bits to serve the request + while (count > randCount) { + int data = getchar(); + if (data == EOF) { + fprintf(stderr, "Exit after reading %zu bytes\n", nbRandBytes); + exit(0); + } + randBits |= (uint32_t)data << randCount; + randCount += 8; + ++nbRandBytes; + } + + uint32_t result = randBits & (((uint32_t)1 << count) - 1); + randBits >>= count; + randCount -= count; + return result; +} + +/** + * Expand a 5-bit color component to 8 bits with minimal bias + */ +static uint8_t _5to8(uint8_t five) { + return five << 3 | five >> 2; +} + +static void generate_random_image(png_structp png, png_infop pngInfo) { +#define NB_TILES 100 + struct { + unsigned char palette; + unsigned char nbColors; + } attributes[NB_TILES]; + uint8_t tileData[NB_TILES][8][8]; + // These two are in tiles, not pixels + // Both width and height are 4-bit values, so nbTiles is 8-bit (OK!) + uint8_t const width = getRandomBits(3) + 3, height = getRandomBits(3) + 3, + nbTiles = width * height; + + for (uint8_t p = 0; p < nbTiles; p++) { + uint8_t pal; + do { + pal = getRandomBits(5); + } while (pal == 0 || (pal > 29)); + attributes[p].palette = 2 * pal + getRandomBits(1); + // Population count (nb of bits set), the simple way + static uint8_t const popcount[] = {1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4}; + attributes[p].nbColors = popcount[pal - 1]; + if (attributes[p].nbColors < 2) { + memset(tileData[p], 0, sizeof(tileData[p])); + continue; + } + uint8_t index, total; + for (index = 0, total = 0; index < p; index++) { + if (attributes[index].nbColors == attributes[p].nbColors) { + total++; + } + } + // index == p at exit + if (total) { + index = getRandomBits(8); + if (index < total) { + total = index + 1; + for (index = 0; total; index++) { + if (attributes[index].nbColors == attributes[p].nbColors) { + total--; + } + } + } else { + index = p; + } + } + if (index != p) { + unsigned rotation = getRandomBits(2); + for (uint8_t y = 0; y < 8; y++) { + for (uint8_t x = 0; x < 8; x++) { + tileData[p][y][x] = + tileData[index][y ^ ((rotation & 2) ? 7 : 0)][x ^ ((rotation & 1) ? 7 : 0)]; + } + } + } else { + switch (attributes[p].nbColors) { + case 2: + for (uint8_t y = 0; y < 8; y++) + for (uint8_t x = 0; x < 8; x++) + tileData[p][y][x] = getRandomBits(1); + break; + case 4: + for (uint8_t y = 0; y < 8; y++) + for (uint8_t x = 0; x < 8; x++) + tileData[p][y][x] = getRandomBits(2); + break; + default: + for (uint8_t y = 0; y < 8; y++) + for (uint8_t x = 0; x < 8; x++) { + do { + index = getRandomBits(2); + } while (index == 3); + tileData[p][y][x] = index; + } + } + } + } + + uint16_t colors[10]; + for (uint8_t p = 0; p < 10; p++) { + colors[p] = getRandomBits(15); + } + // Randomly make color #0 of all palettes transparent + if (!getRandomBits(2)) { + colors[0] |= 0x8000; + colors[5] |= 0x8000; + } + + uint16_t palettes[60][4]; + for (uint8_t p = 0; p < 60; p++) { + const uint16_t *subpal = colors; + if (p & 1) { + subpal += 5; + } + uint8_t total = 0; + for (uint8_t index = 0; index < 5; index++) { + if (p & (2 << index)) { + palettes[p][total++] = subpal[index]; + } + } + } + + png_set_IHDR(png, pngInfo, width * 8, height * 8, 8, PNG_COLOR_TYPE_RGB_ALPHA, + getRandomBits(1) ? PNG_INTERLACE_NONE : PNG_INTERLACE_ADAM7, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + // While it would be nice to write the image little by little, I really don't want to handle + // interlacing myself. (We're doing interlacing to test that RGBGFX correctly handles it.) + uint8_t const SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component) + uint8_t data[height * 8 * width * 8 * SIZEOF_PIXEL]; + uint8_t *rowPtrs[height * 8]; + for (uint8_t y = 0; y < height * 8; ++y) { + rowPtrs[y] = &data[y * width * 8 * SIZEOF_PIXEL]; + } + + for (uint8_t p = 0; p < nbTiles; p++) { + uint8_t tx = 8 * (p % width), ty = 8 * (p / width); + for (uint8_t y = 0; y < 8; y++) { + uint8_t * const row = rowPtrs[ty + y]; + for (uint8_t x = 0; x < 8; x++) { + uint8_t * const pixel = &row[(tx + x) * SIZEOF_PIXEL]; + uint16_t color = palettes[attributes[p].palette][tileData[p][y][x]]; + pixel[0] = _5to8(color & 0x1F); + pixel[1] = _5to8(color >> 5 & 0x1F); + pixel[2] = _5to8(color >> 10 & 0x1F); + pixel[3] = color & 0x8000 ? 0x00 : 0xFF; + } + } + } + png_set_rows(png, pngInfo, rowPtrs); + png_write_png(png, pngInfo, PNG_TRANSFORM_IDENTITY, NULL); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fputs("usage: randtilegen [ [...]]\n", stderr); + return 2; + } + + size_t maxBasenameLen = 0; + for (int index = 1; index < argc; index++) { + size_t length = strlen(argv[index]); + if (length > maxBasenameLen) { + maxBasenameLen = length; + } + } + + char filename[maxBasenameLen + 25]; + for (uint16_t i = 0;; i++) { // 65k images ought to be enough + for (int index = 1; index < argc; index++) { + sprintf(filename, "%s%" PRIu16 ".png", argv[index], i); + FILE *img = fopen(filename, "wb"); + if (!img) { + perror("PNG fopen"); + return 1; + } + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + perror("png_create_write_struct"); + return 1; + } + png_infop pngInfo = png_create_info_struct(png); + if (!pngInfo) { + perror("png_create_info_struct"); + return 1; + } + if (setjmp(png_jmpbuf(png))) { + fprintf(stderr, "FATAL: an error occurred while writing image \"%s\"\n", filename); + return 1; + } + + png_init_io(png, img); + generate_random_image(png, pngInfo); + png_destroy_write_struct(&png, &pngInfo); + fclose(img); + } + + if (i == UINT16_MAX) { + break; + } + } +}