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.
+49 -9
View File
@@ -32,6 +32,16 @@ floor the codec is 110.4% and a decoder-free packed player is **55.2%**. It asks
much headroom the player has and started deciding WHICH PLAYER EXISTS**, and B2
stopped being a nice-to-have. The codec cannot be packed: 47.6.4 is closed and
the answer is no, both ways (61.3).
Amended end of session 30: **K1 IS DONE AND THE ANSWER IS THE GOOD ONE
(FINDINGS 62).** A channel writes the palette registers at `$E82000` byte-exact,
and ONE array-chained start crosses from device registers into GVRAM — so a
frame is a palette entry and 192 row entries, started once, with the CPU halted
throughout, and the array is scene-constant. **It opened B4**: MAME models the
palette as plain `COMBINE_DATA` storage with no handler to be wrong about, so
the run bounds the model and not the board, and what a real palette register
does with a byte write is UNMEASURED. B4 is the cheapest hardware item in the
project and a negative costs 0.28% of a frame. **K2, the packed container, is
next.**
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
tree — a decision point, two outcomes, a death clip — with audio, streaming from
@@ -119,6 +129,23 @@ the priority register `0xE82500` at `0x0000` (47.3).
> dark one. **B2 and B1 are now the same decision from two sides, and B2 is the
> five-minute half.**
**B4. Does a real palette register take a BYTE write?** (62.4, new session 30.)
`$E82000` is 256 16-bit registers. The decoder-free packed player's per-frame
palette rides the frame's array chain as a 193rd entry, which means a
dual-address channel with an 8-bit device port writes it **one byte at a time**,
even bytes into the high half of a register and odd into the low. **MAME cannot
be asked**: it maps the palette to `palette_device::read16/write16`, which is
`memory_array`'s `COMBINE_DATA` over plain storage — RAM that honours
`mem_mask`, with no handler that could refuse. So the run in 62 is a statement
about the model and not about the board.
**It is the cheapest item on this list, cheaper than B2**: write `$A5` to
`$E82000` and `$5A` to `$E82001` from the CPU and read the word back. If it
comes back `$A55A` the palette rides the chain. **The blast radius of a negative
is small and known** — the palette leaves the chain and the CPU writes 256 words
a frame at 61.9's derived 0.28% of a frame — which is why this is B4 and not
above B2.
**B3. Single-address vs dual-address DMA — and now, first, DOES THE CARD DRIVE
`#EXREQ`?** 242 KB/s and 0.69 dB was the old framing. **Session 27 promoted this
to the item that decides whether the design fits at all (59.7).** The frame
@@ -197,14 +224,26 @@ codec's 110.4%, and **+4.89 dB** on the shipping container because a literal
frame is not tied to a scene palette the codec's codewords index into. It costs
**582 KB/s, fixed, with no lever.** Three items, in order.
- **K1. Can a DMA channel write the palette registers at `$E82000`?** (61.9.)
The cheapest item in the project and the one with the largest structural
payoff: if the palette takes a byte-wide DMA the way GVRAM does in buffer mode,
it becomes a **193rd array-chain entry** and one channel start paints a whole
frame — palette included, CPU halted throughout, **no per-frame CPU work in the
video path at all**. Same shape as 61.2's three runs: a run in `dmagate.s`, a
host comparison in `dma.lua`, an assertion in `dma_run.sh`, **and a negative
control.**
~~**K1. Can a DMA channel write the palette registers at `$E82000`?**~~
**DONE, session 30 — FINDINGS 62. YES, in this model.** `dmagate.s` runs 79:
512 B off the disc into the whole graphic palette, **byte-exact in 256
register words** read back out of `$E82000` by the 68000; the same transfer
aimed at RAM leaving the palette as the CPU poisoned it, which is what
attributes the first run to the channel's `MAR`; and **ONE array-chained start
crossing from device registers into GVRAM**, which is the shape of a whole
frame. The destination is POISONED first (62.1) because "it matches" was a
weak claim against a record that is mostly pad, and the host counts the
poison's discriminating power rather than assuming it: 511 of 512.
**And the array is SCENE-constant** (62.3) — the packed layout spends both
256-colour pages, so there is no page to flip and the 193 destinations never
change; the 1,158 B array is built once at scene setup. What is left on the
CPU per frame in the video path is the channel start and the READ(10), and
neither is priced — say "no per-frame PAINT work", not "no per-frame CPU work".
**It opened B4** (62.4): MAME models the palette as a generic `palette_device`
over `memory_array`, whose `write16` is a plain `COMBINE_DATA`, so it has no
handler to be wrong about and cannot discriminate. And it filed one open
design choice, 62.5: palette FIRST or 193rd is visible on screen and is not
decided.
- **K2. A packed container.** 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. Simpler than a coded one:
@@ -222,7 +261,8 @@ DMAC-direct packed player has **no ring** — `ring.i`, `xfer.i` and most of
simplification that large usually hides something, and 61.7.2 names the specific
untested thing: a chained transfer has never run back to back at 12 fps.
**K1 and K2 survive a bad answer to B2. K3 does not.** Do K1 first.
**K1 and K2 survive a bad answer to B2. K3 does not.** ~~Do K1 first.~~ **K1 is
done (session 30, FINDINGS 62); K2 is next.**
---
+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