// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // tileLayerHost.c - Host-cc harness for tile layers (src/core/tileLayer.c // and the generic ring, src/generic/genericTileLayer.c). Every draw is // checked pixel for pixel against the map itself -- the tile a band pixel // falls in, worked out from the origin with no ring -- so the ring's // bookkeeping (which slots a move uncovers, wraps at the map's and the // ring's edges) cannot hide behind a second implementation. Runs of moves // of every size (a pixel, a tile, a view, a jump across the map's seam, // both ways), maps smaller than the view, random bands, and cells changed // with jlTileLayerSetTile, in and out of view. Also: rows outside the band // keep their pixels, the band's rows are marked dirty full width, a // JL_TILE_MAP_SKIP cell draws color 0, and jlTileLayerCreate refuses bad // arguments. // // Built by scripts/check-tilelayer.sh with -DJOEYLIB_PLATFORM_BLANK. #include #include #include #include #include #include "joey/joey.h" #include "surfaceInternal.h" #define TILE_COUNT 64u #define RUNS 300u #define STEPS_PER_RUN 40u #define OUTSIDE_COLOR 9u #define MAX_MAP_SIDE 300u static jlTileT gTiles[TILE_COUNT]; static uint8_t gMap[MAX_MAP_SIDE * MAX_MAP_SIDE]; static uint32_t gChecks; static bool checkBand(jlSurfaceT *stage, uint8_t viewBy, uint8_t viewHeight, uint16_t mapW, uint16_t mapH, uint32_t x, uint32_t y, const char *what); static bool checkCreateRefusals(void); static uint8_t expectedPixel(uint16_t mapW, uint16_t mapH, uint32_t mx, uint32_t my); static uint32_t nextRand(void); static bool runOne(jlSurfaceT *stage, uint32_t run); static uint16_t stepFor(uint32_t mapPx); static uint32_t gSeed = 12345u; static bool checkBand(jlSurfaceT *stage, uint8_t viewBy, uint8_t viewHeight, uint16_t mapW, uint16_t mapH, uint32_t x, uint32_t y, const char *what) { int16_t px; int16_t py; int16_t top; int16_t bottom; uint32_t mapPxW; uint32_t mapPxH; uint8_t got; uint8_t want; mapPxW = (uint32_t)mapW * 8u; mapPxH = (uint32_t)mapH * 8u; top = (int16_t)(viewBy * 8); bottom = (int16_t)((viewBy + viewHeight) * 8); gChecks++; for (py = 0; py < JL_SURFACE_HEIGHT; py++) { bool inBand = (py >= top && py < bottom); if (inBand && (jlgStageMinWord[py] != 0u || jlgStageMaxWord[py] != STAGE_DIRTY_FULL_MAX)) { printf(" FAIL %s: band row %d not marked dirty full width (%u..%u)\n", what, py, jlgStageMinWord[py], jlgStageMaxWord[py]); return false; } for (px = 0; px < JL_SURFACE_WIDTH; px++) { got = jlSamplePixel(stage, px, py); want = inBand ? expectedPixel(mapW, mapH, (x + (uint32_t)px) % mapPxW, (y + (uint32_t)(py - top)) % mapPxH) : OUTSIDE_COLOR; if (got != want) { printf(" FAIL %s: pixel (%d,%d) is %u, want %u (origin %lu,%lu map %ux%u band %u+%u)\n", what, px, py, got, want, (unsigned long)x, (unsigned long)y, mapW, mapH, viewBy, viewHeight); return false; } } } return true; } static bool checkCreateRefusals(void) { bool ok = true; ok = ok && jlTileLayerCreate(NULL, gMap, 10u, 10u, 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, NULL, 10u, 10u, 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, 0u, 10u, 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 0u, 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, (uint16_t)(JL_TILE_LAYER_MAX_SIDE + 1u), 10u, 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, (uint16_t)(JL_TILE_LAYER_MAX_SIDE + 1u), 0u, 25u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 10u, 0u, 0u) == NULL; ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 10u, 1u, 25u) == NULL; jlTileLayerDestroy(NULL); printf(" create refusals: %s\n", ok ? "PASS" : "FAIL"); return ok; } // The color of map pixel (mx, my), straight from the map and the tiles. static uint8_t expectedPixel(uint16_t mapW, uint16_t mapH, uint32_t mx, uint32_t my) { uint8_t idx; uint8_t b; (void)mapH; idx = gMap[(my >> 3) * mapW + (mx >> 3)]; if (idx == JL_TILE_MAP_SKIP) { return 0u; } b = gTiles[idx].pixels[(my & 7u) * JL_TILE_BYTES_PER_ROW + ((mx & 7u) >> 1)]; return (uint8_t)(((mx & 1u) == 0u) ? (b >> 4) : (b & 0x0Fu)); } static uint32_t nextRand(void) { gSeed = gSeed * 1103515245u + 12345u; return (gSeed >> 8) & 0xFFFFFFu; } static bool runOne(jlSurfaceT *stage, uint32_t run) { jlTileLayerT *layer; uint16_t mapW; uint16_t mapH; uint8_t viewBy; uint8_t viewHeight; uint32_t x; uint32_t y; uint32_t i; uint32_t step; char what[64]; // Every fifth run a map smaller than the view, the rest up to MAX_MAP_SIDE. if (run % 5u == 0u) { mapW = (uint16_t)(1u + nextRand() % 12u); mapH = (uint16_t)(1u + nextRand() % 8u); } else { mapW = (uint16_t)(1u + nextRand() % MAX_MAP_SIDE); mapH = (uint16_t)(1u + nextRand() % MAX_MAP_SIDE); } for (i = 0u; i < (uint32_t)mapW * mapH; i++) { gMap[i] = (nextRand() % 17u == 0u) ? JL_TILE_MAP_SKIP : (uint8_t)(nextRand() % TILE_COUNT); } viewHeight = (uint8_t)((run % 3u == 0u) ? JL_TILE_BLOCKS_PER_COL : 1u + nextRand() % JL_TILE_BLOCKS_PER_COL); viewBy = (uint8_t)(nextRand() % (uint32_t)(JL_TILE_BLOCKS_PER_COL - viewHeight + 1u)); layer = jlTileLayerCreate(gTiles, gMap, mapW, mapH, viewBy, viewHeight); if (layer == NULL) { printf(" FAIL run %lu: create %ux%u band %u+%u\n", (unsigned long)run, mapW, mapH, viewBy, viewHeight); return false; } jlFillRect(stage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, OUTSIDE_COLOR); x = nextRand() % ((uint32_t)mapW * 8u); y = nextRand() % ((uint32_t)mapH * 8u); for (step = 0u; step < STEPS_PER_RUN; step++) { uint32_t kind = nextRand() % 10u; if (kind < 7u) { // A move: dx, dy each of stepFor's sizes, either way. uint32_t pxW = (uint32_t)mapW * 8u; uint32_t pxH = (uint32_t)mapH * 8u; x = (x + ((nextRand() & 1u) ? stepFor(pxW) : pxW - stepFor(pxW) % pxW)) % pxW; y = (y + ((nextRand() & 1u) ? stepFor(pxH) : pxH - stepFor(pxH) % pxH)) % pxH; } else if (kind < 9u) { // A cell change, in view half the time. uint16_t tx = (uint16_t)((nextRand() & 1u) ? ((x >> 3) + nextRand() % 41u) % mapW : nextRand() % mapW); uint16_t ty = (uint16_t)((nextRand() & 1u) ? ((y >> 3) + nextRand() % 26u) % mapH : nextRand() % mapH); jlTileLayerSetTile(layer, tx, ty, (nextRand() % 9u == 0u) ? JL_TILE_MAP_SKIP : (uint8_t)(nextRand() % TILE_COUNT)); } // jlStagePresent clears the dirty marks, so the check sees only the // draw's. jlStagePresent(); // Sometimes an origin past the map's size: the layer takes it modulo. if (nextRand() % 6u == 0u) { jlTileLayerDraw(stage, layer, (uint16_t)(x + (uint32_t)mapW * 8u * (nextRand() % 2u)), (uint16_t)(y + (uint32_t)mapH * 8u * (nextRand() % 2u))); } else { jlTileLayerDraw(stage, layer, (uint16_t)x, (uint16_t)y); } snprintf(what, sizeof(what), "run %lu step %lu", (unsigned long)run, (unsigned long)step); if (!checkBand(stage, viewBy, viewHeight, mapW, mapH, x, y, what)) { jlTileLayerDestroy(layer); return false; } } jlTileLayerDestroy(layer); return true; } // A move size: a pixel or a few, about a tile, about a view, or anywhere. static uint16_t stepFor(uint32_t mapPx) { uint32_t pick = nextRand() % 8u; uint32_t s; if (pick < 3u) { s = nextRand() % 4u; } else if (pick < 5u) { s = 6u + nextRand() % 12u; } else if (pick < 7u) { s = 300u + nextRand() % 60u; } else { s = nextRand(); } return (uint16_t)(s % mapPx); } int main(void) { jlConfigT config; jlSurfaceT *stage; uint32_t i; uint32_t run; bool ok; config.codegenBytes = 0u; config.noAudio = true; if (!jlInit(&config)) { printf("tileLayerHost: FAIL -- jlInit: %s\n", jlLastError()); return 1; } stage = jlStageGet(); for (i = 0u; i < TILE_COUNT; i++) { uint32_t b; for (b = 0u; b < JL_TILE_BYTES; b++) { gTiles[i].pixels[b] = (uint8_t)nextRand(); } } printf("tileLayerHost: layers against the map, pixel for pixel\n"); ok = checkCreateRefusals(); for (run = 0u; run < RUNS && ok; run++) { ok = runOne(stage, run); } printf(" %lu draws checked over %lu runs: %s\n", (unsigned long)gChecks, (unsigned long)run, ok ? "PASS" : "FAIL"); jlShutdown(); printf("tileLayerHost: %s\n", ok ? "PASS" : "FAIL"); return ok ? 0 : 1; }