#!/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-amiga-palette.sh - the Amiga's palette and SCB changes through the # copper: fast, and right on the screen. # # The Amiga shows each SCB band's palette from one of two band lists of its # own, which the screen's user copper list (merged by graphics.library once) # jumps to: a present that changes palettes or the SCB writes the list not # shown, and the VBL server switches to it with the planes (src/amiga/hal.c, # bandListUpdate). Before, a band that moved rebuilt and merged the whole # copper list (MrgCop, LoadView, then a WaitTOF) -- about three frames on an # A500 -- and a palette change patched the shown list, a frame before its # pixels. The stage hashes cannot see any of this: they hash the surface, # not the copper. # # PALPROBE (examples/palprobe; make -f make/amiga.mk build/amiga/test/Palprobe) # runs under FS-UAE on an A500. It logs how many palette changes, and SCB # changes (bands moving), it presented a second, back to back: each waits # for the VBL that shows the last, so both rates must be at least MIN_RATE, # nearly the frame rate (a rebuild a change gave 16). Then it holds a screen # of four bands after one palette change and one same-layout SCB change, # and the screenshot's bands must be, top to bottom, red, green, yellow, red. # # Usage: scripts/verify-amiga-palette.sh # Env: TIMEOUT seconds to wait for the probe (default 120) # Exit: 0 PASS, 1 FAIL, 2 SKIP (FS-UAE or the Kickstart not installed) set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" joeyNeed FSUAE fsuae source "$repo/scripts/lib/amigaGate.sh" amigaMachine timeout_s=${TIMEOUT:-120} probe=$repo/build/amiga/test/Palprobe label=verify-amiga-palette # Changes presented a second, back to back: one a frame is 50 on the PAL # A500 (16 when every change rebuilt the copper list). MIN_RATE=45 make -s -f "$repo/make/amiga.mk" "$probe" || { echo "$label: FAIL - building Palprobe failed" >&2 exit 1 } work=$(mktemp -d -t joeylib-amiga-palette.XXXXXX) mkdir -p "$work/s" cp "$probe" "$work/" printf 'stack 32768\n:Palprobe\n' > "$work/s/startup-sequence" display=:$((RANDOM % 400 + 1300)) Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 & xvfb_pid=$! fs_pid= cleanup() { [[ -n $fs_pid ]] && joeyStop "$fs_pid" kill "$xvfb_pid" 2>/dev/null || true wait 2>/dev/null || true rm -rf "$work" } trap cleanup EXIT sleep 0.5 DISPLAY=$display "$FSUAE" "${AMIGA_MACHINE_ARGS[@]}" --fast_memory=2048 \ --hard_drive_0="$work" --hard_drive_0_label=JOEYLIB \ --automatic_input_grab=0 \ --fullscreen=0 --fade_out_duration=0 > "$work/fsuae.log" 2>&1 & fs_pid=$! log=$work/joeylog.txt for ((waited = 0; waited < timeout_s; waited++)); do if grep -q "PALPROBE: ready" "$log" 2>/dev/null; then break fi sleep 1 done if ! grep -q "PALPROBE: ready" "$log" 2>/dev/null; then echo "$label: FAIL - the probe never reached its held screen within ${timeout_s}s" >&2 exit 1 fi # Two seconds into the hold: the copper has run the patched list. sleep 2 DISPLAY=$display import -window root "$work/screen.png" grep "PALPROBE:" "$log" python3 - "$work/screen.png" "$log" "$MIN_RATE" "$label" <<'PY' import re import sys from PIL import Image png, log, minRate, label = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4] rates = {} for line in open(log, encoding="ascii", errors="replace"): m = re.search(r"PALPROBE: (palette|scb)\+present: \d+ in \d+ ms = (\d+)/s", line) if m: rates[m.group(1)] = int(m.group(2)) if sorted(rates) != ["palette", "scb"]: print(f"{label}: FAIL - the log does not have both rates ({rates})") sys.exit(1) # The bands down the middle column, top to bottom, black (the border) # dropped: each run of one colour is a band. img = Image.open(png).convert("RGB") w, h = img.size named = {(255, 0, 0): "red", (0, 255, 0): "green", (255, 255, 0): "yellow"} bands = [] for y in range(h): rgb = img.getpixel((w // 2, y)) if rgb == (0, 0, 0): continue name = named.get(rgb, "other " + str(rgb)) if not bands or bands[-1] != name: bands.append(name) want = ["red", "green", "yellow", "red"] fails = [] for kind in ("palette", "scb"): if rates[kind] < minRate: fails.append(f"{kind} changes presented {rates[kind]}/s, want at least {minRate}") if bands != want: fails.append(f"bands {bands}, want {want}") if fails: print(f"{label}: FAIL - " + "; ".join(fails)) sys.exit(1) print(f"{label}: PASS (palette changes {rates['palette']}/s, SCB changes {rates['scb']}/s; bands {', '.join(want)} after a palette change and a same-layout SCB change)") PY