joeylib2/scripts/verify-x68000-golden.sh
2026-10-19 15:46:47 -05:00

188 lines
7.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-x68000-golden.sh - X68000 golden-hash gate.
#
# Builds UBER with a 1-frame measurement window (the timings are meaningless at
# that setting; this run is for the HASHES), stages it on a Human68k image,
# runs it headless under the patched MAME, extracts joeylog.txt back off the
# image with xdftool, and diffs the hashes against the Apple IIgs reference.
#
# Takes a few minutes. The run EXITS ON COMPLETION, not on a frame budget: UBER
# ends by clearing the whole stage to color index 2 and presenting it before it
# blocks in jlWaitForAnyKey, so the Lua below watches GVRAM for that screen.
# (It used to burn a flat 300,000 frames -- ~90 minutes of emulated time -- a
# budget sized back when this port still drew through the slow generic renderer.
# The actual work is a Human68k boot plus well under 10,000 frames.)
# X68K_GOLDEN_FRAMES is now only a backstop: reaching it means the done-screen
# was never detected, which is a FAILURE, not a normal exit.
#
# bash scripts/verify-x68000-golden.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed X68K_DIR x68kFirmware
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
source "$repo/scripts/lib/x68kDisk.sh"
GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
WALL="${X68K_GOLDEN_WALL:-7200}"
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in
# src/x68000/hal.c (1 -> text, 0 -> gfx). Only the completion DETECTOR cares;
# the hashes are of the surface in RAM and are the same either way.
PRESENT_PLANE="${X68K_PRESENT_PLANE:-text}"
joeyNeed X68K_ROOT toolchain:x68000
export PATH="$X68K_ROOT/bin:$PATH"
work=$(mktemp -d -t joey-x68gold.XXXXXX)
trap 'rm -rf "$work"' EXIT
# -s (strip) keeps the staged binary small. (It used to be load-bearing: the
# data disk was built by deleting the template's root FILES, which leaves the
# stock ~900 KB of Human68k utilities in SYS/ BIN/ BASIC2/ ... still allocated
# and only ~330 KB usable. `xdftool empty` frees the whole 1221-cluster disk,
# so the size pressure is gone -- the strip is now just tidiness.)
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 -DUBER_FRAMES=1u \
-I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \
"$repo/examples/uber/uber.c" \
"$repo/build/x68000/lib/libjoey.a" -lm \
-o "$work/UBER.X" || exit 1
# TWO DISKS: the boot disk carries only Human68k and AUTOEXEC, while the binary
# AND joeylog.txt live on the second disk on B:. AUTOEXEC switches to B: before
# launching so the log lands there too.
x68kBootDisk "$work/gold.xdf" 'B:' 'B:\UBER.X' || exit 1
x68kDataDisk "$work/data.xdf" || exit 1
x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(jlgStage, 2) + jlStagePresent, so an all-color-2 stage in GVRAM
# is the "hashes are computed and flushed" signal. The benchmark's own
# jlSurfaceClear/jlStagePresent ops can paint that color for an instant, so the
# reading only counts once it has held for GREEN_STABLE frames -- the finished
# screen never changes again, a mid-benchmark frame never lasts.
#
# jlLogFlush only fflush()es; the directory entry is finalized by the atexit
# fclose. So the keypress that releases jlWaitForAnyKey still has to be posted,
# and MAME must not be killed until UBER has returned to Human68k -- hence the
# repeated posts and the EXIT_SETTLE wait before exiting.
cat > "$work/gold.lua" <<LUA
local mem = manager.machine.devices[":maincpu"].spaces["program"]
local frame = 0
local greenRun = 0
local exitAt = 0
local done = false
local FRAMES = $FRAMES
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
-- Where the present puts pixels, which depends on the port's
-- X68K_PRESENT_TEXT switch:
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
-- bytes per row, planes 128 KB apart. A pixel's color is
-- one bit from each plane at the same offset.
-- gfx -- crtmod 12, 256-color GRAPHICS plane at \$C00000, one
-- 16-bit word per pixel, 512 words per row.
-- The 320x200 stage is centered at origin (96, 156) either way.
local presentPlane = "$PRESENT_PLANE"
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(px, py)
if presentPlane == "gfx" then
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
end
local x = 96 + px
local byteOff = (156 + shownRow() + py) * 128 + (x >> 3)
local bit = 0x80 >> (x & 7)
local v = 0
for plane = 0, 3 do
if (mem:read_u8(0xE00000 + plane * 0x20000 + byteOff) & bit) ~= 0 then
v = v | (1 << plane)
end
end
return v
end
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
{4,196}, {160,196}, {315,196}, {80,50}, {240,150}, {200,80} }
local function screenIsDone()
for _, p in ipairs(kProbes) do
if stagePixel(p[1], p[2]) ~= 2 then
return false
end
end
return true
end
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if frame % 2000 == 0 then io.write("GOLD f"..frame.."\n"); io.flush() end
if exitAt == 0 then
if screenIsDone() then
greenRun = greenRun + 1
if greenRun >= GREEN_STABLE then
io.write("GOLD done-screen f"..frame.."\n"); io.flush()
exitAt = frame + EXIT_SETTLE
end
else
greenRun = 0
end
else
-- Release jlWaitForAnyKey; extra presses land harmlessly at the prompt.
if frame % 60 == 0 then manager.machine.natkeyboard:post(" ") end
if frame >= exitAt then
done = true
io.write("GOLD exit f"..frame.."\n"); io.flush()
manager.machine:exit()
end
end
-- Backstop only: reaching this means the done-screen never appeared.
if frame > FRAMES then
done = true
io.write("GOLD budget-exhausted f"..frame.."\n"); io.flush()
manager.machine:exit()
end
end)
LUA
# In the work dir, so MAME cfg/ and nvram/ never persist between runs.
runLog=$(cd "$work" && timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/gold.lua" </dev/null 2>&1 | grep '^GOLD')
echo "$runLog"
# The backstop is not a normal exit: it means UBER never reached its done-screen,
# so whatever log is on the disk (if any) is from an unfinished run.
if echo "$runLog" | grep -q 'budget-exhausted'; then
echo "verify-x68000-golden: FAIL - UBER's done-screen never appeared within $FRAMES frames" >&2
echo " (raise X68K_GOLDEN_FRAMES if UBER legitimately got slower; otherwise the run died early)" >&2
exit 1
fi
if ! echo "$runLog" | grep -q 'done-screen'; then
echo "verify-x68000-golden: FAIL - MAME stopped before UBER finished (wall timeout ${WALL}s?)" >&2
exit 1
fi
x68kXdf extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
echo "verify-x68000-golden: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
exit 1
}
tr -d '\r' < "$work/x68.txt" > "$work/x68.clean"
cp "$work/x68.clean" "$repo/build/x68000/joeylog.txt"
"$(joeyTool diffUberHashes)" "$GOLDEN" "$work/x68.clean"