joeylib2/scripts/run-iigs.sh

427 lines
17 KiB
Bash
Executable file

#!/usr/bin/env bash
# Launch the built Apple IIgs examples.
#
# scripts/run-iigs.sh GSplus + Finder: boots GS/OS with
# joey.2mg mounted; pick the demo by
# double-clicking it in Finder.
# scripts/run-iigs.sh staxi MAME auto-launch: builds a
# single-example run disk
# (joey-run.2mg -- joey.2mg is left
# alone so benches stay honest),
# boots GS/OS at real speed, and
# drives Finder via Lua to launch
# the example. No clicking needed.
# GS/OS itself takes ~50 s to boot;
# that part is authentic.
#
# The MAME path needs the apple2gs ROM set (default ~/.mame/roms,
# override with MAME_ROMPATH). MAME_HEADLESS=1 runs it windowless with
# periodic PNG snapshots (batch verification).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/iigs/bin
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
# ---------------------------------------------------------------- MAME
# Example given: auto-launching MAME path.
if [[ $# -eq 1 ]]; then
target=${1^^}
if [[ ! -f "$bin_dir/$target" ]]; then
echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
if compgen -G "$bin_dir/*" > /dev/null; then
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -printf ' %f\n' \
| grep -vE '\.2mg$|\.txt$' >&2 || true
fi
exit 1
fi
if [[ ! -f $sys_disk ]]; then
echo "missing: $sys_disk (run ./toolchains/install.sh)" >&2
exit 1
fi
# ------------------------------------------------------------- AGI
# AGI needs ~880KB (240KB interpreter + 644KB KQ3 resources), which
# overflows the 800KB 3.5" floppy the generic path uses, and MAME's
# IIgs 3.5 drive is 800K-GCR-only. So AGI runs off a CFFA2 CompactFlash
# card instead: partition 1 (-hard1) is a bootable GS/OS, partition 2
# (-hard2) is a 2MB ProDOS volume with AGI + the game data. The boot
# scan hits slot 7 first, boots GS/OS from hard1, and GS/OS then mounts
# the AGI volume; the Lua driver opens it and launches AGI. No BSS/BRAM
# or startup-slot changes are needed with this two-partition topology.
if [[ $target == AGI ]]; then
game=${JOEY_AGI_GAME:-kq3}
# make-agi-iigs-disk.sh needs the toolchain env (cadius path).
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
# MAME loads the CFFA2 firmware as an a2cffa2 device ROM. Prefer the
# copy staged in the repo (toolchains/install.sh territory) so the
# run does not depend on the user's ~/.mame; fall back to the user
# rompath. Legal: the firmware is freely redistributed by dreher.net
# and the card is owned.
staged_roms=$repo/toolchains/emulators/support/mame-roms
user_roms="${MAME_ROMPATH:-$HOME/.mame/roms}"
rompath="$user_roms"
if [[ -f "$staged_roms/a2cffa2/cffa20eec02.bin" ]]; then
rompath="$user_roms;$staged_roms"
elif [[ ! -f "$user_roms/a2cffa2/cffa20eec02.bin" ]]; then
cat >&2 <<EOF
missing CFFA2 firmware: a2cffa2/cffa20eec02.bin
Needed to mount AGI's data disk (4096 bytes, SHA1
080ff88f19de22328e162954ee2b51ee65f9d5cd). It ships in dreher.net's
Run6_CDROM.zip as Firmware/V2.0/CFFA20EEC02.BIN. Drop it in either
$staged_roms/a2cffa2/ or $user_roms/a2cffa2/
EOF
exit 1
fi
headless=${MAME_HEADLESS:-0}
work=$(mktemp -d -t joeylib-run-agi.XXXXXX)
mame_pid=""
cleanup() {
if [[ -n $mame_pid ]] && kill -0 "$mame_pid" 2>/dev/null; then
kill "$mame_pid" 2>/dev/null || true
sleep 1
kill -9 "$mame_pid" 2>/dev/null || true
fi
rm -rf "$work"
}
trap cleanup EXIT INT TERM
# hard1: writable bootable GS/OS copy. hard2: freshly built AGI vol.
cp "$sys_disk" "$work/boot.po"
echo "run-iigs: building AGI CFFA data disk ($game)..."
"$repo/scripts/make-agi-iigs-disk.sh" "$work/agi.2mg" "$game"
# Finder driver: at calibrated frame counts type A (select the AGI
# volume -- boot volume is System.Disk, so 'A' is unambiguous), Cmd-O
# to open it, the full name AGI to select the app, Cmd-O to launch.
cat > "$work/launch.lua" <<LUA
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 headless = ($headless == 1)
local frames = 0
local steps = {
{ 3000, function() nat:post("A") end },
{ 3120, function() press(key_cmd) end },
{ 3126, function() nat:post("o") end },
{ 3180, function() release(key_cmd) end },
{ 3540, function() nat:post("AGI") end },
{ 3660, function() press(key_cmd) end },
{ 3666, function() nat:post("o") end },
{ 3720, function() release(key_cmd) end },
}
if headless then
table.insert(steps, { 4200, function() manager.machine.video:snapshot() end })
table.insert(steps, { 6600, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9000, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9100, function() manager.machine:exit() end })
end
table.sort(steps, function(a, b) return a[1] < b[1] end)
local step_idx = 1
emu.register_periodic(function()
frames = frames + 1
while step_idx <= #steps and frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
end)
LUA
video_arg="-window"
sound_arg=""
throttle_arg=""
if [[ "$headless" = "1" ]]; then
video_arg="-video soft"
sound_arg="-sound none"
throttle_arg="-nothrottle"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
else
export SDL_MOUSE_RELATIVE_MODE_WARP=0
fi
cat <<EOF
MAME apple2gs: AGI ($game) on a CFFA2 disk. GS/OS boots from hard1
(~50 s) and mounts the AGI volume on hard2; the Lua driver opens it and
launches AGI. Quit MAME (or close its window) to end the session.
EOF
cd "$work"
# -ramsize 4M: AGI's ~240KB app plus GS/OS/Finder leaves the stock
# 2MB Memory Manager pool too tight -- its first jlAlloc (the view
# cache) returns NULL and the app exits. 4MB is the assumed target.
mame apple2gs \
-ramsize 4M \
-rompath "$rompath" \
-sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/agi.2mg" \
$video_arg $sound_arg $throttle_arg \
-skip_gameinfo \
-snapshot_directory "$work/snap" \
-autoboot_script "$work/launch.lua" &
mame_pid=$!
wait "$mame_pid" || true
mame_pid=""
if [[ -d $work/snap ]]; then
rm -rf /tmp/run-iigs-snap
mv "$work/snap" /tmp/run-iigs-snap
echo "run-iigs: snapshots -> /tmp/run-iigs-snap"
fi
exit 0
fi
# Dedicated run disk so joey.2mg (the bench/golden disk) is never
# replaced -- a stale-swapped joey.2mg silently corrupts the next
# UBER bench run. Rebuild when missing, holding a different
# example, or older than the example binary.
run_disk=$bin_dir/joey-run.2mg
stamp=$run_disk.contents
need_disk=0
if [[ ! -f $run_disk ]]; then
need_disk=1
elif [[ "$(cat "$stamp" 2>/dev/null)" != "$target" ]]; then
need_disk=1
elif [[ "$bin_dir/$target" -nt $run_disk ]]; then
need_disk=1
fi
if [[ $need_disk -eq 1 ]]; then
if [[ -z "${LLVM816_ROOT:-}" ]]; then
# make-iigs-disk.sh needs the toolchain env (cadius path).
# shellcheck disable=SC1091
source "$repo/toolchains/env.sh" >/dev/null 2>&1 || true
fi
echo "run-iigs: building single-example run disk ($target)..."
JOEY_DISK_EXAMPLES="$target" "$repo/scripts/make-iigs-disk.sh" "$run_disk"
printf '%s' "$target" > "$stamp"
else
echo "run-iigs: run disk is current ($target)"
fi
work=$(mktemp -d -t joeylib-run-iigs.XXXXXX)
mame_pid=""
cleanup() {
# Kill the emulator if we are exiting first (Ctrl-C, timeout):
# bash does not forward signals to a foreground child, and an
# orphaned MAME keeps burning CPU with its work dir deleted
# from under it.
if [[ -n $mame_pid ]] && kill -0 "$mame_pid" 2>/dev/null; then
kill "$mame_pid" 2>/dev/null || true
sleep 1
kill -9 "$mame_pid" 2>/dev/null || true
fi
# Preserve the post-run disk (high scores etc.) for inspection.
if [[ -f $work/joey-run.2mg ]]; then
cp "$work/joey-run.2mg" "$bin_dir/joey-run-after.2mg"
fi
rm -rf "$work"
}
trap cleanup EXIT INT TERM
cp "$sys_disk" "$work/boot.po"
cp "$run_disk" "$work/joey-run.2mg"
# Slim Finder driver -- the keyboard-injection steps proven in
# run-iigs-mame.sh's harness, without the debugger/crash plumbing:
# at calibrated frame counts, type J (select the JOEYLIB volume),
# Cmd-O (open it), the example's full name (select; full-name
# typing disambiguates e.g. DRAW vs DATA), Cmd-O (launch). Frame
# counts are emulated frames, so throttling doesn't shift them.
headless=${MAME_HEADLESS:-0}
cat > "$work/launch.lua" <<LUA
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_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 headless = ($headless == 1)
local frames = 0
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("$target") end },
{ 3660, function() press(key_cmd) end },
{ 3666, function() nat:post("o") end },
{ 3720, function() release(key_cmd) end },
}
if headless then
-- Batch verification: snapshot right after the launch lands, mid
-- asset load, and after the example settles on its first screen,
-- then exit so the caller regains control. Interactive runs never
-- exit on their own.
table.insert(steps, { 3900, function() manager.machine.video:snapshot() end })
table.insert(steps, { 6600, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9000, function() manager.machine.video:snapshot() end })
table.insert(steps, { 9100, function() manager.machine:exit() end })
end
local step_idx = 1
emu.register_periodic(function()
frames = frames + 1
if headless and frames % 600 == 0 then
-- Pacing heartbeat for batch runs (rescued with the snaps).
local f = io.open("launch-progress.txt", "w")
if f ~= nil then
f:write(string.format("frames=%d\n", frames))
f:close()
end
end
while step_idx <= #steps and frames >= steps[step_idx][1] do
steps[step_idx][2]()
step_idx = step_idx + 1
end
end)
LUA
video_arg="-window"
sound_arg=""
throttle_arg=""
if [[ "$headless" = "1" ]]; then
# Verification mode: no window, no sound, and unthrottled so
# the 12k-frame scripted run finishes in about a minute.
video_arg="-video soft"
sound_arg="-sound none"
throttle_arg="-nothrottle"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
fi
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
cat <<EOF
MAME apple2gs: auto-launching $target off $run_disk.
GS/OS takes ~50 s to reach Finder; the Lua driver then opens JOEYLIB
and launches $target for you. Quit MAME (or close its window) to end
the session.
EOF
cd "$work"
# Throttled (real IIgs speed) -- this is the "play it" path,
# unlike the verify/bench harnesses which run -nothrottle. Runs
# as a tracked child so the cleanup trap can reap it on Ctrl-C.
# -skip_gameinfo matters: without it MAME parks on the "driver is
# not working correctly" info screen waiting for a keypress and
# the machine never boots (the -debug harnesses never hit this).
# -snapshot_directory is explicit because MAME otherwise resolves
# it against the user's mame.ini (snapshots vanished into
# ~/.mame/snap during bring-up).
mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" \
-flop4 "$work/joey-run.2mg" \
$video_arg $sound_arg $throttle_arg \
-skip_gameinfo \
-snapshot_directory "$work/snap" \
-autoboot_script "$work/launch.lua" &
mame_pid=$!
wait "$mame_pid" || true
mame_pid=""
if [[ -d $work/snap ]]; then
rm -rf /tmp/run-iigs-snap
mv "$work/snap" /tmp/run-iigs-snap
echo "run-iigs: snapshots -> /tmp/run-iigs-snap"
fi
if [[ -f $work/launch-progress.txt ]]; then
cp "$work/launch-progress.txt" /tmp/run-iigs-progress.txt
fi
exit 0
fi
# -------------------------------------------------------------- GSplus
# No argument: original GSplus + interactive Finder path.
gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom
data_disk=$bin_dir/joey.2mg
null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom
for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do
if [[ ! -f $f ]]; then
echo "missing: $f" >&2
if [[ $f == "$data_disk" ]]; then
echo "run 'make iigs-disk' to build it." >&2
else
echo "run ./toolchains/install.sh (support files should have been staged)." >&2
fi
exit 1
fi
done
# GSplus writes back to disk images during the session; stage writable
# copies so repeated runs do not mutate the originals.
work=$(mktemp -d -t joeylib-iigs.XXXXXX)
cleanup() {
# Stash the post-run disk image for manual inspection.
if [[ -f $work/joey.2mg ]]; then
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg"
echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
fi
rm -rf "$work"
}
trap cleanup EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Stage the null slot-6 PROM as c600.rom in the work dir. GSplus loads
# any file named c600.rom in its search path (cwd first) as the slot 6
# ROM card, overriding the built-in Disk II firmware. The staged image
# is 256 bytes with a leading RTS and no Pascal 1.1 firmware signature,
# so the IIgs boot ROM's slot scan skips slot 6 and the two empty 5.25
# drives never get probed. Source: toolchains/install.sh's
# install_support_iigs_null_c600.
cp "$null_c600" "$work/c600.rom"
cat <<EOF
GSplus launching GS/OS 6.0.4.
Once Finder is up:
1. Open the JOEYLIB disk on the desktop.
2. Double-click whichever demo you want to run.
(Tip: '$0 <example>' auto-launches via MAME instead.)
EOF
# GSplus auto-creates config.kegs in its cwd on first run. cd into
# the scratch dir so that file lands there and gets cleaned up with
# the rest of the temp state, rather than polluting the directory
# the user invoked the script from.
cd "$work"
# GSplus accepts any config.kegs key as a CLI override via "-<key>
# <value>". g_limit_speed=2 caps emulation at the IIgs's native
# 2.8 MHz; without this override GSplus boots at 8 MHz and our
# performance characteristics don't match real hardware. The other
# settings (0=unlimited, 1=1.024 MHz, 3=8 MHz) are reachable through
# the in-emulator speed-cycle key if needed.
#
# No exec: let the bash EXIT trap fire so the scratch dir (and the
# config.kegs GSplus auto-creates) gets cleaned up.
"$gsplus" -rom "$rom" \
-s5d1 "$work/boot.po" \
-s5d2 "$work/joey.2mg" \
-g_limit_speed 2