shithub: rgbds

Download patch

ref: 3cfe7800c7dd02eb17f76b81e2659701ceb49c93
parent: 01cf0c5f98ffae99a85a32457f0310f17b748326
author: ISSOtm <eldredhabert0@gmail.com>
date: Sat Apr 30 16:37:46 EDT 2022

Make randtilegen and rgbgfx_test compile with MSVC

--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,8 +29,18 @@
 if(MSVC)
   # MSVC's standard library triggers warning C5105,
   # "macro expansion producing 'defined' has undefined behavior"
-  add_compile_options(/std:c11 /W1 /MP /wd5105)
+  add_compile_options(/MP /wd5105)
   add_definitions(/D_CRT_SECURE_NO_WARNINGS)
+  # Also, CMake appears not to pass the C11-enabling flag, so we must add it manually... but only for C!
+  if(NOT CMAKE_C_FLAGS MATCHES "std:c11") # The flag may already have been injected by an earlier CMake invocation, so don't add it twice
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std:c11" CACHE STRING "Flags used by the C compiler during all build types." FORCE)
+  endif()
+
+  if(SANITIZERS)
+    set(SAN_FLAGS /fsanitize=address)
+    add_compile_options(${SAN_FLAGS})
+    add_link_options(${SAN_FLAGS})
+  endif()
 else()
   add_compile_options(-Wall -pedantic)
   add_definitions(-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE)
@@ -41,7 +51,7 @@
                   -fsanitize=object-size -fsanitize=bool -fsanitize=enum
                   -fsanitize=alignment -fsanitize=null -fsanitize=address)
     add_compile_options(${SAN_FLAGS})
-    link_libraries(${SAN_FLAGS})
+    add_link_options(${SAN_FLAGS})
     # A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless
     # TODO: this overrides anything previously set... that's a bit sloppy!
     set(CMAKE_C_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
--- a/Makefile
+++ b/Makefile
@@ -129,7 +129,7 @@
 	$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} -x c++ src/version.c ${PNGLDLIBS}
 
 test/gfx/randtilegen: test/gfx/randtilegen.c
-	$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCFLAGS} -Wno-vla ${PNGCFLAGS} ${PNGLDLIBS}
+	$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
 
 test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp
 	$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGLDLIBS}
--- a/include/platform.h
+++ b/include/platform.h
@@ -46,12 +46,14 @@
 # include <unistd.h>
 #endif
 
-/* MSVC doesn't support `[static N]` for array arguments from C99 */
+/* MSVC doesn't support `[static N]` for array arguments from C99 or C11 */
 #ifdef _MSC_VER
 # define MIN_NB_ELMS(N)
+# define ARR_QUALS(...)
 # define NONNULL(ptr) *ptr
 #else
 # define MIN_NB_ELMS(N) static (N)
+# define ARR_QUALS(...) __VA_ARGS__
 # define NONNULL(ptr) ptr[static 1]
 #endif
 
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -1,8 +1,5 @@
 
 add_executable(randtilegen gfx/randtilegen.c)
-if(NOT MSVC)
-  target_compile_options(randtilegen PRIVATE -Wno-vla)
-endif()
 
 add_executable(rgbgfx_test gfx/rgbgfx_test.cpp)
 
--- a/test/gfx/randtilegen.c
+++ b/test/gfx/randtilegen.c
@@ -14,7 +14,7 @@
  */
 
 #include <assert.h>
-#include <inttypes.h>
+#include <errno.h>
 #include <limits.h>
 #include <png.h>
 #include <stdint.h>
@@ -22,43 +22,140 @@
 #include <stdlib.h>
 #include <string.h>
 
-#include "helpers.h"
+#include "platform.h"
 
-FILE *rngRecorder; // File to which the random bytes will be read
-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
+#define STR(x)  #x
+#define XSTR(x) STR(x)
 
-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 is guaranteed to exit without
-	// reading more bytes.
-	assert(count <= sizeof(randBits) * 8 + 1);
+struct Attributes {
+	unsigned char palette;
+	unsigned char nbColors;
+};
 
-	// Read bytes until we have enough bits to serve the request
-	while (count > randCount) {
-		int data = getchar();
+static unsigned long long randbits = 0;
+static unsigned char randcount = 0;
+
+static _Noreturn void fatal(char const *error) {
+	fprintf(stderr, "FATAL: %s\n", error);
+	exit(1);
+}
+
+static FILE *seed;
+
+static unsigned long long getRandomBits(unsigned count) {
+	while (count > randcount) {
+		// Get new random bytes from stdin (assumed to be a stream of random data) to fulfill the
+		// random bits request
+		int data = getc(seed);
 		if (data == EOF) {
 			exit(0);
 		}
-		randBits |= (uint32_t)data << randCount;
-		randCount += 8;
-		fputc(data, rngRecorder);
+		randbits |= (unsigned long long)data << randcount;
+		randcount += 8;
 	}
-
-	uint32_t result = randBits & (((uint32_t)1 << count) - 1);
-	randBits >>= count;
-	randCount -= count;
+	unsigned long long result = randbits & ((1ull << count) - 1);
+	randbits >>= count;
+	randcount -= count;
 	return result;
 }
 
-/**
- * Flush any remaining bits in the RNG storage
- */
-static void flushRng(void) {
-	randCount = 0;
-	randBits = 0;
+static void generate_tile_attributes(struct Attributes * restrict attributes) {
+	/*
+	 * Images have ten colors, grouped into two groups of 5 colors. The palette index indicates two
+	 * things: which one of those groups will be used, and which colors out of those 5 will be used
+	 * by the tile. The low bit indicates the group, and the rest of the value indicates the subset
+	 * of colors. The remainder of the number is treated as a bitfield, where each bit represents a
+	 * color: for instance, a value of 13 in the upper bits (binary 01101) indicates that colors 0,
+	 * 2 and 3 from that group will be used. Values of 0 and 31 are naturally invalid because they
+	 * indicate zero and five colors respectively, and 30 is also excluded to ensure that the
+	 * particular subset of colors 1, 2, 3 and 4 never shows up. This guarantees that every tile
+	 * will be representable using a palette containing color 0 (since those that don't contain
+	 * color 0 will have three colors at most), which in turn ensures that only 4 palettes per group
+	 * (and thus 8 total) are needed to cover the image: 0, 1, 2, 3; 0, 1, 2, 4; 0, 1, 3, 4; and 0,
+	 * 2, 3, 4. This also implies that making color 0 transparent (in both groups) adds a
+	 * transparent color to every palette.
+	 */
+	unsigned char pal;
+	do {
+		pal = getRandomBits(5);
+	} while (pal == 0 || (pal > 29));
+	attributes->palette = 2 * pal + getRandomBits(1);
+
+	// Use an array to look up the number of colors in the palette; this is faster (and simpler)
+	// than doing a population count over the bits
+	static char const popcount[] = {0, 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->nbColors = popcount[pal];
 }
 
+static void generate_tile_data(unsigned char tiledata[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8],
+                               unsigned colorcount) {
+	switch (colorcount) {
+	case 2: // 1bpp
+		for (uint8_t y = 0; y < 8; y++) {
+			for (uint8_t x = 0; x < 8; x++) {
+				tiledata[y][x] = getRandomBits(1);
+			}
+		}
+		break;
+
+	case 4: // 2bpp
+		for (uint8_t y = 0; y < 8; y++) {
+			for (uint8_t x = 0; x < 8; x++) {
+				tiledata[y][x] = getRandomBits(2);
+			}
+		}
+		break;
+
+	case 3: // 2bpp with resampling
+		for (uint8_t y = 0; y < 8; y++) {
+			for (uint8_t x = 0; x < 8; x++) {
+				do {
+					tiledata[y][x] = getRandomBits(2);
+				} while (tiledata[y][x] == 3);
+			}
+		}
+	}
+}
+
+// Can't mark as `const`, as the array type is otherwise not compatible (augh)
+static void
+    copy_tile_data(unsigned char destination[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8],
+                   unsigned char /* const */ source[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8]) {
+	// Apply a random rotation to the copy
+	// coord ^ 7 = inverted coordinate; coord ^ 0 = regular coordinate
+	unsigned xmask = getRandomBits(1) * 7;
+	unsigned ymask = getRandomBits(1) * 7;
+	for (unsigned y = 0; y < 8; y++) {
+		for (unsigned x = 0; x < 8; x++) {
+			destination[y][x] = source[y ^ ymask][x ^ xmask];
+		}
+	}
+}
+
+static void generate_palettes(uint16_t palettes[ARR_QUALS(restrict) MIN_NB_ELMS(60)][4]) {
+	uint16_t colors[10];
+	// Generate 10 random colors (two groups of 5 colors)
+	for (unsigned p = 0; p < 10; p++) {
+		colors[p] = getRandomBits(15);
+	}
+	// Potentially make the first color of each group transparent
+	if (!getRandomBits(2)) {
+		colors[0] |= 0x8000;
+		colors[5] |= 0x8000;
+	}
+
+	for (unsigned p = 0; p < 60; p++) {
+		uint16_t const *group = colors + 5 * (p & 1);
+		uint16_t *palette = palettes[p];
+		for (unsigned index = 0; index < 5; index++) {
+			if (p & (2 << index)) {
+				*(palette++) = group[index];
+			}
+		}
+	}
+}
+
 /**
  * Expand a 5-bit color component to 8 bits with minimal bias
  */
@@ -66,15 +163,26 @@
 	return five << 3 | five >> 2;
 }
 
-struct Attribute {
-	unsigned char palette;
-	unsigned char nbColors;
-};
-#define NB_TILES 10 * 10
-
-static void writePng(png_structp png, png_infop pngInfo, uint8_t width, uint8_t height, uint16_t palettes[][4], struct Attribute const *attributes, uint8_t tileData[][8][8]) {
+// Can't mark as `const`, as the array type is otherwise not compatible (augh)
+static void write_image(char const *filename, uint16_t /* const */ palettes[MIN_NB_ELMS(60)][4],
+                        unsigned char /* const */ (*tileData)[8][8],
+                        struct Attributes const *attributes, uint8_t width, uint8_t height) {
 	uint8_t const nbTiles = width * height;
-	
+	png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+	png_infop pngInfo = png_create_info_struct(png);
+
+	if (setjmp(png_jmpbuf(png))) {
+		fprintf(stderr, "FATAL: An error occurred while writing image \"%s\"", filename);
+		exit(1);
+	}
+
+	FILE *file = fopen(filename, "wb");
+	if (file == NULL) {
+		fprintf(stderr, "FATAL: Failed to open \"%s\": %s\n", filename, strerror(errno));
+		exit(1);
+	}
+	png_init_io(png, file);
+
 	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);
@@ -82,19 +190,24 @@
 	// 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];
+	assert(width != 0);
+	assert(height != 0);
+	uint8_t *data = malloc(height * 8 * width * 8 * SIZEOF_PIXEL);
+	uint8_t **rowPtrs = malloc(height * 8 * sizeof(*rowPtrs));
+	if (data == NULL || rowPtrs == NULL) {
+		fatal("Out of memory");
+	}
 	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);
+		uint8_t const 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]];
+				uint16_t const 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);
@@ -102,174 +215,106 @@
 			}
 		}
 	}
+
 	png_set_rows(png, pngInfo, rowPtrs);
 	png_write_png(png, pngInfo, PNG_TRANSFORM_IDENTITY, NULL);
+	fclose(file);
+	free(rowPtrs);
+	free(data);
+	png_destroy_write_struct(&png, &pngInfo);
 }
 
-static void generate_random_image(png_structp png, png_infop pngInfo) {
-	struct Attribute attributes[NB_TILES];
-	uint8_t tileData[NB_TILES][8][8];
-	// These two are in tiles, not pixels, and in range [3; 10], hence `NB_TILES` above
-	// 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];
-		// Handle single-color tiles the simple way, without trying to pull more random bits
-		if (attributes[p].nbColors < 2) {
-			memset(tileData[p], 0, sizeof(tileData[p]));
+static void generate_random_image(char const *filename) {
+#define MIN_TILES_PER_SIDE 3
+#define MAX_TILES          ((MIN_TILES_PER_SIDE + 7) * (MIN_TILES_PER_SIDE + 7))
+	struct Attributes attributes[MAX_TILES];
+	unsigned char tileData[MAX_TILES][8][8];
+	uint8_t width = getRandomBits(3) + MIN_TILES_PER_SIDE,
+	        height = getRandomBits(3) + MIN_TILES_PER_SIDE;
+	for (uint8_t tile = 0; tile < (width * height); tile++) {
+		generate_tile_attributes(attributes + tile);
+		// If a tile contains only one color, then there's no tile data to generate: all pixels will
+		// use color 0
+		if (attributes[tile].nbColors < 2) {
+			memset(tileData[tile], 0, sizeof(tileData[tile]));
 			continue;
 		}
-		uint8_t index, total;
-		for (index = 0, total = 0; index < p; index++) {
-			if (attributes[index].nbColors == attributes[p].nbColors) {
+
+		// Find tiles with the same number of colors
+		unsigned index = 0, total = 0;
+		for (; index < tile; index++) {
+			if (attributes[index].nbColors == attributes[tile].nbColors) {
 				total++;
 			}
 		}
-		// index == p at exit
+		assert(index == tile); // This is used as a sentinel later on to indicate no tile was found
+
 		if (total) {
+			// If there are such tiles, there's a random chance that this tile will replicate one of
+			// those tiles (potentially rotated)
 			index = getRandomBits(8);
 			if (index < total) {
 				total = index + 1;
 				for (index = 0; total; index++) {
-					if (attributes[index].nbColors == attributes[p].nbColors) {
+					if (attributes[index].nbColors == attributes[tile].nbColors) {
 						total--;
 					}
-					if (!total) {
-						index--;
-					}
 				}
+				if (total == 0) {
+					index--;
+				}
 			} else {
-				index = p;
+				index = tile; // Restore the sentinel
 			}
 		}
-		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)];
-				}
-			}
+
+		if (index == tile) {
+			generate_tile_data(tileData[tile], attributes[index].nbColors);
 		} else {
-			switch (attributes[p].nbColors) {
-			case 2: // Two-color tiles only need one random bit per pixel
-				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: // 4-color tiles can use two random bits per pixel
-				for (uint8_t y = 0; y < 8; y++)
-					for (uint8_t x = 0; x < 8; x++)
-						tileData[p][y][x] = getRandomBits(2);
-				break;
-			case 3: // 3-color tiles must draw two random bits, but reject them if out of range
-				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;
-					}
-				}
-				break;
-			default: // 1-color tiles were handled earlier
-				unreachable_();
-			}
+			copy_tile_data(tileData[tile], tileData[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++) {
-		uint16_t const *subpal = &colors[p & 1 ? 5 : 0];
-		uint8_t total = 0;
-		for (uint8_t index = 0; index < 5; index++) {
-			if (p & (2 << index)) {
-				palettes[p][total++] = subpal[index];
-			}
-		}
-	}
-
-	writePng(png, pngInfo, width, height, palettes, attributes, tileData);
+	generate_palettes(palettes);
+	write_image(filename, palettes, tileData, attributes, width, height);
 }
 
 int main(int argc, char **argv) {
-	if (argc < 2) {
-		fputs("usage: randtilegen <basename> [<basename> [...]]\n", stderr);
+	if (argc < 3 || argc > 4) {
+		fprintf(stderr, "usage: %s <input file> <basename> [<maxcount>]\n", argv[0]);
 		return 2;
 	}
 
-	size_t maxBasenameLen = 0;
-	for (int index = 1; index < argc; index++) {
-		size_t length = strlen(argv[index]);
-		if (length > maxBasenameLen) {
-			maxBasenameLen = length;
-		}
+	seed = fopen(argv[1], "rb");
+	if (!seed) {
+		fprintf(stderr, "FATAL: Cannot open seed file (%s)\n", strerror(errno));
+		return 1;
 	}
 
-	char filename[maxBasenameLen + sizeof("65535.png")];
-	for (uint16_t i = 0;; i++) { // 65k images ought to be enough
-		for (int index = 1; index < argc; index++) {
-			int len = sprintf(filename, "%s%" PRIu16 ".rng", argv[index], i);
-			rngRecorder = fopen(filename, "wb");
-			if (!rngRecorder) {
-				perror(filename);
-				return 1;
-			}
-
-			filename[len - 3] = 'p'; // `.rng` -> `.png`
-			FILE *img = fopen(filename, "wb");
-			if (!img) {
-				perror(filename);
-				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;
-			}
-
-			// Ensure that image generation starts on byte boundaries
-			// (This is necessary so that all involved random bits are recorded in the `.rng` file)
-			flushRng();
-
-			png_init_io(png, img);
-			generate_random_image(png, pngInfo);
-			png_destroy_write_struct(&png, &pngInfo);
-			fclose(img);
-			fclose(rngRecorder);
+	size_t const nameLen = strlen(argv[2]);
+	unsigned long long maxcount = ULLONG_MAX;
+	if (argc > 3) {
+		char *error;
+		maxcount = strtoull(argv[3], &error, 0);
+		if (*error != '\0') {
+			fatal("invalid count");
 		}
+	}
 
-		if (i == UINT16_MAX) {
-			break;
-		}
+	char *filename = malloc(nameLen + sizeof(XSTR(ULLONG_MAX) ".png"));
+	if (!filename) {
+		fatal("out of memory");
 	}
+	memcpy(filename, argv[2], nameLen);
+
+	for (unsigned long long count = 0; count < maxcount; count++) {
+		sprintf(&filename[nameLen], "%llu.png", count);
+		generate_random_image(filename);
+		// Reset the global random state so that subsequent images don't share a random byte
+		randbits = 0;
+		randcount = 0;
+	}
+
+	return 0;
 }
--- a/test/gfx/rgbgfx_test.cpp
+++ b/test/gfx/rgbgfx_test.cpp
@@ -1,6 +1,19 @@
-#include <sys/stat.h>
-#include <sys/wait.h>
 
+// For `execProg` (Windows is its special little snowflake again)
+#ifndef _MSC_VER
+	#include <sys/stat.h>
+	#include <sys/wait.h>
+
+	#include <spawn.h>
+	#include <unistd.h>
+#else
+	#define WIN32_LEAN_AND_MEAN // Include less from `windows.h` to avoid conflicts
+	#include <windows.h>
+	#include <errhandlingapi.h>
+	#include <processthreadsapi.h>
+	#undef max // This macro conflicts with `std::numeric_limits<...>::max()`
+#endif
+
 #include <algorithm>
 #include <array>
 #include <cassert>
@@ -10,11 +23,10 @@
 #include <limits>
 #include <memory>
 #include <png.h>
-#include <spawn.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <string.h>
-#include <unistd.h>
+#include <string>
 #include <vector>
 
 #include "defaultinitalloc.hpp"
@@ -267,12 +279,12 @@
 	~Png() { png_destroy_read_struct(&png, &info, nullptr); }
 };
 
-static int execProg(char const *name, char * const *argv,
-                    posix_spawn_file_actions_t const *actions = nullptr) {
+static char *execProg(char const *name, char * const *argv) {
+#ifndef _MSC_VER
 	pid_t pid;
-	int err = posix_spawn(&pid, argv[0], actions, nullptr, argv, nullptr);
+	int err = posix_spawn(&pid, argv[0], nullptr, nullptr, argv, nullptr);
 	if (err != 0) {
-		return err;
+		return strerror(err);
 	}
 
 	siginfo_t info;
@@ -286,7 +298,74 @@
 		fatal("%s returned with status %d", name, info.si_status);
 	}
 
-	return 0;
+#else /* defined(_MSC_VER) */
+
+	auto winStrerror = [](DWORD errnum) {
+		LPTSTR buf;
+		if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
+		                      | FORMAT_MESSAGE_MAX_WIDTH_MASK,
+		                  nullptr, errnum, 0, (LPTSTR)&buf, 0, nullptr)
+		    == 0) {
+			fatal("Failed to get error message for error 0x%x", errnum);
+		}
+		return buf;
+	};
+
+	char cmdLine[32768]; // Max command line size on Windows
+	char *ptr = cmdLine;
+	for (size_t i = 0; argv[i]; ++i) {
+		char const *src = argv[i];
+		// I miss you, `stpcpy`
+		while (*src) {
+			*ptr++ = *src++;
+		}
+		*ptr++ = ' ';
+	}
+	*ptr = '\0';
+
+	STARTUPINFOA startupInfo;
+	GetStartupInfoA(&startupInfo);
+	STARTUPINFOA childStartupInfo{sizeof(startupInfo),
+	                              nullptr,
+	                              nullptr,
+	                              nullptr,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              nullptr,
+	                              0,
+	                              0,
+	                              0};
+
+	PROCESS_INFORMATION child;
+	if (CreateProcessA(nullptr, cmdLine, nullptr, nullptr, true, 0, nullptr, nullptr,
+	                   &childStartupInfo, &child)
+	    == 0) {
+		return winStrerror(GetLastError());
+	}
+
+	DWORD status;
+	do {
+		if (GetExitCodeProcess(child.hProcess, &status) == 0) {
+			fatal("Error waiting for %s: %ls", name, winStrerror(GetLastError()));
+		}
+	} while (status == STILL_ACTIVE);
+	CloseHandle(child.hProcess);
+	CloseHandle(child.hThread);
+
+	if (status != 0) {
+		fatal("%s returned with status %ld", name, status);
+	}
+#endif
+
+	return nullptr;
 }
 
 int main(int argc, char **argv) {
@@ -296,20 +375,13 @@
 	}
 
 	{
-		posix_spawn_file_actions_t action;
-		// Putting these directly in the array makes them const or something.
 		char path[] = "./randtilegen", file[] = "out";
-		char *args[] = {path, file, nullptr};
+		char *args[] = {path, argv[1], file, nullptr};
 
-		posix_spawn_file_actions_init(&action);
-		posix_spawn_file_actions_addopen(&action, 0, argv[1], O_RDONLY, 0);
-
-		if (int ret = execProg("randtilegen", args, &action); ret != 0) {
+		if (auto ret = execProg("randtilegen", args); ret != nullptr) {
 			fatal("Failed to excute ./randtilegen (%s). Is it in the current working directory?",
-			      strerror(ret));
+			      ret);
 		}
-
-		posix_spawn_file_actions_destroy(&action);
 	}
 
 	{
@@ -321,9 +393,8 @@
 		// Also copy the trailing `nullptr`
 		std::copy_n(&argv[2], argc - 1, std::back_inserter(args));
 
-		if (int ret = execProg("rgbgfx conversion", args.data()); ret != 0) {
-			fatal("Failed to execute ../../rgbgfx (%s). Is it in the parent directory?",
-			      strerror(ret));
+		if (auto ret = execProg("rgbgfx conversion", args.data()); ret != nullptr) {
+			fatal("Failed to execute ../../rgbgfx (%s). Is it in the parent directory?", ret);
 		}
 	}
 
@@ -340,8 +411,8 @@
 		// Also copy the trailing `nullptr`
 		std::copy_n(&argv[2], argc - 1, std::back_inserter(args));
 
-		if (int ret = execProg("rgbgfx reversal", args.data()); ret != 0) {
-			fatal("Failed to execute ../../rgbgfx -r (%s)", strerror(ret));
+		if (auto ret = execProg("rgbgfx reversal", args.data()); ret != nullptr) {
+			fatal("Failed to execute ../../rgbgfx -r (%s)", ret);
 		}
 	}