-- VRAM wait-state measurement.
--
-- Assembles a tight 68000 loop into RAM and gets it executed by hooking the
-- interrupt vectors -- MAME's m68k ignores writes to the rPC state entry, so
-- the CPU cannot simply be pointed at the code. Whichever interrupt fires
-- first enters the routine, which masks interrupts, runs the loop, writes a
-- sentinel and RTEs.
--
-- Loop body is 8 unrolled `tst.w <target>.L` + subq.l + bne.s:
--   nominal 68000 cost = 8*18 + 8 + 10 = 162 cycles/iteration at zero wait.
-- Running the identical loop against main memory and against VRAM isolates the
-- per-access wait: the only difference between runs is the address touched.
--
-- TARGET_ADDR and ITER_COUNT are substituted by the harness.
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local frame = 0

local CODE     = 0x010000
local SENTINEL = 0x011000
local TARGET   = TARGET_ADDR
local ITERS    = ITER_COUNT
local UNROLL   = 8
local NOMINAL  = 8 * 16 + 8 + 10   -- 146: TST.W (xxx).L = 4 + 12 ea

local armed, t0 = false, nil

local function w16(a, v) mem:write_u16(a, v) end

local function assemble()
    local p = CODE
    w16(p, 0x007C); p = p + 2                       -- ori.w #$0700,sr
    w16(p, 0x0700); p = p + 2
    w16(p, 0x203C); p = p + 2                       -- move.l #ITERS,d0
    w16(p, (ITERS >> 16) & 0xFFFF); p = p + 2
    w16(p, ITERS & 0xFFFF); p = p + 2
    local loop = p
    for _ = 1, UNROLL do
        w16(p, 0x4A79); p = p + 2                   -- tst.w <TARGET>.L
        w16(p, (TARGET >> 16) & 0xFFFF); p = p + 2
        w16(p, TARGET & 0xFFFF); p = p + 2
    end
    w16(p, 0x5380); p = p + 2                       -- subq.l #1,d0
    w16(p, 0x6600 | ((loop - (p + 2)) & 0xFF)); p = p + 2   -- bne.s loop
    w16(p, 0x33FC); p = p + 2                       -- move.w #$BEEF,SENTINEL.L
    w16(p, 0xBEEF); p = p + 2
    w16(p, (SENTINEL >> 16) & 0xFFFF); p = p + 2
    w16(p, SENTINEL & 0xFFFF); p = p + 2
    w16(p, 0x4E73)                                  -- rte
end

emu.register_frame_done(function()
    frame = frame + 1

    if frame == 240 then
        w16(SENTINEL, 0x0000)
        assemble()
        if mem:read_u16(CODE) ~= 0x007C then
            io.write("WAITPROBE FAIL code did not land in RAM\n")
            io.flush(); manager.machine:exit(); return
        end
        -- Point the autovectors ($60-$7F) and the MFP vector block ($100-$13F)
        -- at the routine; whichever interrupt fires first runs it once.
        for a = 0x60, 0x7C, 4 do mem:write_u32(a, CODE) end
        for a = 0x100, 0x13C, 4 do mem:write_u32(a, CODE) end
        t0 = manager.machine.time:as_double()
        armed = true
    elseif armed and mem:read_u16(SENTINEL) == 0xBEEF then
        local dt = manager.machine.time:as_double() - t0
        local cycles = dt * 10000000.0
        local periter = cycles / ITERS
        io.write(string.format(
            "WAITPROBE target=%06X iters=%d sec=%.6f cyc=%.0f per_iter=%.3f extra_per_access=%.4f\n",
            TARGET, ITERS, dt, cycles, periter, (periter - NOMINAL) / UNROLL))
        io.flush()
        manager.machine:exit()
    elseif frame > 2000 then
        io.write("WAITPROBE TIMEOUT (routine never entered)\n")
        io.flush()
        manager.machine:exit()
    end
end)
