6 Commits
Author SHA1 Message Date
prosolis f1007a0dbc 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
2026-08-25 07:31:05 -07:00
prosolis c5ca56330e A second emulator agrees, the bus was never counted, and the DMAC loses by one clock
Three things, and the last one reversed itself when the datasheet arrived.

A SECOND EMULATOR. tools/bench/c68k/ links px68k's C68K core into a headless
harness -- no SDL, no ROMs, no emulated machine, because the decoder touches
nothing but RAM, the control block and GVRAM. decode.s is now pixel-exact under
two independent CPU cores, and cycle-table error against MAME is bounded at
3.3%, running against us. MAME 0.277's M68000 turns out to be the MICROCODE
core, not Musashi (m68000.lst + m68000gen.py), so this is two structurally
different timing models agreeing rather than two tables. FINDINGS 28.8's "V4
costs more than RAW" reproduces independently. FINDINGS 37.

THE BUS. Nothing since FINDINGS 24 had counted the 68000's local memory bus --
one 4-clock cycle at a time, carrying instruction prefetch as well as data. The
decoder occupies 86.7% of it and PREFETCH IS 62% OF THAT TRAFFIC, so a data-only
count understates occupancy by 2x. Two sources check each other: c68k_bench
counts every bus callback exactly, and a static walk of decode.lst supplies the
prefetch no emulator here can report. The walk reproduces the measured data half
to 0.04%, which is what licenses its prefetch half, and 15_bus_occupancy.py is a
gate rather than a report because every bus figure depends on that check.
FINDINGS 38.

THE DMAC CHAIN LOSES. FINDINGS 29.6 named it the one uncosted lever. Costed from
bus arithmetic -- a read cycle plus a write cycle, 8 clocks a pixel -- it scored
1/120 frames over budget against the v6 span's 10/120 and looked decisive. Then
the MC68450 manual (Motorola Jul 1989, now at ~/src/mc68450.pdf): Fig 4-25 sheet
4 puts a dual-address word between two 16-bit ports at 9 CLOCKS, because note 2
gives the DMAC 4-clock reads and 5-clock WRITES. The 68000 writes in 4.

    DMAC   9.000 clocks/pixel   datasheet
    v6     9.152 clocks/pixel   measured, FINDINGS 30

1.7%. Scored additively, 86% of what remains of the DMAC's advantage is v6's
24-pixel padding quantum -- a property of its unrolled movem chain, fixable in
software with a finer tail chain, worth 55/120 -> 18/120 against the DMAC's
12/120. Recommendation: fix the quantum, drop the DMAC. Six frames does not buy
a reserved channel, a two-region container layout and a timing dependency
neither emulator here can verify. The container is identical either way -- v6's
record and an HD63450 chaining entry are both 6 bytes, so the chain array IS the
span table -- so nothing is foreclosed. FINDINGS 39.

TWO CORRECTIONS TO MY OWN WORK IN THE SAME SESSION:

- I argued FINDINGS 35's flat CPU debit for the disk was too pessimistic and
  rescored the window at 53/120 with max(CPU, bus). Wrong. A 68000 has no cache
  and a two-word prefetch queue, so it stalls the moment another master takes
  the bus, and the MC68450 hands the bus over in SLABS under limited-rate
  auto-request rather than interleaving per operand. DMA is additive. 84/120
  stands and 14_dmac_chain.py reproduces it exactly. What 86.7% occupancy really
  says is that there is almost no room to overlap anything. FINDINGS 38.3.
- The first DMAC costing was derived where a primary source existed. Both wrong
  answers were confident and both were caught by reading the manual.

Also landed:
- FINDINGS 5's 8 clocks/word for the SCSI DMA, STATUS's own "most load-bearing
  unmeasured number", is now bracketed by the datasheet: 5 clk/word with the bus
  held, ~12 if the DMAC arbitrates per word. 8 is a supported midpoint, and
  which end applies is a player design decision worth 7 clocks a word on a
  480 KB/s stream. FINDINGS 39.7.
- check.sh gains two gates: the C68K pixel-exact decode (seconds, no MAME) and
  the bus-model self-check. Both skip cleanly without a px68k checkout.
- spanned blocks are now charged their mode-map dispatch, which FINDINGS 30.7
  flagged as uncounted in 12_span_tradeoff.py.
- MAME timed runs must be budgeted by WALL CLOCK, not -seconds_to_run: this box
  runs x68000 at ~0.033x realtime and two runs were killed by their own timeout.
  That is why the all-RAW cell in 37.3 is empty. The C68K harness does the same
  work in seconds because it emulates a CPU and not a machine.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 18:30:23 -07:00
prosolis b322e84cd4 Get a real Dragon's Lair frame onto the emulated X68000
First pixels on an actual X68000 screen. Everything up to now was Python-side
or a headless -video none run, which cannot snapshot at all.

The blocker was not the video controller. The IPL leaves CRTC R20 = 0x0B16,
and bit 11 is "G-VRAM set to buffer", which makes MAME's draw_gfx() return
early. GVRAM writes still land and read back correctly while the layer is
invisible, so six attempts at $E82400/$E82500/$E82600 all rendered black with
every register holding the value I intended.

Two more facts, both confirmed against MAME 0.277 source rather than assumed:

- $E8E001 monitor contrast is left at 14 by the IPL, scaling all output to
  93.3%. The player must set it to 15. Contrast 0 blanks the screen, which is
  a free fade-to-black for scene transitions.
- The palette word is GGGGGRRRRRBBBBBI with a shared LSB, expanded as
  pal6bit((field<<1)|I). With contrast at 15 the render is pixel-exact, not
  merely close, which also confirms the 1024-byte GVRAM line stride.

That exactness gives a new quality ceiling: the 15-bit+I palette alone costs
38.88 dB against the 24-bit palettised source, the same order as the scsi
profile's own codec error. scsi is close to display-transparent on hardware,
which bounds how much further it is worth raising.

Unblocks next step 2, the 68000 decoder skeleton, which now has a known-good
reference image to diff against.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 13:12:27 -07:00
prosolis 22c67f1cb8 Record the two shell traps that wedged session 2's background jobs
Eleven watcher shells and one MAME instance were left running for over an
hour. Both had the same shape: a wait that can never be satisfied.

- `until ! pgrep -f foo.py` matches the watching shell's own command line,
  so the loop never terminates. Wait on a PID or a sentinel file instead.
- `timeout N mame` sends a SIGTERM that MAME ignores when its autoboot
  script is blocked; without `-k` the process spins at 100% CPU forever.

Also gitignore vasm's default `a.out` output.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 12:49:32 -07:00
prosolis e4062ed294 Session 2: hybrid VQ codec, two quality profiles, three corrections
Answers session 1's critical-path question. Flat 4x4 VQ at k=256 was prototyped
and REJECTED by eye: Dirk's face disintegrates and ink outlines break into
4-pixel stair-steps. The 256-colour palettised frame is excellent, so the
palette was never the problem -- block VQ was.

Replaced it with a Cinepak-style hybrid: each 4x4 block is SKIP, one 4x4
codeword, four 2x2 codewords, or RAW literal pixels, chosen per block by
rate-distortion. The RAW escape makes lam=0 pixel-exact (measured 0.00 dB loss),
so the quality knob spans lossless to heavily-compressed in one bitstream.

Per the user's decision, ships TWO quality profiles from that one codec, one
decoder and one bitstream -- only the rate knob differs:
  sasi  45 KB/s  lam=300  34.8 dB   stock 10MHz ACE/EXPERT
  scsi  75 KB/s  lam=100  35.9 dB   Super/XVI or CZ-6BS1

Three corrections to earlier numbers:

1. Session 1's "183 KB/s at 12fps" was a bad extrapolation. Halving the
   framerate does not halve the bitrate -- decimation roughly doubles the
   per-frame delta. Re-measured directly: 340 KB/s for session 1's own RLE,
   247 KB/s for changed-spans+deflate. The lossless floor is 319 MB.

2. A FOURTH false-good result, same family as the three in FINDINGS 4:
   k=1024 codebooks appeared to buy +2.4 dB free, because the rate model
   charged 1 byte for a 10-bit index. Charging the true cost reverses the
   verdict -- k=256 wins at every matched bitrate, and by 5 dB at the low end
   where the SASI profile lives. k=256 ships.

3. Stream inventory: the ~3-5MB clips are 1.2-1.7s, not ~60s, and some 60s
   streams are menus, not content. Any survey must classify before averaging.

Also cleared both candidate sources for the game-logic layer: the SNES project
is MIT and DirkSimple is zlib, so the arcade scene graph can be imported and
the two transcriptions diffed against each other.

Encoder is working end-to-end: extract.py -> vq/vq_hybrid/ratectl -> encode.py,
emitting a big-endian DLX1 container the 68000 can parse with plain moves.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 11:56:08 -07:00
reala-misaki 65112b9305 Session 1: hardware research, content measurement, codec decision, MAME harness
Verified GVRAM is one word-access per pixel in ALL color modes; chose 256-color
256x192 with movem.l bursts (page 1 sacrificed as double-buffer).

Measured 8 scenes from the Blu-ray source: blit costs under 8% of the 12fps
cycle budget, so I/O is the bottleneck, not CPU. Naive delta+RLE reaches only
3.2:1 (365 KB/s, 470MB) -> decision to use 4x4 vector quantization (~30 KB/s).

"Shot on twos" assumption failed: the transfer has zero duplicate frames, so
12fps requires explicit decimation.

Documents three false measurement results and their root causes (per-frame
Floyd-Steinberg dithering, temporal denoise, exact-match dedupe on noisy source).

MAME Lua injection harness works and is reusable for cycle-cost measurement;
the IOCS _B_READ disk benchmark is blocked returning -1.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 11:23:49 -07:00