FINDINGS 29 priced a literal-span mode at 4*(50 + 4L*9.08) cycles and labelled
the whole section DERIVED. Session 8 step 0 was to measure it before optimising
over the mode set it implies. Two variants in blit.s, one stream per span length
from prep_spans.py, timed by span.lua, driven by span.sh in ~25 s:
v5, handed (x, npix) and left to work the copy out: 97.9/span + 10.459/px
v6, handed an address and a jump displacement: 43.7/span + 9.152/px
29 assumed 50.0/span + 9.080/px
So 29's arithmetic was right about a format nobody had written. The difference
is not tuning: v5 spends ~122 cycles a span computing a destination, dividing
npix into bursts and handling a 0..15 remainder, all of which the encoder knows
at build time. v6's record is {u32 absolute GVRAM address, u16 jump
displacement} into an unrolled chain of 24-pixel copy units -- no loop, no
remainder, no arithmetic -- and it fits 11 span lengths to 0.3%.
Three things that measurement showed and derivation could not:
- The per-pixel cost is a function of REGISTER PRESSURE. FINDINGS 24's 9.08
was a fixed blit with 12 registers free; v5 can spare 8 and pays 10.46; v6
gets 12 back only because the encoder holds the state.
- Short spans die in the remainder path -- a 12-pixel span costs MORE than a
16-pixel one -- and the fix is padding, not avoidance.
- Odd-x alignment is free (259.0 vs 261.8 cycles/span), as a 16-bit bus
implies but nobody had checked.
Re-priced against the unchanged mode maps, sasi: median 74.4% -> 52.0% (29 said
43.0), misses 37 -> 10/120 (29 said 8), 448.0 KB/s. Break-even moved from runs
of 2 blocks to runs of 4. 29.4 survives: a scene cut needs x >= 0.196 of the
frame as spans and the bus allows x <= 0.373, so it fits at 12fps.
All 23 timing configs are also checked pixel-exact, so none of this was timed
against a decoder that quietly skipped work.
FINDINGS 30. Next: lever B, the cost-aware mode decision.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
119 lines
5.2 KiB
Python
119 lines
5.2 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.
|
|
|
|
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
|
|
GVRAM, YOFF, STRIDE = 0xC00000, 32, 1024
|
|
|
|
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, 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, var=6))
|
|
|
|
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}}},\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)")
|