joeylib2/tests/host/dirtyMarkHost.c

379 lines
14 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// dirtyMarkHost.c - randomized check of the stage's dirty-band writers
// (src/core/surface.c, src/m68k/stageBands.s) against the plain per-row
// widen they must match byte for byte.
//
// jlSurfaceMarkDirtyRows takes rows four at a time and reuses a group's result
// for the next group that held the same bytes, so its answer depends on what
// the rows held: a mark of fresh rows, of rows an earlier rect marked, and of
// rows every earlier mark left different all take different paths. Each
// scenario seeds the bands one of several ways -- clean, per-row random, runs
// of equal bands, every row full, four rows' bands repeating -- then applies a
// few random marks of every height and phase, comparing all 400 band bytes
// with a reference after each.
// Also checks the whole-stage fills (jlStageDirtyClearAll, jlSurfaceMarkDirtyAll),
// jlFillRect's mark (the ST's is asm, src/atarist/lineSpan.s), and jlTileFill's
// whole-tile mark (src/core/tile.c), which takes clean rows and rows already
// holding the tile's band a long at a time.
//
// Built for the host by scripts/check-dirtymark.sh (the C marker), and for the
// Atari ST by scripts/verify-atarist-dirtymark.sh, which runs it under Hatari
// against the 68000 asm the Amiga and X68000 share. -DDIRTY_MARK_SCENARIOS
// sets the count and -DDIRTY_MARK_LOG a file to write the result line to.
// Exits nonzero on any mismatch.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "joey/draw.h"
#include "joey/tile.h"
#include "surfaceInternal.h"
#ifndef DIRTY_MARK_SCENARIOS
#define DIRTY_MARK_SCENARIOS 200000ul
#endif
#define MARKS_PER_SCENARIO 8u
#define RNG_SEED 0x2545F491ul
#define SEED_KINDS 5u
#define SEED_CLEAN 0u
#define SEED_RANDOM_ROWS 1u
#define SEED_RUNS 2u
#define SEED_FULL 3u
#define SEED_PERIODIC 4u
#define PERIOD_ROWS 4u
#define RUN_ROWS_MAX 24u
#define CLEAN_ONE_IN 3u
#define HEIGHT_CLASSES 3u
#define HEIGHT_SMALL_MAX 12u
#define HEIGHT_MEDIUM_MAX 64u
#define FAILURES_SHOWN 5u
#define TILE_SCENARIOS (DIRTY_MARK_SCENARIOS / 4ul)
#define TILE_REPEAT_ONE_IN 3u
#define TILE_FILL_COLOR 1u
#define FILL_SCENARIOS (DIRTY_MARK_SCENARIOS / 4ul)
#define FILL_OFF_EDGE 40u
#define FILL_COLOR 2u
static void bandRandom(uint8_t *minWord, uint8_t *maxWord);
static bool bandsMatch(const char *what);
static void checkFillRectMarks(void);
static void checkTileMarks(void);
static uint16_t heightRandom(uint16_t y);
static void periodicBands(uint8_t *minWord, uint8_t *maxWord);
static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
static uint32_t rngNext(void);
static uint16_t rngBelow(uint16_t n);
static void seedBands(uint16_t kind);
static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord);
static uint32_t gRng = RNG_SEED;
static uint8_t gRefMin[JL_SURFACE_HEIGHT];
static uint8_t gRefMax[JL_SURFACE_HEIGHT];
static uint32_t gChecks = 0ul;
static uint32_t gFailures = 0ul;
// A random real band: first and last word anywhere on the row, first <= last.
static void bandRandom(uint8_t *minWord, uint8_t *maxWord) {
uint8_t a;
uint8_t b;
a = (uint8_t)rngBelow(SURFACE_WORDS_PER_ROW);
b = (uint8_t)rngBelow(SURFACE_WORDS_PER_ROW);
*minWord = (a < b) ? a : b;
*maxWord = (a < b) ? b : a;
}
// Compare the stage's bands with the reference; report the first few misses,
// and put the reference back in the bands after one so the next check starts
// from the same state.
static bool bandsMatch(const char *what) {
uint16_t y;
gChecks++;
if (memcmp(jlgStageMinWord, gRefMin, JL_SURFACE_HEIGHT) == 0 && memcmp(jlgStageMaxWord, gRefMax, JL_SURFACE_HEIGHT) == 0) {
return true;
}
gFailures++;
if (gFailures <= FAILURES_SHOWN) {
for (y = 0u; y < JL_SURFACE_HEIGHT; y++) {
if (jlgStageMinWord[y] != gRefMin[y] || jlgStageMaxWord[y] != gRefMax[y]) {
printf("dirtyMark: FAIL %s: row %u is [%u,%u], expected [%u,%u]\n", what, (unsigned)y, (unsigned)jlgStageMinWord[y], (unsigned)jlgStageMaxWord[y], (unsigned)gRefMin[y], (unsigned)gRefMax[y]);
break;
}
}
}
memcpy(jlgStageMinWord, gRefMin, JL_SURFACE_HEIGHT);
memcpy(jlgStageMaxWord, gRefMax, JL_SURFACE_HEIGHT);
return false;
}
// jlFillRect on the stage, from every seed: rects of every height and any
// width (now and then none), some reaching past the edges, so the clip path
// marks as well as the on-surface one.
static void checkFillRectMarks(void) {
uint32_t scenario;
uint16_t mark;
int16_t x;
int16_t y;
uint16_t w;
uint16_t h;
int16_t left;
int16_t top;
int16_t right;
int16_t bottom;
for (scenario = 0ul; scenario < FILL_SCENARIOS; scenario++) {
seedBands((uint16_t)(scenario % SEED_KINDS));
for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) {
x = (int16_t)((int16_t)rngBelow(JL_SURFACE_WIDTH + 2u * FILL_OFF_EDGE) - (int16_t)FILL_OFF_EDGE);
y = (int16_t)((int16_t)rngBelow(JL_SURFACE_HEIGHT + 2u * FILL_OFF_EDGE) - (int16_t)FILL_OFF_EDGE);
w = rngBelow(JL_SURFACE_WIDTH + 1u);
h = heightRandom(rngBelow(JL_SURFACE_HEIGHT));
jlFillRect(jlgStage, x, y, w, h, FILL_COLOR);
left = (x < 0) ? 0 : x;
top = (y < 0) ? 0 : y;
right = (x + (int16_t)w > JL_SURFACE_WIDTH) ? (int16_t)JL_SURFACE_WIDTH : (int16_t)(x + (int16_t)w);
bottom = (y + (int16_t)h > JL_SURFACE_HEIGHT) ? (int16_t)JL_SURFACE_HEIGHT : (int16_t)(y + (int16_t)h);
if (right > left && bottom > top) {
refMark((uint16_t)top, (uint16_t)bottom, SURFACE_WORD_INDEX(left), SURFACE_WORD_INDEX(right - 1));
}
if (!bandsMatch("jlFillRect") && gFailures <= FAILURES_SHOWN) {
printf("dirtyMark: scenario %lu fill %u: %d,%d %ux%u\n", (unsigned long)scenario, (unsigned)mark, (int)x, (int)y, (unsigned)w, (unsigned)h);
}
}
}
}
// jlTileFill on the stage, from every seed: a third of the fills repeat the
// last tile, so its rows already hold the band.
static void checkTileMarks(void) {
uint32_t scenario;
uint16_t mark;
uint8_t bx;
uint8_t by;
bx = 0u;
by = 0u;
for (scenario = 0ul; scenario < TILE_SCENARIOS; scenario++) {
seedBands((uint16_t)(scenario % SEED_KINDS));
for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) {
if (mark == 0u || rngBelow(TILE_REPEAT_ONE_IN) != 0u) {
bx = (uint8_t)rngBelow(JL_TILE_BLOCKS_PER_ROW);
by = (uint8_t)rngBelow(JL_TILE_BLOCKS_PER_COL);
}
jlTileFill(jlgStage, bx, by, TILE_FILL_COLOR);
refMark((uint16_t)(by * JL_TILE_PIXELS_PER_SIDE), (uint16_t)((by + 1u) * JL_TILE_PIXELS_PER_SIDE), (uint8_t)(bx * 2u), (uint8_t)(bx * 2u + 1u));
if (!bandsMatch("jlTileFill") && gFailures <= FAILURES_SHOWN) {
printf("dirtyMark: scenario %lu fill %u: tile %u,%u\n", (unsigned long)scenario, (unsigned)mark, (unsigned)bx, (unsigned)by);
}
}
}
}
// A mark height for rows from y: short, medium and tall about equally often,
// clipped to the stage.
static uint16_t heightRandom(uint16_t y) {
uint16_t room;
uint16_t h;
room = (uint16_t)(JL_SURFACE_HEIGHT - y);
switch (rngBelow(HEIGHT_CLASSES)) {
case 0u:
h = (uint16_t)(1u + rngBelow(HEIGHT_SMALL_MAX));
break;
case 1u:
h = (uint16_t)(1u + rngBelow(HEIGHT_MEDIUM_MAX));
break;
default:
h = (uint16_t)(1u + rngBelow(JL_SURFACE_HEIGHT));
break;
}
return (h < room) ? h : room;
}
// PERIOD_ROWS random bands; then, a coin each, every min made the least
// of them and every max the greatest (each band still min <= max).
static void periodicBands(uint8_t *minWord, uint8_t *maxWord) {
uint8_t least;
uint8_t greatest;
uint16_t i;
least = STAGE_DIRTY_CLEAN_MIN;
greatest = STAGE_DIRTY_CLEAN_MAX;
for (i = 0u; i < PERIOD_ROWS; i++) {
bandRandom(&minWord[i], &maxWord[i]);
least = (minWord[i] < least) ? minWord[i] : least;
greatest = (maxWord[i] > greatest) ? maxWord[i] : greatest;
}
if (rngBelow(2u) == 0u) {
memset(minWord, least, PERIOD_ROWS);
}
if (rngBelow(2u) == 0u) {
memset(maxWord, greatest, PERIOD_ROWS);
}
}
// The widen every marker must match: each row, one at a time.
static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
for (; y < yEnd; y++) {
if (minWord < gRefMin[y]) {
gRefMin[y] = minWord;
}
if (maxWord > gRefMax[y]) {
gRefMax[y] = maxWord;
}
}
}
static uint32_t rngNext(void) {
gRng ^= gRng << 13;
gRng ^= gRng >> 17;
gRng ^= gRng << 5;
return gRng;
}
static uint16_t rngBelow(uint16_t n) {
return (uint16_t)(rngNext() % n);
}
// Put both the stage's bands and the reference in one of the seed states.
static void seedBands(uint16_t kind) {
uint16_t y;
uint16_t run;
uint8_t minWord;
uint8_t maxWord;
uint8_t periodMin[PERIOD_ROWS];
uint8_t periodMax[PERIOD_ROWS];
y = 0u;
while (y < JL_SURFACE_HEIGHT) {
switch (kind) {
case SEED_CLEAN:
run = JL_SURFACE_HEIGHT;
minWord = STAGE_DIRTY_CLEAN_MIN;
maxWord = STAGE_DIRTY_CLEAN_MAX;
break;
case SEED_FULL:
run = JL_SURFACE_HEIGHT;
minWord = 0u;
maxWord = STAGE_DIRTY_FULL_MAX;
break;
case SEED_PERIODIC:
// Four rows' bands repeating down the stage, their mins (or
// maxes) sometimes all one: band groups whose halves match
// without their bytes all matching.
periodicBands(periodMin, periodMax);
run = JL_SURFACE_HEIGHT;
break;
case SEED_RUNS:
run = (uint16_t)(1u + rngBelow(RUN_ROWS_MAX));
if (rngBelow(CLEAN_ONE_IN) == 0u) {
minWord = STAGE_DIRTY_CLEAN_MIN;
maxWord = STAGE_DIRTY_CLEAN_MAX;
} else {
bandRandom(&minWord, &maxWord);
}
break;
default:
run = 1u;
if (rngBelow(CLEAN_ONE_IN) == 0u) {
minWord = STAGE_DIRTY_CLEAN_MIN;
maxWord = STAGE_DIRTY_CLEAN_MAX;
} else {
bandRandom(&minWord, &maxWord);
}
break;
}
for (; run != 0u && y < JL_SURFACE_HEIGHT; run--, y++) {
if (kind == SEED_PERIODIC) {
setRow(y, periodMin[y % PERIOD_ROWS], periodMax[y % PERIOD_ROWS]);
} else {
setRow(y, minWord, maxWord);
}
}
}
}
static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord) {
jlgStageMinWord[y] = minWord;
jlgStageMaxWord[y] = maxWord;
gRefMin[y] = minWord;
gRefMax[y] = maxWord;
}
int main(void) {
uint32_t scenario;
uint16_t mark;
uint16_t y;
uint16_t h;
uint8_t minWord;
uint8_t maxWord;
#ifdef DIRTY_MARK_LOG
FILE *log;
#endif
// The whole-stage fills. (jlgStage is NULL here, which is the stage.)
seedBands(SEED_RANDOM_ROWS);
jlStageDirtyClearAll();
memset(gRefMin, STAGE_DIRTY_CLEAN_MIN, JL_SURFACE_HEIGHT);
memset(gRefMax, STAGE_DIRTY_CLEAN_MAX, JL_SURFACE_HEIGHT);
bandsMatch("jlStageDirtyClearAll");
seedBands(SEED_RANDOM_ROWS);
jlSurfaceMarkDirtyAll(jlgStage);
memset(gRefMin, 0, JL_SURFACE_HEIGHT);
memset(gRefMax, STAGE_DIRTY_FULL_MAX, JL_SURFACE_HEIGHT);
bandsMatch("jlSurfaceMarkDirtyAll");
for (scenario = 0ul; scenario < DIRTY_MARK_SCENARIOS; scenario++) {
seedBands((uint16_t)(scenario % SEED_KINDS));
for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) {
y = rngBelow(JL_SURFACE_HEIGHT);
h = heightRandom(y);
bandRandom(&minWord, &maxWord);
jlSurfaceMarkDirtyRows(y, (uint16_t)(y + h), minWord, maxWord);
refMark(y, (uint16_t)(y + h), minWord, maxWord);
if (!bandsMatch("jlSurfaceMarkDirtyRows") && gFailures <= FAILURES_SHOWN) {
printf("dirtyMark: scenario %lu mark %u: rows %u..%u [%u,%u]\n", (unsigned long)scenario, (unsigned)mark, (unsigned)y, (unsigned)(y + h - 1u), (unsigned)minWord, (unsigned)maxWord);
}
}
}
// The drawing marks, on a stage made for them.
jlgStage = jlSurfaceCreate();
if (jlgStage == NULL) {
printf("dirtyMark: FAIL could not make a stage for the drawing marks\n");
gFailures++;
} else {
checkFillRectMarks();
checkTileMarks();
jlSurfaceDestroy(jlgStage);
jlgStage = NULL;
}
printf("dirtyMark: %lu passed, %lu failed\n", (unsigned long)(gChecks - gFailures), (unsigned long)gFailures);
#ifdef DIRTY_MARK_LOG
log = fopen(DIRTY_MARK_LOG, "w");
if (log != NULL) {
fprintf(log, "dirtyMark: %lu passed, %lu failed\n", (unsigned long)(gChecks - gFailures), (unsigned long)gFailures);
fclose(log);
}
#endif
return (gFailures == 0ul) ? 0 : 1;
}