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 <address>` 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).
This commit is contained in:
opencode.energia.arti.ee committed 2026-10-30 23:43:02 +00:00
1 parent 487bb1789a
commit 45110270a7
6 files changed
+670 -18

No files matched your search

+25 -1
View File
@@ -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
+206 -7
View File
@@ -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 <address>` 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")
+182
View File
@@ -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())
+9
View File
@@ -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)
+129 -10
View File
@@ -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 */
+119
View File
@@ -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