588 lines
24 KiB
Bash
Executable file
588 lines
24 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-scb.sh - SCB gate for the X68000: proves that the per-band
|
|
# palette reloads the text-plane present depends on land where they must, on
|
|
# a screen that changes palette 147 times a frame.
|
|
#
|
|
# The X68000 port presents to the text plane, which has ONE 16-entry palette,
|
|
# so it honours the SCB by reloading that palette as the beam scans: a CRTC
|
|
# raster interrupt (src/x68000/jlX68kRasterIsr.s) stores a band's sixteen
|
|
# colours in the horizontal blanking before the band's first line. Each reload
|
|
# has a window of about 400 dots (~18 us): after the last stage pixel of the
|
|
# line above and before the first stage pixel of the band. Land early and the
|
|
# end of the line above shows the new colours; land late and the start of the
|
|
# band shows the old ones; miss and the whole band does.
|
|
#
|
|
# Boots Human68k headless under MAME with SCB.X (examples/scb): all sixteen
|
|
# palettes, every colour distinct per palette, a change on every line for 128
|
|
# of the 200 lines, irregular runs for the rest, 20 stripes using every colour
|
|
# on every line, the whole stage redrawn and every palette set again on every
|
|
# pass. Once the stripes are up and have settled, the Lua below taps every
|
|
# write to the text palette ($E82200), to the raster compare (CRTC R09), to
|
|
# MFP ISRA (the interrupt's exit) and every read of the $138 vector (its
|
|
# entry), timestamped on the emulated clock -- MAME's x68000 runs the
|
|
# cycle-exact 68000 core, so a write's timestamp is its bus cycle -- and
|
|
# converts each to a beam position with the screen's own timing. It then
|
|
# checks CAPTURE whole frames, computing what every stage line shows from the
|
|
# program's SCB and palette rules (the same rules as examples/scb/scb.c, not
|
|
# the output):
|
|
#
|
|
# timeline the palette in effect at each line's first stage pixel is that
|
|
# line's palette, and no palette write lands between its first and
|
|
# last stage pixel. This is what a CRT shows: a palette write takes
|
|
# effect at once. Every band start's reload is also measured: how
|
|
# long after the last stage pixel of the line above it began, and
|
|
# how long before the band's first stage pixel it finished.
|
|
# bitmap MAME's own rendering, read back with screen:pixels() on SNAPSHOTS
|
|
# frames, shows each line in its palette -- or, only where the next
|
|
# line starts a band, partly in the next line's: MAME draws a line
|
|
# when the NEXT line's display begins (x68k_crtc.cpp hsync ->
|
|
# update_partial(scan - 1)), so a reload that lands correctly in
|
|
# the blanking before a line is drawn onto the line above it, unless
|
|
# a raster interrupt drew that line first (raster_irq). MAME's
|
|
# bitmap therefore cannot place a reload to the line; the timeline
|
|
# can. This check catches gross misplacement independently of the
|
|
# taps.
|
|
# exit after ESC the program exits and gives the $138 vector and MFP
|
|
# channel 14 back -- and the two clock vectors it held while it
|
|
# ran, which are checked then: $114 (jlX68kTickIsr, chained onto
|
|
# Human68k's Timer C) and $134 (jlX68kVdispIsr, the frame counter
|
|
# _VDISPST installs, with MFP channel 13 enabled).
|
|
#
|
|
# It also reports the interrupt's cost (time inside it per frame), the latency
|
|
# from each raster match to the vector fetch, and the slack of the compare
|
|
# write the interrupt's timing rests on (jlX68kRasterIsr.s step 2).
|
|
#
|
|
# Human68k's Timer C tick runs throughout, and text is typed during the whole
|
|
# capture so the IOCS keyboard interrupt runs too: both are MFP interrupts at
|
|
# IPL 6, like the raster interrupt, and a raster match that falls inside one of
|
|
# them is taken late (the Timer C dispatcher used to make reloads miss their
|
|
# line; see src/x68000/jlX68kTickIsr.s). The DMAC-driven ADPCM path is not
|
|
# exercised: JoeyLib's audio does its work on the main thread
|
|
# (src/x68000/audioPcm.c).
|
|
#
|
|
# Usage: scripts/verify-x68000-scb.sh
|
|
# Env: SCB_PROGRAM run this build of SCB.X instead (negative controls)
|
|
# SCB_SETTLE frames between the stripes appearing and the capture
|
|
# (default 120; the emulation is deterministic, so a
|
|
# different value is what samples a different phase
|
|
# between the frame and Timer C)
|
|
# SCB_CAPTURE frames captured and checked (default 60)
|
|
# Requires: MAME and the X68000 firmware and CG ROM; build/x68000/bin/SCB.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="${SCB_PROGRAM:-$repo/build/x68000/bin/SCB.X}"
|
|
wall=600 # hard cap on MAME, whatever happens to this script
|
|
frames=20000 # emulated frames before the Lua gives up on the program
|
|
settle="${SCB_SETTLE:-120}"
|
|
capture="${SCB_CAPTURE:-60}"
|
|
|
|
if [ ! -f "$program" ]; then
|
|
echo "verify-x68000-scb: $program is not built; run: make x68000" >&2
|
|
exit 2
|
|
fi
|
|
|
|
work=$(mktemp -d -t joeylib-x68-scb.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:\SCB.X' || exit 1
|
|
x68kDataDisk "$work/data.xdf" || exit 1
|
|
x68kXdf add "$work/data.xdf" "$program" SCB.X > /dev/null || exit 1
|
|
|
|
cat > "$work/scb.lua" <<LUA
|
|
local cpu = manager.machine.devices[":maincpu"]
|
|
local mem = cpu.spaces["program"]
|
|
local scr = manager.machine.screens[":screen"]
|
|
local nat = manager.machine.natkeyboard
|
|
|
|
local FRAMES = $frames
|
|
local CAPTURE = $capture
|
|
local SETTLE = $settle
|
|
local SAMPLE = 30 -- frames between looks for the stripes
|
|
local SNAPSHOTS = { [10] = true, [30] = true, [50] = true }
|
|
local KEY_EVERY = 60 -- frames between ESC presses once the verdict is in
|
|
local TYPED = "the quick brown fox jumps over the lazy dog 0123456789 "
|
|
local EXIT_FRAMES = 1200 -- frames the program gets to exit and hand back \$138
|
|
|
|
-- The port's display contract (src/x68000/x68kPlanar.h).
|
|
local ORIGIN_X, ORIGIN_Y = 96, 156
|
|
local STAGE_W, STAGE_H = 320, 200
|
|
local TVRAM, PLANE_STEP, TVRAM_STRIDE = 0xE00000, 0x20000, 128
|
|
local TEXT_PALETTE, COLORS = 0xE82200, 16
|
|
local CRTC, CRTC_R09 = 0xE80000, 0xE80012
|
|
local MFP_IERA, MFP_IMRA, MFP_ISRA = 0xE88007, 0xE88013, 0xE8800F
|
|
local RASTER_VECTOR = 0x138
|
|
local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: jlX68kTickIsr
|
|
local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: jlX68kVdispIsr
|
|
local TIMER_A_BIT = 0x20 -- channel 13 in IERA / IMRA
|
|
local PROGRAM_RAM_END = 0x01000000 -- IOCS's own test for "a handler is installed"
|
|
local GPIP6_BIT = 0x40
|
|
local ISRA_EXIT = 0xBF -- jlX68kRasterIsr's in-service ack
|
|
local CPU_HZ = 10000000 -- 40 MHz / 4 (MAME x68k.cpp)
|
|
local IRQ_HPOS = 2 -- x68k_crtc.cpp: R09 = N fires at (N - 1, 2)
|
|
|
|
-- examples/scb/scb.c: the picture, from its rules.
|
|
local STRIPE_W = 16
|
|
local EVERY_LINE_END, IRREGULAR_END = 64, 136
|
|
local RUN_LENGTH_STEP, RUN_LENGTH_SPAN = 5, 7
|
|
local RUN_PALETTE_STEP, RUN_PALETTE_BASE = 7, 3
|
|
local EVEN_LINE_PALETTE, ODD_LINE_PALETTE = 6, 9
|
|
local BLUE_PALETTE_STEP = 7
|
|
|
|
local scb = {}
|
|
do
|
|
for y = 0, EVERY_LINE_END - 1 do
|
|
scb[y] = y % COLORS
|
|
end
|
|
local run, left, pal = 0, 0, 0
|
|
for y = EVERY_LINE_END, IRREGULAR_END - 1 do
|
|
if left == 0 then
|
|
left = 1 + (run * RUN_LENGTH_STEP) % RUN_LENGTH_SPAN
|
|
pal = (run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % COLORS
|
|
run = run + 1
|
|
end
|
|
scb[y] = pal
|
|
left = left - 1
|
|
end
|
|
for y = IRREGULAR_END, STAGE_H - 1 do
|
|
scb[y] = (y % 2 == 1) and ODD_LINE_PALETTE or EVEN_LINE_PALETTE
|
|
end
|
|
end
|
|
|
|
-- Colour i of palette p as \$0RGB (colour 0 is black in every palette).
|
|
local function rgb12(p, i)
|
|
if i == 0 then
|
|
return 0
|
|
end
|
|
return (p << 8) | (i << 4) | ((p * BLUE_PALETTE_STEP + i) % COLORS)
|
|
end
|
|
|
|
-- A text palette word is GGGGG RRRRR BBBBB I (MAME x68k.cpp GGGGGRRRRRBBBBBI);
|
|
-- the top four bits of each five-bit field are the \$0RGB channel.
|
|
local function wordRgb12(w)
|
|
return (((w >> 7) & 15) << 8) | (((w >> 12) & 15) << 4) | ((w >> 2) & 15)
|
|
end
|
|
|
|
-- A rendered pixel: the top four bits of each 8-bit channel.
|
|
local function pixelRgb12(px)
|
|
return (((px >> 20) & 15) << 8) | (((px >> 12) & 15) << 4) | ((px >> 4) & 15)
|
|
end
|
|
|
|
local function out(s)
|
|
io.write("SCB-X68K " .. s .. "\n")
|
|
io.flush()
|
|
end
|
|
|
|
-- 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(px, py)
|
|
local x = ORIGIN_X + px
|
|
local byteOff = (ORIGIN_Y + shownRow() + py) * TVRAM_STRIDE + (x >> 3)
|
|
local bit = 0x80 >> (x & 7)
|
|
local v = 0
|
|
for plane = 0, 3 do
|
|
if (mem:read_u8(TVRAM + plane * PLANE_STEP + byteOff) & bit) ~= 0 then
|
|
v = v | (1 << plane)
|
|
end
|
|
end
|
|
return v
|
|
end
|
|
|
|
local function stripesUp()
|
|
for s = 0, STAGE_W // STRIPE_W - 1 do
|
|
if stagePixel(s * STRIPE_W + STRIPE_W // 2, STAGE_H // 2) ~= s % COLORS then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Screen timing, filled in when the capture starts.
|
|
local fp, sp, pp, dotsPerLine, vbegin, stageDot0, zero0
|
|
local function posTime(frameIdx, line, dot)
|
|
return zero0 + frameIdx * fp + line * sp + dot * pp
|
|
end
|
|
local function dotsOf(seconds)
|
|
return seconds / pp
|
|
end
|
|
|
|
-- Event log: kind 1 palette write, 2 R09 write, 3 vector fetch, 4 ISRA exit.
|
|
local evT, evKind, evOff, evData = {}, {}, {}, {}
|
|
local function log(kind, offset, data)
|
|
local n = #evT + 1
|
|
evT[n], evKind[n], evOff[n], evData[n] = emu.time(), kind, offset, data
|
|
end
|
|
|
|
local taps, snaps = {}, {}
|
|
local initialPalette = {}
|
|
local frame, stripesAt, captureAt, captured = 0, 0, 0, 0
|
|
local verdictAt, exitDeadline, isrAddress = 0, 0, 0
|
|
local tickAddress, vdispAddress = 0, 0
|
|
local done, failed = false, false
|
|
|
|
local function fail(msg)
|
|
failed = true
|
|
out("FAIL " .. msg)
|
|
end
|
|
|
|
local function startCapture()
|
|
local r = {}
|
|
for i = 0, 8 do
|
|
r[i] = mem:read_u16(CRTC + i * 2)
|
|
end
|
|
fp, sp, pp = scr.frame_period, scr.scan_period, scr.pixel_period
|
|
dotsPerLine = math.floor(sp / pp + 0.5)
|
|
local lines = math.floor(fp / sp + 0.5)
|
|
-- x68k_crtc.cpp refresh_mode: hbegin = R02 * 8 + 1; the text row on screen
|
|
-- line L is L - vbegin, vbegin = R06 (x68k_v.cpp get_text_pixel).
|
|
vbegin = r[6]
|
|
stageDot0 = r[2] * 8 + 1 + ORIGIN_X
|
|
out(string.format("crtc R00=%d R02=%d R03=%d R04=%d R06=%d R07=%d R20=%04X frame=%.3fms line=%.3fus dots=%d lines=%d",
|
|
r[0], r[2], r[3], r[4], r[6], r[7], mem:read_u16(CRTC + 40), fp * 1e3, sp * 1e6, dotsPerLine, lines))
|
|
if lines ~= r[4] + 1 then
|
|
fail(string.format("the display is interlaced or not the mode the port documents (%d lines a frame, R04+1 = %d)", lines, r[4] + 1))
|
|
return false
|
|
end
|
|
for i = 0, COLORS - 1 do
|
|
initialPalette[i] = mem:read_u16(TEXT_PALETTE + i * 2)
|
|
end
|
|
zero0 = emu.time() + scr:time_until_pos(0, 0)
|
|
taps[#taps + 1] = mem:install_write_tap(TEXT_PALETTE, TEXT_PALETTE + COLORS * 2 - 1, "scbPal", function(offset, data, mask)
|
|
log(1, offset, data)
|
|
end)
|
|
taps[#taps + 1] = mem:install_write_tap(CRTC_R09, CRTC_R09 + 1, "scbR09", function(offset, data, mask)
|
|
log(2, offset, data)
|
|
end)
|
|
taps[#taps + 1] = mem:install_read_tap(RASTER_VECTOR, RASTER_VECTOR + 3, "scbVec", function(offset, data, mask)
|
|
if offset == RASTER_VECTOR then
|
|
log(3, offset, data)
|
|
end
|
|
end)
|
|
taps[#taps + 1] = mem:install_write_tap(MFP_ISRA - 1, MFP_ISRA, "scbIsra", function(offset, data, mask)
|
|
if (data & mask & 0xFF) == ISRA_EXIT then
|
|
log(4, offset, data)
|
|
end
|
|
end)
|
|
isrAddress = mem:read_u32(RASTER_VECTOR)
|
|
tickAddress = mem:read_u32(TICK_VECTOR)
|
|
vdispAddress = mem:read_u32(VDISP_VECTOR)
|
|
local ch13 = mem:read_u8(MFP_IERA) & mem:read_u8(MFP_IMRA) & TIMER_A_BIT
|
|
out(string.format("clocks: \$114 = %08X, \$134 = %08X, channel 13 %s", tickAddress, vdispAddress, (ch13 ~= 0) and "on" or "off"))
|
|
if tickAddress >= PROGRAM_RAM_END or vdispAddress >= PROGRAM_RAM_END or ch13 == 0 then
|
|
fail("the clock interrupts are not the program's while it runs")
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function stopTaps()
|
|
for _, t in ipairs(taps) do
|
|
t:remove()
|
|
end
|
|
taps = {}
|
|
end
|
|
|
|
-- Stage span of row y in frame k: [first stage pixel, end of the last).
|
|
-- Row -1 is the border line above the stage: black in every palette, but it
|
|
-- is the line whose stage columns a reload for row 0 must not reach.
|
|
local function spanStart(k, y)
|
|
return posTime(k, vbegin + ORIGIN_Y + y, stageDot0)
|
|
end
|
|
local function spanEnd(k, y)
|
|
return posTime(k, vbegin + ORIGIN_Y + y, stageDot0 + STAGE_W)
|
|
end
|
|
|
|
local function stats(list)
|
|
table.sort(list)
|
|
local n = #list
|
|
if n == 0 then
|
|
return "none"
|
|
end
|
|
local sum = 0
|
|
for _, v in ipairs(list) do
|
|
sum = sum + v
|
|
end
|
|
return string.format("min %.0f median %.0f mean %.0f max %.0f", list[1], list[(n + 1) // 2], sum / n, list[n])
|
|
end
|
|
|
|
-- The timeline check: what a CRT shows, line by line, from the write log.
|
|
local function checkTimeline()
|
|
local state = {}
|
|
for i = 0, COLORS - 1 do
|
|
state[i] = initialPalette[i]
|
|
end
|
|
local ev, nEv = 1, #evT
|
|
local lines, wrong, torn, bands, missed = 0, 0, 0, 0, 0
|
|
local early, late = {}, {}
|
|
local reported = 0
|
|
-- Frame 0 of the capture is partial (the taps went in mid-frame); frames
|
|
-- 1..CAPTURE-1 are checked whole.
|
|
for k = 1, CAPTURE - 1 do
|
|
for y = 0, STAGE_H - 1 do
|
|
local s, e = spanStart(k, y), spanEnd(k, y)
|
|
local prevEnd = (y == 0) and spanEnd(k - 1, STAGE_H - 1) or spanEnd(k, y - 1)
|
|
local first, last = nil, nil
|
|
while ev <= nEv and evT[ev] <= s do
|
|
if evKind[ev] == 1 then
|
|
state[(evOff[ev] - TEXT_PALETTE) // 2] = evData[ev]
|
|
if evT[ev] > prevEnd then
|
|
first = first or evT[ev]
|
|
last = evT[ev]
|
|
end
|
|
end
|
|
ev = ev + 1
|
|
end
|
|
local ok = true
|
|
for i = 0, COLORS - 1 do
|
|
if wordRgb12(state[i]) ~= rgb12(scb[y], i) then
|
|
ok = false
|
|
break
|
|
end
|
|
end
|
|
local tearAt = nil
|
|
local look = ev
|
|
while look <= nEv and evT[look] < e do
|
|
if evKind[look] == 1 then
|
|
tearAt = evT[look]
|
|
break
|
|
end
|
|
look = look + 1
|
|
end
|
|
lines = lines + 1
|
|
if not ok then
|
|
wrong = wrong + 1
|
|
end
|
|
if tearAt then
|
|
torn = torn + 1
|
|
end
|
|
local prevY = (y == 0) and (STAGE_H - 1) or (y - 1)
|
|
if scb[prevY] ~= scb[y] then
|
|
bands = bands + 1
|
|
if first then
|
|
early[#early + 1] = dotsOf(first - spanEnd(k, y - 1))
|
|
late[#late + 1] = dotsOf(s - last)
|
|
else
|
|
missed = missed + 1
|
|
end
|
|
end
|
|
if (not ok or tearAt) and reported < 12 then
|
|
reported = reported + 1
|
|
local shows = {}
|
|
for i = 0, COLORS - 1 do
|
|
shows[#shows + 1] = string.format("%03X", wordRgb12(state[i]))
|
|
end
|
|
out(string.format(" frame %d line %d: palette %d expected, shows [%s]%s", k, y, scb[y],
|
|
table.concat(shows, " "),
|
|
tearAt and string.format(", write %.0f dots into the line's stage pixels", dotsOf(tearAt - s)) or ""))
|
|
end
|
|
end
|
|
end
|
|
out(string.format("timeline lines=%d wrong=%d torn=%d bandStarts=%d reloadsFound=%d", lines, wrong, torn, bands, bands - missed))
|
|
out(" reload start, dots after the last stage pixel of the line above: " .. stats(early))
|
|
out(" reload end, dots before the band's first stage pixel: " .. stats(late))
|
|
out(string.format(" one dot = %.1f ns, one CPU cycle = %.2f dots", pp * 1e9, (1 / CPU_HZ) / pp))
|
|
return wrong == 0 and torn == 0 and missed == 0 and bands > 0
|
|
end
|
|
|
|
-- Interrupt cost, latency and slack from the entry/exit/R09 events. The first
|
|
-- R09 write after an entry re-points the compare at the raster of the line
|
|
-- above the band (jlX68kRasterIsr.s step 2); it must land before that raster
|
|
-- begins, and its slack is how far ahead it did. The last R09 write before an
|
|
-- entry is the arm that raised it.
|
|
local function reportInterrupt()
|
|
local inside, entries, latencies, slacks = 0, 0, {}, {}
|
|
local entryAt, armValue, armAt = nil, nil, nil
|
|
local firstT, lastT = nil, nil
|
|
local awaitReference = false
|
|
for n = 1, #evT do
|
|
local kind = evKind[n]
|
|
if kind == 3 then
|
|
entryAt = evT[n]
|
|
if armValue and armAt then
|
|
-- The raster match: the first (armValue - 1, IRQ_HPOS) after the arm.
|
|
local k = math.floor((armAt - zero0) / fp)
|
|
local assertAt = posTime(k, armValue - 1, IRQ_HPOS)
|
|
if assertAt < armAt then
|
|
assertAt = assertAt + fp
|
|
end
|
|
latencies[#latencies + 1] = (entryAt - assertAt) * CPU_HZ
|
|
end
|
|
armValue = nil
|
|
awaitReference = true
|
|
elseif kind == 2 then
|
|
armValue, armAt = evData[n], evT[n]
|
|
if awaitReference then
|
|
local k = math.floor((evT[n] - zero0) / fp)
|
|
slacks[#slacks + 1] = (posTime(k, evData[n] - 1, IRQ_HPOS) - evT[n]) * CPU_HZ
|
|
awaitReference = false
|
|
end
|
|
elseif kind == 4 and entryAt then
|
|
inside = inside + (evT[n] - entryAt)
|
|
entries = entries + 1
|
|
firstT = firstT or entryAt
|
|
lastT = evT[n]
|
|
entryAt = nil
|
|
end
|
|
end
|
|
if entries == 0 then
|
|
out("interrupt never ran during the capture")
|
|
return
|
|
end
|
|
local span = (lastT - firstT) / fp
|
|
out(string.format("interrupt entries=%d over %.1f frames: %.1f per frame, %.2f ms inside it per frame (%.1f%% of a frame)",
|
|
entries, span, entries / span, (inside / span) * 1e3, (inside / span) / fp * 100))
|
|
out(" latency, raster match -> vector fetch (CPU cycles): " .. stats(latencies))
|
|
out(" slack, step-2 compare write -> its raster (CPU cycles): " .. stats(slacks))
|
|
end
|
|
|
|
-- The bitmap check: MAME's own rendering, within MAME's one-line look-ahead.
|
|
local function checkSnapshot(pix, w, label)
|
|
local rows, exact, ahead, bad = 0, 0, 0, 0
|
|
local stripes = STAGE_W // STRIPE_W
|
|
for y = 0, STAGE_H - 1 do
|
|
local row = ORIGIN_Y + y -- visible area starts at vbegin
|
|
local here = scb[y]
|
|
local next = scb[(y + 1) % STAGE_H]
|
|
local isExact, isAhead = true, true
|
|
for s = 0, stripes - 1 do
|
|
local x = ORIGIN_X + s * STRIPE_W + STRIPE_W // 2
|
|
local c = pixelRgb12(string.unpack("<I4", pix, (row * w + x) * 4 + 1))
|
|
local col = s % COLORS
|
|
if c ~= rgb12(here, col) then
|
|
isExact = false
|
|
end
|
|
if c ~= rgb12(here, col) and c ~= rgb12(next, col) then
|
|
isAhead = false
|
|
end
|
|
end
|
|
rows = rows + 1
|
|
if isExact then
|
|
exact = exact + 1
|
|
elseif isAhead then
|
|
ahead = ahead + 1
|
|
else
|
|
bad = bad + 1
|
|
if bad <= 6 then
|
|
out(string.format(" bitmap %s line %d: neither palette %d nor the next line's %d", label, y, here, next))
|
|
end
|
|
end
|
|
end
|
|
out(string.format("bitmap %s lines=%d exact=%d nextLineAhead=%d wrong=%d", label, rows, exact, ahead, bad))
|
|
return bad == 0
|
|
end
|
|
|
|
emu.register_frame_done(function()
|
|
frame = frame + 1
|
|
if done then
|
|
return
|
|
end
|
|
if stripesAt == 0 then
|
|
if frame % SAMPLE == 0 and stripesUp() then
|
|
stripesAt = frame
|
|
captureAt = frame + SETTLE
|
|
out("stripes up f" .. frame)
|
|
end
|
|
elseif captureAt ~= 0 and frame == captureAt then
|
|
if not startCapture() then
|
|
done = true
|
|
manager.machine:exit()
|
|
return
|
|
end
|
|
nat:post(TYPED .. TYPED .. TYPED)
|
|
elseif captureAt ~= 0 and frame > captureAt and verdictAt == 0 then
|
|
captured = captured + 1
|
|
if SNAPSHOTS[captured] then
|
|
local pix, w = scr:pixels()
|
|
snaps[#snaps + 1] = { pix, w, "f" .. captured }
|
|
end
|
|
if captured >= CAPTURE then
|
|
stopTaps()
|
|
out(string.format("captured %d frames, %d events", CAPTURE, #evT))
|
|
local ok = checkTimeline()
|
|
reportInterrupt()
|
|
for _, sn in ipairs(snaps) do
|
|
ok = checkSnapshot(sn[1], sn[2], sn[3]) and ok
|
|
end
|
|
if not ok then
|
|
fail("the SCB palette reloads do not land where they must")
|
|
end
|
|
verdictAt = frame
|
|
exitDeadline = frame + EXIT_FRAMES
|
|
end
|
|
elseif verdictAt ~= 0 then
|
|
if frame % KEY_EVERY == 0 then
|
|
nat:post_coded("{ESC}")
|
|
end
|
|
local vec = mem:read_u32(RASTER_VECTOR)
|
|
local ch14 = (mem:read_u8(MFP_IERA) | mem:read_u8(MFP_IMRA)) & GPIP6_BIT
|
|
local tick = mem:read_u32(TICK_VECTOR)
|
|
local vdsp = mem:read_u32(VDISP_VECTOR)
|
|
local ch13 = (mem:read_u8(MFP_IERA) | mem:read_u8(MFP_IMRA)) & TIMER_A_BIT
|
|
if vec ~= isrAddress and ch14 == 0 and tick ~= tickAddress and vdsp ~= vdispAddress and ch13 == 0 then
|
|
out(string.format("exit ok: \$138 = %08X (was the interrupt at %08X), channel 14 off", vec, isrAddress))
|
|
out(string.format("exit ok: \$114 = %08X, \$134 = %08X, channel 13 off", tick, vdsp))
|
|
out(failed and "verdict FAIL" or "verdict PASS")
|
|
done = true
|
|
manager.machine:exit()
|
|
elseif frame >= exitDeadline then
|
|
fail(string.format("the program did not exit and hand back \$138 (%08X) and channel 14, \$114 (%08X), \$134 (%08X) and channel 13", vec, tick, vdsp))
|
|
out("verdict FAIL")
|
|
done = true
|
|
manager.machine:exit()
|
|
end
|
|
end
|
|
if not done and frame > FRAMES then
|
|
out("verdict FAIL: the stripes never appeared or the capture never finished (f" .. frame .. ")")
|
|
done = true
|
|
manager.machine:exit()
|
|
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. The Lua ends
|
|
# the run as soon as the verdict is in and the program has exited.
|
|
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/scb.lua" < /dev/null > "$work/mame.log" 2>&1 &
|
|
mame=$!
|
|
mameChild=$(joeyChild "$mame")
|
|
wait "$mame"
|
|
mame=""
|
|
mameChild=""
|
|
|
|
out=$(grep '^SCB-X68K' "$work/mame.log")
|
|
echo "${out:-SCB-X68K: no report from the Lua (MAME died, or hit the ${wall}s cap)}"
|
|
if grep -q '^SCB-X68K verdict PASS' <<< "$out"; then
|
|
echo "verify-x68000-scb: PASS"
|
|
exit 0
|
|
fi
|
|
echo "verify-x68000-scb: FAIL" >&2
|
|
exit 1
|