A second emulator agrees, the bus was never counted, and the DMAC loses by one clock

Three things, and the last one reversed itself when the datasheet arrived.

A SECOND EMULATOR. tools/bench/c68k/ links px68k's C68K core into a headless
harness -- no SDL, no ROMs, no emulated machine, because the decoder touches
nothing but RAM, the control block and GVRAM. decode.s is now pixel-exact under
two independent CPU cores, and cycle-table error against MAME is bounded at
3.3%, running against us. MAME 0.277's M68000 turns out to be the MICROCODE
core, not Musashi (m68000.lst + m68000gen.py), so this is two structurally
different timing models agreeing rather than two tables. FINDINGS 28.8's "V4
costs more than RAW" reproduces independently. FINDINGS 37.

THE BUS. Nothing since FINDINGS 24 had counted the 68000's local memory bus --
one 4-clock cycle at a time, carrying instruction prefetch as well as data. The
decoder occupies 86.7% of it and PREFETCH IS 62% OF THAT TRAFFIC, so a data-only
count understates occupancy by 2x. Two sources check each other: c68k_bench
counts every bus callback exactly, and a static walk of decode.lst supplies the
prefetch no emulator here can report. The walk reproduces the measured data half
to 0.04%, which is what licenses its prefetch half, and 15_bus_occupancy.py is a
gate rather than a report because every bus figure depends on that check.
FINDINGS 38.

THE DMAC CHAIN LOSES. FINDINGS 29.6 named it the one uncosted lever. Costed from
bus arithmetic -- a read cycle plus a write cycle, 8 clocks a pixel -- it scored
1/120 frames over budget against the v6 span's 10/120 and looked decisive. Then
the MC68450 manual (Motorola Jul 1989, now at ~/src/mc68450.pdf): Fig 4-25 sheet
4 puts a dual-address word between two 16-bit ports at 9 CLOCKS, because note 2
gives the DMAC 4-clock reads and 5-clock WRITES. The 68000 writes in 4.

    DMAC   9.000 clocks/pixel   datasheet
    v6     9.152 clocks/pixel   measured, FINDINGS 30

1.7%. Scored additively, 86% of what remains of the DMAC's advantage is v6's
24-pixel padding quantum -- a property of its unrolled movem chain, fixable in
software with a finer tail chain, worth 55/120 -> 18/120 against the DMAC's
12/120. Recommendation: fix the quantum, drop the DMAC. Six frames does not buy
a reserved channel, a two-region container layout and a timing dependency
neither emulator here can verify. The container is identical either way -- v6's
record and an HD63450 chaining entry are both 6 bytes, so the chain array IS the
span table -- so nothing is foreclosed. FINDINGS 39.

TWO CORRECTIONS TO MY OWN WORK IN THE SAME SESSION:

- I argued FINDINGS 35's flat CPU debit for the disk was too pessimistic and
  rescored the window at 53/120 with max(CPU, bus). Wrong. A 68000 has no cache
  and a two-word prefetch queue, so it stalls the moment another master takes
  the bus, and the MC68450 hands the bus over in SLABS under limited-rate
  auto-request rather than interleaving per operand. DMA is additive. 84/120
  stands and 14_dmac_chain.py reproduces it exactly. What 86.7% occupancy really
  says is that there is almost no room to overlap anything. FINDINGS 38.3.
- The first DMAC costing was derived where a primary source existed. Both wrong
  answers were confident and both were caught by reading the manual.

Also landed:
- FINDINGS 5's 8 clocks/word for the SCSI DMA, STATUS's own "most load-bearing
  unmeasured number", is now bracketed by the datasheet: 5 clk/word with the bus
  held, ~12 if the DMAC arbitrates per word. 8 is a supported midpoint, and
  which end applies is a player design decision worth 7 clocks a word on a
  480 KB/s stream. FINDINGS 39.7.
- check.sh gains two gates: the C68K pixel-exact decode (seconds, no MAME) and
  the bus-model self-check. Both skip cleanly without a px68k checkout.
- spanned blocks are now charged their mode-map dispatch, which FINDINGS 30.7
  flagged as uncounted in 12_span_tradeoff.py.
- MAME timed runs must be budgeted by WALL CLOCK, not -seconds_to_run: this box
  runs x68000 at ~0.033x realtime and two runs were killed by their own timeout.
  That is why the all-RAW cell in 37.3 is empty. The C68K harness does the same
  work in seconds because it emulates a CPU and not a machine.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-23 18:30:23 -07:00
parent 7d365b3ff5
commit c5ca56330e
13 changed files with 1390 additions and 46 deletions
+91 -46
View File
@@ -1,47 +1,84 @@
# Status & next-session handoff — end of session 9 (2026-08-23)
# Status & next-session handoff — end of session 10 (2026-08-23)
## Where this stands
Session 9 did three things: dropped a profile on the user's instruction, closed
the last encoder gap, validated the cost model against the machine — and then
found that **the CPU budget every one of those numbers was scored against has
never had the disk in it**.
Session 10 cross-checked the whole cycle model against a second emulator, then
found that the model was denominated in the wrong currency.
**FINDINGS 35 is the headline, and it is bad news.** Raised by the user ("PIO is
such a CPU killer. DMA is not. I'm concerned about us drawing the wrong
conclusions"). Every CPU figure in FINDINGS 24-34 is measured against the full
833,333 cycles/frame, with nothing subtracted for moving the bitstream off SCSI.
Debiting the HD63450 cycle-steal at the long-standing 8 clocks/word estimate:
**FINDINGS 38 is the headline: the project is BUS-bound, not CPU-bound.** Nothing
since FINDINGS 24 had counted the 68000's local memory bus — one 4-clock cycle at
a time, carrying instruction prefetch as well as data. Measured, the decoder
occupies **86.7%** of it, and **prefetch is 62% of that traffic**. Scoring the
`scsi` window with the bus as the shared resource, **52 of 53 missed frames are
bus-limited and one is CPU-limited**. Every optimisation since 24 has been aimed
at the budget that is not binding.
| `scsi` container | budget left | median frame | worst | frames missing |
|---|---:|---:|---:|---:|
| no I/O — the pre-session-9 premise | 833,333 | 99.6% | 110.6% | **1/120** |
| **DMA** (8 clk/word, ESTIMATED) | 738,234 | **112.4%** | 124.8% | **84/120** |
| PIO (12 clk/B, hand-derived floor) | 548,036 | 151.4% | 168.1% | **120/120** |
The measurement is two sources checking each other: `c68k_bench` counts every bus
callback exactly, and a static walk of `decode.lst` adds the prefetch no emulator
here can report. The walk reproduces the measured data half to **0.04%**, which
is what licenses its prefetch half.
So "1 frame of 120 misses" was against the wrong budget. `11_cpu_budget.py` now
takes `--io dma|pio|none`, defaults to `dma`, and warns if asked for `none`.
**The DMAC array-chain LOSES, on the datasheet.** My first pass derived its cost
from bus arithmetic — a read cycle plus a write cycle, 8 clocks a pixel — and
scored it at 1/120 frames over budget against v6's 10/120. Then I read the
MC68450 manual (Motorola Jul 1989, bitsavers). **Fig 4-25 sheet 4: a
dual-address word between two 16-bit ports is 9 clocks, because note 2 gives the
DMAC 4-clock reads and 5-clock WRITES.** The 68000 writes in 4.
**The way out is 10 fps, and it works on paper**: same container, DMA debited,
10 fps -> median 93.7%, worst 104.0%, **1/120** — and that is conservative,
because it holds the 12 fps byte rate. FINDINGS 35.5.
| per pixel | clocks | source |
|---|---:|---|
| DMAC dual-address word | **9.000** | MC68450 Fig 4-25 sheet 4 |
| v6 `movem` chain | **9.152** | MEASURED, FINDINGS 30 |
**SASI is dropped (USER DECISION).** A SASI volume is 40 MB and the game's 22.8
minutes of unique footage is 146 MiB at the *lowest* rate this codec makes.
`scsi` is the only profile. Delivery is SD-backed SCSI, as locked in session 2 —
capacity does not bind there, and it is what rules CD-ROM out. FINDINGS 32.
Scored additively (see below), against the same mode maps:
**The encoder gap is closed.** `encode.py` emits **DLX2**, 4-byte-aligned frame
records: 94/120 record starts were on odd addresses, now 0/120, for 16 B/s.
Re-encoding reproduces FINDINGS 31.1 exactly. FINDINGS 33.
| | frames over budget |
|---|---:|
| today | 84/120 |
| v6 span as built | 55/120 |
| **v6 with a finer chain tail — software only** | **18/120** |
| DMAC chain | 12/120 |
**The cost model is validated on a cost-aware container** (STATUS item 1, done):
errors -3.07% to +0.01% against the 68000, whole-window mean -1.22%. It
under-predicts light frames and is exact on heavy ones. FINDINGS 34.
**86% of the DMAC's advantage is v6's 24-pixel padding quantum**, which is a
property of v6's unrolled `movem` chain and fixable in software: add a second
chain of 2-register units for the tail and the padding drops from 23 pixels to
3, at no per-span cost. **Recommendation (mine): fix the quantum, drop the
DMAC.** Six frames of 120 does not buy a reserved channel, a two-region
container layout, and a timing dependency neither emulator here can verify.
FINDINGS 39.
**A `scsi` window does not fit the 2 MB machine the rig emulates** — 2.84 MB of
stream against a 0x200000 ceiling. The decoder gate now verifies 80 of 120
frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
The container work is not wasted either way: v6's record and an HD63450 chaining
entry are both 6 bytes, so **the chain array is the span table** and nothing has
to change if a hardware measurement later moves the DMAC's number. FINDINGS 39.1.
**I also got FINDINGS 35 wrong before the datasheet corrected me.** I argued its
flat CPU debit was too pessimistic and rescored the window at 53/120 using
`max(CPU, bus)`. A 68000 has no cache and a two-word prefetch queue, so it
stalls the moment another master takes the bus, and the MC68450 hands the bus
over in *slabs* under limited-rate auto-request rather than interleaving per
operand. **DMA is additive. 84/120 stands.** What 86.7% bus occupancy really
says is that there is almost no room to overlap anything on this machine.
FINDINGS 38.3.
**And the datasheet settled STATUS's own "most load-bearing unmeasured number".**
FINDINGS 5's 8 clocks/word for the SCSI DMA: Fig 4-25 sheet 3 gives
single-address write 5 clocks, so it is **5 clk/word with the bus held and ~12
if the DMAC arbitrates per word**. 8 is the midpoint of a bracket the datasheet
supports. Which end applies is a player design decision — how the MB89352 drives
REQ, and whether cycle-steal-with-hold is used — worth 7 clocks a word on a
480 KB/s stream. FINDINGS 39.7.
**The decoder is now pixel-exact under two independent CPU cores.**
`tools/bench/c68k/` links px68k's C68K core into a headless harness and decodes
the same container to the same pixels. Cycle-table error against MAME is bounded
at **3.3%**, and it runs against us. Two incidental results worth keeping: MAME
0.277's `M68000` is the **microcode core**, not Musashi (`m68000.lst` +
`m68000gen.py`), so this is two structurally different timing models agreeing
rather than two tables; and FINDINGS 28.8's "V4 costs more than RAW" reproduces
independently. FINDINGS 37.
Everything below this line is from session 9 and still stands unless a session-10
section above says otherwise.
## NEXT SESSION, in order
@@ -58,13 +95,13 @@ frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
in the tree can only ever audit a prefix of a window.** Getting the coverage
back means gating on more than one window, not one longer pass.
1. **Measure the DMA cycle-steal, or derive it from primary sources.** This is
now the most load-bearing unmeasured number in the project: at 8 clocks/word
the port is marginal, at 12 it is dead, at 4 it is comfortable. It has been
an estimate from a session-2 datasheet reading since FINDINGS 5. MAME cannot
answer it (functional models, not transfer-timing accurate) — this needs the
HD63450 and MB89352 datasheets or real hardware. **Everything below is
contingent on it.**
1. **Measure the finer chain tail with `span.sh`.** It is the largest single win
on the table — 55/120 to 18/120 — it is software only, and the tool that
measures it already exists: `blit.s` gains a v7 with a 2-register tail chain,
`prep_spans.py` generates the streams, `span.sh` times all of it in ~25 s and
asserts every config drew a pixel-exact frame. The 18/120 figure is DERIVED
from a conservative 56-clocks-per-4-pixel-unit model and should not be
believed until it is measured. FINDINGS 39.4.
2. **Make sure the player actually gets DMA.** DMA-vs-PIO is a property of our
code, not the board: the CZ-6BS1's DMA path is real and modelled
@@ -79,12 +116,11 @@ frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
10 fps absorbs the DMA steal on current estimates. This was item 5 and a
quality question; it is now arithmetic. It is still the user's call.
4. **Then spans.** Fully specified by measurement (format FINDINGS 30.2, costs
30.5, scene-cut arithmetic 30.6). Note what 35.2 does to the case for them:
spans buy cycles by spending bandwidth, and the bandwidth comes back as
steal, so 31.6's "fits completely" becomes a worst frame of 114.3%. They are
still worth 14 points at the median — they are just no longer sufficient
alone.
4. **Then spans, on the CPU.** The format is fully specified by measurement
(FINDINGS 30.2, costs 30.5, scene-cut arithmetic 30.6) and `decode.s` does
not implement them yet. Build the v7 tail from item 1 into it. The DMAC
variant is costed and loses (39.5); the container is identical either way
(39.1), so nothing is foreclosed.
5. **Re-run the ring-buffer simulation at the surviving rate** and confirm the
488 KB/s figure's provenance (FINDINGS 29.5/30.7, still open).
@@ -742,6 +778,15 @@ with `extract.py`; the earlier ones lived in `/tmp` and do not survive a reboot.
## Reference material on this box (not in the repo)
- **MC68450 DMAC manual: `~/src/mc68450.pdf`** (Motorola, Jul 1989, from
bitsavers; `curl` it with a browser User-Agent or you get a 403). This is the
primary source for FINDINGS 39 and it overturned two derived answers in one
session. Fig 4-25 is the timing table — sheet 1 chaining, sheet 3
single-address, sheet 4 dual-address; note 2 (4-clock reads, 5-clock writes)
is the one that mattered. Sects 4.5.2.1-3 are the arbitration overheads,
5.2.3.2 the limited-rate auto-request slabs. `pdftotext` handles it.
- **px68k source: `~/src/px68k`** — only `m68000/c68k.c` is used, by
`tools/bench/c68k/`.
- **MAME 0.277 source: `~/src/mame-mame0277/`** (tarball `~/src/mame0277.tar.gz`).
Downloaded this session to settle the graphics-layer question. The files that
matter are `src/mame/sharp/x68k_v.cpp`, `x68k_crtc.cpp`, `x68k_crtc.h`,