forked from arti/vt1200
feat(tiles): 32x32 PIXBLT B,XY tile blit and full-frame paint
This commit is contained in:
1 parent
0b726fda3a
commit
2fa232ba56
6 files changed
+1003
-28
No files matched your search
@@ -8,6 +8,7 @@
|
||||
# VT1200_MODE=stream harness/mame/run-vidstream.sh # the status back-channel
|
||||
# VT1200_MODE=ring harness/mame/run-vidstream.sh # the RX ring's structure
|
||||
# VT1200_MODE=dict harness/mame/run-vidstream.sh # dictionary upload + kinds
|
||||
# VT1200_MODE=tiles harness/mame/run-vidstream.sh # the 32x32 tile blit
|
||||
# STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback
|
||||
#
|
||||
# Mode rx delivers one valid frame and one corrupted frame through the payload's
|
||||
@@ -42,6 +43,15 @@
|
||||
# compared with what was sent. The verdict is in the Lua; the evidence is
|
||||
# $LOGDIR/dict-status.txt.
|
||||
#
|
||||
# Mode tiles asserts the 32x32 tile blit: a 32 KB dictionary of row-varying
|
||||
# entries is uploaded and read back, then one TILES_FULL frame (1,280 indices,
|
||||
# a different dictionary entry at every corner) is delivered and painted. The
|
||||
# Lua's verdict is the dispatch and the record; the PIXEL verdict is
|
||||
# harness/mame/tile_check.py, which renders the screen the indices and the
|
||||
# dictionary require and compares it with the VRAM dump. A wrong SPTCH in
|
||||
# vram_tile.asm is the negative control: the frame is accepted and the pixels
|
||||
# are wrong, which only the checker can see.
|
||||
#
|
||||
# The run uses VT1200_SKIP_LEVEL1=1: the payload's heap is only initialised in
|
||||
# the OS phase. In the diagnostics menu the free counter (0x1E8BA0) and the
|
||||
# free-list head (0x1E8AC0) both read 0, so the malloc success path cannot be
|
||||
@@ -101,6 +111,10 @@ fi
|
||||
if [ "$MODE" = "dict" ]; then
|
||||
rm -f "$LOGDIR/dict-status.txt"
|
||||
fi
|
||||
if [ "$MODE" = "tiles" ]; then
|
||||
rm -f "$LOGDIR/vram-tiles.bin" "$LOGDIR/dict-tiles.bin" \
|
||||
"$LOGDIR/indices-tiles.bin" "$LOGDIR/tiles-status.txt"
|
||||
fi
|
||||
# Absolute: the Lua runs with MAME's cwd, so a relative default only resolves
|
||||
# when the caller happens to be at the repo root.
|
||||
VID_PAYLOAD="$ROOT/harness/udpstream/build/vidstream.bin"
|
||||
@@ -163,4 +177,23 @@ if [ "$MODE" = "stream" ]; then
|
||||
exit "$STATUS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# The tile picture is a separate, independent reading of the geometry
|
||||
# (tile_check.py renders the indices through the dictionary); a dispatch pass
|
||||
# with the wrong picture is a failure. This is the check the SPTCH negative
|
||||
# control is run against.
|
||||
if [ "$MODE" = "tiles" ]; then
|
||||
set +e
|
||||
python3 "$HERE/tile_check.py" \
|
||||
--vram "$LOGDIR/vram-tiles.bin" \
|
||||
--dict "$LOGDIR/dict-tiles.bin" \
|
||||
--indices "$LOGDIR/indices-tiles.bin" \
|
||||
--status "$LOGDIR/tiles-status.txt"
|
||||
STATUS=$?
|
||||
set -e
|
||||
if [ "$STATUS" -ne 0 ]; then
|
||||
echo "run-vidstream.sh: tile check failed (log $LOG)" >&2
|
||||
exit "$STATUS"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
Executable
+300
@@ -0,0 +1,300 @@
|
||||
#!/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())
|
||||
+382
-14
@@ -69,6 +69,18 @@
|
||||
-- premature "ready" fails at the chunk that caused it. Verdict and evidence:
|
||||
-- the printed checks and $DUMP_DIR/dict-status.txt.
|
||||
--
|
||||
-- Mode "tiles" (Task 4) is the tile blit's pixel check. It uploads a 32 KB
|
||||
-- dictionary whose every 128-byte entry is a distinct, row-varying pattern,
|
||||
-- verifies the payload's copy byte for byte, then delivers ONE TILES_FULL frame
|
||||
-- whose 1,280 index bytes name a different dictionary entry at every corner and
|
||||
-- a varied entry everywhere else. It waits for the payload to accept and paint
|
||||
-- it, then dumps the screen, the dictionary and the indices. The comparison is
|
||||
-- harness/mame/tile_check.py's job: this Lua does not render pixels, so a blit
|
||||
-- that paints the wrong tile, the wrong row or the wrong colour cannot be
|
||||
-- blessed here. The negative control is a deliberately wrong SPTCH in
|
||||
-- vram_tile.asm: the frame is then accepted (RX_OK moves) and the checker must
|
||||
-- FAIL on the picture.
|
||||
--
|
||||
-- PLAYING THE CHIP. Ruling R11: harness/mame/vt1200.cpp never creates a
|
||||
-- device_network_interface, so the emulated LANCE is not attached to anything
|
||||
-- and no packet can reach it. The harness therefore does what am79c90.cpp's
|
||||
@@ -88,11 +100,11 @@
|
||||
-- harness/udpstream/frame_parse.h.
|
||||
--
|
||||
-- Env:
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream, ring or dict
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream, ring, dict or tiles
|
||||
-- VID_AT load/entry frame (default 2400, the OS phase)
|
||||
-- VID_END inspection/exit frame (default VID_AT + 60)
|
||||
-- VID_DUMP_DIR where draw, stream, ring and dict modes write their dumps
|
||||
-- (default /tmp/opencode)
|
||||
-- VID_DUMP_DIR where draw, stream, ring, dict and tiles modes write their
|
||||
-- dumps (default /tmp/opencode)
|
||||
-- VID_SENTINEL_CLOBBER=1 draw mode only: overwrite the VRAM canary word
|
||||
-- before the frame is delivered, so the payload must count
|
||||
-- an OS touch. The negative control for OS_TOUCH.
|
||||
@@ -107,6 +119,8 @@
|
||||
-- transition (default 300)
|
||||
-- VID_DICT_WAIT dict mode only: emulated frames to wait for each dictionary
|
||||
-- chunk to be consumed (default 300)
|
||||
-- VID_TILE_WAIT tiles mode only: emulated frames to wait for a dictionary
|
||||
-- chunk or the tile frame to be consumed (default 300)
|
||||
-- VID_PAYLOAD raw .text image (default harness/udpstream/build/vidstream.bin)
|
||||
-- VID_ORIGIN payload load (poke) bit address (default 0x018C0000)
|
||||
-- VID_ENTRY_PTR _start bit address from vidstream.env (default VID_ORIGIN)
|
||||
@@ -142,6 +156,11 @@ elseif MODE == "ring" and not os.getenv("VID_END") then
|
||||
elseif MODE == "dict" and not os.getenv("VID_END") then
|
||||
-- Five phases, each with its own bounded wait (VID_DICT_WAIT, 300).
|
||||
DEFAULT_END = AT + 5 * tonumber(os.getenv("VID_DICT_WAIT") or "300") + 120
|
||||
elseif MODE == "tiles" and not os.getenv("VID_END") then
|
||||
-- One wait for the ring, 23 for the dictionary chunks, one for the tile
|
||||
-- frame, each bounded (VID_TILE_WAIT, 300); the first timeout dumps and
|
||||
-- exits, so this is only the catastrophic-stall backstop.
|
||||
DEFAULT_END = AT + 26 * tonumber(os.getenv("VID_TILE_WAIT") or "300") + 120
|
||||
end
|
||||
local STOP_AT = tonumber(os.getenv("VID_END") or tostring(DEFAULT_END))
|
||||
local ORIGIN = tonumber(os.getenv("VID_ORIGIN") or "0x018C0000")
|
||||
@@ -272,7 +291,8 @@ local VRAM_ROW_BYTES = 256
|
||||
local DUMP_DIR = os.getenv("VID_DUMP_DIR") or "/tmp/opencode"
|
||||
|
||||
if MODE ~= "alloc" and MODE ~= "rx" and MODE ~= "draw"
|
||||
and MODE ~= "stream" and MODE ~= "ring" and MODE ~= "dict" then
|
||||
and MODE ~= "stream" and MODE ~= "ring" and MODE ~= "dict"
|
||||
and MODE ~= "tiles" then
|
||||
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
|
||||
machine:exit()
|
||||
return
|
||||
@@ -585,12 +605,20 @@ local DRAW_WAIT = tonumber(os.getenv("VID_DRAW_WAIT") or "300")
|
||||
-- run, so a failed run cannot be checked against the previous run's screen.
|
||||
local DRAW_FILES = { "vram-draw.bin", "frame-draw.bin", "draw-status.txt" }
|
||||
local STREAM_FILES = { "stream-status.bin", "stream-status.txt" }
|
||||
local TILE_FILES = { "vram-tiles.bin", "dict-tiles.bin", "indices-tiles.bin",
|
||||
"tiles-status.txt" }
|
||||
|
||||
local function clear_dump_files()
|
||||
if MODE == "dict" then
|
||||
os.remove(DUMP_DIR .. "/dict-status.txt")
|
||||
return
|
||||
end
|
||||
if MODE == "tiles" then
|
||||
for _, name in ipairs(TILE_FILES) do
|
||||
os.remove(DUMP_DIR .. "/" .. name)
|
||||
end
|
||||
return
|
||||
end
|
||||
if MODE ~= "draw" and MODE ~= "stream" then
|
||||
return
|
||||
end
|
||||
@@ -1216,6 +1244,11 @@ local DICT_SIZE = 32768 -- 256 entries x 128 B
|
||||
local DICT_CHUNK_BODY = 1448 -- tiledict.MAX_BODY
|
||||
local DICT_OFF = 2 * SLOT_BYTES -- 0xC000: after the two slots
|
||||
|
||||
-- A TILES_FULL frame is one dictionary index per tile, row-major: 40 across x
|
||||
-- 32 down = 1,280 bytes (tilecost.TILES_PER_SCREEN). Tasks 3 and 4 both build
|
||||
-- one, so the constant lives here.
|
||||
local TILES_PER_FRAME = 1280
|
||||
|
||||
local DICT_WAIT = tonumber(os.getenv("VID_DICT_WAIT") or "300")
|
||||
local dict_phase = 0
|
||||
local dict_phase_at = nil
|
||||
@@ -1423,9 +1456,9 @@ local function dict_step()
|
||||
local _, pos0 = ring_state()
|
||||
dict_pos = pos0
|
||||
a = { f = make_frame(KIND_TILES_FULL, 0x0061, 0, 0,
|
||||
DRAW_ROWS * RX_ROW_BYTES,
|
||||
TILES_PER_FRAME,
|
||||
function(i) return (i * 5 + 1) & 0xFF end),
|
||||
n = 16 + DRAW_ROWS * RX_ROW_BYTES }
|
||||
n = 16 + TILES_PER_FRAME }
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted the pre-dictionary TILES_FULL frame")
|
||||
dict_dump(true)
|
||||
@@ -1535,13 +1568,14 @@ local function dict_step()
|
||||
dict_check(inspect_pool(),
|
||||
"the startup allocation is still the one under test")
|
||||
-- A tile frame after the dictionary is complete: Task 4 paints
|
||||
-- it. This tree has no tile blit, so it is reported rather than
|
||||
-- asserted -- the assertion above (before DICT_READY) is Task
|
||||
-- 3's; the paint is Task 4's.
|
||||
-- it (1,280 indices, every one 0x5A = entry 90). The paint is
|
||||
-- reported rather than asserted here -- the assertion above
|
||||
-- (before DICT_READY) is Task 3's, and tiles mode owns the
|
||||
-- pixel verdict.
|
||||
a = { f = make_frame(KIND_TILES_FULL, 0x0062, 0, 0,
|
||||
DRAW_ROWS * RX_ROW_BYTES,
|
||||
TILES_PER_FRAME,
|
||||
function(i) return 0x5A end),
|
||||
n = 16 + DRAW_ROWS * RX_ROW_BYTES,
|
||||
n = 16 + TILES_PER_FRAME,
|
||||
label = nil }
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted the post-dictionary TILES_FULL frame")
|
||||
@@ -1576,9 +1610,9 @@ local function dict_step()
|
||||
and prog:read_u32(DICT_BYTES) == DICT_SIZE,
|
||||
"a TILES_FULL after DICT_READY leaves the dictionary record alone")
|
||||
dict_lines[#dict_lines + 1] = string.format(
|
||||
"INFO post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints this frame; this tree has no tile blit)",
|
||||
"INFO post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints it; the pixels are tiles mode's check)",
|
||||
prog:read_u32(TILES_DROP), prog:read_u32(RX_OK))
|
||||
print(string.format("VID: dict: post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints it)",
|
||||
print(string.format("VID: dict: post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints it; pixels are tiles mode's check)",
|
||||
prog:read_u32(TILES_DROP), prog:read_u32(RX_OK)))
|
||||
dict_dump(false)
|
||||
return true
|
||||
@@ -1586,6 +1620,326 @@ local function dict_step()
|
||||
return false
|
||||
end
|
||||
|
||||
-- --- Task 4: the tile blit --------------------------------------------------
|
||||
--
|
||||
-- Mode "tiles" is the pixel check for the 32x32 tile blit. It plays the sender
|
||||
-- with tiledict.py's wire format -- the same 23 KIND_DICT_CHUNK frames the dict
|
||||
-- mode uses -- then one KIND_TILES_FULL (2) frame whose 1,280 payload bytes are
|
||||
-- one dictionary index per tile, row-major (40 across, 32 down).
|
||||
--
|
||||
-- The dictionary is generated here rather than borrowed from dict mode, and the
|
||||
-- generator is built to be hard to fool:
|
||||
--
|
||||
-- tile_byte(entry, row, col) = (entry*37 + row*11 + col*3 + 5) & 0xFF
|
||||
--
|
||||
-- so every 128-byte entry differs from every other entry, and within an entry
|
||||
-- every row differs from every other row. The second property is what makes a
|
||||
-- wrong SPTCH a visible failure: with SPTCH = 0 every row would read row 0 and
|
||||
-- the picture would repeat the first four bytes 32 times. tile_check.py
|
||||
-- refuses to score the run unless it can see both properties in the files, so a
|
||||
-- degenerate dictionary cannot hide a broken blitter.
|
||||
--
|
||||
-- The index frame is `(tile*7 + 3) & 0xFF`: a different entry nearly everywhere,
|
||||
-- and four distinct entries in the four corners, so a blit that always reads the
|
||||
-- same dictionary entry, or that swaps tx and ty, changes the picture.
|
||||
--
|
||||
-- The dictionary the payload holds is read back from POOL_PTR + 2 * SLOT_BYTES
|
||||
-- (the plan's placement) and compared with what was sent BEFORE the tile frame
|
||||
-- goes out, so an upload mismatch is reported as DICT_MATCHED=0 and the pixel
|
||||
-- comparison is never blamed for it.
|
||||
--
|
||||
-- Files written for tile_check.py: vram-tiles.bin, dict-tiles.bin (the bytes
|
||||
-- that were uploaded; whether the payload's copy matched is DICT_MATCHED in the
|
||||
-- status file, and tile_check.py refuses to score the run unless it did),
|
||||
-- indices-tiles.bin and tiles-status.txt.
|
||||
|
||||
-- The mode's own state and helpers live in a closure: Lua 5.3 allows only 200
|
||||
-- locals in the main chunk of a file this size, and this mode's constants,
|
||||
-- counters and helpers are thirty of them. One table is the interface.
|
||||
local tiles = (function()
|
||||
local TILE_PIXELS = 32
|
||||
local TILE_DICT_SIZE = 32768
|
||||
local TILE_WAIT = tonumber(os.getenv("VID_TILE_WAIT") or "300")
|
||||
|
||||
local function tile_byte(entry, row, col)
|
||||
return (entry * 37 + row * 11 + col * 3 + 5) & 0xFF
|
||||
end
|
||||
|
||||
-- The 32 KB dictionary as a string: 256 entries of 32 rows of four bytes,
|
||||
-- row-major, MSB-first within each byte -- exactly tilecost.py's layout.
|
||||
local function tile_dictionary()
|
||||
local out = {}
|
||||
for e = 0, 255 do
|
||||
local entry = {}
|
||||
for r = 0, TILE_PIXELS - 1 do
|
||||
for c = 0, 3 do
|
||||
entry[#entry + 1] = string.char(tile_byte(e, r, c))
|
||||
end
|
||||
end
|
||||
out[#out + 1] = table.concat(entry)
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
local TILE_DICT = tile_dictionary()
|
||||
|
||||
local function tile_index(i)
|
||||
return (i * 7 + 3) & 0xFF
|
||||
end
|
||||
|
||||
local TILE_INDICES = (function()
|
||||
local out = {}
|
||||
for i = 0, TILES_PER_FRAME - 1 do
|
||||
out[#out + 1] = string.char(tile_index(i))
|
||||
end
|
||||
return table.concat(out)
|
||||
end)()
|
||||
|
||||
local tile_phase = 0
|
||||
local tile_phase_at = nil
|
||||
local tile_fail = 0
|
||||
local tile_lines = {}
|
||||
local tile_vram0 = nil
|
||||
local tile_pos = 0 -- RX_RING_POS expected once the last frame settles
|
||||
local tile_last = nil -- the chunk awaiting its settle
|
||||
local tile_at = 0
|
||||
local tile_dict_matched = 0
|
||||
|
||||
local function tile_chunk_frame(off, body)
|
||||
local f = make_frame(KIND_DICT_CHUNK, 0, 0, 0, 2 + body, function(k)
|
||||
if k == 0 then return off & 0xFF end
|
||||
if k == 1 then return (off >> 8) & 0xFF end
|
||||
return TILE_DICT:byte(off + k - 1)
|
||||
end)
|
||||
return f, 16 + 2 + body
|
||||
end
|
||||
|
||||
local function tile_chunk(i)
|
||||
local off = i * DICT_CHUNK_BODY
|
||||
local n = TILE_DICT_SIZE - off
|
||||
|
||||
if n > DICT_CHUNK_BODY then
|
||||
n = DICT_CHUNK_BODY
|
||||
end
|
||||
return tile_chunk_frame(off, n)
|
||||
end
|
||||
|
||||
local function tile_check(cond, what)
|
||||
if cond then
|
||||
tile_lines[#tile_lines + 1] = "PASS " .. what
|
||||
print("VID: tiles PASS: " .. what)
|
||||
else
|
||||
tile_lines[#tile_lines + 1] = "FAIL " .. what
|
||||
print("VID: tiles FAIL: " .. what)
|
||||
tile_fail = tile_fail + 1
|
||||
end
|
||||
return cond
|
||||
end
|
||||
|
||||
-- The payload has finished with the last injection: rx_poll() publishes
|
||||
-- RX_RING_POS and RX_RING_ACC together after the dispatch, so POS naming the
|
||||
-- descriptor after the last injected one AND no frame in progress is a
|
||||
-- consistent "done" (the same signal dict mode uses; RX_DGRAM is bumped before
|
||||
-- the datagram is processed).
|
||||
local function tile_settled()
|
||||
return prog:read_u32(RX_RING_POS) == tile_pos
|
||||
and prog:read_u32(RX_RING_ACC) == 0
|
||||
end
|
||||
|
||||
local function tile_take(f, n)
|
||||
local ndg = inject_frame(f, n, DRAW_CHUNK)
|
||||
|
||||
if ndg == nil then
|
||||
return false
|
||||
end
|
||||
tile_pos = (tile_pos + ndg) % RX_DESC
|
||||
return true
|
||||
end
|
||||
|
||||
-- Compare the payload's 32 KB at POOL_PTR + 2 * SLOT_BYTES with what was sent.
|
||||
local function tile_verify_dict()
|
||||
local base = prog:read_u32(POOL_PTR)
|
||||
local bit, bad, first, i, v, want
|
||||
|
||||
if base == 0 then
|
||||
return false, -1
|
||||
end
|
||||
bit = base + DICT_OFF * 8
|
||||
bad = 0
|
||||
first = -1
|
||||
for i = 0, TILE_DICT_SIZE // 2 - 1 do
|
||||
v = prog:read_u16(bit + i * 16)
|
||||
want = TILE_DICT:byte(i * 2 + 1) | (TILE_DICT:byte(i * 2 + 2) << 8)
|
||||
if v ~= want then
|
||||
bad = bad + 1
|
||||
if first < 0 then
|
||||
first = i * 2
|
||||
end
|
||||
end
|
||||
end
|
||||
return bad == 0, first
|
||||
end
|
||||
|
||||
local function tile_dump(timed_out)
|
||||
local vram = read_vram()
|
||||
local ok1 = write_file(DUMP_DIR .. "/vram-tiles.bin", vram)
|
||||
local ok2 = write_file(DUMP_DIR .. "/dict-tiles.bin", TILE_DICT)
|
||||
local ok3 = write_file(DUMP_DIR .. "/indices-tiles.bin", TILE_INDICES)
|
||||
local lines = {
|
||||
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
|
||||
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
|
||||
string.format("TILES_DROP=%d", prog:read_u32(TILES_DROP)),
|
||||
string.format("DICT_READY=%d", prog:read_u32(DICT_READY)),
|
||||
string.format("DICT_BYTES=%d", prog:read_u32(DICT_BYTES)),
|
||||
string.format("DICT_BAD=%d", prog:read_u32(DICT_BAD)),
|
||||
string.format("DICT_MATCHED=%d", tile_dict_matched),
|
||||
string.format("OS_TOUCH=%d", prog:read_u32(OS_TOUCH)),
|
||||
string.format("SENTINEL=%d", prog:read_u16(VRAM_BASE
|
||||
+ (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280)),
|
||||
string.format("POOL_PTR=0x%X", prog:read_u32(POOL_PTR)),
|
||||
string.format("DICT_AT=0x%X", prog:read_u32(POOL_PTR) + DICT_OFF * 8),
|
||||
string.format("VRAM_ROW0_BEFORE=%s", tile_vram0 and hex_bytes(tile_vram0) or "-"),
|
||||
string.format("VRAM_ROW0_AFTER=%s", hex_bytes(read_vram_head(32))),
|
||||
string.format("TILE_CHECKS_FAILED=%d", tile_fail),
|
||||
string.format("TIMEOUT=%d", timed_out and 1 or 0),
|
||||
}
|
||||
for _, l in ipairs(tile_lines) do
|
||||
lines[#lines + 1] = "CHECK " .. l
|
||||
end
|
||||
local okf = write_file(DUMP_DIR .. "/tiles-status.txt",
|
||||
table.concat(lines, "\n") .. "\n")
|
||||
local ok = ok1 and ok2 and ok3 and okf and not timed_out and tile_fail == 0
|
||||
print(string.format("VID: tiles: %d checks failed; RX_OK=%d TILES_DROP=%d DICT_READY=%d DICT_MATCHED=%d",
|
||||
tile_fail, prog:read_u32(RX_OK), prog:read_u32(TILES_DROP),
|
||||
prog:read_u32(DICT_READY), tile_dict_matched))
|
||||
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
|
||||
machine:exit()
|
||||
end
|
||||
|
||||
-- One call per emulated frame. Returns true when the run is over.
|
||||
local function tile_step()
|
||||
local f, n, okv, first
|
||||
|
||||
if tile_phase == 0 then
|
||||
if tile_phase_at == nil then
|
||||
tile_phase_at = frames
|
||||
end
|
||||
if not ring_ready() then
|
||||
if frames >= tile_phase_at + TILE_WAIT then
|
||||
tile_check(false, string.format(
|
||||
"the payload published an RX ring (RX_RING_BASE=%s, ST_INIT=%04X)",
|
||||
hex(prog:read_u32(RX_RING_BASE)), prog:read_u32(ST_INIT)))
|
||||
tile_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
tile_vram0 = read_vram_head(32)
|
||||
local _, pos0 = ring_state()
|
||||
tile_pos = pos0
|
||||
tile_check(prog:read_u32(RX_OK) == 0,
|
||||
string.format("no frame had reached the screen before the dictionary (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
tile_phase = 1
|
||||
tile_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if tile_phase == 1 then
|
||||
if tile_last ~= nil then
|
||||
if not tile_settled() then
|
||||
if frames >= tile_phase_at + TILE_WAIT then
|
||||
tile_check(false, string.format(
|
||||
"the payload consumed dictionary chunk %d (DICT_BYTES=%d RX_RING_POS=%d expected %d)",
|
||||
tile_at - 1, prog:read_u32(DICT_BYTES),
|
||||
prog:read_u32(RX_RING_POS), tile_pos))
|
||||
tile_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
tile_check(prog:read_u32(DICT_BYTES) == tile_last.bytes,
|
||||
string.format("dictionary chunk %d: DICT_BYTES=%d (expected %d)",
|
||||
tile_at - 1, prog:read_u32(DICT_BYTES), tile_last.bytes))
|
||||
tile_check(prog:read_u32(DICT_BAD) == 0,
|
||||
string.format("dictionary chunk %d: DICT_BAD=%d (expected 0)",
|
||||
tile_at - 1, prog:read_u32(DICT_BAD)))
|
||||
tile_last = nil
|
||||
end
|
||||
if tile_at >= 23 then
|
||||
tile_check(prog:read_u32(DICT_READY) == 1,
|
||||
string.format("DICT_READY=1 after the upload (DICT_READY=%d DICT_BYTES=%d)",
|
||||
prog:read_u32(DICT_READY), prog:read_u32(DICT_BYTES)))
|
||||
okv, first = tile_verify_dict()
|
||||
tile_dict_matched = okv and 1 or 0
|
||||
tile_check(okv, string.format(
|
||||
"the 32 KB at POOL_PTR + %d is the dictionary sent (first bad byte %d)",
|
||||
DICT_OFF, first))
|
||||
-- The tile frame goes out only after the dictionary readback: a
|
||||
-- wrong upload must not be reported as a wrong picture.
|
||||
f = make_frame(KIND_TILES_FULL, 0x0071, 0, 0, TILES_PER_FRAME,
|
||||
function(i) return tile_index(i) end)
|
||||
n = 16 + TILES_PER_FRAME
|
||||
if not tile_take(f, n) then
|
||||
tile_check(false, "the ring accepted the TILES_FULL frame")
|
||||
tile_dump(true)
|
||||
return true
|
||||
end
|
||||
tile_phase = 2
|
||||
tile_phase_at = frames
|
||||
return false
|
||||
end
|
||||
f, n = tile_chunk(tile_at)
|
||||
if not tile_take(f, n) then
|
||||
tile_check(false, string.format("the ring accepted dictionary chunk %d", tile_at))
|
||||
tile_dump(true)
|
||||
return true
|
||||
end
|
||||
tile_at = tile_at + 1
|
||||
tile_last = { bytes = math.min(tile_at * DICT_CHUNK_BODY, TILE_DICT_SIZE) }
|
||||
tile_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if tile_phase == 2 then
|
||||
if not tile_settled() then
|
||||
if frames >= tile_phase_at + TILE_WAIT then
|
||||
tile_check(false, string.format(
|
||||
"the payload consumed the TILES_FULL frame (RX_OK=%d TILES_DROP=%d RX_RING_POS=%d expected %d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP),
|
||||
prog:read_u32(RX_RING_POS), tile_pos))
|
||||
tile_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
tile_check(prog:read_u32(RX_OK) == 1,
|
||||
string.format("the TILES_FULL frame reached the screen (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
tile_check(prog:read_u32(TILES_DROP) == 0,
|
||||
string.format("TILES_DROP did not move (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
tile_check(prog:read_u32(RX_BAD) == 0,
|
||||
string.format("no frame was refused (RX_BAD=%d)",
|
||||
prog:read_u32(RX_BAD)))
|
||||
tile_check(prog:read_u32(DICT_READY) == 1
|
||||
and prog:read_u32(DICT_BYTES) == TILE_DICT_SIZE,
|
||||
"the tile frame left the dictionary record alone")
|
||||
tile_dump(false)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- The one interface into this mode: step() returns true when the run is over,
|
||||
-- phase() names the phase for the stall message, dump() writes the evidence.
|
||||
return {
|
||||
step = tile_step,
|
||||
phase = function() return tile_phase end,
|
||||
dump = function(timed_out) tile_dump(timed_out) end,
|
||||
}
|
||||
end)()
|
||||
|
||||
--
|
||||
-- `timed_out` matters for what the checker is allowed to say: if the payload
|
||||
-- never accepted the frame, the screen shows the previous picture and calling
|
||||
@@ -1870,6 +2224,13 @@ emu.register_frame_done(function()
|
||||
end
|
||||
end
|
||||
|
||||
-- Mode tiles: the 32x32 blit's pixel check (tiles.step()).
|
||||
if entered and MODE == "tiles" then
|
||||
if tiles.step() then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw mode: inject once, then wait for the payload to accept the frame.
|
||||
-- Two separate observations, because they answer different questions:
|
||||
--
|
||||
@@ -1980,8 +2341,15 @@ emu.register_frame_done(function()
|
||||
return
|
||||
end
|
||||
|
||||
if MODE == "tiles" and frames >= STOP_AT then
|
||||
print(string.format("VID: tiles mode did not finish (phase %d) by frame %d",
|
||||
tiles.phase(), frames))
|
||||
tiles.dump(true)
|
||||
return
|
||||
end
|
||||
|
||||
if frames >= STOP_AT and MODE ~= "stream" and MODE ~= "ring"
|
||||
and MODE ~= "dict"
|
||||
and MODE ~= "dict" and MODE ~= "tiles"
|
||||
and not (MODE == "draw" and draw_at ~= nil) then
|
||||
if not entered then
|
||||
print("VID: payload never entered -> FAIL")
|
||||
|
||||
@@ -60,6 +60,16 @@ if [ -f "$DIR/vram_b.asm" ]; then
|
||||
VRAMB_LST=VRAMB.LST
|
||||
fi
|
||||
|
||||
# vram_tile.asm is plan 2 Task 4's 32x32 tile PIXBLT (one dictionary entry per
|
||||
# call). Same rule: link it when it exists, so a tree without it still builds.
|
||||
TILE_OBJ=
|
||||
TILE_LST=
|
||||
if [ -f "$DIR/vram_tile.asm" ]; then
|
||||
cp "$DIR/vram_tile.asm" "$WORK/VRAMTILE.ASM"
|
||||
TILE_OBJ=VRAMTILE.obj
|
||||
TILE_LST=VRAMTILE.LST
|
||||
fi
|
||||
|
||||
# The allocator request size is a compile-time constant (the 1987 compiler has
|
||||
# no -D we rely on). One generated header keeps the default in one place.
|
||||
printf '#define POOL_BYTES %s\n' "$POOL_BYTES" > "$WORK/POOLCFG.H"
|
||||
@@ -74,6 +84,7 @@ printf '#define POOL_BYTES %s\n' "$POOL_BYTES" > "$WORK/POOLCFG.H"
|
||||
printf '%s\n' 'ENTRY.obj'
|
||||
printf '%s\n' 'VRAM.obj'
|
||||
[ -z "$VRAMB_OBJ" ] || printf '%s\n' "$VRAMB_OBJ"
|
||||
[ -z "$TILE_OBJ" ] || printf '%s\n' "$TILE_OBJ"
|
||||
printf '%s\n' '-l RTS.LIB'
|
||||
printf '%s\n' 'MEMORY'
|
||||
printf '%s\n' '{'
|
||||
@@ -95,7 +106,8 @@ rm -f "$WORK"/VIDSTRM.OBJ "$WORK"/VIDSTRM.OUT "$WORK"/VIDSTRM.LST \
|
||||
"$WORK"/VIDSTRM.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST \
|
||||
"$WORK"/FRMPSR.OBJ "$WORK"/FRMPSR.LST "$WORK"/FRMPSR.ASM \
|
||||
"$WORK"/FRMPSR.CPP "$WORK"/FRMPSR.IF \
|
||||
"$WORK"/VRAM.OBJ "$WORK"/VRAM.LST "$WORK"/VRAMB.OBJ "$WORK"/VRAMB.LST
|
||||
"$WORK"/VRAM.OBJ "$WORK"/VRAM.LST "$WORK"/VRAMB.OBJ "$WORK"/VRAMB.LST \
|
||||
"$WORK"/VRAMTILE.OBJ "$WORK"/VRAMTILE.LST
|
||||
|
||||
# The toolchain runs from a batch file, not from one -c per command. Once the
|
||||
# build grew to a second translation unit the -c list stopped completing:
|
||||
@@ -117,6 +129,7 @@ rm -f "$WORK"/VIDSTRM.OBJ "$WORK"/VIDSTRM.OUT "$WORK"/VIDSTRM.LST \
|
||||
printf 'gspa ENTRY ENTRY.obj ENTRY.lst -l\r\n'
|
||||
printf 'gspa VRAM VRAM.obj VRAM.lst -l\r\n'
|
||||
[ -z "$VRAMB_OBJ" ] || printf 'gspa VRAMB VRAMB.obj VRAMB.lst -l\r\n'
|
||||
[ -z "$TILE_OBJ" ] || printf 'gspa VRAMTILE VRAMTILE.obj VRAMTILE.lst -l\r\n'
|
||||
printf 'gsplnk VIDSTRM.cmd\r\n'
|
||||
printf 'exit\r\n'
|
||||
} > "$WORK/BUILD.BAT"
|
||||
@@ -133,12 +146,12 @@ run_dosbox
|
||||
if [ ! -f "$WORK/VIDSTRM.OUT" ]; then
|
||||
echo "build-stream.sh: GSPLNK produced no VIDSTRM.OUT" >&2
|
||||
sed 's/\r$//' "$WORK/VIDSTRM.MAP" 2>/dev/null | tail -20 >&2 || true
|
||||
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
|
||||
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST $TILE_LST; do
|
||||
sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
|
||||
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST $TILE_LST; do
|
||||
if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then
|
||||
echo "build-stream.sh: $f diagnostics:" >&2
|
||||
sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2
|
||||
@@ -155,6 +168,9 @@ sed 's/\r$//' "$WORK/VRAM.LST" > "$OUT/vram.lst"
|
||||
if [ -n "$VRAMB_LST" ]; then
|
||||
sed 's/\r$//' "$WORK/VRAMB.LST" > "$OUT/vramb.lst"
|
||||
fi
|
||||
if [ -n "$TILE_LST" ]; then
|
||||
sed 's/\r$//' "$WORK/VRAMTILE.LST" > "$OUT/vramtile.lst"
|
||||
fi
|
||||
|
||||
# Guard: the marker block (MARK in the source) must sit clear of .text, or the
|
||||
# payload overwrites its own code with marker writes (an illegal-opcode crash).
|
||||
|
||||
+156
-11
@@ -17,7 +17,9 @@
|
||||
* are assembled into a 32 KB block after the two slots, and the dispatcher
|
||||
* learns the TILES_FULL/TILES_DELTA kinds so a frame that arrives before the
|
||||
* dictionary is complete is refused and counted instead of being mistaken for
|
||||
* a strip.
|
||||
* a strip. Plan 2's Task 4 paints TILES_FULL: one PIXBLT B,XY per 32x32 tile
|
||||
* from the dictionary (vram_tile.asm), 1,280 of them per frame inside one
|
||||
* dint/eint pair. TILES_DELTA is still counted, not painted (Task 5).
|
||||
*
|
||||
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
|
||||
*
|
||||
@@ -152,6 +154,21 @@
|
||||
#define DICT_ENTRY 128 /* one 32x32 1-bpp tile, in bytes */
|
||||
#define DICT_SIZE (DICT_ENTRY * 256) /* 256 entries = 32 KB */
|
||||
|
||||
/*
|
||||
* Task 4: a TILES_FULL frame is 1,280 dictionary indices, row-major, one byte
|
||||
* per tile: 40 tiles across the 1280-pixel screen, 32 tile bands down the 1024
|
||||
* rows. Tile (tx, ty) is dictionary entry `index[ty * 40 + tx]`, painted by
|
||||
* one PIXBLT B,XY from dict_base + index*1024 bits (128 bytes = 1024 bits;
|
||||
* ruling R4 corrected the spec's old index << 7) with SPTCH = 32 bits,
|
||||
* DYDX = (32, 32) and destination XY (ty*32, tx*32) -- vram_tile.asm owns the
|
||||
* operand setup, tile_blit() below is its only caller.
|
||||
*/
|
||||
#define TILE_PIXEL 32 /* a tile is 32x32 pixels */
|
||||
#define TILE_COLS 40 /* 1280 px / 32 */
|
||||
#define TILE_ROWS 32 /* 1024 px / 32 */
|
||||
#define TILES_SCREEN (TILE_COLS * TILE_ROWS) /* 1,280 indices a frame */
|
||||
#define TILE_SHIFT 10 /* 128 bytes = 1024 bits an entry */
|
||||
|
||||
/*
|
||||
* The arithmetic check the brief asks for, as a compile-time assertion.
|
||||
*
|
||||
@@ -263,7 +280,13 @@ unsigned int stack_cushion[1024];
|
||||
unsigned int vram_pattern;
|
||||
unsigned int blit_src, blit_sptch, blit_daddr, blit_dydx;
|
||||
|
||||
/* Task 4's tile primitive (vram_tile.asm): the source bit address and the
|
||||
* destination XY word. DYDX and SPTCH are the constant 32x32 tile's, and are
|
||||
* set in the assembly; only these two vary per call. */
|
||||
unsigned int tile_src, tile_daddr;
|
||||
|
||||
extern void vram_blit_b_(); /* vram_b.asm: PIXBLT B,XY, SPTCH = 0 */
|
||||
extern void tile_blit_(); /* vram_tile.asm: PIXBLT B,XY, 32x32 tile */
|
||||
extern void vid_dint_(); /* vram_b.asm: DINT */
|
||||
extern void vid_eint_(); /* vram_b.asm: EINT */
|
||||
|
||||
@@ -371,6 +394,26 @@ unsigned int dydx;
|
||||
vram_blit_b_();
|
||||
}
|
||||
|
||||
/*
|
||||
* tile_blit(src, tx, ty)
|
||||
*
|
||||
* One PIXBLT B,XY of a 32x32 dictionary tile. `src` is the entry's bit
|
||||
* address (dict_base + index * TILE_BITS), (tx, ty) the tile's position on the
|
||||
* 40x32 grid. The assembly entry takes no arguments (ruling R13); the two
|
||||
* values are published where vram_tile.asm reads them, and the destination XY
|
||||
* is formed here because that is the only per-call arithmetic: x = tx*32,
|
||||
* y = ty*32, packed as (y << 16) | x.
|
||||
*/
|
||||
static void tile_blit(src, tx, ty)
|
||||
unsigned int src;
|
||||
unsigned int tx;
|
||||
unsigned int ty;
|
||||
{
|
||||
tile_src = src;
|
||||
tile_daddr = ((ty * TILE_PIXEL) << 16) | (tx * TILE_PIXEL);
|
||||
tile_blit_();
|
||||
}
|
||||
|
||||
/*
|
||||
* paint(h) -- draw one accepted strip frame from the fragment table.
|
||||
*
|
||||
@@ -455,6 +498,90 @@ fp_header *h;
|
||||
vid_eint_();
|
||||
}
|
||||
|
||||
/*
|
||||
* paint_tiles(h) -- draw one accepted TILES_FULL frame, 1,280 tiles. 1 when
|
||||
* the picture was painted, 0 when the frame could not be decoded (the caller
|
||||
* counts the refusal in TILES_DROP).
|
||||
*
|
||||
* The payload is 1,280 dictionary indices in a single byte, row-major: index
|
||||
* i is tile (tx, ty) with ty = i / 40, tx = i % 40. The nested loop avoids
|
||||
* the division, which the TMS34010 has no instruction for. Each tile is one
|
||||
* PIXBLT B,XY of its 128-byte dictionary entry from dict_base + index*1024
|
||||
* (the shift by TILE_SHIFT, because a shift by a named constant is emitted
|
||||
* without a multiply).
|
||||
*
|
||||
* A frame is refused, and nothing is painted, when:
|
||||
* - the dictionary is not complete (DICT_READY 0 or no base): there is
|
||||
* nothing to decode with -- this is the case Task 3's dict mode pins;
|
||||
* - the payload is not exactly 1,280 bytes: the TILES_FULL format is one
|
||||
* byte per tile and nothing else, so a short frame does not name every
|
||||
* tile and a longer one is not this kind's wire form. Painting a partial
|
||||
* screen over a good one would be worse than leaving the old picture, and
|
||||
* the arbitrary-length tolerance would be a rule nobody wrote down.
|
||||
* A gap in the fragment table in the middle of the burst is impossible for a
|
||||
* completed frame (fp_parse() accepted it and rx_acc is the accumulated length,
|
||||
* so every payload byte is covered); the check is there so such a frame stops
|
||||
* drawing instead of painting from a stale pointer, and it is deliberately not
|
||||
* counted as a drop because the frame was not refused up front. That branch
|
||||
* leaves a partial picture, exactly as paint()'s truncated case does.
|
||||
*
|
||||
* The whole burst runs with interrupts masked (spec 8.9): 1,280 blits take
|
||||
* long enough that a resident OS repaint between two tiles would tear the
|
||||
* picture. The loop is kept small on purpose -- it is the code the
|
||||
* instruction cache holds while the blits run: no call per tile except the
|
||||
* primitive, no division, no per-tile function.
|
||||
*
|
||||
* The OS-repaint canary is the same one paint() uses; a frame that sees it
|
||||
* disturbed counts OS_TOUCH and puts it back.
|
||||
*/
|
||||
static int paint_tiles(h)
|
||||
fp_header *h;
|
||||
{
|
||||
unsigned int tx, ty, k, pos, idx, sent;
|
||||
|
||||
if (!dict_ready || dict_base == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (h->payload_len != TILES_SCREEN) {
|
||||
return 0;
|
||||
}
|
||||
if (rx_nfrag == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
sent = (unsigned int)*(unsigned short *)SENT_ADDR;
|
||||
if (sent != SENT_VALUE) {
|
||||
*(unsigned int *)OS_TOUCH = *(unsigned int *)OS_TOUCH + 1;
|
||||
*(unsigned short *)SENT_ADDR = SENT_VALUE;
|
||||
}
|
||||
|
||||
vid_dint_();
|
||||
k = 0;
|
||||
pos = h->payload_off; /* the frame's first index byte */
|
||||
for (ty = 0; ty < TILE_ROWS; ty++) {
|
||||
for (tx = 0; tx < TILE_COLS; tx++) {
|
||||
while (k < rx_nfrag
|
||||
&& pos >= rx_frag_off[k] + rx_frag_len[k]) {
|
||||
k = k + 1;
|
||||
}
|
||||
if (k >= rx_nfrag) {
|
||||
vid_eint_();
|
||||
return 1; /* truncated: stop drawing */
|
||||
}
|
||||
if (pos < rx_frag_off[k]) {
|
||||
vid_eint_();
|
||||
return 1; /* a gap: never paint across it */
|
||||
}
|
||||
idx = (unsigned int)*(unsigned char *)
|
||||
(rx_frag_ptr[k] + (pos - rx_frag_off[k]) * 8);
|
||||
tile_blit(dict_base + (idx << TILE_SHIFT), tx, ty);
|
||||
pos = pos + 1;
|
||||
}
|
||||
}
|
||||
vid_eint_();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* frag_copy(dst, pos, n) -- copy n frame bytes starting at frame offset `pos`
|
||||
* into memory at bit address `dst`. 1 on success, 0 when the fragment table
|
||||
@@ -838,17 +965,35 @@ unsigned int i;
|
||||
/* A chunk is not a picture: it is counted by DICT_BYTES
|
||||
* and DICT_BAD, never by RX_OK. */
|
||||
dict_chunk(&h);
|
||||
} else if (h.kind == FP_KIND_TILES_FULL
|
||||
|| h.kind == FP_KIND_TILES_DELTA) {
|
||||
} else if (h.kind == FP_KIND_TILES_FULL) {
|
||||
/*
|
||||
* Task 4 turns these into pixels. This tree has no tile
|
||||
* blit, and the point of the branch is that such a frame
|
||||
* is never silently forgotten: before DICT_READY there is
|
||||
* nothing to decode with, and after it this tree still
|
||||
* has no paint, so both are counted and neither touches
|
||||
* VRAM. (A frame that arrived before the dictionary and
|
||||
* was dropped cannot leave a half-painted screen: the
|
||||
* branch draws nothing at all.)
|
||||
* Task 4: a full frame of indices is painted from the
|
||||
* dictionary, one PIXBLT per tile. RX_OK and the record
|
||||
* cells describe only frames that reached the screen, so
|
||||
* they move after the paint, not before it. A frame the
|
||||
* dictionary cannot decode (no DICT_READY, short payload)
|
||||
* is counted in TILES_DROP and touches nothing -- the
|
||||
* same rule Task 3's pre-dictionary branch had.
|
||||
*/
|
||||
if (paint_tiles(&h)) {
|
||||
*(unsigned int *)RX_LAST_LEN = h.payload_len;
|
||||
*(unsigned int *)RX_SLOT = slot;
|
||||
*(unsigned int *)RX_OK =
|
||||
*(unsigned int *)RX_OK + 1;
|
||||
} else {
|
||||
tiles_drop = tiles_drop + 1;
|
||||
*(unsigned int *)TILES_DROP = tiles_drop;
|
||||
}
|
||||
} else if (h.kind == FP_KIND_TILES_DELTA) {
|
||||
/*
|
||||
* Task 5 decodes deltas. This tree has no delta
|
||||
* decoder, and the point of the branch is that such a
|
||||
* frame is never silently forgotten: it is counted in
|
||||
* TILES_DROP and does not touch VRAM, so a delta frame
|
||||
* cannot leave half a picture on the screen. (A delta
|
||||
* frame that arrives before the dictionary is dropped
|
||||
* here too; the dictionary is fixed for the clip, so
|
||||
* DICT_READY is the same condition either way.)
|
||||
*/
|
||||
tiles_drop = tiles_drop + 1;
|
||||
*(unsigned int *)TILES_DROP = tiles_drop;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
; vram_tile.asm -- PIXBLT B,XY: one 32x32 1-bpp dictionary tile to the screen.
|
||||
;
|
||||
; Task 4's tile primitive. A dictionary entry is 32 rows of FOUR bytes (32 px
|
||||
; is two 16-bit words) = 128 bytes = 1024 bits, row-major, MSB first within each
|
||||
; byte -- tilecost.py's layout. PIXBLT's B source form reads packed 1-bit
|
||||
; pixels from a linear bit address and writes an XY destination, so one tile is
|
||||
;
|
||||
; SADDR = dict_base + index * 1024 (128 bytes a tile: index << 10)
|
||||
; SPTCH = 32 (4 bytes a source row)
|
||||
; DADDR = (ty*32 << 16) | (tx*32) (XY destination)
|
||||
; DYDX = (32 << 16) | 32 (32 rows, 32 pixels)
|
||||
;
|
||||
; DESCRIPTION-register assignments for PIXBLT B,XY (TMS34010 User's Guide,
|
||||
; instruction reference; vram.asm already uses the same values for L,XY and
|
||||
; vram_b.asm for B,XY):
|
||||
; SADDR = B0 source bit address (1 bpp, packed)
|
||||
; SPTCH = B1 source pitch in bits (4 bytes = 32 bits a row)
|
||||
; DADDR = B2 destination XY: (y << 16) | x
|
||||
; DPTCH = B3 destination pitch in bits (0x800 = 256-byte screen row)
|
||||
; OFFSET = B4 VRAM base bit address
|
||||
; DYDX = B7 dimensions in XY form: (rows << 16) | columns
|
||||
;
|
||||
; COLOR0 = B8 pixel value for a clear source bit (0 = black)
|
||||
; COLOR1 = B9 pixel value for a set source bit (0xFFFF = white)
|
||||
;
|
||||
; B8/B9 are NOT I/O registers: they are implied operands of every graphics
|
||||
; instruction, and a PIXBLT B expands a packed 1-bpp source into pixel values.
|
||||
; They are set explicitly because the resident OS leaves them arbitrary --
|
||||
; measured in the strip work: the first draw run painted every row black
|
||||
; because nothing had set B9. vram_b.asm's comment records the same finding.
|
||||
;
|
||||
; PSIZE (0C0000150h) 1 one bit per destination pixel
|
||||
; CONVSP (0C0000130h) 14h 0x800-bit pitch code (1 << (~0x14 & 0x1F) = 2048)
|
||||
; CONVDP (0C0000140h) 14h same, for the XY destination
|
||||
; PMASK (0C0000160h) 0 every plane writable
|
||||
; CONTROL (0C00000B0h) raster op 0 (replace), CD/RM/RR preserved
|
||||
;
|
||||
; Entry point / calling form (ruling R13). The TMS34010 C compiler's argument
|
||||
; frame is not decoded by any hand-written assembly in this repository; the
|
||||
; published-word form of vram.asm and vram_b.asm is used instead. This symbol
|
||||
; takes NO arguments: the two values a caller can change live in .bss words
|
||||
;
|
||||
; tile_src the source tile's bit address
|
||||
; tile_daddr the destination XY word
|
||||
;
|
||||
; and the C wrapper tile_blit(src, tx, ty) is the only entry point. B7 (DYDX)
|
||||
; is the constant 32x32 this primitive exists for. All inputs are reloaded on
|
||||
; every call: PIXBLT leaves SADDR/DADDR and the B registers it advanced in a
|
||||
; half-consumed state, so a caller that "saved" them would save that.
|
||||
;
|
||||
; Revert note: this file is new in Task 4 and nothing else calls it. To
|
||||
; revert, delete it, the VRAMTILE entries in build-stream.sh and the
|
||||
; tile_blit/paint_tiles code in vidstream.c; the strip path (vram_b.asm) does
|
||||
; not reference any symbol defined here and is unaffected. The disassembly
|
||||
; this copies is vram_b.asm's, and its value table is quoted above from
|
||||
; vram_b.asm rather than re-derived.
|
||||
;
|
||||
; This file does NOT define _vid_dint_/_vid_eint_: they live in vram_b.asm and
|
||||
; both objects are linked, so a second definition would be a duplicate symbol.
|
||||
|
||||
.ref _tile_src
|
||||
.ref _tile_daddr
|
||||
.text
|
||||
.globl _tile_blit_
|
||||
_tile_blit_:
|
||||
setf 32, 0, 1
|
||||
move b0, -*sp, 1
|
||||
move b1, -*sp, 1
|
||||
move b2, -*sp, 1
|
||||
move b3, -*sp, 1
|
||||
move b4, -*sp, 1
|
||||
move b7, -*sp, 1
|
||||
move b8, -*sp, 1
|
||||
move b9, -*sp, 1
|
||||
|
||||
move @_tile_src, b0, 1 ; SADDR: dictionary entry, index << 10 bits
|
||||
movi 020h, b1 ; SPTCH = 32 bits = four bytes a tile row
|
||||
move @_tile_daddr, b2, 1 ; DADDR (XY): (ty*32 << 16) | tx*32
|
||||
movi 800h, b3 ; DPTCH = 2048 bits = 256-byte screen row
|
||||
movi 04000000h, b4 ; OFFSET = VRAM base
|
||||
movi 0200020h, b7 ; DYDX = (32 rows << 16) | 32 pixels
|
||||
clr b8 ; COLOR0 = 0 (black)
|
||||
movi 0FFFFh, b9 ; COLOR1 = all planes set (white)
|
||||
|
||||
; --- pixel format ------------------------------------------------------
|
||||
movi 1, a1
|
||||
move a1, @0C0000150h, 0 ; PSIZE = 1
|
||||
movi 14h, a1
|
||||
move a1, @0C0000130h, 0 ; CONVSP = pitch code for 0x800 bits
|
||||
move a1, @0C0000140h, 0 ; CONVDP
|
||||
clr a1
|
||||
move a1, @0C0000160h, 0 ; PMASK = 0 (all planes writable)
|
||||
move a1, @0C0000170h, 0 ; upper half, for forward compatibility
|
||||
|
||||
; --- graphics operation: replace, no transparency, no window ----------
|
||||
move @0C00000B0h, a1, 0 ; CONTROL
|
||||
andi 0801Fh, a1 ; keep CD (15) and RM/RR (2-4) only
|
||||
move a1, @0C00000B0h, 0
|
||||
move @0C00000B0h, a1, 0 ; read back: I/O writes are pipelined
|
||||
|
||||
pixblt b, xy
|
||||
|
||||
move *sp+, b9, 1
|
||||
move *sp+, b8, 1
|
||||
move *sp+, b7, 1
|
||||
move *sp+, b4, 1
|
||||
move *sp+, b3, 1
|
||||
move *sp+, b2, 1
|
||||
move *sp+, b1, 1
|
||||
move *sp+, b0, 1
|
||||
rets
|
||||
|
||||
.end
|
||||
Reference in new issue
Block a user