#!/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)")