Files
Dragon-s-Lair-X68k/tools/bench/verify_load.py
T
prosolis 7179339bd2 Move the loader onto the 68000, and find 5,920 bytes nobody counted
src/player/load.i expands both codebooks to word-per-pixel form and packs the
palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against
tools/bench/dlxload.py on both CPU cores.  The palette half is gated on words
read back out of the palette registers at $E82000, so "the words reached the
hardware" is part of what passes.  ROADMAP P1 is done; P2's encoder half (a
reserved black entry, 23.4) is not, and is a re-encode rather than an edit.

A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot
costs 24.70 ms.  The scratch tables describe the CRTC, not the scene, so
pal_tables is a separate entry point built once at boot -- 5.29 ms off every
scene change.

The one that moves something: the scene header is 5,920 B that no rate table in
this tree included, because it belongs to no frame record.  In FINDINGS 51.3's
currency it is divided by the surplus pipe - wire, so it is hypersensitive:
138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the
same bytes.  tools/analysis/22_scene_load.py prices it across explicit rates.

Recorded as open: the two CPU cores agree to <3% on every stage but the table
build, where they differ by 16.4%.  px68k's C68K charges a flat 50 clocks for
MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains
4,608 of the 8,703 clock gap.  4,095 clocks are unexplained.  Nothing else in
src/player/ multiplies, so no figure in FINDINGS 24-52 is affected.

decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same
MD5.  check.sh gains a stage that gates byte-exactness on both cores and
deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a
wall timing would make the green light host-sensitive.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-24 20:20:40 -07:00

74 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Check the 68000's load-time output against tools/bench/dlxload.py, byte for byte.
python3 tools/bench/verify_load.py <in.dlx> [--out tmp/load]
The 68000 ran src/player/load.i over the RAW container header; tools/bench/
load.lua read the results back out of emulated RAM and out of the PALETTE
REGISTERS. This compares them with what the host-side transforms produce.
Byte-for-byte and not "close enough", for both halves:
* the codebooks are indices, so a single wrong byte is a wrong COLOUR in
every block that uses that codeword, in every frame of the scene.
* the palette words carry the shared LSB the encoder's 1.96 dB (FINDINGS
23.3) depends on, and a wrong choice of it is invisible in a diff of the
picture's SHAPE -- it is a slightly wrong colour, which is exactly the sort
of thing that gets attributed to the codec.
The darkest-entry index is checked too: it is what the letterbox is filled
with until the encoder reserves a black entry (23.4, still open), and it comes
out of an argmin whose tie-break has to match numpy's -- first index wins.
"""
import sys, argparse
sys.path.insert(0, "tools/encoder")
sys.path.insert(0, "tools/bench")
from dlx import DLX
import dlxload as DL
ap = argparse.ArgumentParser()
ap.add_argument("container")
ap.add_argument("--out", default="tmp/load")
ap.add_argument("--log", default="tmp/load_check.log",
help="the rig's log, for the DARK= line it printed")
a = ap.parse_args()
d = DLX(a.container)
cb1, cb4 = DL.expand_codebooks(d)
palb, dark, _ = DL.pack_palette(d)
want = cb1.tobytes() + cb4.tobytes() + palb.tobytes()
got = open(a.out + "_out.bin", "rb").read()
if len(got) != len(want):
sys.exit(f"FAIL: the 68000 produced {len(got)} B, expected {len(want)}")
n1, n4 = cb1.nbytes, cb4.nbytes
sections = (("CB1", 0, n1), ("CB4", n1, n1 + n4), ("palette", n1 + n4, len(want)))
bad = 0
for name, lo, hi in sections:
diff = [i for i in range(lo, hi) if got[i] != want[i]]
if diff:
bad += len(diff)
i = diff[0]
print(f"FAIL: {name}: {len(diff)}/{hi-lo} bytes differ; first at "
f"+{i-lo} (68000 {got[i]:#04x}, dlxload {want[i]:#04x})")
else:
print(f" OK {name}: {hi-lo} B identical to dlxload.py")
# The rig prints the index the 68000 chose; parse it rather than re-deriving,
# so a rig that failed to read LDARK cannot pass by silence.
got_dark = None
for line in open(a.log, "rb").read().decode("utf-8", "replace").splitlines():
if "DARK=" in line:
got_dark = int(line.split("DARK=")[1].split()[0].rstrip(","))
if got_dark is None:
sys.exit("FAIL: the rig printed no DARK= line -- it did not reach the dump")
if got_dark != dark:
sys.exit(f"FAIL: darkest palette entry: 68000 says {got_dark}, dlxload says {dark}")
print(f" OK darkest entry {dark}, chosen by the same argmin tie-break")
if bad:
sys.exit(f"FAIL: {bad} bytes differ in total")
print(f"OK the 68000 reproduced all {len(want)} B of load-time output exactly "
f"(P1 codebooks, P2 palette)")