171 lines
7.1 KiB
Bash
Executable file
171 lines
7.1 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-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" <<LUA
|
|
local mem = manager.machine.devices[":maincpu"].spaces["program"]
|
|
local nat = manager.machine.natkeyboard
|
|
local frame = 0
|
|
local idx = 1
|
|
local cmd = manager.machine.ioport.ports[":macadb:KEY3"].fields["Command / Open Apple"]
|
|
|
|
local function dump()
|
|
local out = io.open("$work/shr.bin", "wb")
|
|
local chunk = {}
|
|
for i = 0, 0x7FFF do
|
|
chunk[#chunk + 1] = string.char(mem:read_u8(0xE12000 + i))
|
|
if #chunk == 256 then
|
|
out:write(table.concat(chunk))
|
|
chunk = {}
|
|
end
|
|
end
|
|
out:close()
|
|
end
|
|
|
|
local steps = {
|
|
{3000, function() nat:post("J") end},
|
|
{3120, function() cmd:set_value(1) end},
|
|
{3126, function() nat:post("o") end},
|
|
{3180, function() cmd:set_value(0) end},
|
|
{$readFrame, function() dump(); manager.machine:exit() 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
|
|
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("<H", shr, PALETTES_AT + (scb & 0xF) * 32 + i * 2)[0]) for i in range(16)]
|
|
row = shr[y * ROW_BYTES:(y + 1) * ROW_BYTES]
|
|
for i, byte in enumerate(row):
|
|
if scb & SCB_640:
|
|
for p, base in enumerate((8, 12, 0, 4)):
|
|
values[y, i * 4 + p] = (byte >> (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("<HHHH", open(iconPath, "rb").read(), record)
|
|
data = open(iconPath, "rb").read()[record + 8:]
|
|
rowBytes = (w + 1) // 2
|
|
want = []
|
|
for y in range(h):
|
|
for x in range(w):
|
|
shift = 4 if x % 2 == 0 else 0
|
|
if (data[size + y * rowBytes + x // 2] >> 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
|