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
63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression test for the 256x256 CRTC mode (docs/FINDINGS 23).
|
|
|
|
Checks a native snapshot (default tmp/snap256/x68000/0000.png, override with
|
|
argv[1] -- tools/bench/span.lua verifies twelve of them) against
|
|
tmp/frame256.bin:
|
|
1. native snapshot is 256x512 -- 256 dots, and 512 active scanlines of a
|
|
568-line 31.5kHz raster carrying 256 double-scanned graphics rows
|
|
2. double-scan pairing is (1,2),(3,4),... -- MAME halves the ABSOLUTE
|
|
scanline (x68k_v.cpp get_gfx_pixel) and vbegin=41 is odd, so snapshot
|
|
row 0 is a lone half-line and even rows are gfx rows 0..255
|
|
3. the 192 active rows are PIXEL-EXACT against the palette pushed through
|
|
GGGGGRRRRRBBBBBI with I chosen per entry by minimum squared error
|
|
4. the letterbox bars are TRUE black -- needs both a reserved index-0 black
|
|
entry AND I=0 on it, since pal6bit(1) = 4, not 0
|
|
"""
|
|
import struct, sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
snap = sys.argv[1] if len(sys.argv) > 1 else "tmp/snap256/x68000/0000.png"
|
|
s = np.asarray(Image.open(snap).convert("RGB")).astype(int)
|
|
d = open("tmp/frame256.bin", "rb").read()
|
|
W, H = struct.unpack(">HH", d[4:8])
|
|
pal = np.frombuffer(d[8:8+768], np.uint8).reshape(256, 3).astype(int)
|
|
idx = np.frombuffer(d[8+768:8+768+W*H], np.uint8).reshape(H, W)
|
|
|
|
p6 = lambda v: ((v << 2) | (v >> 4)) & 0xFF
|
|
f = pal >> 3
|
|
render = lambda I: p6((f << 1) | I[:, None])
|
|
I = (((render(np.ones(256, int)) - pal) ** 2).sum(1)
|
|
< ((render(np.zeros(256, int)) - pal) ** 2).sum(1)).astype(int)
|
|
exp = render(I)[idx]
|
|
|
|
fail = []
|
|
if s.shape[:2] != (512, 256):
|
|
fail.append(f"1. geometry: expected 512x256, got {s.shape[1]}x{s.shape[0]}")
|
|
if not all(np.array_equal(s[i], s[i+1]) for i in range(1, s.shape[0]-1, 2)):
|
|
fail.append("2. double-scan pairing (1,2),(3,4),... broken")
|
|
|
|
g = s[0::2]
|
|
yoff = (g.shape[0] - H) // 2
|
|
act = g[yoff:yoff+H]
|
|
if not np.array_equal(act, exp):
|
|
diff = abs(act - exp)
|
|
fail.append(f"3. active area not pixel-exact: maxdiff {diff.max()}, "
|
|
f"{diff.any(2).sum()} px differ")
|
|
|
|
bars = np.concatenate([g[:yoff], g[yoff+H:]])
|
|
if bars.max() != 0:
|
|
fail.append(f"4. letterbox not true black: max channel {bars.max()}")
|
|
|
|
for x in fail:
|
|
print("FAIL " + x)
|
|
if fail:
|
|
sys.exit(1)
|
|
|
|
mse = ((act - pal[idx]) ** 2).mean()
|
|
print(f"OK {snap}: 256x512 native, double-scan exact, active {W}x{H} pixel-exact, "
|
|
f"letterbox true black")
|
|
print(f" palette ceiling vs 24-bit palettised source: "
|
|
f"{10*np.log10(255**2/mse):.2f} dB ({(I==0).sum()}/256 entries use I=0)")
|