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
This commit is contained in:
prosolis
2026-08-24 20:20:40 -07:00
parent ed172c2da2
commit 7179339bd2
13 changed files with 1189 additions and 18 deletions
+30 -4
View File
@@ -103,14 +103,26 @@ the disk it is where nothing fits at any container size. The ROM drives SASI
rather than the MB89352, so it does not settle W, but a cheap configuration is
now the thing that has to be shown rather than assumed (FINDINGS 52).
**The player builds its own codebooks and palette now.** The two load-time
transforms — codebooks to word-per-pixel form, palette to `GGGGGRRRRRBBBBBI`
with the shared LSB picked per entry — ran host-side until session 21 and now
run on the 68000, out of the raw container header, byte-exact against the host
implementation on both CPU cores and with the palette read back out of the
hardware registers. A scene change costs **18.96 ms**, a third of one 12fps
frame slot. The finding underneath it is a cost nothing had counted: a scene
header is **5,920 bytes** that must arrive before frame 0, and in the currency
of seek slack those bytes lengthen the refill climb by 138 ms at 488 KB/s and by
**1.099 s at 451.4 KB/s**, because the surplus they are divided by goes to zero
(FINDINGS 53).
**Current encode:** 496.7 KB/s at 29.19 dB, 1 frame of 120 over the 12fps
budget, and that one is frame 0, the intra frame, late on purpose.
**Green-light check:** `./tools/bench/check.sh` (~3 min, needs the Blu-ray
**Green-light check:** `./tools/bench/check.sh` (~4 min, needs the Blu-ray
mounted) re-runs both display regression tests, the rate-control drift gate, the
display-path coherency counterexample, a 120-frame 68000 decode on two CPU
cores, the ring and paced-ring passes and the DMAC configuration gate, then
prints `ALL GREEN`.
cores, the ring and paced-ring passes, the DMAC configuration gate and the
load-time transforms on both cores, then prints `ALL GREEN`.
## Reproducing this
@@ -255,6 +267,10 @@ tools/analysis/ measurement scripts, numbered in the order they were written.
model, sharing no code with the Lua producer it checks.
21 decodes the IPL ROM's HD63450 configuration and gates on the
bytes being where it says they are.
22 prices a scene change: header bytes, load-time clocks and
what both cost in accumulated seek slack, across explicit
rates. Its cycle counts are PARSED out of the rig's log, not
pasted in, so they cannot go stale silently.
buscost.py is the shared bus-cycle table. The per-block
constants live in tools/encoder/vq_hybrid.py and are imported,
never copied.
@@ -275,7 +291,12 @@ tools/bench/ MAME Lua injection harness and 68000 benchmark sources.
through a bounded ring at a modelled pipe rate, so the rig is
not bounded by the emulated machine's RAM and a stock 2 MB
machine runs the whole window. dlxload.py holds the
codebook/palette load-time maths both preps share.
codebook/palette load-time maths both preps share -- and
the reference src/player/load.i is gated against.
prep_load.py/load.lua/verify_load.py/load_run.sh run those
transforms ON the 68000 and compare all 10,752 output bytes
with dlxload.py's, palette words read back out of the palette
registers rather than a RAM shadow.
tools/bench/c68k/ headless px68k C68K harness, a SECOND emulator for every
68000 cycle figure. Links only px68k's CPU core: no SDL, no
ROMs, no emulated machine. `make PX68K=~/src/px68k` then
@@ -296,6 +317,11 @@ tools/encoder/ hybrid VQ encoder and DLX3 container writer.
dlx.py is the reference DECODER, ground truth for the 68000.
src/player/ decode.s is the 68000 DLX3 decoder with a preloaded-stream
front-end. stream.s is the same decoder behind a bounded ring.
load.i is the LOAD-time half: codebook expansion and palette
packing, out of the raw container header, with loadgate.s as
its rig front-end. Its three scratch tables describe the
machine rather than the scene, so they are a separate entry
point a player calls once at boot.
Both include frame.i (the block loop and span chain) and
geom.i (the constants), so there is exactly ONE copy of the
bytes every cycle constant is fitted to. The span pass is