joeylib2/src/core/assetLoad.c
2026-06-30 18:07:12 -05:00

248 lines
9.1 KiB
C

// Native asset loaders for baked .spr and .tbk files produced by
// tools/assetbake/assetbake.py. Both formats are documented in detail
// in that tool; the short version:
//
// .spr cross-target chunky 4bpp sprite cels (the Phase 11 sprite
// walker reads chunky and c2p's inline at draw time, so a
// single bake serves every platform)
// .tbk per-target planar tile bytes (Amiga plane-major, ST row-
// major-with-planes-per-row, DOS+IIgs chunky 4bpp); the
// loader validates the file's target byte matches the build
// platform and refuses mismatches
//
// Both share a 44-byte header:
// 0 4 magic ("JSP1" or "JTB1")
// 4 1 target (0=cross-platform, 1=amiga, 2=ST, 3=dos, 4=iigs)
// 5 1 hasPalette
// 6 2 widthTiles + heightTiles (sprite) / tileCount LE16 (tile)
// 8 4 cellCount LE16 + reserved (sprite) / reserved (tile)
// 12 32 palette[16] LE16 $0RGB (zeros if hasPalette==0)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "joey/sprite.h"
#include "joey/tile.h"
#include "spriteInternal.h"
// Push the file-IO out of the root segment on the IIgs. Every
// fopen / fread / fclose call drags a chunk of the stdio cluster
// in; concentrating them in one named segment keeps the root bank
// from overflowing past 64 KB (the failure mode is the linker's
// "Code exceeds code bank size" / "Address is not in current bank"
// errors documented in the feedback memory).
// ----- Constants -----
#define ASSET_HEADER_BYTES 44
#define ASSET_PALETTE_OFFSET 12
#define ASSET_PALETTE_ENTRIES 16
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows
// Largest legitimately drawable sprite cel: the surface is 40x25 tiles
// (320/8 x 200/8), so any cel larger than that can never be fully
// on-screen. Reject crafted/truncated headers claiming more, which
// would otherwise force an unbounded per-cel malloc (DoS) and feed the
// downstream emitters/draw walkers out-of-range dimensions.
#define ASSET_MAX_TILES_W 40
#define ASSET_MAX_TILES_H 25
// Per-platform expected target byte for .tbk files. .spr files carry
// target=0 (cross-platform chunky) and pass on every build.
#if defined(JOEYLIB_PLATFORM_AMIGA)
#define EXPECTED_TILE_TARGET 1u
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#define EXPECTED_TILE_TARGET 2u
#elif defined(JOEYLIB_PLATFORM_DOS)
#define EXPECTED_TILE_TARGET 3u
#elif defined(JOEYLIB_PLATFORM_IIGS)
#define EXPECTED_TILE_TARGET 4u
#elif defined(JOEYLIB_PLATFORM_BLANK)
#define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops)
#else
#error "Unknown platform for asset loader"
#endif
// ----- Prototypes -----
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
// ----- Internal helpers (alphabetical) -----
// Open `path`, read the shared 44-byte header, and validate the magic
// + target byte common to both .spr and .tbk. On success leaves *outFp
// open (caller must fclose) and returns true; on any failure closes
// the file (if opened) and returns false. When outPalette is non-NULL
// and the header advertises a palette (header[5] != 0), the palette is
// decoded into outPalette. Shared by jlSpriteBankLoad and
// jlTileBankLoad so the open/read/validate/parse-palette boilerplate
// (and its fclose-on-error pattern) lives in one place.
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp) {
FILE *fp;
fp = fopen(path, "rb");
if (fp == NULL) {
return false;
}
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
fclose(fp);
return false;
}
if (memcmp(header, magic, 4) != 0) {
fclose(fp);
return false;
}
if (header[4] != expectedTarget) {
fclose(fp);
return false;
}
if (outPalette != NULL && header[5] != 0u) {
readPaletteFromHeader(header, outPalette);
}
*outFp = fp;
return true;
}
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
uint8_t i;
for (i = 0; i < ASSET_PALETTE_ENTRIES; i++) {
outPalette[i] = (uint16_t)(header[ASSET_PALETTE_OFFSET + i * 2]
| (header[ASSET_PALETTE_OFFSET + i * 2 + 1] << 8));
}
}
// ----- Public API (alphabetical) -----
// Returns the number of cels successfully loaded into outCels[0..N).
// A return value strictly less than min(headerCellCount, maxCels)
// indicates the file was truncated/corrupt mid-stream relative to its
// own header count; the cels that were read are still valid.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
uint16_t *outPalette) {
FILE *fp;
uint8_t header[ASSET_HEADER_BYTES];
uint8_t widthTiles;
uint8_t heightTiles;
uint16_t cellCount;
uint32_t tileBytes;
uint16_t i;
uint16_t loaded;
uint16_t toLoad;
if (path == NULL || outCels == NULL || maxCels == 0u) {
return 0u;
}
// Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
// else means the file was either baked wrong or is a stale .spr
// from the old compiled-codegen pipeline.
if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
return 0u;
}
widthTiles = header[6];
heightTiles = header[7];
cellCount = (uint16_t)(header[8] | (header[9] << 8));
// Reject degenerate (0 in either dimension -> tileBytes 0, an
// implementation-defined malloc(0)) and oversized (a crafted header
// could force a multi-MB per-cel malloc) sprites. Matches
// jlSpriteCreate's invariant and the 40x25-tile surface ceiling.
if (widthTiles == 0u || heightTiles == 0u ||
widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H) {
fclose(fp);
return 0u;
}
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
toLoad = (cellCount < maxCels) ? cellCount : maxCels;
loaded = 0u;
for (i = 0; i < toLoad; i++) {
uint8_t *buf;
jlSpriteT *sp;
buf = (uint8_t *)malloc((size_t)tileBytes);
if (buf == NULL) {
break;
}
if (fread(buf, 1, (size_t)tileBytes, fp) != (size_t)tileBytes) {
free(buf);
break;
}
sp = (jlSpriteT *)malloc(sizeof(jlSpriteT));
if (sp == NULL) {
free(buf);
break;
}
sp->tileData = buf;
sp->widthTiles = widthTiles;
sp->heightTiles = heightTiles;
sp->ownsTileData = true;
sp->slot = NULL;
// 0xFF == SPRITE_NOT_COMPILED for every routine offset (0 is a
// valid offset, so zero-fill would alias a real entry).
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
outCels[i] = sp;
loaded++;
}
fclose(fp);
return loaded;
}
// Returns the number of tiles successfully loaded. outValid[0..maxTiles)
// is always fully written: true for each tile read, false for the rest
// (including slots past a truncated/short file), so the caller need not
// pre-zero it. A return value strictly less than min(headerTileCount,
// maxTiles) indicates the file was truncated/corrupt mid-stream.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette) {
FILE *fp;
uint8_t header[ASSET_HEADER_BYTES];
uint16_t tileCount;
uint16_t i;
uint16_t loaded;
uint16_t toLoad;
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
return 0u;
}
// The target byte for .tbk must match the build platform. .spr
// files carry target=0; tiles carry the per-platform value. A
// mismatch is a build-system bug -- the per-target make rule
// should have produced a .tbk with the matching target byte.
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
return 0u;
}
tileCount = (uint16_t)(header[6] | (header[7] << 8));
// Define the whole outValid array up front so leftover slots read
// false exactly as the tile.h contract promises, regardless of
// caller pre-zeroing.
if (outValid != NULL) {
for (i = 0; i < maxTiles; i++) {
outValid[i] = false;
}
}
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
loaded = 0u;
for (i = 0; i < toLoad; i++) {
if (fread(outTiles[i].pixels, 1, ASSET_TILE_DATA_BYTES, fp) != ASSET_TILE_DATA_BYTES) {
break;
}
if (outValid != NULL) {
outValid[i] = true;
}
loaded++;
}
fclose(fp);
return loaded;
}