Files
vt1200/harness/mame/vidstream_check.py
T

206 lines
8.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""vidstream_check.py -- did the row-strip blit put every source row on screen?
Task 5's pixel check. The Lua side (harness/mame/vidstream.lua, VT1200_MODE=draw)
injects ONE strip frame into the payload's slot, waits for the payload to accept
and blit it, then dumps three files:
vram-draw.bin the screen as memory: 1024 rows of 256 bytes from bit 0x04000000
frame-draw.bin the bytes actually poked into the slot (16-byte header + payload)
draw-status.txt key=value lines (RX_OK, RX_BAD, OS_TOUCH, SENTINEL, TIMEOUT, ...)
A run whose wait for the payload expired writes TIMEOUT=1, and that is reported
as a timeout -- not as a screen full of wrong pixels, which would blame the
blit for a frame the payload never accepted (ruling R15). The canary word is
also asserted (0xA5C3), so a payload that stopped arming it is a failure rather
than a silently missing check.
This script is the independent reading of the geometry, not a copy of the code
under test. The frame is 1280 pixels wide, 1 bpp, 160 bytes a row; the screen
row pitch is 2048 bits = 256 bytes (the real unit's, measured in
harness/c-hello/README.md -- MAME's own renderer uses a different pitch and is
NOT what this test checks). A source row r stretched by S must appear, byte for
byte, on screen rows r*S .. r*S+S-1.
The comparison uses the bytes that were actually delivered to the slot, not a
re-implementation of the pattern: whatever the sender wrote is what must land on
screen. The pattern itself is checked too, because a frame whose rows are all
identical would let a blit that always read row 0 pass.
usage:
vidstream_check.py [--vram PATH] [--frame PATH] [--status PATH]
[--rows 128] [--stretch 8]
Exit status 0 only if every check passes.
"""
import argparse
import os
import sys
HEADER_SIZE = 16
ROW_BYTES = 160 # 1280 pixels, 1 bpp
VRAM_PITCH = 256 # bytes per screen row (2048 bits)
SCREEN_ROWS = 1024
ZERO_ROW_MOD = 3 # rows with r % 3 == 2 are the all-zero rows
ZERO_ROW_REM = 2
FILL_BYTE = 0xF0 # rows that are not zero are 0xF0F0 words -> 0xF0 bytes
CANARY = 0xA5C3 # SENT_VALUE in vidstream.c, at row 1023 / column 1280
failures = 0
def check(cond, what):
global failures
if not cond:
failures += 1
print("FAIL: " + what)
return cond
def read_file(path):
"""The whole file, or None with a diagnosable line instead of a traceback."""
try:
with open(path, "rb") as f:
return f.read()
except OSError as e:
print("vidstream_check: cannot read %s: %s" % (path, e))
return None
def read_status(path):
st = {}
try:
with open(path, "r", encoding="ascii", errors="replace") as f:
for line in f:
if "=" in line:
k, v = line.strip().split("=", 1)
try:
st[k] = int(v, 0)
except ValueError:
st[k] = v
except OSError as e:
print("vidstream_check: cannot read %s: %s" % (path, e))
return None
return st
def main():
ap = argparse.ArgumentParser()
d = os.environ.get("VID_DUMP_DIR", "/tmp/opencode")
ap.add_argument("--vram", default=os.path.join(d, "vram-draw.bin"))
ap.add_argument("--frame", default=os.path.join(d, "frame-draw.bin"))
ap.add_argument("--status", default=os.path.join(d, "draw-status.txt"))
ap.add_argument("--rows", type=int, default=128)
ap.add_argument("--stretch", type=int, default=8)
args = ap.parse_args()
vram = read_file(args.vram)
frame = read_file(args.frame)
status = read_status(args.status)
if vram is None or frame is None or status is None:
return 2
print("vidstream_check: vram %s (%d B) frame %s (%d B) status %s"
% (args.vram, len(vram), args.frame, len(frame), args.status))
# --- a timed-out wait is not a pixel verdict (ruling R15) -------------
if status.get("TIMEOUT", 0) != 0:
print("FAIL: timed out waiting for the frame (RX_OK=%r, "
"RX_DGRAM=%r: the payload did not finish with the frame)"
% (status.get("RX_OK"), status.get("RX_DGRAM")))
print("vidstream_check: FAIL (timeout)")
return 1
# --- the frame that was delivered ------------------------------------
rows, stretch = args.rows, args.stretch
if not check(stretch > 0, "stretch must be positive, got %d" % stretch):
return 1
if not check(len(frame) == HEADER_SIZE + rows * ROW_BYTES,
"frame is %d bytes, expected 16 + %d*%d = %d"
% (len(frame), rows, ROW_BYTES, HEADER_SIZE + rows * ROW_BYTES)):
return 1
if not check(frame[0:2] == b"VS" and frame[2] == 1 and frame[3] == 1,
"delivered frame is not a version-1 KIND_STRIP frame"):
return 1
check(frame[8] == rows, "header rows=%d, expected %d" % (frame[8], rows))
check(frame[9] == stretch, "header stretch=%d, expected %d"
% (frame[9], stretch))
plen = frame[6] | (frame[7] << 8)
check(plen == rows * ROW_BYTES,
"header payload_len=%d, expected %d" % (plen, rows * ROW_BYTES))
check(rows * stretch == SCREEN_ROWS,
"rows*stretch = %d, the screen has %d rows" % (rows * stretch,
SCREEN_ROWS))
payload = frame[HEADER_SIZE:]
# --- the pattern is distinctive per row class -------------------------
zero_rows = 0
for r in range(rows):
row = payload[r * ROW_BYTES:(r + 1) * ROW_BYTES]
if r % ZERO_ROW_MOD == ZERO_ROW_REM:
zero_rows += 1
check(row == bytes(ROW_BYTES),
"source row %d should be all zero" % r)
else:
check(row == bytes([FILL_BYTE]) * ROW_BYTES,
"source row %d should be 0x%02X bytes" % (r, FILL_BYTE))
print("vidstream_check: source %d rows x %d B, stretch %d, %d zero rows"
% (rows, ROW_BYTES, stretch, zero_rows))
# --- the screen ------------------------------------------------------
if not check(len(vram) == SCREEN_ROWS * VRAM_PITCH,
"vram dump is %d B, expected %d"
% (len(vram), SCREEN_ROWS * VRAM_PITCH)):
return 1
matched = 0
bad = []
for y in range(SCREEN_ROWS):
r = y // stretch
want = payload[r * ROW_BYTES:(r + 1) * ROW_BYTES]
got = vram[y * VRAM_PITCH:y * VRAM_PITCH + ROW_BYTES]
if got == want:
matched += 1
else:
bad.append((y, r, want, got))
for y, r, want, got in bad[:8]:
print("vidstream_check: screen row %d (source row %d): "
"expected %s, got %s"
% (y, r, want[:16].hex(), got[:16].hex()))
print("vidstream_check: %d/%d screen rows match" % (matched, SCREEN_ROWS))
check(matched == SCREEN_ROWS,
"%d screen rows do not carry their source row"
% (SCREEN_ROWS - matched))
# --- the payload's own record ---------------------------------------
rx_ok = status.get("RX_OK")
rx_bad = status.get("RX_BAD")
os_touch = status.get("OS_TOUCH")
canary = status.get("SENTINEL")
check(rx_ok == 1, "RX_OK=%r, expected 1 accepted frame" % (rx_ok,))
check(rx_bad == 0, "RX_BAD=%r, expected 0 rejected frames" % (rx_bad,))
check(os_touch == 0,
"OS_TOUCH=%r: something else wrote to the sentinel corner of VRAM"
% (os_touch,))
# A missing canary (a payload that never armed it, or a status file from
# an older payload) must fail loudly: 0 was the firmware's RAM pattern, so
# "not written" and "correct" would otherwise look alike.
check(canary == CANARY,
"canary word is %r, expected 0x%04X (SENT_VALUE in vidstream.c)"
% (canary, CANARY))
print("vidstream_check: RX_OK=%r RX_BAD=%r OS_TOUCH=%r canary=%s"
% (rx_ok, rx_bad, os_touch,
("0x%04X" % canary) if isinstance(canary, int) else repr(canary)))
if failures:
print("vidstream_check: FAIL (%d checks)" % failures)
return 1
print("vidstream_check: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())