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