// 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/.mk), from tests/host/tileBank.png: // own.tbk this port's bake: the bank that must load // refuse.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.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= pasteHash= refused=4/4 // which scripts/verify--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 #include #include #include #include // ----- 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 JL_SURFACE_COLORS_PER_PALETTE #define EXPECT_OFF_PALETTE LE16_BYTES #define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES) #define TILE_PIXELS (JL_TILE_PIXELS_PER_SIDE * JL_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 ((JL_SURFACE_WIDTH - LAMP_W) / 2) #define LAMP_Y ((JL_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 % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_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 < JL_SURFACE_HEIGHT; y++) { for (x = 0; x < JL_SURFACE_WIDTH; x++) { block = (uint16_t)((y / JL_TILE_PIXELS_PER_SIDE) * JL_TILE_BLOCKS_PER_ROW + x / JL_TILE_PIXELS_PER_SIDE); want = FILL_COLOR; if (block < tilesDrawn) { want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % JL_TILE_PIXELS_PER_SIDE) * JL_TILE_PIXELS_PER_SIDE + x % JL_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; config.noAudio = true; 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; }