joeylib2/scripts/verify-x68000-mouse.sh

247 lines
10 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-mouse.sh - Headless mouse acceptance gate for the X68000
# port. Boots Human68k under the patched MAME with KEYS.X in
# AUTOEXEC.BAT, waits for the mouse verification scanline's 5A5A
# sentinel in GVRAM, then drives the emulated mouse (an x68k_mouse
# serial device on SCC channel B, decoded by the real IOCS ROM) through
# MAME's input-port override API and reads the scanline back after each
# step. Covered: delta accumulation in both directions, clamping at all
# four stage edges, movement after clamping, and press/hold/release of
# both buttons.
#
# The mouse axes are 12-bit absolute counters the device diffs into
# delta packets, so each Lua set_value(v) below moves the mouse by
# (v - previous v). Values are biased to 2000 mid-boot for headroom in
# both directions. MAME's analog override is timing-sensitive: an
# injected delta is sometimes consumed once and sometimes re-sent over
# several solicitations, so exact interior positions are NOT testable
# through this API. Every position expectation therefore lands ON a
# clamp edge, where over-travel is absorbed: each axis is slammed
# independently into all four stage edges, which proves delta sign,
# axis attribution, and all four clamps deterministically. Slams use
# five 127-unit steps (635 > 319 even if two adjacent steps merge into
# one clamped 127-unit packet).
#
# 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). Mouse verification row = stage y 199 -> screen y 355.
#
# bash scripts/verify-x68000-mouse.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_MOUSE_WALL:-900}"
[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-mouse: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-mouse: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68mouse.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/mouse.lua" <<'LUA'
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local frame = 0
local t0 = 0
local biased = false
local done = false
local checks = 0
local fails = {}
-- 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, y)
return textPix(px, y)
end
-- Decode the mouse verification row (stage y 199): 5A5A sentinel,
-- present/left/right/middle nibbles, then x and y as 4 nibbles each.
local function readMouse()
local m = {}
m.sentinel = pix(0, 199) == 0x5 and pix(1, 199) == 0xA and
pix(2, 199) == 0x5 and pix(3, 199) == 0xA
m.present = pix(4, 199)
m.left = pix(5, 199)
m.right = pix(6, 199)
m.middle = pix(7, 199)
m.x = (pix(8, 199) << 12) | (pix(9, 199) << 8) | (pix(10, 199) << 4) | pix(11, 199)
m.y = (pix(12, 199) << 12) | (pix(13, 199) << 8) | (pix(14, 199) << 4) | pix(15, 199)
return m
end
-- The x68k mouse device (rs232 slot "mouse_port", option "x68k") owns
-- three ports: X and Y are 12-bit absolute counters it diffs into
-- delta packets, BTN holds left (mask 2) and right (mask 1).
local ports = manager.machine.ioport.ports
local axisX, axisY, btnLeft, btnRight
local px_port = ports[":mouse_port:x68k:X"]
local py_port = ports[":mouse_port:x68k:Y"]
local pb_port = ports[":mouse_port:x68k:BTN"]
if px_port then for _, f in pairs(px_port.fields) do axisX = f end end
if py_port then for _, f in pairs(py_port.fields) do axisY = f end end
if pb_port then
for _, f in pairs(pb_port.fields) do
if f.mask == 2 then btnLeft = f end
if f.mask == 1 then btnRight = f end
end
end
if not (axisX and axisY and btnLeft and btnRight) then
io.write("VERIFY-X68K-MOUSE error: mouse ioports not found\n")
io.flush()
manager.machine:exit()
end
local function expect(name, got, want)
checks = checks + 1
if got ~= want then
fails[#fails + 1] = string.format("%s=%d(want %d)", name, got, want)
end
end
local function expectRow(step, m, x, y, l, r)
checks = checks + 1
if not m.sentinel then fails[#fails + 1] = step .. ":sentinel" end
expect(step .. ":present", m.present, 1)
expect(step .. ":x", m.x, x)
expect(step .. ":y", m.y, y)
expect(step .. ":left", m.left, l)
expect(step .. ":right", m.right, r)
expect(step .. ":middle", m.middle, 0)
end
-- Steps run at fixed offsets from t0 (the frame the sentinel landed
-- plus settle time). Slam sub-steps are 15 frames apart and every
-- read sits 60+ frames after the last injection, so the 4800-baud
-- packets, the IOCS solicitation, and the app's poll all settle.
-- Down-slams walk the counter 2000 -> 1365, up-slams walk it back.
local function walkDown(axis, at, steps)
for i = 1, 5 do
steps[#steps + 1] = { at = at + 15 * (i - 1), run = function() axis:set_value(2000 - 127 * i) end }
end
end
local function walkUp(axis, at, steps)
for i = 1, 5 do
steps[#steps + 1] = { at = at + 15 * (i - 1), run = function() axis:set_value(1365 + 127 * i) end }
end
end
local steps = {}
-- Slam both axes to the (0, 0) corner: any possible starting position
-- is absorbed by the clamps, making the state absolute from here on.
walkDown(axisX, 0, steps)
walkDown(axisY, 0, steps)
steps[#steps + 1] = { at = 120, run = function() expectRow("slamOrigin", readMouse(), 0, 0, 0, 0) end }
-- One axis at a time into each remaining edge: proves delta sign and
-- axis attribution (the untouched axis must stay put) plus the clamp.
walkUp(axisX, 130, steps)
steps[#steps + 1] = { at = 250, run = function() expectRow("slamRight", readMouse(), 319, 0, 0, 0) end }
walkUp(axisY, 260, steps)
steps[#steps + 1] = { at = 380, run = function() expectRow("slamBottom", readMouse(), 319, 199, 0, 0) end }
walkDown(axisX, 390, steps)
steps[#steps + 1] = { at = 510, run = function() expectRow("slamLeft", readMouse(), 0, 199, 0, 0) end }
walkDown(axisY, 520, steps)
steps[#steps + 1] = { at = 640, run = function() expectRow("slamTop", readMouse(), 0, 0, 0, 0) end }
-- Button choreography at a settled position.
steps[#steps + 1] = { at = 650, run = function() btnLeft:set_value(1) end }
steps[#steps + 1] = { at = 710, run = function() expectRow("leftPress", readMouse(), 0, 0, 1, 0) end }
steps[#steps + 1] = { at = 740, run = function() expectRow("leftHold", readMouse(), 0, 0, 1, 0) end }
steps[#steps + 1] = { at = 750, run = function() btnLeft:clear_value(); btnRight:set_value(1) end }
steps[#steps + 1] = { at = 810, run = function() expectRow("rightPress", readMouse(), 0, 0, 0, 1) end }
steps[#steps + 1] = { at = 820, run = function() btnRight:clear_value() end }
steps[#steps + 1] = { at = 880, run = function()
expectRow("released", readMouse(), 0, 0, 0, 0)
done = true
io.write(string.format("VERIFY-X68K-MOUSE checks=%d fails=%d%s\n",
checks, #fails,
#fails > 0 and (" detail=" .. table.concat(fails, ",")) or ""))
io.flush()
manager.machine:exit()
end }
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if not biased and frame == 300 then
-- Mid-boot counter bias for two-directional headroom. However
-- the boot-time solicitation history consumes this, the slam
-- steps above make the position deterministic afterwards.
biased = true
axisX:set_value(2000)
axisY:set_value(2000)
end
if t0 == 0 and frame > 600 and frame % 100 == 0 then
-- KEYS is up once both verification sentinels are stamped.
if pix(0, 198) == 0xA and pix(1, 198) == 0x5 and
pix(0, 199) == 0x5 and pix(1, 199) == 0xA then
t0 = frame + 60
end
end
if t0 ~= 0 then
for _, s in ipairs(steps) do
if frame == t0 + s.at then s.run() end
end
end
if t0 == 0 and frame > 30000 then
done = true
io.write("VERIFY-X68K-MOUSE 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/mouse.lua" </dev/null 2>&1) || true
line=$(echo "$out" | grep -E '^VERIFY-X68K-MOUSE ' | tail -1)
echo "$line"
if [ -z "$line" ] || echo "$line" | grep -q -e timeout -e error; then
echo "verify-x68000-mouse: FAIL - KEYS never came up or ports missing" >&2
echo "$out" | tail -10 >&2
exit 1
fi
if echo "$line" | grep -q 'fails=0$'; then
echo "verify-x68000-mouse: PASS (deltas, all four clamps, and both buttons verified through the IOCS ROM)"
else
echo "verify-x68000-mouse: FAIL" >&2
exit 1
fi