Files
Dragon-s-Lair-X68k/tools/bench/span.lua
T
prosolis c520a89e14 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
2026-08-23 19:02:23 -07:00

265 lines
10 KiB
Lua

-- 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 == 5 and 60 or (cfg.var == 6 and 50 or 70))
+ 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
-- v7 has two per-pixel costs -- the 24-pixel coarse chain and the 2-pixel fine
-- chain -- so its fit is cycles = A*spans + Bc*coarse_px + Bf*fine_px, solved
-- by plain Gaussian elimination on the 3x3 normal equations. prep_spans.py
-- picks span lengths so every fine remainder a real span can have (0,4,..,20)
-- appears, which is what makes the three terms separable.
local function fit3(rs)
local M3 = {{0,0,0,0},{0,0,0,0},{0,0,0,0}}
for _,r in ipairs(rs) do
local x = {r.cfg.nspans, r.cfg.cpx, r.cfg.fpx}
for i=1,3 do
for j=1,3 do M3[i][j] = M3[i][j] + x[i]*x[j] end
M3[i][4] = M3[i][4] + x[i]*r.cyc
end
end
for c=1,3 do
local piv = c
for r=c+1,3 do if math.abs(M3[r][c]) > math.abs(M3[piv][c]) then piv=r end end
M3[c], M3[piv] = M3[piv], M3[c]
for r=1,3 do
if r ~= c then
local f = M3[r][c]/M3[c][c]
for k=c,4 do M3[r][k] = M3[r][k] - f*M3[c][k] end
end
end
end
return M3[1][4]/M3[1][1], M3[2][4]/M3[2][2], M3[3][4]/M3[3][3]
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 > 900 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,7} 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 ~= 5 or r.cfg.p % 16 == 0 then fitset[#fitset+1] = r end
end
local A, Bp, Bf
if v == 7 then
A, Bp, Bf = fit3(fitset)
P(string.format("-- v7: cycles = %.1f per span + %.3f per COARSE pixel"
.." + %.3f per FINE pixel (fitted on %d of %d configs)",
A, Bp, Bf, #fitset, #sub))
else
A, Bp = fit(fitset)
Bf = Bp
P(string.format("-- v%d: cycles = %.1f per span + %.3f per pixel"
.." (fitted on %d of %d configs)", v, A, Bp, #fitset, #sub))
end
for _,r in ipairs(sub) do
local model = A*r.cfg.nspans + Bp*r.cfg.cpx + Bf*r.cfg.fpx
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, cyc = 4*L, nil
if v == 6 then
px = math.ceil(px/24)*24
cyc = A + px*Bp
elseif v == 7 then
local c = math.floor(px/24)*24
cyc = A + c*Bp + (px-c)*Bf -- a multiple of 4 pads to nothing
else
cyc = A + px*Bp
end
line = line..string.format("L=%d %.0f ", L, 4*cyc/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)