joeylib2/examples/tilebank/tilebank.c
2026-10-13 14:01:46 -05:00

322 lines
12 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Tile-bank check on a real port: loads this port's bake of the host gate's
// fixture both ways, checks every pixel, and makes sure every other target's
// bake is refused. It is the on-machine half of scripts/check-tilebank.sh:
// that host gate proves the shared loader and the generic paste; this proves
// that assetbake's encoder for THIS target writes exactly the layout this
// port's native tile paste reads, and that this build refuses the others.
//
// DATA/tilebank/ is staged by tests/host/tileBankStage.py (the make rule in
// make/<port>.mk), from tests/host/tileBank.png:
// own.tbk this port's bake: the bank that must load
// refuse<n>.tbk n = 1, 2, ...: every other target's bake
// expect.bin the PNG decoded by PIL, independent of assetbake's
// encoders: tileCount LE16, palette[16] LE16 $0RGB, then
// tileCount * 64 colour indices (tiles in bank order, each
// row-major)
//
// The checks, all on the stage:
// refused each refuse<n>.tbk loads nothing through jlTileBankLoad and
// pastes nothing through jlTileBankLoadToSurface
// streamed jlTileBankLoadToSurface pastes every tile of own.tbk, the
// palette it returns is the PNG's, and every pixel of the stage
// is right: blocks below the tile count show the PNG's tiles in
// bank order, the rest keep FILL_COLOR
// pasted the same pixels again through jlTileBankLoad + jlTilePaste
// Both loads draw onto a stage cleared to the host harness's FILL_COLOR with
// the default palette and SCBs, exactly the surface whose hash the host
// harness prints, so both hashes must equal that one cross-port value.
//
// Result: one jlLog line, flushed before anything is shown,
// TILEBANK: PASS tiles=55 streamed=55 pasted=55 palette=ok streamHash=<hash> pasteHash=<hash> refused=4/4
// which scripts/verify-<port>-tilebank.sh reads (and holds the hashes against
// the host's), then the tiles in the bank's own colours over a lamp centred on
// the stage: LAMP_PASS_COLOR or LAMP_FAIL_COLOR in palette LAMP_PALETTE, with
// the rest of its rows left at FILL_COLOR. The MAME gates find the verdict
// there before they read the log. Any key exits.
//
// Portable C only: nothing here knows which port it runs on.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <joey/joey.h>
#include <joey/file.h>
// ----- Constants -----
// The staged files (tests/host/tileBankStage.py names them).
#define OWN_BANK "tilebank/own.tbk"
#define EXPECT_NAME "tilebank/expect.bin"
#define REFUSE_BANK_FORMAT "tilebank/refuse%u.tbk"
#define BANK_NAME_BYTES 32
// expect.bin layout (see the header comment).
#define LE16_BYTES 2
#define BYTE_BITS 8
#define PALETTE_COLORS SURFACE_COLORS_PER_PALETTE
#define EXPECT_OFF_PALETTE LE16_BYTES
#define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES)
#define TILE_PIXELS (TILE_PIXELS_PER_SIDE * TILE_PIXELS_PER_SIDE)
// The stage is cleared to this before every load. It MUST equal FILL_COLOR in
// tests/host/tileBankHost.c: the host's hash is of a surface cleared to it.
#define FILL_COLOR 9u
// No sprites here, so the port's default codegen cache is plenty.
#define CODEGEN_DEFAULT 0u
// The verdict lamp, centred on the stage. Its rows get their own palette so
// the lamp colours cannot collide with the bank's sixteen.
#define LAMP_W 64
#define LAMP_H 64
#define LAMP_X ((SURFACE_WIDTH - LAMP_W) / 2)
#define LAMP_Y ((SURFACE_HEIGHT - LAMP_H) / 2)
#define LAMP_PALETTE 1u
#define LAMP_PASS_COLOR 1u
#define LAMP_FAIL_COLOR 2u
#define LAMP_DARK_RGB 0x0000u
#define LAMP_PASS_RGB 0x00F0u
#define LAMP_FAIL_RGB 0x0F00u
// ----- Prototypes -----
static bool loadExpected(void);
static void pasteTiles(uint16_t count);
static bool readAll(FILE *fp, uint8_t *dst, uint32_t bytes);
static uint16_t readLe16(const uint8_t *bytes);
static bool refuseBankFound(char *name, uint16_t n);
static uint16_t refuseCheck(uint16_t *banks);
static bool runChecks(void);
static void showVerdict(bool pass);
static bool stageMatches(uint16_t tilesDrawn);
// ----- State -----
static uint16_t gBankPalette[PALETTE_COLORS];
static uint16_t gExpectCount = 0u;
static uint16_t gExpectPalette[PALETTE_COLORS];
static uint8_t *gExpectPixels = NULL;
static jlSurfaceT *gScreen = NULL;
static jlTileT *gTiles = NULL;
static bool *gValid = NULL;
// Read expect.bin, and size the tile buffers to its tile count.
static bool loadExpected(void) {
FILE *fp;
uint8_t head[EXPECT_HEADER_BYTES];
uint32_t pixelBytes;
uint16_t i;
bool ok;
fp = jlDataOpen(EXPECT_NAME, "rb");
if (fp == NULL) {
return false;
}
ok = readAll(fp, head, sizeof(head));
if (ok) {
gExpectCount = readLe16(head);
ok = gExpectCount > 0u;
}
if (ok) {
for (i = 0u; i < PALETTE_COLORS; i++) {
gExpectPalette[i] = readLe16(&head[EXPECT_OFF_PALETTE + i * LE16_BYTES]);
}
pixelBytes = (uint32_t)gExpectCount * TILE_PIXELS;
gExpectPixels = (uint8_t *)jlAlloc(pixelBytes);
gTiles = (jlTileT *)jlAlloc((uint32_t)gExpectCount * sizeof(jlTileT));
gValid = (bool *)jlAlloc((uint32_t)gExpectCount * sizeof(bool));
ok = gExpectPixels != NULL && gTiles != NULL && gValid != NULL && readAll(fp, gExpectPixels, pixelBytes);
}
fclose(fp);
return ok;
}
// Clear the stage, then paste gTiles[0..count) in the row-major block order
// jlTileBankLoadToSurface uses.
static void pasteTiles(uint16_t count) {
uint16_t i;
jlSurfaceClear(gScreen, FILL_COLOR);
for (i = 0u; i < count; i++) {
jlTilePaste(gScreen, (uint8_t)(i % TILE_BLOCKS_PER_ROW), (uint8_t)(i / TILE_BLOCKS_PER_ROW), &gTiles[i]);
}
}
// fread until done: one fread need not fill a large request (the IIgs libc
// stops short), so a short read is only an error when nothing more comes.
static bool readAll(FILE *fp, uint8_t *dst, uint32_t bytes) {
uint32_t done;
size_t got;
done = 0u;
while (done < bytes) {
got = fread(dst + done, 1, (size_t)(bytes - done), fp);
if (got == 0u) {
return false;
}
done += (uint32_t)got;
}
return true;
}
static uint16_t readLe16(const uint8_t *bytes) {
return (uint16_t)(bytes[0] | (bytes[1] << BYTE_BITS));
}
// Put refuse bank n's name in name (BANK_NAME_BYTES long); true when that
// bank was staged.
static bool refuseBankFound(char *name, uint16_t n) {
FILE *fp;
snprintf(name, BANK_NAME_BYTES, REFUSE_BANK_FORMAT, (unsigned)n);
fp = jlDataOpen(name, "rb");
if (fp == NULL) {
return false;
}
fclose(fp);
return true;
}
// Every staged refuse bank, both ways: jlTileBankLoad must load nothing and
// mark nothing valid, jlTileBankLoadToSurface must paste nothing. Returns how
// many were refused both ways; *banks gets how many were staged.
static uint16_t refuseCheck(uint16_t *banks) {
char name[BANK_NAME_BYTES];
uint32_t fillHash;
uint16_t refused;
uint16_t loaded;
uint16_t streamed;
jlSurfaceClear(gScreen, FILL_COLOR);
fillHash = jlSurfaceHash(gScreen);
refused = 0u;
*banks = 0u;
while (refuseBankFound(name, (uint16_t)(*banks + 1u))) {
(*banks)++;
jlSurfaceClear(gScreen, FILL_COLOR);
loaded = jlTileBankLoad(name, gTiles, gExpectCount, gValid, NULL);
streamed = jlTileBankLoadToSurface(name, gScreen, NULL);
if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(gScreen) == fillHash) {
refused++;
}
}
return refused;
}
// The checks in the header comment, reported as one log line. The paste runs
// last so its tiles are what the verdict screen shows.
static bool runChecks(void) {
uint16_t banks;
uint16_t refused;
uint16_t streamed;
uint16_t pasted;
uint32_t streamHash;
uint32_t pasteHash;
bool paletteOk;
bool streamOk;
bool pasteOk;
bool pass;
refused = refuseCheck(&banks);
jlSurfaceClear(gScreen, FILL_COLOR);
streamed = jlTileBankLoadToSurface(OWN_BANK, gScreen, gBankPalette);
streamOk = streamed == gExpectCount && stageMatches(streamed);
streamHash = jlSurfaceHash(gScreen);
paletteOk = memcmp(gBankPalette, gExpectPalette, sizeof(gBankPalette)) == 0;
pasted = jlTileBankLoad(OWN_BANK, gTiles, gExpectCount, gValid, NULL);
pasteTiles(pasted);
pasteOk = pasted == gExpectCount && stageMatches(pasted);
pasteHash = jlSurfaceHash(gScreen);
pass = streamOk && pasteOk && paletteOk && streamHash == pasteHash && banks > 0u && refused == banks;
jlLogF("TILEBANK: %s tiles=%u streamed=%u pasted=%u palette=%s streamHash=%08lX pasteHash=%08lX refused=%u/%u", pass ? "PASS" : "FAIL", (unsigned)gExpectCount, (unsigned)streamed, (unsigned)pasted, paletteOk ? "ok" : "wrong", (unsigned long)streamHash, (unsigned long)pasteHash, (unsigned)refused, (unsigned)banks);
return pass;
}
// Show the bank's tiles in its own colours, and the lamp.
static void showVerdict(bool pass) {
uint16_t colors[PALETTE_COLORS];
uint16_t i;
for (i = 0u; i < PALETTE_COLORS; i++) {
colors[i] = LAMP_DARK_RGB;
}
colors[LAMP_PASS_COLOR] = LAMP_PASS_RGB;
colors[LAMP_FAIL_COLOR] = LAMP_FAIL_RGB;
jlPaletteSet(gScreen, 0u, gBankPalette);
jlPaletteSet(gScreen, LAMP_PALETTE, colors);
jlScbSetRange(gScreen, LAMP_Y, LAMP_Y + LAMP_H - 1, LAMP_PALETTE);
jlFillRect(gScreen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, pass ? LAMP_PASS_COLOR : LAMP_FAIL_COLOR);
jlStagePresent();
}
// Every pixel of the stage: blocks below tilesDrawn must show the PNG's tile
// of that number, every other block FILL_COLOR. tilesDrawn never exceeds
// gExpectCount (the callers check the count first).
static bool stageMatches(uint16_t tilesDrawn) {
int16_t x;
int16_t y;
uint16_t block;
uint8_t want;
for (y = 0; y < SURFACE_HEIGHT; y++) {
for (x = 0; x < SURFACE_WIDTH; x++) {
block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE);
want = FILL_COLOR;
if (block < tilesDrawn) {
want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % TILE_PIXELS_PER_SIDE) * TILE_PIXELS_PER_SIDE + x % TILE_PIXELS_PER_SIDE];
}
if (jlSamplePixel(gScreen, x, y) != want) {
return false;
}
}
}
return true;
}
int main(void) {
jlConfigT config;
bool pass;
config.codegenBytes = CODEGEN_DEFAULT;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return EXIT_FAILURE;
}
gScreen = jlStageGet();
pass = loadExpected();
if (pass) {
pass = runChecks();
} else {
jlLog("TILEBANK: FAIL DATA/" EXPECT_NAME " is missing or short, or memory ran out");
}
// Flushed BEFORE the verdict shows: a gate that sees the lamp can read
// the log at once (on the IIgs the flush is what writes the file).
jlLogFlush();
showVerdict(pass);
jlWaitForAnyKey();
jlFree(gValid);
jlFree(gTiles);
jlFree(gExpectPixels);
jlShutdown();
return pass ? EXIT_SUCCESS : EXIT_FAILURE;
}