From 07f36c2af9d25df560cf791bbeb861b0c0a86c83 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:06:44 -0700 Subject: [PATCH] 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 --- README.md | 16 +++ docs/FINDINGS.md | 122 +++++++++++++++++++++ docs/ROADMAP.md | 58 ++++++++-- docs/STATUS.md | 116 ++++++++++++++++++++ src/player/dmagate.s | 166 ++++++++++++++++++++++++++++- tools/analysis/29_packed_player.py | 36 ++++++- tools/bench/check.sh | 13 ++- tools/bench/dma.lua | 132 ++++++++++++++++++++--- tools/bench/dma_run.sh | 37 +++++++ 9 files changed, 664 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2209e54..7b2c1fd 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index 1e7b55f..7646529 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 15a09e6..21c7b77 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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.** --- diff --git a/docs/STATUS.md b/docs/STATUS.md index 0668202..fc43562 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 diff --git a/src/player/dmagate.s b/src/player/dmagate.s index fa0ab13..a57bde6 100644 --- a/src/player/dmagate.s +++ b/src/player/dmagate.s @@ -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" diff --git a/tools/analysis/29_packed_player.py b/tools/analysis/29_packed_player.py index 29ba072..3d656f8 100644 --- a/tools/analysis/29_packed_player.py +++ b/tools/analysis/29_packed_player.py @@ -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 diff --git a/tools/bench/check.sh b/tools/bench/check.sh index 3a297b8..da525c7 100755 --- a/tools/bench/check.sh +++ b/tools/bench/check.sh @@ -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" diff --git a/tools/bench/dma.lua b/tools/bench/dma.lua index 665bba7..277f586 100644 --- a/tools/bench/dma.lua +++ b/tools/bench/dma.lua @@ -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. diff --git a/tools/bench/dma_run.sh b/tools/bench/dma_run.sh index 9a5f26b..83e8a05 100755 --- a/tools/bench/dma_run.sh +++ b/tools/bench/dma_run.sh @@ -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