Files
Dragon-s-Lair-X68k/tools/bench/load.lua
T
prosolis 7179339bd2 Move the loader onto the 68000, and find 5,920 bytes nobody counted
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
2026-08-24 20:20:40 -07:00

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)