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:
@@ -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 7–9, 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 4–6 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
@@ -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 7–9:
|
||||
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
@@ -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 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 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
|
||||
|
||||
Reference in New Issue
Block a user