Measure the span: the mode survives, and it is an encoder format
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
This commit is contained in:
@@ -31,6 +31,43 @@
|
||||
; the block needs only one base pointer. V4 deliberately scrambles the
|
||||
; picture (it reads a row-linear source in block order); it is a timing
|
||||
; probe, which is why the correctness snapshot is taken after V1.
|
||||
; V5 ROW-LINEAR LITERAL SPANS, the mode priced in FINDINGS 29 and never
|
||||
; measured. Walks a stream of per-row span records
|
||||
; row: u16 nspans, then nspans * { u16 x, u16 npix, npix*u16 pixels }
|
||||
; for 192 rows, copying each span's word-expanded pixels straight from
|
||||
; the stream buffer into GVRAM. Unlike V1-V4 the work per call is set by
|
||||
; the STREAM, not by the code, so one variant measures every span length:
|
||||
; tools/bench/prep_spans.py generates a stream per span length and
|
||||
; tools/bench/span.lua times them and fits cycles = A*spans + B*pixels.
|
||||
; The point of the measurement is A -- the per-span overhead FINDINGS 29
|
||||
; guessed at 50 cycles -- and how much B degrades from V1's 9.08 when a
|
||||
; span is too short to burst. Every config covers the whole frame, so
|
||||
; V5 draws the SAME picture V1 does and can be verified, not just timed.
|
||||
;
|
||||
; Bursts are 8 registers (d0-d3/a3-a6 = 32 bytes = 16 pixels), not V1's
|
||||
; 12: a0/a1/a2 and d4-d7 are all live across a span (stream, row base,
|
||||
; destination, and three counters). The remainder is copied move.l at a
|
||||
; time with a leading move.w when it is odd, so a 4-pixel span never
|
||||
; reaches a movem at all -- which is exactly the case FINDINGS 29's
|
||||
; full-row-width extrapolation flatters.
|
||||
;
|
||||
; V6 the SAME spans with the arithmetic moved into the encoder. V5 measures
|
||||
; a decoder that is handed (x, npix) and has to work out how to copy it;
|
||||
; most of its per-span cost is that working-out, and an encoder can do it
|
||||
; once at build time instead of 12 times a second. V6's record is
|
||||
; { u32 absolute GVRAM address, u16 jump displacement } -- no row
|
||||
; structure, no counters, no remainder logic -- and the displacement
|
||||
; jumps into an unrolled chain of 24-pixel copy units, so a span of any
|
||||
; supported length is straight-line code with no loop at all.
|
||||
; GVRAM sits at a fixed $C00000 on every X68000, so absolute destinations
|
||||
; are a legitimate thing for an encoder to bake in.
|
||||
;
|
||||
; Two consequences of the format. Span lengths are multiples of 24
|
||||
; pixels, and a span may overrun the 256 visible pixels of its row by up
|
||||
; to 23 -- harmless, because the line stride is 1024 bytes and only the
|
||||
; first 512 are displayed, so the overrun lands in the invisible half.
|
||||
; And with row and remainder handling gone, 12 registers are free again
|
||||
; (d0-d6/a1/a3-a6), which is why the unit is 24 pixels and not V5's 16.
|
||||
;
|
||||
; 12 registers per movem burst (d0-d7/a2-a5 = 48 bytes) is the maximum
|
||||
; available: a0=src, a1=dst, a6=end sentinel. The row counter lives in the
|
||||
@@ -43,10 +80,14 @@
|
||||
FLAG = $18000 ; 0 idle / 1 running / $FF done
|
||||
VAR = $18004 ; variant selector, written by Lua
|
||||
ITER = $18008 ; iteration count, written by Lua
|
||||
SPTR = $1800C ; V5 span stream pointer, written by Lua
|
||||
SRCW = $60000 ; word-expanded frame 192*512 = 96KB
|
||||
SRCB = $80000 ; byte-per-pixel frame 192*256 = 48KB
|
||||
DST0 = $C08000 ; GVRAM + 32*1024 (first picture row)
|
||||
DSTE = $C38000 ; GVRAM + 224*1024 (one past last)
|
||||
ROWS = 192 ; picture rows a V5 stream describes
|
||||
V6UNIT = 12 ; bytes of code per V6 chain unit
|
||||
V6MAX = 11 ; chain units = 11*24 = 264 pixels >= one row
|
||||
|
||||
org $10000
|
||||
start:
|
||||
@@ -58,6 +99,10 @@ start:
|
||||
beq v2
|
||||
cmp.l #4,d0
|
||||
beq v4
|
||||
cmp.l #5,d0
|
||||
beq v5
|
||||
cmp.l #6,d0
|
||||
beq v6
|
||||
bra v3
|
||||
|
||||
; ---------------------------------------------------------------- V1
|
||||
@@ -152,5 +197,90 @@ v4blk: movem.l (a0)+,d0-d7 ; 32 bytes = one 4x4 block, expanded
|
||||
bne v4
|
||||
bra done
|
||||
|
||||
; ---------------------------------------------------------------- V5
|
||||
; a0 stream, a1 row base, a2 span destination, d7 rows, d6 spans, d5 pixels,
|
||||
; d4 burst/tail counter. Everything else (d0-d3/a3-a6) is burst payload.
|
||||
v5: move.l SPTR.l,a0
|
||||
lea DST0,a1
|
||||
move.w #ROWS-1,d7
|
||||
v5row: move.w (a0)+,d6 ; spans in this row
|
||||
subq.w #1,d6
|
||||
bmi.s v5eor ; a row may legitimately have none
|
||||
v5span: move.w (a0)+,d0 ; x, in pixels
|
||||
add.w d0,d0 ; one pixel = one word
|
||||
lea 0(a1,d0.w),a2
|
||||
move.w (a0)+,d5 ; pixels in this span
|
||||
move.w d5,d4
|
||||
lsr.w #4,d4 ; 16-pixel bursts
|
||||
beq.s v5tail
|
||||
subq.w #1,d4
|
||||
v5burst: movem.l (a0)+,d0-d3/a3-a6 ; 32 bytes straight out of the stream
|
||||
movem.l d0-d3/a3-a6,(a2)
|
||||
lea 32(a2),a2
|
||||
dbra d4,v5burst
|
||||
v5tail: moveq #15,d4
|
||||
and.w d5,d4 ; 0..15 pixels left
|
||||
beq.s v5eos
|
||||
lsr.w #1,d4 ; C = odd pixel count
|
||||
bcc.s v5t2
|
||||
move.w (a0)+,(a2)+
|
||||
v5t2: subq.w #1,d4
|
||||
bmi.s v5eos
|
||||
v5tl: move.l (a0)+,(a2)+
|
||||
dbra d4,v5tl
|
||||
v5eos: dbra d6,v5span
|
||||
v5eor: lea 1024(a1),a1
|
||||
dbra d7,v5row
|
||||
subq.l #1,ITER.l
|
||||
bne v5
|
||||
bra done
|
||||
|
||||
; ---------------------------------------------------------------- V6
|
||||
; a0 stream, a2 destination, d7 spans remaining; everything else is payload.
|
||||
v6: move.l SPTR.l,a0
|
||||
move.w (a0)+,d7 ; total spans in the frame
|
||||
subq.w #1,d7
|
||||
v6span: move.l (a0)+,a2 ; absolute GVRAM destination
|
||||
move.w (a0)+,d0 ; (V6MAX - units) * V6UNIT, from the encoder
|
||||
jmp v6ch(pc,d0.w)
|
||||
v6ch:
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
movem.l (a0)+,d0-d6/a1/a3-a6
|
||||
movem.l d0-d6/a1/a3-a6,(a2)
|
||||
lea 48(a2),a2
|
||||
dbra d7,v6span
|
||||
subq.l #1,ITER.l
|
||||
bne v6
|
||||
bra done
|
||||
|
||||
done: move.l #$FF,FLAG.l ; timer stops here
|
||||
halt: bra.s halt
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/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)")
|
||||
@@ -0,0 +1,218 @@
|
||||
-- Measure the cost of a row-linear literal SPAN on the 68000 (FINDINGS 29.5.1).
|
||||
--
|
||||
-- FINDINGS 29 proposes one new decoder mode and prices it at
|
||||
-- 4 * (50 + 4L*9.08) cycles for a run of L blocks
|
||||
-- then labels the whole section DERIVED, NOT MEASURED, because both terms are
|
||||
-- extrapolations: the 50-cycle per-span overhead is hand-derived, and the 9.08
|
||||
-- cycles/pixel is a FINDINGS 24 measurement taken at FULL ROW WIDTH with
|
||||
-- 12-register bursts. A 4-pixel span cannot burst at all. Everything session
|
||||
-- 8 wants to do downstream optimises over the mode set this number decides, so
|
||||
-- it goes first.
|
||||
--
|
||||
-- Method: v5 in tools/bench/blit.s walks a stream of per-row span records and
|
||||
-- copies each span into GVRAM. tools/bench/prep_spans.py emits one stream per
|
||||
-- span length, every one covering the same whole frame, so the work differs
|
||||
-- only in how finely it is cut. Regressing
|
||||
-- cycles = A*spans + B*pixels
|
||||
-- over the set reads A (the per-span overhead) and B (the per-pixel cost)
|
||||
-- straight off, and every config also draws a verifiable picture: the frame is
|
||||
-- cleared before each run and snapshotted after, so a config that timed fast
|
||||
-- by not writing pixels fails tools/bench/verify_frame256.py.
|
||||
--
|
||||
-- MEASUREMENT SCOPE, unchanged from blit.lua: MAME's gvram_w carries no timing,
|
||||
-- so these are 68000 instruction cycles against zero-wait-state memory -- a
|
||||
-- LOWER BOUND on real hardware. Interrupts are masked (SR=$2700).
|
||||
|
||||
M = manager.machine
|
||||
SP = M.devices[":maincpu"].spaces["program"]
|
||||
|
||||
local function findfile(n)
|
||||
for _,p in ipairs{"../tools/bench/"..n, "tools/bench/"..n, n} do
|
||||
local f = io.open(p,"rb"); if f then f:close(); return p end
|
||||
end
|
||||
error(n.." not found")
|
||||
end
|
||||
local MODE = loadfile(findfile("crtc_mode.lua"))()
|
||||
local SPEC = loadfile("spans_meta.lua")()
|
||||
|
||||
local FLAG, VAR, ITER, SPTR = 0x18000, 0x18004, 0x18008, 0x1800C
|
||||
local STREAM = 0x90000
|
||||
local GVRAM, GPAL = 0xC00000, 0xE82000
|
||||
local CPUHZ = 10000000 -- x68k.cpp:1133, 40_MHz_XTAL/4
|
||||
local FRAME12 = CPUHZ / 12
|
||||
|
||||
local code do local f=io.open("blit.bin","rb"); code=f:read("a"); f:close() end
|
||||
local blob do local f=io.open("spans.bin","rb"); blob=f:read("a"); f:close() end
|
||||
local frame do local f=io.open("frame256.bin","rb"); frame=f:read("a"); f:close() end
|
||||
|
||||
local function B(i) return string.byte(frame,i) end
|
||||
local W, H = B(5)*256+B(6), B(7)*256+B(8)
|
||||
local PAL0 = 9
|
||||
local YOFF = (MODE.height - H) // 2
|
||||
|
||||
-- Identical packing to blit.lua / show_frame256.lua: shared LSB I per entry.
|
||||
local function pal6(v) return ((v<<2)|(v>>4)) & 0xff end
|
||||
local function pack(r,g,b)
|
||||
local f = {r>>3, g>>3, b>>3}
|
||||
local best, bestI = nil, 1
|
||||
for I = 0,1 do
|
||||
local e = 0
|
||||
for c = 1,3 do
|
||||
local want = ({r,g,b})[c]
|
||||
local d = pal6((f[c]<<1)|I) - want
|
||||
e = e + d*d
|
||||
end
|
||||
if best == nil or e < best then best, bestI = e, I end
|
||||
end
|
||||
return (f[2]<<11)|(f[1]<<6)|(f[3]<<1)|bestI
|
||||
end
|
||||
|
||||
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
|
||||
local function P(s) print("[SPAN] "..s) end
|
||||
|
||||
-- 148 KB one byte at a time is 148k Lua->C calls; longwords cut that by four.
|
||||
local function push(addr, s, from, len)
|
||||
local i, n = from, len
|
||||
while n >= 4 do
|
||||
SP:write_u32(addr, (string.unpack(">I4", s, i)))
|
||||
addr, i, n = addr+4, i+4, n-4
|
||||
end
|
||||
while n > 0 do
|
||||
SP:write_u8(addr, string.byte(s,i)); addr, i, n = addr+1, i+1, n-1
|
||||
end
|
||||
end
|
||||
|
||||
local function clear_picture() -- so a config that writes nothing is caught
|
||||
for y = YOFF, YOFF+H-1 do
|
||||
local base = GVRAM + y*1024
|
||||
for x = 0, MODE.width-1, 2 do SP:write_u32(base + x*2, 0) end
|
||||
end
|
||||
end
|
||||
|
||||
local function setup()
|
||||
MODE.apply(SP)
|
||||
for y = 0, MODE.height-1 do
|
||||
local base = GVRAM + y*1024
|
||||
for x = 0, MODE.width-1, 2 do SP:write_u32(base + x*2, 0) end
|
||||
end
|
||||
for c = 0, 255 do
|
||||
local o = PAL0 + c*3
|
||||
SP:write_u16(GPAL + c*2, pack(B(o), B(o+1), B(o+2)))
|
||||
end
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
P(string.format("loaded blit.bin=%d B, %d span configs, picture %dx%d at yoff=%d",
|
||||
#code, #SPEC.configs, W, H, YOFF))
|
||||
end
|
||||
|
||||
local function launch(cfg)
|
||||
push(STREAM, blob, cfg.off+1, cfg.len)
|
||||
clear_picture()
|
||||
-- ~4 emulated seconds per config: 1/55.46 s granularity costs under 0.5%.
|
||||
local est = cfg.nspans*(cfg.var == 6 and 50 or 60) + cfg.npix*10
|
||||
cfg.iter = math.max(4, math.floor(4*CPUHZ/est))
|
||||
SP:write_u32(FLAG, 0)
|
||||
SP:write_u32(VAR, cfg.var)
|
||||
SP:write_u32(ITER, cfg.iter)
|
||||
SP:write_u32(SPTR, STREAM)
|
||||
local cpu = M.devices[":maincpu"]
|
||||
cpu.state["SR"].value = 0x2700
|
||||
cpu.state["SP"].value = 0x8000
|
||||
cpu.state["PC"].value = 0x10000
|
||||
end
|
||||
|
||||
local results = {}
|
||||
local function report(cfg, dt)
|
||||
local cyc = dt * CPUHZ / cfg.iter
|
||||
results[#results+1] = {cfg=cfg, cyc=cyc}
|
||||
P(string.format("v%d span %4s px: %5d spans %6d px %d iter in %.4f s -> %8.0f cyc/frame"
|
||||
.." %5.2f cyc/px %5.1f%% of a 12fps frame",
|
||||
cfg.var, cfg.name, cfg.nspans, cfg.npix, cfg.iter, dt, cyc,
|
||||
cyc/cfg.npix, 100*cyc/FRAME12))
|
||||
end
|
||||
|
||||
-- Ordinary least squares on cycles = A*spans + B*pixels, no intercept: the
|
||||
-- 192 row headers and the outer loop are the only work not attributable to a
|
||||
-- span or a pixel, and at ~10 cycles a row they are 0.2% of the smallest run.
|
||||
local function fit(rs)
|
||||
local ss,sp,pp,sy,py = 0,0,0,0,0
|
||||
for _,r in ipairs(rs) do
|
||||
local s,p,y = r.cfg.nspans, r.cfg.npix, r.cyc
|
||||
ss=ss+s*s; sp=sp+s*p; pp=pp+p*p; sy=sy+s*y; py=py+p*y
|
||||
end
|
||||
local det = ss*pp - sp*sp
|
||||
return (sy*pp - py*sp)/det, (ss*py - sp*sy)/det
|
||||
end
|
||||
|
||||
local step, st, t0 = 0, "boot", nil
|
||||
|
||||
SUB = emu.add_machine_frame_notifier(function()
|
||||
local ok, err = pcall(function()
|
||||
local t = T()
|
||||
if st == "boot" then
|
||||
if t < 3.0 then return end
|
||||
setup(); step = 1; launch(SPEC.configs[1]); st, t0 = "running", nil; return
|
||||
end
|
||||
if st == "running" then
|
||||
local fl = SP:read_u32(FLAG)
|
||||
if fl == 1 and not t0 then t0 = t; return end
|
||||
if fl == 0xFF then
|
||||
report(SPEC.configs[step], t - (t0 or t))
|
||||
st = "snap"; return
|
||||
end
|
||||
if t > 300 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end
|
||||
return
|
||||
end
|
||||
if st == "snap" then
|
||||
M.video:snapshot() -- verified by tools/bench/span.sh
|
||||
step = step + 1
|
||||
if SPEC.configs[step] then
|
||||
launch(SPEC.configs[step]); st, t0 = "running", nil
|
||||
else
|
||||
st = "finish"
|
||||
end
|
||||
return
|
||||
end
|
||||
if st == "finish" then
|
||||
P("---- measured (instruction cycles only; real GVRAM adds wait states) ----")
|
||||
for _,v in ipairs{5,6} do
|
||||
local sub = {}
|
||||
for _,r in ipairs(results) do if r.cfg.var == v then sub[#sub+1] = r end end
|
||||
-- v5's fit is over its BURSTING configs only (span length a multiple of
|
||||
-- the 16-pixel burst). Mixing the remainder-path configs in would hide
|
||||
-- the two costs behind one bad line; they are reported against the fit
|
||||
-- instead, which is where the remainder shows up as error.
|
||||
local fitset = {}
|
||||
for _,r in ipairs(sub) do
|
||||
if v == 6 or r.cfg.p % 16 == 0 then fitset[#fitset+1] = r end
|
||||
end
|
||||
local A, Bp = fit(fitset)
|
||||
P(string.format("-- v%d: cycles = %.1f per span + %.3f per pixel"
|
||||
.." (fitted on %d of %d configs)", v, A, Bp, #fitset, #sub))
|
||||
for _,r in ipairs(sub) do
|
||||
local model = A*r.cfg.nspans + Bp*r.cfg.npix
|
||||
P(string.format(" span %4s px %8.0f cyc %5.2f cyc/px %6.1f cyc/span"
|
||||
.." vs fit %+6.1f%%", r.cfg.name, r.cyc,
|
||||
r.cyc/r.cfg.npix, r.cyc/r.cfg.nspans, 100*(model/r.cyc-1)))
|
||||
end
|
||||
-- What the mode decision actually needs: a run of L horizontally
|
||||
-- adjacent 4x4 blocks is 4 spans of 4L pixels, one per pixel row, and
|
||||
-- v6 pads each to a whole 24-pixel chain unit.
|
||||
local line = " -> cycles per 4x4 block in a run of L blocks: "
|
||||
for _,L in ipairs{1,2,4,8,16,64} do
|
||||
local px = 4*L
|
||||
if v == 6 then px = math.ceil(px/24)*24 end
|
||||
line = line..string.format("L=%d %.0f ", L, 4*(A + px*Bp)/L)
|
||||
end
|
||||
P(line.."(V1 is 299.9)")
|
||||
if v == 5 then
|
||||
P(" v5's fit only holds where 4L is a whole number of 16-pixel bursts.")
|
||||
P(" L=1 and L=2 are extrapolations its own measured spans"
|
||||
.." contradict: 721 and 482.")
|
||||
end
|
||||
end
|
||||
P(" FINDINGS 29 assumed 50.0 per span + 9.080 per pixel, 4 spans per run")
|
||||
M:exit()
|
||||
end
|
||||
end)
|
||||
if not ok then print("[SPAN] LUA ERROR: "..tostring(err)); M:exit() end
|
||||
end)
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Measure the cost of a row-linear literal span on the 68000 (FINDINGS 30).
|
||||
# ~25 s. Run from the repo root. Needs tmp/frame256.bin (check.sh makes it).
|
||||
#
|
||||
# NOT part of check.sh, for the same reason blit.s is not: the output is a wall
|
||||
# timing, so gating on it would make the green light host-sensitive. What IS
|
||||
# gated here is correctness -- all 23 configs must draw a pixel-exact frame,
|
||||
# which is what stops a config timing fast by quietly writing nothing.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
[ -f tmp/frame256.bin ] || { echo "need tmp/frame256.bin -- run tools/bench/check.sh"; exit 2; }
|
||||
|
||||
python3 tools/bench/prep_spans.py
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/blit.bin tools/bench/blit.s > /dev/null
|
||||
mkdir -p tmp/snap_span
|
||||
rm -f tmp/snap_span/x68000/*.png
|
||||
( cd tmp && SDL_VIDEODRIVER=dummy timeout -k 5 1800 mame x68000 -bios ipl10 \
|
||||
-ramsize 2M -video soft -window -sound none -nothrottle -plugins \
|
||||
-autoboot_script ../tools/bench/span.lua \
|
||||
-snapshot_directory ./snap_span -snapview native -seconds_to_run 150 \
|
||||
> span.log 2>&1 )
|
||||
grep -a "^\[SPAN\]" tmp/span.log
|
||||
|
||||
n=0
|
||||
for f in tmp/snap_span/x68000/*.png; do
|
||||
python3 tools/bench/verify_frame256.py "$f" > /dev/null || {
|
||||
echo "FAIL: $f is not pixel-exact"; python3 tools/bench/verify_frame256.py "$f"; exit 1; }
|
||||
n=$((n+1))
|
||||
done
|
||||
[ "$n" -eq 23 ] || { echo "FAIL: $n snapshots, expected 23"; exit 1; }
|
||||
echo "OK $n/23 span configs drew a pixel-exact frame"
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression test for the 256x256 CRTC mode (docs/FINDINGS 23).
|
||||
|
||||
Checks tmp/snap256/x68000/0000.png against tmp/frame256.bin:
|
||||
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
|
||||
@@ -16,7 +18,8 @@ import struct, sys
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
s = np.asarray(Image.open("tmp/snap256/x68000/0000.png").convert("RGB")).astype(int)
|
||||
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)
|
||||
@@ -53,7 +56,7 @@ if fail:
|
||||
sys.exit(1)
|
||||
|
||||
mse = ((act - pal[idx]) ** 2).mean()
|
||||
print(f"OK 256x512 native, double-scan exact, active {W}x{H} pixel-exact, "
|
||||
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)")
|
||||
|
||||
Reference in New Issue
Block a user