src/player/load.i expands both codebooks to word-per-pixel form and packs the palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against tools/bench/dlxload.py on both CPU cores. The palette half is gated on words read back out of the palette registers at $E82000, so "the words reached the hardware" is part of what passes. ROADMAP P1 is done; P2's encoder half (a reserved black entry, 23.4) is not, and is a re-encode rather than an edit. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot costs 24.70 ms. The scratch tables describe the CRTC, not the scene, so pal_tables is a separate entry point built once at boot -- 5.29 ms off every scene change. The one that moves something: the scene header is 5,920 B that no rate table in this tree included, because it belongs to no frame record. In FINDINGS 51.3's currency it is divided by the surplus pipe - wire, so it is hypersensitive: 138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the same bytes. tools/analysis/22_scene_load.py prices it across explicit rates. Recorded as open: the two CPU cores agree to <3% on every stage but the table build, where they differ by 16.4%. px68k's C68K charges a flat 50 clocks for MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains 4,608 of the 8,703 clock gap. 4,095 clocks are unexplained. Nothing else in src/player/ multiplies, so no figure in FINDINGS 24-52 is affected. decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same MD5. check.sh gains a stage that gates byte-exactness on both cores and deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a wall timing would make the green light host-sensitive. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
179 lines
7.6 KiB
Lua
179 lines
7.6 KiB
Lua
-- 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/55.46 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)
|