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

293 lines
11 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-joy.sh - Joystick gate for the X68000: both ports, every
# direction, both triggers, and every release, driven through MAME's joystick
# inputs and read back from what examples/joy draws.
#
# The X68000's two Atari-style ports are 8255 PPI ports A and B ($E9A001,
# $E9A003); MAME plugs an FM Towns two-button pad into each (x68k.cpp
# MSX_GENERAL_PURPOSE_PORT "townspad"), whose lines reach the PPI exactly as a
# stick's would: up, down, left, right on bits 0-3 and triggers A and B on bits
# 5 and 6, all active low. The Lua below holds those lines with
# ioport_field:set_value, which MAME applies without its 4/8-way filter, so
# diagonals are exact.
#
# JOY.X (examples/joy) redraws, for each stick, a dot at one of nine positions
# (the axis signs; green off-center, gray at rest), two trigger squares, and a
# patch that says whether the port reports a stick. The Lua decodes all of it
# from the text plane and compares the WHOLE state of BOTH sticks after every
# step: each direction alone, two diagonals, each trigger alone and both
# together, with a release after every press so a latched line shows up, and a
# step holding different lines on both sticks at once so crosstalk between the
# ports shows up too.
#
# Text-plane geometry: four 1bpp planes 0x20000 apart at 0xE00000, 128 bytes
# per row, leftmost pixel in bit 7; the 320x200 stage is centered at origin
# (96, 156).
#
# Usage: scripts/verify-x68000-joy.sh
# Env: JOY_PROGRAM run this build of JOY.X instead (negative controls)
# Requires: MAME and the X68000 firmware and CG ROM; build/x68000/bin/JOY.X
# (make x68000).
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"
joeyNeedFile "$X68K_SYSTEM_DISK" x68kFirmware
program="${JOY_PROGRAM:-$repo/build/x68000/bin/JOY.X}"
wall=600 # hard cap on MAME, whatever happens to this script
frames=20000 # emulated frames before the Lua gives up on the program
if [ ! -f "$program" ]; then
echo "verify-x68000-joy: $program is not built; run: make x68000" >&2
exit 2
fi
work=$(mktemp -d -t joeylib-x68-joy.XXXXXX)
mame=""
mameChild=""
cleanup() {
joeyStop "$mameChild" "$mame"
rm -rf "$work"
}
trap cleanup EXIT
# The boot disk carries only Human68k and AUTOEXEC; the program lives on the
# data disk in B:, which AUTOEXEC makes current.
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\JOY.X' || exit 1
x68kDataDisk "$work/data.xdf" || exit 1
x68kXdf add "$work/data.xdf" "$program" JOY.X > /dev/null || exit 1
cat > "$work/joy.lua" <<LUA
local mem = manager.machine.devices[":maincpu"].spaces["program"]
local FRAMES = $frames
local SAMPLE = 30 -- frames between looks for the program
local SETTLE = 20 -- frames between holding the lines and reading back
-- The port's display contract (src/x68000/x68kPlanar.h).
local ORIGIN_X, ORIGIN_Y = 96, 156
local TVRAM, PLANE_STEP, TVRAM_STRIDE, PLANES = 0xE00000, 0x20000, 128, 4
-- examples/joy/joy.c: its layout and colors.
local STICK_LEFT = { 24, 216 }
local STICK_TOP = 16
local FRAME_SIZE = 80
local FRAME_INSET = 2
local DOT_SIZE = 8
local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 - FRAME_INSET
local BUTTON_W, BUTTON_H, BUTTON_GAP = 24, 16, 4
local ROW_GAP = 6
local BUTTON_TOP = STICK_TOP + FRAME_SIZE + ROW_GAP
local STATUS_W, STATUS_H = 80, 4
local STATUS_TOP = BUTTON_TOP + BUTTON_H + ROW_GAP
local COLOR_BACKGROUND, COLOR_DOT_OFF, COLOR_DOT_ON = 0, 2, 3
local COLOR_BUTTON_OFF, COLOR_BUTTON_ON = 4, 5
local COLOR_CONNECTED, COLOR_DISCONNECTED = 6, 7
-- MAME's pad on each port (townspad.cpp): one field per line.
local LINE = { up = 0x001, down = 0x002, left = 0x004, right = 0x008, a = 0x010, b = 0x020 }
local PAD = { ":joy1:townspad:PAD", ":joy2:townspad:PAD" }
local function out(text)
io.write("JOY-X68K " .. text .. "\n")
io.flush()
end
local fields = {}
for stick = 1, 2 do
local port = manager.machine.ioport.ports[PAD[stick]]
fields[stick] = {}
if port then
for _, f in pairs(port.fields) do
for name, mask in pairs(LINE) do
if f.mask == mask then
fields[stick][name] = f
end
end
end
end
for name in pairs(LINE) do
if not fields[stick][name] then
out("error: " .. PAD[stick] .. " has no " .. name .. " line")
manager.machine:exit()
return
end
end
end
-- Stage pixel (x, y) -> its color index, one bit from each text 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(x, y)
local sx = ORIGIN_X + x
local addr = TVRAM + (ORIGIN_Y + shownRow() + y) * TVRAM_STRIDE + (sx >> 3)
local bit = 7 - (sx & 7)
local v = 0
for p = 0, PLANES - 1 do
v = v | (((mem:read_u8(addr + p * PLANE_STEP) >> bit) & 1) << p)
end
return v
end
-- What JOY shows for one stick, as "x,y,a,b,connected": x and y are the axis
-- signs (-1, 0, 1), read from which of the nine dot positions is lit.
local function readStick(stick)
local left = STICK_LEFT[stick]
local lit = {}
for sy = -1, 1 do
for sx = -1, 1 do
local c = stagePixel(left + FRAME_SIZE // 2 - DOT_SIZE // 2 + sx * DOT_TRAVEL + DOT_SIZE // 2,
STICK_TOP + FRAME_SIZE // 2 - DOT_SIZE // 2 + sy * DOT_TRAVEL + DOT_SIZE // 2)
if c ~= COLOR_BACKGROUND then
local want = (sx == 0 and sy == 0) and COLOR_DOT_OFF or COLOR_DOT_ON
lit[#lit + 1] = (c == want) and string.format("%d,%d", sx, sy) or string.format("dot%d?", c)
end
end
end
local dot = (#lit == 1) and lit[1] or ("dots[" .. table.concat(lit, ";") .. "]")
local centerX = left + FRAME_SIZE // 2
local function button(x)
local c = stagePixel(x + BUTTON_W // 2, BUTTON_TOP + BUTTON_H // 2)
if c == COLOR_BUTTON_ON then return "1" end
if c == COLOR_BUTTON_OFF then return "0" end
return "btn" .. c .. "?"
end
local statusColor = stagePixel(left + (FRAME_SIZE - STATUS_W) // 2 + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
local status = (statusColor == COLOR_CONNECTED) and "1" or ((statusColor == COLOR_DISCONNECTED) and "0" or ("st" .. statusColor .. "?"))
return dot .. "," .. button(centerX - BUTTON_W - BUTTON_GAP // 2) .. "," .. button(centerX + BUTTON_GAP // 2) .. "," .. status
end
-- The state a set of held lines must produce: positive wins a pair, as on the
-- Amiga and ST (src/x68000/hal.c joyUpdate); every port reports a stick.
local function expected(held)
local function sign(neg, pos)
if held[pos] then return 1 end
if held[neg] then return -1 end
return 0
end
return string.format("%d,%d,%d,%d,1", sign("left", "right"), sign("up", "down"),
held.a and 1 or 0, held.b and 1 or 0)
end
local function hold(stick, held)
for name, f in pairs(fields[stick]) do
if held[name] then
f:set_value(1)
else
f:clear_value()
end
end
end
-- Each step holds these lines on sticks 1 and 2, then reads both back.
local NONE = {}
local steps = { { "rest", NONE, NONE } }
local singles = {
{ "up", { up = true } }, { "down", { down = true } },
{ "left", { left = true } }, { "right", { right = true } },
{ "upLeft", { up = true, left = true } }, { "downRight", { down = true, right = true } },
{ "a", { a = true } }, { "b", { b = true } }, { "ab", { a = true, b = true } },
}
for stick = 1, 2 do
for _, s in ipairs(singles) do
local one = (stick == 1) and s[2] or NONE
local two = (stick == 2) and s[2] or NONE
steps[#steps + 1] = { "s" .. stick .. s[1], one, two }
steps[#steps + 1] = { "s" .. stick .. s[1] .. "Released", NONE, NONE }
end
end
steps[#steps + 1] = { "bothSticks", { up = true, right = true, a = true }, { down = true, left = true, b = true } }
steps[#steps + 1] = { "bothReleased", NONE, NONE }
local frame = 0
local readyAt = 0
local stepAt = 0
local step = 0
local checks = 0
local fails = {}
local done = false
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if readyAt == 0 then
-- JOY is up once both status patches are painted, in either color.
if frame % SAMPLE == 0 then
local one = stagePixel(STICK_LEFT[1] + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
local two = stagePixel(STICK_LEFT[2] + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
if (one == COLOR_CONNECTED or one == COLOR_DISCONNECTED) and
(two == COLOR_CONNECTED or two == COLOR_DISCONNECTED) then
readyAt = frame + SAMPLE
out("up f" .. frame)
end
end
if frame > FRAMES then
out("verdict FAIL: JOY never came up (f" .. frame .. ")")
done = true
manager.machine:exit()
end
return
end
if frame < readyAt then return end
if step == 0 or frame == stepAt + SETTLE then
if step > 0 then
local s = steps[step]
local want = expected(s[2]) .. " " .. expected(s[3])
local got = readStick(1) .. " " .. readStick(2)
checks = checks + 1
if got ~= want then
fails[#fails + 1] = s[1] .. ": got " .. got .. " want " .. want
out("step " .. s[1] .. " FAIL got " .. got .. " want " .. want)
end
end
step = step + 1
if step > #steps then
out(string.format("checks=%d fails=%d", checks, #fails))
out("verdict " .. ((#fails == 0) and "PASS" or "FAIL"))
done = true
manager.machine:exit()
return
end
hold(1, steps[step][2])
hold(2, steps[step][3])
stepAt = frame
end
end)
LUA
# From the work dir, so MAME's cfg/ and nvram/ never persist between runs.
# Under timeout so a hung run cannot outlive $wall whatever happens to this
# script; mameChild is MAME itself, which cleanup stops first.
cd "$work" || exit 1
timeout -s KILL "$wall" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/joy.lua" < /dev/null > "$work/mame.log" 2>&1 &
mame=$!
mameChild=$(joeyChild "$mame")
wait "$mame"
mame=""
mameChild=""
out=$(grep '^JOY-X68K' "$work/mame.log")
echo "${out:-JOY-X68K: no report from the Lua (MAME died, or hit the ${wall}s cap)}"
if grep -q '^JOY-X68K verdict PASS' <<< "$out"; then
echo "verify-x68000-joy: PASS"
exit 0
fi
echo "verify-x68000-joy: FAIL" >&2
exit 1