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
265 lines
10 KiB
Lua
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)
|