Files
vt1200/harness/mame/tile_check.py
T

301 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""tile_check.py -- did the 32x32 tile blit put every dictionary entry on screen?
Task 4's pixel check. The Lua side (harness/mame/vidstream.lua,
VT1200_MODE=tiles) uploads a 32 KB dictionary through the payload's own RX
ring, sends one TILES_FULL frame (1,280 index bytes) and then dumps four files:
vram-tiles.bin the screen as memory: 1024 rows of 256 bytes from bit
0x04000000
dict-tiles.bin the 32,768 dictionary bytes that were uploaded (the Lua
read them back from POOL_PTR + 2 * SLOT_BYTES before sending the frame, so
an upload mismatch fails the mode instead of being blamed on the blit)
indices-tiles.bin the 1,280 index bytes of the TILES_FULL frame
tiles-status.txt key=value lines (RX_OK, TILES_DROP, DICT_READY, SENTINEL,
OS_TOUCH, TIMEOUT, ...)
This script renders the screen the indices and the dictionary say must be
there and compares it with the dump, tile by tile:
for each tile (tx, ty): screen rows ty*32 .. ty*32+31,
bytes tx*4 .. tx*4+3 of each 256-byte row
<- dictionary[index(ty, tx)] row-major, 32 rows of four bytes.
That is an independent reading of the geometry (tilecost.py's TILE_* constants
and the design's section 8.3), not a copy of the payload's loop. A source tile
is 32 px = two 16-bit words = four bytes a row, 128 bytes in all; the screen
row pitch is 2048 bits = 256 bytes, the real unit's (harness/c-hello/README.md).
Only the first 160 bytes of a screen row are pixels the blitter writes (40
tiles x 32 px = 1280 px); the remaining 96 bytes are outside every tile and are
not compared -- they belong to the resident OS and no tile blit touches them.
A timed-out run writes TIMEOUT=1 and is reported as a timeout, not as a screen
full of wrong pixels: blaming the blit for a frame the payload never accepted
would be the wrong verdict (ruling R15). The VRAM canary is asserted too, so a
payload that stopped arming it cannot pass as a correct screen.
The check is deliberately sensitive to a wrong SPTCH (source pitch), which is
the negative control Task 4 asks for: every dictionary entry has 32 distinct
rows, and this script refuses to call a dictionary usable unless that holds, so
a blit that re-reads row 0 for every row cannot match.
usage:
tile_check.py [--vram PATH] [--dict PATH] [--indices PATH] [--status PATH]
Exit status 0 only if every check passes.
"""
import argparse
import os
import sys
HEADER_SIZE = 16
TILE_PIXELS = 32 # px per tile side, and rows per tile
TILE_ROW_BYTES = 4 # 32 px, 1 bpp
TILE_BYTES = TILE_PIXELS * TILE_ROW_BYTES # 128
TILE_COLS = 40 # 1280 px / 32 px
TILE_ROWS = 32 # 1024 px / 32 px
TILES_PER_SCREEN = TILE_COLS * TILE_ROWS # 1,280
DICT_ENTRIES = 256
DICT_BYTES = DICT_ENTRIES * TILE_BYTES # 32,768
ACTIVE_ROW_BYTES = TILE_COLS * TILE_ROW_BYTES # 160 bytes = 1280 pixels
VRAM_PITCH = 256 # bytes per screen row (2048 bits)
SCREEN_ROWS = 1024
CANARY = 0xA5C3 # SENT_VALUE in vidstream.c, at row 1023 / column 1280
# The four tiles the brief calls out, plus the order this script reports them.
CORNERS = ((0, 0, "top-left"), (TILE_COLS - 1, 0, "top-right"),
(0, TILE_ROWS - 1, "bottom-left"),
(TILE_COLS - 1, TILE_ROWS - 1, "bottom-right"))
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("tile_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("tile_check: cannot read %s: %s" % (path, e))
return None
return st
def tile_dict(dictionary, entry):
return dictionary[entry * TILE_BYTES:(entry + 1) * TILE_BYTES]
def tile_index(indices, tx, ty):
return indices[ty * TILE_COLS + tx]
def rendered_row(dictionary, indices, ty, row):
"""The 160 active bytes of screen row ty*32+row, from the indices."""
out = bytearray()
for tx in range(TILE_COLS):
entry = tile_index(indices, tx, ty)
t = tile_dict(dictionary, entry)
out += t[row * TILE_ROW_BYTES:(row + 1) * TILE_ROW_BYTES]
return bytes(out)
def main():
ap = argparse.ArgumentParser()
d = os.environ.get("VID_DUMP_DIR", "/tmp/opencode")
ap.add_argument("--vram", default=os.path.join(d, "vram-tiles.bin"))
ap.add_argument("--dict", default=os.path.join(d, "dict-tiles.bin"))
ap.add_argument("--indices", default=os.path.join(d, "indices-tiles.bin"))
ap.add_argument("--status", default=os.path.join(d, "tiles-status.txt"))
args = ap.parse_args()
vram = read_file(args.vram)
dictionary = read_file(args.dict)
indices = read_file(args.indices)
status = read_status(args.status)
if vram is None or dictionary is None or indices is None or status is None:
return 2
print("tile_check: vram %s (%d B) dict %s (%d B) indices %s (%d B)"
% (args.vram, len(vram), args.dict, len(dictionary),
args.indices, len(indices)))
# --- 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, "
"TILES_DROP=%r: the payload did not finish with the tile frame)"
% (status.get("RX_OK"), status.get("TILES_DROP")))
print("tile_check: FAIL (timeout)")
return 1
# --- the payload's own record -------------------------------------------
check(status.get("RX_OK") == 1,
"RX_OK=%r, expected exactly 1 accepted TILES_FULL frame"
% (status.get("RX_OK"),))
check(status.get("RX_BAD") == 0,
"RX_BAD=%r, expected 0 rejected frames" % (status.get("RX_BAD"),))
check(status.get("TILES_DROP") == 0,
"TILES_DROP=%r: a tile frame with a complete dictionary must paint"
% (status.get("TILES_DROP"),))
check(status.get("DICT_READY") == 1,
"DICT_READY=%r, expected 1" % (status.get("DICT_READY"),))
check(status.get("DICT_BYTES") == DICT_BYTES,
"DICT_BYTES=%r, expected %d" % (status.get("DICT_BYTES"), DICT_BYTES))
check(status.get("DICT_MATCHED") == 1,
"DICT_MATCHED=%r: the dictionary the payload holds is not the one "
"that was uploaded" % (status.get("DICT_MATCHED"),))
check(status.get("OS_TOUCH") == 0,
"OS_TOUCH=%r: something else wrote the sentinel corner of VRAM"
% (status.get("OS_TOUCH"),))
check(status.get("SENTINEL") == CANARY,
"canary word is %r, expected 0x%04X (SENT_VALUE in vidstream.c)"
% (status.get("SENTINEL"), CANARY))
print("tile_check: RX_OK=%r RX_BAD=%r TILES_DROP=%r DICT_READY=%r "
"SENTINEL=%r"
% (status.get("RX_OK"), status.get("RX_BAD"),
status.get("TILES_DROP"), status.get("DICT_READY"),
status.get("SENTINEL")))
# --- the inputs have the size the geometry needs -----------------------
if not check(len(dictionary) == DICT_BYTES,
"dictionary is %d B, expected %d"
% (len(dictionary), DICT_BYTES)):
return 1
if not check(len(indices) == TILES_PER_SCREEN,
"index frame is %d B, expected %d"
% (len(indices), TILES_PER_SCREEN)):
return 1
if not check(len(vram) == SCREEN_ROWS * VRAM_PITCH,
"vram dump is %d B, expected %d"
% (len(vram), SCREEN_ROWS * VRAM_PITCH)):
return 1
# --- the test data can tell a right blit from a broken one -------------
#
# A dictionary whose entries all look alike, or whose rows are all the
# same, would let a wrong tile index or a wrong source pitch pass. The
# checks below are about the test DATA, not the payload: if they fail the
# run has nothing to say about the blitter either way.
used = sorted(set(indices))
print("tile_check: %d distinct dictionary entries used by the frame"
% len(used))
data_failures = failures
check(len(used) >= 100,
"the index frame uses %d distinct entries; a frame with few distinct "
"tiles cannot detect a wrong tile mapping" % len(used))
corner_idx = []
for tx, ty, name in CORNERS:
corner_idx.append(tile_index(indices, tx, ty))
check(len(set(corner_idx)) == len(corner_idx),
"the four corner indices are not distinct: %r" % (corner_idx,))
for entry in used:
t = tile_dict(dictionary, entry)
rows = [t[r * TILE_ROW_BYTES:(r + 1) * TILE_ROW_BYTES]
for r in range(TILE_PIXELS)]
if not check(len(set(rows)) > 1,
"dictionary entry %d has 32 identical rows: a wrong "
"SPTCH could not be detected" % entry):
break
if not check(any(b != 0 for b in t) and any(b != 0xFF for b in t),
"dictionary entry %d is a solid field" % entry):
break
if failures > data_failures:
# The test data cannot tell a right blit from a broken one; comparing
# pixels would report a mismatch that says nothing about the payload.
return 1
# --- the screen, tile by tile ------------------------------------------
matched = 0
bad = []
for ty in range(TILE_ROWS):
for tx in range(TILE_COLS):
entry = tile_index(indices, tx, ty)
t = tile_dict(dictionary, entry)
ok = True
for r in range(TILE_PIXELS):
y = ty * TILE_PIXELS + r
want = t[r * TILE_ROW_BYTES:(r + 1) * TILE_ROW_BYTES]
got = vram[y * VRAM_PITCH + tx * TILE_ROW_BYTES:
y * VRAM_PITCH + (tx + 1) * TILE_ROW_BYTES]
if got != want:
ok = False
break
if ok:
matched += 1
elif len(bad) < 8:
bad.append((tx, ty, entry, r, want, got))
for tx, ty, entry, r, want, got in bad:
print("tile_check: tile (%d,%d) entry %d, row %d: expected %s, got %s"
% (tx, ty, entry, r, want.hex(), got.hex()))
print("tile_check: %d/%d tiles match their dictionary entry"
% (matched, TILES_PER_SCREEN))
check(matched == TILES_PER_SCREEN,
"%d tiles do not carry their dictionary entry"
% (TILES_PER_SCREEN - matched))
# --- the four corners, named ------------------------------------------
for tx, ty, name in CORNERS:
entry = tile_index(indices, tx, ty)
t = tile_dict(dictionary, entry)
ok = True
for r in range(TILE_PIXELS):
y = ty * TILE_PIXELS + r
got = vram[y * VRAM_PITCH + tx * TILE_ROW_BYTES:
y * VRAM_PITCH + (tx + 1) * TILE_ROW_BYTES]
if got != t[r * TILE_ROW_BYTES:(r + 1) * TILE_ROW_BYTES]:
ok = False
break
print("tile_check: %s corner (%d,%d) entry %d: %s"
% (name, tx, ty, entry, "PASS" if ok else "FAIL"))
check(ok, "%s corner tile (%d,%d) is not dictionary entry %d"
% (name, tx, ty, entry))
# --- the full-screen row view (one comparison per row) -----------------
row_bad = 0
for ty in range(TILE_ROWS):
for row in range(TILE_PIXELS):
y = ty * TILE_PIXELS + row
want = rendered_row(dictionary, indices, ty, row)
got = vram[y * VRAM_PITCH:y * VRAM_PITCH + ACTIVE_ROW_BYTES]
if got != want:
row_bad += 1
if row_bad <= 8:
print("tile_check: screen row %d: expected %s..., got %s..."
% (y, want[:16].hex(), got[:16].hex()))
check(row_bad == 0,
"%d of %d active screen rows do not match the rendered frame"
% (row_bad, SCREEN_ROWS))
if failures:
print("tile_check: FAIL (%d checks)" % failures)
return 1
print("tile_check: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())