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
+283
View File
@@ -1428,6 +1428,11 @@ without closing it.
## 29. Trading bytes for cycles: the bus has 4x the headroom the CPU has (session 7)
> **ALSO SUPERSEDED IN PART BY 38.** "The bus has 4x the headroom the CPU has"
> is about the SCSI pipe. The 68000's LOCAL bus is a different resource and the
> decoder occupies 86.7% of it, so trading cycles for bytes is not free in the
> currency that turned out to bind. 29.6's DMAC idea is costed in 39.
>
> **SUPERSEDED IN PART BY 30, which measured it.** The mode survives and the
> conclusion holds, but every number in this section moved: a span costs 43.7
> cycles + 9.152/pixel *only* in an encoder-assisted format (the obvious
@@ -1978,6 +1983,14 @@ progress.**
## 35. The CPU budget has never had the disk in it (session 9)
> **TESTED BY 38 AND IT STANDS.** Session 10 first argued that the flat
> subtraction here is too pessimistic -- that the disk DMA could hide in bus
> cycles the CPU was not using -- and scored the same window at 53/120 instead
> of 84/120. **That was wrong.** A 68000 has no cache and a two-word prefetch
> queue, so it stalls as soon as another master takes the bus; DMA time is
> additive, which is exactly what this section assumed. The 84/120 stands and
> 38.3 now reproduces it.
**Raised by the user: "PIO is such a CPU killer. DMA is not. I'm concerned about
us drawing the wrong conclusions."** The concern is correct, and it is larger
than the labelling question of 32.4. This is the seventh false premise this
@@ -2129,3 +2142,273 @@ parsing lengths out of unmapped memory and walking wherever they pointed. Any
"the decoder is 4x slower than the model on RAW-heavy streams" conclusion drawn
from that run would have been entirely false, which is the third time in this
session that an unobservable run nearly produced a wrong finding.
## 37. A second emulator, and MAME is not running the core we thought (session 10)
Every 68000 cycle figure in FINDINGS 24-35 came from one instrument. This is a
second one, run against byte-for-byte the same `decode.bin` and the same
container.
`tools/bench/c68k/` links **px68k's C68K core** into a headless harness: a
hand-built X68000 memory map, no SDL, no ROMs, no emulated machine. The decoder
touches nothing but RAM, the control block and GVRAM, so the machine around it
was never part of the measurement.
### 37.1 What the two instruments actually are
**MAME 0.277's `M68000` is not Musashi.** `src/devices/cpu/m68000/m68000.lst`
plus `m68000gen.py`: it is the microcode core, where timing emerges from the
modelled micro-sequence and 4-clock bus cycles. C68K is a static per-instruction
cycle table (`ORI_CLOCKS_*` / `EA_CLOCKS_*` in `c68kmacro.h`), hand-transcribed
from the Motorola manual by a different author.
Those are two different ways of arriving at a number, which is what makes the
agreement worth something. It would be worth much less if both were tables.
### 37.2 The harness is self-validating
It decodes all 80 frames and dumps the screen; `verify_c68k.py` checks it
against `tools/encoder/dlx.py` **pixel for pixel, on palette indices**. That is
the licence for the cycle numbers: the harness rebuilds px68k's memory model
from scratch -- byte-swapped RAM (`mem_wrap.c:420`), GVRAM word writes that
discard the high byte -- and any of it being subtly wrong would still print
plausible cycles. It could not print a pixel-exact 80-frame temporal recursion.
It does. **`decode.s` is now pixel-exact under two independent CPU cores.**
### 37.3 The numbers
```
anchor MAME C68K delta MAME C68K of a 12fps frame
min non-SKIP 42.8% 600982 620760 +3.29% 72.1% 74.5%
median 65.2% 841038 869036 +3.33% 100.9% 104.3%
p90 72.9% 836124 856872 +2.48% 100.3% 102.8%
max non-SKIP 100.0% 921187 923090 +0.21% 110.5% 110.8%
synthetic all-SKIP 40729 40946 +0.53% 4.9% 4.9%
synthetic all-V1 921187 923090 +0.21% 110.5% 110.8%
synthetic all-V4 1376881 1420754 +3.19% 165.2% 170.5%
synthetic all-RAW -- 1273298 -- 152.8%
```
The `all-RAW` cell is empty because MAME's timed pass did not reach it. That is
an operational fact worth recording: with `-video soft -nothrottle` this box runs
`x68000` at about **0.033x realtime**, so `decode.lua`'s eight anchors plus two
full passes — ~48 emulated seconds — cost ~25 minutes of wall clock, and two runs
were killed by their own `timeout`. The C68K harness does the same work in
seconds because it emulates a CPU and not a machine. **Anchor MAME runs by wall
clock, not by `-seconds_to_run`.**
**Cycle-table error is bounded at 3.3%, and it runs against us** -- C68K reads
high on every anchor. Nothing here rescues FINDINGS 35. The disagreement is
mode-dependent (all-V1 +0.21%, all-V4 +3.19%), so it localises to the V4 path's
indexed two-register `movem.l`, not to a systematic clock difference.
**FINDINGS 28.8 is confirmed independently**: under C68K, V4 (170.5%) still
costs more than RAW (152.8%). That conclusion inverts the encoder's mode
preference, so having it from a second core matters more than most.
### 37.4 What it does not settle
px68k has no bus-timing model anywhere in `x68k/*.c` -- grep it. Neither
instrument charges GVRAM wait states, so this is **the same lower bound,
measured twice**. It bounds cycle-table error. It says nothing about the
distance to a real X68000; that is still BENCHMARK.md Tier 3.
### 37.5 One trap, recorded because it will catch the next person
C68K is 64-bit-unsafe by construction: its `MOVEM` macros do
`src = (UINT32)(&D0)` -- they truncate the host address of the register file and
dereference it -- and `C68k_Set_Fetch` keeps the opcode-fetch base in a `UINT32`.
Under the default PIE the binary loads near `0x555555550000` and the first
`movem` segfaults. The Makefile builds `-no-pie` and the harness mmaps its arena
`MAP_32BIT`. Both are load-bearing, not tidiness.
## 38. The bus, measured: the project is bus-bound, not CPU-bound (session 10)
> **This supersedes part of 29 and part of 35.** FINDINGS 29's "the bus has 4x
> the headroom the CPU has" is true of the SCSI pipe and false of the 68000's
> local bus, and they are different resources. FINDINGS 35's flat CPU debit for
> the disk charges the CPU for bus cycles it was not going to use.
Everything since FINDINGS 24 has been costed in CPU clocks. The 68000 has
another budget nobody had counted: its **memory bus**, one 4-clock cycle at a
time, carrying instruction prefetch as well as data.
### 38.1 Two sources that check each other
`tools/bench/c68k/c68k_bench` counts every Read/Write callback the C68K core
makes -- exact, because C68K splits a long access into two word calls, which is
what the 16-bit bus does. It cannot count **instruction prefetch**: C68K reads
opcodes straight through a host pointer with no callback, and MAME exposes no
fetch count either.
So `tools/analysis/15_bus_occupancy.py` derives prefetch by walking
`decode.s`'s straight-line paths in `tools/bench/decode.lst` and multiplying by
each frame's mode histogram. The same walk also predicts the data half -- and
that half is measurable:
```
measured mean 66,700 data bus cycles/frame
derived mean 66,672 error -0.04% mean, 0.06% worst
```
The walk reproduces the measurement, so its prefetch figure stands on the same
footing. `15_bus_occupancy.py` exits non-zero if that check ever stops holding.
### 38.2 The result
```
mean median worst frame
bus slots in a frame 201,497 211,013 230,772
data accesses 66,672 68,044 105,216
instruction prefetch 108,002 110,982 122,910
total bus cycles 174,674 181,998 193,248
bus OCCUPANCY 86.7% 86.8% 88.3%
slots left for a DMAC 26,823 26,618 21,115
```
**The decoder occupies 86.7% of its own bus, and prefetch is 62% of that.** A
data-only count understates occupancy by about 2x, which is exactly the mistake
an instrumented emulator would lead you into.
Per mode, bus clocks against measured clocks: V1 204/299.9 (68%), V4 308/448.2
(69%), RAW 316/400.4 (79%), and the v6 span **9.0/9.152 (98%)**.
### 38.3 What that does to the frame budget -- and one wrong turn
The first thing done with 86.7% was to argue that FINDINGS 35's flat CPU debit
for the disk is too pessimistic: the decoder leaves ~26,800 bus slots a frame
idle against the disk's ~23,000, so score it as contention,
`frame = max(CPU clocks, 4 x bus cycles)`, and the window misses 53/120 rather
than 84/120.
**That is wrong, and the MC68450 manual is what says so.** A 68000 relinquishes
the bus on BGACK and cannot execute without it -- no cache, a two-word prefetch
queue that empties immediately. Worse, the DMAC does not interleave at operand
granularity by default: limited-rate auto-request hands it the bus in *bursts*
of `2(BT+4)` clocks out of a sample period of `2(BT+BR+5)`, taking
`2^-(BR+1)` of the bandwidth in slabs (MC68450 sect 5.2.3.2, Fig 5-2). During a
slab the CPU is stopped.
So **DMA time is additive to CPU time**, which is what FINDINGS 35 assumed all
along. `14_dmac_chain.py` reproduces its 84/120 exactly in the `today` column.
What 86.7% *does* say is worse than the thing it appeared to rescue: **there is
almost no room to overlap anything.** The 13.3% of bus slots the decoder leaves
idle are single gaps inside a `movem`-heavy loop, not windows a bus master can
be handed. Any design whose case rests on DMA hiding under CPU work on this
machine should be assumed dead until measured on hardware.
The measurement still earns its place: it is what prices the span painter
against a DMAC in 39, and it is the reason the answer there came out the way it
did.
### 38.4 What is not counted
Bus arbitration. The 68000's BR/BG/BGACK handover costs cycles a cycle-steal
DMA cannot avoid, and the disk debit here embeds it only insofar as FINDINGS 5's
8 clocks/word already does. Also: no GVRAM wait states, as everywhere since 24.
Both make the real occupancy **higher** than 86.7%, not lower.
## 39. The DMAC chain against the span: the datasheet says no (session 10)
FINDINGS 29.6 named "let the DMAC do the copy" the one lever that could move the
budget without spending a byte, and left it uncosted. This costs it, and the
answer is **no** -- but only after the constants came from the MC68450 manual
rather than from bus arithmetic, which is the whole lesson of the section.
### 39.1 They are the same container
v6's record is `{u32 absolute GVRAM address, u16 jump displacement}` = 6 bytes.
An MC68450/HD63450 **array-chaining entry** is `{u32 memory address, u16
transfer count}` = 6 bytes. Set the channel dual-address, direction
device->memory, Sequence Control counting both addresses up: MAR reloads per
entry (the GVRAM destination), DAR walks the stream buffer, MTC is the span's
word count. **The chain array IS the span table.** Every byte figure in
FINDINGS 30 carries over, and this is not a fork in the format -- the encoder
emits the same thing either way, only the executor changes. That much is real
and survives everything below.
### 39.2 The first answer was wrong by a clock
Session 10 first derived the DMAC's cost from bus arithmetic: moving a pixel is
a read cycle plus a write cycle, 2 bus cycles, 8 clocks, against v6's measured
9.152 -- a 12.6% edge. On that basis the design scored 1/120 frames over budget
against v6's 10/120 and looked decisive.
The datasheet does not agree. **MC68450 Fig 4-25 sheet 4**, dual address /
operand size WORD / device size 16 bits, D->M or M->D:
`{WORD READ, WORD WRITE}` = **9 CLOCKS**. Confirmed by the long-operand row, two
of each for 18. And **Fig 4-25 note 2** says why: the DMAC's reads take four
clocks and its **writes take five**. The 68000 writes in four.
| per pixel | clocks | source |
|---|---:|---|
| DMAC, dual-address word, two 16-bit ports | **9.000** | MC68450 Fig 4-25 sheet 4 |
| v6 `movem` chain | **9.152** | MEASURED, FINDINGS 30 |
**1.7%.** One clock on every DMAC write is the entire difference between a
12.6% win and a rounding error. Per span, sequential array chaining costs
**36 clocks** (Fig 4-25 sheet 1: three word reads for the 6-byte entry, plus
reload) against v6's measured 43.7 -- the DMAC's one genuine edge, and it is
7.7 clocks.
### 39.3 Scored additively, as 38.3 requires
```
today v6 span v6 fine tail DMAC chain
bitrate KB/s 270.8 479.2 479.9 479.9
frame, median 108.1% 99.3% 96.5% 95.0%
frame, worst 114.7% 112.0% 111.4% 110.3%
frames missing 84/120 55/120 18/120 12/120
blocks spanned/frame 0 727 839 845
```
`today` reproduces FINDINGS 35's 84/120 exactly, which is the check that the
scenario lines up.
### 39.4 What the DMAC actually buys, and who else can sell it
`v6 fine tail` is the decomposition. v6 pads every span up to 24 pixels because
its copy is an unrolled chain of 12-register `movem` units; adding a second,
finer chain of 2-register units caps the padding at 3 pixels instead of 23, for
the price of some more unrolled code and **nothing per span**. Priced
conservatively (a 4-pixel unit costs 56 clocks against a full unit's 220 for 24,
so it is dearer per pixel and paid at most once a span):
| | frames over |
|---|---:|
| v6 as built | 55/120 |
| **v6 with a finer chain tail -- software only** | **18/120** |
| DMAC chain | 12/120 |
**86% of the DMAC's advantage over v6 is the 24-pixel padding quantum**, and
that is a property of v6's unrolled chain, not of the CPU. The residual is 1.7%
a pixel and 7.7 clocks a span, worth 6 frames of 120.
Break-even against all-V1 moves the same way: v6 as built needs a run of 4
blocks, v6 with the finer tail needs 3, the DMAC needs 1.
### 39.5 The verdict
**Fix the quantum in software.** Six frames of 120 does not buy a reserved DMAC
channel, a two-region container layout, and a dependency on transfer timing that
cannot be verified in either emulator on this box. The `v6 fine tail` figure is
itself DERIVED and should be measured with `span.sh` before it is believed --
that is a day's work in a tool that already exists, against a hardware
dependency that needs an actual X68000.
Keep 39.1 on the record. If a later measurement moves the DMAC's per-pixel cost
below 8 clocks -- for instance if GVRAM tolerates a four-clock DMAC write in a
way the datasheet's typical-system assumption does not model -- the container
does not have to change to take advantage of it.
### 39.6 What else would have to be true, if it is ever revisited
- **A free channel.** Four exist; channel 3 is ADPCM (`adpcm_drq_tick` asserts
`drq3_w`) and the SCSI stream needs one.
- **Two regions per frame.** Chaining fetches entries from an array while DAR
walks the pixel data, so the span table and the literal words cannot be
interleaved as v6 interleaves them.
- **The mode-map walk stays on the CPU.** 39.3 charges it; FINDINGS 30.7 flagged
that 12_span_tradeoff.py did not.
### 39.7 A number the datasheet settled on the way past
FINDINGS 5's **8 clocks/word** for the SCSI DMA has been an unsourced estimate
since session 1 and STATUS has called it the most load-bearing unmeasured number
in the project. Fig 4-25 sheet 3 gives single-address `W/B READ` 4 clocks and
`W/B WRITE` 5; a device->memory disk transfer is one memory write. So it is
**5 clocks/word if the DMAC holds the bus** and about **12 if it arbitrates per
word** (front-end 5 best case / 8 worst, sect 4.5.2.1; back-end 2, sect 4.5.2.2).
The feature list's "up to 5 Megabytes per Second at 10 MHz, no wait states"
is the held-bus case: 2 bytes per 4-clock cycle.
**8 is the midpoint of a bracket the datasheet supports, not a guess.** Which
end applies depends on how the MB89352 drives REQ and whether cycle-steal-with-
hold is used, which is a design decision the player has not made yet -- and it
is worth 7 clocks a word on a 480 KB/s stream, so it is worth making
deliberately.