// 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 #include #include #include #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; }