The 68000 decoder draws pixel-exact frames, and does not fit
src/player/decode.s parses DLX1 and decodes straight into GVRAM. Verified pixel-exact over a 120-frame sequential run of the worst sustained window on the disc -- all four block modes, full temporal recursion, so the last frame is only right if all 120 were. In check.sh. It costs a mean of 81.7% of a 12fps frame budget, and 31% of frames exceed 100% (42% at scsi). CPU is now the binding constraint. FINDINGS 28. Three things that were believed and are not true: - The dual-display-path plan of FINDINGS 24.5/25.6 is incoherent. The compose path needs a RAM copy of the previous reconstruction; the direct path's selling point is that it keeps none. Mixing them shows stale pixels on 70 of 120 frames, worst frame 18.8% of the screen. Every coherent repair is dearer than not mixing, and 24.5's two figures were both copies with no decode in either, so there was never a crossover to find. One path ships, and the 96KB reference frame is gone. tools/analysis/10_pathmix_drift.py keeps the counterexample runnable; check.sh asserts it still reproduces. - The four block modes do not cost the same. V1 300, V4 448, RAW 400 cycles against the old model's flat 207.8. V4 is 25% of blocks and 50% of the cycles, and the mode decision charges it bytes it does not charge cycles for. tools/analysis/11_cpu_budget.py reproduces all four frames timed on the 68000 to within 1 point. Hand-derived timings agree to 0.5% on V1. - The container is big-endian but not aligned. Variable-length records laid end to end put frame 1's length field at an odd address, and move.l (a0)+ there is an address error: frame 0 decoded perfectly and then vectored into the IPL for 59 emulated seconds looking like a hang. Found by dumping PC, not by reading the source. Also: an all-V1 frame, the cheapest possible full redraw, is 110.5% of budget. No mode assignment fits a scene cut at 12fps. That one needs a decision, not a measurement. Next: charge cycles in the mode decision and bisect against 833,333 per frame, the way session 6 bisects lam against bytes -- but with no bucket, because a late frame cannot be banked. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is the 68000 decoder's output pixel-exact against the reference decoder?
|
||||
|
||||
python3 tools/bench/verify_decode.py <in.dlx> [--snap tmp/snap_decode]
|
||||
|
||||
Checks tmp/snap_decode/x68000/0000.png -- the screen after src/player/decode.s
|
||||
has decoded every frame of the container in sequence -- against
|
||||
tools/encoder/dlx.py's reconstruction of the final frame.
|
||||
|
||||
This is a stronger test than the blit regression it is modelled on. The blit
|
||||
proved the 68000 could COPY a frame; this proves it can PARSE one. And because
|
||||
the decoder is temporally recursive -- a SKIP block is a claim that the previous
|
||||
frame is still in GVRAM -- the last frame of a sequential run is only correct if
|
||||
every frame before it was, so a single comparison audits all of them.
|
||||
"""
|
||||
import argparse, sys
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from dlx import DLX
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container")
|
||||
ap.add_argument("--snap", default="tmp/snap_decode")
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLX(a.container)
|
||||
canvas = np.zeros((d.H, d.W), np.uint8)
|
||||
for f in range(d.nframes):
|
||||
d.paint(canvas, f)
|
||||
|
||||
pal = d.pal.astype(int)
|
||||
p6 = lambda v: ((v << 2) | (v >> 4)) & 0xFF
|
||||
fl = pal >> 3
|
||||
render = lambda I: p6((fl << 1) | I[:, None])
|
||||
I = (((render(np.ones(256, int)) - pal) ** 2).sum(1)
|
||||
< ((render(np.zeros(256, int)) - pal) ** 2).sum(1)).astype(int)
|
||||
exp = render(I)[canvas]
|
||||
|
||||
s = np.asarray(Image.open(f"{a.snap}/x68000/0000.png").convert("RGB")).astype(int)
|
||||
fail = []
|
||||
if s.shape[:2] != (512, 256):
|
||||
fail.append(f"1. geometry: expected 512x256, got {s.shape[1]}x{s.shape[0]}")
|
||||
else:
|
||||
if not all(np.array_equal(s[i], s[i+1]) for i in range(1, s.shape[0]-1, 2)):
|
||||
fail.append("2. double-scan pairing (1,2),(3,4),... broken")
|
||||
g = s[0::2]
|
||||
yoff = (g.shape[0] - d.H) // 2
|
||||
act = g[yoff:yoff+d.H]
|
||||
if not np.array_equal(act, exp):
|
||||
diff = abs(act - exp)
|
||||
bad = diff.any(2)
|
||||
by, bx = np.where(bad)
|
||||
blocks = sorted(set(zip((by//4).tolist(), (bx//4).tolist())))
|
||||
fail.append(f"3. frame {d.nframes-1} not pixel-exact: {bad.sum()} px in "
|
||||
f"{len(blocks)} blocks differ, maxdiff {diff.max()}; "
|
||||
f"first block (by={blocks[0][0]}, bx={blocks[0][1]})")
|
||||
|
||||
for x in fail:
|
||||
print("FAIL " + x)
|
||||
if fail:
|
||||
sys.exit(1)
|
||||
print(f"OK {d.nframes} frames decoded on the 68000, final frame pixel-exact "
|
||||
f"against tools/encoder/dlx.py")
|
||||
print(f" {d.W}x{d.H}, {d.nb} blocks/frame, k1={d.k1} k4={d.k4}, "
|
||||
f"all four block modes exercised")
|
||||
Reference in New Issue
Block a user