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 @@
|
||||
"""Bus-cycle cost of src/player/decode.s and of tools/bench/blit.s's v6 spans.
|
||||
|
||||
A 68000 bus cycle is 4 clocks (S0-S7) with no wait states, and the 68000
|
||||
prefetches every instruction word over the same bus. So a block's bus cost is
|
||||
`instruction words + data accesses`, a long access counting twice on the 16-bit
|
||||
bus and `movem.l` of N registers counting 2N.
|
||||
|
||||
The per-path word counts are read off tools/bench/decode.lst and
|
||||
tools/bench/blit.s. tools/analysis/15_bus_occupancy.py checks the DATA half of
|
||||
this table against tools/bench/c68k/c68k_bench, which counts every bus callback
|
||||
the C68K core makes: they agree to 0.04%. The prefetch half cannot be measured
|
||||
from either emulator -- MAME does not expose a fetch count and C68K reads
|
||||
opcodes through a host pointer with no callback -- so it rests on that check.
|
||||
"""
|
||||
BUS_CLK = 4
|
||||
|
||||
# --- decode.s, per block ---------------------------------------------------
|
||||
# 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 = 4 reads + 2 move.l = 4 writes) = 36
|
||||
# V1 body $100E2..$10106 = 36 B = 18w; 1 byte read
|
||||
# + movem.l 8 = 16 reads + 4 x movem.l 2 = 16 wr = 33
|
||||
# RAW body $10106..$10164 = 94 B = 47w; 8 x (2 byte reads
|
||||
# + 1 move.l = 2 writes) = 32
|
||||
BODY = {0: (0, 0), 1: (18, 33), 2: (41, 36), 3: (47, 32)}
|
||||
DISPATCH = {0: 6, 1: 8, 2: 10, 3: 10}
|
||||
SK_TAIL = 1 # addq.l #8,a4
|
||||
GROUP_HEAD = 3 # tst.b (a1) + beq allskip
|
||||
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
|
||||
|
||||
# --- blit.s v6 spans -------------------------------------------------------
|
||||
# One chain unit moves 12 registers = 48 B = 24 pixels:
|
||||
# movem.l (a0)+,12 = 2w instr + 24 word reads = 26
|
||||
# movem.l 12,(a2) = 2w instr + 24 word writes = 26
|
||||
# lea 48(a2),a2 = 2w instr = 2
|
||||
# Per span: move.l (a0)+,a2 (1w + 2 reads) + move.w (a0)+,d0 (1w + 1 read)
|
||||
# + jmp v6ch(pc,d0.w) (2w) + dbra (2w) = 9
|
||||
V6_UNIT_PX = 24
|
||||
V6_UNIT_BUS = 54
|
||||
V6_SPAN_BUS = 9
|
||||
V6_SPAN_CYC = 43.7 # MEASURED, FINDINGS 30
|
||||
V6_PX_CYC = 9.152 # MEASURED, FINDINGS 30
|
||||
|
||||
# --- a DMAC array-chaining span -------------------------------------------
|
||||
# SOURCED, MC68450 Direct Memory Access Controller, Motorola, Jul 1989
|
||||
# (bitsavers). These replace session-10's first pass, which guessed 2 bus
|
||||
# cycles a pixel from bus arithmetic and was 12% optimistic.
|
||||
#
|
||||
# Fig 4-25 sheet 4, DUAL ADDRESS / OPERAND SIZE IS WORD / DEVICE SIZE IS
|
||||
# 16-BITS, D->M or M->D: {WORD READ, WORD WRITE} = 9 CLOCKS.
|
||||
# Confirmed by the long-operand row: two of each = 18 clocks.
|
||||
# Fig 4-25 note 2: reads are 4 clocks and WRITES ARE 5. That extra clock on
|
||||
# every write is the whole story -- it is why the DMAC does not beat a 68000
|
||||
# movem chain, which writes in 4.
|
||||
DMA_PX_CLK = 9
|
||||
# Fig 4-25 sheet 1, SEQUENTIAL ARRAY CHAINING: 36 CLOCKS per entry (three
|
||||
# word reads to fetch the 6-byte entry, plus reload).
|
||||
DMA_CHAIN_CLK = 36
|
||||
# Sect 4.5.2.1 front-end overhead 5 clocks best case, 8 worst; 4.5.2.2
|
||||
# back-end 2 clocks best. Once per period of bus ownership, not per span.
|
||||
DMA_FRONT_CLK, DMA_BACK_CLK = 5, 2
|
||||
# Fig 4-25 sheet 3, SINGLE ADDRESS: W/B READ 4 clocks, W/B WRITE 5 clocks.
|
||||
# A device->memory disk transfer is one memory WRITE = 5 clocks if the DMAC
|
||||
# holds the bus, or 5 + front + back = 12 if it arbitrates per word.
|
||||
# FINDINGS 5's long-standing 8 clk/word ESTIMATE sits inside that range.
|
||||
DMA_DISK_CLK_WORD_HELD, DMA_DISK_CLK_WORD_ARB = 5, 12
|
||||
|
||||
# The 68000 cannot execute while another master owns the bus: no cache, and a
|
||||
# two-word prefetch queue that empties immediately. So DMA time is ADDITIVE to
|
||||
# CPU time, not overlapped -- which is what FINDINGS 35's flat debit assumed
|
||||
# and session 10's first pass wrongly "refined".
|
||||
DMA_OVERLAPS = False
|
||||
|
||||
|
||||
def pad24(npix):
|
||||
return -(-npix // V6_UNIT_PX) * V6_UNIT_PX
|
||||
|
||||
|
||||
def block_bus(mode_map, spanned=None):
|
||||
"""(instruction words, data accesses) for one frame's CPU block decode.
|
||||
|
||||
`spanned` is a boolean array the same shape as mode_map marking blocks a
|
||||
span will paint instead; those blocks still cost their dispatch, because
|
||||
the mode map is walked either way, but not their body."""
|
||||
nby, nbx = mode_map.shape
|
||||
pref = nby * (ROW_HEAD + ROW_TAIL)
|
||||
data = 0
|
||||
for by in range(nby):
|
||||
row = mode_map[by]
|
||||
sp = spanned[by] if spanned is not None else None
|
||||
for gi in range(0, 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 k, b in enumerate(g):
|
||||
b = int(b)
|
||||
if sp is not None and sp[gi + k]:
|
||||
b = 0 # the span paints it
|
||||
pw, pd = BODY[b]
|
||||
pref += DISPATCH[b] + pw + SK_TAIL
|
||||
data += 1 + pd
|
||||
return pref, data
|
||||
|
||||
|
||||
# --- v6 with a finer tail (PROPOSAL, unmeasured -- Claude's, session 10) ----
|
||||
# v6 pads every span up to 24 pixels because its unrolled chain is built from
|
||||
# 12-register movem units. Adding a second, finer chain of 2-register units
|
||||
# (4 pixels) for the tail caps the padding at 3 pixels instead of 23, for the
|
||||
# price of some more unrolled code and nothing per span.
|
||||
# A 4-pixel unit: movem.l (a0)+,2 = 2w instr + 4 reads; movem.l 2,(a2) = 2w +
|
||||
# 4 writes; lea = 2w. 14 bus cycles for 4 pixels = 56 clocks, against a full
|
||||
# unit's 24 x 9.152 = 220 for 24. Dearer per pixel, paid at most once a span.
|
||||
V6_TAIL_PX, V6_TAIL_CLK = 4, 56
|
||||
|
||||
|
||||
def v6_fine(npix):
|
||||
"""(pixels carried, CPU clocks) for a span with the finer tail."""
|
||||
k, r = divmod(npix, V6_UNIT_PX)
|
||||
t = -(-r // V6_TAIL_PX)
|
||||
return (k * V6_UNIT_PX + t * V6_TAIL_PX,
|
||||
V6_SPAN_CYC + k * V6_UNIT_PX * V6_PX_CYC + t * V6_TAIL_CLK)
|
||||
Reference in New Issue
Block a user