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
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
-- 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)
|
||||
Reference in New Issue
Block a user