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
+16
View File
@@ -240,6 +240,22 @@ the frame. The packed player is at 55.2% today. **The codec is kept on disk and
not built on**, because B2 is unanswered and 48.1's prior leans against packing —
if buffer mode blanks, it is the only thing left (48.3).
**And a frame is now one channel start.** Session 30 asked the packed player's
one open structural question: a frame is a picture *and* a palette, and nothing
had ever pointed a DMA channel at the palette registers. It writes them —
512 B off the disc byte-exact into 256 registers at `$E82000`, read back out of
the registers by the 68000 — and **one array-chained start crosses from those
registers into GVRAM**, which is the shape of a whole frame: a palette entry and
192 row entries, walked by the channel with the CPU halted throughout. 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 disc read; there is no per-frame *paint*. **What it
does not settle is the board** — MAME models the palette as plain
`COMBINE_DATA` storage with no handler that could refuse a byte write, so the
run bounds the model and not the hardware, and "does a real palette register
take a byte write" joins the hardware list as B4. A negative answer costs 0.28%
of a frame and nothing else (FINDINGS 62).
**The scene graph is in, and the worst gap between two decision points is
zero.** `tools/import/scenegraph.py` imports the arcade scene graph — 40 scenes,
516 sequences, 906 input windows — and 5.4% of the game's 612 branch transitions
+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
+163 -3
View File
@@ -37,13 +37,21 @@
; burst modelled as held.
DGFLAG = $18600 ; 0 idle / 1 done
DGREC = $18610 ; 6 x 32 B: rc, err, mtc0, spin, csr, cer, mtc, mar
DGREC = $18800 ; 9 x 32 B: rc, err, mtc0, spin, csr, cer, mtc, mar
; $18800 AND NOT $18610, WHERE THIS LIVED: nine
; records of 32 B run to $188FF, and from $18610
; they would have run over DGWIN at $18700 --
; the window run's own result, which run 10 then
; writes back. A silent overlap between two
; runs' evidence is the kind of thing that makes
; a gate report the wrong run's numbers.
DGREC_SZ = 32
DGWIN = $18700 ; u32 return of the WINDOWED dma read (want -1)
DGWERR = $18704 ; u32 SC_ERR after it (want SCE_WINDOW)
DGR20 = $18708 ; u32 R20 as it stood during run 4
DGR20N = $1870C ; u32 R20 as it stood during run 5 (the control)
DGR20C = $18710 ; u32 R20 as it stood during run 6 (chained)
DGR20P = $18714 ; u32 R20 as it stood during run 9 (palette+rows)
R20_BUF = $0916 ; 256-colour, 31.5 kHz, G-VRAM SET TO BUFFER
R20_MSK = $0116 ; the SAME, bit 11 CLEAR -- the negative control
DGDST4 = $C0C000 ; where the control writes
@@ -60,6 +68,25 @@ DGDST0 = $20000 ; PIO
DGDST1 = $24000 ; DMA, bus held
DGDST2 = $28000 ; DMA, cycle stealing
; ---- runs 7-9: THE PALETTE (ROADMAP K1, FINDINGS 61.9's first open item).
DGPAL = $E82000 ; the GRAPHIC palette: 256 words, GGGGGRRRRRBBBBBI
DGPALN = 256 ; words in it -- and 512 B is exactly ONE sector
DGPBLK = 1 ; so the whole palette is one block off the disc
DGPOIS = $A500 ; the poison: word i = DGPOIS|i. A palette that
; still reads this was not written by anything.
DGDST6 = $2C000 ; run 8's destination: RAM, so the palette is
; left alone and must still read poison
DGDST7 = $C14000 ; run 9's six GVRAM rows, at the 1024 B stride
DGCHA2 = $19100 ; run 9's array: 7 x {u32 MAR, u16 MTC}
DGCHN2 = 7 ; palette 512 B + 6 rows x 256 B = 2,048 B
DGCROW2 = 256
DGPS7 = $1A000 ; the palette as it stood after run 7 ...
DGPS8 = $1A200 ; ... after run 8 (the control: poison) ...
DGPS9 = $1A400 ; ... and after run 9 (chained). SNAPSHOTS, not
; a late read: each run overwrites the previous
; run's palette, so a host that looks once at
; the end sees only the last of the three.
org $10000
start:
clr.l DGFLAG.l
@@ -167,7 +194,7 @@ start:
; 256 B of a 1024 B line stride, so a frame is 192 destinations and not one.
; 46.6 said "no stride for a DMAC to skip" about the bytes WITHIN a row and left
; the rows themselves unexamined; a channel cannot skip 768 B any more than it
; could skip the 300 B in front of a record (run 7 below).
; could skip the 300 B in front of a record (run 10 below).
;
; The MC68450 answers this with SEQUENTIAL ARRAY CHAINING -- an array of 6-byte
; {u32 MAR, u16 MTC} entries it walks by itself -- and MAME implements it. So
@@ -199,7 +226,109 @@ dg_mkch:
bsr dg_save
clr.l DM_BARV.l ; and OFF again, so run 7 is unchained
; ---- 7. and a WINDOWED read through the channel, which must be REFUSED. This
; ---- 7. THE PALETTE. Can a channel write $E82000? (FINDINGS 61.9, ROADMAP
; K1.) Runs 4-6 put the PICTURE on the channel; a packed frame is a picture AND
; a palette, and if the palette registers take a byte-wide DMA the way GVRAM
; does in buffer mode then the palette is a 193rd array-chain entry and ONE
; channel start paints a whole frame -- no per-frame CPU work in the video path
; at all. If they do not, the CPU writes 256 words a frame (61.9 derives that
; at ~2,370 clocks, 0.28% of a frame) and the architecture still stands; this is
; the difference between cheap and free, and it is worth one run to know which.
;
; 512 B IS THE WHOLE PALETTE AND EXACTLY ONE SECTOR, which is why this run reads
; one block where the others read four: a transfer that ran long would write
; $E82200 (the TEXT palette) and then $E82400 (the video controller's own
; registers, priority included), and a probe that reconfigures the video
; controller as a side effect is not a probe.
;
; THE PALETTE IS POISONED FIRST, and that is what stops this being run 4's trap
; a second time. A destination that already holds the right bytes cannot tell a
; channel that wrote them from a channel that did nothing; RAM at $20000 was
; zero and the record is mostly pad, so "it matches" has been a weak claim all
; session. Word i is set to $A500|i, which no 512 B of container matches by
; accident, and the host reports how many of the 512 positions the poison and
; the disc actually differ in rather than assuming all of them.
bsr scsi_init
bsr dg_poison
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGPBLK,d4
lea DGPAL,a1
bsr scsi_read
lea DGPS7,a1
bsr dg_palsnap ; before run 8 overwrites it
lea DGREC+6*DGREC_SZ,a0
bsr dg_save
; ---- 8. THE NEGATIVE CONTROL, and it is an ATTRIBUTION control rather than a
; mechanism one. Run 5's control could point at a mode bit; there is no mode
; bit here, so what has to be excluded is that run 7's palette held the disc's
; bytes for some reason OTHER than the channel having written them there --
; a readback that aliases somewhere else, the SPC's own path touching the
; registers, the poison never having landed.
;
; Same transfer, same channel, same bytes; ONE thing different, the destination
; address. The disc's bytes must appear at $2C000, and the palette must still
; read poison in all 256 words. Two claims from one run, and the second is the
; one that makes run 7 mean anything: it also proves the CPU's poison writes
; reach the registers the host reads back, which is the positive half.
bsr scsi_init
bsr dg_poison
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGPBLK,d4
lea DGDST6,a1
bsr scsi_read
lea DGPS8,a1
bsr dg_palsnap ; must be poison, word for word
lea DGREC+7*DGREC_SZ,a0
bsr dg_save
; ---- 9. THE 193rd ENTRY: one start, the palette AND the picture rows. This is
; the run K1 exists for. Runs 7 and 8 only show that a channel can write the
; palette registers; what the architecture needs is that ONE array chain can
; cross from a hardware register area into GVRAM without the CPU between them,
; because a frame is a palette entry followed by 192 row entries and the whole
; claim is that the CPU starts the channel once.
;
; Seven entries, 2,048 B, out of the same LBA as everything else: 512 B into the
; palette and then six rows of 256 B at the 1024 B line stride. The destination
; regions are of two different KINDS -- device registers and video RAM in buffer
; mode -- which is exactly the crossing that has never been run.
bsr scsi_init
bsr dg_poison
move.w #R20_BUF,CRTC20.l
move.l #R20_BUF,DGR20P.l
lea DGCHA2,a0
move.l #DGPAL,(a0)+ ; entry 0: the palette, a whole sector
move.w #DGPALN*2,(a0)+
lea DGDST7,a1
moveq #DGCHN2-2,d5 ; the remaining six are picture rows
dg_mkch2:
move.l a1,(a0)+
move.w #DGCROW2,(a0)+
lea 1024(a1),a1
dbra d5,dg_mkch2
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR|$08,DM_OCRV.l ; OCR CHAIN = %10, array chain
move.l #DGCHA2,DM_BARV.l
move.l #DGCHN2,DM_BTCV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST7,a1 ; ignored under chaining, as in run 6
bsr scsi_read
lea DGPS9,a1
bsr dg_palsnap
lea DGREC+8*DGREC_SZ,a0
bsr dg_save
clr.l DM_BARV.l ; and OFF again, so run 10 is unchained
; ---- 10. and a WINDOWED read through the channel, which must be REFUSED. This
; is the one test here that is expected to fail, and it has to fail LOUDLY: the
; alternative is a channel writing a whole sector into a ring that has room for
; a record, over the top of records the decoder has not finished with.
@@ -240,4 +369,35 @@ dg_save:
clr.l DM_MARF.l
rts
; ---- the poison. Word i of the graphic palette <- $A500|i, written by the
; 68000 itself. Two jobs: a destination that cannot hold the right answer by
; accident, and a known pattern the control run reads back out of the registers
; to show the readback path shows what is actually in them.
dg_poison:
movem.l d0-d2/a0,-(sp)
lea DGPAL,a0
moveq #0,d1 ; i
move.w #DGPALN-1,d0
.p: move.w d1,d2
ori.w #DGPOIS,d2 ; $A500|i, and i never exceeds 255
move.w d2,(a0)+
addq.w #1,d1
dbra d0,.p
movem.l (sp)+,d0-d2/a0
rts
; ---- the palette as it stands, copied to (a1) by the 68000 READING THE
; REGISTERS. A snapshot and not a late host read: each palette run overwrites
; the last one's result, so all three have to be kept while they are true. It
; is also the same shape of evidence 53.3 used for pal_pack -- the words come
; back out of $E82000 rather than out of the buffer they were built in.
dg_palsnap:
movem.l d0/a0-a1,-(sp)
lea DGPAL,a0
move.w #DGPALN-1,d0
.s: move.w (a0)+,(a1)+
dbra d0,.s
movem.l (sp)+,d0/a0-a1
rts
include "src/player/scsi.i"
+32 -4
View File
@@ -330,10 +330,14 @@ print(f""" PSNR against the 24-bit source, 18_text_plane_16col.py over the same
one entry. The tree has already measured a reserved entry at
0.04 dB (60.3), so this is noise against {PSNR_FRAME_256-PSNR_SHIPPED:+.2f}.
NOT SETTLED, and it is the obvious next probe: whether a DMA CHANNEL can write
the palette registers at $E82000, which would make the palette a 193rd chain
entry and cost the CPU nothing at all. Untested; 61.2 only ever pointed a
channel at GVRAM.
SETTLED IN SESSION 30, AND THE ANSWER IS YES (FINDINGS 62): a channel writes
the palette registers at $E82000 byte-exact, and ONE array-chained start
crosses from those registers into GVRAM -- so the palette IS a 193rd chain
entry and the clocks row above is what the CPU pays only if it does the write
itself. dmagate.s runs 7-9. What that does NOT settle is the board: MAME maps
the palette to palette_device over memory_array, whose write16 is a plain
COMBINE_DATA, so there is no handler that could refuse a byte write and the
model cannot discriminate. ROADMAP B4.
AND THE PSNR FIGURES ARE PIL's MEDIANCUT, not this project's own palette
builder (vq.scene_palette / H.build). The DIRECTION is measured and the
@@ -344,6 +348,14 @@ print(f""" PSNR against the 24-bit source, 18_text_plane_16col.py over the same
w9 = 9.0
free_packed_dma = PACKED_BPF * w9 + CHAIN_CLK + AUD_CLK
free_packed_cpu = PACKED_BPF * w9 + blit[8] + AUD_CLK
# ... and the same two rows with the PER-FRAME PALETTE actually charged, which
# is what a player ships. The picture rows above are the comparison against the
# codec and are left alone so the published 55.2% / 81.6% do not drift; these
# are the shipping figures. Session 30 (FINDINGS 62) made the DMAC row's
# version legal: the palette is a 193rd chain ENTRY, so it costs 512 more
# delivered bytes and one more entry rather than 256 CPU word writes.
pal_dma = (PACKED_BPF + PAL_BYTES) * w9 + CHAIN_CLK + B.DMA_CHAIN_CLK + AUD_CLK
pal_cpu = (PACKED_BPF + PAL_BYTES) * w9 + blit[8] + pal_clk + AUD_CLK
codec_9 = codec_bpf * w9 + codec_decode + AUD_CLK
print("\n" + "=" * 78)
print(f"""THE ANSWER, AT THE ONE RUNG THIS MACHINE CAN BE SHOWN TO RUN (W=9)
@@ -352,6 +364,22 @@ print(f"""THE ANSWER, AT THE ONE RUNG THIS MACHINE CAN BE SHOWN TO RUN (W=9)
free / DMAC->GVRAM / PACKED {100*free_packed_dma/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_dma/FRAME_CLK:.0f}% to spare
free / CPU-painted / PACKED {100*free_packed_cpu/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_cpu/FRAME_CLK:.0f}% to spare
WITH THE PER-FRAME PALETTE CHARGED, which is what would ship:
DMAC-direct, palette on the CHAIN (62) {100*pal_dma/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s
CPU-painted, palette written by the CPU {100*pal_cpu/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s
The palette costs the same on the WIRE either way -- {PAL_BYTES} B a frame,
+{100*PAL_BYTES/PACKED_BPF:.1f}% -- and the wire is where this design is expensive. The gap
between the two rows is the PAINT, not the palette.
What session 30 bought is smaller than either and is worth stating exactly:
{pal_clk:,.0f} CPU clocks of palette writing replaced by one more chain entry at
{B.DMA_CHAIN_CLK} clocks, a net {100*(pal_clk-B.DMA_CHAIN_CLK)/FRAME_CLK:.2f}% of a frame -- plus the structural half,
which is that the video path then contains no per-frame PAINT at all. The
CPU still issues the READ(10) and starts the channel, and neither of those
is priced anywhere in this tree.
THE DECODER-FREE PACKED PLAYER FITS THE CLOCK BUDGET THAT THE CODEC MISSES.
That is not a small correction to 47.5, it is the reverse of the reason the
codec exists. 44.7 said it in advance and on a different cost model: "the
+12 -1
View File
@@ -550,6 +550,16 @@ echo "--- session 27: the DMAC drives the data phase, and holds the bus (FINDING
# again, so the run asserts the contrast and not just the held value;
# * the channel's own CSR/CER/MTC/MAR, which must say it moved every byte
# without error;
# * THE PALETTE REGISTERS AT $E82000 (session 30, ROADMAP K1): the same
# transfer aimed at the palette, byte-exact into 256 register words read
# back by the 68000; the SAME transfer aimed 20 KB away 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 the registers
# into GVRAM, which is the shape of a whole frame -- a palette entry and
# 192 row entries, started once. What this does NOT settle is the board:
# MAME models the palette as a generic palette_device over memory_array,
# whose write16 is a plain COMBINE_DATA, so it cannot tell a register file
# that takes byte writes from one that does not (FINDINGS 62.4);
# * and a WINDOWED read through the channel REFUSED. 117 of 120 records start
# part way into a sector (58.3); a channel writes a contiguous run and cannot
# drop the bytes in front of one, so it would write the neighbouring records
@@ -565,7 +575,8 @@ if command -v chdman > /dev/null; then
bash tools/bench/dma_run.sh "$DLX" > tmp/dma_gate.log 2>&1 || {
echo "FAIL: the DMAC did not drive the SCSI data phase."
tail -16 tmp/dma_gate.log; exit 1; }
grep -aE "BYTES OK|MTC one instruction|trips round|REFUSED" tmp/dma_gate.log \
grep -aE "BYTES OK|MTC one instruction|trips round|REFUSED|PALETTE|ONE START" \
tmp/dma_gate.log \
| sed 's/^ *//;s/^/ /'
else
echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume"
+117 -15
View File
@@ -24,25 +24,40 @@ local SP = M.devices[":maincpu"].spaces["program"]
local function P(s) print("[DMA] "..s) end
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18610, 32
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18800, 32
local DGWIN, DGWERR, DGR20, DGR20N, DGR20C = 0x18700, 0x18704, 0x18708, 0x1870C, 0x18710
local DGR20P = 0x18714
local CHROW, CHN, CHBASE = 256, 8, 0xC10000
local R20OF -- filled in after the mailbox addresses are known
local GV = 0xC00000
local DGLBA, DGBLK = 1000, 4
local DST = {0x20000, 0x24000, 0x28000, 0xC08000, 0xC0C000, 0xC10000}
-- the palette runs (ROADMAP K1). PS7/PS8/PS9 are the SNAPSHOTS dmagate.s takes
-- by reading $E82000 back with the 68000 after each run; the registers
-- themselves hold only the last of the three by the time the host looks.
local PAL, PALN, PALB, POIS = 0xE82000, 256, 512, 0xA500
local PS7, PS8, PS9 = 0x1A000, 0x1A200, 0x1A400
local CHROW2, CH2BASE, CHN2ROWS = 256, 0xC14000, 6
local DST = {0x20000, 0x24000, 0x28000, 0xC08000, 0xC0C000, 0xC10000,
PAL, 0x2C000, CH2BASE}
local NAME = {"PIO (the path FINDINGS 58 measured)",
"DMA, BUS HELD (DCR $00 burst, OCR $81 max rate)",
"DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)",
"DMA -> GVRAM (bus held, R20 bit 11 = BUFFER MODE) [47.6.2]",
"DMA -> GVRAM (the SAME, bit 11 CLEAR -- NEGATIVE CONTROL)",
"DMA -> GVRAM (ARRAY CHAINED, 8 rows at the 1024 B line stride)"}
local SHORT = {"pio", "held", "steal", "gvram", "masked", "chain"}
"DMA -> GVRAM (ARRAY CHAINED, 8 rows at the 1024 B line stride)",
"DMA -> PALETTE (bus held, 512 B into $E82000) [K1, 61.9]",
"DMA -> RAM (the SAME read aimed elsewhere -- NEGATIVE CONTROL:"
.." the palette must still read poison)",
"DMA -> PALETTE + SIX ROWS (ONE array-chained start across two"
.." kinds of destination)"}
local SHORT = {"pio", "held", "steal", "gvram", "masked", "chain",
"pal", "palctl", "palchain"}
local LENOF = {[6]=PALB, [7]=PALB} -- everything else is DGBLK*512
local ERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
"UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived",
"NON-ZERO SCSI STATUS",
"WINDOWED READ REFUSED -- a channel cannot drop bytes"}
R20OF = {[3]=DGR20, [4]=DGR20N, [5]=DGR20C}
R20OF = {[3]=DGR20, [4]=DGR20N, [5]=DGR20C, [8]=DGR20P}
local DISK = os.getenv("DLX_SCSI_IMG") or "dlxdisk.img"
local code do local f=io.open("dmagate.bin","rb"); code=f:read("a"); f:close() end
@@ -75,8 +90,8 @@ SUB = emu.add_machine_frame_notifier(function()
return
end
if not want then P("no "..DISK.." to check against"); P("done"); M:exit(); return end
local LEN = DGBLK*512
for i = 0, 5 do
for i = 0, 8 do
local LEN = LENOF[i] or DGBLK*512
local b = DGREC + i*DGREC_SZ
local rc = SP:read_u32(b)
local e = SP:read_u32(b+4)
@@ -101,21 +116,43 @@ SUB = emu.add_machine_frame_notifier(function()
local pg1, pg0, bad_hi, bad_lo = 0, 0, 0, 0
for k = 1, LEN do
local got
-- `a` is set for every destination that has to be read a WORD at a
-- time and split by hand -- GVRAM in buffer mode, and the palette
-- snapshots, whose words are what the 68000 read back out of
-- $E82000. Where it stays nil the destination is plain RAM.
local off, a
if i == 5 then
-- The chained run's destination is not linear: byte k of the
-- transfer is byte k%256 of row k//256, and the rows are a full
-- 1024 B line stride apart. If the channel had ignored the array
-- and run contiguously, every byte past the first row would be
-- in the wrong place and this comparison would say so.
local off = (k-1) % CHROW
local a = CHBASE + ((k-1) // CHROW) * 1024 + (off & ~1)
off = (k-1) % CHROW
a = CHBASE + ((k-1) // CHROW) * 1024 + (off & ~1)
elseif i == 6 then
off = (k-1) % 2
a = PS7 + ((k-1) & ~1)
elseif i == 8 then
-- ONE transfer across two kinds of destination: the first sector
-- is the palette, the rest is six picture rows at the line
-- stride. The split is the array's, and this walks it the same
-- way the channel was told to.
if k <= PALB then
off = (k-1) % 2
a = PS9 + ((k-1) & ~1)
else
local idx = k - PALB - 1
off = idx % 2
a = CH2BASE + (idx // CHROW2) * 1024 + ((idx % CHROW2) & ~1)
end
elseif i >= 3 and i ~= 7 then
off = (k-1) % 2
a = DST[i+1] + ((k-1) & ~1)
end
if a then
local w = SP:read_u16(a)
if (off % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
elseif i >= 3 then
local w = SP:read_u16(DST[i+1] + ((k-1) & ~1))
if ((k-1) % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
else
got = SP:read_u8(DST[i+1]+k-1)
end
@@ -125,14 +162,35 @@ SUB = emu.add_machine_frame_notifier(function()
else bad_lo = bad_lo + 1 end
end
end
if i >= 3 then
if R20OF[i] then
P(string.format(" R20 during the run = $%04X (bit 11 %s); %d bytes "
.."read back as page 1 (high half) and %d as page 0",
.."read back out of the HIGH half of a destination "
.."word and %d out of the LOW half",
SP:read_u32(R20OF[i]),
((SP:read_u32(R20OF[i]) & 0x0800) ~= 0)
and "SET" or "CLEAR",
pg1, pg0))
end
-- THE PALETTE RUNS' OWN VACUITY CHECK. Run 7's destination was
-- poisoned by the 68000 first, so "it matches the disc" cannot be
-- satisfied by a channel that did nothing -- but only if the poison
-- and the disc actually differ everywhere they are compared. That is
-- a property of THIS record and is counted rather than assumed.
if i == 6 then
local diff = 0
for j = 0, PALN-1 do
local w = (POIS | j) & 0xffff
if ((w >> 8) & 0xff) ~= string.byte(want, 2*j+1) then diff = diff + 1 end
if (w & 0xff) ~= string.byte(want, 2*j+2) then diff = diff + 1 end
end
P(string.format(" PALETTE POISON IS A DISCRIMINATOR: %d of %d "
.."positions differ from the disc's bytes -- a "
.."channel that wrote nothing could not have passed "
.."in those.", diff, PALB))
P(string.format(" %d bytes read back out of the HIGH half of a "
.."palette word (G and the top of R) and %d out of "
.."the LOW half", pg1, pg0))
end
if bad == 0 then
P(string.format(" BYTES OK: %d B from LBA %d match %s byte for byte "
.."[%s]", LEN, DGLBA, DISK, SHORT[i+1]))
@@ -152,6 +210,26 @@ SUB = emu.add_machine_frame_notifier(function()
P(" CONTROL DID NOT FAIL: the masked write path delivered every "
.."byte too, so the run above is not evidence about R20 bit 11.")
end
if i == 6 then
P(" A CHANNEL WRITES THE PALETTE REGISTERS: 512 B off the disc "
.."became 256 palette words, read back OUT OF $E82000 by the "
.."68000 itself, with the CPU halted for the transfer. Each "
.."disc byte landed in its own half of a register word, so a "
.."per-frame palette needs no CPU (61.9, ROADMAP K1).")
end
if i == 8 then
P(string.format(" ONE START PAINTED THE PALETTE AND %d ROWS: a "
.."single array-chained transfer crossed from device registers "
.."at $%06X into GVRAM at $%06X, %d B in %d entries, CPU halted "
.."throughout. A frame is that shape with %d row entries "
.."instead of %d.", CHN2ROWS, PAL, CH2BASE, LEN, CHN2ROWS+1,
192, CHN2ROWS))
end
elseif i == 6 or i == 8 then
P(string.format(" PALETTE WRONG [%s]: %d of %d differ, first at "
.."+%d -- %d at EVEN offsets (the HIGH half of a "
.."word), %d at ODD.",
SHORT[i+1], bad, LEN, first, bad_hi, bad_lo))
elseif i == 4 then
-- THE CLAIM IS NOT "half the bytes differ". In masked 256-colour
-- mode gvram_w takes `data & 0x00ff` and ignores mem_mask, so a byte
@@ -175,6 +253,30 @@ SUB = emu.add_machine_frame_notifier(function()
SHORT[i+1], bad, LEN, first))
end
end
if i == 7 then
-- THE ATTRIBUTION CONTROL'S SECOND CLAIM, and the one that makes run
-- 7 mean something: the same transfer aimed 20 KB away leaves the
-- palette exactly as the 68000 poisoned it. If this comes back with
-- the disc's bytes in it, something other than the channel's MAR
-- decides what reaches $E82000 and run 7 measured that instead.
local stale, first_s = 0, nil
for j = 0, PALN-1 do
if SP:read_u16(PS8 + 2*j) ~= ((POIS | j) & 0xffff) then
stale = stale + 1; first_s = first_s or j
end
end
if stale == 0 then
P(string.format(" PALETTE UNTOUCHED BY THE CONTROL: %d of %d words "
.."still read the poison the 68000 wrote, so the "
.."bytes in run 7 got there because the channel's "
.."MAR pointed at $%06X.", PALN, PALN, PAL))
else
P(string.format(" CONTROL DID NOT FAIL [palctl]: %d of %d palette "
.."words are no longer poison (first at entry %d) "
.."-- the palette changed without a channel aimed "
.."at it.", stale, PALN, first_s))
end
end
if i > 0 then
-- THE DISCRIMINATOR. MTC as the instruction after START saw it, and
-- the number of times the CPU went round its own wait loop.
+37
View File
@@ -108,6 +108,43 @@ grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[chain\]" tmp/dma_run.log || \
grep -aq "THE CHANNEL WALKED THE ARRAY ITSELF" tmp/dma_run.log || \
fail "the chained run did not report walking its own array."
# ---- THE PALETTE (ROADMAP K1, FINDINGS 61.9). If the registers at $E82000 take
# a byte-wide DMA the way GVRAM does in buffer mode, a per-frame palette is a
# 193rd array-chain entry and ONE channel start paints a whole frame; if they do
# not, the CPU writes 256 words a frame and the architecture still stands. The
# run is poisoned first and controlled twice -- once by aiming the same transfer
# elsewhere, once by counting how many of the 512 positions the poison and the
# disc actually differ in.
grep -aq "BYTES OK: 512 B from LBA 1000 .*\[pal\]" tmp/dma_run.log || \
fail "the channel did not write the palette registers at \$E82000 -- so a
per-frame palette costs the CPU 256 word writes and cannot ride the
frame's array chain (61.9). That is a RESULT, not a broken run: check the
PALETTE WRONG line above for whether the bytes were dropped or misplaced."
DIFF=$(sed -n 's/.*PALETTE POISON IS A DISCRIMINATOR: \([0-9]*\) of 512.*/\1/p' \
tmp/dma_run.log)
[ -n "$DIFF" ] && [ "$DIFF" -ge 500 ] || \
fail "the poison and the disc's bytes agree in ${DIFF:-?} of 512 positions, so
the palette run could have passed without a channel writing anything --
this is run 4's could-not-fail trap in a new place. Change DGPOIS."
grep -aq "BYTES OK: 512 B from LBA 1000 .*\[palctl\]" tmp/dma_run.log || \
fail "the ATTRIBUTION control's read did not land in RAM, so its palette claim
is about a transfer that did not happen."
grep -aq "PALETTE UNTOUCHED BY THE CONTROL: 256 of 256 words" tmp/dma_run.log || \
fail "the palette changed during a transfer aimed 20 KB away from it. Then
what reached \$E82000 in the run above was not decided by the channel's
MAR, and that run measured something else."
if grep -aq "CONTROL DID NOT FAIL \[palctl\]" tmp/dma_run.log
then
fail "the control reported its own failure -- see the line above it."
fi
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[palchain\]" tmp/dma_run.log || \
fail "ONE array-chained start could not cross from the palette registers into
GVRAM. A frame is one palette entry and 192 row entries; if the two kinds
of destination cannot share a chain, the CPU is back in the video path
once a frame to start the second half of it."
grep -aq "ONE START PAINTED THE PALETTE AND 6 ROWS" tmp/dma_run.log || \
fail "the palette+rows run did not report the crossing it exists to show."
grep -aq "WINDOWED DMA READ REFUSED" tmp/dma_run.log || \
fail "a WINDOWED read through the channel was not refused. 117 of 120 records
start part way into a sector (58.3), and a channel cannot drop the bytes