src/player/decode.s now paints v7 literal spans, pixel-exact under MAME and px68k's C68K core over a container where every frame carries 128-216 spans covering up to 38% of the picture. The span pass is blit.s v7 verbatim: the 66.0/9.143/9.978 fit was measured on that instruction sequence. The container is DLX3 -- a span section between the mode header and the block payload, since that is the only place the 68000 can reach without first parsing something of variable length. 16_span_roundtrip.py gates it in check.sh, and asserts it emitted enough spans to have tested anything. Two synthetic all-SPAN anchors price v7 inside decode.s at 151.2 and 225.6 clocks per 4x4 block, against FINDINGS 40's table of 151 and 226 -- 0.2% on both emulators. The measured mode costs what it was said to cost. Two things that were not on the list: TWO BYTE BUDGETS. FINDINGS 40's 18/120 was scored against the 488 KB/s PIPE, not the 280 KB/s profile, and at the profile rate the lam search has already spent the allowance -- spans fired on 5 frames of 120 and looked like a regression. The profile is a chosen quality rate point; the pipe is hardware. --kbps and --span-kbps are now separate and spans run before mu, because a span pays in bytes and mu pays in picture. Delivered: 86/120 over budget without spans, 77/120 at the profile budget, 34/120 on the pipe for +0.36 dB. C_SKIP_MIXED WAS NEVER MEASURED, and it was 18% low -- 45.0, now 55.0. It is the one constant in the table that came from a derivation, because the synthetic frame that would measure it cannot exist: a byte needs a coded block for its SKIP to be mixed. Four bracketing anchors measure it on both emulators with the header byte rotated through all four positions, and the partner mode solves back to its own anchored value to 0.2%. With it corrected the model predicts a real spanned decode to -0.06% mean / 0.09% worst, against -2.99% / 4.30%. It matters because a span marks its run SKIP, so mixed SKIPs dominate exactly the frames spans are judged on. Also: the rig had been writing its synthetic timing frames 26 KB past the top of a 2 MB machine, and got away with it because the modes it overran are data-independent. A span's jump displacements come out of the stream, so it is not. And frames-over-budget is no longer a safe headline -- the controller aims at the deadline, so 55 of 120 frames sit within 5% of it and a 1% cost shift moves 22 frames. FINDINGS 41. check.sh ALL GREEN, now gating on a span-heavy DLX3 container. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
223 lines
10 KiB
Python
223 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
|
|
import vq_hybrid as _H
|
|
C_V1, C_V4, C_RAW = _H.C_V1, _H.C_V4, _H.C_RAW # FINDINGS 28.2 (MEASURED)
|
|
# 45.0 until session 12 measured it at 55.0 (FINDINGS 41.5) -- imported now, so
|
|
# the correction cannot be undone by a stale copy.
|
|
C_SKIP_CLUSTERED, C_SKIP_MIXED = _H.C_SKIP_CLUSTERED, _H.C_SKIP_MIXED
|
|
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")
|