#!/usr/bin/env python3 """Is EVERY frame the packed player put on screen pixel-exact? ROADMAP K3. python3 tools/bench/verify_packed.py [--snap tmp/snap_packed] [--map tmp/packed_snaps.csv] [--min-frames N] WHY THIS CHECKS ALL OF THEM AND tools/bench/verify_decode.py CHECKS ONE. The codec is temporally recursive: a SKIP block is a claim that the previous frame is still in GVRAM, so the last frame of a sequential run is only correct if every frame before it was, and one comparison audits 120. A packed frame is a LITERAL -- 192 rows of picture and a whole new palette, written over whatever was there. Frame 119 being right says nothing at all about frame 60. The simplification that deleted the ring, the codebooks and the decoder also deleted the gate's free lunch, and this is the bill. WHAT IS COMPARED. MAME's own screen, through MAME's own video code: the snapshot is what the display produced out of GVRAM and the palette REGISTERS. Nothing here re-implements the packed interleave -- that is deliberate and it is the same rule tools/bench/gvpack/verify_dlxp.py was built on, because a container round-trips against its own inverse whether or not its byte order is the one the hardware wants. The reference is dlxp.render(i), which is the palette in the record applied to the indices in the record. THE LETTERBOX IS CHECKED TOO, and it is not padding. The picture is 192 rows of a 256-row screen; the other 64 rows are STATIC SETUP the 68000 wrote once at scene start (packed.s pg_static) and the channel never touches again. If they were wrong -- or if they decayed as the per-frame palette moved under them -- the picture would still be pixel-exact and the screen would not be. Index 255 is black in every frame's palette by construction (vq.frame_palette), so this also gates that reservation across all 120 records. """ import argparse, csv, os, sys sys.path.insert(0, "tools/encoder") import numpy as np from PIL import Image from dlxp import DLXP ap = argparse.ArgumentParser() ap.add_argument("container") ap.add_argument("--snap", default="tmp/snap_packed") ap.add_argument("--map", default="tmp/packed_snaps.csv") ap.add_argument("--min-frames", type=int, default=1, help="fail if fewer than this many frames were sampled -- a " "run that displayed nothing must not pass as a run with " "no mismatches in it") a = ap.parse_args() d = DLXP(a.container) if not d.has_palette: # A --no-palette container leaves the palette registers holding whatever the # scene setup put there, and this rig's player writes none -- so there is no # reference for what the screen should show. Say so rather than compare # against an assumption. sys.exit(f"{a.container} carries no palette; this gate has no reference " f"for what the display should have produced.") with open(a.map) as fh: pairs = [(r["snapshot"], int(r["frame"])) for r in csv.DictReader(fh)] if len(pairs) < a.min_frames: print(f"FAIL 0. only {len(pairs)} frames were sampled, --min-frames is " f"{a.min_frames}. A player whose write window never closed displays " f"nothing, and an empty comparison is not a pass.") sys.exit(1) SCRH, SCRW = 256, 256 YOFF = (SCRH - d.H) // 2 fails, checked = [], 0 for name, fr in pairs: path = f"{a.snap}/x68000/{name}.png" if not os.path.exists(path): fails.append(f"snapshot {name} (frame {fr}) is missing from {a.snap}") continue s = np.asarray(Image.open(path).convert("RGB")).astype(int) if s.shape[:2] != (2 * SCRH, SCRW): fails.append(f"frame {fr}: geometry {s.shape[1]}x{s.shape[0]}, " f"expected {SCRW}x{2*SCRH}") continue if not all(np.array_equal(s[i], s[i + 1]) for i in range(1, s.shape[0] - 1, 2)): fails.append(f"frame {fr}: double-scan pairing (1,2),(3,4),... broken") continue g = s[0::2] pal = d.palette_rgb(fr) exp = np.empty((SCRH, SCRW, 3), int) exp[:] = pal[255] # the letterbox, and the reservation exp[YOFF:YOFF + d.H] = d.render(fr) checked += 1 if np.array_equal(g, exp): continue bad = (g != exp).any(2) by, bx = np.where(bad) inpic = ((by >= YOFF) & (by < YOFF + d.H)).sum() fails.append(f"frame {fr} (snapshot {name}): {bad.sum()} px differ " f"({inpic} in the picture, {bad.sum()-inpic} in the " f"letterbox), first at y={by[0]} x={bx[0]}, maxdiff " f"{abs(g-exp).max()}") for f in fails[:12]: print("FAIL " + f) if len(fails) > 12: print(f"FAIL ... and {len(fails)-12} more") if fails: print(f" {checked-len([f for f in fails])} of {len(pairs)} sampled " f"frames compared clean") sys.exit(1) lo, hi = min(f for _, f in pairs), max(f for _, f in pairs) print(f"OK {checked} frames of {a.container} pixel-exact on the emulated " f"68000, frames {lo}..{hi} of {d.nframes}") print(f" every one of them a LITERAL: no decoder, no codebook, no ring. " f"Screen {SCRW}x{SCRH}, picture {d.W}x{d.H} at y={YOFF}, letterbox on " f"the reserved index 255.") print(f" palette {'LAST' if d.palette_last else 'FIRST'} in the record, " f"{d.pal_bytes} B, compared as the DISPLAY renders it (GRB555+I out of " f"the palette registers)")