shithub: rgbds

Download patch

ref: cc27169ecdd4e29944e207acc4a39f25e78b0a8f
parent: 843022772be7e72c5348a72709d61dce1c62adb6
author: ISSOtm <[email protected]>
date: Sun Mar 27 16:21:00 EDT 2022

Implement preliminary version of "reverse" feature

Not hooked to all RGBGFX flags yet, but good enough for most use cases
(and as a base for future development, should I need to `reset --hard`.)

TODOs marked appropriately.

--- a/Makefile
+++ b/Makefile
@@ -110,6 +110,7 @@
 	src/gfx/pal_sorting.o \
 	src/gfx/process.o \
 	src/gfx/proto_palette.o \
+	src/gfx/reverse.o \
 	src/gfx/rgba.o \
 	src/extern/getopt.o \
 	src/error.o
--- a/include/gfx/main.hpp
+++ b/include/gfx/main.hpp
@@ -20,6 +20,9 @@
 #include "gfx/rgba.hpp"
 
 struct Options {
+	uint8_t reversedWidth = 0; // -r, in pixels
+	bool reverse() const { return reversedWidth != 0; }
+
 	bool useColorCurve = false; // -C
 	bool fixInput = false; // -f
 	bool allowMirroring = false; // -m
@@ -36,7 +39,7 @@
 	} palSpecType = NO_SPEC; // -c
 	std::vector<std::array<Rgba, 4>> palSpec{};
 	uint8_t bitDepth = 2; // -d
-	std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L
+	std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS)
 	std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
 	uint8_t nbPalettes = 8; // -n
 	std::string output{}; // -o
@@ -83,5 +86,13 @@
 
 	uint8_t size() const;
 };
+
+static constexpr uint8_t flip(uint8_t byte) {
+	// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
+	byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
+	byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
+	byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
+	return byte;
+}
 
 #endif /* RGBDS_GFX_MAIN_HPP */
--- /dev/null
+++ b/include/gfx/reverse.hpp
@@ -1,0 +1,14 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_REVERSE_HPP
+#define RGBDS_GFX_REVERSE_HPP
+
+void reverse();
+
+#endif /* RGBDS_GFX_REVERSE_HPP */
--- a/include/gfx/rgba.hpp
+++ b/include/gfx/rgba.hpp
@@ -17,12 +17,22 @@
 	uint8_t blue;
 	uint8_t alpha;
 
-	Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
+	constexpr Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
+	    : red(r), green(g), blue(b), alpha(a) {}
 	/**
 	 * Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
 	 */
-	explicit Rgba(uint32_t rgba = 0)
+	explicit constexpr Rgba(uint32_t rgba = 0)
 	    : red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
+
+	static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
+		constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
+			fiveBpp &= 0b11111; // For caller's convenience
+			return fiveBpp << 3 | fiveBpp >> 2;
+		};
+		return {_5to8(cgbColor), _5to8(cgbColor >> 5), _5to8(cgbColor >> 10),
+		        (uint8_t)(cgbColor & 0x8000 ? 0x00 : 0xFF)};
+	}
 
 	/**
 	 * Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
--- a/man/rgbgfx.1
+++ b/man/rgbgfx.1
@@ -14,6 +14,7 @@
 .Nd Game Boy graphics converter
 .Sh SYNOPSIS
 .Nm
+.Op Fl r Ar stride
 .Op Fl CfmuVZ
 .Op Fl v Op Fl v No ...
 .Op Fl a Ar attrmap | Fl A
@@ -33,7 +34,7 @@
 .Sh DESCRIPTION
 The
 .Nm
-program converts PNG images into data suitable for display on the Game Boy and Game Boy Color.
+program converts PNG images into data suitable for display on the Game Boy and Game Boy Color, or vice-versa.
 .Pp
 The main function of
 .Nm
@@ -214,6 +215,22 @@
 cannot be more than
 .Ql 1 << Ar depth
 .Pq see Fl d .
+.It Fl r Ar width , Fl Fl reverse Ar width
+Switches
+.Nm
+into
+.Dq Sy reverse
+mode.
+In this mode, instead of converting a PNG image into Game Boy data,
+.Nm
+will attempt to reverse the process, and render Game Boy data into an image.
+See
+.Sx REVERSE MODE
+below for details.
+.Pp
+.Ar width
+is the image's width, in tiles
+.Pq including any margins specified by Fl L .
 .It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
 Generate a file of tile indices.
 For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file.
@@ -430,6 +447,46 @@
 TODO.
 .Ss Attrmap data
 TODO.
+.Sh REVERSE MODE
+.Nm
+can produce a PNG image from valid data.
+This may be useful for ripping graphics, recovering lost source images, etc.
+An important caveat on that last one, though: the conversion process is
+.Sy lossy
+both ways, so the
+.Do reversed Dc image won't be perfectly identical to the original\(embut it should be close to a Game Boy's output .
+.Pq Keep in mind that many of consoles output different colors, so there is no true reference rendering.
+.Pp
+When using reverse mode, make sure to pass the same flags that were given when generating the data, especially
+.Fl C , d , N , s , x ,
+and
+.Fl Z .
+.Do At-files Dc may help with this .
+.Nm
+will warn about any inconsistencies it detects.
+.Pp
+Files that are normally outputs
+.Pq Fl a , p , t
+become inputs, and
+.Ar file
+will be written to instead of read from, and thus needs not exist beforehand.
+Any of these inputs not passed is assumed to be some default:
+.Bl -column "attribute map"
+.It palettes Ta Unspecified palette data makes
+.Nm
+assume DMG (monochrome Game Boy) mode: a single palette of 4 grays.
+It is possible to pass palettes using
+.Fl c
+instead of
+.Fl p .
+.It tile data Ta Tile data must be provided, as there is no reasonable assumption to fall back on.
+.It tile map Ta A missing tile map makes
+.Nm
+assume that tiles were not deduplicated, and should be laid out in the order they are stored.
+.It attribute map Ta Without an attribute map,
+.Nm
+assumes that no tiles were mirrored.
+.El
 .Sh NOTES
 Some flags have had their functionality removed.
 .Fl D
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -75,6 +75,7 @@
     "gfx/pal_sorting.cpp"
     "gfx/process.cpp"
     "gfx/proto_palette.cpp"
+    "gfx/reverse.cpp"
     "gfx/rgba.cpp"
     "extern/getopt.c"
     "error.c"
--- a/src/gfx/main.cpp
+++ b/src/gfx/main.cpp
@@ -27,6 +27,7 @@
 #include "version.h"
 
 #include "gfx/process.hpp"
+#include "gfx/reverse.hpp"
 
 using namespace std::literals::string_view_literals;
 
@@ -83,7 +84,7 @@
 }
 
 // Short options
-static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:s:Tt:U:uVvx:Z";
+static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:r:s:Tt:U:uVvx:Z";
 
 /*
  * Equivalent long options
@@ -113,6 +114,7 @@
     {"output",          required_argument, NULL, 'o'},
     {"output-palette",  no_argument,       NULL, 'P'},
     {"palette",         required_argument, NULL, 'p'},
+    {"reverse",         required_argument, NULL, 'r'},
     {"output-tilemap",  no_argument,       NULL, 'T'},
     {"tilemap",         required_argument, NULL, 't'},
     {"unit-size",       required_argument, NULL, 'U'},
@@ -125,7 +127,7 @@
 };
 
 static void printUsage(void) {
-	fputs("Usage: rgbgfx [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n"
+	fputs("Usage: rgbgfx [-r] [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n"
 	      "       [-c color_spec] [-d <depth>] [-L slice] [-N nb_tiles] [-n nb_pals]\n"
 	      "	      [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n"
 	      "	      [-U unit_size] [-x <tiles>] <file>\n"
@@ -430,6 +432,14 @@
 			break;
 		case 'n':
 			options.nbPalettes = parseNumber(arg, "Number of palettes", 8);
+			if (*arg != '\0') {
+				error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
+			}
+			if (options.nbPalettes > 8) {
+				error("Number of palettes (-n) must not exceed 8!");
+			} else if (options.nbPalettes == 0) {
+				error("Number of palettes (-n) may not be 0!");
+			}
 			break;
 		case 'o':
 			options.output = musl_optarg;
@@ -441,15 +451,24 @@
 			autoPalettes = false;
 			options.palettes = musl_optarg;
 			break;
+		case 'r':
+			options.reversedWidth = parseNumber(arg, "Reversed image stride");
+			if (*arg != '\0') {
+				error("Reversed image stride (-r) must be a valid number, not \"%s\"", musl_optarg);
+			}
+			if (options.reversedWidth == 0) {
+				error("Reversed image stride (-r) may not be 0!");
+			}
+			break;
 		case 's':
 			options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
 			if (*arg != '\0') {
-				error("Palette size (-s) argument must be a valid number, not \"%s\"", musl_optarg);
+				error("Palette size (-s) must be a valid number, not \"%s\"", musl_optarg);
 			}
 			if (options.nbColorsPerPal > 4) {
-				error("Palette size (-s) argument must not exceed 4!");
+				error("Palette size (-s) must not exceed 4!");
 			} else if (options.nbColorsPerPal == 0) {
-				error("Palette size (-s) argument may not be 0!");
+				error("Palette size (-s) may not be 0!");
 			}
 			break;
 		case 'T':
@@ -678,7 +697,11 @@
 		return 0;
 	}
 
-	process();
+	if (options.reverse()) {
+		reverse();
+	} else {
+		process();
+	}
 
 	return 0;
 }
--- a/src/gfx/process.cpp
+++ b/src/gfx/process.cpp
@@ -552,14 +552,6 @@
 	}
 }
 
-static uint8_t flip(uint8_t byte) {
-	// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
-	byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
-	byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
-	byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
-	return byte;
-}
-
 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
--- /dev/null
+++ b/src/gfx/reverse.cpp
@@ -1,0 +1,301 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "gfx/reverse.hpp"
+
+#include <algorithm>
+#include <array>
+#include <assert.h>
+#include <cinttypes>
+#include <errno.h>
+#include <fstream>
+#include <optional>
+#include <png.h>
+#include <string.h>
+#include <tuple>
+#include <vector>
+
+#include "defaultinitalloc.hpp"
+#include "helpers.h"
+
+#include "gfx/main.hpp"
+
+static DefaultInitVec<uint8_t> readInto(std::string path) {
+	std::filebuf file;
+	file.open(path, std::ios::in | std::ios::binary);
+	DefaultInitVec<uint8_t> data(128 * 16); // Begin with some room pre-allocated
+
+	size_t curSize = 0;
+	for (;;) {
+		size_t oldSize = curSize;
+		curSize = data.size();
+
+		// Fill the new area ([oldSize; curSize[) with bytes
+		size_t nbRead =
+		    file.sgetn(reinterpret_cast<char *>(&data.data()[oldSize]), curSize - oldSize);
+		if (nbRead != curSize - oldSize) {
+			// Shrink the vector to discard bytes that weren't read
+			data.resize(oldSize + nbRead);
+			break;
+		}
+		// If the vector has some capacity left, use it; otherwise, double the current size
+
+		// Arbitrary, but if you got a better idea...
+		size_t newSize = oldSize != data.capacity() ? data.capacity() : oldSize * 2;
+		assert(oldSize != newSize);
+		data.resize(newSize);
+	}
+
+	return data;
+}
+
+[[noreturn]] static void pngError(png_structp png, char const *msg) {
+	fatal("Error writing reversed image (\"%s\"): %s",
+	      static_cast<char const *>(png_get_error_ptr(png)), msg);
+}
+
+static void pngWarning(png_structp png, char const *msg) {
+	warning("While writing reversed image (\"%s\"): %s",
+	        static_cast<char const *>(png_get_error_ptr(png)), msg);
+}
+
+void writePng(png_structp png, png_bytep data, size_t length) {
+	auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
+	pngFile.sputn(reinterpret_cast<char *>(data), length);
+}
+
+void flushPng(png_structp png) {
+	auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
+	pngFile.pubsync();
+}
+
+void reverse() {
+	options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
+
+	// Check for weird flag combinations
+
+	if (options.output.empty()) {
+		fatal("Tile data must be provided when reversing an image!");
+	}
+
+	if (!options.allowDedup && options.tilemap.empty()) {
+		warning("Tile deduplication is enabled, but no tilemap is provided?");
+	}
+
+	if (options.useColorCurve) {
+		warning("The color curve is not yet supported in reverse mode...");
+	}
+
+	options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
+	auto const tiles = readInto(options.output);
+	uint8_t tileSize = 8 * options.bitDepth;
+	if (tiles.size() % tileSize != 0) {
+		fatal("Tile data size must be a multiple of %" PRIu8 " bytes! (Read %zu)", tileSize,
+		      tiles.size());
+	}
+
+	// By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles
+	size_t nbTileInstances = tiles.size() / tileSize + options.trim; // Image size in tiles
+	options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTileInstances);
+	std::optional<DefaultInitVec<uint8_t>> tilemap;
+	if (!options.tilemap.empty()) {
+		tilemap = readInto(options.tilemap);
+		nbTileInstances = tilemap->size();
+
+		// TODO: range check
+	}
+
+	if (nbTileInstances > options.maxNbTiles[0] + options.maxNbTiles[1]) {
+		warning("Read %zu tiles, more than the limit of %zu + %zu", nbTileInstances,
+		        options.maxNbTiles[0], options.maxNbTiles[1]);
+	}
+
+	if (nbTileInstances % options.reversedWidth) {
+		fatal("Image size (%zu tiles) is not divisible by the provided stride (%zu tiles), cannot "
+		      "determine image dimensions",
+		      nbTileInstances, options.reversedWidth);
+	}
+	size_t width, height;
+	size_t usefulWidth = options.reversedWidth - options.inputSlice[1] - options.inputSlice[3];
+	if (usefulWidth % 8 != 0) {
+		fatal(
+		    "No input slice specified (`-L`), and specified image width (%zu) not a multiple of 8",
+		    usefulWidth);
+	} else {
+		width = usefulWidth / 8;
+		if (nbTileInstances % width != 0) {
+			fatal("Total number of tiles read (%zu) cannot be divided by image width (%zu tiles)",
+			      nbTileInstances, width);
+		}
+		height = nbTileInstances / width;
+	}
+	options.verbosePrint(Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width,
+	                     height);
+
+	// TODO: -U
+
+	std::vector<std::array<Rgba, 4>> palettes{
+	    {Rgba(0xffffffff), Rgba(0xaaaaaaff), Rgba(0x555555ff), Rgba(0x000000ff)}
+    };
+	if (!options.palettes.empty()) {
+		std::filebuf file;
+		file.open(options.palettes, std::ios::in | std::ios::binary);
+
+		palettes.clear();
+		std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
+		size_t nbRead;
+		do {
+			nbRead = file.sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
+			if (nbRead == buf.size()) {
+				// Expand the colors
+				auto &palette = palettes.emplace_back();
+				std::generate(palette.begin(), palette.begin() + options.nbColorsPerPal,
+				              [&buf, i = 0]() mutable {
+					              i += 2;
+					              return Rgba::fromCGBColor(buf[i - 2] + (buf[i - 1] << 8));
+				              });
+			} else if (nbRead != 0) {
+				fatal("Palette data size (%zu) is not a multiple of %zu bytes!\n",
+				      palettes.size() * buf.size() + nbRead, buf.size());
+			}
+		} while (nbRead != 0);
+
+		if (palettes.size() > options.nbPalettes) {
+			warning("Read %zu palettes, more than the specified limit of %zu", palettes.size(),
+			        options.nbPalettes);
+		}
+	}
+
+	std::optional<DefaultInitVec<uint8_t>> attrmap;
+	if (!options.attrmap.empty()) {
+		attrmap = readInto(options.attrmap);
+		if (attrmap->size() != nbTileInstances) {
+			fatal("Attribute map size (%zu tiles) doesn't match image's (%zu)", attrmap->size(),
+			      nbTileInstances);
+		}
+
+		// Scan through the attributes for inconsistencies
+		// We do this now for two reasons:
+		// 1. Checking those during the main loop is harmful to optimization, and
+		// 2. It clutters the code more, and it's not in great shape to begin with
+	}
+
+	// TODO: palette map (overrides attributes)
+
+	options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
+	std::filebuf pngFile;
+	pngFile.open(options.input, std::ios::out | std::ios::binary);
+	png_structp png = png_create_write_struct(
+	    PNG_LIBPNG_VER_STRING,
+	    const_cast<png_voidp>(static_cast<void const *>(options.input.c_str())), pngError,
+	    pngWarning);
+	if (!png) {
+		fatal("Couldn't create PNG write struct: %s", strerror(errno));
+	}
+	png_infop pngInfo = png_create_info_struct(png);
+	if (!pngInfo) {
+		fatal("Couldn't create PNG info struct: %s", strerror(errno));
+	}
+	png_set_write_fn(png, &pngFile, writePng, flushPng);
+
+	// TODO: if `-f` is passed, write the image indexed instead of RGB
+	png_set_IHDR(png, pngInfo, options.reversedWidth,
+	             height * 8 + options.inputSlice[0] + options.inputSlice[2], 8,
+	             PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
+	             PNG_FILTER_TYPE_DEFAULT);
+	png_write_info(png, pngInfo);
+
+	png_color_8 sbitChunk;
+	sbitChunk.red = 5;
+	sbitChunk.green = 5;
+	sbitChunk.blue = 5;
+	sbitChunk.alpha = 1;
+	png_set_sBIT(png, pngInfo, &sbitChunk);
+
+	constexpr uint8_t SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component)
+	size_t const SIZEOF_ROW = options.reversedWidth * SIZEOF_PIXEL;
+	std::vector<uint8_t> tileRow(8 * SIZEOF_ROW, 0xFF); // Data for 8 rows of pixels
+	uint8_t * const rowPtrs[8] = {
+	    &tileRow.data()[0 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[1 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[2 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[3 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[4 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[5 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[6 * SIZEOF_ROW + options.inputSlice[3]],
+	    &tileRow.data()[7 * SIZEOF_ROW + options.inputSlice[3]],
+	};
+
+	auto const fillRows = [&png, &tileRow](size_t nbRows) {
+		for (size_t _ = 0; _ < nbRows; ++_) {
+			png_write_row(png, tileRow.data());
+		}
+	};
+	fillRows(options.inputSlice[0]);
+
+	for (size_t ty = 0; ty < height; ++ty) {
+		for (size_t tx = 0; tx < width; ++tx) {
+			size_t index = options.columnMajor ? ty + tx * width : ty * width + tx;
+			// Get the tile ID at this location
+			uint8_t gbcTileID = tilemap.has_value() ? (*tilemap)[index] : index;
+			// By default, a tile is unflipped, in bank 0, and uses palette #0
+			uint8_t attribute = attrmap.has_value() ? (*attrmap)[index] : 0x00;
+			bool bank = attribute & 0x08;
+			gbcTileID -= options.baseTileIDs[bank];
+			size_t tileID = gbcTileID + bank * options.maxNbTiles[0];
+			assert(tileID < nbTileInstances); // Should have been checked earlier
+
+			// We do not have data for tiles trimmed with `-x`, so assume they are "blank"
+			static std::array<uint8_t, 16> const trimmedTile{
+			    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			};
+			uint8_t const *tileData = tileID > nbTileInstances - options.trim
+			                              ? trimmedTile.data()
+			                              : &tiles[tileID * tileSize];
+			assert((attribute & 0b111) < palettes.size()); // Should be ensured on data read
+			auto const &palette = palettes[attribute & 0b111];
+			for (uint8_t y = 0; y < 8; ++y) {
+				// If vertically mirrored, fetch the bytes from the other end
+				uint8_t realY = attribute & 0x40 ? 7 - y : y;
+				uint8_t bitplane0 = tileData[realY * 2], bitplane1 = tileData[realY * 2 + 1];
+				if (attribute & 0x20) { // Handle horizontal flip
+					bitplane0 = flip(bitplane0);
+					bitplane1 = flip(bitplane1);
+				}
+				uint8_t *ptr = &rowPtrs[y][tx * 8 * SIZEOF_PIXEL];
+				for (uint8_t x = 0; x < 8; ++x) {
+					uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80;
+					Rgba const &pixel = palette[bit0 >> 7 | bit1 >> 6];
+					*ptr++ = pixel.red;
+					*ptr++ = pixel.green;
+					*ptr++ = pixel.blue;
+					*ptr++ = pixel.alpha;
+
+					// Shift the pixel out
+					bitplane0 <<= 1;
+					bitplane1 <<= 1;
+				}
+			}
+		}
+		// We never modify the pointers, and neither should libpng, despite the overly lax function
+		// signature.
+		// (AIUI, casting away const-ness is okay as long as you don't actually modify the
+		// pointed-to data)
+		png_write_rows(png, const_cast<png_bytepp>(rowPtrs), 8);
+	}
+	// Clear the first row again for the function
+	std::fill(tileRow.begin(), tileRow.begin() + SIZEOF_ROW, 0xFF);
+	fillRows(options.inputSlice[2]);
+
+	// Finalize the write
+	png_write_end(png, pngInfo);
+
+	png_destroy_write_struct(&png, &pngInfo);
+	pngFile.close();
+}