joeylib2/tools/joeysprite/joeysprite.c

342 lines
10 KiB
C

// joeysprite: host-side compiler that turns raw tile data into a
// `.spr` file ready to be loaded at runtime by spriteLoadFile.
//
// Usage:
// joeysprite --target {iigs,amiga,atarist,dos}
// --width-tiles N --height-tiles M
// input.tiles output.spr
//
// `input.tiles` is widthTiles * heightTiles * 32 bytes, laid out
// tile-major as the runtime SpriteT.tileData expects: tile (0,0)
// first 32 bytes, tile (1,0) next 32, ... tile (widthTiles-1, 0),
// then tile (0,1), and so on. Inside each tile, rows are stored
// top-to-bottom and each row is 4 bytes (8 pixels at 4bpp packed,
// high nibble = left pixel).
//
// Output `.spr` format (target-native byte order for code; see
// DESIGN.md §12). Mirrors src/core/sprite.c's reader:
// byte 0 widthTiles
// byte 1 heightTiles
// bytes 2-3 codeSize (LE16)
// bytes 4-5 tileBytes (LE16) = widthTiles*heightTiles*32
// ... offsets (JOEY_SPRITE_SHIFT_COUNT * SPRITE_OP_COUNT *
// uint16_t LE): [draw_s0, save_s0, restore_s0,
// draw_s1, save_s1, restore_s1]. Save/restore offsets
// are 0 here -- the runtime keeps the memcpy-based
// interpreter for those ops.
// ... compiled code (codeSize bytes)
// ... raw tile data (tileBytes bytes; same layout as the
// input file, lets the runtime interpreter handle
// clipped draws without decoding the compiled bytes).
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "joey/sprite.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
typedef enum {
TARGET_IIGS,
TARGET_AMIGA,
TARGET_ATARIST,
TARGET_DOS,
TARGET_INVALID
} TargetE;
// ----- Constants -----
#define MAX_SCRATCH_BYTES (16u * 1024u)
#define SPR_HEADER_SIZE 6
// Save/restore offsets are reserved (0) for now -- the runtime
// memcpy interpreter handles them.
#define SHIFT_OPS 3
#define OFFSET_TABLE_BYTES (JOEY_SPRITE_SHIFT_COUNT * SHIFT_OPS * 2u)
// ----- Prototypes -----
static int compileToSpr(const SpriteT *sp, TargetE target, const char *outPath);
static uint16_t emitForTarget(uint8_t *out, const SpriteT *sp, uint8_t shift, TargetE target);
static int loadTileData(const char *path, uint8_t **outBytes, uint32_t *outSize);
static TargetE parseTarget(const char *name);
static int usage(const char *prog);
static int writeLE16(FILE *fp, uint16_t v);
// ----- Internal helpers (alphabetical) -----
static int compileToSpr(const SpriteT *sp, TargetE target, const char *outPath) {
uint8_t *scratch;
uint8_t *codeBuf;
uint16_t shiftLengths[JOEY_SPRITE_SHIFT_COUNT];
uint32_t totalCodeSize;
uint8_t shift;
uint8_t op;
uint16_t written;
uint16_t cursor;
uint16_t offset;
FILE *fp;
int rc;
scratch = (uint8_t *)malloc(MAX_SCRATCH_BYTES);
if (scratch == NULL) {
fprintf(stderr, "joeysprite: out of memory\n");
return 2;
}
totalCodeSize = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
written = emitForTarget(scratch, sp, shift, target);
shiftLengths[shift] = written;
totalCodeSize += written;
}
if (totalCodeSize > 0xFFFFu) {
fprintf(stderr, "joeysprite: emitted %u code bytes; max is 65535\n",
(unsigned)totalCodeSize);
free(scratch);
return 2;
}
codeBuf = (uint8_t *)malloc(totalCodeSize);
if (codeBuf == NULL) {
fprintf(stderr, "joeysprite: out of memory for code buffer\n");
free(scratch);
return 2;
}
cursor = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
written = emitForTarget(codeBuf + cursor, sp, shift, target);
cursor = (uint16_t)(cursor + written);
}
fp = fopen(outPath, "wb");
if (fp == NULL) {
fprintf(stderr, "joeysprite: cannot open %s for writing\n", outPath);
free(codeBuf);
free(scratch);
return 2;
}
rc = 0;
if (fputc(sp->widthTiles, fp) == EOF) rc = 2;
if (rc == 0 && fputc(sp->heightTiles, fp) == EOF) rc = 2;
if (rc == 0 && writeLE16(fp, (uint16_t)totalCodeSize) != 0) rc = 2;
if (rc == 0 && writeLE16(fp, (uint16_t)(sp->widthTiles * sp->heightTiles * 32u)) != 0) rc = 2;
// Offset table: cumulative draw offsets + zeros for save/restore.
offset = 0;
for (shift = 0; rc == 0 && shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SHIFT_OPS; op++) {
uint16_t value;
if (op == SPRITE_OP_DRAW) {
value = offset;
} else {
value = 0;
}
if (writeLE16(fp, value) != 0) {
rc = 2;
break;
}
}
offset = (uint16_t)(offset + shiftLengths[shift]);
}
if (rc == 0) {
if (fwrite(codeBuf, 1, totalCodeSize, fp) != totalCodeSize) {
rc = 2;
}
}
if (rc == 0) {
// Append the raw tile data so the runtime interpreter has it
// available for clipped draws.
uint32_t tileBytes = (uint32_t)sp->widthTiles * (uint32_t)sp->heightTiles * 32u;
if (sp->tileData == NULL) {
fprintf(stderr, "joeysprite: sprite missing tile data, cannot save\n");
rc = 2;
} else if (fwrite(sp->tileData, 1, tileBytes, fp) != tileBytes) {
rc = 2;
}
}
fclose(fp);
free(codeBuf);
free(scratch);
if (rc == 0) {
printf("joeysprite: %u code bytes -> %s (target=%s, %ux%u tiles)\n",
(unsigned)totalCodeSize, outPath,
target == TARGET_IIGS ? "iigs" :
target == TARGET_AMIGA ? "amiga" :
target == TARGET_ATARIST ? "atarist" : "dos",
sp->widthTiles, sp->heightTiles);
}
return rc;
}
static uint16_t emitForTarget(uint8_t *out, const SpriteT *sp, uint8_t shift, TargetE target) {
switch (target) {
case TARGET_DOS:
return spriteEmitDrawX86(out, sp, shift);
case TARGET_AMIGA:
case TARGET_ATARIST:
return spriteEmitDraw68k(out, sp, shift);
case TARGET_IIGS:
return spriteEmitDrawIigs(out, sp, shift);
default:
return 0;
}
}
static int loadTileData(const char *path, uint8_t **outBytes, uint32_t *outSize) {
FILE *fp;
long fileSize;
uint8_t *buf;
size_t read;
fp = fopen(path, "rb");
if (fp == NULL) {
fprintf(stderr, "joeysprite: cannot open %s\n", path);
return 2;
}
if (fseek(fp, 0L, SEEK_END) != 0) {
fclose(fp);
return 2;
}
fileSize = ftell(fp);
if (fileSize <= 0) {
fprintf(stderr, "joeysprite: %s is empty\n", path);
fclose(fp);
return 2;
}
if (fseek(fp, 0L, SEEK_SET) != 0) {
fclose(fp);
return 2;
}
buf = (uint8_t *)malloc((size_t)fileSize);
if (buf == NULL) {
fclose(fp);
return 2;
}
read = fread(buf, 1, (size_t)fileSize, fp);
fclose(fp);
if (read != (size_t)fileSize) {
free(buf);
return 2;
}
*outBytes = buf;
*outSize = (uint32_t)fileSize;
return 0;
}
static TargetE parseTarget(const char *name) {
if (strcmp(name, "iigs") == 0) return TARGET_IIGS;
if (strcmp(name, "amiga") == 0) return TARGET_AMIGA;
if (strcmp(name, "atarist") == 0) return TARGET_ATARIST;
if (strcmp(name, "dos") == 0) return TARGET_DOS;
return TARGET_INVALID;
}
static int usage(const char *prog) {
fprintf(stderr,
"usage: %s --target {iigs,amiga,atarist,dos} \\\n"
" --width-tiles N --height-tiles M \\\n"
" input.tiles output.spr\n", prog);
return 2;
}
// 65816 / x86 / 68k all expect target-native byte order in the .spr
// header offsets, but the file format is little-endian (matches the
// runtime spriteFromCompiledMem parser, which reads byte-by-byte).
static int writeLE16(FILE *fp, uint16_t v) {
if (fputc((int)(v & 0xFFu), fp) == EOF) return -1;
if (fputc((int)((v >> 8) & 0xFFu), fp) == EOF) return -1;
return 0;
}
// ----- main -----
int main(int argc, char **argv) {
const char *targetName;
const char *inPath;
const char *outPath;
long widthTiles;
long heightTiles;
int i;
TargetE target;
uint8_t *tileBytes;
uint32_t tileSize;
uint32_t expectedTileSize;
SpriteT sp;
int rc;
targetName = NULL;
widthTiles = 0;
heightTiles = 0;
inPath = NULL;
outPath = NULL;
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "--target") == 0 && i + 1 < argc) {
targetName = argv[++i];
} else if (strcmp(argv[i], "--width-tiles") == 0 && i + 1 < argc) {
widthTiles = strtol(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--height-tiles") == 0 && i + 1 < argc) {
heightTiles = strtol(argv[++i], NULL, 10);
} else if (inPath == NULL) {
inPath = argv[i];
} else if (outPath == NULL) {
outPath = argv[i];
} else {
return usage(argv[0]);
}
}
if (targetName == NULL || widthTiles <= 0 || widthTiles > 255 ||
heightTiles <= 0 || heightTiles > 255 ||
inPath == NULL || outPath == NULL) {
return usage(argv[0]);
}
target = parseTarget(targetName);
if (target == TARGET_INVALID) {
fprintf(stderr, "joeysprite: unknown --target %s\n", targetName);
return usage(argv[0]);
}
rc = loadTileData(inPath, &tileBytes, &tileSize);
if (rc != 0) {
return rc;
}
expectedTileSize = (uint32_t)(widthTiles * heightTiles * 32);
if (tileSize != expectedTileSize) {
fprintf(stderr,
"joeysprite: %s is %u bytes; expected %u (%ld * %ld tiles * 32 bytes)\n",
inPath, (unsigned)tileSize, (unsigned)expectedTileSize,
widthTiles, heightTiles);
free(tileBytes);
return 2;
}
sp.tileData = tileBytes;
sp.widthTiles = (uint8_t)widthTiles;
sp.heightTiles = (uint8_t)heightTiles;
sp.ownsTileData = false;
sp.slot = NULL;
memset(sp.routineOffsets, 0, sizeof(sp.routineOffsets));
sp.flags = SPRITE_FLAGS_NONE;
rc = compileToSpr(&sp, target, outPath);
free(tileBytes);
return rc;
}