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
164 lines
7.5 KiB
Python
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
|