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
220 lines
10 KiB
Python
220 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Would letting the HD63450 paint the spans beat letting the 68000 do it?
|
|
|
|
python3 tools/analysis/14_dmac_chain.py [container.dlx] [--bus 488]
|
|
[--dma-px-bus 2] [--disk-bus-byte 1]
|
|
|
|
FINDINGS 29.6 called this the one lever that could move the CPU budget without
|
|
spending a byte, and left it uncosted. FINDINGS 30 measured the alternative --
|
|
the 68000 painting spans itself, 43.7 cycles per span + 9.152 per pixel. This
|
|
prices the two against each other, and the answer turns on a resource neither
|
|
section costed: the 68000's own LOCAL BUS.
|
|
|
|
FINDINGS 29's "the bus has 4x the headroom the CPU has" is about the SCSI pipe,
|
|
110 KB/s of 488. That is a different bus. The 68000's memory bus runs one 4-clock
|
|
cycle at a time and carries instruction prefetch as well as data, and
|
|
tools/analysis/15_bus_occupancy.py measures the decoder using 86.7% of it.
|
|
|
|
THE TWO DESIGNS ARE THE SAME CONTAINER. v6's record is {u32 absolute GVRAM
|
|
address, u16 jump displacement} = 6 bytes; an MC68450/HD63450 array-chaining
|
|
entry is {u32 memory address, u16 transfer count} = 6 bytes. Set the channel to
|
|
dual-address, direction device->memory, Sequence Control counting both addresses
|
|
up: MAR reloads per entry (the GVRAM destination), DAR walks the stream buffer,
|
|
MTC is the span's word count. The chain array IS the span table.
|
|
|
|
THE DMAC CONSTANTS ARE NOW SOURCED, and they killed the first answer. From the
|
|
MC68450 manual (Motorola, Jul 1989, bitsavers), Fig 4-25 sheet 4: a dual-address
|
|
WORD operand between two 16-bit ports is **9 clocks**, because note 2 gives the
|
|
DMAC 4-clock reads and **5-clock writes**. The 68000 writes in 4. So:
|
|
|
|
DMAC 9.000 clocks/pixel (datasheet)
|
|
v6 9.152 clocks/pixel (measured, FINDINGS 30)
|
|
|
|
A 1.7% difference. Session 10's first pass guessed 2 bus cycles = 8 clocks from
|
|
bus arithmetic and was 12% optimistic; the extra clock on every DMAC write is
|
|
the whole story. Per span, sequential array chaining costs 36 clocks (Fig 4-25
|
|
sheet 1) against v6's measured 43.7 -- the DMAC's one real edge, and it is small.
|
|
|
|
AND DMA DOES NOT OVERLAP. The 68000 has no cache and a two-word prefetch queue,
|
|
so it stalls as soon as another master takes the bus. Frame time is therefore
|
|
CPU + DMA, additive. Session 10's first pass used max(CPU, bus) and got 53/120
|
|
where the additive model gives 84/120; FINDINGS 35's flat debit was right.
|
|
|
|
So the only material difference left is v6's 24-pixel padding quantum -- and
|
|
that is a property of v6's unrolled chain, not of the CPU. The `v7 fine tail`
|
|
column prices fixing it in software instead, and as of session 11 that column
|
|
is MEASURED on the 68000 (blit.s v7, tools/bench/span.sh, FINDINGS 40) rather
|
|
than derived: 66.0 clocks per span + 9.143 per coarse pixel + 9.978 per fine
|
|
pixel, with a 2-pixel quantum that a run of 4x4 blocks pads to exactly.
|
|
"""
|
|
import sys, os, argparse
|
|
sys.path.insert(0, "tools/encoder")
|
|
sys.path.insert(0, "tools/analysis")
|
|
import numpy as np
|
|
from dlx import DLX
|
|
import buscost as B
|
|
|
|
FRAME_CYC = 833333.0
|
|
AUDIO_KBPS = 7.8
|
|
C_V1, C_V4, C_RAW = 299.9, 448.2, 400.4 # FINDINGS 28.2 (MEASURED)
|
|
C_SKIP_CLUSTERED, C_SKIP_MIXED = 13.25, 45.0
|
|
SPAN_BYTES_PX, SPAN_HDR = 2, 6
|
|
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_cpufit.dlx")
|
|
ap.add_argument("--bus", type=float, default=488.0, help="SCSI pipe, KB/s")
|
|
ap.add_argument("--fps", type=float, default=12.0)
|
|
ap.add_argument("--dma-px-clk", type=float, default=B.DMA_PX_CLK,
|
|
help="clocks the DMAC spends per pixel, dual-address word "
|
|
"between two 16-bit ports. 9 is the DATASHEET figure "
|
|
"(MC68450 Fig 4-25 sheet 4).")
|
|
ap.add_argument("--disk-clk-word", type=float, default=8.0,
|
|
help="clocks the SCSI DMA steals per word. The datasheet "
|
|
"brackets it at 5 (DMAC holds the bus) to 12 (arbitrates "
|
|
"per word); FINDINGS 5's estimate of 8 is the midpoint.")
|
|
a = ap.parse_args()
|
|
if not os.path.exists(a.container):
|
|
sys.exit(f"missing {a.container}")
|
|
|
|
BYTE_BUD = (a.bus - AUDIO_KBPS) * 1024 / a.fps
|
|
BUS_SLOTS = FRAME_CYC / B.BUS_CLK
|
|
d = DLX(a.container)
|
|
BLK_C = {1: C_V1, 2: C_V4, 3: C_RAW}
|
|
BLK_B = {1: 1, 2: 4, 3: 16}
|
|
|
|
|
|
def runs(m, by):
|
|
dirty = m[by] != 0
|
|
i = 0
|
|
while i < d.nbx:
|
|
if not dirty[i]:
|
|
i += 1; continue
|
|
j = i
|
|
while j < d.nbx and dirty[j]:
|
|
j += 1
|
|
yield i, j
|
|
i = j
|
|
|
|
|
|
def span_cost(design, L):
|
|
"""(pixels carried, clocks charged to the frame) for a run of L blocks,
|
|
as 4 rows of 4L pixels. Every design is charged additively: the 68000
|
|
cannot execute while the DMAC owns the bus."""
|
|
if design == "v6":
|
|
px = B.pad24(4 * L)
|
|
return 4 * px, 4 * (B.V6_SPAN_CYC + px * B.V6_PX_CYC)
|
|
if design == "v7":
|
|
px, c = B.v7_span(4 * L)
|
|
return 4 * px, 4 * c
|
|
px = 4 * L
|
|
return 4 * px, 4 * (B.DMA_CHAIN_CLK + px * a.dma_px_clk)
|
|
|
|
|
|
def score(design):
|
|
"""Greedy, as 12_span_tradeoff.py: buy the best clocks-saved per byte spent
|
|
until the frame's byte budget is gone. Unlike 12, a spanned block still pays
|
|
its mode-map dispatch, which FINDINGS 30.7 flagged as uncounted."""
|
|
out = []
|
|
for f in range(d.nframes):
|
|
m = d.modes(f).reshape(d.nby, d.nbx)
|
|
byt = d.mode_bytes + sum(BLK_B.get(int(x), 0) for x in m.ravel())
|
|
spanned = np.zeros_like(m, bool)
|
|
span_clk = 0.0
|
|
|
|
cand = []
|
|
if design != "none":
|
|
for by in range(d.nby):
|
|
for i, j in runs(m, by):
|
|
L = j - i
|
|
cur_c = sum(BLK_C[int(b)] for b in m[by][i:j])
|
|
cur_b = sum(BLK_B[int(b)] for b in m[by][i:j])
|
|
px, sc = span_cost(design, L)
|
|
sc += L * C_SKIP_MIXED # the dispatch still happens
|
|
# v7 carries a second u16 (the fine displacement) per span.
|
|
hdr = B.V7_SPAN_HDR if design == "v7" else SPAN_HDR
|
|
span_b = 4 * hdr + px * SPAN_BYTES_PX
|
|
if sc < cur_c:
|
|
cand.append((cur_c - sc, span_b - cur_b, by, i, j, sc, L))
|
|
cand.sort(key=lambda s: -(s[0] / max(s[1], 1)))
|
|
for dc, db, by, i, j, sc, L in cand:
|
|
if byt + db <= BYTE_BUD:
|
|
byt += db
|
|
spanned[by][i:j] = True
|
|
span_clk += sc - L * C_SKIP_MIXED
|
|
g = m.copy()
|
|
g[spanned] = 0
|
|
gg = g.reshape(-1, 4)
|
|
allskip = (gg == 0).all(1)
|
|
cpu = allskip.sum() * 4 * C_SKIP_CLUSTERED
|
|
mm = gg[~allskip]
|
|
cpu += (mm == 0).sum() * C_SKIP_MIXED
|
|
for k, c in BLK_C.items():
|
|
cpu += (mm == k).sum() * c
|
|
pref, data = B.block_bus(m, spanned)
|
|
disk = byt / 2.0 * a.disk_clk_word
|
|
# additive: CPU work, then span painting, then the disk stealing the bus
|
|
out.append((cpu + span_clk + disk, (pref + data) * B.BUS_CLK, byt,
|
|
spanned.sum()))
|
|
return np.array(out).T
|
|
|
|
|
|
DESIGNS = [("today", "none"), ("v6 span", "v6"),
|
|
("v7 fine tail", "v7"), ("DMAC chain", "dmac")]
|
|
res = {n: score(k) for n, k in DESIGNS}
|
|
|
|
print(f"{a.container}: {d.nframes} frames, {d.nb} blocks, {a.fps:g} fps")
|
|
print(f"SCSI pipe {a.bus:.0f} KB/s -> {BYTE_BUD:,.0f} B/frame; "
|
|
f"68000 bus {BUS_SLOTS:,.0f} cycles/frame; CPU {FRAME_CYC:,.0f} clocks\n")
|
|
|
|
print("PER PIXEL AND PER SPAN -- datasheet against measurement")
|
|
print(f" DMAC dual-address word, two 16-bit ports {B.DMA_PX_CLK:.3f} clocks "
|
|
f"MC68450 Fig 4-25 sheet 4")
|
|
print(f" v6 movem chain {B.V6_PX_CYC:.3f} clocks "
|
|
f"MEASURED, FINDINGS 30")
|
|
print(f" -> the DMAC is {100*(B.V6_PX_CYC-B.DMA_PX_CLK)/B.V6_PX_CYC:+.1f}% per pixel. "
|
|
f"The 68000 writes in 4 clocks; the DMAC takes 5.")
|
|
print(f" per span: DMAC array chaining {B.DMA_CHAIN_CLK} clocks against v6's "
|
|
f"{B.V6_SPAN_CYC:.1f}\n")
|
|
|
|
w = 15
|
|
print(f"{'':<26}" + "".join(f"{n:>{w}}" for n, _ in DESIGNS))
|
|
def row(label, fmt, get):
|
|
print(f" {label:<24}" + "".join(f"{fmt(get(res[n])):>{w}}" for n, _ in DESIGNS))
|
|
|
|
row("bitrate KB/s", lambda v: f"{v:.1f}", lambda r: r[2].mean() * a.fps / 1024)
|
|
row("frame, median", lambda v: f"{v:.1f}%", lambda r: 100*np.median(r[0])/FRAME_CYC)
|
|
row("frame, worst", lambda v: f"{v:.1f}%", lambda r: 100*r[0].max()/FRAME_CYC)
|
|
row("frames missing", lambda v: f"{v}/{d.nframes}",
|
|
lambda r: int((r[0] > FRAME_CYC).sum()))
|
|
row("blocks spanned/frame", lambda v: f"{v:,.0f}", lambda r: r[3].mean())
|
|
print(f"\n ADDITIVE: frame = CPU + span painting + disk DMA. The 68000 has no"
|
|
f"\n cache and a two-word prefetch queue, so it stalls the moment another"
|
|
f"\n master takes the bus. Disk debited at {a.disk_clk_word:g} clocks/word.")
|
|
|
|
# What is left of the case, isolated.
|
|
v6m = int((res["v6 span"][0] > FRAME_CYC).sum())
|
|
finem = int((res["v7 fine tail"][0] > FRAME_CYC).sum())
|
|
dmam = int((res["DMAC chain"][0] > FRAME_CYC).sum())
|
|
print(f"\nWHAT THE DMAC ACTUALLY BUYS, decomposed")
|
|
print(f" v6 as built {v6m}/{d.nframes} frames over")
|
|
print(f" v7, a finer chain tail (MEASURED) {finem}/{d.nframes}")
|
|
print(f" DMAC chain {dmam}/{d.nframes}")
|
|
print(f" -> of the gap between v6 and the DMAC, "
|
|
f"{100*(v6m-finem)/max(v6m-dmam,1):.0f}% is the 24-pixel padding")
|
|
print(f" quantum, which is a property of v6's unrolled chain and fixable")
|
|
print(f" in software. The rest is 1.7% a pixel and 7.7 clocks a span.")
|
|
|
|
# The additive model here IS FINDINGS 35's flat debit, and reproduces its
|
|
# 84/120 exactly in the "today" column. Session 10's first pass replaced it with
|
|
# max(CPU, bus) and got 53/120; that was wrong, because a 68000 cannot execute
|
|
# while the DMAC holds the bus.
|
|
|
|
print(f"\nbreak-even against all-V1 ({C_V1:.1f} cycles/block), clocks per block")
|
|
print(f" {'L':<16}" + "".join(f"{L:>8}" for L in (1, 2, 3, 4, 8, 16, 64)))
|
|
for nm, dz in (("v6 as built", "v6"), ("v7 fine tail", "v7"), ("DMAC chain", "dmac")):
|
|
print(f" {nm:<16}" + "".join(f"{span_cost(dz, L)[1]/L:>8.0f}"
|
|
for L in (1, 2, 3, 4, 8, 16, 64)))
|
|
for nm, dz in (("v6 as built", "v6"), ("v7 fine tail", "v7"), ("DMAC chain", "dmac")):
|
|
brk = next((L for L in range(1, 65) if span_cost(dz, L)[1] < L * C_V1), None)
|
|
print(f" {nm:<16} beats all-V1 from L={brk} blocks up")
|