joeylib2/scripts/verify-x68000-serial-mouse.sh

586 lines
23 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-serial-mouse.sh - the X68000's RS-232C port and mouse at once.
#
# Both live on the one Z8530 SCC: RS-232C on channel A, where JoeyLib's serial
# add-on owns the receive interrupt (src/x68000/x68kSccRxIsr.s), and the mouse
# on channel B, where the IOCS ROM owns the receive interrupt and solicits a
# packet every other Timer C tick. The chip has ONE register pointer for both
# channels, so a select-then-access register sequence split by the other
# side's interrupt goes to the wrong register, and the interrupt-under-service
# reset it swallows wedges the chip's interrupt chain. The serial and mouse
# gates each run one side alone; this one runs both together, hard, for long
# enough to matter.
#
# Boots SERMOUSE.X (examples/sermouse) under the patched MAME with SCC channel
# A on a null_modem socket and every SCC channel at the real chip's 3-byte
# receive FIFO (JOEY_Z80SCC_RX_FIFO=3), so an interrupt held off too long loses
# bytes here as it would on the machine. Then, all at once:
# serial the guest streams byte k = streamByte(k, 0xA5) and checks the
# peer's stream, byte k = streamByte(k, 0x00), in order; the host
# peer (scripts/lib/serialStreamPeer.py) checks the guest's and
# answers byte for byte. The guest also raises DTR 16 times a loop,
# the add-on's own select-then-write of channel A's WR5, so that
# sequence runs under both channels' interrupts all the time.
# mouse the Lua visits the four stage corners, pushing both axes into the
# corner EVERY frame -- a packet on channel B at every solicitation,
# while the pointer stays pinned where the clamp puts it -- checks
# the position there, then presses a button and checks it, then
# releases it (checked at the next corner). Clamp edges only: MAME's
# relative-axis injection is timing-sensitive, so interior positions
# are not testable (see verify-x68000-mouse.sh). Two left clicks a
# pass also prove the ROM's software keyboard stays off (hal.c
# X68K_SKEY_OFF): the second would bring it up and eat the next
# right click.
# Timeline: PASSES corner passes with the traffic flowing, every check also
# requiring both stream counts to have moved and no bad byte; SPACE pauses the
# guest's stream, the peer follows, and once quiet the guest's counts must
# hold still and equal a quiet snapshot of the peer's EXACTLY -- every byte
# sent either way arrived, in order; one more pass in the quiet; SPACE resumes
# and both counts must move again (the receive interrupt still fires at the
# end, the program still runs).
#
# MAME 0.264 needs one correction here, made in the Lua and not in MAME. Its
# Z8530 (src/devices/machine/z80scc.cpp) answers an interrupt acknowledge
# wrongly when BOTH channels have one pending: z80daisy_irq_ack puts the
# highest-priority source in service but returns m_chanB->m_rr2, the vector of
# whichever source was triggered LAST. The CPU then runs the other channel's
# handler, whose interrupt-under-service reset clears the source the chip
# acknowledged, and that channel's byte is left in its FIFO with nothing
# pending: the channel runs a byte behind until its FIFO overruns (serial
# loses bytes) or a stale byte starts a mouse packet (pointer and buttons go
# wrong). Measured before the correction: both, within seconds. Silicon
# returns the vector of the source it acknowledges, so a tap on the CPU's
# level-5 acknowledge rewrites the vector to that source's, and the verdict
# line reports how many acknowledges it corrected (iackFixed). Only this gate
# keeps both channels busy at once, so no other gate needs it.
#
# The guest reports through two verification rows at the bottom of the stage
# (one nibble per pixel, high nibble first), read from the text plane:
# row 198 3C3C, status, received(8), bad, sent(8), short writes, first bad(8)
# row 199 5A5A, present/left/right/middle, x, y (examples/keys' mouse row)
# The serial row changes at most every 4 frames, so a read counts only when
# two consecutive frames agree (a present caught half done cannot).
#
# PASS: every Lua check right, the peer's stream PASS, at least MIN_BYTES each
# way, and the guest's quiet counts among the peer's quiet snapshots.
#
# Variants, built here into the work directory from examples/sermouse:
# X68K_SERMOUSE_BAUD=19200|38400 the line rate (default 9600, MAME's
# null_modem default); RSDRV.SYS, which the
# boot disk loads, is what reaches past 9600
# X68K_SERMOUSE_DENSE=n stage lines 0 .. n-1 change palette on
# every line (default 0: one palette), so
# the SCB raster interrupt holds IPL 6 for
# the whole run every frame while the
# streams and the mouse run
# scripts/verify-x68000-serial-scb.sh runs the dense combinations this port
# guarantees; see "Serial under a dense SCB" in claudeDocs/x68000_port.md.
#
# Requires: ./install.sh install x68000 -- the patched MAME
# (installer/patches/mame: 0002 for the FIFO depth, 0004 for the
# RS-232C slot and the mouse) and the Human68k system disk.
#
# bash scripts/verify-x68000-serial-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"
PORT="${X68K_SERMOUSE_PORT:-6801}"
WALL="${X68K_SERMOUSE_WALL:-600}"
PASSES="${X68K_SERMOUSE_PASSES:-3}"
MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}"
BAUD="${X68K_SERMOUSE_BAUD:-9600}"
DENSE="${X68K_SERMOUSE_DENSE:-0}"
PROG="$repo/build/x68000/bin/SERMOUSE.X"
FIFO_DEPTH=3
DEFAULT_BAUD=9600
# The peer outlives MAME by this much, so it always sees the link close, and
# is given this long to report once MAME is gone.
PEER_MARGIN=30
PEER_GRACE=10
[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-serial-mouse: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
[ -f "$PROG" ] || { echo "verify-x68000-serial-mouse: build SERMOUSE.X first (make -f make/x68000.mk)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68sermouse.XXXXXX)
peer=""
mame=""
mameChild=""
cleanup() {
joeyStop "$mameChild" "$mame" "$peer"
rm -rf "$work"
}
trap cleanup EXIT
# A variant is examples/sermouse built against the same library with its
# line rate and SCB set (see the header).
if [ "$BAUD" != "$DEFAULT_BAUD" ] || [ "$DENSE" != 0 ]; then
joeyNeed X68K_ROOT toolchain:x68000
"$X68K_ROOT/bin/m68k-xelf-gcc" -O2 -m68000 -fomit-frame-pointer \
-DJOEYLIB_PLATFORM_X68000 -DLINE_BAUD="${BAUD}u" -DDENSE_SCB_LINES="${DENSE}u" \
-I"$repo/include" \
"$repo/examples/sermouse/sermouse.c" \
"$repo/build/x68000/lib/libjoey.a" -lm \
-o "$work/SERMOUSE.X" || exit 1
PROG="$work/SERMOUSE.X"
fi
x68kBootDisk "$work/boot.xdf" 'A:\SERMOUSE.X' || exit 1
x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1
{
printf 'local PASSES = %d\n' "$PASSES"
printf 'local MIN_BYTES = %d\n' "$MIN_BYTES"
printf 'local BAUD = %d\n' "$BAUD"
printf 'local DENSE = %d\n' "$DENSE"
cat <<'LUA'
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local scc = manager.machine.devices[":scc"]
local sccA = manager.machine.devices[":scc:cha"]
local frame = 0
local t0 = 0
local biased = false
local done = false
local checks = 0
local fails = {}
local steps = {}
local pending = nil
local lastRaw = nil
local prev = nil
local quiet = nil
local pushX = 0
local pushY = 0
local axisValX = 0
local axisValY = 0
-- Stage geometry on 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).
local TEXT_BASE = 0xE00000
local PLANES = 4
local PLANE_BYTES = 0x20000
local ROW_BYTES = 128
local ORIGIN_X = 96
local ORIGIN_Y = 156
-- The verification rows (examples/sermouse/sermouse.c): 16-bit words and
-- 32-bit longs, one nibble per pixel, laid end to end from the sentinel.
local WORD = 4
local LONG = 8
local SERIAL_ROW = 198
local SERIAL_SENTINEL = 0x3C3C
local SERIAL_NIBBLES = 4 * WORD + 3 * LONG
local MOUSE_ROW = 199
local MOUSE_SENTINEL = 0x5A5A
local MOUSE_NIBBLES = 4 * WORD
local STAGE_RIGHT = 319
local STAGE_BOTTOM = 199
local STATUS_OPEN = 0x0001
local STATUS_PAUSED = 0x0002
local NO_BAD_INDEX = 0xFFFFFFFF
-- Mouse drive: the x68k mouse's X/Y are 12-bit absolute counters it diffs
-- into delta packets; biased mid-range so a pass (net zero per axis, at most
-- two phases one way) never wraps them.
local AXIS_BIAS = 2048
local PUSH_STEP = 6 -- counter units per frame, per axis
local PUSH_FRAMES = 90 -- 540 units per phase: over the 319-pixel stage with room to spare
local BUTTON_FRAMES = 40
local CORNER_FRAMES = PUSH_FRAMES + BUTTON_FRAMES
local PASS_FRAMES = 4 * CORNER_FRAMES
local QUIET_SETTLE = 120 -- frames after SPACE before the line is read as quiet
local QUIET_HOLD = 60 -- and again this much later: it must not move
local RESUME_FRAMES = 120
local BOOT_BIAS_FRAME = 300
local SENTINEL_FIRST = 600
local SENTINEL_EVERY = 50
local SETTLE_FRAMES = 60
local STEADY_LIMIT = 60 -- frames a serial read may take to see two agreeing frames
local BOOT_LIMIT = 30000
-- The acknowledge-vector correction (see the script header). MAME keeps the
-- chip's six interrupt sources in m_int_state, highest priority first,
-- channel A's three then channel B's, each INT (pending) and/or IEO (in
-- service); m_int_source holds each one's type, which with the channel is the
-- 3-bit status the vector carries in bits 3-1 (WR9 status-low, as the X68000
-- runs it). The 68000 fetches the level-5 vector at 0xFFFFFA in cpu space.
local IACK5 = 0xFFFFFA
local SCC_SOURCES = 6
local SCC_A_SOURCES = 3
local DAISY_IEO = 0x02
local WR9_VIS = 0x01
local WR9_NV_SHSL = 0x12 -- no vector / status high: vectors this does not model
local STATUS_CHANNEL_A = 0x04
local STATUS_TYPE_MASK = 0x03
local VECTOR_STATUS_KEEP = 0xF1
local VECTOR_BYTE = 0xFF
local intState = emu.item(scc.items["0/m_int_state"])
local intSource = emu.item(scc.items["0/m_int_source"])
local wr9 = emu.item(scc.items["0/m_wr9"])
local wr2 = emu.item(sccA.items["0/m_wr2"])
local iackFixed = 0
-- 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 = ORIGIN_X + px
local addr = TEXT_BASE + (ORIGIN_Y + shownRow() + y) * ROW_BYTES + (sx >> 3)
local bit = 7 - (sx & 7)
local v = 0
for p = 0, PLANES - 1 do
v = v | (((mem:read_u8(addr + p * PLANE_BYTES) >> bit) & 1) << p)
end
return v
end
local function rowNibbles(y, count)
local t = {}
for i = 0, count - 1 do
t[#t + 1] = textPix(i, y)
end
return t
end
-- A reader over a row's nibbles: each call takes the next field, high nibble
-- first.
local function fields(t)
local pos = 0
return function(count)
local v = 0
for i = pos + 1, pos + count do
v = (v << 4) | t[i]
end
pos = pos + count
return v
end
end
local function parseSerial(t)
local take = fields(t)
local s = {}
s.sentinel = take(WORD) == SERIAL_SENTINEL
s.status = take(WORD)
s.rx = take(LONG)
s.bad = take(WORD)
s.tx = take(LONG)
s.short = take(WORD)
s.firstBad = take(LONG)
return s
end
-- The flags word is one nibble per predicate: present, left, right, middle.
local function readMouse()
local take = fields(rowNibbles(MOUSE_ROW, MOUSE_NIBBLES))
local m = {}
m.sentinel = take(WORD) == MOUSE_SENTINEL
m.present = take(1)
m.left = take(1)
m.right = take(1)
m.middle = take(1)
m.x = take(WORD)
m.y = take(WORD)
return m
end
local ports = manager.machine.ioport.ports
local axisX, axisY, btnLeft, btnRight
local portX = ports[":mouse_port:x68k:X"]
local portY = ports[":mouse_port:x68k:Y"]
local portB = ports[":mouse_port:x68k:BTN"]
if portX then for _, f in pairs(portX.fields) do axisX = f end end
if portY then for _, f in pairs(portY.fields) do axisY = f end end
if portB then
for _, f in pairs(portB.fields) do
if f.mask == 2 then btnLeft = f end
if f.mask == 1 then btnRight = f end
end
end
-- After MAME has acknowledged: the source now in service is the one the
-- vector must name. A global, because a collected tap handle removes the tap.
iackTap = cpu.spaces["cpu_space"]:install_read_tap(IACK5, IACK5 + 1, "iackVector", function(offset, data, mask)
local w = wr9:read(0)
if (w & WR9_VIS) == 0 or (w & WR9_NV_SHSL) ~= 0 then
return nil
end
for i = 0, SCC_SOURCES - 1 do
if (intState:read(i) & DAISY_IEO) ~= 0 then
local status = (intSource:read(i) & STATUS_TYPE_MASK) | (i < SCC_A_SOURCES and STATUS_CHANNEL_A or 0)
local vector = (wr2:read(0) & VECTOR_STATUS_KEEP) | (status << 1)
if vector == (data & VECTOR_BYTE) then
return nil
end
iackFixed = iackFixed + 1
return (data & ~VECTOR_BYTE) | vector
end
end
return nil
end)
local function finish(verdict)
done = true
local s = prev or {}
io.write(string.format("VERIFY-X68K-SERMOUSE %s baud=%d dense=%d checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n",
verdict, BAUD, DENSE, checks, #fails, s.rx or 0, s.tx or 0,
quiet and string.format("%d:%d", quiet.rx, quiet.tx) or "none", iackFixed,
#fails > 0 and (" detail=" .. table.concat(fails, ",")) or ""))
io.flush()
manager.machine:exit()
end
if not (axisX and axisY and btnLeft and btnRight) then
done = true
io.write("VERIFY-X68K-SERMOUSE error: mouse ioports not found\n")
io.flush()
manager.machine:exit()
end
-- One check; the first failure is the verdict, so the run stops there.
local function expect(name, ok, detail)
if done then return end
checks = checks + 1
if not ok then
fails[#fails + 1] = name .. (detail and ("(" .. detail .. ")") or "")
finish("FAIL")
end
end
local function expectMouse(step, x, y, l, r)
local m = readMouse()
expect(step .. ":sentinel", m.sentinel)
expect(step .. ":present", m.present == 1, tostring(m.present))
expect(step .. ":xy", m.x == x and m.y == y, string.format("%d,%d want %d,%d", m.x, m.y, x, y))
expect(step .. ":buttons", m.left == l and m.right == r and m.middle == 0,
string.format("%d%d%d want %d%d0", m.left, m.right, m.middle, l, r))
end
-- A steady serial read: fn runs with the row once two consecutive frames
-- agree on it.
local function whenSteady(name, fn)
pending = { name = name, fn = fn, since = frame }
lastRaw = nil
end
local function serialHealthy(step, s)
expect(step .. ":sentinel", s.sentinel)
expect(step .. ":open", (s.status & STATUS_OPEN) ~= 0, string.format("status %04X", s.status))
expect(step .. ":bad", s.bad == 0 and s.firstBad == NO_BAD_INDEX, string.format("%d first %d", s.bad, s.firstBad))
expect(step .. ":short", s.short == 0, tostring(s.short))
end
-- Traffic flowing: both counts moved since the last read.
local function expectMoving(step)
whenSteady(step, function(s)
serialHealthy(step, s)
expect(step .. ":paused", (s.status & STATUS_PAUSED) == 0)
if prev then
expect(step .. ":rxMoved", s.rx > prev.rx, string.format("%d -> %d", prev.rx, s.rx))
expect(step .. ":txMoved", s.tx > prev.tx, string.format("%d -> %d", prev.tx, s.tx))
end
prev = s
end)
end
-- Line quiet: both counts exactly where the pause left them.
local function expectFrozen(step)
whenSteady(step, function(s)
serialHealthy(step, s)
expect(step .. ":paused", (s.status & STATUS_PAUSED) ~= 0)
if quiet == nil then
quiet = s
expect(step .. ":rxMin", s.rx >= MIN_BYTES, tostring(s.rx))
expect(step .. ":txMin", s.tx >= MIN_BYTES, tostring(s.tx))
else
expect(step .. ":rxFrozen", s.rx == quiet.rx, string.format("%d -> %d", quiet.rx, s.rx))
expect(step .. ":txFrozen", s.tx == quiet.tx, string.format("%d -> %d", quiet.tx, s.tx))
end
prev = s
end)
end
local function at(t, fn)
steps[#steps + 1] = { at = t, run = fn }
end
-- Corners in visiting order: where the clamp pins the pointer, the push
-- direction that pins it there, and the button pressed once it is.
local corners = {
{ name = "topLeft", x = 0, y = 0, dx = -1, dy = -1, button = "left" },
{ name = "topRight", x = STAGE_RIGHT, y = 0, dx = 1, dy = -1, button = "right" },
{ name = "bottomRight", x = STAGE_RIGHT, y = STAGE_BOTTOM, dx = 1, dy = 1, button = "left" },
{ name = "bottomLeft", x = 0, y = STAGE_BOTTOM, dx = -1, dy = 1, button = "right" },
}
-- One pass over the corners from frame `base`; `serialCheck` runs beside
-- each position check.
local function pass(base, label, serialCheck)
for i, c in ipairs(corners) do
local t = base + (i - 1) * CORNER_FRAMES
local step = label .. ":" .. c.name
local button = c.button == "left" and btnLeft or btnRight
local l = c.button == "left" and 1 or 0
at(t, function() pushX = c.dx; pushY = c.dy end)
at(t + PUSH_FRAMES, function()
expectMouse(step, c.x, c.y, 0, 0)
serialCheck(step)
button:set_value(1)
end)
at(t + CORNER_FRAMES - 1, function()
expectMouse(step .. "Button", c.x, c.y, l, 1 - l)
button:clear_value()
end)
end
end
local t = 0
for p = 1, PASSES do
pass(t, "busy" .. p, expectMoving)
t = t + PASS_FRAMES
end
at(t, function() nat:post(" ") end)
at(t + QUIET_SETTLE, function() expectFrozen("pause") end)
at(t + QUIET_SETTLE + QUIET_HOLD, function() expectFrozen("pauseHold") end)
t = t + QUIET_SETTLE + QUIET_HOLD + 1
pass(t, "quiet", expectFrozen)
t = t + PASS_FRAMES
at(t, function() nat:post(" ") end)
at(t + RESUME_FRAMES, function()
prev = quiet
expectMoving("resumed")
end)
local finalFrame = t + RESUME_FRAMES
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if not biased and frame == BOOT_BIAS_FRAME then
biased = true
axisValX = AXIS_BIAS
axisValY = AXIS_BIAS
axisX:set_value(axisValX)
axisY:set_value(axisValY)
end
if t0 == 0 then
if frame > SENTINEL_FIRST and frame % SENTINEL_EVERY == 0 then
local s = parseSerial(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES))
if s.sentinel and readMouse().sentinel then
t0 = frame + SETTLE_FRAMES
end
end
if frame > BOOT_LIMIT then
done = true
io.write("VERIFY-X68K-SERMOUSE timeout: SERMOUSE never came up\n")
io.flush()
manager.machine:exit()
end
return
end
if pushX ~= 0 or pushY ~= 0 then
axisValX = axisValX + pushX * PUSH_STEP
axisValY = axisValY + pushY * PUSH_STEP
axisX:set_value(axisValX)
axisY:set_value(axisValY)
end
for _, s in ipairs(steps) do
if frame == t0 + s.at and not done then s.run() end
end
if pending and not done then
local raw = table.concat(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES), ",")
if raw == lastRaw then
local p = pending
pending = nil
p.fn(parseSerial(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES)))
elseif frame - pending.since > STEADY_LIMIT then
expect(pending.name .. ":steady", false)
else
lastRaw = raw
end
end
if not done and not pending and frame > t0 + finalFrame then
finish(#fails == 0 and "PASS" or "FAIL")
end
end)
LUA
} > "$work/sermouse.lua"
python3 "$repo/scripts/lib/serialStreamPeer.py" "$PORT" "$((WALL + PEER_MARGIN))" "$MIN_BYTES" \
> "$work/peer.out" 2>&1 &
peer=$!
# The peer must be listening before MAME connects out to it.
for _ in $(seq 1 50); do
grep -q '^STREAM listening' "$work/peer.out" 2>/dev/null && break
sleep 0.1
done
# From the work dir, so MAME's cfg/ and nvram/ land there. Under timeout so a
# hung run cannot outlive WALL whatever happens to this script; mameChild is
# MAME itself, which cleanup stops first (stopping only the timeout process
# could leave MAME running).
cd "$work" || exit 1
if [ "$BAUD" != "$DEFAULT_BAUD" ]; then
mkdir -p "$work/cfg"
python3 "$repo/scripts/lib/mameNullModemCfg.py" x68000 :rs232c:null_modem "$BAUD" \
> "$work/cfg/x68000.cfg" || exit 2
fi
JOEY_Z80SCC_RX_FIFO=$FIFO_DEPTH timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \
-rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \
-video none -sound none -nothrottle \
-autoboot_script "$work/sermouse.lua" </dev/null > "$work/mame.out" 2>&1 &
mame=$!
sleep 1
mameChild=$(pgrep -P "$mame" | head -1)
# The Lua ends the run: after its last check, or at its first failure.
wait "$mame"
mame=""
mameChild=""
for _ in $(seq 1 "$PEER_GRACE"); do
joeyAlive "$peer" || break
sleep 1
done
joeyStop "$peer"
peer=""
lua=$(grep -E '^VERIFY-X68K-SERMOUSE ' "$work/mame.out" | tail -1)
stream=$(grep -E '^STREAM (PASS|FAIL)' "$work/peer.out" | tail -1)
echo "${lua:-VERIFY-X68K-SERMOUSE: no verdict from the Lua}"
echo "${stream:-STREAM: no verdict from the peer}"
if ! grep -q "RX FIFO depth $FIFO_DEPTH" "$work/mame.out"; then
echo "verify-x68000-serial-mouse: SKIP - this MAME does not take JOEY_Z80SCC_RX_FIFO (reinstall: ./install.sh install mame)" >&2
exit 2
fi
fail=0
if ! echo "$lua" | grep -q '^VERIFY-X68K-SERMOUSE PASS .* fails=0 '; then
echo "verify-x68000-serial-mouse: FAIL - guest or mouse check (Lua line above)" >&2
tail -5 "$work/mame.out" >&2
fail=1
fi
if ! echo "$stream" | grep -q '^STREAM PASS '; then
echo "verify-x68000-serial-mouse: FAIL - the guest's stream to the host (peer line above)" >&2
fail=1
fi
guestQuiet=$(echo "$lua" | sed -n 's/.* quiet=\([0-9]*:[0-9]*\).*/\1/p')
peerQuiet=$(echo "$stream" | sed -n 's/.* quiet=\([0-9:,]*\) .*/\1/p')
if [ -z "$guestQuiet" ] || [[ ",$peerQuiet," != *",$guestQuiet,"* ]]; then
echo "verify-x68000-serial-mouse: FAIL - quiet counts differ: guest received:sent $guestQuiet, peer sent:received $peerQuiet" >&2
fail=1
fi
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "verify-x68000-serial-mouse: PASS at $BAUD baud, $DENSE dense-SCB lines (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)"