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:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Two emulators, one decoder: MAME's cycles against px68k's C68K core.
|
||||
|
||||
python3 tools/bench/c68k/compare.py [--mame tmp/mame_timed.log]
|
||||
[--c68k tmp/c68k.log]
|
||||
|
||||
WHY THIS EXISTS. Every 68000 cycle figure in FINDINGS 24-35 comes from one
|
||||
instrument. This puts a second, structurally different one next to it:
|
||||
|
||||
MAME 0.277 M68000 is the microcode core (src/devices/cpu/m68000/m68000.lst
|
||||
+ m68000gen.py), NOT Musashi -- timing emerges from the 68000's
|
||||
modelled micro-sequence and 4-clock bus cycles.
|
||||
C68K a static per-instruction cycle table hand-transcribed from the
|
||||
Motorola manual (ORI_CLOCKS_* / EA_CLOCKS_* in c68kmacro.h).
|
||||
|
||||
Those are two different ways of being right, so agreement is evidence and
|
||||
disagreement localises to whichever instruction the anchors separate. NEITHER
|
||||
charges GVRAM wait states, so both are the same lower bound on real hardware.
|
||||
"""
|
||||
import argparse, re, sys
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mame", default="tmp/mame_timed.log")
|
||||
ap.add_argument("--c68k", default="tmp/c68k.log")
|
||||
ap.add_argument("--meta", default="tmp/decode_meta.lua")
|
||||
a = ap.parse_args()
|
||||
|
||||
meta = open(a.meta).read()
|
||||
fps = int(re.search(r"fps=(\d+)", meta).group(1))
|
||||
budget = 10_000_000 / fps
|
||||
# anchor name -> stream offset, so the two logs can be joined: decode.lua
|
||||
# reports by name, the C68K harness by offset.
|
||||
names = {int(o): n for n, o in re.findall(r'name="([^"]+)", off=(\d+)', meta)}
|
||||
|
||||
mame = {}
|
||||
txt = open(a.mame, errors="replace").read()
|
||||
for nm, cyc in re.findall(r"\[DEC\] frame @ (.+?)\n.*?->\s+(\d+) cycles/frame", txt):
|
||||
mame[nm.strip()] = int(cyc)
|
||||
m_seq = re.search(r"full \d+-frame pass.*?\n.*?->\s+(\d+) cycles/frame", txt)
|
||||
|
||||
c68k, c_seq = {}, None
|
||||
for line in open(a.c68k, errors="replace"):
|
||||
m = re.search(r"anchor off=(\d+)\s+(\d+) cyc", line)
|
||||
if m and int(m.group(1)) in names:
|
||||
c68k[names[int(m.group(1))]] = int(m.group(2))
|
||||
m = re.search(r"sequential pass = (\d+) cyc, mean (\d+)", line)
|
||||
if m:
|
||||
c_seq = int(m.group(2))
|
||||
|
||||
if not mame:
|
||||
sys.exit(f"no MAME anchor timings in {a.mame} -- run decode.lua WITHOUT "
|
||||
f"DLX_VERIFY_ONLY=1 and give -seconds_to_run enough to finish")
|
||||
|
||||
w = max(len(n) for n in c68k) + 2
|
||||
print(f"{'anchor':<{w}}{'MAME':>10}{'C68K':>10}{'delta':>9} {'MAME':>7}{'C68K':>7} of a {fps}fps frame")
|
||||
rows = []
|
||||
for nm, c in c68k.items():
|
||||
m = mame.get(nm)
|
||||
if m is None:
|
||||
print(f"{nm:<{w}}{'--':>10}{c:>10}{'':>9} {'--':>7}{100*c/budget:>6.1f}% (MAME run did not reach it)")
|
||||
continue
|
||||
d = 100 * (c - m) / m
|
||||
rows.append(d)
|
||||
print(f"{nm:<{w}}{m:>10}{c:>10}{d:>+8.2f}% {100*m/budget:>6.1f}%{100*c/budget:>6.1f}%")
|
||||
|
||||
if m_seq and c_seq:
|
||||
m, c = int(m_seq.group(1)), c_seq
|
||||
d = 100 * (c - m) / m
|
||||
print(f"{'MEAN over the window':<{w}}{m:>10}{c:>10}{d:>+8.2f}% "
|
||||
f"{100*m/budget:>6.1f}%{100*c/budget:>6.1f}%")
|
||||
|
||||
if rows:
|
||||
print(f"\nspread over {len(rows)} anchors: {min(rows):+.2f}% .. {max(rows):+.2f}%")
|
||||
print("C68K reads HIGH throughout." if min(rows) > 0 else
|
||||
"C68K reads high on some anchors and low on others.")
|
||||
print("Neither instrument charges GVRAM wait states, so both are the same\n"
|
||||
"LOWER BOUND: this bounds cycle-table error, not the distance to a\n"
|
||||
"real X68000 (docs/BENCHMARK.md Tier 3).")
|
||||
Reference in New Issue
Block a user