joeylib2/tests/host/dirtyMarkHost.c
2026-10-13 14:01:46 -05:00

250 lines
8.2 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.
//
// surfaceMarkDirtyRows 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 -- 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 (stageDirtyClearAll, surfaceMarkDirtyAll).
//
// 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 "surfaceInternal.h"
#ifndef DIRTY_MARK_SCENARIOS
#define DIRTY_MARK_SCENARIOS 200000ul
#endif
#define MARKS_PER_SCENARIO 8u
#define RNG_SEED 0x2545F491ul
#define SEED_KINDS 4u
#define SEED_CLEAN 0u
#define SEED_RANDOM_ROWS 1u
#define SEED_RUNS 2u
#define SEED_FULL 3u
#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
static void bandRandom(uint8_t *minWord, uint8_t *maxWord);
static bool bandsMatch(const char *what);
static uint16_t heightRandom(uint16_t y);
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[SURFACE_HEIGHT];
static uint8_t gRefMax[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.
static bool bandsMatch(const char *what) {
uint16_t y;
gChecks++;
if (memcmp(gStageMinWord, gRefMin, SURFACE_HEIGHT) == 0 && memcmp(gStageMaxWord, gRefMax, SURFACE_HEIGHT) == 0) {
return true;
}
gFailures++;
if (gFailures <= FAILURES_SHOWN) {
for (y = 0u; y < SURFACE_HEIGHT; y++) {
if (gStageMinWord[y] != gRefMin[y] || gStageMaxWord[y] != gRefMax[y]) {
printf("dirtyMark: FAIL %s: row %u is [%u,%u], expected [%u,%u]\n", what, (unsigned)y, (unsigned)gStageMinWord[y], (unsigned)gStageMaxWord[y], (unsigned)gRefMin[y], (unsigned)gRefMax[y]);
break;
}
}
}
return false;
}
// 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)(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(SURFACE_HEIGHT));
break;
}
return (h < room) ? h : room;
}
// 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;
y = 0u;
while (y < SURFACE_HEIGHT) {
switch (kind) {
case SEED_CLEAN:
run = SURFACE_HEIGHT;
minWord = STAGE_DIRTY_CLEAN_MIN;
maxWord = STAGE_DIRTY_CLEAN_MAX;
break;
case SEED_FULL:
run = SURFACE_HEIGHT;
minWord = 0u;
maxWord = STAGE_DIRTY_FULL_MAX;
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 < SURFACE_HEIGHT; run--, y++) {
setRow(y, minWord, maxWord);
}
}
}
static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord) {
gStageMinWord[y] = minWord;
gStageMaxWord[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. (gStage is NULL here, which is the stage.)
seedBands(SEED_RANDOM_ROWS);
stageDirtyClearAll();
memset(gRefMin, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
memset(gRefMax, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);
bandsMatch("stageDirtyClearAll");
seedBands(SEED_RANDOM_ROWS);
surfaceMarkDirtyAll(gStage);
memset(gRefMin, 0, SURFACE_HEIGHT);
memset(gRefMax, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
bandsMatch("surfaceMarkDirtyAll");
for (scenario = 0ul; scenario < DIRTY_MARK_SCENARIOS; scenario++) {
seedBands((uint16_t)(scenario % SEED_KINDS));
for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) {
y = rngBelow(SURFACE_HEIGHT);
h = heightRandom(y);
bandRandom(&minWord, &maxWord);
surfaceMarkDirtyRows(y, (uint16_t)(y + h), minWord, maxWord);
refMark(y, (uint16_t)(y + h), minWord, maxWord);
if (!bandsMatch("surfaceMarkDirtyRows")) {
if (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);
}
memcpy(gStageMinWord, gRefMin, SURFACE_HEIGHT);
memcpy(gStageMaxWord, gRefMax, SURFACE_HEIGHT);
}
}
}
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;
}