From 2fa232ba56c4d6a1c05928505600cf8af4dac1fd Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Sat, 31 Oct 2026 02:12:52 +0000 Subject: [PATCH] feat(tiles): 32x32 PIXBLT B,XY tile blit and full-frame paint --- harness/mame/run-vidstream.sh | 33 +++ harness/mame/tile_check.py | 300 ++++++++++++++++++++++ harness/mame/vidstream.lua | 396 ++++++++++++++++++++++++++++-- harness/udpstream/build-stream.sh | 22 +- harness/udpstream/vidstream.c | 167 ++++++++++++- harness/udpstream/vram_tile.asm | 113 +++++++++ 6 files changed, 1003 insertions(+), 28 deletions(-) create mode 100755 harness/mame/tile_check.py create mode 100644 harness/udpstream/vram_tile.asm diff --git a/harness/mame/run-vidstream.sh b/harness/mame/run-vidstream.sh index 80416c1..2af8b53 100755 --- a/harness/mame/run-vidstream.sh +++ b/harness/mame/run-vidstream.sh @@ -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 diff --git a/harness/mame/tile_check.py b/harness/mame/tile_check.py new file mode 100755 index 0000000..fdd2234 --- /dev/null +++ b/harness/mame/tile_check.py @@ -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()) diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua index 63bccf0..fa835a3 100644 --- a/harness/mame/vidstream.lua +++ b/harness/mame/vidstream.lua @@ -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") diff --git a/harness/udpstream/build-stream.sh b/harness/udpstream/build-stream.sh index eb8837d..50cdbc0 100755 --- a/harness/udpstream/build-stream.sh +++ b/harness/udpstream/build-stream.sh @@ -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). diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c index 71dee21..0f5ed6b 100644 --- a/harness/udpstream/vidstream.c +++ b/harness/udpstream/vidstream.c @@ -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; diff --git a/harness/udpstream/vram_tile.asm b/harness/udpstream/vram_tile.asm new file mode 100644 index 0000000..a08daaf --- /dev/null +++ b/harness/udpstream/vram_tile.asm @@ -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