joeylib2/tools/spritebake/spritebake.c

217 lines
7.2 KiB
C

// spritebake -- offline pre-compiler for JoeyLib sprite blit routines.
//
// Reads a cross-platform .spr cel bank (as tools/assetbake/assetbake.py
// produces), runs the SAME per-CPU emitters + staging the runtime JIT uses
// (src/codegen/spriteStage.c, linked with one src/*/spriteEmit*.c chosen at
// compile time), and writes a per-platform .spc holding the pre-emitted
// native routine images + slot-relative routineOffsets. The runtime loader
// (jlSpriteBankLoadPrecompiled) then skips jlSpriteCompile entirely.
//
// Built once per target by make/tools.mk with -DJOEYLIB_PLATFORM_<port> so
// JOEY_SPRITE_SHIFT_COUNT and the emitted machine code match that target,
// while running on the build host -- the emitters use only fixed/gated
// compile-time constants, so host output is byte-identical to the target JIT.
//
// spritebake <in.spr> <out.spc>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "joey/tile.h"
#include "spriteInternal.h"
#include "spritePrecompiled.h"
#include "spriteStage.h"
#define SPR_MAGIC "JSP1"
#define SPR_HEADER_BYTES 44
#define SPR_OFF_WIDTH 6
#define SPR_OFF_HEIGHT 7
#define SPR_OFF_CELLS 8
#define SPR_OFF_PALETTE 12
#define SPR_PALETTE_BYTES 32
#if defined(JOEYLIB_PLATFORM_AMIGA)
#define SPC_TARGET 1
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#define SPC_TARGET 2
#elif defined(JOEYLIB_PLATFORM_DOS)
#define SPC_TARGET 3
#elif defined(JOEYLIB_PLATFORM_IIGS)
#define SPC_TARGET 4
#else
#error "spritebake: build with -DJOEYLIB_PLATFORM_<port>"
#endif
#if defined(JOEYLIB_PLATFORM_ATARIST)
// The ST emitter bakes the address of this walker into each shifted-DRAW
// thunk. On the host bake tool the value is don't-care (the .spc loader
// re-stamps the real target address at load), so a never-called stub just
// resolves the link -- the real walker is src/atarist/spriteWalk.s on target.
void stSpriteDrawShiftWalker(void) {}
#endif
static void putLE16(FILE *f, uint16_t v) {
fputc((int)(v & 0xFFu), f);
fputc((int)((v >> 8) & 0xFFu), f);
}
static void putLE32(FILE *f, uint32_t v) {
fputc((int)(v & 0xFFu), f);
fputc((int)((v >> 8) & 0xFFu), f);
fputc((int)((v >> 16) & 0xFFu), f);
fputc((int)((v >> 24) & 0xFFu), f);
}
int main(int argc, char **argv) {
FILE *in;
FILE *out;
uint8_t header[SPR_HEADER_BYTES];
uint8_t widthTiles;
uint8_t heightTiles;
uint16_t cellCount;
uint32_t tileBytes;
uint8_t *staging;
uint16_t cel;
if (argc != 3) {
fprintf(stderr, "usage: spritebake <in.spr> <out.spc>\n");
return 2;
}
in = fopen(argv[1], "rb");
if (in == NULL) {
fprintf(stderr, "spritebake: cannot open %s\n", argv[1]);
return 1;
}
if (fread(header, 1, SPR_HEADER_BYTES, in) != SPR_HEADER_BYTES ||
memcmp(header, SPR_MAGIC, 4) != 0 || header[SPC_OFF_TARGET] != 0u) {
fprintf(stderr, "spritebake: %s is not a JSP1 cross-target .spr\n", argv[1]);
fclose(in);
return 1;
}
widthTiles = header[SPR_OFF_WIDTH];
heightTiles = header[SPR_OFF_HEIGHT];
cellCount = (uint16_t)(header[SPR_OFF_CELLS] | (header[SPR_OFF_CELLS + 1] << 8));
if (widthTiles == 0u || heightTiles == 0u || cellCount == 0u) {
fprintf(stderr, "spritebake: degenerate geometry in %s\n", argv[1]);
fclose(in);
return 1;
}
tileBytes = (uint32_t)widthTiles * heightTiles * TILE_BYTES;
staging = (uint8_t *)malloc(SPRITE_STAGING_BYTES);
if (staging == NULL) {
fclose(in);
return 1;
}
out = fopen(argv[2], "wb");
if (out == NULL) {
fprintf(stderr, "spritebake: cannot write %s\n", argv[2]);
free(staging);
fclose(in);
return 1;
}
// .spc header: same 44-byte prefix, but JSC1 + per-machine target byte,
// plus the baked shift count at byte 10 for the loader to validate.
fwrite(SPC_MAGIC, 1, 4, out);
fputc(SPC_TARGET, out);
fputc(header[SPC_OFF_HAS_PALETTE], out);
fputc(widthTiles, out);
fputc(heightTiles, out);
putLE16(out, cellCount);
fputc((int)JOEY_SPRITE_SHIFT_COUNT, out);
fputc(0, out); // reserved
fwrite(header + SPR_OFF_PALETTE, 1, SPR_PALETTE_BYTES, out);
for (cel = 0; cel < cellCount; cel++) {
uint8_t *tileData;
jlSpriteT sp;
uint32_t total;
uint16_t relocOffsets[JOEY_SPRITE_SHIFT_COUNT];
uint16_t relocCount;
uint8_t shift;
uint8_t op;
tileData = (uint8_t *)malloc(tileBytes);
if (tileData == NULL) {
fprintf(stderr, "spritebake: OOM cel %u\n", (unsigned)cel);
goto fail;
}
if (fread(tileData, 1, tileBytes, in) != tileBytes) {
fprintf(stderr, "spritebake: %s truncated at cel %u\n", argv[1], (unsigned)cel);
free(tileData);
goto fail;
}
// Run the identical staging the runtime JIT would: same emitters,
// same tier order, same offsets -> byte-identical image.
memset(&sp, 0, sizeof(sp));
sp.tileData = tileData;
sp.widthTiles = widthTiles;
sp.heightTiles = heightTiles;
sp.widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
sp.heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
total = spriteStageAndPlan(staging, &sp, false);
// ST shifted-phase DRAW thunks bake an absolute jmp to the shared
// walker; record its patch site so the loader re-stamps the runtime
// address. Every other target/op is fully position-independent.
relocCount = 0u;
#if defined(JOEYLIB_PLATFORM_ATARIST)
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
uint16_t drawOff = sp.routineOffsets[shift][SPRITE_OP_DRAW];
if ((shift & 7u) != 0u && drawOff != SPRITE_NOT_COMPILED) {
relocOffsets[relocCount] = (uint16_t)(drawOff + SPC_ST_WALKER_JMP_OFFSET);
relocCount++;
}
}
#endif
// celMeta: imageSize, routineOffsets[shift][op], relocCount.
putLE32(out, total);
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
putLE16(out, sp.routineOffsets[shift][op]);
}
}
putLE16(out, relocCount);
// Raw tile blob (kept for the interpreter/clip fallback + dims), then
// the pre-emitted routine image, then the reloc site list.
fwrite(tileData, 1, tileBytes, out);
if (total != 0u) {
fwrite(staging, 1, total, out);
}
for (uint16_t r = 0; r < relocCount; r++) {
putLE16(out, relocOffsets[r]);
}
free(tileData);
}
if (fclose(out) != 0) {
fprintf(stderr, "spritebake: write error on %s\n", argv[2]);
free(staging);
fclose(in);
return 1;
}
free(staging);
fclose(in);
printf("spritebake: %s -> %s (%u cels, target %d, %d shifts)\n",
argv[1], argv[2], (unsigned)cellCount, SPC_TARGET, (int)JOEY_SPRITE_SHIFT_COUNT);
return 0;
fail:
fclose(out);
free(staging);
fclose(in);
return 1;
}