116 lines
4.2 KiB
Bash
Executable file
116 lines
4.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# verify-dos-input.sh - Headless typed-character acceptance gate for the
|
|
# DOS port. Runs KEYS.EXE in DOSBox Staging under Xvfb, types the
|
|
# RetroNet definition-of-done string with xdotool (real X key events,
|
|
# shift and all, so ':' exercises the INT 9 shift path), takes a RAW
|
|
# native-resolution screenshot, and decodes the example's verification
|
|
# scanline (row 198) from the exact VGA DAC colors. KEYS gives all 16
|
|
# palette entries distinct colors precisely so this decode is exact.
|
|
#
|
|
# Usage: scripts/verify-dos-input.sh
|
|
# Requires: dosbox (staging), Xvfb, xdotool, python3 + PIL;
|
|
# build/dos/bin/KEYS.EXE (make dos).
|
|
set -euo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
bin_dir=$repo/build/dos/bin
|
|
conf=$repo/scripts/dosbox-386sx16.conf
|
|
|
|
target="192.168.1.10:6510"
|
|
|
|
[ -f "$bin_dir/KEYS.EXE" ] || { echo "verify-dos-input: build KEYS.EXE first (make dos)" >&2; exit 2; }
|
|
|
|
work=$(mktemp -d -t joeylib-dos-input.XXXXXX)
|
|
display_num=$((RANDOM % 500 + 300))
|
|
cleanup() {
|
|
[ -n "${dosbox_pid:-}" ] && kill "$dosbox_pid" 2>/dev/null
|
|
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 &
|
|
xvfb_pid=$!
|
|
sleep 1
|
|
|
|
DISPLAY=":$display_num" dosbox \
|
|
-conf "$conf" \
|
|
-set "capture capture_dir=$work" \
|
|
-set "capture default_image_capture_formats=raw" \
|
|
-c "C:" -c "KEYS.EXE" \
|
|
"$bin_dir" >/dev/null 2>&1 &
|
|
dosbox_pid=$!
|
|
|
|
# Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16.
|
|
sleep 12
|
|
|
|
# Staging titles its window "<PROG> - <n> cycles/ms"; it is the only
|
|
# client on this private display, so match any named window.
|
|
win=$(DISPLAY=":$display_num" xdotool search --name -- "." | head -1)
|
|
if [ -z "$win" ]; then
|
|
echo "verify-dos-input: FAIL - DOSBox window not found" >&2
|
|
exit 1
|
|
fi
|
|
DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true
|
|
DISPLAY=":$display_num" xdotool type --window "$win" --delay 150 -- "$target"
|
|
sleep 2
|
|
# Ctrl+F5 = DOSBox screenshot hotkey.
|
|
DISPLAY=":$display_num" xdotool key --window "$win" ctrl+F5
|
|
sleep 2
|
|
|
|
png=$(ls "$work"/*.png 2>/dev/null | head -1)
|
|
if [ -z "$png" ]; then
|
|
echo "verify-dos-input: FAIL - no screenshot captured" >&2
|
|
exit 1
|
|
fi
|
|
|
|
python3 - "$png" "$target" <<'PY'
|
|
import sys
|
|
from PIL import Image
|
|
|
|
png, target = sys.argv[1], sys.argv[2]
|
|
img = Image.open(png).convert("RGB")
|
|
w, h = img.size
|
|
# Raw capture is native 320x200; tolerate an integer upscale.
|
|
sx, sy = w // 320, h // 200
|
|
if sx < 1 or sy < 1 or w % 320 or h % 200:
|
|
print(f"verify-dos-input: FAIL - unexpected screenshot size {w}x{h}")
|
|
sys.exit(1)
|
|
|
|
def dac8(c4):
|
|
v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F
|
|
return (v6 << 2) | (v6 >> 4)
|
|
|
|
# KEYS palette 0 (see buildPalette): gray ramp with the four demo
|
|
# colors and red at 15. 12-bit 0x0RGB -> exact 8-bit via the port's
|
|
# 6-bit DAC expansion.
|
|
pal12 = [(i << 8) | (i << 4) | i for i in range(16)]
|
|
pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF
|
|
pal12[15] = 0xF00
|
|
# Nearest-color match: emulators differ in how they scale the 6-bit
|
|
# DAC to 8-bit (bit-replication vs rounding), but the palette entries
|
|
# sit ~17 units apart per channel, so nearest is unambiguous.
|
|
palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12]
|
|
|
|
def pix(px):
|
|
r, g, b = img.getpixel((px * sx, 198 * sy))
|
|
best, bestDist = 0, 1 << 30
|
|
for n, (pr, pg, pb) in enumerate(palRgb):
|
|
d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
|
|
if d < bestDist:
|
|
best, bestDist = n, d
|
|
if bestDist > 3 * 8 ** 2:
|
|
print(f"verify-dos-input: FAIL - pixel {px} color {(r, g, b)} too far from any palette entry")
|
|
sys.exit(1)
|
|
return best
|
|
|
|
row = bytes((pix(2 * i) << 4) | pix(2 * i + 1) for i in range(40))
|
|
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
|
|
got = row[: len(expect)]
|
|
print("VERIFY-DOS-INPUT row=" + row.hex().upper())
|
|
if got == expect:
|
|
print(f'verify-dos-input: PASS (typed "{target}", all {len(target)} chars arrived via jlInputGetChar)')
|
|
else:
|
|
print(f"verify-dos-input: FAIL\n expected: {expect.hex().upper()}\n got: {got.hex().upper()}")
|
|
sys.exit(1)
|
|
PY
|