695 lines
28 KiB
C
695 lines
28 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 <stdint.h>
|
|
|
|
#include "joey/file.h"
|
|
#include "joey/sprite.h"
|
|
#include "joey/tile.h"
|
|
#include "port.h"
|
|
#include "spriteInternal.h"
|
|
#include "spritePrecompiled.h"
|
|
|
|
#if defined(JOEYLIB_PLATFORM_ATARIST)
|
|
// The Atari ST shifted-phase DRAW thunk tail-jumps to this shared walker via
|
|
// a baked absolute address; the .spc loader re-stamps it after load (the only
|
|
// port needing any relocation -- see spritePrecompiled.h).
|
|
extern void stSpriteDrawShiftWalker(void);
|
|
#endif
|
|
|
|
// IIgs segment placement (PERF-AUDIT #60): this TU carries NO named
|
|
// segment directive, deliberately. An earlier comment here claimed the
|
|
// file-IO was concentrated in a named segment to keep the stdio
|
|
// cluster from overflowing the root bank -- that directive never
|
|
// existed, and the failure mode it guarded against ("Code exceeds code
|
|
// bank size" / "Address is not in current bank") was a problem of the
|
|
// ORCA-Linker era; the clang/OMF pipeline splits oversized segments
|
|
// automatically. Do not add a segment directive here without link-map
|
|
// evidence -- root-segment packing is pressure-sensitive (see the
|
|
// bank-overflow notes in the feedback memory).
|
|
|
|
// ----- Constants -----
|
|
|
|
#define ASSET_HEADER_BYTES 44
|
|
#define ASSET_PALETTE_OFFSET 12
|
|
#define ASSET_PALETTE_ENTRIES 16
|
|
|
|
// All game data is opened relative to this directory (see joey/file.h).
|
|
#define DATA_DIR_PREFIX "DATA/"
|
|
#define DATA_PATH_MAX 256
|
|
|
|
// Tile-bank payload reads stream the whole run in big freads instead of
|
|
// one 32-byte fread per tile: jlTileT is exactly TILE_BYTES with no
|
|
// padding (compile-time-checked below), so tiles are contiguous in the
|
|
// output array. The old per-tile loop issued 1000 32-byte freads for a
|
|
// full font bank -- on the IIgs each fread is a GS/OS round trip through
|
|
// SmartPort firmware, which dominated (and on heavily-sparse .tbk files
|
|
// wedged) asset loading. NOTE: a single fread is NOT guaranteed to
|
|
// satisfy a large request in one call (the IIgs libc returned ~4 KB of
|
|
// an 8 KB request), so jlTileBankLoad loops the fread until the payload
|
|
// is fully read -- see the CONTRACT FIX comment there.
|
|
|
|
// Compile-time guard for the contiguous read: consecutive jlTileT
|
|
// entries must be contiguous pixel payloads (no padding), or the
|
|
// bulk fread would misalign every tile after the first.
|
|
typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1];
|
|
|
|
// Tile geometry comes from joey/tile.h, the single source of truth for
|
|
// the 4bpp packed layout (PERF-AUDIT #30): the per-tile payload read
|
|
// below is TILE_BYTES -- exactly what jlTileT.pixels holds -- and the
|
|
// largest legitimately drawable sprite cel is the full surface grid,
|
|
// TILE_BLOCKS_PER_ROW x TILE_BLOCKS_PER_COL (SURFACE_WIDTH/HEIGHT over
|
|
// TILE_PIXELS_PER_SIDE, 40x25 on the 320x200 surface). Any cel larger
|
|
// than that can never be fully on-screen, so reject crafted/truncated
|
|
// headers claiming more, which would otherwise force an unbounded
|
|
// per-cel allocation (DoS) and feed the downstream emitters/draw
|
|
// walkers out-of-range dimensions.
|
|
#define ASSET_MAX_TILES_W TILE_BLOCKS_PER_ROW
|
|
#define ASSET_MAX_TILES_H TILE_BLOCKS_PER_COL
|
|
|
|
// 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)
|
|
#elif defined(JOEYLIB_PLATFORM_X68000)
|
|
// Plane-major like the Amiga (src/m68k/planarTile.h reads jlTileT.pixels as
|
|
// 4 planes x 8 rows), baked by assetbake --target x68000.
|
|
#define EXPECTED_TILE_TARGET 5u
|
|
#else
|
|
#error "Unknown platform for asset loader"
|
|
#endif
|
|
|
|
// Per-platform target byte for .spc files (pre-compiled sprite code, so
|
|
// it is per MACHINE CODE, not per pixel format: every port's banks carry
|
|
// their own byte, so a bank baked for any other build is refused). Mirrors
|
|
// SPC_TARGET in tools/spritebake.
|
|
#if defined(JOEYLIB_PLATFORM_AMIGA)
|
|
#define EXPECTED_SPC_TARGET 1u
|
|
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
|
#define EXPECTED_SPC_TARGET 2u
|
|
#elif defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_BLANK)
|
|
#define EXPECTED_SPC_TARGET 3u
|
|
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
|
#define EXPECTED_SPC_TARGET 4u
|
|
#elif defined(JOEYLIB_PLATFORM_X68000)
|
|
#define EXPECTED_SPC_TARGET 5u
|
|
#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 = jlDataOpen(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) -----
|
|
|
|
// Open a game-data file by its name relative to DATA/. Forcing the prefix
|
|
// here is the single place the DATA/ convention is enforced -- every asset
|
|
// loader and example routes file opens through this, so callers never
|
|
// hard-code "DATA/" and the runtime tree layout stays consistent across
|
|
// ports. Returns NULL if the joined path overflows DATA_PATH_MAX or the
|
|
// file cannot be opened.
|
|
FILE *jlDataOpen(const char *name, const char *mode) {
|
|
char path[DATA_PATH_MAX];
|
|
size_t prefixLen = sizeof(DATA_DIR_PREFIX) - 1u; // literal length, no NUL
|
|
size_t nameLen;
|
|
|
|
// A manual prefix concat instead of snprintf: the format is fixed
|
|
// ("DATA/" + name), and snprintf drags the whole C formatted-output
|
|
// engine (~20 KB of code plus a soft-float dependency) into every
|
|
// binary that opens a data file, which the IIgs bank-0 budget cannot
|
|
// spare. strlen/memcpy do the same join with none of that.
|
|
if (name == NULL) {
|
|
return NULL;
|
|
}
|
|
nameLen = strlen(name);
|
|
if (prefixLen + nameLen + 1u > sizeof(path)) {
|
|
return NULL;
|
|
}
|
|
memcpy(path, DATA_DIR_PREFIX, prefixLen);
|
|
memcpy(path + prefixLen, name, nameLen + 1u); // copy the NUL too
|
|
return fopen(path, mode);
|
|
}
|
|
|
|
|
|
// 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)TILE_BYTES;
|
|
toLoad = (cellCount < maxCels) ? cellCount : maxCels;
|
|
|
|
loaded = 0u;
|
|
for (i = 0; i < toLoad; i++) {
|
|
uint8_t *buf;
|
|
jlSpriteT *sp;
|
|
|
|
// jlpBigAlloc for the same #31/#81 reasons as sprite.c: these
|
|
// buffers overflow the tiny IIgs C heap, whose malloc returns
|
|
// garbage instead of NULL at exhaustion.
|
|
buf = (uint8_t *)jlpBigAlloc(tileBytes);
|
|
if (buf == NULL) {
|
|
break;
|
|
}
|
|
if (fread(buf, 1, (size_t)tileBytes, fp) != (size_t)tileBytes) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
|
|
if (sp == NULL) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
// Shared init path (spriteInternal.h, claudeDocs/PERF-AUDIT.md #32): sets
|
|
// the 0xFF/0 compiled-state sentinels and registers the sprite
|
|
// for shutdown. ownsTileData true: the cel owns its buf copy.
|
|
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
|
|
outCels[i] = sp;
|
|
loaded++;
|
|
}
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|
|
|
|
|
|
// Load a .spc pre-compiled sprite bank (tools/spritebake output) for THIS
|
|
// platform: same as jlSpriteBankLoad, but each cel arrives already compiled
|
|
// -- the pre-emitted native blit-routine image is copied straight into a
|
|
// codegen-arena slot and sp->slot + sp->routineOffsets are stamped from the
|
|
// file, so no runtime jlSpriteCompile is needed. A cel that cannot fit the
|
|
// arena (or whose bank had nothing to compile) is left uncompiled exactly as
|
|
// jlSpriteBankLoad leaves every cel, so the interpreter fallback still draws
|
|
// it -- callers can treat this as a drop-in faster jlSpriteBankLoad.
|
|
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette) {
|
|
FILE *fp;
|
|
uint8_t header[ASSET_HEADER_BYTES];
|
|
uint8_t *imageBuf;
|
|
uint8_t widthTiles;
|
|
uint8_t heightTiles;
|
|
uint8_t shiftCount;
|
|
uint16_t cellCount;
|
|
uint32_t tileBytes;
|
|
uint16_t toLoad;
|
|
uint16_t i;
|
|
uint16_t loaded;
|
|
|
|
if (path == NULL || outCels == NULL || maxCels == 0u) {
|
|
return 0u;
|
|
}
|
|
// "JSC1" + the per-machine target byte (pre-compiled code is machine-
|
|
// specific, validated like a .tbk but against the .spc target table).
|
|
if (!openReadValidateHeader(path, SPC_MAGIC, EXPECTED_SPC_TARGET, header, outPalette, &fp)) {
|
|
return 0u;
|
|
}
|
|
widthTiles = header[SPC_OFF_WIDTH_TILES];
|
|
heightTiles = header[SPC_OFF_HEIGHT_TILES];
|
|
cellCount = (uint16_t)(header[SPC_OFF_CELL_COUNT] | (header[SPC_OFF_CELL_COUNT + 1] << 8));
|
|
shiftCount = header[SPC_OFF_SHIFT_COUNT];
|
|
// Reject wrong geometry AND a shift count that does not match this build's
|
|
// table width -- a .spc baked for the wrong target would otherwise stamp a
|
|
// mis-sized routineOffsets table and the dispatcher would read garbage.
|
|
if (widthTiles == 0u || heightTiles == 0u ||
|
|
widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H ||
|
|
shiftCount != (uint8_t)JOEY_SPRITE_SHIFT_COUNT) {
|
|
fclose(fp);
|
|
return 0u;
|
|
}
|
|
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)TILE_BYTES;
|
|
toLoad = (cellCount < maxCels) ? cellCount : maxCels;
|
|
|
|
// One reused staging buffer for each cel's routine image (bounded to
|
|
// <64 KB by the uint16 offset budget, same as the JIT). Read the image
|
|
// here BEFORE building the sprite so every fallible fread happens before
|
|
// spriteInitFields registers the sprite -- no dangling registry entry.
|
|
imageBuf = (uint8_t *)jlpBigAlloc(SPC_MAX_IMAGE_BYTES);
|
|
if (imageBuf == NULL) {
|
|
fclose(fp);
|
|
return 0u;
|
|
}
|
|
|
|
loaded = 0u;
|
|
for (i = 0; i < toLoad; i++) {
|
|
uint8_t meta[4u + (uint32_t)JOEY_SPRITE_SHIFT_COUNT * SPRITE_OP_COUNT * 2u + 2u];
|
|
uint16_t routeOff[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
|
|
uint16_t relocOffsets[JOEY_SPRITE_SHIFT_COUNT];
|
|
uint8_t *buf;
|
|
jlSpriteT *sp;
|
|
ArenaSlotT *slot;
|
|
uint32_t metaBytes;
|
|
uint32_t imageSize;
|
|
uint32_t p;
|
|
uint16_t relocCount;
|
|
uint8_t shift;
|
|
uint8_t op;
|
|
|
|
// celMeta: imageSize LE32, routineOffsets[shiftCount][3] LE16, relocCount LE16.
|
|
metaBytes = 4u + (uint32_t)shiftCount * SPRITE_OP_COUNT * 2u + 2u;
|
|
if (fread(meta, 1, (size_t)metaBytes, fp) != (size_t)metaBytes) {
|
|
break;
|
|
}
|
|
imageSize = (uint32_t)meta[0] | ((uint32_t)meta[1] << 8)
|
|
| ((uint32_t)meta[2] << 16) | ((uint32_t)meta[3] << 24);
|
|
p = 4u;
|
|
for (shift = 0; shift < shiftCount; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
routeOff[shift][op] = (uint16_t)(meta[p] | (meta[p + 1] << 8));
|
|
p += 2u;
|
|
}
|
|
}
|
|
relocCount = (uint16_t)(meta[p] | (meta[p + 1] << 8));
|
|
if (imageSize > SPC_MAX_IMAGE_BYTES || relocCount > shiftCount) {
|
|
break; // corrupt / out-of-spec
|
|
}
|
|
|
|
// Tile blob (owned by the cel; feeds dims + the interpreter/clip
|
|
// fallback + SAVE/RESTORE), then the routine image, then reloc sites.
|
|
buf = (uint8_t *)jlpBigAlloc(tileBytes);
|
|
if (buf == NULL) {
|
|
break;
|
|
}
|
|
if (fread(buf, 1, (size_t)tileBytes, fp) != (size_t)tileBytes) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
if (imageSize > 0u && fread(imageBuf, 1, (size_t)imageSize, fp) != (size_t)imageSize) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
if (relocCount > 0u) {
|
|
uint8_t relocRaw[JOEY_SPRITE_SHIFT_COUNT * 2u];
|
|
uint16_t r;
|
|
|
|
if (fread(relocRaw, 1, (size_t)relocCount * 2u, fp) != (size_t)relocCount * 2u) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
for (r = 0; r < relocCount; r++) {
|
|
relocOffsets[r] = (uint16_t)(relocRaw[r * 2u] | (relocRaw[r * 2u + 1] << 8));
|
|
}
|
|
}
|
|
|
|
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
|
|
if (sp == NULL) {
|
|
jlpBigFree(buf);
|
|
break;
|
|
}
|
|
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
|
|
|
|
// Install the pre-compiled image. On arena-full (compact-and-retry,
|
|
// like jlSpriteCompile) leave the cel uncompiled -> interpreter, no
|
|
// error: the sprite still draws, just at the old speed.
|
|
slot = NULL;
|
|
if (imageSize > 0u) {
|
|
slot = codegenArenaAlloc(imageSize);
|
|
if (slot == NULL) {
|
|
codegenArenaCompact();
|
|
slot = codegenArenaAlloc(imageSize);
|
|
}
|
|
}
|
|
if (slot != NULL) {
|
|
uint8_t *slotBase = codegenArenaSlotBase(slot);
|
|
|
|
memcpy(slotBase, imageBuf, (size_t)imageSize);
|
|
for (shift = 0; shift < shiftCount; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
sp->routineOffsets[shift][op] = routeOff[shift][op];
|
|
}
|
|
}
|
|
#if defined(JOEYLIB_PLATFORM_ATARIST)
|
|
// Re-stamp the ST shifted-DRAW thunks' absolute jmp to the shared
|
|
// walker (the one relocation any port needs). 68k big-endian, so a
|
|
// native uint32 store lays the 4 address bytes high-word-first,
|
|
// matching the baked thunk layout.
|
|
{
|
|
uint16_t r;
|
|
|
|
for (r = 0; r < relocCount; r++) {
|
|
*(uint32_t *)(slotBase + relocOffsets[r]) = (uint32_t)(uintptr_t)stSpriteDrawShiftWalker;
|
|
}
|
|
}
|
|
#else
|
|
(void)relocOffsets;
|
|
#endif
|
|
sp->slot = slot;
|
|
}
|
|
|
|
outCels[i] = sp;
|
|
loaded++;
|
|
}
|
|
jlpBigFree(imageBuf);
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|
|
|
|
|
|
// Serialize an already-compiled sprite bank to a .spc cache file -- the
|
|
// runtime twin of tools/spritebake. Call it after jlSpriteBankLoad +
|
|
// jlSpriteCompile so a later launch can jlSpriteBankLoadPrecompiled the cache
|
|
// and skip the JIT. This is the "ship the small cross-platform .spr, compile
|
|
// once, cache the (large) per-platform compiled bank" model -- ideal on the
|
|
// planar ports whose full .spc is far too big to ship on a floppy. The
|
|
// emitted routines are position-independent and routineOffsets are slot-
|
|
// relative, so the arena bytes serialize verbatim (Atari ST alone records the
|
|
// shifted-DRAW walker patch sites, which the loader re-stamps on reload).
|
|
// Cels without an arena slot (compile skipped / arena full) are written
|
|
// uncompiled and simply JIT again next run. `palette` (or NULL) is embedded
|
|
// so the cache is self-contained. Returns false if the destination is not
|
|
// writable (e.g. a write-protected disk) -- harmless; the app just keeps
|
|
// JITing every launch.
|
|
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette) {
|
|
FILE *fp;
|
|
uint8_t widthTiles;
|
|
uint8_t heightTiles;
|
|
uint32_t tileBytes;
|
|
uint16_t i;
|
|
|
|
if (path == NULL || cels == NULL || count == 0u || cels[0] == NULL) {
|
|
return false;
|
|
}
|
|
widthTiles = cels[0]->widthTiles;
|
|
heightTiles = cels[0]->heightTiles;
|
|
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)TILE_BYTES;
|
|
|
|
fp = jlDataOpen(path, "wb");
|
|
if (fp == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// Shared 44-byte header (mirrors tools/spritebake + jlSpriteBankLoadPrecompiled).
|
|
fwrite(SPC_MAGIC, 1, 4, fp);
|
|
fputc((int)EXPECTED_SPC_TARGET, fp);
|
|
fputc(palette != NULL ? 1 : 0, fp);
|
|
fputc((int)widthTiles, fp);
|
|
fputc((int)heightTiles, fp);
|
|
fputc((int)(count & 0xFFu), fp);
|
|
fputc((int)((count >> 8) & 0xFFu), fp);
|
|
fputc((int)JOEY_SPRITE_SHIFT_COUNT, fp);
|
|
fputc(0, fp); // reserved
|
|
for (i = 0; i < ASSET_PALETTE_ENTRIES; i++) {
|
|
uint16_t v = (palette != NULL) ? palette[i] : 0u;
|
|
|
|
fputc((int)(v & 0xFFu), fp);
|
|
fputc((int)((v >> 8) & 0xFFu), fp);
|
|
}
|
|
|
|
for (i = 0; i < count; i++) {
|
|
jlSpriteT *sp = cels[i];
|
|
uint16_t relocOffsets[JOEY_SPRITE_SHIFT_COUNT];
|
|
uint16_t relocCount = 0u;
|
|
uint32_t imageSize = (sp != NULL && sp->slot != NULL) ? sp->slot->size : 0u;
|
|
uint8_t shift;
|
|
uint8_t op;
|
|
uint16_t r;
|
|
|
|
#if defined(JOEYLIB_PLATFORM_ATARIST)
|
|
if (sp != NULL && sp->slot != NULL) {
|
|
for (shift = 0; shift < (uint8_t)JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
|
uint16_t d = sp->routineOffsets[shift][SPRITE_OP_DRAW];
|
|
|
|
if ((shift & 7u) != 0u && d != SPRITE_NOT_COMPILED) {
|
|
relocOffsets[relocCount] = (uint16_t)(d + SPC_ST_WALKER_JMP_OFFSET);
|
|
relocCount++;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
// celMeta: imageSize LE32, routineOffsets[shift][op] LE16, relocCount LE16.
|
|
fputc((int)(imageSize & 0xFFu), fp);
|
|
fputc((int)((imageSize >> 8) & 0xFFu), fp);
|
|
fputc((int)((imageSize >> 16) & 0xFFu), fp);
|
|
fputc((int)((imageSize >> 24) & 0xFFu), fp);
|
|
for (shift = 0; shift < (uint8_t)JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
uint16_t v = (sp != NULL) ? sp->routineOffsets[shift][op] : (uint16_t)SPRITE_NOT_COMPILED;
|
|
|
|
fputc((int)(v & 0xFFu), fp);
|
|
fputc((int)((v >> 8) & 0xFFu), fp);
|
|
}
|
|
}
|
|
fputc((int)(relocCount & 0xFFu), fp);
|
|
fputc((int)((relocCount >> 8) & 0xFFu), fp);
|
|
|
|
// tileData (raw cel, for the interpreter/clip fallback + dims).
|
|
if (sp != NULL && sp->tileData != NULL) {
|
|
fwrite(sp->tileData, 1, (size_t)tileBytes, fp);
|
|
} else {
|
|
uint32_t z;
|
|
|
|
for (z = 0; z < tileBytes; z++) {
|
|
fputc(0, fp);
|
|
}
|
|
}
|
|
// image: the compiled routines, straight from the arena slot.
|
|
if (imageSize > 0u) {
|
|
fwrite(codegenArenaSlotBase(sp->slot), 1, (size_t)imageSize, fp);
|
|
}
|
|
for (r = 0; r < relocCount; r++) {
|
|
fputc((int)(relocOffsets[r] & 0xFFu), fp);
|
|
fputc((int)((relocOffsets[r] >> 8) & 0xFFu), fp);
|
|
}
|
|
}
|
|
|
|
if (fclose(fp) != 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// 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;
|
|
size_t got;
|
|
|
|
if (outTiles == NULL || maxTiles == 0u) {
|
|
return 0u;
|
|
}
|
|
// Define the whole outValid array up front -- BEFORE the file is
|
|
// opened -- so leftover slots read false exactly as the tile.h
|
|
// contract promises, regardless of caller pre-zeroing. Clearing it
|
|
// only after the header validated left a refused, missing or
|
|
// short-header load reporting whatever an earlier load into the
|
|
// same array had marked valid (caught by scripts/check-tilebank.sh).
|
|
if (outValid != NULL) {
|
|
for (i = 0; i < maxTiles; i++) {
|
|
outValid[i] = false;
|
|
}
|
|
}
|
|
// 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. A
|
|
// NULL path needs no guard here: jlDataOpen refuses it.
|
|
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
|
|
return 0u;
|
|
}
|
|
tileCount = (uint16_t)(header[6] | (header[7] << 8));
|
|
|
|
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
|
|
|
|
// Stream the whole tile payload as ONE contiguous byte run: jlTileT
|
|
// is pure pixels[TILE_BYTES], so tiles are back-to-back in outTiles[].
|
|
// CONTRACT FIX (2026-07-12): the old batched loop treated a short
|
|
// fread as EOF (`if gotTiles < batch: break`). That is WRONG on the
|
|
// IIgs, whose libc fread does not always satisfy a large request in
|
|
// ONE call -- it returned only ~133 of Space Taxi's 256 tiles,
|
|
// truncating every tile past $84 so the title logo flip-book (chars
|
|
// $85..$88) never loaded and never animated (acceptance #3). Loop
|
|
// until the requested bytes are read or fread returns 0 (true
|
|
// EOF/error). Byte-granular, not tile-granular, so a partial final
|
|
// tile from a short read cannot desync the stream. On ports whose
|
|
// fread already fills the request (DOS/Amiga/ST) this is behaviour-
|
|
// identical: same total bytes, same `loaded` count.
|
|
{
|
|
uint8_t *payload = (uint8_t *)outTiles[0].pixels;
|
|
uint32_t wantBytes = (uint32_t)toLoad * (uint32_t)TILE_BYTES;
|
|
uint32_t doneBytes = 0u;
|
|
|
|
while (doneBytes < wantBytes) {
|
|
got = fread(payload + doneBytes, 1, (size_t)(wantBytes - doneBytes), fp);
|
|
if (got == 0u) {
|
|
break;
|
|
}
|
|
doneBytes += (uint32_t)got;
|
|
}
|
|
loaded = (uint16_t)(doneBytes / (uint32_t)TILE_BYTES);
|
|
if (outValid != NULL) {
|
|
for (i = 0; i < loaded; i++) {
|
|
outValid[i] = true;
|
|
}
|
|
}
|
|
}
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|
|
|
|
|
|
// Streaming variant of jlTileBankLoad: pastes tiles onto dst's block
|
|
// grid as they arrive instead of filling a caller buffer, so the
|
|
// caller needs NO tile array at all (contract in joey/tile.h). The
|
|
// scratch is a small static -- 8 tiles per fread keeps the read count
|
|
// low without stressing the 65816 soft stack.
|
|
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette) {
|
|
static jlTileT scratch[8];
|
|
FILE *fp;
|
|
uint8_t header[ASSET_HEADER_BYTES];
|
|
uint16_t tileCount;
|
|
uint16_t toLoad;
|
|
uint16_t loaded;
|
|
uint16_t batch;
|
|
uint16_t gotTiles;
|
|
uint16_t i;
|
|
uint8_t bx;
|
|
uint8_t by;
|
|
size_t got;
|
|
|
|
if (path == NULL || dst == NULL) {
|
|
return 0u;
|
|
}
|
|
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
|
|
return 0u;
|
|
}
|
|
tileCount = (uint16_t)(header[6] | (header[7] << 8));
|
|
toLoad = (uint16_t)(TILE_BLOCKS_PER_ROW * TILE_BLOCKS_PER_COL);
|
|
if (tileCount < toLoad) {
|
|
toLoad = tileCount;
|
|
}
|
|
|
|
loaded = 0u;
|
|
while (loaded < toLoad) {
|
|
batch = (uint16_t)(toLoad - loaded);
|
|
if (batch > (uint16_t)(sizeof(scratch) / sizeof(scratch[0]))) {
|
|
batch = (uint16_t)(sizeof(scratch) / sizeof(scratch[0]));
|
|
}
|
|
got = fread(scratch[0].pixels, 1, (size_t)batch * TILE_BYTES, fp);
|
|
gotTiles = (uint16_t)(got / TILE_BYTES);
|
|
for (i = 0; i < gotTiles; i++) {
|
|
bx = (uint8_t)((loaded + i) % TILE_BLOCKS_PER_ROW);
|
|
by = (uint8_t)((loaded + i) / TILE_BLOCKS_PER_ROW);
|
|
jlTilePaste(dst, bx, by, &scratch[i]);
|
|
}
|
|
loaded = (uint16_t)(loaded + gotTiles);
|
|
if (gotTiles < batch) {
|
|
break;
|
|
}
|
|
}
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|