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
171 lines
7.9 KiB
Python
171 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate V5 span streams for tools/bench/span.lua (FINDINGS 29.5 item 1).
|
|
|
|
FINDINGS 29 prices a new decoder mode -- a row-linear run of word-expanded
|
|
literal pixels, movem.l'd straight from the stream buffer into GVRAM -- at
|
|
`4 * (50 + 4L * 9.08)` cycles for a run of L blocks. Both halves of that are
|
|
extrapolations: the 50-cycle per-span overhead is hand-derived, and the 9.08
|
|
cycles/pixel was measured (FINDINGS 24 V1) at FULL ROW WIDTH with 12-register
|
|
bursts, which a short span cannot match. This script builds the stimulus that
|
|
replaces both numbers with measured ones.
|
|
|
|
One stream per span length. Every stream covers the SAME 192x256 picture
|
|
completely, so all of them draw an identical, verifiable frame and differ only
|
|
in how many spans it is cut into -- which is what lets span.lua regress
|
|
cycles = A * spans + B * pixels
|
|
across the set and read the per-span overhead off directly.
|
|
|
|
Two stream formats, both big-endian, both drawing the same frame.
|
|
|
|
v5 -- a decoder handed (x, npix) that works out the copy itself:
|
|
per row, 192 rows in order:
|
|
u16 nspans
|
|
nspans * { u16 x, u16 npix, npix * u16 pixel }
|
|
|
|
v6 -- the same spans with that arithmetic moved here, where it is free:
|
|
u16 nspans (whole frame; there is no row structure)
|
|
nspans * { u32 absolute GVRAM address, u16 jump displacement,
|
|
units * 48 bytes of pixels }
|
|
Span lengths are multiples of 24 pixels (one chain unit) and the last span
|
|
in a row may overrun the visible 256 by up to 23 pixels, which is free: the
|
|
line stride is 1024 bytes and only the first 512 are displayed. The jump
|
|
displacement selects an entry point into the decoder's unrolled copy chain.
|
|
|
|
v7 -- v6 plus a second, FINER chain for the tail (FINDINGS 39.4). Padding to
|
|
v6's 24-pixel quantum wastes ~11 pixels on an average span, and FINDINGS
|
|
39.3 attributes 86% of the DMAC array-chain's advantage over v6 to it. A
|
|
v7 span is 24*c + 2*f pixels, so the quantum is 2 and a run of 4x4 blocks
|
|
(always a multiple of 4 pixels) pads to NOTHING:
|
|
u16 nspans
|
|
nspans * { u32 absolute GVRAM address, u16 coarse displacement,
|
|
c * 48 bytes of pixels,
|
|
u16 fine displacement, f * 4 bytes of pixels }
|
|
The fine displacement is in the STREAM rather than the record because that
|
|
is what lets the decoder keep all 12 payload registers: the coarse chain
|
|
falls out into a `move.w (a0)+,d0 / jmp` with d0 dead and a0 pointing at
|
|
it. Costed here as an 8-byte record, since it is 2 more bytes a span.
|
|
|
|
Pixels are word-expanded with the palette index in the low byte; the high byte
|
|
is whatever we put there because gvram_w masks it off (x68k_crtc.cpp:501).
|
|
"""
|
|
import struct, sys
|
|
import numpy as np
|
|
|
|
SRC = sys.argv[1] if len(sys.argv) > 1 else "tmp/frame256.bin"
|
|
OUT = sys.argv[2] if len(sys.argv) > 2 else "tmp/spans.bin"
|
|
META = OUT.replace(".bin", "_meta.lua")
|
|
|
|
d = open(SRC, "rb").read()
|
|
assert d[:4] == b"DLXR", SRC
|
|
W, H = struct.unpack(">HH", d[4:8])
|
|
idx = np.frombuffer(d[8+768:8+768+W*H], np.uint8).reshape(H, W)
|
|
assert (W, H) == (256, 192), f"{W}x{H}: span bench assumes the 256x192 picture"
|
|
|
|
# (span length in pixels, x of the first span). 4 px = one 4x4 block wide, the
|
|
# case the whole FINDINGS 29 argument turns on; 256 = one span per row, the
|
|
# case closest to the V1 measurement it extrapolates from. 16u starts at an
|
|
# odd x so its bursts run at addr mod 4 == 2: a claim about the 68000's 16-bit
|
|
# bus that costs nothing to test and would be embarrassing to assume.
|
|
CONFIGS = [(4, 0), (8, 0), (12, 0), (16, 0), (16, 1), (20, 0), (24, 0),
|
|
(32, 0), (48, 0), (64, 0), (128, 0), (256, 0)]
|
|
|
|
# v6 geometry, and it must match blit.s: 12 registers per movem = 48 bytes =
|
|
# 24 pixels per chain unit, 11 units in the chain.
|
|
UNITPX, UNITSZ, UNITS = 24, 12, 11
|
|
# v7 geometry, and it must match blit.s: coarse unit as v6, fine unit is one
|
|
# `move.l (a0)+,(a2)+` = 2 bytes of code = 2 pixels, 11 of them (22 px > 24).
|
|
FINEPX, FINESZ, FINES = 2, 2, 11
|
|
GVRAM, YOFF, STRIDE = 0xC00000, 32, 1024
|
|
|
|
# v7 span lengths, in pixels. Multiples of 4 (a real span is a run of 4x4
|
|
# blocks), chosen so the fine remainder P mod 24 takes every value a real span
|
|
# can: 0, 4, 8, 12, 16, 20. 4/8/12/16/20 are pure-fine, 24/48/72/120/240 are
|
|
# pure-coarse, the rest mix -- which is what makes the three-term fit
|
|
# cycles = A*spans + Bc*coarse_px + Bf*fine_px identifiable.
|
|
V7CONFIGS = [4, 8, 12, 16, 20, 24, 28, 44, 48, 72, 100, 120, 256]
|
|
|
|
blob, metas = bytearray(), []
|
|
for P, x0 in CONFIGS:
|
|
off = len(blob)
|
|
nspans = npix = 0
|
|
for y in range(H):
|
|
cuts = []
|
|
x = 0
|
|
if x0: # a short leading span to shift the phase
|
|
cuts.append((0, x0)); x = x0
|
|
while x < W:
|
|
n = min(P, W - x)
|
|
cuts.append((x, n)); x += n
|
|
blob += struct.pack(">H", len(cuts))
|
|
for x, n in cuts:
|
|
blob += struct.pack(">HH", x, n)
|
|
blob += idx[y, x:x+n].astype(">u2").tobytes()
|
|
nspans += 1; npix += n
|
|
metas.append(dict(name=f"{P}{'u' if x0 else ''}", p=P, x0=x0, off=off,
|
|
len=len(blob)-off, nspans=nspans, npix=npix,
|
|
cpx=npix, fpx=0, var=5))
|
|
|
|
# v6: one config per chain depth, so the fit sees spans from 24 to 264 pixels.
|
|
for units in range(1, UNITS+1):
|
|
P = units * UNITPX
|
|
off = len(blob)
|
|
nspans = npix = 0
|
|
rows = []
|
|
for y in range(H):
|
|
x = 0
|
|
while x < W:
|
|
rows.append((y, x)); x += P
|
|
blob += struct.pack(">H", len(rows))
|
|
for y, x in rows:
|
|
blob += struct.pack(">IH", GVRAM + (YOFF+y)*STRIDE + x*2,
|
|
(UNITS-units)*UNITSZ)
|
|
# Pad the last span of a row past the visible width; the overrun lands
|
|
# in the undisplayed half of the line.
|
|
px = np.concatenate([idx[y, x:x+P], np.zeros(max(0, x+P-W), np.uint8)])
|
|
blob += px.astype(">u2").tobytes()
|
|
nspans += 1; npix += P
|
|
metas.append(dict(name=f"{P}", p=P, x0=0, off=off, len=len(blob)-off,
|
|
nspans=nspans, npix=npix, cpx=npix, fpx=0, var=6))
|
|
|
|
# v7: same tiling, but the span is cut at a 2-pixel quantum instead of 24.
|
|
for P in V7CONFIGS:
|
|
units, fine = divmod(P, UNITPX)
|
|
assert fine % FINEPX == 0 and fine // FINEPX <= FINES, P
|
|
assert units <= UNITS, P
|
|
off = len(blob)
|
|
nspans = npix = 0
|
|
rows = []
|
|
for y in range(H):
|
|
x = 0
|
|
while x < W:
|
|
rows.append((y, x)); x += P
|
|
blob += struct.pack(">H", len(rows))
|
|
for y, x in rows:
|
|
assert x + P <= STRIDE // 2, (P, x) # the overrun must stay on the line
|
|
blob += struct.pack(">IH", GVRAM + (YOFF+y)*STRIDE + x*2,
|
|
(UNITS-units)*UNITSZ)
|
|
px = np.concatenate([idx[y, x:x+P], np.zeros(max(0, x+P-W), np.uint8)])
|
|
blob += px[:units*UNITPX].astype(">u2").tobytes()
|
|
blob += struct.pack(">H", (FINES - fine//FINEPX)*FINESZ)
|
|
blob += px[units*UNITPX:].astype(">u2").tobytes()
|
|
nspans += 1; npix += P
|
|
metas.append(dict(name=f"{P}", p=P, x0=0, off=off, len=len(blob)-off,
|
|
nspans=nspans, npix=npix,
|
|
cpx=nspans*units*UNITPX, fpx=nspans*fine, var=7))
|
|
|
|
open(OUT, "wb").write(blob)
|
|
with open(META, "w") as f:
|
|
f.write("-- generated by tools/bench/prep_spans.py -- do not edit\nreturn {\n")
|
|
f.write(f" W={W}, H={H}, total={len(blob)},\n configs = {{\n")
|
|
for m in metas:
|
|
f.write(" {{var={var}, name=\"{name}\", p={p}, x0={x0}, off={off},"
|
|
" len={len}, nspans={nspans}, npix={npix}, cpx={cpx},"
|
|
" fpx={fpx}}},\n".format(**m))
|
|
f.write(" },\n}\n")
|
|
|
|
print(f"{SRC} {W}x{H} -> {OUT} {len(blob)} B, {len(metas)} configs")
|
|
for m in metas:
|
|
print(f" v{m['var']} span {m['name']:>4} px: {m['nspans']:6d} spans, "
|
|
f"{m['npix']:6d} px, {m['len']:7d} B "
|
|
f"(+{100*m['len']/(2*W*H)-100:.1f}% over bare pixels)")
|