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
219 lines
8.5 KiB
Lua
219 lines
8.5 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 == 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)
|