joeylib2/tests/host/indexedHost.c

511 lines
18 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// indexedHost.c - Host-cc harness for jlSurfacePasteIndexed and
// jlSurfaceSnapIndexed (src/core/surface.c clipping, the port's rectangle
// writer and reader behind it). Each case is checked against a reference
// built one pixel at a time with jlDrawPixel and jlSamplePixel:
// paste the surface after a paste equals a copy of the same background
// with every on-surface pixel of the rectangle plotted by hand
// (high nibbles in the source ignored, doubleWide pairs, a
// doubleWide byte cut by the left or right edge showing its one
// on-surface pixel)
// snap every byte of the rectangle equals jlSamplePixel of its column
// (0 off the surface), and the bytes between w and the stride are
// left alone
// Edge cases (odd columns, both edges, rows past both ends, w 65535, the
// int16_t limits) run first, then seeded random rectangles.
//
// The planar ports' paste and snap (src/m68k/planarIndexed.c, plain C) run
// here too, on host buffers in both of their layouts -- four separate
// planes (Amiga, X68000) and the ST's interleaved plane words -- given
// on-surface rectangles as the core hands them over, and checked against
// the same kind of pixel-by-pixel reference.
//
// Built by scripts/check-indexed.sh with -DJOEYLIB_PLATFORM_BLANK.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "joey/joey.h"
#include "surfaceInternal.h"
#define SRC_BYTES 81920u
#define RANDOM_CASES 600u
#define RANDOM_SEED 0x1D2C3B4Au
#define RANDOM_X_SPAN 700
#define RANDOM_X_LOW (-360)
#define RANDOM_Y_SPAN 440
#define RANDOM_Y_LOW (-220)
#define RANDOM_W_MAX 200u
#define RANDOM_H_MAX 220u
#define RANDOM_PAD_MAX 5u
#define CANARY 0xA5u
#define BACK_X_MUL 7
#define BACK_Y_MUL 3
#define PLANAR_PLANES 4u
#define PLANAR_ROW_BYTES (JL_SURFACE_WIDTH / 8)
#define PLANAR_CASES 500u
#define PLANAR_H_MAX 24u
#define ST_ROW_BYTES 160u
#define ST_GROUP_BYTES 8u
typedef struct LayoutT {
const char *name;
uint8_t *planes[PLANAR_PLANES];
uint16_t rowBytes;
uint16_t pairStep;
} LayoutT;
typedef struct CaseT {
int16_t x;
int16_t y;
uint16_t w;
uint16_t h;
uint16_t pad;
bool doubleWide;
} CaseT;
static const CaseT kEdgeCases[] = {
{ 0, 0, 160u, 200u, 0u, true },
{ 0, 0, 320u, 200u, 0u, false },
{ 1, 3, 17u, 5u, 2u, false },
{ 1, 3, 16u, 5u, 0u, false },
{ 1, 3, 17u, 5u, 1u, true },
{ -1, 10, 20u, 4u, 0u, true },
{ -1, 10, 20u, 4u, 3u, false },
{ -2, 10, 20u, 4u, 0u, true },
{ -3, 10, 2u, 4u, 0u, true },
{ -3, 10, 1u, 4u, 0u, true },
{ 319, 50, 8u, 3u, 0u, true },
{ 318, 50, 8u, 3u, 0u, true },
{ 317, 50, 8u, 3u, 0u, true },
{ 319, 50, 8u, 3u, 0u, false },
{ 320, 50, 8u, 3u, 0u, true },
{ -9, -5, 170u, 210u, 4u, true },
{ -9, -5, 340u, 210u, 4u, false },
{ 40, 198, 10u, 10u, 0u, false },
{ 40, -10, 10u, 10u, 0u, true },
{ 40, -11, 10u, 10u, 0u, true },
{ 40, 200, 10u, 10u, 0u, true },
{ 5, 5, 0u, 10u, 2u, true },
{ 5, 5, 10u, 0u, 0u, true },
{ -301, 7, 65535u, 1u, 0u, true },
{ -301, 7, 65535u, 1u, 0u, false },
{-32768, -32768, 9u, 2u, 0u, true },
{ 32767, 32767, 9u, 2u, 0u, false }
};
#define EDGE_CASE_COUNT ((uint16_t)(sizeof(kEdgeCases) / sizeof(kEdgeCases[0])))
static uint8_t gSrc[SRC_BYTES];
static uint8_t gSnap[SRC_BYTES];
static uint32_t gRandom = RANDOM_SEED;
static uint8_t gSeparate[PLANAR_PLANES][PLANAR_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gInterleaved[ST_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gPlanarBack[PLANAR_PLANES][ST_ROW_BYTES * JL_SURFACE_HEIGHT];
static uint8_t gRef[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH];
static bool check(const char *name, bool ok, const char *detail);
static bool comparePaste(const jlSurfaceT *got, const jlSurfaceT *want, const CaseT *c, char *detail, size_t size);
static bool compareSnap(const jlSurfaceT *back, const CaseT *c, char *detail, size_t size);
static void fillBackground(jlSurfaceT *s);
static uint32_t nextRandom(void);
static void pasteReference(jlSurfaceT *s, const CaseT *c, uint16_t stride);
static uint8_t planarGet(const LayoutT *l, uint16_t x, uint16_t y);
static uint8_t *planarByte(const LayoutT *l, uint16_t p, uint16_t x, uint16_t y);
static bool planarCase(LayoutT *l, const CaseT *c, char *detail, size_t size);
static void planarSet(const LayoutT *l, uint16_t x, uint16_t y, uint8_t color);
static void planarStore(const LayoutT *l, bool save);
static bool runCase(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, const CaseT *c, char *detail, size_t size);
static bool runCases(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, bool random);
static bool runPlanar(LayoutT *l);
static uint8_t sampleWide(const jlSurfaceT *s, int32_t x, int32_t y);
static bool check(const char *name, bool ok, const char *detail) {
printf(" %s: %s%s%s\n", name, ok ? "PASS" : "FAIL", detail[0] != '\0' ? " -- " : "", detail);
return ok;
}
static bool comparePaste(const jlSurfaceT *got, const jlSurfaceT *want, const CaseT *c, char *detail, size_t size) {
int16_t x;
int16_t y;
uint8_t a;
uint8_t b;
if (jlSurfaceHash(got) == jlSurfaceHash(want)) {
return true;
}
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
a = jlSamplePixel(got, x, y);
b = jlSamplePixel(want, x, y);
if (a != b) {
snprintf(detail, size, "paste at (%d,%d) w %u h %u%s: pixel (%d,%d) is %u, want %u", c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", x, y, a, b);
return false;
}
}
}
snprintf(detail, size, "paste at (%d,%d) w %u h %u: hashes differ, pixels equal", c->x, c->y, c->w, c->h);
return false;
}
static bool compareSnap(const jlSurfaceT *back, const CaseT *c, char *detail, size_t size) {
const uint8_t *row;
uint32_t stride;
uint32_t r;
uint32_t i;
uint8_t want;
stride = (uint32_t)c->w + c->pad;
for (r = 0; r < c->h; r++) {
row = gSnap + r * stride;
for (i = 0; i < c->w; i++) {
want = sampleWide(back, (int32_t)c->x + (int32_t)(i << (c->doubleWide ? 1u : 0u)), (int32_t)c->y + (int32_t)r);
if (row[i] != want) {
snprintf(detail, size, "snap at (%d,%d) w %u h %u%s: byte %lu of row %lu is %u, want %u", c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", (unsigned long)i, (unsigned long)r, row[i], want);
return false;
}
}
for (; i < stride; i++) {
if (row[i] != CANARY) {
snprintf(detail, size, "snap at (%d,%d) w %u h %u: wrote past w at byte %lu of row %lu", c->x, c->y, c->w, c->h, (unsigned long)i, (unsigned long)r);
return false;
}
}
}
return true;
}
static void fillBackground(jlSurfaceT *s) {
int16_t x;
int16_t y;
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
jlDrawPixel(s, x, y, (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F));
}
}
}
static uint32_t nextRandom(void) {
gRandom ^= gRandom << 13;
gRandom ^= gRandom >> 17;
gRandom ^= gRandom << 5;
return gRandom;
}
static void pasteReference(jlSurfaceT *s, const CaseT *c, uint16_t stride) {
uint32_t r;
uint32_t i;
uint32_t k;
uint32_t span;
int32_t px;
int32_t py;
uint8_t color;
span = c->doubleWide ? 2u : 1u;
for (r = 0; r < c->h; r++) {
py = (int32_t)c->y + (int32_t)r;
if (py < 0 || py >= JL_SURFACE_HEIGHT) {
continue;
}
for (i = 0; i < c->w; i++) {
color = (uint8_t)(gSrc[r * stride + i] & 0x0Fu);
for (k = 0; k < span; k++) {
px = (int32_t)c->x + (int32_t)(i * span + k);
if (px >= 0 && px < JL_SURFACE_WIDTH) {
jlDrawPixel(s, (int16_t)px, (int16_t)py, color);
}
}
}
}
}
static uint8_t planarGet(const LayoutT *l, uint16_t x, uint16_t y) {
uint16_t p;
uint8_t bit;
uint8_t color;
bit = (uint8_t)(0x80u >> (x & 7u));
color = 0u;
for (p = 0; p < PLANAR_PLANES; p++) {
if ((*planarByte(l, p, x, y) & bit) != 0u) {
color = (uint8_t)(color | (1u << p));
}
}
return color;
}
static uint8_t *planarByte(const LayoutT *l, uint16_t p, uint16_t x, uint16_t y) {
uint16_t c;
c = (uint16_t)(x >> 3);
return l->planes[p] + (uint32_t)y * l->rowBytes + (uint32_t)(c >> 1) * l->pairStep + (c & 1u);
}
// One on-surface rectangle: pasted on the planes and, a pixel at a time, on
// gRef, then compared everywhere; snapped back from the planes and compared
// with gRef's pixels at the rectangle's columns.
static bool planarCase(LayoutT *l, const CaseT *c, char *detail, size_t size) {
uint32_t bytes;
uint32_t i;
uint16_t stride;
uint16_t r;
uint16_t x;
uint16_t y;
uint16_t span;
uint16_t k;
uint8_t got;
stride = (uint16_t)(c->w + c->pad);
bytes = (uint32_t)stride * c->h;
span = c->doubleWide ? 2u : 1u;
for (i = 0; i < bytes; i++) {
gSrc[i] = (uint8_t)nextRandom();
}
planarStore(l, false);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
gRef[y][x] = (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F);
}
}
jlPlanarPasteIndexed(l->planes, l->rowBytes, l->pairStep, (uint16_t)c->x, (uint16_t)c->y, c->w, c->h, gSrc, stride, c->doubleWide);
for (r = 0; r < c->h; r++) {
for (i = 0; i < c->w; i++) {
for (k = 0; k < span; k++) {
gRef[c->y + r][c->x + i * span + k] = (uint8_t)(gSrc[r * stride + i] & 0x0Fu);
}
}
}
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
got = planarGet(l, x, y);
if (got != gRef[y][x]) {
snprintf(detail, size, "%s paste at (%d,%d) w %u h %u%s: pixel (%u,%u) is %u, want %u", l->name, c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", x, y, got, gRef[y][x]);
return false;
}
}
}
memset(gSnap, CANARY, bytes);
jlPlanarSnapIndexed((const uint8_t *const *)l->planes, l->rowBytes, l->pairStep, (uint16_t)c->x, (uint16_t)c->y, c->w, c->h, gSnap, stride, c->doubleWide);
for (r = 0; r < c->h; r++) {
for (i = 0; i < stride; i++) {
got = gSnap[r * stride + i];
if (i < c->w ? got != gRef[c->y + r][c->x + i * span] : got != CANARY) {
snprintf(detail, size, "%s snap at (%d,%d) w %u h %u%s: byte %lu of row %u is %u", l->name, c->x, c->y, c->w, c->h, c->doubleWide ? " doubleWide" : "", (unsigned long)i, r, got);
return false;
}
}
}
return true;
}
static void planarSet(const LayoutT *l, uint16_t x, uint16_t y, uint8_t color) {
uint8_t *b;
uint16_t p;
uint8_t bit;
bit = (uint8_t)(0x80u >> (x & 7u));
for (p = 0; p < PLANAR_PLANES; p++) {
b = planarByte(l, p, x, y);
if (((color >> p) & 1u) != 0u) {
*b = (uint8_t)(*b | bit);
} else {
*b = (uint8_t)(*b & ~bit);
}
}
}
// Save the layout's background to gPlanarBack, or put it back.
static void planarStore(const LayoutT *l, bool save) {
uint8_t *buffer;
size_t bytes;
uint16_t p;
if (l->pairStep == ST_GROUP_BYTES) {
buffer = gInterleaved;
bytes = sizeof(gInterleaved);
if (save) {
memcpy(gPlanarBack[0], buffer, bytes);
} else {
memcpy(buffer, gPlanarBack[0], bytes);
}
return;
}
for (p = 0; p < PLANAR_PLANES; p++) {
bytes = sizeof(gSeparate[p]);
if (save) {
memcpy(gPlanarBack[p], gSeparate[p], bytes);
} else {
memcpy(gSeparate[p], gPlanarBack[p], bytes);
}
}
}
static bool runCase(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, const CaseT *c, char *detail, size_t size) {
uint32_t bytes;
uint32_t i;
uint16_t stride;
stride = (uint16_t)(c->w + c->pad);
bytes = (uint32_t)stride * c->h;
for (i = 0; i < bytes; i++) {
gSrc[i] = (uint8_t)nextRandom();
}
jlSurfaceCopy(stage, back);
jlSurfaceCopy(ref, back);
jlSurfacePasteIndexed(stage, c->x, c->y, c->w, c->h, gSrc, stride, c->doubleWide);
pasteReference(ref, c, stride);
if (!comparePaste(stage, ref, c, detail, size)) {
return false;
}
memset(gSnap, CANARY, bytes);
jlSurfaceSnapIndexed(back, c->x, c->y, c->w, c->h, gSnap, stride, c->doubleWide);
return compareSnap(back, c, detail, size);
}
static bool runCases(jlSurfaceT *stage, jlSurfaceT *ref, const jlSurfaceT *back, bool random) {
CaseT c;
char detail[200];
uint16_t count;
uint16_t n;
detail[0] = '\0';
count = random ? (uint16_t)RANDOM_CASES : EDGE_CASE_COUNT;
for (n = 0; n < count; n++) {
if (random) {
c.x = (int16_t)(RANDOM_X_LOW + (int32_t)(nextRandom() % RANDOM_X_SPAN));
c.y = (int16_t)(RANDOM_Y_LOW + (int32_t)(nextRandom() % RANDOM_Y_SPAN));
c.w = (uint16_t)(nextRandom() % (RANDOM_W_MAX + 1u));
c.h = (uint16_t)(nextRandom() % (RANDOM_H_MAX + 1u));
c.pad = (uint16_t)(nextRandom() % (RANDOM_PAD_MAX + 1u));
c.doubleWide = (nextRandom() & 1u) != 0u;
} else {
c = kEdgeCases[n];
}
if (!runCase(stage, ref, back, &c, detail, sizeof(detail))) {
break;
}
}
if (n == count) {
snprintf(detail, sizeof(detail), "%u rectangles", count);
}
return check(random ? "random rectangles" : "edge rectangles", n == count, detail);
}
static bool runPlanar(LayoutT *l) {
CaseT c;
char detail[200];
char name[48];
uint16_t x;
uint16_t y;
uint16_t n;
uint16_t maxW;
detail[0] = '\0';
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
planarSet(l, x, y, (uint8_t)((x * BACK_X_MUL + y * BACK_Y_MUL) & 0x0F));
}
}
planarStore(l, true);
for (n = 0; n < PLANAR_CASES; n++) {
c.doubleWide = (nextRandom() & 1u) != 0u;
c.x = (int16_t)(nextRandom() % JL_SURFACE_WIDTH);
if (c.doubleWide && c.x == JL_SURFACE_WIDTH - 1) {
c.x--;
}
maxW = (uint16_t)((JL_SURFACE_WIDTH - c.x) >> (c.doubleWide ? 1u : 0u));
// Mostly narrow (the edge columns), now and then the whole span left.
c.w = (uint16_t)(1u + nextRandom() % ((n & 3u) == 0u ? maxW : (maxW < 20u ? maxW : 20u)));
c.y = (int16_t)(nextRandom() % JL_SURFACE_HEIGHT);
c.h = (uint16_t)(1u + nextRandom() % PLANAR_H_MAX);
if (c.y + c.h > JL_SURFACE_HEIGHT) {
c.h = (uint16_t)(JL_SURFACE_HEIGHT - c.y);
}
c.pad = (uint16_t)(nextRandom() % (RANDOM_PAD_MAX + 1u));
if (!planarCase(l, &c, detail, sizeof(detail))) {
break;
}
}
if (n == PLANAR_CASES) {
snprintf(detail, sizeof(detail), "%u rectangles", n);
}
snprintf(name, sizeof(name), "planar %s", l->name);
return check(name, n == PLANAR_CASES, detail);
}
static uint8_t sampleWide(const jlSurfaceT *s, int32_t x, int32_t y) {
if (x < 0 || x >= JL_SURFACE_WIDTH || y < 0 || y >= JL_SURFACE_HEIGHT) {
return 0u;
}
return jlSamplePixel(s, (int16_t)x, (int16_t)y);
}
int main(void) {
LayoutT separate;
LayoutT interleaved;
jlConfigT config;
jlSurfaceT *stage;
jlSurfaceT *ref;
jlSurfaceT *back;
uint16_t p;
bool ok;
config.codegenBytes = 0u;
config.noAudio = true;
if (!jlInit(&config)) {
printf("indexedHost: FAIL -- jlInit: %s\n", jlLastError());
return 1;
}
stage = jlStageGet();
ref = jlSurfaceCreate();
back = jlSurfaceCreate();
if (ref == NULL || back == NULL) {
printf("indexedHost: FAIL -- jlSurfaceCreate: %s\n", jlLastError());
jlShutdown();
return 1;
}
fillBackground(back);
printf("indexedHost: indexed paste and snap, chunky and planar, against pixel-by-pixel references\n");
ok = true;
ok &= runCases(stage, ref, back, false);
ok &= runCases(stage, ref, back, true);
separate.name = "separate planes";
separate.rowBytes = PLANAR_ROW_BYTES;
separate.pairStep = PLANAR_SEPARATE_PAIR_STEP;
interleaved.name = "interleaved planes";
interleaved.rowBytes = ST_ROW_BYTES;
interleaved.pairStep = ST_GROUP_BYTES;
for (p = 0; p < PLANAR_PLANES; p++) {
separate.planes[p] = gSeparate[p];
interleaved.planes[p] = gInterleaved + p * sizeof(uint16_t);
}
ok &= runPlanar(&separate);
ok &= runPlanar(&interleaved);
jlSurfaceDestroy(back);
jlSurfaceDestroy(ref);
jlShutdown();
printf("indexedHost: %s\n", ok ? "PASS" : "FAIL");
return ok ? 0 : 1;
}