// 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 #include #include #include #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[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. static bool bandsMatch(const char *what) { uint16_t y; gChecks++; if (memcmp(gStageMinWord, gRefMin, JL_SURFACE_HEIGHT) == 0 && memcmp(gStageMaxWord, gRefMax, JL_SURFACE_HEIGHT) == 0) { return true; } gFailures++; if (gFailures <= FAILURES_SHOWN) { for (y = 0u; y < JL_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)(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; } // 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 < 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_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++) { 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, JL_SURFACE_HEIGHT); memset(gRefMax, STAGE_DIRTY_CLEAN_MAX, JL_SURFACE_HEIGHT); bandsMatch("stageDirtyClearAll"); seedBands(SEED_RANDOM_ROWS); surfaceMarkDirtyAll(gStage); memset(gRefMin, 0, JL_SURFACE_HEIGHT); memset(gRefMax, STAGE_DIRTY_FULL_MAX, JL_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(JL_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, JL_SURFACE_HEIGHT); memcpy(gStageMaxWord, gRefMax, JL_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; }