#!/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-icon.sh - IIgs programs' icons as the Finder shows them. # # make bakes every IIgs program a Finder icon file (make/iigs.mk: # tools/iconBake.py iigs, type $CA: an icon record for a GS/OS application of # the program's name, black and white), and make-iigs-disk.sh puts it in the # JOEYLIB volume's Icons folder, which the Finder reads when it starts. GS/OS # boots under MAME into its Finder with joey.2mg mounted; the gate opens the # JOEYLIB window as verify-iigs.sh does (J, then Command-O) and at READ_FRAME # reads the Super Hi-Res screen out of emulated memory -- the pixels, the # scan line control bytes and the palettes -- and draws it as it shows in 640 # mode. The window must show the baked icon: every pixel its mask leaves # opaque, as the two screen pixels QuickDraw draws a 4-bit icon pixel as in # 640 mode, the first its high two bits and the second its low two. The # screen's own 2-bit values are compared, so the check does not lean on the # palette; the icon's colours are the Finder's palette's (tools/iconBake.py). # # Usage: scripts/verify-iigs-icon.sh # Env: IIGS_ICON_KEEP keep the screen as build/iigs/iigs-icon.png # MAME_READ_FRAME the frame the screen is read at (default 5400) # Exit: 0 PASS, 1 FAIL, 2 SKIP (MAME or the IIgs firmware not installed, # or joey.2mg not built) # Requires: ./install.sh install emulator:iigs, python3 + PIL + numpy; # build/iigs/bin/joey.2mg (make iigs-disk). set -euo 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 label=verify-iigs-icon data_disk=$repo/build/iigs/bin/joey.2mg icon=$repo/build/iigs/icon/DRAW/finder.icons readFrame="${MAME_READ_FRAME:-5400}" [ -f "$data_disk" ] && [ -f "$icon" ] || { echo "$label: build the disk first (make iigs-disk)" >&2; exit 2; } work=$(mktemp -d -t joeylib-iigs-icon.XXXXXX) trap 'rm -rf "$work"' EXIT cp "$GSOS_SYSTEM_DISK" "$work/boot.po" cp "$data_disk" "$work/joey.2mg" # The Finder keystrokes verify-iigs.sh calibrated: select the JOEYLIB volume # (J), Command-O to open its window. At readFrame the SHR block, $E1/2000 to # $E1/9FFF (pixels, scan line control bytes, palettes), goes to shr.bin. cat > "$work/icon.lua" <= steps[idx][1] do steps[idx][2]() idx = idx + 1 end end) LUA (cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ timeout 300 "$MAME" apple2gs -rompath "$IIGS_ROMPATH" \ -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ -video none -sound none -nothrottle \ -autoboot_script "$work/icon.lua" < /dev/null > "$work/mame.log" 2>&1) || true [ -s "$work/shr.bin" ] || { echo "$label: FAIL - MAME never reached frame $readFrame" >&2; tail -5 "$work/mame.log" >&2; exit 1; } python3 - "$work/shr.bin" "$icon" "$label" "${IIGS_ICON_KEEP:+$repo/build/iigs/iigs-icon.png}" <<'PY' import struct import sys import numpy from PIL import Image shrPath, iconPath, label, keep = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] WIDTH_640 = 640 WIDTH_320 = 320 HEIGHT = 200 ROW_BYTES = 160 SCB_AT = 0x7D00 PALETTES_AT = 0x7E00 SCB_640 = 0x80 # A 640-mode screen pixel is 2 bits; so is each half of an icon pixel. HALF_BITS = 2 HALF_MASK = 3 shr = open(shrPath, "rb").read() def colour(word): """A $0RGB palette word as 8-bit RGB.""" return (((word >> 8) & 0xF) * 17, ((word >> 4) & 0xF) * 17, (word & 0xF) * 17) # The screen at 640 pixels a line: a 640-mode line's pixel takes its colour # from the palette quarter its place in the byte gives it (bits 7-6 from # entries 8-11, 5-4 from 12-15, 3-2 from 0-3, 1-0 from 4-7); a 320-mode # line's pixel is shown twice. # Each 640-mode screen pixel's own 2-bit value goes in values (-1 on a # 320-mode line). screen = numpy.zeros((HEIGHT, WIDTH_640, 3), dtype=numpy.uint8) values = numpy.full((HEIGHT, WIDTH_640), -1, dtype=numpy.int8) for y in range(HEIGHT): scb = shr[SCB_AT + y] palette = [colour(struct.unpack_from("> (6 - 2 * p)) & HALF_MASK screen[y, i * 4 + p] = palette[base + values[y, i * 4 + p]] else: for p, value in enumerate((byte >> 4, byte & 0xF)): screen[y, i * 4 + p * 2] = palette[value] screen[y, i * 4 + p * 2 + 1] = palette[value] if keep: Image.fromarray(screen).resize((WIDTH_640, HEIGHT * 2), Image.NEAREST).save(keep) # The big icon of the file's one record: its 4-bit pixels and mask, the even # pixel in the high nibble. record = 26 + 86 iconType, size, h, w = struct.unpack_from("> shift) & 0xF: want.append((y, x, (data[y * rowBytes + x // 2] >> shift) & 0xF)) # In 640 mode an icon pixel is two screen pixels: its high half, then its # low half. ys = numpy.array([p[0] for p in want for half in (0, 1)]) xs = numpy.array([p[1] * 2 + half for p in want for half in (0, 1)]) halves = numpy.array([(p[2] >> HALF_BITS) if half == 0 else (p[2] & HALF_MASK) for p in want for half in (0, 1)], dtype=numpy.int8) found = [] for top in range(HEIGHT - h + 1): for left in range(WIDTH_640 - w * 2 + 1): if numpy.array_equal(values[top + ys, left + xs], halves): found.append((left, top)) if not found: print(f"{label}: FAIL - no program's icon is in the JOEYLIB window ({len(want)} opaque pixels looked for)") sys.exit(1) print(f"{label}: PASS (the Finder shows {len(found)} programs by the baked {w}x{h} colour icon, the first at {found[0][0]},{found[0][1]}: all {len(want)} opaque pixels, each its two screen pixels' values)") PY