#!/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 data_disk=$repo/build/iigs/bin/joey.2mg 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" <= 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" &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