335 lines
14 KiB
Bash
Executable file
335 lines
14 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-iigs-serial-loss.sh - Receive-loss gate for the Apple IIgs serial add-on.
|
|
#
|
|
# SERLOSS (examples/serloss/serloss.c) fills and presents the whole screen in a
|
|
# loop while this script's peer streams 4096 bytes (byte k = k & 0xFF) into the
|
|
# port at 9600 baud. The probe counts, sequence-checks and sums what arrives and
|
|
# leaves the result in a mailbox at $E1:9DD0, which the MAME Lua reads.
|
|
#
|
|
# PASS all 4096 bytes arrived, in order, with the right sum
|
|
# FAIL anything missing or out of sequence
|
|
# SKIP (exit 2) a tool or ROM is missing, or MAME did not confirm the
|
|
# real SCC receive FIFO depth (below)
|
|
#
|
|
# scripts/verify-iigs-serial-loss.sh scc modem port (SCC channel B)
|
|
# scripts/verify-iigs-serial-loss.sh ssc Super Serial Card in slot 2
|
|
#
|
|
# The emulator must model the real receive FIFO depth, or a receiver that
|
|
# falls behind still looks fine. The installed MAME's
|
|
# installer/patches/mame/0002-z80scc-rx-fifo.patch gives the Z8530 a 512-byte
|
|
# receive FIFO by default, so polled receivers survive in the other gates;
|
|
# this script runs it with JOEY_Z80SCC_RX_FIFO=3, the real chip's depth. MAME
|
|
# then prints "RX FIFO depth 3 (JOEY_Z80SCC_RX_FIFO)" per SCC channel, and an
|
|
# scc PASS counts only with that line (installer/patches/mame/README.md). The
|
|
# 6551 on the Super Serial Card has no FIFO to widen.
|
|
#
|
|
# The card's interrupt switch (SW2-6) is OFF by default, as on a real Super
|
|
# Serial Card; the Lua turns it on so the 6551's IRQ reaches the slot. MAME
|
|
# needs the card's firmware, a2ssc.zip, which the iigsFirmware component
|
|
# installs into $IIGS_ROMPATH.
|
|
#
|
|
# Environment:
|
|
# SERLOSS_MAME MAME binary (default $MAME); it runs with JOEY_Z80SCC_RX_FIFO=3.
|
|
# An scc PASS from one that does not report that depth (a
|
|
# build without the 0002 patch, whose FIFO is already the
|
|
# real 3) stands, with a warning; from $MAME it is a SKIP,
|
|
# since that means the installed MAME predates the switch
|
|
# SERLOSS_BIN probe binary (default build/iigs/bin/SERLOSS or SERLOSSS)
|
|
# SERLOSS_PORT TCP port for the bitbanger (default 45241)
|
|
# SERLOSS_MACHINE apple2gs (ROM 03, default) or apple2gsr1 (ROM 01)
|
|
# SERLOSS_LAUNCHER set to boot straight into the probe instead of driving
|
|
# the Finder (whose timeline only fits ROM 03)
|
|
# SERLOSS_BAUD peer line rate, must match the probe's -DSERLOSS_BAUD (default 9600)
|
|
# SERLOSS_ROMPATH MAME rompath (default $IIGS_ROMPATH, which holds a2ssc)
|
|
# SERLOSS_WALL wall-clock cap for MAME in seconds (default 600)
|
|
# SERLOSS_MAX_FRAME emulated-frame cap (default 12000)
|
|
# SERLOSS_TAP file: log every data-register read with its emulated time
|
|
# SERLOSS_PADDLE plug MAME's joystick into the game port and hold both paddles
|
|
# at this value (0..255) for a -DSERLOSS_JOY probe; 255, full
|
|
# deflection, is the longest paddle read there is
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
joeyNeed MAME mame
|
|
joeyNeed IIGS_ROMPATH iigsFirmware
|
|
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
|
|
|
|
mode="${1:-scc}"
|
|
case "$mode" in
|
|
scc) name=SERLOSS ;;
|
|
ssc) name=SERLOSSS ;;
|
|
*) echo "verify-iigs-serial-loss: mode must be scc or ssc" >&2; exit 2 ;;
|
|
esac
|
|
mame="${SERLOSS_MAME:-$MAME}"
|
|
rompath="${SERLOSS_ROMPATH:-$IIGS_ROMPATH}"
|
|
bin="${SERLOSS_BIN:-$repo/build/iigs/bin/$name}"
|
|
PORT="${SERLOSS_PORT:-45241}"
|
|
WALL="${SERLOSS_WALL:-600}"
|
|
maxFrame="${SERLOSS_MAX_FRAME:-12000}"
|
|
total=4096
|
|
|
|
[ -x "$mame" ] || { echo "verify-iigs-serial-loss: no MAME at $mame" >&2; exit 2; }
|
|
[ -f "$bin" ] || { echo "verify-iigs-serial-loss: build $bin first" >&2; exit 2; }
|
|
if [ "$mode" = ssc ] && [ -z "${SERLOSS_ROMPATH:-}" ]; then
|
|
joeyNeedFile "$IIGS_ROMPATH/a2ssc.zip" iigsFirmware
|
|
fi
|
|
|
|
work=$(mktemp -d -t joeylib-iigs-loss.XXXXXX)
|
|
peer_pid=""
|
|
cleanup() {
|
|
[ -n "$peer_pid" ] && kill -9 "$peer_pid" 2>/dev/null
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mkdir -p "$work/bin"
|
|
cp "$bin" "$work/bin/$name"
|
|
BINDIR="$work/bin" JOEY_DISK_EXAMPLES="$name" bash "$repo/scripts/make-iigs-disk.sh" "$work/loss.2mg" >/dev/null 2>&1 || {
|
|
echo "verify-iigs-serial-loss: FAIL - could not build the probe disk" >&2
|
|
exit 1
|
|
}
|
|
# SERLOSS_LAUNCHER=1 boots straight into the probe (make-iigs-boot-disk.sh)
|
|
# instead of driving the Finder, whose keystroke timeline is calibrated for
|
|
# ROM 03 only.
|
|
if [ -n "${SERLOSS_LAUNCHER:-}" ]; then
|
|
bash "$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null 2>&1 || {
|
|
echo "verify-iigs-serial-loss: FAIL - could not build the launcher boot disk" >&2
|
|
exit 1
|
|
}
|
|
finderAt=999999
|
|
else
|
|
cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
|
|
finderAt=3000
|
|
fi
|
|
|
|
# Peer: listen (MAME connects out), wait for the probe's READY, stream the
|
|
# pattern, then keep draining whatever the probe sends until MAME goes away.
|
|
peer=$(joeyTool iigsSerialPeer) || exit 1
|
|
"$peer" loss "$PORT" "$total" "$work/peer.log" &
|
|
peer_pid=$!
|
|
sleep 1
|
|
|
|
if [ "$mode" = ssc ]; then
|
|
serialArgs=(-sl2 ssc -sl2:ssc:rs232 null_modem -bitb "socket.127.0.0.1:$PORT")
|
|
dipPort=":sl2:ssc:DSWX"
|
|
modemTag=":sl2:ssc:rs232:null_modem"
|
|
else
|
|
serialArgs=(-modem null_modem -bitb "socket.127.0.0.1:$PORT")
|
|
dipPort=""
|
|
modemTag=":modem:null_modem"
|
|
fi
|
|
# The apple2gs has nothing on its game port by default (every paddle reads 0).
|
|
paddle="${SERLOSS_PADDLE:-}"
|
|
gameArgs=()
|
|
if [ -n "$paddle" ]; then
|
|
gameArgs=(-gameio joy)
|
|
fi
|
|
paddleValue="${paddle:-0}"
|
|
case "${SERLOSS_BAUD:-9600}" in
|
|
2400) baudCode=5 ;;
|
|
4800) baudCode=6 ;;
|
|
9600) baudCode=7 ;;
|
|
19200) baudCode=9 ;;
|
|
38400) baudCode=11 ;;
|
|
57600) baudCode=12 ;;
|
|
115200) baudCode=13 ;;
|
|
*) echo "verify-iigs-serial-loss: unsupported SERLOSS_BAUD" >&2; exit 2 ;;
|
|
esac
|
|
|
|
cat > "$work/loss.lua" <<LUA
|
|
local cpu = manager.machine.devices[":maincpu"]
|
|
local mem = cpu.spaces["program"]
|
|
local nat = manager.machine.natkeyboard
|
|
local frame = 0
|
|
local idx = 1
|
|
local launched = false
|
|
|
|
local function field(port, name)
|
|
local p = manager.machine.ioport.ports[port]
|
|
if p == nil then return nil end
|
|
return p.fields[name]
|
|
end
|
|
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
|
|
local function press(f) if f then f:set_value(1) end end
|
|
local function release(f) if f then f:set_value(0) end end
|
|
|
|
local function u16(off)
|
|
return mem:read_u8(0xE19DD0 + off) | (mem:read_u8(0xE19DD1 + off) << 8)
|
|
end
|
|
|
|
-- The stick spread bytes are biased by 128 (serloss.c MB_JOY_BIAS).
|
|
local function s8(off)
|
|
return mem:read_u8(0xE19DD0 + off) - 128
|
|
end
|
|
|
|
local function report()
|
|
io.write(string.format("VERIFY-IIGS-LOSS frame=%d sig=%02X%02X phase=%02X received=%d inSequence=%d breaks=%d presents=%d ticks=%d overruns=%d dropped=%d sum=%04X now=%d fillTicks=%d fillPresentTicks=%d joyPolls=%d joyX=%d..%d joyY=%d..%d sfxShots=%d sfxFills=%d\n",
|
|
frame, mem:read_u8(0xE19DD0), mem:read_u8(0xE19DD1), mem:read_u8(0xE19DD2),
|
|
u16(4), u16(6), u16(8), u16(10), u16(12), u16(14), u16(16), u16(18), u16(20), u16(22), u16(24),
|
|
u16(26), s8(28), s8(29), s8(30), s8(31), u16(32), u16(34)))
|
|
io.flush()
|
|
end
|
|
|
|
-- With the paddles in play, serve VERTCNT/HORIZCNT (C02E/C02F) with the
|
|
-- real machine's 262-line mapping (VERTCNT*2 + HORIZCNT bit 7 = 250..511).
|
|
-- MAME's apple2gs get_vpos() wraps with -261 instead of -262, so its counter
|
|
-- repeats line 501 and never reads 250; the tight joystick read times its
|
|
-- passes with these counters, and a span across the repeat or the wrap is 65
|
|
-- ticks (~6 counts) off. The beam position comes from time_until_pos: MAME's
|
|
-- Lua has no screen vpos/hpos.
|
|
local vcTaps = {}
|
|
local function beamCountersFix()
|
|
if "$paddle" == "" then return end
|
|
local scr = manager.machine.screens[":screen"]
|
|
local function beam()
|
|
local e = scr.frame_period - scr:time_until_pos(0, 0)
|
|
if e >= scr.frame_period then e = e - scr.frame_period end
|
|
if e < 0 then e = e + scr.frame_period end
|
|
e = e + scr.pixel_period / 2
|
|
local v = math.floor(e / scr.scan_period)
|
|
return v % 262, math.floor((e - v * scr.scan_period) / scr.pixel_period)
|
|
end
|
|
local function counters(offset, data, mask)
|
|
local v, hp = beam()
|
|
local line = v + 501
|
|
if line > 511 then line = line - 262 end
|
|
if (offset & 0xFF) == 0x2E then return line >> 1 end
|
|
local h = hp // 11
|
|
if h > 0 then h = h + 0x40 end
|
|
return h | ((line & 1) << 7)
|
|
end
|
|
for _, bank in ipairs({0x000000, 0xE00000, 0xE10000}) do
|
|
vcTaps[#vcTaps + 1] = mem:install_read_tap(bank + 0xC02E, bank + 0xC02F, "beam" .. bank, counters)
|
|
end
|
|
end
|
|
|
|
-- Hold both paddles of MAME's joystick (-gameio joy) still.
|
|
local function paddlesHold()
|
|
if "$paddle" == "" then return end
|
|
for _, name in ipairs({"joystick_1_x", "joystick_1_y"}) do
|
|
local p = manager.machine.ioport.ports[":gameio:joy:" .. name]
|
|
if p ~= nil then
|
|
for _, f in pairs(p.fields) do f:set_value($paddleValue) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function sscInterruptsOn()
|
|
local p = manager.machine.ioport.ports["$dipPort"]
|
|
if p == nil then return end
|
|
local f = p.fields["Interrupts"]
|
|
if f ~= nil then f.user_value = 0 end
|
|
end
|
|
|
|
-- Optional (SERLOSS_TAP=file): log every CPU read of the data register with
|
|
-- the emulated time, for a receive-latency histogram.
|
|
local tapFile = "${SERLOSS_TAP:-}"
|
|
local taps = {}
|
|
local function installTaps()
|
|
if tapFile == "" then return end
|
|
local out = io.open(tapFile, "w")
|
|
local dataReg = ("$mode" == "ssc") and 0xC0A8 or 0xC03A
|
|
for _, bank in ipairs({0x000000, 0xE00000, 0xE10000}) do
|
|
taps[#taps + 1] = mem:install_read_tap(bank + dataReg, bank + dataReg, "rx" .. bank,
|
|
function(offset, data, mask)
|
|
out:write(string.format("%.7f %d\n", manager.machine.time:as_double(), data & 0xFF))
|
|
out:flush()
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- The null_modem's line rate is a configuration setting (default 9600).
|
|
local function peerBaud()
|
|
for _, name in ipairs({"RS232_TXBAUD", "RS232_RXBAUD"}) do
|
|
local p = manager.machine.ioport.ports["$modemTag:" .. name]
|
|
if p ~= nil then
|
|
for _, f in pairs(p.fields) do f.user_value = $baudCode end
|
|
end
|
|
end
|
|
end
|
|
|
|
local steps = {
|
|
{10, function() if "$dipPort" ~= "" then sscInterruptsOn() end; peerBaud(); paddlesHold(); beamCountersFix(); installTaps() end},
|
|
{11, function() if $finderAt > 3000 then launched = true end end},
|
|
{$finderAt, function() nat:post("J") end},
|
|
{$finderAt + 120, function() press(key_cmd) end},
|
|
{$finderAt + 126, function() nat:post("o") end},
|
|
{$finderAt + 180, function() release(key_cmd) end},
|
|
{$finderAt + 540, function() nat:post("$name") end},
|
|
{$finderAt + 660, function() press(key_cmd) end},
|
|
{$finderAt + 666, function() nat:post("o") end},
|
|
{$finderAt + 720, function() release(key_cmd); launched = true end},
|
|
}
|
|
|
|
emu.register_frame_done(function()
|
|
frame = frame + 1
|
|
while idx <= #steps and frame >= steps[idx][1] do
|
|
steps[idx][2]()
|
|
idx = idx + 1
|
|
end
|
|
if launched and frame % 60 == 0 then
|
|
local sig = mem:read_u8(0xE19DD0) == 0xA5 and mem:read_u8(0xE19DD1) == 0x5A
|
|
local phase = mem:read_u8(0xE19DD2)
|
|
if sig and (phase == 0x05 or phase == 0xEE) then
|
|
report(); manager.machine:exit()
|
|
end
|
|
end
|
|
if frame >= $maxFrame then
|
|
report(); manager.machine:exit()
|
|
end
|
|
end)
|
|
LUA
|
|
|
|
cd "$work"
|
|
out=$(JOEY_Z80SCC_RX_FIFO=3 QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
|
|
timeout -s KILL "$WALL" "$mame" "${SERLOSS_MACHINE:-apple2gs}" \
|
|
-rompath "$rompath" \
|
|
-flop3 "$work/boot.po" -flop4 "$work/loss.2mg" \
|
|
"${serialArgs[@]}" "${gameArgs[@]}" \
|
|
-video none -sound none -nothrottle \
|
|
-autoboot_script "$work/loss.lua" </dev/null 2>&1) || true
|
|
|
|
sleep 1
|
|
kill -9 "$peer_pid" 2>/dev/null
|
|
peer_pid=""
|
|
cat "$work/peer.log" 2>/dev/null
|
|
|
|
line=$(echo "$out" | grep -E '^VERIFY-IIGS-LOSS ' | tail -1)
|
|
echo "$line"
|
|
if [ -z "$line" ]; then
|
|
echo "verify-iigs-serial-loss: FAIL - MAME produced no report" >&2
|
|
echo "$out" | tail -10 >&2
|
|
exit 1
|
|
fi
|
|
get() { echo "$line" | grep -oE "$1=[0-9A-F]+" | cut -d= -f2; }
|
|
phase=$(get phase); received=$(get received); inSeq=$(get inSequence); breaks=$(get breaks); sum=$(get sum)
|
|
presents=$(get presents); fillTicks=$(get fillTicks); bothTicks=$(get fillPresentTicks)
|
|
if [ -n "$bothTicks" ] && [ -n "$fillTicks" ]; then
|
|
# 60 fills vs 60 fill+present pairs, in 60 Hz ticks: the difference is 60 presents.
|
|
echo " full-screen present: $(( (bothTicks - fillTicks) * 1000000 / 3600 )) us each (fills $fillTicks ticks, fills+presents $bothTicks ticks)"
|
|
fi
|
|
joyPolls=$(get joyPolls)
|
|
if [ -n "$joyPolls" ] && [ "$joyPolls" != 0 ]; then
|
|
# A -DSERLOSS_JOY probe calibrates the held stick first, so every reading
|
|
# should be 0: the spread is how far interrupts pushed the paddle reads.
|
|
echo " joystick: $joyPolls polls, paddles held at ${paddle:-0}, X read $(echo "$line" | grep -oE 'joyX=[-0-9.]+' | cut -d= -f2), Y read $(echo "$line" | grep -oE 'joyY=[-0-9.]+' | cut -d= -f2)"
|
|
fi
|
|
if [ "$phase" = 05 ] && [ "$received" = "$total" ] && [ "$inSeq" = "$total" ] && [ "$breaks" = 0 ] && [ "$sum" = F800 ]; then
|
|
# An scc PASS proves something only at the real FIFO depth (see the header).
|
|
if [ "$mode" = scc ] && ! grep -qF 'RX FIFO depth 3 (JOEY_Z80SCC_RX_FIFO)' <<< "$out"; then
|
|
if [ -z "${SERLOSS_MAME:-}" ]; then
|
|
echo "verify-iigs-serial-loss: SKIP - $mame did not confirm RX FIFO depth 3 (installed before JOEY_Z80SCC_RX_FIFO existed?), so a 512-byte SCC FIFO may be hiding loss; run: ./install.sh install mame" >&2
|
|
exit 2
|
|
fi
|
|
echo "verify-iigs-serial-loss: WARNING - $mame did not report JOEY_Z80SCC_RX_FIFO=3; trusting it to model the real SCC FIFO" >&2
|
|
fi
|
|
echo "verify-iigs-serial-loss: PASS ($mode: $total/$total bytes, in order, through $presents full-screen presents)"
|
|
exit 0
|
|
fi
|
|
echo "verify-iigs-serial-loss: FAIL ($mode: received $received/$total, $breaks sequence breaks, phase $phase)" >&2
|
|
exit 1
|