Measure the blit on the 68000: the 38% estimate was 53.6%
First 68000 instructions in this project to draw a pixel. Everything before this was GVRAM filled from Lua, which costs zero 68000 cycles, so the blit figure the whole CPU budget rests on had never been validated. Four variants of a full-frame 256x192 paint, timed in MAME and each also hand-derived from the MC68000 timing tables beforehand; the two agree to 0.006-0.43%, which is what makes the result trustworthy after this project's history of false-good measurements. V1 movem.l blit from a word-expanded RAM frame 446,286 cyc 53.6% V2 naive move.b/move.w per pixel 1,284,174 cyc 154.1% V3 write-only floor, no source read 225,789 cyc 27.1% V4 same writes in 4x4 block order 637,971 cyc 76.6% Scope: MAME's gvram_w/gvram_r carry no timing at all, so these are instruction cycles against zero-wait-state memory -- a floor, not a hardware prediction. V1's output snapshots pixel-exact through verify_frame256.py, closing FINDINGS 23.5. The V1/V3 gap shows reading the source frame is exactly half the cost, which makes the architecture question live: decode-direct-to-GVRAM needs no RAM reference frame and scales with the non-SKIP block fraction, crossing compose-then-blit at 70% of blocks changed. That fraction is now the top priority and is already a by-product of vq_hybrid.py's mode decision. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
-- Time the full-frame GVRAM blit (tools/bench/blit.s) on the emulated 68000.
|
||||
--
|
||||
-- This is the first measurement in the project where 68000 instructions, not
|
||||
-- Lua, put the pixels on screen. It validates (or kills) the 38% full-frame
|
||||
-- blit estimate the whole CPU budget rests on.
|
||||
--
|
||||
-- MEASUREMENT SCOPE. MAME's gvram_w/gvram_r (x68k_crtc.cpp:501,595) contain
|
||||
-- no timing whatsoever -- no wait states, no icount adjustment. So what is
|
||||
-- measured here is pure 68000 instruction cycles against zero-wait-state
|
||||
-- memory. Real X68000 GVRAM stalls the CPU; every number below is therefore
|
||||
-- a LOWER BOUND, not a prediction. Interrupts are masked (SR=$2700) so the
|
||||
-- IPL's timer and VBL handlers cannot steal cycles into the measurement.
|
||||
--
|
||||
-- Timing resolution is one video frame (1/55.46 s = 18.03 ms), because Lua
|
||||
-- gets no cycle counter -- luaengine.cpp exposes machine.time and nothing
|
||||
-- from device_execute_interface. Each variant therefore loops enough times
|
||||
-- to run ~4 emulated seconds, putting the granularity error near 0.4%.
|
||||
|
||||
M = manager.machine
|
||||
SP = M.devices[":maincpu"].spaces["program"]
|
||||
|
||||
local function load_mode()
|
||||
for _,p in ipairs{"../tools/bench/crtc_mode.lua","tools/bench/crtc_mode.lua","crtc_mode.lua"} do
|
||||
local f = loadfile(p); if f then return f() end
|
||||
end
|
||||
error("crtc_mode.lua not found")
|
||||
end
|
||||
local MODE = load_mode()
|
||||
|
||||
local FLAG, VAR, ITER = 0x18000, 0x18004, 0x18008
|
||||
local SRCW, SRCB = 0x60000, 0x80000
|
||||
local GVRAM, GPAL = 0xC00000, 0xE82000
|
||||
local CPUHZ = 10000000 -- x68k.cpp:1133, 40_MHz_XTAL/4
|
||||
local FRAME12 = CPUHZ / 12 -- 833333 cycles at 12 fps
|
||||
|
||||
-- Iteration counts sized so every variant runs ~4 emulated seconds.
|
||||
local PLAN = {
|
||||
{var=1, iter=100, name="V1 movem.l blit from word-expanded RAM (96KB read + 96KB write)"},
|
||||
{var=2, iter= 50, name="V2 naive byte-source expansion (move.b/move.w per pixel)"},
|
||||
{var=3, iter=200, name="V3 write-only floor (no source read at all)"},
|
||||
{var=4, iter= 60, name="V4 same 96KB of writes, issued in 4x4 BLOCK order (decoder access pattern)"},
|
||||
}
|
||||
|
||||
local code do
|
||||
local f = io.open("blit.bin","rb"); code = 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, PIX0 = 9, 9+256*3
|
||||
local YOFF = (MODE.height - H) // 2
|
||||
|
||||
-- Identical packing to show_frame256.lua: shared LSB I chosen 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("[BLIT] "..s) end
|
||||
|
||||
local function setup()
|
||||
MODE.apply(SP)
|
||||
-- letterbox rows: GVRAM holds IPL leftovers, not zeros
|
||||
for y = 0, MODE.height-1 do
|
||||
if y < YOFF or y >= YOFF+H then
|
||||
local base = GVRAM + y*1024
|
||||
for x = 0, MODE.width-1 do SP:write_u16(base + x*2, 0) end
|
||||
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
|
||||
-- Source frames in main RAM. SRCW holds one pixel per WORD with the index
|
||||
-- in the low byte; the high byte is left as-is because gvram_w masks it off.
|
||||
for y = 0, H-1 do
|
||||
local row = PIX0 + y*W
|
||||
for x = 0, W-1 do
|
||||
local px = B(row+x)
|
||||
SP:write_u16(SRCW + y*512 + x*2, px)
|
||||
SP:write_u8 (SRCB + y*256 + x, px)
|
||||
end
|
||||
end
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
P(string.format("loaded blit.bin=%d bytes, source frame %dx%d at yoff=%d", #code, W, H, YOFF))
|
||||
end
|
||||
|
||||
local step, st, t0, snapped = 0, "boot", nil, false
|
||||
local results = {}
|
||||
|
||||
local function launch(p)
|
||||
SP:write_u32(FLAG, 0)
|
||||
SP:write_u32(VAR, p.var)
|
||||
SP:write_u32(ITER, p.iter)
|
||||
local cpu = M.devices[":maincpu"]
|
||||
cpu.state["SR"].value = 0x2700 -- supervisor, ALL interrupts masked
|
||||
cpu.state["SP"].value = 0x8000
|
||||
cpu.state["PC"].value = 0x10000
|
||||
st, t0 = "running", nil
|
||||
end
|
||||
|
||||
local function report(p, dt)
|
||||
local cyc = dt * CPUHZ / p.iter
|
||||
local pct = 100 * cyc / FRAME12
|
||||
results[#results+1] = {p=p, cyc=cyc, pct=pct}
|
||||
P(string.format("%s", p.name))
|
||||
P(string.format(" %d iterations in %.4f s -> %.0f cycles/frame = %.1f%% of a 12fps frame",
|
||||
p.iter, dt, cyc, pct))
|
||||
end
|
||||
|
||||
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(PLAN[1]); 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(PLAN[step], t - (t0 or t))
|
||||
if step == 1 and not snapped then st, snapped = "snap", true; return end
|
||||
step = step + 1
|
||||
if PLAN[step] then launch(PLAN[step]) else st = "finish" end
|
||||
return
|
||||
end
|
||||
if t > 60 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end
|
||||
return
|
||||
end
|
||||
if st == "snap" then
|
||||
M.video:snapshot(); P("snapshot taken after V1 -- 68000-drawn frame")
|
||||
step = step + 1; launch(PLAN[step]); return
|
||||
end
|
||||
if st == "finish" then
|
||||
P("---- summary (instruction cycles only; real GVRAM adds wait states) ----")
|
||||
for _,r in ipairs(results) do
|
||||
P(string.format(" V%d %8.0f cyc %5.1f%% of 12fps frame", r.p.var, r.cyc, r.pct))
|
||||
end
|
||||
M:exit()
|
||||
end
|
||||
end)
|
||||
if not ok then print("[BLIT] LUA ERROR: "..tostring(err)); M:exit() end
|
||||
end)
|
||||
Reference in New Issue
Block a user