Put the frame in a container with no decoder, and find the palette is not free

ROADMAP K2. DLXP1: a 49,664 B record that is 97 sectors exactly, no index and
no length word, because a packed record's length is geometry rather than
content. 582.0 KB/s, which is what FINDINGS 61.9 predicted to the tenth, and it
encodes in 3.3 s because there is no k-means in it.

px68k's own x68k/gvram.c renders the container's bytes index-exact with the
harness computing no interleave -- the only test that can catch an encoder whose
byte order is wrong, since a container round-trips against its own inverse
either way. Both negative controls fail as they must.

The picture is re-derived against this project's builder rather than PIL's
(34.05 dB against 61.9's 34.08) and the GGGGGRRRRRBBBBBI word is charged for the
first time in this tree: 0.53 dB, on every row, so it moves no comparison.

What the control found is the finding. A packed container on a SCENE palette
lands exactly on the codec's ceiling, so the whole +2.31 dB is the per-frame
palette and nothing else -- and 231 of 256 entries change every frame, which
makes a mismatched paint 12.8 dB worse than the correct pairing, on screen for
roughly half of every frame slot if buffer mode does not blank. So B2 now
decides which packed CONTAINER ships, not only which player. The fallback is
already a flag: --scene-palette --no-palette is 30.79 dB, zero churn, 576.0 KB/s
and still +2.07 dB on the shipping codec.

62.5 is priced and is a wash: palette first 20.32 dB, palette last 20.33.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 07:31:05 -07:00
parent 07f36c2af9
commit f1007a0dbc
15 changed files with 1189 additions and 16 deletions
+56
View File
@@ -256,6 +256,42 @@ run bounds the model and not the hardware, and "does a real palette register
take a byte write" joins the hardware list as B4. A negative answer costs 0.28%
of a frame and nothing else (FINDINGS 62).
**The packed container exists, and the palette that makes it better than the
codec is not free after all.** `tools/encoder/dlxp.py` is DLXP1 and `pack.py`
writes it: a **49,664 byte record that is 97 sectors exactly**, no record index
and no length word — a packed record's length is geometry, so record *i* is at
`off + i*rec` and a seek is arithmetic — at **582.0 KB/s**, which is what
FINDINGS 61.9 predicted to the tenth, encoded in **3.3 seconds** because there is
no k-means in it. **px68k's own `gvram.c` renders the container's bytes
index-exact with the harness computing no interleave**, which is the only test
that can catch an encoder whose byte order is wrong: a container round-trips
against its own inverse either way. And the picture is re-derived against this
project's own quantiser rather than PIL's — **34.05 dB against 61.9's 34.08**
with the X68000's `GGGGGRRRRRBBBBBI` word charged for the first time in this
tree, 0.53 dB, on every row, so it moves no comparison.
**What the control found is the finding.** A packed container built on a *scene*
palette lands **exactly on the codec's ceiling, 30.79 dB**, so the whole +2.31 dB
the packed branch has over that ceiling is **the per-frame palette and nothing
else**. And a per-frame palette is not a small delta: **231 of 256 entries change
every frame**, and a picture under the neighbouring frame's palette is **12.8 dB
worse** than the correct pairing — a wipe on screen for roughly half of every
frame slot, forever, *if* buffer mode does not blank the layer. Correct render,
the same frame under the next frame's palette, and the 24-bit source; the frame
is the one whose mismatch is closest to the mean, so it is not an outlier picked
to make the point:
![The palette mismatch: correct, mismatched, source](docs/img/palette-mismatch.png)
It is chroma speckle and a shifted ground rather than a scramble — two
median-cut palettes of adjacent frames occupy a similar gamut — which is milder
than 12.8 dB sounds and worse than a still can show, because a still does not
show it arriving and leaving twelve times a second. So B2 stopped
being a question about headroom and became one about **which packed container
ships**. The fallback is already a flag: `--scene-palette --no-palette` is
30.79 dB, zero churn, **576.0 KB/s**, and still +2.07 dB on the shipping codec as
the display renders both (FINDINGS 63).
**The scene graph is in, and the worst gap between two decision points is
zero.** `tools/import/scenegraph.py` imports the arcade scene graph — 40 scenes,
516 sequences, 906 input windows — and 5.4% of the game's 612 branch transitions
@@ -469,6 +505,15 @@ tools/bench/c68k/ headless px68k C68K harness, a SECOND emulator for every
which MAME cannot report. The Makefile's -no-pie and the
harness's MAP_32BIT arena are load-bearing: C68K truncates
host pointers to 32 bits.
tools/bench/gvpack/ the same second-emulator argument for the DISPLAY: it links
px68k's real x68k/gvram.c, so the address decode, the R20
bit-11 write path, the page-byte selection, the scroll wrap and
the index-0 transparency test are px68k's own code.
verify_gvpack.py checks the LAYOUT (the harness computes the
interleave); verify_dlxp.py checks the CONTAINER, writing a
DLXP1 record's bytes into GVRAM verbatim with no interleave
computed anywhere, which is the only way to catch an encoder
whose byte order is wrong.
25 imports nothing itself: it reads the DLXSCENE1 scene
table and reports the worst gap between two decision points,
what the input layer has to survive, and what both cost in
@@ -479,6 +524,11 @@ tools/import/ the ONLY code in this tree coupled to somebody else's source.
project's own DLXSCENE1 schema, with the sources' licences and
attribution inside it. Nothing is vendored and the output is
gitignored derived data.
30 is the PACKED container's gate: the format's invariants
(round-trip, sectors, the transparency key, the palette word),
and the quality re-derivation 61.9 asked for, in both the RGB888
domain every encoder PSNR here is quoted in and the GRB555 one a
player actually displays.
tools/media/ builds docs/img/ from a paced recording run
tools/vasm/ vasm m68k assembler, binary plus source tarball
tools/encoder/ hybrid VQ encoder and DLX3 container writer.
@@ -492,6 +542,12 @@ tools/encoder/ hybrid VQ encoder and DLX3 container writer.
record as whole sectors straight into the ring with no window
and no bounce copy; dlx.record_lengths() is the one place that
rule is applied.
dlxp.py and pack.py are the OTHER container -- DLXP1, the
decoder-free packed one (FINDINGS 63). Nothing is shared with
the codec's writer on purpose: a packed record is a palette and
a picture, both geometry, and dlxp.py is the one place the
interleave and the 97-sector record are stated. There is no
rate control in pack.py because there is no rate lever.
dlx.py is the reference DECODER, ground truth for the 68000.
24 models the ring with the 68000 owning it: the request
queue, the poll-only-when-not-decoding rule and 54.4's frame