joeylib2/tools/assetbake/assetbake.c

385 lines
13 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// assetbake.c -- bake indexed PNG assets into JoeyLib's native binary
// formats:
// * Tile banks (.tbk) are per target: plane-major on the Amiga and the
// X68000, a row's four plane bytes together on the ST, chunky 4bpp on
// DOS and the IIgs (the layout each port's tile paste reads).
// * Sprite cels (.spr) are chunky 4bpp for every target: the sprite
// walker converts at draw time.
// Both embed the PNG palette's first 16 entries as $0RGB.
//
// Source PNGs must be indexed with pixel dimensions that are multiples of
// 8. Tile banks take one 8x8 tile a grid cell, row by row; sprite sheets
// one cel a grid cell, the cel size given in tiles with --cell WxH.
//
// assetbake --type tile --target amiga|atarist|dos|iigs|x68000 in.png out.tbk
// assetbake --type sprite --cell WxH in.png out.spr
//
// .tbk: "JTB1", target (1 amiga, 2 atarist, 3 dos, 4 iigs, 5 x68000),
// hasPalette, tileCount (LE16), 4 reserved, 16 LE16 $0RGB palette words,
// then 32 bytes a tile.
// .spr: "JSP1", target 0, hasPalette, widthTiles, heightTiles, cellCount
// (LE16), 2 reserved, the palette, then each cel's tiles left to right, top
// to bottom, 8 rows of 4 chunky bytes each (high nibble the left pixel).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "toolFile.h"
#include "toolPng.h"
#define TOOL_NAME "assetbake"
#define MAGIC_BYTES 4u
#define TARGET_CROSS_CHUNKY 0u
#define TARGET_AMIGA 1u
#define TARGET_ATARIST 2u
#define TARGET_DOS 3u
#define TARGET_IIGS 4u
#define TARGET_X68000 5u
#define TILE_PX 8u
#define TILE_BYTES 32u
#define PLANES 4u
#define PALETTE_ENTRIES 16u
#define HEADER_BYTES 44u
#define HEADER_PALETTE_AT 12u
#define MAX_COUNT 0xFFFFu
#define NIBBLE_MASK 0x0Fu
#define NIBBLE_SHIFT 4u
#define USAGE_EXIT 2
typedef void (*TileEncoderT)(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
typedef struct TargetT {
const char *name;
uint8_t id;
TileEncoderT encoder;
} TargetT;
static void encodeTileChunky(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
static void encodeTilePlaneMajor(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
static void encodeTileRowPlanes(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
static void headerWrite(uint8_t *header, const char *magic, const ToolImageT *img);
static void loadIndexedPng(const char *path, ToolImageT *img);
static const char *optionValue(int argc, char **argv, int *a, const char *name);
static void parseCell(const char *spec, uint32_t *w, uint32_t *h);
static uint8_t planeByte(const ToolImageT *img, uint32_t ox, uint32_t y, uint32_t plane);
static void usage(const char *message);
static void writeOutput(const char *path, const uint8_t *header, const uint8_t *body, size_t bodyLen);
static const TargetT kTargets[] = {
{ "amiga", TARGET_AMIGA, encodeTilePlaneMajor },
{ "atarist", TARGET_ATARIST, encodeTileRowPlanes },
{ "dos", TARGET_DOS, encodeTileChunky },
{ "iigs", TARGET_IIGS, encodeTileChunky },
{ "x68000", TARGET_X68000, encodeTilePlaneMajor }
};
#define TARGET_COUNT (sizeof(kTargets) / sizeof(kTargets[0]))
// 8 rows of 4 bytes, the high nibble the left pixel.
static void encodeTileChunky(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
const uint8_t *row;
uint32_t r;
uint32_t c;
for (r = 0; r < TILE_PX; r++) {
row = img->pixels + (size_t)(oy + r) * img->width + ox;
for (c = 0; c < TILE_PX; c += 2u) {
*out++ = (uint8_t)(((row[c] & NIBBLE_MASK) << NIBBLE_SHIFT) | (row[c + 1u] & NIBBLE_MASK));
}
}
}
// The Amiga and the X68000: plane 0's 8 row bytes, then plane 1's, ...
static void encodeTilePlaneMajor(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
uint32_t k;
uint32_t r;
for (k = 0; k < PLANES; k++) {
for (r = 0; r < TILE_PX; r++) {
*out++ = planeByte(img, ox, oy + r, k);
}
}
}
// The ST: each row's plane bytes 0..3 together.
static void encodeTileRowPlanes(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
uint32_t r;
uint32_t k;
for (r = 0; r < TILE_PX; r++) {
for (k = 0; k < PLANES; k++) {
*out++ = planeByte(img, ox, oy + r, k);
}
}
}
// The header both formats share: the magic, the PNG palette's first 16
// entries as LE16 $0RGB (an entry the PNG lacks is 0), and the caller's
// fields at 4..11. An indexed PNG always has a palette.
static void headerWrite(uint8_t *header, const char *magic, const ToolImageT *img) {
uint16_t word;
uint32_t i;
memset(header, 0, HEADER_BYTES);
memcpy(header, magic, MAGIC_BYTES);
for (i = 0; i < PALETTE_ENTRIES && i < img->paletteCount; i++) {
word = (uint16_t)((img->palette[i][0] >> NIBBLE_SHIFT) << 8 | (img->palette[i][1] >> NIBBLE_SHIFT) << 4 | (img->palette[i][2] >> NIBBLE_SHIFT));
header[HEADER_PALETTE_AT + i * 2u] = (uint8_t)word;
header[HEADER_PALETTE_AT + i * 2u + 1u] = (uint8_t)(word >> 8);
}
}
static void loadIndexedPng(const char *path, ToolImageT *img) {
char err[TOOL_PNG_ERROR_BYTES];
if (!toolPngLoad(path, img, err)) {
toolFail("%s: %s", path, err);
}
if (!img->indexed) {
toolFail("%s is not an indexed PNG, need 'P' (indexed)", path);
}
if (img->width % TILE_PX != 0u || img->height % TILE_PX != 0u) {
toolFail("%s dims %ux%u not 8-px-tile multiples", path, img->width, img->height);
}
}
// The value of option `name` at argv[*a] (`--name value` or `--name=value`),
// *a stepped past it; NULL when argv[*a] is not that option.
static const char *optionValue(int argc, char **argv, int *a, const char *name) {
size_t len;
len = strlen(name);
if (strncmp(argv[*a], name, len) != 0) {
return NULL;
}
if (argv[*a][len] == '=') {
return argv[*a] + len + 1;
}
if (argv[*a][len] != '\0') {
return NULL;
}
if (*a + 1 >= argc) {
usage("an option needs a value");
}
*a += 1;
return argv[*a];
}
// "WxH" in tiles, as the Python read it: exactly one x (either case), and
// two integers, both positive.
static void parseCell(const char *spec, uint32_t *w, uint32_t *h) {
const char *x;
long width;
long height;
x = strpbrk(spec, "xX");
if (x == NULL || strpbrk(x + 1, "xX") != NULL) {
toolFail("bad --cell '%s', need 'WxH'", spec);
}
if (!toolParseInt(spec, (size_t)(x - spec), &width) || !toolParseInt(x + 1, strlen(x + 1), &height)) {
toolFail("bad --cell '%s', non-integer", spec);
}
if (width <= 0 || height <= 0) {
toolFail("bad --cell '%s', zero/negative", spec);
}
*w = (uint32_t)width;
*h = (uint32_t)height;
}
// One plane's bits of the 8 pixels from (ox, y), the leftmost in bit 7.
static uint8_t planeByte(const ToolImageT *img, uint32_t ox, uint32_t y, uint32_t plane) {
const uint8_t *row;
uint32_t c;
uint8_t b;
row = img->pixels + (size_t)y * img->width + ox;
b = 0u;
for (c = 0; c < TILE_PX; c++) {
if (((row[c] >> plane) & 1u) != 0u) {
b = (uint8_t)(b | (0x80u >> c));
}
}
return b;
}
// The command line, the targets read from kTargets (scripts read them from
// here: tests/host/tileBankStage.sh).
static void usage(const char *message) {
size_t i;
fprintf(stderr, "usage: %s --type {sprite,tile} [--target {", TOOL_NAME);
for (i = 0; i < TARGET_COUNT; i++) {
fprintf(stderr, "%s%s", i == 0u ? "" : ",", kTargets[i].name);
}
fprintf(stderr, "}] [--cell WxH] input output\n");
fprintf(stderr, "%s: error: %s\n", TOOL_NAME, message);
exit(USAGE_EXIT);
}
static void writeOutput(const char *path, const uint8_t *header, const uint8_t *body, size_t bodyLen) {
FILE *f;
bool ok;
f = fopen(path, "wb");
if (f == NULL) {
toolFail("cannot write %s", path);
}
ok = fwrite(header, 1, HEADER_BYTES, f) == HEADER_BYTES && fwrite(body, 1, bodyLen, f) == bodyLen;
if (fclose(f) != 0 || !ok) {
toolFail("cannot write %s", path);
}
}
int main(int argc, char **argv) {
ToolImageT img;
const TargetT *target;
const char *type;
const char *targetName;
const char *cell;
const char *paths[2];
const char *value;
uint8_t header[HEADER_BYTES];
uint8_t *body;
uint8_t *out;
size_t bodyLen;
uint32_t pathCount;
uint32_t count;
uint32_t cellW;
uint32_t cellH;
uint32_t cx;
uint32_t cy;
uint32_t tx;
uint32_t ty;
uint32_t i;
int a;
toolFileName(TOOL_NAME);
type = NULL;
targetName = NULL;
cell = NULL;
pathCount = 0u;
for (a = 1; a < argc; a++) {
value = optionValue(argc, argv, &a, "--type");
if (value != NULL) {
type = value;
continue;
}
value = optionValue(argc, argv, &a, "--target");
if (value != NULL) {
targetName = value;
continue;
}
value = optionValue(argc, argv, &a, "--cell");
if (value != NULL) {
cell = value;
continue;
}
if (argv[a][0] == '-' && argv[a][1] != '\0') {
usage("unrecognized arguments");
}
if (pathCount == 2u) {
usage("unrecognized arguments");
}
paths[pathCount++] = argv[a];
}
if (type == NULL || pathCount < 2u) {
usage("the following arguments are required: --type, input, output");
}
if (strcmp(type, "tile") != 0 && strcmp(type, "sprite") != 0) {
usage("argument --type: invalid choice");
}
target = NULL;
if (targetName != NULL) {
for (i = 0; i < TARGET_COUNT; i++) {
if (strcmp(targetName, kTargets[i].name) == 0) {
target = &kTargets[i];
}
}
if (target == NULL) {
usage("argument --target: invalid choice");
}
}
if (strcmp(type, "tile") == 0) {
if (target == NULL) {
toolFail("--type tile requires --target");
}
loadIndexedPng(paths[0], &img);
count = (img.width / TILE_PX) * (img.height / TILE_PX);
if (count > MAX_COUNT) {
toolFail("%s %u tiles exceeds 65535", paths[0], count);
}
headerWrite(header, "JTB1", &img);
header[4] = target->id;
header[5] = 1u;
header[6] = (uint8_t)count;
header[7] = (uint8_t)(count >> 8);
bodyLen = (size_t)count * TILE_BYTES;
body = (uint8_t *)malloc(bodyLen > 0u ? bodyLen : 1u);
if (body == NULL) {
toolFail("out of memory");
}
out = body;
for (ty = 0; ty < img.height / TILE_PX; ty++) {
for (tx = 0; tx < img.width / TILE_PX; tx++) {
target->encoder(&img, tx * TILE_PX, ty * TILE_PX, out);
out += TILE_BYTES;
}
}
} else {
if (cell == NULL) {
toolFail("--type sprite requires --cell WxH");
}
parseCell(cell, &cellW, &cellH);
loadIndexedPng(paths[0], &img);
if (img.width % (cellW * TILE_PX) != 0u || img.height % (cellH * TILE_PX) != 0u) {
toolFail("%s %ux%u not multiple of cell %ux%u", paths[0], img.width, img.height, cellW * TILE_PX, cellH * TILE_PX);
}
count = (img.width / (cellW * TILE_PX)) * (img.height / (cellH * TILE_PX));
if (count > MAX_COUNT) {
toolFail("%s %u cels exceeds 65535", paths[0], count);
}
headerWrite(header, "JSP1", &img);
header[4] = TARGET_CROSS_CHUNKY;
header[5] = 1u;
header[6] = (uint8_t)cellW;
header[7] = (uint8_t)cellH;
header[8] = (uint8_t)count;
header[9] = (uint8_t)(count >> 8);
bodyLen = (size_t)count * cellW * cellH * TILE_BYTES;
body = (uint8_t *)malloc(bodyLen > 0u ? bodyLen : 1u);
if (body == NULL) {
toolFail("out of memory");
}
out = body;
for (cy = 0; cy < img.height / (cellH * TILE_PX); cy++) {
for (cx = 0; cx < img.width / (cellW * TILE_PX); cx++) {
for (ty = 0; ty < cellH; ty++) {
for (tx = 0; tx < cellW; tx++) {
encodeTileChunky(&img, (cx * cellW + tx) * TILE_PX, (cy * cellH + ty) * TILE_PX, out);
out += TILE_BYTES;
}
}
}
}
}
writeOutput(paths[1], header, body, bodyLen);
free(body);
toolImageFree(&img);
return 0;
}