#!/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-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" < its colour 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 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