joeylib2/scripts/verify-x68000-input.sh

237 lines
8.7 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-input.sh - Headless typed-character acceptance gate for
# the X68000 port. Boots Human68k under the patched MAME with KEYS.X in
# AUTOEXEC.BAT, waits for the example's A5A5 sentinel to appear on the
# screen, types the RetroNet definition-of-done string on the emulated
# keyboard, and reads the verification scanline back out of text VRAM.
# KEYS encodes every character it received from jlInputGetChar as nibble
# pixel pairs, so the row must equal the typed ASCII exactly.
#
# Then the named keys the X68000 gives a second key for (src/x68000/hal.c's
# kKeyAlias): the keypad's 8/2/4/6 as the arrows, its ENTER as Return, and
# OPT.2 as Alt, each held alone and read back from KEYS's cells, and the
# real Up and the keypad's 8 held together, then either let go, which must
# leave Up down.
#
# Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128
# bytes per row, leftmost pixel in bit 7; the 320x200 stage is centred at
# origin (96, 156). Verification row = stage y 198 -> screen y 354.
#
# bash scripts/verify-x68000-input.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"
WALL="${X68K_INPUT_WALL:-900}"
target="192.168.1.10:6510"
[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-input: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-input: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68input.XXXXXX)
trap 'rm -rf "$work"' EXIT
x68kBootDisk "$work/boot.xdf" 'A:\KEYS.X' || exit 1
x68kXdf add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
cat > "$work/input.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local typedAt = 0
local keysAt = 0
local done = false
-- KEYS's row 4 cells (examples/keys: 28x26 cells 32 and 30 apart from
-- (2, 2)) for Return, Alt, Up, Down, Left, and Right: their centres' x, and
-- the row's centre y. A lit cell is colour 2.
local CELL_X = { 80, 144, 176, 208, 240, 272 }
local CELL_Y = 135
local LIT = 2
local STEP_FRAMES = 30
local FIELDS = { "Tenkey 8", "Tenkey 2", "Tenkey 4", "Tenkey 6", "Tenkey ENTER", "OPT.2", "Cursor Up" }
-- Each step holds these fields (every other is let go) and wants these
-- cells lit, in CELL_X's order.
local STEPS = {
{ "rest", {}, "000000" },
{ "keypad 8 as Up", { "Tenkey 8" }, "001000" },
{ "keypad 2 as Down", { "Tenkey 2" }, "000100" },
{ "keypad 4 as Left", { "Tenkey 4" }, "000010" },
{ "keypad 6 as Right", { "Tenkey 6" }, "000001" },
{ "keypad ENTER as Return", { "Tenkey ENTER" }, "100000" },
{ "OPT.2 as Alt", { "OPT.2" }, "010000" },
{ "Up", { "Cursor Up" }, "001000" },
{ "Up and keypad 8", { "Cursor Up", "Tenkey 8" }, "001000" },
{ "Up, keypad 8 let go", { "Cursor Up" }, "001000" },
{ "keypad 8, Up let go", { "Tenkey 8" }, "001000" },
{ "rest again", {}, "000000" },
}
local fields = {}
-- Stage pixel (px, y) -> its 4-bit colour index. The port presents to the
-- TEXT plane (src/x68000/hal.c X68K_PRESENT_TEXT): four bit planes 128 KB
-- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage
-- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at
-- 0xC00000, which this gate used to read.)
-- 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 textPix(px, y)
local sx = 96 + px
local addr = 0xE00000 + (156 + shownRow() + y) * 128 + (sx >> 3)
local bit = 7 - (sx & 7)
local v = 0
for p = 0, 3 do
v = v | (((mem:read_u8(addr + p * 0x20000) >> bit) & 1) << p)
end
return v
end
local function pix(px)
return textPix(px, 198)
end
local function findField(name)
for _, port in pairs(manager.machine.ioport.ports) do
if port.fields[name] then return port.fields[name] end
end
return nil
end
local function hold(names)
local held = {}
for _, name in ipairs(names) do held[name] = true end
for _, name in ipairs(FIELDS) do fields[name]:set_value(held[name] and 1 or 0) end
end
local function cells()
local lit = {}
for i, x in ipairs(CELL_X) do
lit[#lit + 1] = textPix(x, CELL_Y) == LIT and "1" or "0"
end
return table.concat(lit)
end
local function report()
local bytes = {}
for i = 0, 39 do
bytes[#bytes + 1] = string.format("%02X", (pix(2 * i) << 4) | pix(2 * i + 1))
end
io.write(string.format("VERIFY-X68K-INPUT frame=%d typedAt=%d row=%s\n",
frame, typedAt, table.concat(bytes)))
io.flush()
end
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if typedAt == 0 and frame > 600 and frame % 100 == 0 then
-- KEYS is up once initialPaint stamped the A5A5 sentinel.
if pix(0) == 0xA and pix(1) == 0x5 and pix(2) == 0xA and pix(3) == 0x5 then
typedAt = frame + 60
end
end
if typedAt ~= 0 and frame == typedAt then
nat:post("$target")
end
if typedAt ~= 0 and keysAt == 0 and frame >= typedAt + 3000 then
report()
for _, name in ipairs(FIELDS) do
fields[name] = findField(name)
if not fields[name] then
io.write("VERIFY-X68K-KEY missing field " .. name .. "\n")
io.flush()
done = true
manager.machine:exit()
return
end
end
keysAt = frame
end
if keysAt ~= 0 then
local step = (frame - keysAt) // STEP_FRAMES + 1
local into = (frame - keysAt) % STEP_FRAMES
if step > #STEPS then
done = true
manager.machine:exit()
return
end
if into == 0 then
hold(STEPS[step][2])
elseif into == STEP_FRAMES - 1 then
io.write(string.format("VERIFY-X68K-KEY %s|%s|%s\n", STEPS[step][1], cells(), STEPS[step][3]))
io.flush()
end
end
if frame > 30000 then
done = true
io.write("VERIFY-X68K-INPUT timeout: sentinel never appeared\n")
io.flush()
manager.machine:exit()
end
end)
LUA
# Run from the work dir so MAME's cfg/ and nvram/ droppings land there
# instead of the caller's cwd.
cd "$work"
out=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/input.lua" </dev/null 2>&1) || true
line=$(echo "$out" | grep -E '^VERIFY-X68K-INPUT ' | tail -1)
echo "$line"
if [ -z "$line" ] || echo "$line" | grep -q timeout; then
echo "verify-x68000-input: FAIL - KEYS never came up" >&2
echo "$out" | tail -10 >&2
exit 1
fi
row=$(echo "$line" | sed -E 's/.*row=([0-9A-Fa-f]+).*/\1/')
expect="A5A5"
expect+=$(printf '%02X' ${#target})
expect+="00"
expect+=$(printf '%s' "$target" | od -A n -t x1 | tr -d ' \n' | tr 'a-f' 'A-F')
got=${row:0:${#expect}}
if [ "$got" != "$expect" ]; then
echo "verify-x68000-input: FAIL" >&2
echo " expected row prefix: $expect" >&2
echo " got: $got" >&2
exit 1
fi
if echo "$out" | grep -q '^VERIFY-X68K-KEY missing'; then
echo "verify-x68000-input: FAIL - $(echo "$out" | grep '^VERIFY-X68K-KEY missing' | sed 's/^VERIFY-X68K-KEY //')" >&2
exit 1
fi
keyFails=""
keySteps=0
while IFS='|' read -r name lit want; do
keySteps=$((keySteps + 1))
echo "verify-x68000-input: $name: Return/Alt/Up/Down/Left/Right $lit"
[ "$lit" = "$want" ] || keyFails="${keyFails:+$keyFails; }$name: lit $lit, want $want"
done < <(echo "$out" | grep -E '^VERIFY-X68K-KEY ' | sed 's/^VERIFY-X68K-KEY //')
if [ "$keySteps" = 0 ]; then
echo "verify-x68000-input: FAIL - the named-key steps did not run" >&2
exit 1
fi
if [ -n "$keyFails" ]; then
echo "verify-x68000-input: FAIL - $keyFails" >&2
exit 1
fi
echo "verify-x68000-input: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar; $keySteps named-key steps: the keypad's arrows and ENTER, OPT.2 as Alt, Up held with its alias)"