feat(tiles): 32x32 PIXBLT B,XY tile blit and full-frame paint

This commit is contained in:
opencode.energia.arti.ee committed 2026-10-31 02:20:50 +00:00
1 parent 0b726fda3a
commit 2fa232ba56
6 files changed
+1003 -28

No files matched your search

+33
View File
@@ -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
+300
View File
@@ -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
View File
@@ -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")
+19 -3
View File
@@ -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
View File
@@ -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;
+113
View File
@@ -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