65816-llvm-mos/scripts/runMultiSeg.sh
2026-09-29 17:22:52 -05:00

181 lines
6.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# runMultiSeg.sh — run a multi-segment program in MAME via a
# mini in-Lua loader. Reads the link816 manifest, loads each
# segment's image at its base address, sets PC to segment 1's
# entry, lets the program run, then reads check-address values.
#
# Usage: runMultiSeg.sh <manifest.json> [check args like runInMame.sh]
#
# Optional hooks for long-running programs (all off by default):
# MAME_DONE_ADDR / MAME_DONE_VAL run the checks as soon as the u16 at
# DONE_ADDR reads DONE_VAL, instead of at MAME_CHECK_FRAME (which then
# only caps the run), so the wall time follows the program's own.
# MAME_TICK_ADDR store the emulated time in milliseconds there (u32) every
# frame: a clock for bare-metal code, which runs with IRQs off.
# MAME_TEXT_ADDR print the NUL-terminated text there (4 KB at most) as
# MAME-TEXT lines when the checks run; its first byte is zeroed at load.
set -euo pipefail
source "$(dirname "$0")/common.sh"
MANIFEST="$1"
shift
CHECK_FRAME=${MAME_CHECK_FRAME:-300}
SECS=${MAME_SECS:-6}
DONE_ADDR=${MAME_DONE_ADDR:-}
DONE_VAL=${MAME_DONE_VAL:-}
TICK_ADDR=${MAME_TICK_ADDR:-}
TEXT_ADDR=${MAME_TEXT_ADDR:-}
# Build address list as Lua table entries, mirroring runInMame.sh.
LUA_CHECKS=""
EXPECT_LIST=()
ADDR_LIST=()
if [ "$1" = "--check" ]; then
shift
for pair in "$@"; do
ADDR="${pair%=*}"
EXP="${pair#*=}"
ADDR_LIST+=("$ADDR")
EXPECT_LIST+=("$EXP")
LUA_CHECKS="$LUA_CHECKS print(string.format('MAME-READ addr=0x%06x val=0x%04x', $ADDR, mem:read_u16($ADDR)))"$'\n'
done
else
ADDR="$1"
EXP="$2"
ADDR_LIST+=("$ADDR")
EXPECT_LIST+=("$EXP")
LUA_CHECKS="print(string.format('MAME-READ addr=0x%06x val=0x%04x', $ADDR, mem:read_u16($ADDR)))"
fi
[ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
# Parse manifest with python (every machine has it). Emit a Lua
# table of (image_path, base, entry_offset_from_seg1).
PARSED=$(python3 - <<EOF
import json, os, sys
m = json.load(open("$MANIFEST"))
for s in m["segments"]:
base = int(s["base"], 16)
entry = int(s.get("entry_offset", "0x0"), 16) if s["num"] == 1 else 0
sz = s["size"]
print(f'{s["image"]}|{base}|{entry}|{sz}')
EOF
)
[ -n "$PARSED" ] || die "manifest parse failed"
LUA_PATH=$(mktemp --suffix=.lua)
# MAME's cfg / nvram (the IIgs battery RAM) go to a throwaway directory:
# run from a test directory with no mame.ini, MAME would otherwise leave
# them there, and a saved BRAM would carry settings from run to run.
MAME_STATE=$(mktemp -d)
trap 'rm -f "$LUA_PATH"; rm -rf "${MAME_STATE:?}"' EXIT
# Build the per-segment load lua.
LOAD_LUA=""
ENTRY_BASE=0
ENTRY_OFF=0
while IFS='|' read -r img base entry sz; do
LOAD_LUA="$LOAD_LUA
do
local f = io.open('$img', 'rb')
if not f then print('SEG-MISSING $img'); manager.machine:exit(); return end
local data = f:read('*all'); f:close()
for i = 1, #data do
local addr = $base + i - 1
if not (addr >= 0x00C000 and addr < 0x00D000) then
mem:write_u8(addr, data:byte(i))
end
end
print('SEG-LOADED base=0x' .. string.format('%06x', $base) .. ' bytes=' .. #data)
end
"
if [ "$entry" != "0" ] || [ "$ENTRY_BASE" = "0" ]; then
ENTRY_BASE="$base"
ENTRY_OFF="$entry"
fi
done <<< "$PARSED"
# Lua fragments for the optional hooks.
# Each hook's memory is cleared at load, so stale RAM can neither end the
# run early nor pass for program output.
DONE_LUA="local done = false"
INIT_LUA=""
if [ -n "$DONE_ADDR" ]; then
DONE_LUA="local done = loaded and mem:read_u16($DONE_ADDR) == $DONE_VAL"
INIT_LUA="$INIT_LUA mem:write_u16($DONE_ADDR, 0)"
fi
TICK_LUA=""
if [ -n "$TICK_ADDR" ]; then
TICK_LUA="if loaded then mem:write_u32($TICK_ADDR, math.floor(emu.time() * 1000) & 0xFFFFFFFF) end"
fi
TEXT_LUA=""
if [ -n "$TEXT_ADDR" ]; then
INIT_LUA="$INIT_LUA mem:write_u8($TEXT_ADDR, 0)"
TEXT_LUA="local text = ''
for i = 0, 0x0FFF do
local c = mem:read_u8($TEXT_ADDR + i)
if c == 0 then break end
text = text .. string.char(c)
end
for line in text:gmatch('[^\\n]+') do print('MAME-TEXT ' .. line) end"
fi
cat > "$LUA_PATH" <<EOF
local frame = 0
local loaded = false
emu.register_frame_done(function()
frame = frame + 1
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
if frame == 30 and not loaded then
$LOAD_LUA
$INIT_LUA
loaded = true
cpu.state["PC"].value = $ENTRY_BASE + $ENTRY_OFF
cpu.state["PB"].value = ($ENTRY_BASE >> 16) & 0xff
cpu.state["DB"].value = 0x00
cpu.state["D"].value = 0x00
cpu.state["P"].value = 0x34
cpu.state["E"].value = 0
cpu.state["S"].value = 0x01FF
print('MAME-READY pc=0x' .. string.format('%06x', $ENTRY_BASE + $ENTRY_OFF))
end
$TICK_LUA
$DONE_LUA
if done or frame == $CHECK_FRAME then
print(string.format('MAME-STOP frame=%d secs=%.3f', frame, emu.time()))
$TEXT_LUA
$LUA_CHECKS
manager.machine:exit()
end
end)
EOF
RAMSIZE_ARG=()
if [ -n "${MAME_RAMSIZE:-}" ]; then
RAMSIZE_ARG=(-ramsize "$MAME_RAMSIZE")
fi
OUT=$(SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout "${MAME_TIMEOUT:-30}" mame apple2gs \
"${RAMSIZE_ARG[@]}" \
-rompath "$PROJECT_ROOT/tools/mame/roms" \
-cfg_directory "$MAME_STATE" -nvram_directory "$MAME_STATE" \
-plugins -autoboot_script "$LUA_PATH" \
-video none -sound none -nothrottle -seconds_to_run "$SECS" 2>&1 | /usr/bin/grep -E "^(MAME-|SEG-)")
echo "$OUT"
# Only the MAME-READ lines: MAME-TEXT is program output and could contain
# anything.
mapfile -t GOT_LIST < <(printf '%s\n' "$OUT" | /usr/bin/grep '^MAME-READ' | /usr/bin/grep -oE 'val=0x[0-9a-f]+' | sed 's/val=0x//')
ok=1
for i in "${!EXPECT_LIST[@]}"; do
if [ "${GOT_LIST[$i]:-}" != "${EXPECT_LIST[$i]}" ]; then
warn "MAME mismatch at ${ADDR_LIST[$i]}: got 0x${GOT_LIST[$i]:-MISSING} expected 0x${EXPECT_LIST[$i]}"
ok=0
fi
done
if [ $ok -eq 1 ]; then
log "MAME (multi-seg) OK: ${#EXPECT_LIST[@]} reads matched"
exit 0
fi
exit 1