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,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""How much of the bus does the 68000 decoder actually leave for a DMAC?
|
||||
|
||||
python3 tools/analysis/15_bus_occupancy.py [container.dlx] [--nframes N]
|
||||
|
||||
FINDINGS 29.6's DMAC idea only pays if the DMAC can find bus slots the CPU is
|
||||
not using. That is not a cycle count, it is a BUS count, and nothing in the tree
|
||||
had one.
|
||||
|
||||
Two sources, and the point is that they check each other:
|
||||
|
||||
DATA accesses MEASURED by tools/bench/c68k/c68k_bench, which counts every
|
||||
Read/Write callback the C68K core makes. Exact.
|
||||
INSTRUCTION DERIVED here by walking src/player/decode.s's straight-line
|
||||
prefetch paths in tools/bench/decode.lst and multiplying by the mode
|
||||
histogram. Not measurable from either emulator: MAME's core
|
||||
does not expose a fetch count and C68K reads opcodes straight
|
||||
through a host pointer with no callback.
|
||||
|
||||
If the derived DATA figure matches the measured one, the derived PREFETCH figure
|
||||
from the same walk is trustworthy too. That check is the first thing printed,
|
||||
and this script exits non-zero if it fails.
|
||||
|
||||
A 68000 bus cycle is 4 clocks, so a frame of C clocks holds C/4 bus slots.
|
||||
"""
|
||||
import sys, os, argparse, csv
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from dlx import DLX
|
||||
|
||||
BUS_CLK = 4
|
||||
|
||||
# --- straight-line path costs, read off tools/bench/decode.lst -------------
|
||||
# (instruction words, data bus cycles). A long access is two bus cycles on the
|
||||
# 68000's 16-bit bus; movem.l of N registers is 2N.
|
||||
#
|
||||
# dispatch move.b (a1),d0 / lsr.b / and.w #3 / beq .sk 6w, 1 read
|
||||
# + subq / beq .v1 -> 8w
|
||||
# + subq / bne .rw -> 10w
|
||||
# V4 body $10090..$100E2 = 82 B = 41w; 4 x (1 byte read
|
||||
# + movem.l 2 regs = 4 reads + 2 move.l = 4 writes) = 36
|
||||
# V1 body $100E2..$10106 = 36 B = 18w; 1 byte read
|
||||
# + movem.l 8 regs = 16 reads + 4 x movem.l 2 = 16 w = 33
|
||||
# RAW body $10106..$10164 = 94 B = 47w; 8 x (2 byte reads
|
||||
# + 1 move.l = 2 writes) = 32
|
||||
# .sk tail addq.l #8,a4 1w
|
||||
# BLOCK 0 has no lsr.b, so one of the four dispatches in a group is 1w cheaper.
|
||||
DISPATCH_SK, DISPATCH_V1, DISPATCH_V4 = 6, 8, 10
|
||||
BODY = {0: (0, 0), 1: (18, 33), 2: (41, 36), 3: (47, 32)}
|
||||
DISPATCH = {0: DISPATCH_SK, 1: DISPATCH_V1, 2: DISPATCH_V4, 3: DISPATCH_V4}
|
||||
SK_TAIL = 1
|
||||
GROUP_HEAD = 3 # tst.b (a1) 1w + beq allskip 2w
|
||||
GROUP_TAIL = 4 # addq.l #1,a1 / cmpa.l a5,a4 / bne byteloop
|
||||
ALLSKIP = 9 # the whole four-block fast path, tst.b included
|
||||
ROW_HEAD, ROW_TAIL = 3, 7
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_cpufit.dlx")
|
||||
ap.add_argument("--csv", default="tmp/c68k_frames.csv",
|
||||
help="per-frame output of tools/bench/c68k/run.sh")
|
||||
ap.add_argument("--nframes", type=int, default=None)
|
||||
a = ap.parse_args()
|
||||
if not os.path.exists(a.container):
|
||||
sys.exit(f"missing {a.container}")
|
||||
|
||||
d = DLX(a.container)
|
||||
meas = {}
|
||||
if os.path.exists(a.csv):
|
||||
for r in csv.DictReader(open(a.csv)):
|
||||
meas[int(r["frame"])] = (int(r["cycles"]),
|
||||
int(r["bus_reads"]) + int(r["bus_writes"]))
|
||||
NF = a.nframes or (max(meas) + 1 if meas else d.nframes)
|
||||
|
||||
pref_t, data_t, cyc_t = [], [], []
|
||||
for f in range(NF):
|
||||
m = d.modes(f).reshape(d.nby, d.nbx)
|
||||
pref = d.nby * (ROW_HEAD + ROW_TAIL)
|
||||
data = 0
|
||||
for by in range(d.nby):
|
||||
row = m[by]
|
||||
for gi in range(0, d.nbx, 4):
|
||||
g = row[gi:gi+4]
|
||||
if (g == 0).all():
|
||||
pref += ALLSKIP; data += 1
|
||||
continue
|
||||
pref += GROUP_HEAD + GROUP_TAIL - 1 # BLOCK 0 has no lsr.b
|
||||
data += 1
|
||||
for b in g:
|
||||
b = int(b)
|
||||
pw, pd = BODY[b]
|
||||
pref += DISPATCH[b] + pw + SK_TAIL
|
||||
data += 1 + pd
|
||||
pref_t.append(pref); data_t.append(data)
|
||||
cyc_t.append(meas.get(f, (0, 0))[0])
|
||||
|
||||
pref_t, data_t, cyc_t = map(np.array, (pref_t, data_t, cyc_t))
|
||||
|
||||
print(f"{a.container}: {NF} frames, {d.nb} blocks/frame\n")
|
||||
if meas:
|
||||
md = np.array([meas[f][1] for f in range(NF)])
|
||||
err = 100 * (data_t - md) / md
|
||||
print("CHECK -- derived DATA bus cycles against the C68K harness's measurement")
|
||||
print(f" measured mean {md.mean():>10,.0f} /frame")
|
||||
print(f" derived mean {data_t.mean():>10,.0f} /frame "
|
||||
f"error {err.mean():+.2f}% mean, {np.abs(err).max():.2f}% worst")
|
||||
if np.abs(err).max() > 2.0:
|
||||
sys.exit("\nFAIL: the path walk does not reproduce the measured data "
|
||||
"accesses, so its prefetch figure cannot be trusted either.")
|
||||
print(" the walk reproduces the measurement, so its prefetch count stands\n")
|
||||
|
||||
slots = cyc_t / BUS_CLK
|
||||
tot = pref_t + data_t
|
||||
print(f"{'':<22}{'mean':>12}{'median':>12}{'worst frame':>14}")
|
||||
for label, v in (("bus slots in a frame", slots),
|
||||
(" data accesses", data_t),
|
||||
(" instruction prefetch", pref_t),
|
||||
(" total bus cycles", tot)):
|
||||
print(f"{label:<22}{v.mean():>12,.0f}{np.median(v):>12,.0f}{v.max():>14,.0f}")
|
||||
occ = 100 * tot / slots
|
||||
print(f"{'bus OCCUPANCY':<22}{occ.mean():>11.1f}%{np.median(occ):>11.1f}%"
|
||||
f"{occ.max():>13.1f}%")
|
||||
free = slots - tot
|
||||
print(f"{'slots left for a DMAC':<22}{free.mean():>12,.0f}{np.median(free):>12,.0f}"
|
||||
f"{free.min():>14,.0f} (worst = fewest)")
|
||||
print(f"\nprefetch is {100*pref_t.sum()/tot.sum():.0f}% of the decoder's bus traffic: "
|
||||
f"the data-only\nfigure the harness prints understates occupancy by about 2x.")
|
||||
print(f"A DMAC painting spans at 8 clocks (2 bus cycles) per pixel could use at\n"
|
||||
f"most {free.mean()/2:,.0f} pixels' worth of the mean frame's spare slots "
|
||||
f"-- against {d.nb*16:,} pixels\nin a whole screen.")
|
||||
Reference in New Issue
Block a user