Two sessions, unrecorded until now, committed together because their edits share files and cannot be split cleanly after the fact. Session 28 (FINDINGS 60): the container is DLX5 -- every record sector-aligned, 120/120 starting on a boundary where 3/120 did, +0.48% on the wire and zero clocks -- and the ring's release rounds to RECALN so no pad is stranded. Two encoder levers measured and refused: `--spans all` buys +0.19 dB for +67% of the wire, and joint span/lam selection emits byte-identical containers because `lam` never leaves its floor on any of 120 frames. Session 29 (FINDINGS 61): the packed full-frame blit is 27.3% of a 12 fps frame, a channel fills GVRAM in buffer mode off the disc with the CPU halted, and it walks the 1,024 B line stride itself through array chaining. At the 9 clk/B dual-address floor the codec is 110.4% of a frame and a decoder-free packed literal player is 55.2%, at +4.89 dB -- 2.75 dB past a ceiling the codec's scene-wide palette cannot cross. Encoder work is parked; the codec is kept and not built on. check.sh is ALL GREEN before and after, plus one new stage that gates the ORDER of the measured paint costs rather than their values. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
183 lines
7.2 KiB
Lua
183 lines
7.2 KiB
Lua
-- 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/56.69 s = 17.64 ms -- MAME's, not
|
|
-- the hardware's 55.46; see crtc_mode.lua), 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, SRCP = 0x60000, 0x80000, 0x90000
|
|
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)"},
|
|
-- V8 is V1 with R20 bit 11's packing: 48KB read + 48KB write for the SAME
|
|
-- 49,152 pixels. It is the per-frame work of a decoder-free packed player
|
|
-- (FINDINGS 44.7 / 46.6 / 47.5), and 47.6.1 filed its `movem` shape as an
|
|
-- ASSUMPTION -- this is the measurement that assumption was standing in for.
|
|
{var=8, iter=200, name="V8 PACKED movem.l blit (48KB read + 48KB write, same 49,152 pixels as V1)"},
|
|
-- V9/V10 are the two ways a BLOCK decoder could survive the packed layout
|
|
-- (47.6.4, open since session 16): sixteen move.b at stride 2 per block, or
|
|
-- pair the blocks 128 columns apart in the encoder and get V4's movem back.
|
|
{var= 9, iter= 40, name="V9 PACKED block order, 16 move.b at stride 2 per 4x4 block"},
|
|
{var=10, iter=120, name="V10 PACKED block order, blocks PAIRED so a movem writes whole words"},
|
|
}
|
|
|
|
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
|
|
-- SRCP: the PACKED frame, interleaved the way tools/bench/show_frame256_packed.lua
|
|
-- lays it out -- word i of a row is (column i+128) << 8 | (column i), because
|
|
-- page 0 is the low byte at screen column i and page 1 the high byte at i+128.
|
|
-- Only V8 reads it, and only its SIZE (128 words a row) affects the timing;
|
|
-- the interleave is written correctly so the buffer is the real artefact and
|
|
-- not a same-sized stand-in.
|
|
for y = 0, H-1 do
|
|
local row = PIX0 + y*W
|
|
for i = 0, (W//2)-1 do
|
|
SP:write_u16(SRCP + y*(W//2)*2 + i*2, (B(row+i+W//2) << 8) | B(row+i))
|
|
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)
|