Put the palette on the channel, and find one start paints a whole frame

ROADMAP K1, the packed player's one open structural item.  A frame is a picture
AND a palette, and no run in this tree had pointed a DMA channel at the palette
registers.  dmagate.s runs 7-9, gated by dma_run.sh and check.sh:

  7. 512 B off the disc into $E82000, bus held -- byte-exact in 256 register
     words, read back OUT OF the registers by the 68000;
  8. the SAME transfer aimed at RAM -- byte-exact at $2C000, and 256 of 256
     palette words still read the poison the CPU wrote, which is what attributes
     run 7 to the channel's MAR rather than to the readback path;
  9. ONE array-chained start across two kinds of destination -- the palette and
     six picture rows at the 1,024 B line stride, 2,048 B byte-exact.

So a packed frame is one channel start: a 193-entry array, palette first, CPU
halted from the first byte to the last.  The array is scene-constant, because
the packed layout spends both 256-colour pages and there is no page to flip.
What is left on the CPU per frame in the video path is the channel start and the
READ(10) -- no per-frame PAINT, which is not the same claim as no per-frame CPU.

The destination is POISONED first (62.1).  Runs 4-6 wrote into RAM that was zero
and GVRAM that was stale against a record that is mostly pad; "it matches the
disc" was weaker than it read as.  The host counts whether the poison actually
discriminates instead of assuming it: 511 of 512, and the gate refuses under 500.

And it opened a hardware item (62.4, ROADMAP B4).  MAME maps the palette to
palette_device over memory_array, whose write16 is a plain COMBINE_DATA -- RAM
that honours mem_mask, with no handler that could refuse a byte write.  Unlike
GVRAM's 256-colour arm there is nothing here to be wrong about, so the run
bounds the model and not the board.  What a real palette register does with a
byte write is unmeasured.  A negative costs 0.28% of a frame and nothing else.

29_packed_player.py now also prints the two rows with the per-frame palette
charged -- 55.7% of a frame on the chain, 582 KB/s -- alongside the picture-only
figures the codec comparison is quoted against.

check.sh ALL GREEN before (tmp/check_s30_start.log) and after
(tmp/check_s30_end.log).

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 07:06:44 -07:00
parent 1be428c270
commit 07f36c2af9
9 changed files with 664 additions and 32 deletions
+116
View File
@@ -1,3 +1,119 @@
# Status & next-session handoff — end of session 30 (2026-08-25)
## Session 30: one channel start paints a whole frame, and the palette has no handler to be wrong about
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this (`tmp/check_s30_start.log`) and ALL GREEN after** — the same stages, with
three new runs inside the DMA gate.
**FINDINGS 62.** **Emulated** — MAME 0.277, `x68000 -bios ipl10`, `-exp1 cz6bs1`.
No real hardware ran. 47.4 — does buffer mode blank the layer — is still the
board question and is still open, and **this session opened a second one.**
**FIRST, THE UNRECORDED WORK WAS COMMITTED.** Sessions 28 and 29 were sitting in
a dirty tree with the last commit being session 27's. They are one commit now
(`1be428c`), because their edits share files and could not be split honestly
after the fact. Session 30's own work is a commit of its own.
**THE HEADLINE. ROADMAP K1 is done and the answer is the good one: a DMA channel
writes the palette registers at `$E82000`, and one array-chained start crosses
from device registers into GVRAM.** So a packed frame is **one channel start**:
a 193-entry array — the palette, then 192 picture rows at the 1,024 B line
stride — walked by the channel with the 68000 halted from the first byte to the
last. `dmagate.s` runs 7, 8 and 9; `dma_run.sh` gates all three; `check.sh` runs
it.
| run | result |
|---|---|
| **7. DMA → `$E82000`, bus held** | **512 B byte-exact in 256 register words**, read back out of the registers by the 68000 |
| **8. the SAME transfer aimed at RAM** | byte-exact at `$2C000`, and **256 of 256 palette words still poison** |
| **9. ONE array-chained start, palette + 6 rows** | **2,048 B byte-exact across BOTH kinds of destination**, `MAR` at `+5,376` |
**1. THE DESTINATION IS POISONED NOW, and it should have been all along** (62.1).
Runs 46 wrote into RAM that was zero and GVRAM that was stale, against a record
that is mostly pad — "it matches the disc" was a weaker claim than it read as.
`dg_poison` puts `$A500|i` in palette word *i* first, and the host **counts**
whether that actually discriminates rather than assuming it: 511 of 512
positions differ from the disc's bytes, and the gate refuses a run under 500.
**2. THE CONTROL IS AN ATTRIBUTION CONTROL, and it is a different shape from run
5's** (62.2). There is no mode bit here to point at. The same transfer aimed
20 KB away makes two claims at once: the bytes land at `$2C000`, and the palette
still reads poison — so what reached `$E82000` in run 7 was decided by the
channel's `MAR`. The second half is also the positive control on the readback
path, because the poison it finds is what the CPU wrote.
**3. THE ARRAY IS SCENE-CONSTANT** (62.3). The row bases never change: the
packed layout spends both 256-colour pages — page 0 the low byte of a word,
page 1 the high — so there is no page to flip into. The 1,158 B array is built
once at scene setup and started once a frame. **What is still on the CPU per
frame is the channel start and the READ(10)**, and neither is priced here: say
"no per-frame PAINT work", not "no per-frame CPU work".
**4. AND THE NEW BOARD QUESTION, B4** (62.4). **MAME cannot be asked this
one.** `x68k.cpp:817` maps the palette to `palette_device::read16/write16`,
`emupal.cpp:417` forwards to `memory_array::write16`, and `memarray.h:75` is a
plain `COMBINE_DATA` — RAM that honours `mem_mask`, with **no handler that could
refuse a byte write**. That is a different bound from 61.2's: GVRAM has a real
256-colour arm, which is why run 5 could find a mechanism to fail on. What a
real X68000 palette register does with a byte write is **UNMEASURED — not
folklore, an absence.** B4 is the cheapest hardware item in the project: write
`$A5` to `$E82000`, `$5A` to `$E82001`, read the word back. A negative costs
0.28% of a frame and nothing else, which is why it sits below B2.
**5. ONE DESIGN CHOICE IS FILED, NOT ANSWERED** (62.5). Palette FIRST or 193rd
is visible on screen — old rows under the new palette, or new rows under the old
one — for one paint either way. It is moot if buffer mode blanks (B2). It is
named so the final player's order is a decision and not an accident of which run
happened to be written first.
## HANDOFF — start here
**THE TREE IS ALL GREEN AND COMMITTED.** Two commits: `1be428c` (sessions 28+29,
unrecorded until now) and session 30's own.
### The work, in the order it should be done
**1. K2 — A PACKED CONTAINER.** Simpler than a coded one: quantise to **254**
colours (index 0 is the transparency key, black at 255 — 47.2), interleave
column *i* with *i+128*, emit a per-frame palette, sector-align the record. No
VQ, no rate control, no mode map, no `lam`. `encode.py` already sector-aligns
(DLX5) and `prep_frame.py --pack-transparent` already emits the exact pixel
layout. **Re-derive 61.9's 34.08 dB against `vq.scene_palette` / `H.build`
here** — the measured figure is PIL's MEDIANCUT and is a direction, not the
player's number (risk 2 below).
**2. K3 — END TO END, OFF THE DISC.** Scene setup — palette, page-1 X-scroll
384, priority `vc1 = 0x0002`, R20 bit 11 — then **one chained DMA a frame**,
now including the palette entry K1 just established. Gate it pixel-exact over
the same 120-frame window the decoder is gated on.
### What is PARKED, so it is not re-derived
**E7, E4 and C1** (61.8), and **P4a's wiring** for the video path: a DMAC-direct
packed player has no ring at all. Do not delete any of it — B2 is unanswered and
48.1's prior leans against packing.
### Risks that are OURS, not hardware
1. **A chained transfer has never run back to back at 12 fps.** Unchanged by
this session: run 9 was seven entries, once. A frame is 193 and a second is
twelve frames.
2. **34.08 dB is PIL's MEDIANCUT**, not `vq.scene_palette` / `H.build`.
3. **The packed player deletes `ring.i`, `xfer.i` and most of `stream.s` from
the video path.** A simplification that large usually hides something.
4. **B2 sits over all of it**, and now B4 sits over the palette entry
specifically — with a known, small blast radius.
### Reproducing this session
./tools/bench/check.sh # ALL GREEN, ~5 min
bash tools/bench/dma_run.sh # runs 1-10, with controls
**WHAT IS NEXT.** K2: the packed container.
---
# Status & next-session handoff — end of session 29 (2026-08-25)
## Session 29: the decoder-free packed player fits the budget the codec misses