// Uber demo: exercise every JoeyLib public API and measure throughput
// of the per-frame-hot ones. Results are written to joeylog.txt via
// jlLogF. A green screen on exit means the run completed.
//
// Timing model: each test aligns to a VBL boundary via jlWaitVBL,
// records the starting jlFrameCount, then runs the op in a tight
// loop polling jlFrameCount until UBER_FRAMES frames have elapsed.
// Reported metric is ops/sec, computed as iters * jlFrameHz() /
// UBER_FRAMES so results are directly comparable across ports
// regardless of CPU speed or VBL rate.
//
// jlFrameCount is wall-clock-based per port, but READING it is not
// free: on the IIgs it is a Misc Toolset GetTick call (several
// hundred cycles per poll -- an earlier comment here claimed
// ~10-30 cyc, which was wrong by more than an order of magnitude
// and inflated every fast op's measured cost by a near-constant
// ~600-800 cycles/iter). runForFrames therefore polls the clock
// once per batch of UBER_BATCH op calls for sub-frame ops
// (calibrated by the first call), amortizing the poll to noise.
//
// One-shot ops (jlSpriteCompile) get one call each, timed by frame
// delta -- coarser but representative.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

#include <joey/joey.h>


// ----- Timing primitives -----

// 4-frame measurement window. Long enough that loop overhead doesn't
// dominate; short enough to keep the full demo run under ~10 sec.
/* 16 frames per timed op gives 4x the iter-count resolution of the
 * earlier 4-frame budget. Exposes the actual per-op cost on slow
 * ops where 4 frames produced the same iter count on different
 * framerates -- e.g. jlDrawCircle r=80 read as "4 iters / 4 frames"
 * on both 60 Hz IIgs (16.7 ms/frame, 67 ms window) and 50 Hz Amiga
 * (20 ms/frame, 80 ms window) even though per-op cost was equal,
 * just because 4 ops at 16-17 ms happen to fit both windows. The
 * 16-frame budget extends the windows to 267 ms / 320 ms; quantum
 * gap shrinks to ~6%. Total run time scales 4x (~80 sec each). */
/* Overridable so a diagnostic build can shrink the measurement window
 * (-DUBER_FRAMES=1) when the run is wanted for its CHK/PROBE rows
 * rather than its timings. The golden binary is built without the
 * define and stays at 16, so goldened output is unaffected. A -DUBER_
 * PROBE run at 16 does not fit bench-iigs.sh's 1200-second MAME cap. */
#ifndef UBER_FRAMES
#define UBER_FRAMES  16u
#endif


// Op calls per clock poll for sub-frame ops. The unrolled batch in
// runForFrames must contain exactly this many op() calls. Ops slower
// than one frame poll every call instead, so a batch cannot overrun
// the measurement window by more than one op.
#define UBER_BATCH 16u

// ----- IIgs progress mailbox (headless-MAME diagnosis) -----
//
// The SHR SCB region covers rows 0..199 at $E1:9D00-$E1:9DC7; the bytes
// at $E1:9DC8-$E1:9DFF are unused by hardware and by jlpPresent. UBER
// mirrors its progress there so a MAME Lua probe can see WHERE a
// headless run is (and whether the timing loop and the tick are alive)
// even when the joeylog FST flush never lands on disk. Layout:
//   +0 (9DC8) uint8  current timed-op index (from timeOp); during
//                     phase 5 it is 100 + the correctness-check number
//   +1 (9DC9) uint8  phase (UBER_PHASE_*)
//   +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass)
//   +4 (9DCC) uint16 last jlFrameCount() value the timing loop saw
// No-ops on every other port.
#define UBER_PHASE_SETUP_SPRITE  1u
#define UBER_PHASE_AUDIO_INIT    2u
#define UBER_PHASE_SHOWCASE      3u
#define UBER_PHASE_TIMED_RUN     4u
#define UBER_PHASE_CHECKS        5u
#define UBER_PHASE_DONE          6u

#ifdef JOEYLIB_PLATFORM_IIGS
#define UBER_MB_OP        ((volatile uint8_t  *)0xE19DC8L)
#define UBER_MB_PHASE     ((volatile uint8_t  *)0xE19DC9L)
#define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL)
#define UBER_MB_TICK      ((volatile uint16_t *)0xE19DCCL)
// One-time layout facts for probe correlation: +8 stage pointer value,
// +12 address of uber.c's gStage static, +16 log-ring base address,
// +20 log-ring head-counter address (see src/core/debug.c's IIgs RAM
// ring -- the Lua probe drains the log live through these).
#define UBER_MB_MAGIC     ((volatile uint16_t *)0xE19DCEL)
#define UBER_MB_STAGEPTR  ((volatile uint32_t *)0xE19DD0L)
#define UBER_MB_GSTAGEAT  ((volatile uint32_t *)0xE19DD4L)
#define UBER_MB_LOGRING   ((volatile uint32_t *)0xE19DD8L)
#define UBER_MB_LOGHEAD   ((volatile uint32_t *)0xE19DDCL)
// TEMP #84: the gSprite pointer value, poked after setupSprite so the
// Lua probe can watch the struct bytes get overwritten in place.
#define UBER_MB_SPRITEPTR ((volatile uint32_t *)0xE19DEAL)
// "JL" -- the Lua probes refuse to trust the layout words until this
// appears (the SCB region holds 0x80 fill before the app writes it).
#define UBER_MB_MAGIC_VAL 0x4A4Cu
extern uint8_t  gJoeyLogRing[];
extern uint16_t gJoeyLogRingHead;
#define uberMbOp(_i)      (*UBER_MB_OP = (uint8_t)(_i))
#define uberMbPhase(_p)   (*UBER_MB_PHASE = (uint8_t)(_p))
#define uberMbBeat()      (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u))
#define uberMbTick(_t)    (*UBER_MB_TICK = (uint16_t)(_t))
#define uberMbLayout()    (*UBER_MB_STAGEPTR = (uint32_t)gStage, *UBER_MB_GSTAGEAT = (uint32_t)&gStage, *UBER_MB_LOGRING = (uint32_t)&gJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&gJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL)
#elif defined(JOEYLIB_PLATFORM_X68000)
// Same mailbox words at the same addresses, for the same reason: a headless
// probe needs to know WHERE the run is when the serial log has gone quiet.
// $E19DC8 is text plane 0's UNUSED upper half -- the plane spans 128 KB but
// only 512 rows x 128 bytes = 64 KB are displayed -- so these writes cost no
// visible pixel and need no memory of their own.
#define UBER_MB_OP        ((volatile uint8_t  *)0xE19DC8L)
#define UBER_MB_PHASE     ((volatile uint8_t  *)0xE19DC9L)
#define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL)
#define UBER_MB_TICK      ((volatile uint16_t *)0xE19DCCL)
#define uberMbOp(_i)      (*UBER_MB_OP = (uint8_t)(_i))
#define uberMbPhase(_p)   (*UBER_MB_PHASE = (uint8_t)(_p))
#define uberMbBeat()      (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u))
#define uberMbTick(_t)    (*UBER_MB_TICK = (uint16_t)(_t))
// No layout word: the RAM log ring it advertises is IIgs-only (src/core/debug.c
// compiles a FILE* logger everywhere else), so there is nothing to point at.
#define uberMbLayout()    ((void)0)
#else
#define uberMbOp(_i)      ((void)0)
#define uberMbPhase(_p)   ((void)0)
#define uberMbBeat()      ((void)0)
#define uberMbTick(_t)    ((void)0)
#define uberMbLayout()    ((void)0)
#endif


typedef void (*OpFn)(void);

static const char *gCurName = "(none)";
static jlSurfaceT   *gStage   = NULL;
static jlSpriteT    *gSprite  = NULL;
static jlSpriteBackupT gBackup;
static unsigned char gBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));

static jlTileT     gTileScratch;

// Phase 0 workload rows (NATIVE-PERF-PLAN.md item 3): patterned tiles
// seeded via draw+snap (tile pixels are platform-private; only a snap
// round-trips portably) and dedicated gameFrame sprite backups so the
// correctness-check gBackup state stays untouched.
static jlTileT          gTileMapA;
static jlTileT          gTileMapB;

#define UBER_FRAME_SPRITES 3

static jlSpriteBackupT  gFrameBackups[UBER_FRAME_SPRITES];
static unsigned char    gFrameBackupBytes[UBER_FRAME_SPRITES][JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));


// Current timed-op number (1-based); mirrored into the mailbox. Lives
// up here because runForFrames re-writes it every loop pass: the SHR
// SCB upload in jlStagePresent can overwrite the adjacent mailbox
// bytes, so a single write at timeOp start can be stomped mid-window.
static uint8_t gOpIndex = 0;


// Run `op` in a tight loop until `targetFrames` jlFrameCount ticks
// have elapsed. Returns iterations completed.
static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *actualFramesOut, uint32_t *millisDeltaOut) {
    unsigned long count;
    uint16_t      startFrame;
    uint16_t      endFrame;
    uint16_t      now;
    uint32_t      startMillis;
    uint32_t      endMillis;
    bool          slowOp;

    count = 0UL;

    jlWaitVBL();
    startFrame  = jlFrameCount();
    startMillis = jlMillisElapsed();

    // Calibrate: jlWaitVBL aligned us to a tick edge, so a sub-frame op
    // cannot see the counter advance during a single call. If the first
    // call DOES advance it, the op is slower than one frame -- poll the
    // clock every call so a batch cannot overrun the window.
    op();
    count++;
    slowOp = ((uint16_t)(jlFrameCount() - startFrame) != 0u);

    for (;;) {
        now = jlFrameCount();
        uberMbTick(now);
        uberMbBeat();
        uberMbOp(gOpIndex);
        if ((uint16_t)(now - startFrame) >= targetFrames) {
            break;
        }
        if (slowOp) {
            op();
            count++;
        } else {
            // Exactly UBER_BATCH calls between clock polls (the poll is
            // a toolbox call on IIgs; per-op polling dominated fast ops).
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            op();
            count += UBER_BATCH;
        }
    }
    /* Capture the actual elapsed frames -- the last iter typically
     * overruns the target. Using actual instead of target as the
     * ops/sec divisor stays honest for ops slower than 1 frame
     * (where count is forced low while real time stretches well
     * past targetFrames). */
    endFrame         = jlFrameCount();
    endMillis        = jlMillisElapsed();
    *actualFramesOut = (uint16_t)(endFrame - startFrame);
    if (*actualFramesOut == 0u) {
        *actualFramesOut = 1u;       /* defensive: avoid div-by-zero */
    }
    *millisDeltaOut = endMillis - startMillis;
    if (*millisDeltaOut == 0u) {
        *millisDeltaOut = 1u;        /* defensive: avoid div-by-zero */
    }
    return count;
}


// Time and log one op. Reports iters / N frames AND the derived
// ops/sec so per-port results are directly comparable against IIgs
// regardless of CPU speed or display refresh rate. Also logs an
// FNV-1a hash of the surface state after timing -- this is the
// pixel-perfect comparison input for the cross-port validation
// harness (tools/diff-uber-hashes.py). Captured against IIgs as the
// golden reference; planar 68k rewrites validate by matching it.
static void timeOp(const char *name, OpFn op) {
    unsigned long iters;
    unsigned long opsPerSec;
    unsigned long opsPerSecFr;
    uint16_t      actualFrames;
    uint32_t      millisDelta;
    uint32_t      hash;

    gCurName = name;
    gOpIndex++;
    uberMbOp(gOpIndex);

    iters = runForFrames(op, UBER_FRAMES, &actualFrames, &millisDelta);

    if (iters == 0UL) {
        jlLogF("UBER: %s: 0 iters (op too slow?)\n", name);
        return;
    }

    /* Report ops/sec from the MILLISECOND clock, not the frame counter.
     * jlMillisElapsed is refresh-independent on every port (ST Timer-C
     * 200 Hz, Amiga audio-ISR tick, DOS PIT, IIgs GetTick-derived so
     * identical to the old formula there), so it does not depend on
     * jlFrameHz() matching the emulator's real VBL rate -- the ST used
     * to hardcode 50 Hz while Hatari runs ~60, under-reporting ~17%.
     * The frame-derived rate is kept as the UBER-CLK cross-check: after
     * the jlpFrameHz fix the two agree; a divergence flags a clock bug. */
    opsPerSec   = (iters * 1000UL) / (unsigned long)millisDelta;
    opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
    hash        = jlSurfaceHash(gStage);
    jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX\n",
             name, iters, actualFrames, opsPerSec, (unsigned long)hash);
    jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)\n",
             name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta);
}




// ----- Test ops -----

static void op_drawPixel       (void) { jlDrawPixel       (gStage, 100, 100, 5); }
static void op_drawLineH       (void) { jlDrawLine        (gStage,   0,  50, 319,  50, 5); }
static void op_drawLineV       (void) { jlDrawLine        (gStage,  50,   0,  50, 199, 5); }
static void op_drawLineDiag    (void) { jlDrawLine        (gStage,   0,   0, 319, 199, 5); }
static void op_drawRect        (void) { jlDrawRect        (gStage,  10,  10, 100, 100, 5); }
static void op_drawCircleSmall (void) { jlDrawCircle      (gStage, 160, 100,  16, 5); }
static void op_drawCircleLarge (void) { jlDrawCircle      (gStage, 160, 100,  80, 5); }
static void op_fillRectSmall   (void) { jlFillRect        (gStage,  20,  20,  16,  16, 7); }
static void op_fillRectMid     (void) { jlFillRect        (gStage,  20,  20,  80,  80, 7); }
static void op_fillRectFull    (void) { jlFillRect        (gStage,   0,   0, 320, 200, 7); }
static void op_fillCircle      (void) { jlFillCircle      (gStage, 160, 100,  40, 7); }
static void op_samplePixel     (void) { (void)jlSamplePixel(gStage, 100, 100); }
static void op_surfaceClear    (void) { jlSurfaceClear    (gStage, 0); }

static void op_paletteSet(void) {
    static uint16_t colors[16] = {
        0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF,
        0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444
    };
    jlPaletteSet(gStage, 0, colors);
}
static void op_scbSetRange     (void) { jlScbSetRange     (gStage,  0, 199, 0); }

static void op_tileFill        (void) { jlTileFill        (gStage, 5, 5, 7); }
static void op_tileCopy        (void) { jlTileCopy        (gStage, 6, 6, gStage, 5, 5); }
static void op_tileCopyMasked  (void) { jlTileCopyMasked  (gStage, 7, 7, gStage, 5, 5, 0); }
static void op_tilePaste       (void) { jlTilePaste       (gStage, 8, 8, &gTileScratch); }
static void op_tileSnap        (void) { jlTileSnap        (gStage, 5, 5, &gTileScratch); }

static int16_t gSpriteX = 40;
static int16_t gSpriteY = 30;

static void op_spriteSave        (void) { jlSpriteSaveUnder   (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); }
static void op_spriteDraw        (void) { jlSpriteDraw        (gStage, gSprite, gSpriteX, gSpriteY); }
static void op_spriteRestore     (void) { jlSpriteRestoreUnder(gStage, &gBackup); }
static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); }

static void op_stagePresent     (void) { jlStagePresent(); }

static void op_inputPoll       (void) { jlInputPoll(); }
static void op_keyDown         (void) { (void)jlKeyDown(KEY_A); }
static void op_keyPressed      (void) { (void)jlKeyPressed(KEY_A); }
static void op_mouseX          (void) { (void)jlMouseX(); }
static void op_joyConnected    (void) { (void)jlJoystickConnected(JOYSTICK_1); }

static void op_audioFrameTick  (void) { jlAudioFrameTick(); }
static void op_audioIsPlaying  (void) { (void)jlAudioIsPlayingMod(); }

static void op_surfaceMarkDirty(void) { /* jlDrawPixel already marks; use fill instead */
                                         jlFillRect(gStage, 0, 0, 32, 32, 0); }


// ----- Phase 0 workload ops (NATIVE-PERF-PLAN.md item 3) -----

// Worst-case unaligned x on every port at once: odd -> IIgs/DOS
// compiled shift-1 (widened RMW row); x % 8 == 7 -> ST shift index 2
// (never compiled, interpreted jlpSpriteDrawPlanes) and Amiga shift 7
// (only shift 0 compiled) with the maximal 7-bit spill across three
// plane bytes per row. Phase 1's all-shift emitters must pull this row
// to ~parity with the aligned jlSpriteDraw row.
#define UBER_UNALIGNED_X 71
#define UBER_UNALIGNED_Y 120

static void op_spriteDrawUnaligned(void) { jlSpriteDraw(gStage, gSprite, UBER_UNALIGNED_X, UBER_UNALIGNED_Y); }


// All 16 x phases in ONE call so the op is idempotent: per-port
// iteration counts and the 16-call unrolled batch would otherwise
// leave a port-dependent final x and a port-dependent hash. 208..223
// covers each Amiga shift twice, both ST compiled shifts (208, 216)
// plus all 14 interpreter phases, and 8 even + 8 odd IIgs/DOS draws.
// Reported ops/sec is SWEEPS per second: multiply by 16 for draws/sec.
#define UBER_SWEEP_X0 208
#define UBER_SWEEP_Y  120

static void op_spriteDrawSweep(void) {
    int16_t x;

    for (x = UBER_SWEEP_X0; x < (int16_t)(UBER_SWEEP_X0 + 16); x++) {
        jlSpriteDraw(gStage, gSprite, x, UBER_SWEEP_Y);
    }
}


// 50 single-tile pastes = the per-call wrapper tax paid 50 times; the
// Phase 3 jlTileMapPaste batch API must beat this row by roughly the
// per-call overhead fraction. Fixed checker of two snapped tiles.
#define UBER_TILEMAP_X0 12
#define UBER_TILEMAP_Y0 12
#define UBER_TILEMAP_W  10
#define UBER_TILEMAP_H  5

// Batch twin of op_tileMapPaste: the SAME 10x5 alternating map in ONE
// jlTileMapPaste call (NATIVE-PERF Phase 3) at a disjoint spot two
// block-rows below, so both rows keep independent pixel signatures.
// gTileSet[0/1] are struct copies of gTileMapA/B made at setup.
#define UBER_TILEMAP_B_Y0 (UBER_TILEMAP_Y0 + UBER_TILEMAP_H + 1)
static jlTileT gTileSet[2];
static uint8_t gTileMapIndices[UBER_TILEMAP_W * UBER_TILEMAP_H];

static void op_tileMapPasteBatch(void) {
    jlTileMapPaste(gStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices);
}


static void op_tileMapPaste(void) {
    uint8_t tx;
    uint8_t ty;

    for (ty = 0; ty < UBER_TILEMAP_H; ty++) {
        for (tx = 0; tx < UBER_TILEMAP_W; tx++) {
            if (((tx + ty) & 1u) == 0u) {
                jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapA);
            } else {
                jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapB);
            }
        }
    }
}


// One realistic game frame: repaint a play region, paste a HUD tile
// row, save+draw three sprites, restore them in reverse, present.
// The fill runs FIRST in the same call, so the saves always capture
// UBER_FRAME_BG and the surface is net-identical after every call --
// the post-window hash is iteration-count independent. All sprite x
// are mod-16-aligned (compiled shift 0 on every port): this row
// isolates the per-call wrapper tax + present; the rows above own the
// alignment signal. Presenting a re-dirtied stage every call also
// fixes the present-row artifact (the timed jlStagePresent row goes
// idle after its first copy).
#define UBER_FRAME_RX      32
#define UBER_FRAME_RY      64
#define UBER_FRAME_RW      256
#define UBER_FRAME_RH      96
#define UBER_FRAME_BG      6
#define UBER_FRAME_TILES   16
#define UBER_FRAME_TILE_TX 4
#define UBER_FRAME_TILE_TY 8

static const int16_t gFrameSpriteX[UBER_FRAME_SPRITES] = { 48, 144, 240 };
static const int16_t gFrameSpriteY[UBER_FRAME_SPRITES] = { 80,  96, 112 };

// The frame's static content (play-region fill + HUD tile row) --
// shared between op_gameFrame's per-call repaint and the one-time
// clean-surface paint of op_gameFrameClean.
static void paintFrameStatic(void) {
    uint8_t t;

    jlFillRect(gStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG);
    for (t = 0; t < UBER_FRAME_TILES; t++) {
        if ((t & 1u) == 0u) {
            jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapA);
        } else {
            jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB);
        }
    }
}


// Everything op_gameFrame does except the present -- shared with the
// UBER_PROBE frameNoPresent row so the probe build measures exactly
// the work the golden row pays before its present.
static void gameFrameBody(void) {
    int16_t i;

    paintFrameStatic();
    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSpriteSaveAndDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]);
    }
    for (i = UBER_FRAME_SPRITES - 1; i >= 0; i--) {
        jlSpriteRestoreUnder(gStage, &gFrameBackups[i]);
    }
}


static void op_gameFrame(void) {
    gameFrameBody();
    jlStagePresent();
}


// Clean-buffer twin of op_gameFrame (NATIVE-PERF Phase 4 STRATEGY
// row -- it measures the clean-buffer pattern for static-background
// games, not an op-for-op speedup of gameFrame). The background +
// HUD are painted ONCE into gCleanSurface before the timed window,
// so the per-frame body is 3 window erases from the clean copy + 3
// sprite draws + present: the per-frame background repaint and the
// per-sprite save-under disappear. Erase-all-then-draw-all per the
// jlSurfaceCopyRect contract. End state every call = sprites drawn
// over clean background, stable from call 1, so the post-window
// hash is iteration-count independent.
static jlSurfaceT *gCleanSurface = NULL;

static void op_gameFrameClean(void) {
    int16_t i;

    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
    }
    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
    }
    jlStagePresent();
}


#ifdef UBER_PROBE
// NATIVE-PERF Phase 6 W0 probe rows. NOT part of the golden contract:
// this block only exists in the scratch probe binary (-DUBER_PROBE,
// built like TILETEST) so the goldened UBER stays byte-identical. The
// rows isolate what no golden row measures -- the DIRTY present -- by
// marking known bands with the library's own marker and presenting.
// Deltas of the present telemetry counters (surfaceInternal.h) are
// logged around each row to split O(rows) scan cost from O(bytes)
// copy cost and to expose over-copy.
// Unconditional since W2 item 0: the IIgs present asm now increments
// the counters too (iigsBlitStageToShr), so every port logs deltas.
extern uint32_t gPresentCopiedBytes;
extern uint32_t gPresentScannedRows;
#ifdef JOEYLIB_PLATFORM_IIGS
extern void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
#define probeMarkRows(_y0, _y1, _w0, _w1) iigsMarkDirtyRowsInner((uint16_t)(_y0), (uint16_t)(_y1), (uint16_t)(_w0), (uint16_t)(_w1))
#else
extern void surfaceMarkDirtyRows(uint16_t yStart, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
#define probeMarkRows(_y0, _y1, _w0, _w1) surfaceMarkDirtyRows((uint16_t)(_y0), (uint16_t)(_y1), (uint8_t)(_w0), (uint8_t)(_w1))
#endif

// Same w65816 register-allocator escape hatch as DRAWSHOWCASE_ATTR:
// a -DUBER_PROBE IIgs build with the probe ops compiled at -O2 dies
// before main (silent no-launch from Finder; bisected 2026-07-07 --
// the trivial-op variant launched, the full block did not). The
// probes measure LIBRARY time, not their own glue, so optnone here
// costs nothing that matters.
#if defined(__clang__)
#define PROBE_ATTR __attribute__((noinline, optnone))
#else
#define PROBE_ATTR __attribute__((noinline))
#endif

// P1: the exact gameFrame band (rows 64..159, words 8..71 = pixels
// 32..287) marked dirty and presented -- the real dirty-present cost
// with zero drawing work.
static void PROBE_ATTR op_probePresent96(void) {
    probeMarkRows(64, 160, 8, 71);
    jlStagePresent();
}


// P2: the three gameFrameClean sprite windows (16x16 at the mod-16
// x positions) -- the scattered-small-band present. On a per-row-band
// port the copy is 3*16 rows x 8 bytes; a bounding-box port over-
// copies the full enclosing rect, which the byte counter exposes.
static void PROBE_ATTR op_probePresent3win(void) {
    probeMarkRows(80, 96, 12, 15);
    probeMarkRows(96, 112, 36, 39);
    probeMarkRows(112, 128, 60, 63);
    jlStagePresent();
}


// P4: gameFrame minus its present -- must land at the component-row
// sum if the harness and dispatch are honest (the forensic model says
// it will; a residual here would mean op interleaving costs exist).
static void PROBE_ATTR op_probeFrameNoPresent(void) {
    gameFrameBody();
}


// P8 (W2): the all-clean present -- the per-present FIXED cost
// isolated (scan + chunks + dirty-clear + dispatch glue, zero copy).
// The call site runs two untimed warm-up presents first (the first
// settles any leftover bands, the second proves the early-out), so
// every timed iteration is a true idle present. Expected per-op
// counter deltas on every port: copied == 0, scanned == 200.
static void PROBE_ATTR op_probePresentIdle(void) {
    jlStagePresent();
}


// P7-1 (Phase 7): 68k sprite save/restore layer split by DIFFERENCE.
// The paper ceilings for these rows are UNREVISED blitter models while
// the implementation is a CPU move.l window copy, so before any sprite
// work the plan requires locating where the ~2x gap actually lives:
// wrapper/validation, dispatch, window size, or body.
//   spriteSaveVal:    fully-offscreen save -- exits through the slow
//                     path's clip-fail return, so it measures the
//                     wrapper entry + NULL checks + field loads +
//                     isFullyOnSurface + clip fail (~ the fast path's
//                     wrapper/validation tax) with ZERO copy body.
//   spriteRestoreVal: backup with an ODD x -- fails the (bx & 1) term,
//                     the LAST check in the restore validation chain,
//                     so the FULL wrapper + validation runs with zero
//                     dispatch/body/mark.
//   spriteSave g16:   full save at x % 16 == 0 -- window class 0 (one
//                     16-px group/row) vs the golden row's x = 40
//                     class-1 window (two groups). The difference is
//                     the pure extra-group copy cost (window-size
//                     bucket). Dedicated backup keeps gBackup (used by
//                     later correctness checks) untouched.
// full-call baselines = the golden jlSpriteSaveUnder / RestoreUnder
// rows in this same log. Stage pixels are never written (saves read;
// the restoreVal call fails validation), so these rows are hash-inert.
static jlSpriteBackupT gProbeSpriteBackup;
static unsigned char   gProbeSpriteBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));

static void PROBE_ATTR op_probeSpriteSaveVal(void) {
    jlSpriteSaveUnder(gStage, gSprite, -100, -100, &gProbeSpriteBackup);
}


static void PROBE_ATTR op_probeSpriteRestoreVal(void) {
    jlSpriteRestoreUnder(gStage, &gProbeSpriteBackup);
}


static void PROBE_ATTR op_probeSpriteSaveG16(void) {
    jlSpriteSaveUnder(gStage, gSprite, 48, 30, &gProbeSpriteBackup);
}


// P7-1 restore-mark cost isolation (scoping the restore-mark asm
// fusion, 2026-07-11). The compiled restore fast path runs body +
// dispatch identically whether the destination is the stage or a work
// surface, but spriteMarkDirty early-returns on s != gStage -- so a
// restore to a NON-stage surface runs the whole call MINUS the 16-row
// dirty-band widen. Differencing the golden jlSpriteRestoreUnder row
// (restores gBackup to gStage -> marks) against this row (restores an
// identical class-1 backup to gProbeRestoreWork -> mark skipped) yields
// the mark body + its call overhead: the UPPER BOUND on what fusing the
// mark into the emitted asm could recover (fusion keeps the widen, only
// removes the C call boundary). Dedicated backup + surface keep gStage
// and gBackup untouched, so the row is inert to the golden hashes.
static jlSurfaceT     *gProbeRestoreWork = NULL;
static jlSpriteBackupT gProbeRestoreBackup;
static unsigned char   gProbeRestoreBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));

static void PROBE_ATTR op_probeRestoreWork(void) {
    jlSpriteRestoreUnder(gProbeRestoreWork, &gProbeRestoreBackup);
}


// P7-1 trusted-restore lever: jlSpriteRestoreUnderTrusted skips the
// geometry-validation chain. Restores the SAME valid class-1 backup to
// gStage as the golden jlSpriteRestoreUnder row (same body + inline
// mark), so the difference vs that row is exactly the validation cost
// the trusted entry saves. Hash-inert: writes the saved-under pixels
// back to gStage (idempotent) behind UBER_PROBE.
static void PROBE_ATTR op_probeRestoreTrusted(void) {
    jlSpriteRestoreUnderTrusted(gStage, &gProbeRestoreBackup);
}


// P7-1 trusted save / save-and-draw: same geometry as the golden
// jlSpriteSaveUnder / jlSpriteSaveAndDraw rows (sprite at 40,30 into a
// valid backup), so each difference vs its golden row is the validation
// the trusted entry skips. Hash-inert behind UBER_PROBE.
static void PROBE_ATTR op_probeSaveTrusted(void) {
    jlSpriteSaveUnderTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
}


static void PROBE_ATTR op_probeSaveAndDrawTrusted(void) {
    jlSpriteSaveAndDrawTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
}


// Rank 9: trusted draw, same geometry as the golden jlSpriteDraw row
// (sprite at 40,30) so the difference vs that row is exactly the
// validation the trusted entry skips. Hash-inert behind UBER_PROBE.
static void PROBE_ATTR op_probeDrawTrusted(void) {
    jlSpriteDrawTrusted(gStage, gSprite, 40, 30);
}


// P5 (W1): sprite-mark band fingerprint. djb2-style fold of the whole
// dirty-band array after marks through every sprite mark arm: compiled
// 16-row draw/restore marks (gameFrameBody), interpreter clip marks
// (h == 16, h == 8, and h == 11 rolled-fallback), and an unaligned-x
// band derive. The value must be BIT-IDENTICAL before and after any
// mark-path change -- marks are invisible to the stage hashes, so this
// fingerprint is the correctness gate for them. The leading present
// resets every band to CLEAN, so the sum is independent of prior rows.
// The band arrays are port-neutral, so the value must also be
// IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any
// fused asm mark must reproduce.
extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];

static uint32_t PROBE_ATTR probeBandFold(void) {
    uint16_t y;
    uint32_t sum;

    sum = 0;
    for (y = 0; y < SURFACE_HEIGHT; y++) {
        sum = (uint32_t)(sum * 33u + gStageMinWord[y]);
        sum = (uint32_t)(sum * 33u + gStageMaxWord[y]);
    }
    return sum;
}

static void PROBE_ATTR op_probeSpriteBandSum(void) {
    uint32_t sum;

    jlStagePresent();
    gameFrameBody();
    jlSpriteDraw(gStage, gSprite, -8, 100);
    jlSpriteDraw(gStage, gSprite, 150, -8);
    jlSpriteDraw(gStage, gSprite, 150, 189);
    jlSpriteDraw(gStage, gSprite, 71, 120);
    sum = probeBandFold();
    jlLogF("UBER-PROBE: spriteBandSum=%lu\n", (unsigned long)sum);
}


// P6 (W1 item 2): fillRect band fingerprint, all four ports. A battery
// of fills exercises every fused-entry arm (odd-x / odd-w partial
// nibbles, lead+trail with no middle, single-pixel columns at both
// surface edges, midBytes == 1, a plain 16x16), plus the two
// batched-mark callers of the NoMark twin (jlDrawRect outline,
// jlFillCircle spans). Cross-port identity is the gate: the three
// C-mark ports produce the reference band state the IIgs fused asm
// mark (stageRectMark) must reproduce exactly.
static void PROBE_ATTR op_probeFillBandSum(void) {
    uint32_t sum;

    jlStagePresent();
    jlFillRect(gStage, 13, 7, 51, 3, 5);
    jlFillRect(gStage, 3, 5, 2, 4, 6);
    jlFillRect(gStage, 2, 9, 1, 1, 7);
    jlFillRect(gStage, 319, 0, 1, 1, 8);
    jlFillRect(gStage, 1, 20, 3, 2, 9);
    jlFillRect(gStage, 40, 40, 16, 16, 10);
    jlDrawRect(gStage, 60, 60, 100, 100, 11);
    jlFillCircle(gStage, 200, 120, 30, 12);
    sum = probeBandFold();
    jlLogF("UBER-PROBE: fillBandSum=%lu\n", (unsigned long)sum);
}


// P7 (fnAddr-cache epoch invalidation) lives after the ball-sprite
// section below -- it creates sprites from gBallTiles.


#ifdef JOEYLIB_PLATFORM_IIGS
// P9 (W2): display-side parity -- the "no stale rows on screen" gate.
// The battery drives every present path: 40-row full-width runs,
// single-row runs at rows 0 and 199, computed-entry runs, a run
// clipped at the 40-row chunk edge, narrow MVN rows, a right-anchored
// band the new predicate demotes to MVN, plus the two arms where the
// W2 predicate CHANGES behavior vs the old >=33-word rule: a
// left-anchored W=16 band (old MVN -> new slam) and a right-anchored
// W=40 band with max+1 > 1.5W (old slam -> new MVN). After the final
// present all bands are CLEAN and the stage ($01:2000) is the
// authoritative image, so all 32,000 displayed bytes at $E1:2000 must
// equal it. Any skipped dirty row, mis-based D/SP, wrong entry index,
// or short MVN leaves >= 1 differing byte.
// P9 TAIL: the 712 display-state bytes ABOVE the pixel block -- the SCB
// array ($E1:9D00-$9DC7, 200 bytes) and all 16 palettes ($E1:9E00-$9FFF,
// 512 bytes). The pixel compare stops at $9CFF and jlSurfaceHash covers
// the STAGE, not $E1, so until now nothing in the tree ever checked the
// bytes the shifter actually reads to pick a palette per scanline. A
// wrong palette is invisible to the golden gate and to the pixel probe;
// it shows only as wrong colors on a real screen. Reached only via
// `make iigs-verify-shrtail` (this whole block is behind UBER_PROBE,
// which no normal build defines). The "palettes 1-15 wrong on a DRAW"
// note this was written to chase turned out to describe correct data --
// see src/iigs/hal.c's jlpPresent -- so this is a standing regression
// gate for the region, not a reproduction of a known defect.
//
// $9DC8-$9DFF is deliberately NOT compared: it is scratch, not display
// state, so there is no source of truth for it. The slam parks its
// per-run SP offset at $9DFE and UBER's mailbox owns $9DC8-$9DCF
// (UBER_MB_MAGIC at $9DCE is a uint16, so it ends at $9DCF, and the
// $9DD0-$9DED layout words above it are live during a probe run too).
#define SHR_TAIL_PALETTES   16u
#define SHR_TAIL_COLORS     16u
#define SHR_TAIL_SCB_BYTES  200u
#define SHR_TAIL_NONE       0xFFFFu

// Compare $E1's SCB block and palette block against the stage, which is
// the authoritative copy. Counts mismatching SCB bytes and mismatching
// palette ENTRIES, and reports the first palette index that differs so a
// failure names the range rather than just the count.
static void PROBE_ATTR probeShrTailCheck(uint16_t *badScb, uint16_t *badPal, uint16_t *firstBadPal) {
    const uint8_t  *shrScb;
    const uint16_t *shrPal;
    uint16_t        want[SHR_TAIL_COLORS];
    uint16_t        line;
    uint16_t        p;
    uint16_t        c;

    shrScb       = (const uint8_t *)0xE19D00L;
    shrPal       = (const uint16_t *)0xE19E00L;
    *badScb      = 0;
    *badPal      = 0;
    *firstBadPal = SHR_TAIL_NONE;
    for (line = 0; line < SHR_TAIL_SCB_BYTES; line++) {
        if (shrScb[line] != jlScbGet(gStage, line)) {
            (*badScb)++;
        }
    }
    for (p = 0; p < SHR_TAIL_PALETTES; p++) {
        jlPaletteGet(gStage, (uint8_t)p, want);
        for (c = 0; c < SHR_TAIL_COLORS; c++) {
            if (shrPal[(p * SHR_TAIL_COLORS) + c] != want[c]) {
                (*badPal)++;
                if (*firstBadPal == SHR_TAIL_NONE) {
                    *firstBadPal = p;
                }
            }
        }
    }
}


// Give every palette a distinct, non-default value so a stale or
// replicated block is detectable: red nibble = palette index, green
// nibble = color index, blue nibble = their sum. Entry 0 is left to
// jlPaletteSet, which forces it to $000 for every palette by contract.
// SCB bands walk all 16 palettes so the SCB block is equally distinct.
static void PROBE_ATTR probeShrTailSeed(void) {
    uint16_t colors[SHR_TAIL_COLORS];
    uint16_t p;
    uint16_t c;

    for (p = 0; p < SHR_TAIL_PALETTES; p++) {
        for (c = 0; c < SHR_TAIL_COLORS; c++) {
            colors[c] = (uint16_t)(((p & 0x0Fu) << 8) | ((c & 0x0Fu) << 4) | ((p + c) & 0x0Fu));
        }
        jlPaletteSet(gStage, (uint8_t)p, colors);
        jlScbSetRange(gStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p);
    }
}


static void PROBE_ATTR op_probeShrParity(void) {
    const uint8_t *stagePx;
    const uint8_t *shrPx;
    uint16_t       badRows;
    uint16_t       firstBad;
    uint16_t       badScbUp;
    uint16_t       badPalUp;
    uint16_t       firstPalUp;
    uint16_t       badScbSlam;
    uint16_t       badPalSlam;
    uint16_t       firstPalSlam;
    uint16_t       x;
    uint16_t       y;

    jlSurfaceClear(gStage, 3);
    jlStagePresent();                     // 5 chunks x 40-row full runs
    probeMarkRows(0, 1, 0, 79);
    jlStagePresent();                     // 1-row full run, top edge
    probeMarkRows(199, 200, 10, 60);
    jlStagePresent();                     // 1-row computed-entry run, bottom edge
    jlFillRect(gStage, 240, 50, 80, 3, 9);
    jlStagePresent();                     // words 60..79, W=20 -> MVN both rules
    jlFillRect(gStage, 0, 55, 64, 1, 10);
    jlStagePresent();                     // words 0..15, W=16 -> old MVN, NEW SLAM
    jlFillRect(gStage, 160, 57, 160, 1, 11);
    jlStagePresent();                     // words 40..79, W=40, 80 > 60 -> old slam, NEW MVN
    jlFillRect(gStage, 0, 60, 144, 2, 4);
    jlStagePresent();                     // words 0..35 -> computed entry
    probeMarkRows(38, 44, 0, 79);
    jlStagePresent();                     // run clipped at the 39/40 chunk edge
    jlFillRect(gStage, 32, 100, 256, 6, 12);
    jlFillRect(gStage, 300, 106, 16, 5, 13);
    jlStagePresent();                     // wide run + narrow MVN in one present
    stagePx  = (const uint8_t *)0x012000L;
    shrPx    = (const uint8_t *)0xE12000L;
    badRows  = 0;
    firstBad = 0xFFFFu;
    for (y = 0; y < 200u; y++) {
        for (x = 0; x < 160u; x++) {
            if (stagePx[x] != shrPx[x]) {
                badRows++;
                if (firstBad == 0xFFFFu) {
                    firstBad = y;
                }
                break;
            }
        }
        stagePx += 160u;
        shrPx   += 160u;
    }
    jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u\n", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad);

    // Tail phase 1: seed all 16 palettes + the SCB bands, then present.
    // jlPaletteSet / jlScbSetRange set the dirty flags, so THIS present
    // is the one that uploads them. A mismatch here means the C-side
    // upload never wrote the bytes correctly in the first place.
    probeShrTailSeed();
    jlStagePresent();
    probeShrTailCheck(&badScbUp, &badPalUp, &firstPalUp);
    jlLogF("UBER-PROBE: shrTailUpload %s badScb=%u badPal=%u firstPal=%u\n", ((badScbUp | badPalUp) == 0u) ? "OK" : "FAIL", (unsigned int)badScbUp, (unsigned int)badPalUp, (unsigned int)firstPalUp);

    // Tail phase 2: pixel-only presents. The dirty flags were cleared by
    // the upload above and nothing below touches a palette or an SCB, so
    // no re-upload happens -- anything that differs now was clobbered
    // AFTER being written correctly. The bands are chosen to put wide
    // slam runs on the highest rows, where a descending-push overrun
    // would land in $9D00+ first, plus a narrow MVN row for that path.
    probeMarkRows(190, 200, 0, 79);
    jlStagePresent();
    probeMarkRows(199, 200, 0, 79);
    jlStagePresent();
    probeMarkRows(196, 200, 70, 79);
    jlStagePresent();
    jlFillRect(gStage, 0, 180, 320, 20, 6);
    jlStagePresent();
    probeShrTailCheck(&badScbSlam, &badPalSlam, &firstPalSlam);
    jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u\n", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam);
}
#endif


#ifdef JOEYLIB_PLATFORM_ATARIST
extern uint32_t jlpProbeDisplayMismatch(const jlSurfaceT *src);

// P10 (W2): display-side staleness trap. Frame A dirties a tall band;
// frame B dirties a small inner rect. With A/B page flipping the
// frame-B page is TWO frames old outside the small rect, so any
// union(prev,cur) or copy-path bug leaves stale bytes on the DISPLAYED
// page; the goldens cannot see it (they gate the shadow, not the
// screen). Zero mismatch against the shadow planes after each present
// is the complete proof: "front page == shadow" IS the present's
// contract. The third (settle) and fourth (early-out) presents must
// keep it zero. Validated on the PRE-change build first so the probe
// itself is trusted.
static void PROBE_ATTR op_probeDisplayCheck(void) {
    uint32_t afterB;
    uint32_t afterSettle;
    uint32_t afterIdle;

    jlFillRect(gStage, 0, 50, 320, 100, 3);
    jlStagePresent();
    jlFillRect(gStage, 100, 60, 40, 10, 9);
    jlStagePresent();
    afterB = jlpProbeDisplayMismatch(gStage);
    jlStagePresent();
    afterSettle = jlpProbeDisplayMismatch(gStage);
    jlStagePresent();
    afterIdle = jlpProbeDisplayMismatch(gStage);
    jlLogF("UBER-PROBE: displayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu\n", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle);
}
#endif


#ifdef JOEYLIB_PLATFORM_AMIGA
extern void amigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum);

// P11 (W2): display-side present verification. After EVERY present in
// a battery covering each run arm -- scattered THIN runs, a full-width
// run, per-row span changes (staircase), marks accumulated across
// multiple draws, both corner rows, and two disjoint windows two
// presents apart (forces the prev-union refresh of the page written
// two flips ago) -- the copper-fetched front planes must equal the
// shadow planes byte-for-byte. The two trailing draw-free checks
// prove the settle present and the genuine idle early-out keep the
// invariant. fails == 0 proves no stale rows can reach the display;
// stage hashes are blind to this.
static uint8_t PROBE_ATTR probePageSumCheck(void) {
    uint32_t frontSum;
    uint32_t shadowSum;

    jlStagePresent();
    amigaProbePresentPageSums(gStage, &frontSum, &shadowSum);
    return (uint8_t)(frontSum != shadowSum);
}


static void PROBE_ATTR op_probePresentPageSum(void) {
    uint8_t fails;

    fails = 0;
    jlSurfaceClear(gStage, 3);
    fails = (uint8_t)(fails + probePageSumCheck());
    jlFillRect(gStage, 48, 80, 16, 16, 5);
    jlFillRect(gStage, 144, 96, 16, 16, 9);
    jlFillRect(gStage, 240, 112, 16, 16, 12);
    fails = (uint8_t)(fails + probePageSumCheck());
    jlFillRect(gStage, 0, 0, 320, 1, 7);
    jlFillRect(gStage, 316, 199, 4, 1, 8);
    jlFillRect(gStage, 100, 150, 8, 1, 4);
    jlFillRect(gStage, 108, 151, 8, 1, 5);
    jlFillRect(gStage, 90, 152, 40, 1, 6);
    fails = (uint8_t)(fails + probePageSumCheck());
    fails = (uint8_t)(fails + probePageSumCheck());
    jlFillRect(gStage, 8, 8, 16, 16, 10);
    fails = (uint8_t)(fails + probePageSumCheck());
    jlFillRect(gStage, 288, 176, 16, 16, 11);
    fails = (uint8_t)(fails + probePageSumCheck());
    // Narrow-tall column (P7-3 audit gap): clear + settle to isolate the
    // run, then a 16-px-wide x 96-row fill coalesces to ONE run whose
    // height*words (96*2 = 192) crosses the Amiga blitter-flush
    // threshold with a NARROW span (blitWords ~2, modBytes ~36). The
    // wide present96 band (modBytes 8, blitWords 16) never exercises the
    // narrow modulo-blit path; this arm verifies it displays correctly.
    jlSurfaceClear(gStage, 3);
    fails = (uint8_t)(fails + probePageSumCheck());
    fails = (uint8_t)(fails + probePageSumCheck());
    jlFillRect(gStage, 48, 40, 16, 96, 13);
    fails = (uint8_t)(fails + probePageSumCheck());
    fails = (uint8_t)(fails + probePageSumCheck());
    fails = (uint8_t)(fails + probePageSumCheck());
    jlLogF("UBER-PROBE: presentPageSum %s (fails=%u)\n", fails == 0 ? "PASS" : "FAIL", (unsigned)fails);
}

#endif


#ifdef JOEYLIB_PLATFORM_DOS
extern uint32_t dosProbeVgaMismatch(const jlSurfaceT *src);

// P12 (perf-hunt-2 rank 4): DOS display-side parity -- the gate the
// other three ports already had (P9/P10/P11) and DOS lacked. DOS
// presents write dirty rows straight into the single VGA buffer, so
// after any present the whole screen must equal the stage; the golden
// hashes gate the stage, never the screen, so a present bug (skipped
// row, short expand, wrong LUT word) is invisible without this.
// Battery mirrors P10: full clear, tall band, small inner rect,
// settle present, idle present -- mismatch must be 0 after each.
// dosProbeVgaMismatch re-derives every byte independently of the
// gExpandLut fast path.
static void PROBE_ATTR op_probeVgaCheck(void) {
    uint32_t afterB;
    uint32_t afterSettle;
    uint32_t afterIdle;

    jlSurfaceClear(gStage, 3);
    jlStagePresent();
    jlFillRect(gStage, 0, 50, 320, 100, 3);
    jlStagePresent();
    jlFillRect(gStage, 100, 60, 40, 10, 9);
    jlStagePresent();
    afterB = dosProbeVgaMismatch(gStage);
    jlStagePresent();
    afterSettle = dosProbeVgaMismatch(gStage);
    jlStagePresent();
    afterIdle = dosProbeVgaMismatch(gStage);
    jlLogF("UBER-PROBE: vgaMismatch afterB=%lu afterSettle=%lu afterIdle=%lu\n", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle);
}
#endif


#if defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_IIGS)
// P7-4 (perf-hunt-2 rank 4): gameFrameClean component split. The
// golden row = 3 copyRect window erases + 3 sprite draws + present;
// differencing these two rows against it isolates each share:
//   golden - cleanNoPresent = the present
//   cleanNoPresent - cleanErases = the sprite draws
static void PROBE_ATTR op_probeCleanNoPresent(void) {
    int16_t i;

    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
    }
    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
    }
}


static void PROBE_ATTR op_probeCleanErases(void) {
    int16_t i;

    for (i = 0; i < UBER_FRAME_SPRITES; i++) {
        jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
    }
}
#endif


// Wrap a probe timeOp with counter-delta logging. The IIgs skips the
// wrapper entirely: its present is asm and never touches the
// counters (always zero there), and the IIgs -DUBER_PROBE build dies
// before main when this block is compiled in (bisected 2026-07-07:
// trivial-op probe launches, counter/fn-ptr wrapper variants do not,
// optnone does not help -- llvm-mos issue, flagged to Scott). Direct
// timeOp calls carry the IIgs probe rows instead.
#ifndef JOEYLIB_PLATFORM_IIGS
static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) {
    uint32_t bytes0;
    uint32_t rows0;

    bytes0 = gPresentCopiedBytes;
    rows0  = gPresentScannedRows;
    timeOp(name, fn);
    jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu\n", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0));
    // Steady-delta second pass (W2): one more call OUTSIDE the timed
    // window. Its single-op delta is the exact steady-state gate value
    // -- the timed average carries a first-iteration union carry-in on
    // the A/B-page ports (iteration 1 unions with the PREVIOUS row's
    // leftover band, e.g. P2's first present drags P1's 96-row band).
    bytes0 = gPresentCopiedBytes;
    rows0  = gPresentScannedRows;
    fn();
    jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu\n", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0));
}
#endif
#endif


// ----- Build the ball sprite procedurally -----

#define BALL_TILES_X 2
#define BALL_TILES_Y 2
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * 32u)

static const uint8_t gBallAuthored[16 * 8] = {
    0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00,
    0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00,
    0x02, 0x22, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20,
    0x02, 0x23, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20,
    0x22, 0x33, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
    0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20,
    0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20,
    0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00,
    0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x00
};
static uint8_t gBallTiles[BALL_TILE_BYTES];

static void buildBallSprite(void) {
    uint16_t tx;
    uint16_t ty;
    uint16_t row;
    uint16_t b;
    uint8_t *dst;

    for (ty = 0; ty < BALL_TILES_Y; ty++) {
        for (tx = 0; tx < BALL_TILES_X; tx++) {
            dst = &gBallTiles[(ty * BALL_TILES_X + tx) * 32u];
            for (row = 0; row < 8; row++) {
                for (b = 0; b < 4; b++) {
                    dst[row * 4 + b] =
                        gBallAuthored[((ty * 8) + row) * 8 + (tx * 4) + b];
                }
            }
        }
    }
}


#ifdef UBER_PROBE
// P7 (W1 item 3): compaction re-derive. Victim sprite V is created
// and compiled BEFORE survivor S, so destroying V leaves a hole BELOW
// S and jlSpriteCompact moves S's arena bytes -- every subsequent
// draw must re-derive S's routine address from the rewritten
// slot->offset (the fused IIgs entries receive it from the C glue per
// call). S is drawn before the compact, at both nibble parities
// after, and once more after a nothing-freed compact. A stale-address
// draw shows as a wild JSL (hang) or content drift in the surface
// hash; the band sum catches mark drift. Cross-port identity gates
// it. Lives below the ball-sprite section for gBallTiles;
// probeBandFold and PROBE_ATTR come from the probe block above.
static void PROBE_ATTR op_probeCompactEpoch(void) {
    jlSpriteT *victim;
    jlSpriteT *survivor;

    jlSurfaceClear(gStage, 0);
    jlStagePresent();
    victim   = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    survivor = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    if (victim == NULL || survivor == NULL || !jlSpriteCompile(victim) || !jlSpriteCompile(survivor)) {
        jlLogF("UBER-PROBE: compactEpoch SKIPPED (create/compile failed)\n");
        if (victim != NULL) {
            jlSpriteDestroy(victim);
        }
        if (survivor != NULL) {
            jlSpriteDestroy(survivor);
        }
        return;
    }
    jlSpriteDraw(gStage, survivor, 32, 40);
    jlSpriteDestroy(victim);
    jlSpriteCompact();
    jlSpriteDraw(gStage, survivor, 96, 40);
    jlSpriteDraw(gStage, survivor, 129, 60);
    jlSpriteCompact();
    jlSpriteDraw(gStage, survivor, 160, 80);
    jlSpriteDestroy(survivor);
    jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu\n", (unsigned long)jlSurfaceHash(gStage), (unsigned long)probeBandFold());
}
#endif


// ----- Visual showcase -----
//
// Before the (visually meaningless) timed benchmark, draw one example
// of each primitive into its own grid cell so a viewer can actually SEE
// what the library renders instead of a single solid color. The
// benchmark that follows never presents, so this showcase stays on
// screen for the whole timed run; results go to joeylog.txt. A legend
// mapping cell index -> primitive is also logged.

#define SC_SCREEN_W       320
#define SC_SCREEN_H       200
#define SC_PAL_COUNT      16
#define SC_PAL_SWATCH_W   (SC_SCREEN_W / SC_PAL_COUNT)
#define SC_PAL_STRIP_H    12
#define SC_GRID_TOP       16
#define SC_COLS           4
#define SC_ROWS           3
#define SC_CELLS          (SC_COLS * SC_ROWS)
#define SC_GUTTER         4
#define SC_INSET          5
#define SC_BG_COLOR       10
#define SC_BORDER_COLOR   1
#define SC_HOLD_FRAMES    210


static const uint16_t gShowcasePal[SC_PAL_COUNT] = {
    0x000, 0xFFF, 0xF00, 0x0F0, 0x00F, 0xFF0, 0x0FF, 0xF0F,
    0xF80, 0xAAA, 0x555, 0x8AF, 0x8F8, 0xF8C, 0x840, 0xACE
};


static void showcaseCellRect(uint16_t index, int16_t *outX, int16_t *outY, int16_t *outW, int16_t *outH) {
    uint16_t col;
    uint16_t row;
    int16_t  cellW;
    int16_t  cellH;

    col   = (uint16_t)(index % SC_COLS);
    row   = (uint16_t)(index / SC_COLS);
    cellW = (int16_t)((SC_SCREEN_W - (SC_COLS + 1) * SC_GUTTER) / SC_COLS);
    cellH = (int16_t)((SC_SCREEN_H - SC_GRID_TOP - (SC_ROWS + 1) * SC_GUTTER) / SC_ROWS);
    *outX = (int16_t)(SC_GUTTER + (int16_t)col * (cellW + SC_GUTTER));
    *outY = (int16_t)(SC_GRID_TOP + SC_GUTTER + (int16_t)row * (cellH + SC_GUTTER));
    *outW = cellW;
    *outH = cellH;
}


// optnone: this 12-case showcase has too many simultaneously-live locals for
// the w65816 register allocator at -O2 (it bails "ran out of registers"). It
// renders once at startup, so dropping optimization here costs nothing.
// optnone is a clang-only attribute; GCC (DOS/Amiga/ST) rejects it under
// -Werror=attributes and does not need it, so guard it to clang.
#if defined(__clang__)
#define DRAWSHOWCASE_ATTR __attribute__((noinline, optnone))
#else
#define DRAWSHOWCASE_ATTR __attribute__((noinline))
#endif
static void DRAWSHOWCASE_ATTR drawShowcase(void) {
    uint16_t cell;
    int16_t  x;
    int16_t  y;
    int16_t  w;
    int16_t  h;
    int16_t  ix;
    int16_t  iy;
    int16_t  iw;
    int16_t  ih;
    int16_t  cx;
    int16_t  cy;
    int16_t  r;
    int16_t  k;
    int16_t  px;
    int16_t  py;
    int16_t  minDim;
    uint8_t  bx;
    uint8_t  by;
    uint8_t  bxStart;
    uint8_t  bxEnd;
    uint8_t  byStart;
    uint8_t  byEnd;
    uint16_t held;

    uberMbPhase(UBER_PHASE_SHOWCASE);
    jlPaletteSet(gStage, 0, gShowcasePal);
    jlScbSetRange(gStage, 0, 199, 0);
    jlSurfaceClear(gStage, SC_BG_COLOR);

    // Top strip: all 16 palette entries as swatches.
    for (k = 0; k < SC_PAL_COUNT; k++) {
        jlFillRect(gStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k);
    }

    for (cell = 0; cell < SC_CELLS; cell++) {
        showcaseCellRect(cell, &x, &y, &w, &h);
        jlDrawRect(gStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR);
        ix     = (int16_t)(x + SC_INSET);
        iy     = (int16_t)(y + SC_INSET);
        iw     = (int16_t)(w - 2 * SC_INSET);
        ih     = (int16_t)(h - 2 * SC_INSET);
        cx     = (int16_t)(ix + iw / 2);
        cy     = (int16_t)(iy + ih / 2);
        minDim = (iw < ih) ? iw : ih;

        switch (cell) {
            case 0:
                // Pixels: a scatter of single plotted pixels.
                for (py = iy; py < iy + ih; py += 3) {
                    for (px = ix; px < ix + iw; px += 3) {
                        jlDrawPixel(gStage, px, py, (uint8_t)(2 + ((px + py) % 14)));
                    }
                }
                break;
            case 1:
                // Horizontal lines.
                for (k = 0; k < ih; k += 4) {
                    jlDrawLine(gStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14));
                }
                break;
            case 2:
                // Vertical lines.
                for (k = 0; k < iw; k += 4) {
                    jlDrawLine(gStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14));
                }
                break;
            case 3:
                // Diagonals: an X plus a fan from the center.
                jlDrawLine(gStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5);
                jlDrawLine(gStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6);
                for (k = 0; k < iw; k += 8) {
                    jlDrawLine(gStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8));
                }
                break;
            case 4:
                // Rectangle outlines, concentric.
                for (k = 0; 2 * k < minDim - 4; k += 5) {
                    jlDrawRect(gStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14));
                }
                break;
            case 5:
                // Filled rectangles, overlapping.
                jlFillRect(gStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2);
                jlFillRect(gStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4);
                break;
            case 6:
                // Circle outlines, concentric.
                for (r = (int16_t)(minDim / 2); r > 2; r -= 4) {
                    jlDrawCircle(gStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14));
                }
                break;
            case 7:
                // Filled circles.
                jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8);
                jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 4), 5);
                break;
            case 8:
                // Tiles: an 8x8-block checkerboard inside the cell.
                bxStart = (uint8_t)((ix + 7) / 8);
                bxEnd   = (uint8_t)((ix + iw) / 8);
                byStart = (uint8_t)((iy + 7) / 8);
                byEnd   = (uint8_t)((iy + ih) / 8);
                for (by = byStart; by < byEnd; by++) {
                    for (bx = bxStart; bx < bxEnd; bx++) {
                        jlTileFill(gStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8));
                    }
                }
                break;
            case 9:
                // Sprite: the compiled ball at a few positions.
                jlSpriteDraw(gStage, gSprite, ix, iy);
                jlSpriteDraw(gStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16));
                jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8));
                break;
            case 10:
                // Flood fill: outline a circle, then flood its interior.
                jlDrawCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1);
                jlFloodFill(gStage, cx, cy, 12);
                break;
            default:
                // Mini scene: ground, sun, horizon, ball.
                jlFillRect(gStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4);
                jlFillCircle(gStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5);
                jlDrawLine(gStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1);
                jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16));
                break;
        }
    }

    jlLogF("UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene\n");
    jlStagePresent();

    // Hold the showcase on screen, then auto-advance to the benchmark so
    // the headless perf-capture run still completes without a keypress.
    held = jlFrameCount();
    while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) {
        /* hold */
    }
}


// ----- Non-timed correctness checks (Phase 0 verification harness) -----
//
// Each check draws a deterministic scene and logs the surface hash so
// tools/diff-uber-hashes can compare ports against each other and against
// the frozen golden logs. Several checks intentionally capture the CURRENT
// behavior of known bugs (PERF-AUDIT.md #1, #11, #12, #72): their hash
// lines are EXPECTED to change when the Phase 1 fixes land -- re-golden
// exactly those lines then, nothing else.
//
// PASS/FAIL lines assert invariants that must hold on every port both
// before and after the fixes (round-trips, forced palette color 0, the
// PRNG golden sequence, arena bookkeeping).

static void __attribute__((noinline)) chkHash(const char *name) {
    jlLogF("UBER-CHK: %s: hash=%08lX\n", name, (unsigned long)jlSurfaceHash(gStage));
}


static void __attribute__((noinline)) chkPassFail(const char *name, bool pass) {
    jlLogF("UBER-CHK: %s: %s\n", name, pass ? "PASS" : "FAIL");
}


// Edge-coordinate draws. The off-surface pixel coords stay modest (within
// ~200 rows of the surface) so the pre-fix dirty-band overwrite (finding #1,
// non-IIgs) lands inside the paired band arrays instead of unrelated
// globals; Phase 1 turns these into true no-ops.
static void __attribute__((noinline)) checkEdgeDraws(void) {
    uberMbOp(101);
    jlDrawPixel(gStage, -5, 100, 5);
    jlDrawPixel(gStage, 330, 100, 5);
    jlDrawPixel(gStage, 100, -3, 5);
    jlDrawPixel(gStage, 100, 210, 5);
    chkHash("edge-pixels");
    // w=65535 draws phantom edges today (finding #11); h==2 exercises the
    // zero-height interior-edge fills; the third rect clips normally.
    jlDrawRect(gStage, 10, 10, 65535u, 100, 5);
    jlDrawRect(gStage, 50, 20, 30, 2, 6);
    jlDrawRect(gStage, -10, 150, 340, 40, 7);
    chkHash("edge-drawRect");
    // r=300 clips every span; r=40000 should cover the surface but draws
    // nothing today (finding #12).
    jlFillCircle(gStage, 160, 100, 300, 4);
    jlFillCircle(gStage, 160, 100, 40000u, 9);
    jlDrawCircle(gStage, 10, 10, 40000u, 3);
    chkHash("edge-circles");
    // Far-off-surface endpoints. The H line should fill the whole visible
    // row but draws nothing today (finding #72: the H/V fast path clamps
    // span to 320 BEFORE clipping, so a huge span anchored off-surface
    // clips away entirely). The diagonal clips per-pixel and is correct.
    jlDrawLine(gStage, -20000, 50, 20000, 50, 2);
    jlDrawLine(gStage, 60, -20000, 60, 20000, 2);
    jlDrawLine(gStage, -300, -200, 620, 400, 8);
    chkHash("edge-lines");
}


// Clipped sprite draws take the interpreted path on every port (the
// compiled routines require fully-on-surface): the ONLY cross-port
// coverage of that path, which UBER's timed ops (on-surface, compiled)
// never touch. Also asserts the save/draw/restore round-trip restores
// the exact pre-save pixels at a clipped position.
static void __attribute__((noinline)) checkClippedSprites(void) {
    uint32_t before;

    uberMbOp(102);
    jlSpriteDraw(gStage, gSprite, -8, 50);
    jlSpriteDraw(gStage, gSprite, 312, 50);
    jlSpriteDraw(gStage, gSprite, 150, -8);
    jlSpriteDraw(gStage, gSprite, 150, 192);
    jlSpriteDraw(gStage, gSprite, -8, -8);
    jlSpriteDraw(gStage, gSprite, 400, 100);
    chkHash("sprite-clipped");
    before = jlSurfaceHash(gStage);
    jlSpriteSaveUnder(gStage, gSprite, -8, 100, &gBackup);
    jlSpriteDraw(gStage, gSprite, -8, 100);
    jlSpriteRestoreUnder(gStage, &gBackup);
    chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(gStage) == before);
}


static void __attribute__((noinline)) checkTileMonoFlood(void) {
    static jlTileT mono;
    uint8_t i;

    uberMbOp(103);
    for (i = 0; i < TILE_BYTES; i++) {
        mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0);
    }
    jlTilePasteMono(gStage, 10, 10, &mono, 5, 9);
    jlTilePasteMono(gStage, 11, 10, &mono, 14, 0);
    chkHash("tilePasteMono");
    jlDrawRect(gStage, 240, 150, 40, 30, 1);
    jlFloodFill(gStage, 250, 160, 12);
    chkHash("floodFill");
}


static void __attribute__((noinline)) checkDrawText(void) {
    static uint16_t asciiMap[256];
    uint16_t i;

    uberMbOp(104);
    for (i = 0; i < 256u; i++) {
        asciiMap[i] = TILE_NO_GLYPH;
    }
    // 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here --
    // the section-opening surface clear wiped whatever the timed tile ops
    // left there.
    jlTileFill(gStage, 5, 5, 9);
    jlTileFill(gStage, 6, 6, 3);
    asciiMap['A'] = (uint16_t)(5u | (5u << 8));
    asciiMap['B'] = (uint16_t)(6u | (6u << 8));
    jlDrawText(gStage, 2, 20, gStage, asciiMap, "ABBA");
    chkHash("drawText");
    // Out-of-range start: the entry sanitation loop (Phase 5, #22)
    // reproduces the historical absorb-and-wrap semantics bit-for-bit
    // (the first glyph is consumed without drawing, the second wraps
    // to (0,21) and draws) while guaranteeing jlpTileCopyMasked never
    // sees an out-of-range destination. Separate hash line so any
    // future semantic change stays isolated.
    jlDrawText(gStage, 200, 20, gStage, asciiMap, "AB");
    chkHash("drawText-offgrid");
}


static void __attribute__((noinline)) checkPalette(void) {
    static const uint16_t conforming[16] = {
        0x0ABC, 0x0111, 0x0222, 0x0333, 0x0444, 0x0555, 0x0666, 0x0777,
        0x0888, 0x0999, 0x0AAA, 0x0BBB, 0x0CCC, 0x0DDD, 0x0EEE, 0x0123
    };
    uint16_t readBack[16];
    uint16_t i;
    bool     ok;

    // Contract invariants (stable across the Phase 2 #16 change): color 0
    // is forced to $000 even when the caller passes nonzero, and
    // conforming $0RGB entries 1..15 round-trip exactly.
    uberMbOp(105);
    jlPaletteSet(gStage, 2, conforming);
    jlPaletteGet(gStage, 2, readBack);
    ok = (readBack[0] == 0x0000u);
    for (i = 1; i < 16u; i++) {
        if (readBack[i] != conforming[i]) {
            ok = false;
        }
    }
    chkPassFail("palette-roundtrip", ok);
}


static void __attribute__((noinline)) checkRandom(void) {
    static const uint32_t expected[4] = {
        0x87985AA5UL, 0x155B24A3UL, 0x4820F4C4UL, 0x81B3AC98UL
    };
    uint16_t i;
    bool     ok;

    // xorshift32 golden sequence from a fixed seed; bit-identical on
    // every port and across the Phase 8 (#43) IIgs rewrite.
    uberMbOp(106);
    jlRandomSeed(0x12345678UL);
    ok = true;
    for (i = 0; i < 4u; i++) {
        if (jlRandom() != expected[i]) {
            ok = false;
        }
    }
    if (jlRandomRange(100u) != 43u) {
        ok = false;
    }
    chkPassFail("random-golden", ok);
    jlRandomSeed(1u);
}


// Arena churn: create/compile/destroy so a hole opens and is reused, then
// compact and assert the used-byte counter returns to its pre-churn value.
// Covers the codegen allocator paths UBER's single long-lived sprite never
// exercises (findings #5, #34 land here in Phase 1). Also runs the
// owned-tileData path (jlSpriteCreateFromSurface) that finding #31
// re-allocates in Phase 1, and a sprite-bank load if an asset is present.
static void __attribute__((noinline)) checkAllocator(void) {
    jlSpriteT *a;
    jlSpriteT *b;
    jlSpriteT *c;
    uint32_t   usedBefore;

    // Sub-markers 111+ pinpoint the statement that hangs (Phase 1
    // stabilization; the coarse marker froze on this check).
    uberMbOp(111);
    usedBefore = jlSpriteCodegenBytesUsed();
    a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    if (a == NULL || b == NULL) {
        chkPassFail("arena-churn", false);
        return;
    }
    uberMbOp(112);
    (void)jlSpriteCompile(a);
    (void)jlSpriteCompile(b);
    uberMbOp(113);
    jlSpriteDestroy(a);
    uberMbOp(114);
    c = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    if (c == NULL) {
        jlSpriteDestroy(b);
        chkPassFail("arena-churn", false);
        return;
    }
    (void)jlSpriteCompile(c);
    uberMbOp(115);
    jlSpriteDestroy(b);
    jlSpriteDestroy(c);
    uberMbOp(116);
    jlSpriteCompact();
    uberMbOp(117);
    chkPassFail("arena-churn", jlSpriteCodegenBytesUsed() == usedBefore);
    uberMbOp(118);
    a = jlSpriteCreateFromSurface(gStage, 0, 0, 2, 2);
    if (a != NULL) {
        (void)jlSpriteCompile(a);
        uberMbOp(119);
        jlSpriteDraw(gStage, a, 280, 20);
        jlSpriteDestroy(a);
    }
    chkPassFail("sprite-from-surface", a != NULL);
    uberMbOp(120);
    chkHash("sprite-from-surface-draw");
}


// Coverage the primary checkTileMonoFlood golden misses (found in the
// #3/#4 re-analysis): the alternating 0x0F/0xF0 mono pattern only
// selects IIgs #3 combo indices 1 and 2, and jlFloodFill only drives
// the matchEqual branch of the ST #4 plane hooks. Runs LAST so it
// perturbs no earlier cumulative full-stage hash.
static void __attribute__((noinline)) checkTileMonoFloodExtra(void) {
    // 0x00 -> combo idx 0 (bg:bg), 0xFF -> idx 3 (fg:fg), 0x0F -> idx 1
    // (bg:fg), 0xF0 -> idx 2 (fg:bg): all four opacity pairs in one tile.
    static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u };
    static jlTileT        monoAll;
    uint8_t               i;

    uberMbOp(121);
    for (i = 0; i < TILE_BYTES; i++) {
        monoAll.pixels[i] = comboBytes[i & 3u];
    }
    jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9);
    jlTilePasteMono(gStage, 11, 12, &monoAll, 14, 0);
    chkHash("tilePasteMonoAll");

    // Bounded flood: an enclosed color-3 box over a pre-cleared interior
    // so the fill stays contained and drives the bounded stop mask
    // (pix == boundary || pix == new) plus the planar group-skip across
    // several 16px groups. matchColor = boundaryColor = 3, matchEqual = 0.
    jlFillRect(gStage, 40, 140, 64, 40, 0);
    jlDrawRect(gStage, 44, 144, 52, 32, 3);
    jlFloodFillBounded(gStage, 70, 160, 7, 3);
    chkHash("floodFillBounded");
}


static void __attribute__((noinline)) runCorrectnessChecks(void) {
    uberMbPhase(UBER_PHASE_CHECKS);
    jlLogF("UBER-CHK: ----- begin -----\n");
    jlSurfaceClear(gStage, 0);
    checkEdgeDraws();
    checkClippedSprites();
    checkTileMonoFlood();
    checkDrawText();
    checkPalette();
    checkRandom();
    checkAllocator();
    checkTileMonoFloodExtra();
    // jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown
    // of the whole library mid-run; it gets a dedicated micro-example in
    // Phase 1 rather than risking the benchmark's display state here.
    jlLogF("UBER-CHK: ----- end -----\n");
}


// ----- Main -----

static void __attribute__((noinline)) runAllTests(void) {
    uberMbPhase(UBER_PHASE_TIMED_RUN);
    jlLogF("UBER: ----- begin -----\n");

    // Surface / palette / SCB.
    timeOp("jlSurfaceClear",       op_surfaceClear);
    timeOp("jlPaletteSet",         op_paletteSet);
    timeOp("jlScbSetRange",        op_scbSetRange);

    // Drawing primitives.
    timeOp("jlDrawPixel",          op_drawPixel);
    timeOp("jlDrawLine H",         op_drawLineH);
    timeOp("jlDrawLine V",         op_drawLineV);
    timeOp("jlDrawLine diag",      op_drawLineDiag);
    timeOp("jlDrawRect 100x100",   op_drawRect);
    timeOp("jlDrawCircle r=16",    op_drawCircleSmall);
    timeOp("jlDrawCircle r=80",    op_drawCircleLarge);
    timeOp("jlFillRect 16x16",     op_fillRectSmall);
    timeOp("jlFillRect 80x80",     op_fillRectMid);
    timeOp("jlFillRect 320x200",   op_fillRectFull);
    timeOp("jlFillCircle r=40",    op_fillCircle);
    timeOp("jlSamplePixel",        op_samplePixel);

    // Tiles. Seed scratch tile + dest cells with non-zero pixels first.
    jlFillRect(gStage, 0, 0, 320, 64, 7);
    jlTileSnap(gStage, 5, 5, &gTileScratch);
    timeOp("jlTileFill",           op_tileFill);
    timeOp("jlTileCopy",           op_tileCopy);
    timeOp("jlTileCopyMasked",     op_tileCopyMasked);
    timeOp("jlTilePaste",          op_tilePaste);
    timeOp("jlTileSnap",           op_tileSnap);

    // Sprites. Background must be non-empty so save-under has work
    // to do (otherwise it's a 4 KB memset of zeros, atypical).
    jlSurfaceClear(gStage, 4);
    timeOp("jlSpriteSaveUnder",    op_spriteSave);
    timeOp("jlSpriteDraw",         op_spriteDraw);
    timeOp("jlSpriteRestoreUnder", op_spriteRestore);
    timeOp("jlSpriteSaveAndDraw",  op_spriteSaveAndDraw);

    // Present. One warm-up call before each timed loop primes any
    // per-port one-time setup (Amiga: copper list rebuild after the
    // jlPaletteSet / jlScbSetRange tests dirty the cache; without warm-up
    // the rebuild's MakeScreen + MrgCop + WaitTOF chain consumes the
    // entire 4-frame measurement window) so we measure steady-state
    // throughput rather than first-call penalty.
    jlStagePresent();
    timeOp("jlStagePresent full",  op_stagePresent);

    // Input.
    timeOp("jlInputPoll",      op_inputPoll);
    timeOp("jlKeyDown",        op_keyDown);
    timeOp("jlKeyPressed",     op_keyPressed);
    timeOp("jlMouseX",         op_mouseX);
    timeOp("joeyJoyConnected",   op_joyConnected);

    // Audio.
    timeOp("jlAudioFrameTick",     op_audioFrameTick);
    timeOp("jlAudioIsPlayingMod",  op_audioIsPlaying);

    // Surface mark dirty (via jlFillRect's mark step).
    timeOp("surfaceMarkDirtyRect (via jlFillRect 32x32)", op_surfaceMarkDirty);

    // ----- Phase 0 workload rows (NATIVE-PERF-PLAN.md item 3). Appended
    // last on purpose: timed hashes are cumulative and the correctness
    // checks clear the stage first, so the frozen lines above stay
    // bit-identical (Phase-7 golden-extension precedent).
    timeOp("jlSpriteDraw unaligned", op_spriteDrawUnaligned);
    timeOp("jlSpriteDraw sweep16",   op_spriteDrawSweep);

    // Seed two patterned tiles via portable draws + snap. jlTileT.pixels
    // is platform-private (plane bytes on ST/Amiga), so hand-written
    // bytes would render differently per port; only a snap round-trips.
    jlFillRect(gStage, 0, 72, 4, 4, 3);
    jlFillRect(gStage, 4, 72, 4, 4, 5);
    jlFillRect(gStage, 0, 76, 4, 4, 5);
    jlFillRect(gStage, 4, 76, 4, 4, 3);
    jlTileSnap(gStage, 0, 9, &gTileMapA);
    jlFillRect(gStage, 8, 72, 8, 8, 9);
    jlDrawLine(gStage, 8, 72, 15, 79, 1);
    jlTileSnap(gStage, 1, 9, &gTileMapB);
    gTileSet[0] = gTileMapA;
    gTileSet[1] = gTileMapB;
    {
        uint8_t tx;
        uint8_t ty;
        for (ty = 0; ty < UBER_TILEMAP_H; ty++) {
            for (tx = 0; tx < UBER_TILEMAP_W; tx++) {
                gTileMapIndices[ty * UBER_TILEMAP_W + tx] = (uint8_t)((tx + ty) & 1u);
            }
        }
    }
    gFrameBackups[0].bytes = gFrameBackupBytes[0];
    gFrameBackups[1].bytes = gFrameBackupBytes[1];
    gFrameBackups[2].bytes = gFrameBackupBytes[2];

    timeOp("jlTilePaste map 10x5",   op_tileMapPaste);
    timeOp("gameFrame composite",    op_gameFrame);
    timeOp("jlTileMapPaste 10x5",    op_tileMapPasteBatch);

    // Clean-buffer strategy row (appended last per the cumulative-
    // hash safety rule): paint the frame's static content, snapshot
    // the WHOLE stage into the clean surface (clean == stage
    // everywhere makes the snapped erase windows hash-safe by
    // construction), then time erase+draw+present.
    gCleanSurface = jlSurfaceCreate();
    if (gCleanSurface != NULL) {
        paintFrameStatic();
        jlSurfaceCopy(gCleanSurface, gStage);
        timeOp("gameFrameClean composite", op_gameFrameClean);
        jlSurfaceDestroy(gCleanSurface);
        gCleanSurface = NULL;
    } else {
        jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)\n");
    }

#ifdef UBER_PROBE
    // W0 probe rows, present-cost forensics. Order matters: the idle
    // present row above (timed earlier) is P3; P1/P2 mark-and-present
    // are self-consistent per call; P4 runs last among the TIMED rows
    // because it leaves dirty bands unpresented. P5 (untimed, the W1
    // sprite-mark fingerprint) runs after P4 -- its leading present
    // flushes P4's leftovers and re-baselines every band to CLEAN.
    // The counter-wrapped gameFrame row is the copied-bytes control
    // for mark-path changes. IIgs: no counter wrapper (see probeOp),
    // no P5 (mark inertness there is proven by object-code compare).
#ifdef JOEYLIB_PLATFORM_IIGS
    // Direct delta logging: the probeOp fn-pointer wrapper dies before
    // main on llvm-mos (W0 bisect); direct extern reads are the
    // bisected-safe shape. Same rows + steady-delta protocol as the
    // probeOp arm below.
    {
        uint32_t probeBytes0;
        uint32_t probeRows0;

        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        timeOp("probe present96", op_probePresent96);
        jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        op_probePresent96();
        jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        timeOp("probe present3win", op_probePresent3win);
        jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        op_probePresent3win();
        jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        timeOp("probe frameNoPresent", op_probeFrameNoPresent);
        // P7-1 IIgs trusted rows: same geometry (sprite at 40,30 into a
        // valid backup) as the golden jlSpriteSaveUnder / RestoreUnder /
        // SaveAndDraw rows, so each diff vs its golden row = the
        // validation the trusted entry skips. Placed BEFORE the present
        // pair below so the stage writes these do are re-synced to SHR
        // before op_probeShrParity. timeOp (not probeOp) because the
        // probeOp fn-pointer wrapper dies pre-main on llvm-mos.
        gProbeRestoreBackup.sprite = gSprite;
        gProbeRestoreBackup.bytes  = gProbeRestoreBackupBytes;
        jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
        timeOp("probe restoreTrusted",      op_probeRestoreTrusted);
        timeOp("probe saveTrusted",         op_probeSaveTrusted);
        timeOp("probe saveAndDrawTrusted",  op_probeSaveAndDrawTrusted);
        timeOp("probe drawTrusted",         op_probeDrawTrusted);
        jlStagePresent();
        jlStagePresent();
        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        timeOp("probe presentIdle", op_probePresentIdle);
        jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        probeBytes0 = gPresentCopiedBytes;
        probeRows0  = gPresentScannedRows;
        op_probePresentIdle();
        jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
        // Hunt-3 rank 1: gameFrameClean component split on IIgs (the
        // 16-21 ms unattributed residual). Same recipe as the golden
        // row (gCleanSurface was destroyed after it): re-create with
        // clean == stage everywhere, run the two split rows via direct
        // timeOp (the probeOp wrapper dies pre-main on llvm-mos),
        // destroy again.
        gCleanSurface = jlSurfaceCreate();
        if (gCleanSurface != NULL) {
            paintFrameStatic();
            jlSurfaceCopy(gCleanSurface, gStage);
            timeOp("probe cleanNoPresent", op_probeCleanNoPresent);
            timeOp("probe cleanErases",    op_probeCleanErases);
            jlSurfaceDestroy(gCleanSurface);
            gCleanSurface = NULL;
        }
    }
#else
    probeOp("probe present96",      op_probePresent96);
    probeOp("probe present3win",    op_probePresent3win);
    probeOp("probe gameFrame",      op_gameFrame);
    probeOp("probe frameNoPresent", op_probeFrameNoPresent);
    // P7-1 sprite layer-split rows. bytes/sprite BEFORE the saves so
    // the g16 row takes the compiled fast path (bytes == NULL would
    // demote it to the metadata-only path); the odd-x fields AFTER the
    // saves (which overwrite the metadata), right before restoreVal.
    gProbeSpriteBackup.sprite = gSprite;
    gProbeSpriteBackup.bytes  = gProbeSpriteBackupBytes;
    probeOp("probe spriteSaveVal",    op_probeSpriteSaveVal);
    probeOp("probe spriteSave g16",   op_probeSpriteSaveG16);
    gProbeSpriteBackup.x      = 41;
    gProbeSpriteBackup.y      = 30;
    gProbeSpriteBackup.width  = 16;
    gProbeSpriteBackup.height = 16;
    probeOp("probe spriteRestoreVal", op_probeSpriteRestoreVal);
    // Restore-mark cost: populate a VALID class-1 backup (save at 40,30
    // matches the golden restore's geometry exactly) into a dedicated
    // buffer, then time a restore to a non-stage work surface. The
    // difference vs the golden jlSpriteRestoreUnder row = the mark.
    gProbeRestoreWork = jlSurfaceCreate();
    gProbeRestoreBackup.sprite = gSprite;
    gProbeRestoreBackup.bytes  = gProbeRestoreBackupBytes;
    jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
    if (gProbeRestoreWork != NULL) {
        probeOp("probe restoreWork", op_probeRestoreWork);
    }
    probeOp("probe restoreTrusted", op_probeRestoreTrusted);
    probeOp("probe saveTrusted",    op_probeSaveTrusted);
    probeOp("probe saveAndDrawTrusted", op_probeSaveAndDrawTrusted);
    probeOp("probe drawTrusted",    op_probeDrawTrusted);
#ifdef JOEYLIB_PLATFORM_DOS
    // P7-4 gameFrameClean component split. The golden row destroys
    // gCleanSurface right after its timed window, so re-create it with
    // the same recipe (clean == stage everywhere) for the split rows.
    gCleanSurface = jlSurfaceCreate();
    if (gCleanSurface != NULL) {
        paintFrameStatic();
        jlSurfaceCopy(gCleanSurface, gStage);
        probeOp("probe cleanNoPresent", op_probeCleanNoPresent);
        probeOp("probe cleanErases",    op_probeCleanErases);
        jlSurfaceDestroy(gCleanSurface);
        gCleanSurface = NULL;
    }
#endif
    // P8 warm-ups: the first present settles P4's leftover bands, the
    // second proves the idle early-out, so every timed P8 iteration is
    // a true idle present.
    jlStagePresent();
    jlStagePresent();
    probeOp("probe presentIdle",    op_probePresentIdle);
#endif
    op_probeSpriteBandSum();
    op_probeFillBandSum();
    op_probeCompactEpoch();
    // Display-side gates (W2): AFTER the P5/P6/P7 fingerprint sequence
    // so those values stay byte-comparable across waves.
#ifdef JOEYLIB_PLATFORM_IIGS
    op_probeShrParity();
#endif
#ifdef JOEYLIB_PLATFORM_ATARIST
    op_probeDisplayCheck();
#endif
#ifdef JOEYLIB_PLATFORM_AMIGA
    op_probePresentPageSum();
#endif
#ifdef JOEYLIB_PLATFORM_DOS
    op_probeVgaCheck();
#endif
#endif

    jlLogF("UBER: ----- end -----\n");
}


// Extracted from main so main's register pressure stays under the w65816
// allocator's ceiling -- the 16-entry pal[] + loop index was the overflow.
// noinline is load-bearing at -O2 (the backend would otherwise re-inline a
// single-call static and recreate the pressure).
static void __attribute__((noinline)) setupPalette(void) {
    uint16_t pal[16];
    int      i;

    for (i = 0; i < 16; i++) {
        pal[i] = (uint16_t)((i << 8) | (i << 4) | i);  // grey ramp
    }
    pal[ 0] = 0x000;
    pal[ 1] = 0x800;   // dark red (running)
    pal[ 2] = 0x080;   // green (done)
    pal[ 3] = 0x008;   // blue
    pal[ 5] = 0xFF0;   // yellow (test pixels)
    pal[ 7] = 0xFFF;   // white (fills)
    pal[15] = 0xF00;   // red
    jlPaletteSet(gStage, 0, pal);
    jlScbSetRange(gStage, 0, 199, 0);
}


// Extracted from main for the same register-pressure reason as setupPalette.
static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) {
    uint16_t      endFrame;
    uint16_t      elapsedFrames;
    unsigned long elapsedMs;

    endFrame      = jlFrameCount();
    elapsedFrames = (uint16_t)(endFrame - startFrame);
    elapsedMs     = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz();
    jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)\n",
             elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
}


// Extracted from main (register pressure). Returns false on sprite-create fail.
static bool __attribute__((noinline)) setupSprite(void) {
    uint16_t before;

    uberMbPhase(UBER_PHASE_SETUP_SPRITE);
    buildBallSprite();
    gSprite = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
    if (gSprite == NULL) {
        jlLog("UBER: jlSpriteCreate failed");
        return false;
    }
    // jlSpriteCompile is a one-shot. Time at frame resolution.
    jlWaitVBL();
    before = jlFrameCount();
    if (!jlSpriteCompile(gSprite)) {
        jlLog("UBER: jlSpriteCompile failed");
    }
    while (jlFrameCount() == before) {
        /* wait for next VBL edge */
    }
    jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame\n");
    gBackup.bytes = gBackupBytes;
    return true;
}


// Extracted from main (register pressure): the jlConfigT struct on main's
// frame, on top of the rest of main, pushed the w65816 allocator over its
// limit. noinline keeps it out at -O2.
static bool __attribute__((noinline)) initJoeyLib(void) {
    jlConfigT config;

    /* 32 KB fits one 16x16 sprite's full compiled set on either 68k
     * port (Amiga: 8 pre-shifted DRAW variants ~15-18 KB; ST: 16-phase
     * thunk+table DRAWs ~12 KB; plus ~1-2 KB of save/restore classes).
     * UL on the multiply because a 16-bit int overflows on 32 * 1024. */
    config.codegenBytes = 32UL * 1024;
    config.audioBytes   = 64UL * 1024;
    return jlInit(&config);
}


int main(void) {
    uint16_t startFrame;

    if (!initJoeyLib()) {
        return 1;
    }
    /* jlFrameCount is VBL-driven, so it only ticks after halInit
     * installed its VBL ISR -- captured here is "everything from now
     * to press-any-key". Pre-init setup time is small and not the
     * cost the user is chasing; runAllTests dominates. */
    startFrame = jlFrameCount();

    gStage = jlStageGet();
    if (gStage == NULL) {
        jlShutdown();
        return 1;
    }
    uberMbLayout();

    // A simple visible palette so users see SOMETHING during the run.
    setupPalette();

    // Indicate "running": red bar at top of screen.
    jlSurfaceClear(gStage, 0);
    jlFillRect(gStage, 0, 0, 320, 8, 1);
    jlStagePresent();

    if (!setupSprite()) {
        jlShutdown();
        return 1;
    }
#ifdef JOEYLIB_PLATFORM_IIGS
    *UBER_MB_SPRITEPTR = (uint32_t)gSprite;
#endif

    // Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx
    // without first calling jlAudioPlayMod leaves NTP's engine in a
    // half-initialized state -- NTPstreamsound is designed to OVERLAY on
    // an already-running module. Without NTPprepare/NTPplay first, the
    // streamer oscillator is fired but no music tick ever advances or
    // silences it, and you get a stuck high-pitched scream. UBER doesn't
    // ship a MOD asset, so we skip the SFX exercise. The frame-tick and
    // isPlayingMod calls below still get timed (both are no-op fast
    // paths on IIgs).
    uberMbPhase(UBER_PHASE_AUDIO_INIT);
    if (jlAudioInit()) {
        jlLogF("UBER: audioInit OK\n");
    } else {
        jlLogF("UBER: audioInit failed (skipping audio)\n");
    }

    // Visual showcase: render one of each primitive into its own grid
    // cell so the run shows something legible (the timed benchmark below
    // never presents, so this stays on screen throughout it). The first
    // timed op is jlSurfaceClear, so this leaves the benchmark untouched.
    drawShowcase();

    runAllTests();

    runCorrectnessChecks();

    reportElapsed(startFrame);

    // Done. Green screen + waitForKey.
    uberMbPhase(UBER_PHASE_DONE);
    jlSurfaceClear(gStage, 2);
    jlStagePresent();

    jlLogF("UBER: press any key to exit\n");
    // Flush the log to disk BEFORE the blocking key wait, so an automated
    // (headless) run that kills the process at this prompt still captures
    // the results -- joeyLog otherwise only flushes at the atexit fclose.
    jlLogFlush();
    jlWaitForAnyKey();

    jlSpriteDestroy(gSprite);
    jlShutdown();
    return 0;
}
