From 45110270a7fd1fdadeaa22dd455e7d4fe707696e Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Fri, 30 Oct 2026 23:41:13 +0000 Subject: [PATCH] feat(stream): row-strip blit with SPTCH=0 vertical stretch One PIXBLT B,XY per source row paints the whole screen: DYDX names `stretch` rows and SPTCH = 0 makes each of them read the same packed source row, so the vertical stretch costs no extra CPU work. vram_b.asm carries the primitive (PSIZE=1, CONVSP/CONVDP=0x14, PMASK=0, OFFSET=0x04000000, DPTCH=0x800, raster op 0) plus DINT/EINT thunks; vidstream.c masks interrupts for the burst (spec 8.9), refuses geometry the screen cannot hold, and counts an OS repaint through a sentinel word in the unused right edge of the last screen row (OS_TOUCH, 0x01810220). MAME mode draw injects a 128-row frame (stretch 8 = 1024 screen rows, ruling R3) and dumps the screen; vidstream_check.py compares all 1024 rows against the bytes actually delivered. RED: 0/1024 rows matched. GREEN: 1024/1024, RX_OK=1, OS_TOUCH=0. Fixes a latent harness bug found on the way: `bpclear
` takes a breakpoint INDEX, so the idle-loop breakpoint was never cleared; once a burst masked interrupts long enough, its stale action fired again and re-entered main (a second malloc, counters back to 0). --- harness/mame/run-vidstream.sh | 26 +++- harness/mame/vidstream.lua | 213 ++++++++++++++++++++++++++++++-- harness/mame/vidstream_check.py | 182 +++++++++++++++++++++++++++ harness/udpstream/frame_parse.h | 9 ++ harness/udpstream/vidstream.c | 139 +++++++++++++++++++-- harness/udpstream/vram_b.asm | 119 ++++++++++++++++++ 6 files changed, 670 insertions(+), 18 deletions(-) create mode 100755 harness/mame/vidstream_check.py create mode 100644 harness/udpstream/vram_b.asm diff --git a/harness/mame/run-vidstream.sh b/harness/mame/run-vidstream.sh index 5e7d218..4fd7e7d 100755 --- a/harness/mame/run-vidstream.sh +++ b/harness/mame/run-vidstream.sh @@ -4,17 +4,24 @@ # harness/mame/run-vidstream.sh # build, then mode alloc # BENCH=0 harness/mame/run-vidstream.sh # use the existing .bin # VT1200_MODE=rx harness/mame/run-vidstream.sh # alloc + the receive path +# VT1200_MODE=draw harness/mame/run-vidstream.sh # alloc + rx + the blit # STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback # # Mode rx injects one valid frame and one corrupted frame with the debugger and # asserts RX_OK == 1 && RX_BAD == 1 (see vidstream.lua and the task-4 report). # +# Mode draw injects one 128-row strip frame (stretch 8 = 1024 screen rows), lets +# the payload blit it, and dumps the screen and the delivered frame; this script +# then runs harness/mame/vidstream_check.py, which compares every screen row +# against its source row. The pixel verdict is the checker's, not the Lua's. +# # 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 # tested there. The skip is a documented divergence (harness/mame/README.md). # -# Exit status: 0 only if vidstream.lua printed "VID: RESULT PASS". +# Exit status: 0 only if vidstream.lua printed "VID: RESULT PASS" and, in draw +# mode, vidstream_check.py passed too. set -eu HERE=$(cd "$(dirname "$0")" && pwd) @@ -57,6 +64,7 @@ set +e VT1200_SKIP_LEVEL1=1 \ VT1200_MODE="$MODE" \ VID_PAYLOAD="$VID_PAYLOAD" \ +VID_DUMP_DIR="$LOGDIR" \ VID_POOL_BYTES="$POOL_BYTES" \ VID_STATIC_PTR="$STATIC_PTR" \ VID_ENTRY_PTR="$ENTRY_PTR" \ @@ -79,4 +87,20 @@ if ! grep -q '^VID: RESULT PASS' "$LOG"; then echo "run-vidstream.sh: check failed (log $LOG)" >&2 exit 1 fi + +# The pixel comparison is a separate, independent reading of the geometry +# (vidstream_check.py); a transport pass with the wrong picture is a failure. +if [ "$MODE" = "draw" ]; then + set +e + python3 "$HERE/vidstream_check.py" \ + --vram "$LOGDIR/vram-draw.bin" \ + --frame "$LOGDIR/frame-draw.bin" \ + --status "$LOGDIR/draw-status.txt" + STATUS=$? + set -e + if [ "$STATUS" -ne 0 ]; then + echo "run-vidstream.sh: pixel check failed (log $LOG)" >&2 + exit "$STATUS" + fi +fi exit 0 diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua index 4ca1531..86bb40c 100644 --- a/harness/mame/vidstream.lua +++ b/harness/mame/vidstream.lua @@ -25,6 +25,15 @@ -- is the accepted frame's payload_len, that RX_SLOT names slot A, and that the -- payload cleared both ready flags. -- +-- Mode "draw" (Task 5) is the pixel check. It injects ONE valid 128-row strip +-- frame (stretch 8 -- exactly 1024 screen rows, ruling R3) whose rows are a +-- known pattern, waits for the payload to accept and blit it, then dumps the +-- screen (1024 rows x 256 bytes from bit 0x04000000), the frame bytes it poked, +-- and the payload's counters. The comparison is harness/mame/vidstream_check.py's +-- job: this Lua does not compare pixels, so a blit that paints the wrong rows +-- cannot be blessed here. No blit exists in the payload before Task 5, which is +-- why this mode fails on a Task 4 tree. +-- -- Why injection and not a real Ethernet frame: harness/mame/vt1200.cpp never -- creates a device_network_interface, so the emulated LANCE is not attached to -- the host at all (ruling R11). Wiring it would be a MAME change, which @@ -38,9 +47,14 @@ -- including the RX_* cells below, is in harness/udpstream/frame_parse.h. -- -- Env: --- VT1200_MODE alloc (default) or rx +-- VT1200_MODE alloc (default), rx or draw -- VID_AT load/entry frame (default 2400, the OS phase) -- VID_END inspection/exit frame (default VID_AT + 60) +-- VID_DUMP_DIR where draw mode writes vram-draw.bin / frame-draw.bin / +-- draw-status.txt (default /tmp/opencode) +-- VID_SENTINEL_CLOBBER=1 draw mode only: overwrite the VRAM canary word +-- before the frame is announced, so the payload must count +-- an OS touch. The negative control for OS_TOUCH. -- 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) @@ -92,6 +106,9 @@ local RX_LEN_A = MARK_PAGE + 0x6c * 8 local RX_READY_B = MARK_PAGE + 0x70 * 8 local RX_LEN_B = MARK_PAGE + 0x74 * 8 +-- Task 5: the OS-repaint canary (frame_parse.h, ruling R12). +local OS_TOUCH = MARK_PAGE + 0x44 * 8 + -- Must match vidstream.c. local SLOT_BYTES = 24576 @@ -104,7 +121,23 @@ local RX_ROW_BYTES = 160 local RX_PLEN = RX_ROWS * RX_ROW_BYTES local RX_FRAME = 16 + RX_PLEN -if MODE ~= "alloc" and MODE ~= "rx" then +-- Draw mode: 128 rows x 8 screen rows = 1024, the whole screen (ruling R3). +-- 20,480 B of payload + 16 B header fits the 24,576 B slot. +local DRAW_ROWS = 128 +local DRAW_STRETCH = 8 +local DRAW_PLEN = DRAW_ROWS * RX_ROW_BYTES +local DRAW_FRAME = 16 + DRAW_PLEN + +-- Screen memory: bit 0x04000000, 256-byte row pitch (the real unit's, see +-- harness/c-hello/README.md; MAME's own renderer uses a different pitch and is +-- not what this check reads). +local VRAM_BASE = 0x04000000 +local VRAM_ROW_BITS = 0x800 +local VRAM_ROWS = 1024 +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" then print("VID ERROR: mode '" .. MODE .. "' is not implemented") machine:exit() return @@ -129,7 +162,9 @@ local function crc_add(crc, bytes, first, count) end -- A complete frame, header and payload, laid out as frame.py writes it. -local function make_frame(kind, frame_no, rows, stretch, plen) +-- `fill`, when given, computes payload byte i; draw mode needs a per-row +-- pattern, the rx mode keeps the arbitrary (i*7+3) filler. +local function make_frame(kind, frame_no, rows, stretch, plen, fill) local b = {} b[0] = 0x56 -- 'V' b[1] = 0x53 -- 'S' @@ -144,7 +179,11 @@ local function make_frame(kind, frame_no, rows, stretch, plen) b[10] = 0 b[11] = 0 for i = 0, plen - 1 do - b[16 + i] = (i * 7 + 3) & 0xFF + if fill then + b[16 + i] = fill(i) + else + b[16 + i] = (i * 7 + 3) & 0xFF + end end local crc = crc_add(0xFFFF, b, 0, 12) crc = crc_add(crc, b, 16, plen) @@ -223,13 +262,31 @@ local function in_payload(pc) end local function enter_direct(reason) - dbg:command("bpclear " .. hex(ENTRY_PC)) + dbg:command("bpclear") -- clears ALL: see disarm_entry() cpu.state['PC'].value = ENTRY_PTR direct = true print(string.format("VID: frame %d: %s; entered payload directly at %s", frames, reason, hex(ENTRY_PTR))) end +-- Disarm the idle-loop breakpoint and say so. +-- +-- `bpclear
` does NOT clear by address: MAME's command takes a +-- BREAKPOINT INDEX (debugcmd.cpp, execute_bpclear -> "Invalid breakpoint +-- number"). Task 3/4 passed the address, so the breakpoint stayed armed the +-- whole run. It did not matter while the payload never yielded -- but as soon +-- as a frame burst masked interrupts long enough for the OS to reach its idle +-- loop, the stale action fired again and re-entered _start (Task 5 measured a +-- second malloc and RX_OK back at 0). This script owns the debugger session +-- and sets exactly one breakpoint, so "clear all" is the safe form. +local function disarm_entry() + dbg:command("bpclear") + dbg:command("bplist") + local last = "?" + pcall(function() last = tostring(dbg.consolelog[#dbg.consolelog]) end) + print("VID: entry breakpoint disarmed; breakpoint list: " .. last) +end + -- Fill slot A with a valid frame and slot B with the same frame carrying one -- flipped payload byte, then raise both ready flags. The payload has already -- published the slot addresses by the time this runs; a zero there means the @@ -261,6 +318,127 @@ local function inject() return true end +-- Draw mode: one valid 128-row frame into slot A. Row r is the 16-bit word +-- 0xF0F0 repeated (160 bytes of 0xF0), except every third row (r % 3 == 2), +-- which is all zero -- two row classes, so a blit that always read the same +-- source row cannot pass. The frame bytes are kept for the checker, which +-- compares against what was actually delivered rather than re-deriving it. +local draw_at = nil +local draw_bytes = nil + +local function draw_row_byte(i) + if (i // RX_ROW_BYTES) % 3 == 2 then + return 0x00 + end + return 0xF0 +end + +local function inject_draw() + local a = prog:read_u32(RX_SLOT_A) + + if a == 0 then + return false + end + + local f = make_frame(1, 0x0051, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN, + draw_row_byte) + poke(a, f, DRAW_FRAME) + draw_bytes = f + prog:write_u32(RX_LEN_A, DRAW_FRAME) + -- Negative control for the OS-repaint canary: clobber the sentinel word + -- (last screen row, column 1280) just before the payload is told a frame + -- is ready. The first blit must then count an OS touch. Off by default; + -- see VID_SENTINEL_CLOBBER in the env list. + if os.getenv("VID_SENTINEL_CLOBBER") then + prog:write_u16(VRAM_BASE + (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280, + 0x1234) + print(string.format("VID: frame %d: sentinel clobbered to 1234 (canary control)", + frames)) + end + prog:write_u32(RX_READY_A, 1) + print(string.format("VID: frame %d: injected slot A (%s, %d B, valid, %d rows x %d stretch)", + frames, hex(a), DRAW_FRAME, DRAW_ROWS, DRAW_STRETCH)) + local okb, badb = verify(a, f, DRAW_FRAME) + print(string.format("VID: frame %d: draw frame readback %s (%d bad)", + frames, okb and "OK" or "FAILED", badb)) + return true +end + +-- Read the screen as memory: 1024 rows x 256 bytes from bit 0x04000000. +-- read_u16 is used rather than read_u32 so the byte order (a TMS34010 word is +-- little-endian on the wire, low byte first) is explicit rather than assumed. +local function read_vram() + local rows = {} + + for y = 0, VRAM_ROWS - 1 do + local off = VRAM_BASE + y * VRAM_ROW_BITS + local chars = {} + for w = 0, VRAM_ROW_BYTES // 2 - 1 do + local v = prog:read_u16(off + w * 16) + chars[#chars + 1] = string.char(v & 0xFF, (v >> 8) & 0xFF) + end + rows[#rows + 1] = table.concat(chars) + end + return table.concat(rows) +end + +local function write_file(path, s) + local f, err = io.open(path, "wb") + if not f then + print("VID ERROR: cannot write " .. path .. ": " .. tostring(err)) + return false + end + f:write(s) + f:close() + return true +end + +-- Dump the screen and the payload's record for vidstream_check.py, then end +-- the run. The pass/fail for pixels belongs to the checker; here we only +-- report what the payload itself says (one accepted frame, no rejects, no +-- repaint by the OS). +local function dump_draw() + local vram = read_vram() + local chars = {} + for i = 0, DRAW_FRAME - 1 do + chars[#chars + 1] = string.char(draw_bytes[i]) + end + local frame = table.concat(chars) + local okv = write_file(DUMP_DIR .. "/vram-draw.bin", vram) + local okf = write_file(DUMP_DIR .. "/frame-draw.bin", frame) + local rx_ok = prog:read_u32(RX_OK) + local rx_bad = prog:read_u32(RX_BAD) + local os_touch = prog:read_u32(OS_TOUCH) + local ready = prog:read_u32(RX_READY_A) + local alive = prog:read_u32(VID_ALIVE) + local ptr = prog:read_u32(POOL_PTR) + local slota = prog:read_u32(RX_SLOT_A) + local pcv = cpu.state['PC'].value + local sent = prog:read_u16(VRAM_BASE + (VRAM_ROWS - 1) * VRAM_ROW_BITS + + 1280) + local ok = okv and okf + + print(string.format("VID: heartbeat %d, PC=%s (%s)", alive, hex(pcv), + in_payload(pcv) and "inside the payload" or "OUTSIDE the payload")) + print(string.format("VID: POOL_PTR=%s RX_SLOT_A=%s (an unchanged pointer means " + .. "the payload did not re-enter main)", hex(ptr), hex(slota))) + + write_file(DUMP_DIR .. "/draw-status.txt", string.format( + "RX_OK=%d\nRX_BAD=%d\nOS_TOUCH=%d\nRX_READY_A=%d\nSENTINEL=%d\n", + rx_ok, rx_bad, os_touch, ready, sent)) + + print(string.format("VID: frame %d: screen dumped to %s/vram-draw.bin (%d B), frame to %s/frame-draw.bin (%d B)", + frames, DUMP_DIR, #vram, DUMP_DIR, DRAW_FRAME)) + print(string.format("VID: RX_OK=%d RX_BAD=%d OS_TOUCH=%d RX_READY_A=%d SENTINEL=%04X", + rx_ok, rx_bad, os_touch, ready, sent)) + + if rx_ok ~= 1 or rx_bad ~= 0 or ready ~= 0 or os_touch ~= 0 then + ok = false + end + print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL") + machine:exit() +end + local function inspect_pool() local ptr = prog:read_u32(POOL_PTR) local freeb = prog:read_u32(POOL_FREEB) @@ -418,7 +596,7 @@ emu.register_frame_done(function() if loaded and not entered and in_payload(pcv) then entered = true - dbg:command("bpclear " .. hex(ENTRY_PC)) + disarm_entry() alive0 = prog:read_u32(VID_ALIVE) print(string.format("VID: payload entered by frame %d (PC=%s, heartbeat=%d)", frames, hex(pcv), alive0)) @@ -439,11 +617,32 @@ emu.register_frame_done(function() end end + -- Draw mode: inject once, then wait for the payload to consume the slot + -- (it clears RX_READY_A) and give it a few frames of slack before reading + -- the screen back. The timeout is generous on purpose: fp_parse() runs a + -- BITWISE CRC, and a 20 KB frame costs it ~40 emulated frames (measured), + -- so an unlucky 10-frame wait would read the screen before the blit. + -- A payload that never takes the frame still gets its screen dumped, so + -- the checker reports the truth instead of the run hanging. + if entered and MODE == "draw" and draw_at == nil then + if inject_draw() then + draw_at = frames + end + end + + if MODE == "draw" and draw_at ~= nil and frames > draw_at + 1 then + if (prog:read_u32(RX_READY_A) == 0 and frames >= draw_at + 5) + or frames >= draw_at + 300 then + dump_draw() + return + end + end + if entered and frames % 10 == 0 and frames < STOP_AT then alive_last = prog:read_u32(VID_ALIVE) end - if frames >= STOP_AT then + if frames >= STOP_AT and not (MODE == "draw" and draw_at ~= nil) then if not entered then print("VID: payload never entered -> FAIL") print("VID: RESULT FAIL") diff --git a/harness/mame/vidstream_check.py b/harness/mame/vidstream_check.py new file mode 100755 index 0000000..ca3e588 --- /dev/null +++ b/harness/mame/vidstream_check.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""vidstream_check.py -- did the row-strip blit put every source row on screen? + +Task 5's pixel check. The Lua side (harness/mame/vidstream.lua, VT1200_MODE=draw) +injects ONE strip frame into the payload's slot, waits for the payload to accept +and blit it, then dumps three files: + + vram-draw.bin the screen as memory: 1024 rows of 256 bytes from bit 0x04000000 + frame-draw.bin the bytes actually poked into the slot (16-byte header + payload) + draw-status.txt key=value lines (RX_OK, RX_BAD, OS_TOUCH, ...) + +This script is the independent reading of the geometry, not a copy of the code +under test. The frame is 1280 pixels wide, 1 bpp, 160 bytes a row; the screen +row pitch is 2048 bits = 256 bytes (the real unit's, measured in +harness/c-hello/README.md -- MAME's own renderer uses a different pitch and is +NOT what this test checks). A source row r stretched by S must appear, byte for +byte, on screen rows r*S .. r*S+S-1. + +The comparison uses the bytes that were actually delivered to the slot, not a +re-implementation of the pattern: whatever the sender wrote is what must land on +screen. The pattern itself is checked too, because a frame whose rows are all +identical would let a blit that always read row 0 pass. + +usage: + vidstream_check.py [--vram PATH] [--frame PATH] [--status PATH] + [--rows 128] [--stretch 8] + +Exit status 0 only if every check passes. +""" +import argparse +import os +import sys + +HEADER_SIZE = 16 +ROW_BYTES = 160 # 1280 pixels, 1 bpp +VRAM_PITCH = 256 # bytes per screen row (2048 bits) +SCREEN_ROWS = 1024 +ZERO_ROW_MOD = 3 # rows with r % 3 == 2 are the all-zero rows +ZERO_ROW_REM = 2 +FILL_BYTE = 0xF0 # rows that are not zero are 0xF0F0 words -> 0xF0 bytes + +failures = 0 + + +def check(cond, what): + global failures + if not cond: + failures += 1 + print("FAIL: " + what) + return cond + + +def read_file(path): + """The whole file, or None with a diagnosable line instead of a traceback.""" + try: + with open(path, "rb") as f: + return f.read() + except OSError as e: + print("vidstream_check: cannot read %s: %s" % (path, e)) + return None + + +def read_status(path): + st = {} + try: + with open(path, "r", encoding="ascii", errors="replace") as f: + for line in f: + if "=" in line: + k, v = line.strip().split("=", 1) + try: + st[k] = int(v, 0) + except ValueError: + st[k] = v + except OSError as e: + print("vidstream_check: cannot read %s: %s" % (path, e)) + return None + return st + + +def main(): + ap = argparse.ArgumentParser() + d = os.environ.get("VID_DUMP_DIR", "/tmp/opencode") + ap.add_argument("--vram", default=os.path.join(d, "vram-draw.bin")) + ap.add_argument("--frame", default=os.path.join(d, "frame-draw.bin")) + ap.add_argument("--status", default=os.path.join(d, "draw-status.txt")) + ap.add_argument("--rows", type=int, default=128) + ap.add_argument("--stretch", type=int, default=8) + args = ap.parse_args() + + vram = read_file(args.vram) + frame = read_file(args.frame) + status = read_status(args.status) + if vram is None or frame is None or status is None: + return 2 + + print("vidstream_check: vram %s (%d B) frame %s (%d B) status %s" + % (args.vram, len(vram), args.frame, len(frame), args.status)) + + # --- the frame that was delivered ------------------------------------ + rows, stretch = args.rows, args.stretch + if not check(stretch > 0, "stretch must be positive, got %d" % stretch): + return 1 + if not check(len(frame) == HEADER_SIZE + rows * ROW_BYTES, + "frame is %d bytes, expected 16 + %d*%d = %d" + % (len(frame), rows, ROW_BYTES, HEADER_SIZE + rows * ROW_BYTES)): + return 1 + if not check(frame[0:2] == b"VS" and frame[2] == 1 and frame[3] == 1, + "delivered frame is not a version-1 KIND_STRIP frame"): + return 1 + check(frame[8] == rows, "header rows=%d, expected %d" % (frame[8], rows)) + check(frame[9] == stretch, "header stretch=%d, expected %d" + % (frame[9], stretch)) + plen = frame[6] | (frame[7] << 8) + check(plen == rows * ROW_BYTES, + "header payload_len=%d, expected %d" % (plen, rows * ROW_BYTES)) + check(rows * stretch == SCREEN_ROWS, + "rows*stretch = %d, the screen has %d rows" % (rows * stretch, + SCREEN_ROWS)) + + payload = frame[HEADER_SIZE:] + + # --- the pattern is distinctive per row class ------------------------- + zero_rows = 0 + for r in range(rows): + row = payload[r * ROW_BYTES:(r + 1) * ROW_BYTES] + if r % ZERO_ROW_MOD == ZERO_ROW_REM: + zero_rows += 1 + check(row == bytes(ROW_BYTES), + "source row %d should be all zero" % r) + else: + check(row == bytes([FILL_BYTE]) * ROW_BYTES, + "source row %d should be 0x%02X bytes" % (r, FILL_BYTE)) + print("vidstream_check: source %d rows x %d B, stretch %d, %d zero rows" + % (rows, ROW_BYTES, stretch, zero_rows)) + + # --- the screen ------------------------------------------------------ + if not check(len(vram) == SCREEN_ROWS * VRAM_PITCH, + "vram dump is %d B, expected %d" + % (len(vram), SCREEN_ROWS * VRAM_PITCH)): + return 1 + + matched = 0 + bad = [] + for y in range(SCREEN_ROWS): + r = y // stretch + want = payload[r * ROW_BYTES:(r + 1) * ROW_BYTES] + got = vram[y * VRAM_PITCH:y * VRAM_PITCH + ROW_BYTES] + if got == want: + matched += 1 + else: + bad.append((y, r, want, got)) + for y, r, want, got in bad[:8]: + print("vidstream_check: screen row %d (source row %d): " + "expected %s, got %s" + % (y, r, want[:16].hex(), got[:16].hex())) + + print("vidstream_check: %d/%d screen rows match" % (matched, SCREEN_ROWS)) + check(matched == SCREEN_ROWS, + "%d screen rows do not carry their source row" + % (SCREEN_ROWS - matched)) + + # --- the payload's own record --------------------------------------- + rx_ok = status.get("RX_OK") + rx_bad = status.get("RX_BAD") + os_touch = status.get("OS_TOUCH") + check(rx_ok == 1, "RX_OK=%r, expected 1 accepted frame" % (rx_ok,)) + check(rx_bad == 0, "RX_BAD=%r, expected 0 rejected frames" % (rx_bad,)) + check(os_touch == 0, + "OS_TOUCH=%r: something else wrote to the sentinel corner of VRAM" + % (os_touch,)) + print("vidstream_check: RX_OK=%r RX_BAD=%r OS_TOUCH=%r" + % (rx_ok, rx_bad, os_touch)) + + if failures: + print("vidstream_check: FAIL (%d checks)" % failures) + return 1 + print("vidstream_check: PASS") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/harness/udpstream/frame_parse.h b/harness/udpstream/frame_parse.h index 663a389..3945a44 100644 --- a/harness/udpstream/frame_parse.h +++ b/harness/udpstream/frame_parse.h @@ -42,6 +42,13 @@ * RX_BAD +0xA0 0x018101A0 frames rejected by fp_parse * RX_LAST_LEN +0xC0 0x018101C0 payload_len of the last accepted * RX_SLOT +0xE0 0x018101E0 slot of the last accepted (0=A) + * OS_TOUCH +0x220 0x01810220 times the VRAM canary was not ours + * + * OS_TOUCH is Task 5's OS-repaint canary (design spec 8.9): the blit leaves one + * 16-bit sentinel word in the unused right edge of the last screen row, and + * OS_TOUCH counts how often that word was not what the payload left there. It + * is at cell 0x44 -- free between the RX_* counters and the injection cells + * below (ruling R12). * * Debugger injection interface (Task 4; the harness plays the receive path): * @@ -97,6 +104,8 @@ #define RX_LAST_LEN MARK_CELL(0x38) #define RX_SLOT MARK_CELL(0x3c) +#define OS_TOUCH MARK_CELL(0x44) + #define RX_SLOT_A MARK_CELL(0x60) #define RX_SLOT_B MARK_CELL(0x64) #define RX_READY_A MARK_CELL(0x68) diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c index 0c56792..b00780d 100644 --- a/harness/udpstream/vidstream.c +++ b/harness/udpstream/vidstream.c @@ -2,10 +2,12 @@ * vidstream.c -- terminal side of the VT1200 video stream. * * Task 3 claimed the working set with ONE malloc at startup and published - * where it landed. Task 4 adds the receive side: two 24 KB frame slots at the + * where it landed. Task 4 added the receive side: two 24 KB frame slots at the * head of that pool, and a poll loop that hands each completed slot to * fp_parse() (frame_parse.c, shared with the host unit test) and counts what - * it accepted. Task 5 replaces the accept path's placeholder with the blit. + * it accepted. Task 5 blits an accepted strip frame to the screen: one + * PIXBLT B,XY per source row, repeated down the row `stretch` times by + * SPTCH=0 (vram_b.asm). * * Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB): * @@ -17,9 +19,24 @@ * * A slot holds one complete frame -- 16-byte header and payload -- exactly as * it arrives on the wire, so the bytes fp_parse() validates are the bytes the - * blit will read. Neither slot is ever split across a ring wrap: the two ring + * blit reads. Neither slot is ever split across a ring wrap: the two ring * entries' descriptors are re-armed per slot at frame start. * + * Screen geometry. VRAM is at bit 0x04000000 and a screen row is 0x800 bits + * (256 bytes), the real unit's pitch (harness/c-hello/README.md). MAME's own + * renderer decodes the same memory at 128 bytes a row; that difference is a + * property of the harness, so the draw test compares VRAM as memory, not the + * rendered picture. + * + * OS repaint canary (spec 8.9). The blit covers screen rows 0..1023 and + * columns 0..1279 only. One 16-bit sentinel word sits in the unused right + * edge of the last screen row (row 1023, column 1280), outside every pixel the + * blit writes. If something else -- the resident OS -- paints over us it also + * disturbs that corner; OS_TOUCH counts how often that was seen. This + * decoder has no partial update: an accepted frame repaints the whole screen + * anyway, so detecting the touch and repainting are the same act here, and + * OS_TOUCH is the record. + * * Marker layout: see the table in frame_parse.h. Markers are 32-bit cells on * the byte-aligned page at bit 0x01810000, 0x20 bits (4 bytes) apart. The * design plan names them 0x01810020, 0x01810024, ... but the TMS34010 is @@ -28,7 +45,9 @@ * * marker named 0x018100NN -> bit 0x01810000 + NN * 8 * - * (POOL_PTR = 0x01810100, ...). vidstream.lua uses the same rule. + * (POOL_PTR = 0x01810100, ...). Task 5's OS_TOUCH is 0x01810220 (ruling R12), + * i.e. cell 0x44 -- free between the RX_* counters and the injection cells. + * vidstream.lua uses the same rule. * * K&R C only: no prototypes, no '//', declarations before statements. */ @@ -41,6 +60,22 @@ #define STATIC_BYTES 249856 /* the designed working set, 244 KB */ +/* Screen memory and the strip geometry (see the header comment). */ +#define VRAM_BASE 0x04000000 /* video RAM, bit address */ +#define VRAM_ROW 0x800 /* 2048 bits = 256 bytes a screen row */ +#define SCREEN_ROWS 1024 /* rows in the framebuffer */ +#define ROW_PIXELS 1280 /* a strip row is 1280 pixels */ +#define ROW_BYTES 160 /* 1280 pixels, 1 bpp */ + +/* + * The sentinel word: last screen row, first column past the 1280 the blit + * writes. Outside the blitted area by construction, inside mapped VRAM (the + * next 256-byte row would be bit 0x06000000, which the driver maps as the NVR, + * not as video memory). + */ +#define SENT_ADDR (VRAM_BASE + (SCREEN_ROWS - 1) * VRAM_ROW + ROW_PIXELS) +#define SENT_VALUE 0xA5C3 + /* * The two frame slots. SLOT_BYTES is the plan's 24 KB: a 128-row strip frame * (128 x 160 B + 16 B header = 20,496 B = 15 packets) fits, and so does any @@ -62,11 +97,17 @@ unsigned int stack_cushion[1024]; /* * VRAM.ASM's externals. vram.asm is linked into every payload in this - * directory; the C side owns the values. Task 5 fills the blit_* words. + * directory; the C side owns the values. vram_b.asm (PIXBLT B,XY) shares + * blit_src/blit_daddr/blit_dydx and ignores blit_sptch, because its SPTCH is + * always 0 -- that is what makes one source row fill `stretch` screen rows. */ unsigned int vram_pattern; unsigned int blit_src, blit_sptch, blit_daddr, blit_dydx; +extern void vram_blit_b_(); /* vram_b.asm: PIXBLT B,XY, SPTCH = 0 */ +extern void vid_dint_(); /* vram_b.asm: DINT */ +extern void vid_eint_(); /* vram_b.asm: EINT */ + /* * The firmware's free-space counter is a 32-bit count IN BITS at * 0x1E8BA0 (dwt/notes/HEAP.md). POOL_FREE reports bytes, because the @@ -77,6 +118,79 @@ static unsigned int heap_bytes() return *(unsigned int *)HEAP_FREE / 8; } +/* + * blit_row(src, daddr_xy, dydx) + * + * One PIXBLT B,XY. The three values are published where vram_b.asm reads + * them (see the note there on the calling form; the assembly entry point takes + * no arguments). `src` is the packed 1-bpp source row as a bit address, + * `daddr_xy` is the screen position as (y << 16) | x, and `dydx` is the + * rectangle as (screen rows << 16) | pixels. The primitive always sets + * SPTCH = 0, so the "screen rows" dimension reads the same source row that + * many times: that is the vertical stretch. + */ +static void blit_row(src, daddr_xy, dydx) +unsigned int src; +unsigned int daddr_xy; +unsigned int dydx; +{ + blit_src = src; + blit_daddr = daddr_xy; + blit_dydx = dydx; + vram_blit_b_(); +} + +/* + * paint_frame(slot, h) + * + * Draw one accepted strip frame: source row r goes down screen rows + * r*stretch .. r*stretch+stretch-1. A frame the screen cannot hold is not + * drawn: zero rows or zero stretch, more than SCREEN_ROWS screen rows, or a + * payload shorter than the rows it claims (fp_parse() has already proved that + * payload_len bytes arrived, so that bound is real). A refused frame never + * touches VRAM, so a malformed header cannot paint garbage over a good one. + * + * The whole burst runs with interrupts masked (spec 8.9) so the resident OS + * cannot repaint between two of our source rows and tear the picture. + */ +static void paint_frame(slot, h) +unsigned int slot; +fp_header *h; +{ + unsigned int r, rows, stretch, src, sent; + + rows = h->rows; + stretch = h->stretch; + if (h->kind != FP_KIND_STRIP || rows == 0 || stretch == 0) { + return; + } + if (rows * stretch > SCREEN_ROWS) { + return; + } + if (rows * ROW_BYTES > h->payload_len) { + return; + } + + /* The canary word lives outside every pixel this frame writes. If it + * is not ours any more, something else painted VRAM: count it and put + * it back. There is no partial update here -- this frame repaints the + * whole screen anyway -- so OS_TOUCH is the record, not a trigger. */ + 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_(); + src = slot + h->payload_off * 8; + for (r = 0; r < rows; r++) { + blit_row(src + r * ROW_PIXELS, + (r * stretch) << 16, + (stretch << 16) | ROW_PIXELS); + } + vid_eint_(); +} + /* * Handle one slot if the receive path has raised its ready flag. * @@ -125,11 +239,10 @@ unsigned int slot; *(unsigned int *)RX_OK = *(unsigned int *)RX_OK + 1; *(unsigned int *)RX_LAST_LEN = h.payload_len; *(unsigned int *)RX_SLOT = slot; - /* Task 5 blits the accepted frame from here: h.payload_off - * bytes into the slot, h.rows rows of 160 bytes (1280 pixels, - * 1 bpp, MSB first), h.stretch scanlines each. Nothing is - * drawn for a rejected frame, so a corrupt frame can never - * reach VRAM. */ + /* Only an accepted frame reaches VRAM: the bytes just validated + * are the bytes the blit reads, and a corrupt frame can never + * paint over a good one. */ + paint_frame(base, &h); } else { *(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1; } @@ -161,6 +274,12 @@ int main() *(unsigned int *)RX_LEN_A = 0; *(unsigned int *)RX_READY_B = 0; *(unsigned int *)RX_LEN_B = 0; + *(unsigned int *)OS_TOUCH = 0; + + /* Arm the VRAM canary before any frame is accepted, so a repaint + * between now and the first blit is counted rather than mistaken for + * our own last frame. */ + *(unsigned short *)SENT_ADDR = SENT_VALUE; before = heap_bytes(); ptr = malloc_(POOL_BYTES); /* malloc_ takes BYTES, returns BITS */ diff --git a/harness/udpstream/vram_b.asm b/harness/udpstream/vram_b.asm new file mode 100644 index 0000000..6104e1a --- /dev/null +++ b/harness/udpstream/vram_b.asm @@ -0,0 +1,119 @@ +; vram_b.asm -- PIXBLT B,XY: a packed 1-bpp RAM row straight to the screen. +; +; Task 5's blit primitive. PIXBLT's B source form reads the source as packed +; 1-bit pixels from a LINEAR bit address and writes an XY destination, which is +; exactly the strip decoder's shape: a 1280-pixel row (160 bytes) in the slot, +; painted down `stretch` screen rows. +; +; The vertical stretch is one instruction, not a loop over screen rows: SPTCH +; = 0 makes every source row read the SAME source address, so a PIXBLT whose +; DYDX names `stretch` rows paints the one row `stretch` times. +; +; DESCRIPTION-register assignments for PIXBLT B,XY (TMS34010 User's Guide, +; instruction reference; vram.asm already uses the same values for L,XY): +; SADDR = B0 source bit address (1 bpp, packed) +; SPTCH = B1 source pitch in bits; 0 = do not advance the source +; 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: COLOR0 and COLOR1 are implied B-register +; operands of every graphics instruction (vram.asm's FILL passes its pattern in +; B9 for the same reason). They are explicit here because a PIXBLT B expands +; a packed 1-bpp source into pixel values, so whatever the resident OS left in +; B9 becomes the colour of every set pixel -- measured: the first draw run +; painted every row it blitted black because nothing had set B9 (the padding +; past column 1280, which the blit does not write, kept the OS's old bytes). +; +; 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 points. The TMS34010 C compiler passes arguments in its own frame- +; pointer scheme, and no hand-written assembly in this repository has ever +; DECODED that frame: the ROM thunks in entry.asm forward the caller's stack +; frame through `calla` without touching it, and the two array primitives in +; vram.asm take their inputs from published words in .bss with a no-argument +; entry (called by lance_cmd.c, measured on the unit). This primitive follows +; the vram.asm form, so the three interface values named in the plan live in +; blit_src / blit_daddr / blit_dydx (blit_sptch only exists for vram.asm and is +; ignored: this primitive always sets SPTCH = 0). The C side wraps them in +; blit_row() so the call reads as blit_row(src, daddr_xy, dydx). +; +; All inputs are reloaded on every call: PIXBLT leaves SADDR/DADDR advanced, so +; a caller that "saves" them saves a half-consumed rectangle. +; +; _vid_dint_ / _vid_eint_ -- interrupts off/on. The C compiler cannot emit +; DINT/EINT, and spec section 8.9 wants the OS masked for the whole burst so a +; resident repaint cannot tear the picture. + + .ref _blit_src + .ref _blit_daddr + .ref _blit_dydx + .text + .globl _vram_blit_b_ +_vram_blit_b_: + 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 @_blit_src, b0, 1 ; SADDR: packed 1-bpp source row + clr b1 ; SPTCH = 0: every row is the same source row + move @_blit_daddr, b2, 1 ; DADDR (XY) + movi 800h, b3 ; DPTCH = 2048 bits = 256-byte row pitch + movi 04000000h, b4 ; OFFSET = VRAM base + move @_blit_dydx, b7, 1 ; DYDX (rows, columns) + 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 + + .globl _vid_dint_ +_vid_dint_: + dint + rets + + .globl _vid_eint_ +_vid_eint_: + eint + rets + + .end