Files
Dragon-s-Lair-X68k/tools/analysis/buscost.py
T
prosolis c520a89e14 Measure the finer chain tail: 84/120 becomes 18/120, and the derivation was right by cancellation
blit.s gains v7 -- v6's 24-pixel movem chain plus a second chain whose unit is
one `move.l (a0)+,(a2)+`. Measured over 13 span lengths by span.sh, every config
pixel-exact:

    cycles = 66.0 per span + 9.143 per COARSE pixel + 9.978 per FINE pixel

fitting all 13 to within 0.2%. v5 and v6 re-measure to FINDINGS 30 exactly, so
the harness has not drifted underneath the new variant.

Rescored against the same scsi window and the same additive model, v7 takes
84/120 frames over budget to 18/120 -- exactly what FINDINGS 39.4 derived, and
that agreement is two cancelling errors: the derivation's 2-register movem tail
is 29% too dear per pixel, and its "nothing per span" for the second chain entry
is 22.3 clocks too cheap. The plain post-incrementing move.l is the right tail
instruction, and it makes the padding quantum 2 pixels, which a run of 4x4
blocks pads to exactly zero.

The DMAC stays dropped on a measurement now rather than an argument: v7 takes
back 37 of the 43 frames the array chain would, with no reserved channel and no
timing neither emulator here can verify. Break-even against all-V1 moves from
L=4 blocks to L=2.

The fine displacement is carried mid-stream rather than in the span record, so
the decoder holds nothing across the copy and keeps all 12 payload registers --
which is the whole reason the coarse unit is 24 pixels.

span.sh is now -seconds_to_run 200 (30 s wall, 36 configs) and takes its
expected snapshot count from the generated metadata instead of a literal 23.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 19:02:23 -07:00

164 lines
7.5 KiB
Python

"""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
# --- v7: v6 with a finer tail (MEASURED, session 11, FINDINGS 40) ----------
# v6 pads every span up to 24 pixels because its unrolled chain is built from
# 12-register movem units, and FINDINGS 39.3 attributed 86% of the DMAC array
# chain's advantage over v6 to exactly that padding. v7 keeps the coarse chain
# and appends a second chain whose unit is one `move.l (a0)+,(a2)+` -- 2 pixels,
# so the quantum is 2 and a run of 4x4 blocks pads to NOTHING.
#
# Session 10 proposed a 2-REGISTER MOVEM tail (4 pixels, derived at 56 clocks)
# and that would have been the wrong instruction: movem.l (a0)+,d0-d1 plus
# movem.l d0-d1,(a2) plus the lea is 14 bus cycles for 4 pixels, where two plain
# move.l are 10. The plainest instruction on the machine wins the tail.
#
# The second entry point needs a second dispatch, and the fine displacement is
# carried MID-STREAM (after the coarse pixels, before the fine ones) rather than
# in the span record, so the decoder holds nothing extra across the copy and
# keeps all 12 payload registers. Costed as 2 more bytes per span.
#
# MEASURED by tools/bench/span.sh (blit.s v7, 13 span lengths, every config
# pixel-exact): cycles = 66.0/span + 9.143/coarse pixel + 9.978/fine pixel,
# fitting all 13 to within 0.2%.
V7_SPAN_CYC = 66.0 # MEASURED, FINDINGS 40
V7_CPX_CYC = 9.143 # MEASURED, FINDINGS 40 (24-pixel coarse unit)
V7_FPX_CYC = 9.978 # MEASURED, FINDINGS 40 (2-pixel fine unit)
V7_FINE_PX = 2
# Bus: per span v6's 9 plus a second {move.w (a0)+,d0 ; jmp} = 2 + 2.
# Per fine unit: move.l (a0)+,(a2)+ = 1 instruction word + 2 reads + 2 writes.
V7_SPAN_BUS = 13
V7_FINE_BUS = 5
V7_SPAN_HDR = 8 # {u32 address, u16 coarse disp} + u16 fine disp
def pad2(npix):
return -(-npix // V7_FINE_PX) * V7_FINE_PX
def v7_span(npix):
"""(pixels carried, CPU clocks) for a v7 span of npix pixels."""
k, r = divmod(pad2(npix), V6_UNIT_PX)
return (k * V6_UNIT_PX + r,
V7_SPAN_CYC + k * V6_UNIT_PX * V7_CPX_CYC + r * V7_FPX_CYC)
def v7_span_bus(npix):
"""Bus CYCLES a v7 span occupies -- instruction words plus data accesses."""
k, r = divmod(pad2(npix), V6_UNIT_PX)
return V7_SPAN_BUS + k * V6_UNIT_BUS + (r // V7_FINE_PX) * V7_FINE_BUS
def v6_span_bus(npix):
k = pad24(npix) // V6_UNIT_PX
return V6_SPAN_BUS + k * V6_UNIT_BUS