280 lines
11 KiB
Bash
Executable file
280 lines
11 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# 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-centre, grey 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 centred 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 colours.
|
|
local STICK_LEFT = { 24, 216 }
|
|
local STICK_TOP = 16
|
|
local FRAME_SIZE = 80
|
|
local DOT_SIZE = 8
|
|
local DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2
|
|
local BUTTON_W, BUTTON_H, BUTTON_GAP = 24, 16, 4
|
|
local BUTTON_TOP = STICK_TOP + FRAME_SIZE + 6
|
|
local STATUS_W, STATUS_H = 80, 4
|
|
local STATUS_TOP = BUTTON_TOP + BUTTON_H + 6
|
|
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 colour index, one bit from each text plane.
|
|
local function stagePixel(x, y)
|
|
local sx = ORIGIN_X + x
|
|
local addr = TVRAM + (ORIGIN_Y + 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 centreX = 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(centreX - BUTTON_W - BUTTON_GAP // 2) .. "," .. button(centreX + 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 colour.
|
|
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
|