joeylib2/tests/host/tileLayerHost.c
2026-10-19 15:46:47 -05:00

240 lines
9 KiB
C

// 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 <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}