-- Time and verify src/player/load.i on the emulated 68000 (ROADMAP P1+P2). -- -- Two questions, one run, exactly as decode.lua asks them of the decoder: -- 1. CORRECTNESS. Does the 68000 produce, out of the RAW container header, -- byte for byte what tools/bench/dlxload.py produces host-side? The -- expanded codebooks are read back out of RAM and the palette out of the -- PALETTE REGISTERS -- not out of a RAM shadow, because "the words reached -- $E82000" is the claim being tested. tools/bench/verify_load.py does the -- comparison against dlxload.py, so the ground truth stays in one place. -- 2. COST. How long does it take, split into the codebook expansion and the -- palette pack, and what is that as a fraction of a 12 fps frame -- the -- only unit this project prices anything in. -- -- Nothing here is pre-chewed: the blob pushed into RAM is the first 5,920 bytes -- of the container as they come off the disc. That is the whole point of the -- exercise, and it is also, not incidentally, exactly the read a player has to -- complete at a scene change before it can draw a single frame. -- -- MEASUREMENT SCOPE, unchanged from decode.lua: MAME's memory carries no wait -- states, so these are pure 68000 instruction cycles -- a LOWER BOUND on real -- hardware. Interrupts are masked (SR=$2700). The host clock has 1/56.69 s -- granularity and the job takes milliseconds, so each configuration is repeated -- LITER times and divided; repeating is honest because do_load is not -- temporally recursive -- every pass rewrites what the last one wrote, from the -- same source bytes. 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 META = loadfile("load_meta.lua")() local LFLAG, LHDR, LDARK = 0x18040, 0x18044, 0x18048 local LK1, LK4, LMODE, LITER = 0x1804C, 0x18050, 0x18054, 0x18058 local CB1, CB4, RAW = 0x20000, 0x22000, 0x30000 local GPAL = 0xE82000 local CPUHZ = 10000000 -- x68k.cpp:1133, 40_MHz_XTAL/4 local FPS = 12 local FRAME12 = CPUHZ / FPS local ITER = tonumber(os.getenv("DLX_LOAD_ITER") or "40") local code do local f=io.open("loadgate.bin","rb"); code=f:read("a"); f:close() end local data do local f=io.open("load_data.bin","rb"); data=f:read("a"); f:close() end local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end local function P(s) print("[LOD] "..s) end 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 -- Poison every destination before each run. Without this a stage that wrote -- NOTHING would still compare equal to the previous stage's output, and the -- palette-only run would "pass" the codebook check for free. -- -- The three scratch tables are poisoned only before a run that CLAIMS to build -- them (mode bit 2). They are scene-independent, so the palette-entry stage is -- entitled to find them already there -- that is the whole point of measuring -- it separately -- but a stage that says it builds them must be shown to. local P6TAB, TABEND = 0x19000, 0x19340 local function poison(mode) for a = CB1, CB1 + META.cb1_len - 2, 2 do SP:write_u16(a, 0xDEAD) end for a = CB4, CB4 + META.cb4_len - 2, 2 do SP:write_u16(a, 0xDEAD) end for c = 0, 255 do SP:write_u16(GPAL + c*2, 0xDEAD) end SP:write_u32(LDARK, 0xFFFFFFFF) if mode & 4 ~= 0 then for a = P6TAB, TABEND - 2, 2 do SP:write_u16(a, 0xDEAD) end end end local function setup() MODE.apply(SP) push(RAW, data, 1, META.raw_len) for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end P(string.format("loaded loadgate.bin=%d B, raw container header %d B at 0x%X", #code, META.raw_len, RAW)) end local function launch(mode, iter) poison(mode) SP:write_u32(LFLAG, 0) SP:write_u32(LHDR, RAW) SP:write_u32(LMODE, mode) SP:write_u32(LITER, 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 end -- Written after the mode-3 run, and only after it: it is the output of ONE -- do_load call over the whole header, which is what the player does. local function dump() local out = io.open("load_out.bin", "wb") for a = CB1, CB1 + META.cb1_len - 1 do out:write(string.char(SP:read_u8(a))) end for a = CB4, CB4 + META.cb4_len - 1 do out:write(string.char(SP:read_u8(a))) end for c = 0, 255 do out:write(string.pack(">I2", SP:read_u16(GPAL + c*2) & 0xFFFF)) end out:close() P(string.format("dumped %d B of 68000 output to tmp/load_out.bin", META.cb1_len + META.cb4_len + 512)) P(string.format("DARK=%d (host-side dlxload.py says %d), K1=%d K4=%d", SP:read_u32(LDARK), META.dark, SP:read_u32(LK1), SP:read_u32(LK4))) end -- Order matters: the scratch tables are built by the first stage and the -- palette-entry stage runs on them, which is exactly how a player would be -- arranged. The two stages that stand for real player events -- boot, and a -- scene change -- come last, and the dump the verifier checks is taken from the -- BOOT one, so the path that is proved correct is the one that builds -- everything from nothing. local PLAN = { {name="scratch tables only (boot, once)", mode=4, iter=ITER}, {name="codebook expansion only (P1)", mode=1, iter=ITER}, {name="palette entries only (P2)", mode=2, iter=ITER}, {name="BOOT: tables + codebooks + palette", mode=7, iter=ITER, dump=true}, {name="SCENE CHANGE: codebooks + palette", mode=3, iter=ITER}, } local step, st, t0 = 0, "boot", nil local results = {} 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].mode, PLAN[1].iter) st, t0 = "running", nil; return end if st == "running" then local fl = SP:read_u32(LFLAG) if fl == 1 and not t0 then t0 = t; return end if fl == 0xEE then P("BAD HEADER -- load.i did not find the 'DLX3' magic at LHDR") M:exit(); return end if fl == 0xFF then local p = PLAN[step] local dt = t - (t0 or t) local cyc = dt * CPUHZ / p.iter results[#results+1] = {name=p.name, cyc=cyc} P(string.format("%s: %d passes in %.4f s -> %.0f cycles = %.1f%% of a " .."%dfps frame (%.2f ms)", p.name, p.iter, dt, cyc, 100*cyc/FRAME12, FPS, 1000*cyc/CPUHZ)) if p.dump then dump() end step = step + 1 if PLAN[step] then launch(PLAN[step].mode, PLAN[step].iter); st, t0 = "running", nil else st = "finish" end return end if t > 400 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end return end if st == "finish" then P("---- summary (instruction cycles only; real RAM adds wait states) ----") for _,r in ipairs(results) do P(string.format(" %-44s %8.0f cyc %5.1f%% of a frame %6.2f ms", r.name, r.cyc, 100*r.cyc/FRAME12, 1000*r.cyc/CPUHZ)) end P("done") M:exit() end end) if not ok then print("[LOD] LUA ERROR: "..tostring(err)); M:exit() end end)