#!/usr/bin/env bash
# bench-iigs.sh - Headless UBER perf capture for the IIgs (the reference
# port). Until this script existed, PERF.md's IIgs column was carried over
# from old captures because nothing could get joeylog.txt off the emulated
# disk; every "hashes identical x4" gate needs this.
#
# Boots GS/OS under headless MAME with a WORK COPY of joey.2mg, drives the
# Finder to launch UBER (same keystroke timeline as verify-iigs.sh), then
# polls the Super Hi-Res framebuffer for UBER's solid-green "done" screen
# (jlSurfaceClear color 2 -> SHR bytes 0x22). When seen, MAME exits and
# cadius extracts JOEYLOG.TXT from the work image into build/iigs/bin/
# joeylog.txt, where tools/uber-perf-table and diff-uber-hashes expect it.
#
# Usage:   scripts/bench-iigs.sh
#   MAME_ROMPATH        override ROM dir (default ~/.mame/roms)
#   BENCH_MAX_FRAMES    hard frame cap before giving up (default 36000)
#
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg with UBER on
# it (run 'make iigs-disk', ensure UBER was not skipped for space).
set -euo pipefail

repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

: "${LLVM816_ROOT:?bench-iigs.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"

sys_disk=$repo/toolchains/emulators/support/gsos-system.po
# Overridable so a diagnostic build (e.g. the -DUBER_PROBE binary built by
# `make iigs-verify-shrtail`) can be run off its own single-app disk into its
# own log without disturbing the goldened joey.2mg / joeylog.txt pair. Both
# default to the standard locations, so normal bench runs are unchanged.
data_disk="${BENCH_DATA_DISK:-$repo/build/iigs/bin/joey.2mg}"
out_log="${BENCH_OUT_LOG:-$repo/build/iigs/bin/joeylog.txt}"
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
maxFrames="${BENCH_MAX_FRAMES:-36000}"

[ -x "$CADIUS" ] || { echo "bench-iigs: cadius not found at $CADIUS" >&2; exit 2; }
for f in "$sys_disk" "$data_disk"; do
    [ -f "$f" ] || { echo "bench-iigs: missing $f (run 'make iigs-disk')" >&2; exit 2; }
done
if ! "$CADIUS" CATALOG "$data_disk" 2>/dev/null | grep -qE '^  UBER +S16'; then
    echo "bench-iigs: UBER not on joey.2mg (800KB floppy full? JOEY_DISK_EXAMPLES=\"UBER\" scripts/make-iigs-disk.sh)" >&2
    exit 2
fi

work=$(mktemp -d -t joeylib-bench.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk"  "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"

# Finder keystroke timeline is verify-iigs.sh's, launching UBER. From frame
# 4500 on, sample the SHR framebuffer every 60 frames: UBER ends on a solid
# color-2 clear + present, so nearly every byte reads 0x22. The red "running"
# bar and the showcase can never look like that. On green, exit; the log was
# already flushed (uber.c calls jlLogFlush before the key wait).
cat > "$work/bench.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1

local function field(port, name)
    local p = manager.machine.ioport.ports[port]
    if p == nil then return nil end
    return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f)   if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end

local function greenDone()
    -- 64 samples spread across the 32000-byte SHR buffer; solid color-2
    -- fill means byte 0x22 everywhere.
    local hits = 0
    for i = 0, 63 do
        if mem:read_u8(0xE12000 + i * 500) == 0x22 then hits = hits + 1 end
    end
    return hits >= 60
end

-- UBER's progress mailbox in the unused SCB bytes (see uber.c):
-- 9DC8 opIndex, 9DC9 phase, 9DCA-B heartbeat, 9DCC-D last-seen tick,
-- 9DD8-B log-ring base address, 9DDC-F log-ring head-counter address.
local function mailbox()
    local op    = mem:read_u8(0xE19DC8)
    local phase = mem:read_u8(0xE19DC9)
    local hb    = mem:read_u8(0xE19DCA) | (mem:read_u8(0xE19DCB) << 8)
    local tick  = mem:read_u8(0xE19DCC) | (mem:read_u8(0xE19DCD) << 8)
    return string.format("op=%d phase=%d heartbeat=%d tick=%d", op, phase, hb, tick)
end

local function rd32(addr)
    return mem:read_u8(addr) | (mem:read_u8(addr+1) << 8) | (mem:read_u8(addr+2) << 16) | (mem:read_u8(addr+3) << 24)
end

-- Live drain of the IIgs RAM log ring (src/core/debug.c): the app pokes
-- the ring base and head-counter addresses into the mailbox at startup;
-- we read new bytes each frame and emit whole lines as UBERLOG events.
-- The head is a free-running 16-bit total; ring size 8192 must match
-- JOEY_LOG_RING_BYTES.
local RING_SIZE = 8192
local logRingBase = 0
local logHeadAddr = 0
local logRead = 0
local logPartial = ""
local function drainLog()
    if logRingBase == 0 then
        -- Latch only after the app's magic word appears: the SCB region
        -- holds 0x80 fill before uberMbLayout() runs, and a garbage
        -- latch reads noise forever.
        local magic = mem:read_u8(0xE19DCE) | (mem:read_u8(0xE19DCF) << 8)
        if magic ~= 0x4A4C then
            return
        end
        logRingBase = rd32(0xE19DD8) & 0xFFFFFF
        logHeadAddr = rd32(0xE19DDC) & 0xFFFFFF
        if logRingBase ~= 0 then
            io.write(string.format("BENCH-IIGS logring base=%06X head=%06X\n", logRingBase, logHeadAddr))
            io.flush()
        end
        return
    end
    local head = mem:read_u8(logHeadAddr) | (mem:read_u8(logHeadAddr + 1) << 8)
    while logRead ~= head do
        local b = mem:read_u8(logRingBase + (logRead % RING_SIZE))
        logRead = (logRead + 1) & 0xFFFF
        if b == 10 then
            io.write("UBERLOG " .. logPartial .. "\n")
            logPartial = ""
        elseif b >= 32 and b < 127 then
            logPartial = logPartial .. string.char(b)
        end
    end
    io.flush()
end

local steps = {
    {3000, function() nat:post("J") end},
    {3120, function() press(key_cmd) end},
    {3126, function() nat:post("o") end},
    {3180, function() release(key_cmd) end},
    {3540, function() nat:post("UBER") end},
    {3660, function() press(key_cmd) end},
    {3666, function() nat:post("o") end},
    {3720, function() release(key_cmd) end},
}

local pcs = {}

emu.register_frame_done(function()
    frame = frame + 1
    while idx <= #steps and frame >= steps[idx][1] do
        steps[idx][2]()
        idx = idx + 1
    end
    if frame >= 4200 and frame % 20 == 0 then
        drainLog()
    end
    if frame >= 4500 and frame % 2000 == 0 then
        io.write(string.format("BENCH-IIGS progress frame=%d %s\n", frame, mailbox()))
        -- TEMP #84: sprite-geometry diagnostics from sprite.c pokes.
        io.write(string.format("BENCH-IIGS spritediag drawW=%d drawH=%d tiles=%04X saveW=%d saveH=%d\n",
            mem:read_u8(0xE19DE0) | (mem:read_u8(0xE19DE1) << 8),
            mem:read_u8(0xE19DE2) | (mem:read_u8(0xE19DE3) << 8),
            mem:read_u8(0xE19DE4) | (mem:read_u8(0xE19DE5) << 8),
            mem:read_u8(0xE19DE6) | (mem:read_u8(0xE19DE7) << 8),
            mem:read_u8(0xE19DE8) | (mem:read_u8(0xE19DE9) << 8)))
        -- TEMP #84: watch the sprite struct bytes in place (16 bytes at
        -- the pointer setupSprite poked; printable ASCII shown).
        local sptr = rd32(0xE19DEA) & 0xFFFFFF
        if sptr ~= 0 then
            local bytes = {}
            local text = {}
            for i = 0, 15 do
                local b = mem:read_u8(sptr + i)
                bytes[#bytes + 1] = string.format("%02X", b)
                if b >= 32 and b < 127 then
                    text[#text + 1] = string.char(b)
                else
                    text[#text + 1] = "."
                end
            end
            io.write(string.format("BENCH-IIGS spritemem ptr=%06X %s |%s| stagePtr=%08X\n",
                sptr, table.concat(bytes, " "), table.concat(text), rd32(0xE19DD0)))
        end
        io.flush()
    end
    if frame >= 4500 and frame % 60 == 0 and greenDone() then
        io.write(string.format("BENCH-IIGS done frame=%d %s\n", frame, mailbox()))
        io.flush()
        manager.machine:exit()
    end
    -- On timeout, sample the PC across 40 frames first so a hang site
    -- can be resolved against the link map (ALIGN=0x10000 means the
    -- bank-local offset equals the link offset within its segment).
    if frame >= $maxFrames and frame < $maxFrames + 40 then
        local pc = cpu.state["PC"].value
        pcs[string.format("%06X", pc)] = (pcs[string.format("%06X", pc)] or 0) + 1
    end
    if frame >= $maxFrames + 40 then
        io.write(string.format("BENCH-IIGS timeout frame=%d %s\n", frame, mailbox()))
        for k, v in pairs(pcs) do
            io.write(string.format("BENCH-IIGS pc %s count=%d\n", k, v))
        end
        io.flush()
        manager.machine:exit()
    end
end)
LUA

echo "bench-iigs: running UBER headless under MAME (cap $maxFrames frames)..." >&2
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
    timeout 1200 mame apple2gs \
        -rompath "$rompath" \
        -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
        -video none -sound none -nothrottle \
        -autoboot_script "$work/bench.lua" </dev/null 2>&1) || true

echo "$out" | grep -E '^BENCH-IIGS (progress|pc|spritediag|spritemem|logring)' | sed 's/^/bench-iigs: /' >&2
marker=$(echo "$out" | grep -E '^BENCH-IIGS (done|timeout)' | tail -1)
echo "bench-iigs: $marker" >&2
if echo "$marker" | grep -q timeout; then
    echo "bench-iigs: WARNING -- frame cap hit before the green done screen (partial capture likely)" >&2
fi
if [ -z "$marker" ]; then
    echo "bench-iigs: FAIL -- MAME produced no marker (boot/launch failed)" >&2
    echo "$out" | tail -15 >&2
    exit 1
fi

# Primary log source: the RAM-ring drain (UBERLOG lines emitted live by
# the Lua probe -- immune to the FST non-persistence and floppy-write
# stalls of findings #80/#82). Falls back to cadius extraction of the
# on-disk joeylog.txt only if the ring never produced anything.
echo "$out" | grep -E '^UBERLOG ' | sed 's/^UBERLOG //' > "$out_log"
if [ ! -s "$out_log" ]; then
    echo "bench-iigs: no ring log captured; falling back to on-disk extraction" >&2
    "$CADIUS" EXTRACTFILE "$work/joey.2mg" "/JOEYLIB/JOEYLOG.TXT" "$work/" >/dev/null 2>&1 || true
    extracted=$(find "$work" -maxdepth 1 -iname 'joeylog.txt*' | head -1)
    if [ -z "$extracted" ]; then
        echo "bench-iigs: FAIL -- no ring log AND no JOEYLOG.TXT on the work image" >&2
        "$CADIUS" CATALOG "$work/joey.2mg" >&2 || true
        exit 1
    fi
    # cadius may suffix the ProDOS type (#04xxxx); normalize and strip CRs.
    tr -d '\r' < "$extracted" > "$out_log"
fi

ops=$(grep -c "ops/sec" "$out_log" || true)
chks=$(grep -c "UBER-CHK" "$out_log" || true)
done_marker=$(grep -c "press any key to exit" "$out_log" || true)
echo "bench-iigs: captured $ops op lines, $chks check lines (complete=$done_marker)" >&2
echo "  -> $out_log" >&2
grep "UBER" "$out_log" | sed 's/ | hash=.*//' || true
