ROADMAP P3 said "needs MFP timer or VBL" and neither can do it. The MFP's timer clock is 16 MHz/4, its prescalers stop at 200 and its data register is 8 bits, so the slowest tick any single timer can make is 78.125 Hz -- 6.5x faster than a frame -- and 4e6/12 is not an integer, so no setting reaches 12 Hz at all. The raster has no whole divide near 12 either: 4 refreshes is 13.86 fps and 5 is 11.09. tools/analysis/23_frame_clock.py walks all 7x256 timer settings rather than asserting it. src/player/clock.i takes the V-DISP falling edge on MFP GPIP4 -- the start of vertical blanking, which is when a player would present -- and adds fps*VTOTAL per edge to a 16-bit accumulator, emitting a tick at 31,500 and keeping the remainder. The long-run rate is fps*VTOTAL/VTOTAL = 12.000000 fps exactly, and both constants are read out of the CRTC at init, so the clock is derived from the registers that generate the raster it counts. Measured over 3,000 refreshes: 3,000 interrupts, 649 ticks where 649.1429 were due. It costs 181.35 clocks per V-DISP, 838 per frame, 0.1006% of the budget -- timed by the 68000 itself, because the host's granularity is 17.64 ms and the interrupt is microseconds. The loop's own cost was calibrated rather than looked up and landed on 38.000002 clocks, which both licenses the subtraction and confirms buscost.py's model; the 181.35 then decomposes exactly, leaving 43.99 clocks for the interrupt exception -- the textbook 44, measured. THE ONE THAT MOVES SOMETHING: 12 fps on a 55.4577 Hz raster is 4.6215 refreshes, so a frame is shown for 4 refreshes (72.13 ms) or 5 (90.16 ms), 37.9% of them short. The 833,333-clock budget every figure in this project is priced against is the MEAN slot, and the short one is 13.4% under it. The cadence was already in the tree unnamed: stream.lua's tick is sampled at frame boundaries, so its gaps were always 4 or 5, and every host-paced result in FINDINGS 49/51 carried it. P3 moved who produces it onto the machine and made it visible. It is not a dropped frame -- the pace gate lets an overrun eat the next frame's idle -- and on the gate container it costs 4 frames of 120 their idle against 1 for the nominal model, most of that the frame-0 transient at 111% of budget. stream.s counts it now, and the rig matches an offline model of the divider exactly. Also struck: MAME's raster runs 2.22% fast. refresh_mode() builds the frame period from scr.max_x*scr.max_y with scr.max_x = m_htotal - 8, one character cell short and an inclusive bound used as a count, so it runs at 56.6901 Hz where the registers say 55.4577 -- agreeing to six digits with the arithmetic. Every "1/55.46 s granularity" note in this tree was wrong and is 1/56.69 s, corrected in six files with the derivation put once in crtc_mode.lua. No conclusion changes and no 68000 cycle figure moves; the CPU clock is unrelated to the screen. But anything paced by the raster runs fast under MAME, so the rig reports both rates and prices the interrupt against the hardware's. decode.s and frame.i are unchanged; decode.bin is still 1,296 B at the same MD5. The pace gate's wait loop is byte-for-byte the one FINDINGS 51 measured and the free-running path executes none of the new code. check.sh gains two stages: the clock's own measurement, and 120 frames decoded pixel-exact with nothing outside the machine deciding when a frame may start. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
161 lines
5.8 KiB
Lua
161 lines
5.8 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 = 0x60000, 0x80000
|
|
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)"},
|
|
}
|
|
|
|
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
|
|
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)
|