Measure the finer chain tail: 84/120 becomes 18/120, and the derivation was right by cancellation

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
This commit is contained in:
prosolis
2026-08-23 19:02:23 -07:00
parent c5ca56330e
commit c520a89e14
9 changed files with 522 additions and 52 deletions
+55 -3
View File
@@ -31,6 +31,20 @@ v6 -- the same spans with that arithmetic moved here, where it is free:
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).
"""
@@ -58,8 +72,18 @@ CONFIGS = [(4, 0), (8, 0), (12, 0), (16, 0), (16, 1), (20, 0), (24, 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)
@@ -78,7 +102,8 @@ for P, x0 in CONFIGS:
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))
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):
@@ -100,7 +125,33 @@ for units in range(1, UNITS+1):
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))
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:
@@ -108,7 +159,8 @@ with open(META, "w") as f:
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))
" 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")