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
+122
View File
@@ -6310,3 +6310,125 @@ What the per-frame palette costs:
and `H.build` are what would actually ship the palette. The direction is
measured and the magnitude is about right; re-derive the per-frame figure
against the real builder before quoting it as the player's number.
## 62. One channel start paints a whole frame — the palette registers take the DMA (session 30)
**Emulated.** MAME 0.277, `x68000 -bios ipl10`, `-exp1 cz6bs1` with a
zero-filled `scsiexrom.bin`. No real hardware ran. 47.4 — does buffer mode blank
the layer — is still the board question and is still open, and **62.4 adds a
second board question this run created.**
61.9 left the decoder-free packed player one open structural item, ROADMAP K1:
a frame is a picture *and* a palette, and 61.2 had only ever pointed a channel
at GVRAM. If the palette registers at `$E82000` take a byte-wide DMA the way
GVRAM does in buffer mode, the palette is a 193rd array-chain entry and the
whole video path is **one channel start a frame**; if they do not, the CPU
writes 256 words a frame (61.9 derives ~2,370 clocks, 0.28% of a frame) and the
architecture stands anyway. It is the difference between cheap and free, and it
took one run to know which.
`src/player/dmagate.s` runs 79, gated by `tools/bench/dma_run.sh`, which
`check.sh` runs. **The answer is free, in this model.**
| run | what it does | result |
|---|---|---|
| **7. DMA → `$E82000`, bus held** | 512 B off the disc into the whole graphic palette | **byte-exact in 256 register words**, read back out of the registers by the 68000 |
| **8. the SAME transfer aimed at RAM** | the attribution control: `$2C000` instead | **byte-exact at `$2C000`, and 256 of 256 palette words still read the poison** |
| **9. ONE array-chained start** | 7 entries: the palette, then six picture rows at the 1,024 B line stride | **2,048 B byte-exact across BOTH kinds of destination**, `MAR` ended at `+5,376` |
In all three the discriminator reads as 59.1's and 61.2's do: **`MTC` sampled by
the instruction after `START` is 0 and the CPU went round its wait loop once** —
the transfer happened between two instructions, with the 68000 not executing.
### 62.1 The destination was POISONED first, because "it matches" has been a weak claim all along
Runs 46 wrote into RAM that was zero and GVRAM that was stale, against a record
that is mostly pad. A destination that could already hold the right answer
cannot distinguish a channel that wrote from a channel that did nothing — which
is run 4's could-not-fail trap wearing different clothes (61.2).
So `dg_poison` fills the palette with word *i* = `$A500|i` before each palette
run, written by the 68000 and read back by it. The host does not assume the
poison is a discriminator, it **counts**: `PALETTE POISON IS A DISCRIMINATOR:
511 of 512 positions differ from the disc's bytes`, and the gate refuses a run
where fewer than 500 do. One position coincides, and the pass does not rest on
it.
### 62.2 The control is an ATTRIBUTION control, not a mechanism one — and it says so
Run 5 could point at a mode bit; there is no mode bit here. What had to be
excluded is that run 7's palette held the disc's bytes for some reason other
than the channel having put them there — a readback that aliases somewhere else,
the SPC's own path touching the registers, the poison never having landed at
all. So run 8 is the same transfer with **one thing different, the destination
address**, and it makes two claims from one run:
- the disc's bytes appear at `$2C000`, so the transfer happened;
- **the palette still reads poison in all 256 words**, so what reached
`$E82000` in run 7 was decided by the channel's `MAR`.
The second claim is also the positive half: it shows the CPU's own writes reach
the registers the host reads back, so the readback path is not the thing under
test.
### 62.3 The 193rd entry is literal, and the array is SCENE-constant
Run 9 is the one that changes the architecture, and it is not "the palette
works" — it is that **one array chain crosses two kinds of destination**: device
registers at `$E82000` and video RAM at `$C14000`, in one start, with the CPU
halted from the first byte to the last. A frame is that shape with 192 row
entries instead of six.
**And the array does not have to be rebuilt per frame.** The row bases are
`$C00000 + row * 1024` and they do not change: the packed layout spends *both*
256-colour pages — page 0 is the low byte of a word and page 1 the high byte
(47.2, and 46.1's page masks) — so there is no spare page to flip into and no
alternate set of destinations to alternate between. The 193-entry array
(1,158 B) is built once at scene setup and started once a frame.
**What is left on the CPU in the video path is therefore the channel start and
the READ(10) that fetches the record — and neither is priced here.** The command
issue is already inside the transport's own account (58.2); the start is about a
dozen register writes and is DERIVED as small rather than measured. Do not quote
"no per-frame CPU work" without that sentence attached: it is no per-frame
*paint* work.
### 62.4 What this does NOT settle, and it is a NEW BOARD QUESTION
**MAME cannot discriminate here, and the reason is in its source.** The graphic
palette is not modelled as a register file at all: `x68k.cpp:817` maps
`$E82000-$E821FF` to `palette_device::read16/write16`, `emupal.cpp:417` forwards
to `memory_array::write16`, and `memarray.h:75` is a plain `COMBINE_DATA`. That
is **RAM that honours `mem_mask`** — so a byte write lands in its half by
construction, and a green run says *nothing in the model forbids it* rather than
*the board takes it*.
This is a different kind of bound from 61.2's. GVRAM has a real handler with a
real 256-colour arm, which is why run 5 could find a mechanism to fail on; the
palette has no handler to be wrong about. **What a byte write to a real
X68000 palette register does is UNMEASURED and this project has no figure for
it** — not folklore, not an estimate, an absence. It goes on the hardware list
as **B4**, and it is cheap: write `$A5` to `$E82000` and `$5A` to `$E82001` from
the CPU on a real board and read the word back.
**The blast radius if B4 comes back negative is small and known**: the palette
leaves the chain, the CPU writes 256 words a frame at 61.9's derived 0.28% of a
frame, and every other claim in 61 and 62 stands. Run 9's crossing would still
have to be re-asked, because it would no longer have a device-register end.
### 62.5 The chain's ORDER is a free choice with a visible consequence, and it is not decided
Run 9 puts the palette FIRST. It could as easily be 193rd, which is what 61.9
called it. The two are not equivalent on screen and neither is obviously right:
- **palette first** — the 192 rows of the *previous* frame are displayed under
the *new* palette until each is overwritten;
- **palette last** — the new frame's rows are displayed under the *old* palette
until the chain reaches the end.
The mismatch lasts one paint either way. Which is less visible depends on how
much the palette moves between consecutive frames, which is a property of the
encoder K2 has not been written yet, and the whole question is **moot if buffer
mode blanks the layer** (47.4/B2) because nothing is displayed during the paint
at all. **Filed, not answered.** It is named here so that the choice in the
final player is a decision rather than an accident of which run happened first.