-- 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/56.69 s granularity (crtc_mode.lua) -- 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)