#!/usr/bin/env bash # 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). # # GVRAM geometry (crtmod 13, 256-colour graphics plane): 16-bit word # per pixel at $C00000, 512 words per row; the 320x200 stage is centred # at origin (96, 156). Mouse verification row = stage y 199 -> GVRAM # y 355. # # X68K_SCRATCH= bash scripts/verify-x68000-mouse.sh set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" MAME="$repo/toolchains/cache/mame-mame0264/x68k" TEMPLATE="$SP/x68mame/HUMAN302.XDF" WALL="${X68K_MOUSE_WALL:-900}" [ -x "$MAME" ] || { echo "verify-x68000-mouse: missing patched MAME at $MAME" >&2; exit 2; } [ -f "$TEMPLATE" ] || { echo "verify-x68000-mouse: missing $TEMPLATE" >&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 printf 'A:\\KEYS.X\r\n\x1a' > "$work/AUTOEXEC.BAT" cp "$TEMPLATE" "$work/boot.xdf" for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 done python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1 python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null 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) -> GVRAM word low byte. local function pix(px, y) local rowBase = 0xC00000 + ((156 + y) * 512 + 96) * 2 return mem:read_u8(rowBase + 2 * px + 1) 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 "$SP/x68mame/roms" -flop1 "$work/boot.xdf" \ -video none -sound none -nothrottle \ -autoboot_script "$work/mouse.lua" &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