Files
vt1200/harness/mame/vidstream.lua
T

1502 lines
57 KiB
Lua

-- vidstream.lua -- load the streaming payload and check its startup allocation
-- and its receive path.
--
-- MECHANISM: debugger entry, not a delivery mechanism. The firmware is run
-- with VT1200_SKIP_LEVEL1=1 (a documented divergence) to the OS, because the
-- level-0 diagnostics menu's heap is NOT initialised (both the free counter at
-- 0x1E8BA0 and the free-list head at 0x1E8AC0 read 0 there -- measured; see
-- the task-3 report). The payload is poked into RAM at VID_AT and entered at
-- the OS idle loop by a breakpoint, the same way harness/mame/hello.lua enters
-- its payload at the diagnostics idle loop.
--
-- Mode "alloc" (Task 3) asserts:
-- * POOL_PTR is non-zero and its BYTE address is in 0x100000..0x400000;
-- * the pointer is 16-bit-word aligned and, for a real allocation, its byte
-- address is BELOW the payload origin (a pool above .text would overlap
-- .text/.bss -- the old range test alone accepted byte 0x318000/0x380000);
-- * the heap free counter dropped by POOL_BYTES (plus allocator header);
-- * on a failed malloc, POOL_PTR is the linked static buffer and the low bit
-- of POOL_FREE (before) marks the fallback, i.e. the payload keeps running.
--
-- Mode "rx" (Task 4) is "alloc" plus the receive path: it delivers one VALID
-- frame and then the same frame with one header byte flipped, and asserts
-- RX_OK == 1 && RX_BAD == 1, that RX_LAST_LEN is the accepted frame's
-- payload_len, and that RX_SLOT names the slot the good frame went into.
--
-- Mode "draw" (Task 5) is the pixel check. It delivers 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
-- delivered, 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.
--
-- Mode "stream" (Task 6) is the status frame. It plays the sender: 200 strip
-- frames, one per emulated frame whenever the ring has room. The payload
-- accepts them and, every 64 accepted frames, builds a 60-byte status frame in
-- its own buffer and hands it to the LANCE. This mode watches ST_SENT move,
-- reads the frame back from ST_BUF, and when the 200th frame has been accepted
-- it sets RX_EOF so the terminal reports the final count (192 is the last
-- multiple of 64, so without RX_EOF the run could never see 200). It writes
-- stream-status.bin (the frames exactly as the payload built them) and
-- stream-status.txt (counters), and vidstream_status_check.py decodes both with
-- the same wire format the host sender uses. What this does NOT do is put a
-- byte on a wire: the emulated LANCE has no network interface (R11), so the
-- transmit is proven by the descriptor completing (TMD1's OWN cleared) and by
-- the bytes in the buffer, not by a socket.
--
-- Mode "ring" (Task 7) is the transport itself. The payload now owns a real
-- 16-descriptor LANCE RX ring (2 x 24 KB slots of 16 x 1526-byte buffers), so
-- every mode injects through that ring -- see "PLAYING THE CHIP" below -- and
-- this mode asserts the ring's own structure:
-- * the payload arms all 16 descriptors with OWN set, each pointing at its own
-- 1526-byte buffer inside slot A, RMD2 = 0x10000 - 1526;
-- * a complete frame moves the write pointer to slot B: the frame is accepted
-- (RX_OK 1, RX_SLOT 0), and all 16 descriptors are re-armed on slot B;
-- * a frame whose header claims more than SLOT_BYTES is rejected without a
-- blit: RX_BAD becomes 1, RX_OK stays 1, the screen does not change, and the
-- ring recovers on slot A.
-- It writes ring-status.txt and prints the verdict itself: these are structural
-- facts about the descriptors, not pixels (draw mode owns the pixels).
--
-- 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
-- recv_complete_cb() does: it writes the datagram into the buffer of the
-- descriptor at RX_RING_POS, then writes RMD3 (delivered length including the
-- 4-byte FCS) and RMD1 (STP | ENP, OWN clear). A multi-datagram frame fills
-- consecutive descriptors in ring order, which is the order the chip uses
-- (INIT resets its ring position to 0 and it advances one descriptor per
-- packet). That is a stand-in for the DMA only -- the payload's parsing,
-- framing, slot alternation and painting are all its own.
--
-- Marker page: bit 0x01810000. A marker named 0x018100NN in the plan is a
-- BYTE offset into that page (0x01810024 is four bits after 0x01810020 --
-- inside the same 32-bit cell -- so the literal reading cannot work). The C
-- side uses the same rule: bit = 0x01810000 + NN * 8. The full table,
-- including the RX_* and RX_RING_* cells below, is in
-- harness/udpstream/frame_parse.h.
--
-- Env:
-- VT1200_MODE alloc (default), rx, draw, stream or ring
-- 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 and ring 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.
-- VID_DRAW_WAIT draw mode only: emulated frames to wait for the payload to
-- accept the frame (default 300). Set it to 1 to exercise
-- the timeout path.
-- VID_STREAM_FRAMES stream mode only: how many frames the sender sends
-- (default 200)
-- VID_STREAM_WAIT stream mode only: emulated frames to wait for the final
-- status after RX_EOF (default 900)
-- VID_RING_WAIT ring mode only: emulated frames to wait for each ring
-- transition (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)
-- VID_ENTRY_PC breakpoint PC used to enter (default 0x3F579970, OS idle)
-- VID_POOL_BYTES POOL_BYTES the image was built with (default 249856)
-- VID_STATIC_PTR linked address of the fallback pool, from vidstream.env
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("VID ERROR: debugger not enabled; pass -debug")
machine:exit()
return
end
local MODE = os.getenv("VT1200_MODE") or "alloc"
local AT = tonumber(os.getenv("VID_AT") or "2400")
-- The stop frame depends on the mode: a stream run needs room for
-- VID_STREAM_FRAMES frames plus the final-status wait, and the payload needs
-- a few emulated frames per accepted frame (one full-screen blit each).
local DEFAULT_END = AT + 60
if MODE == "stream" and not os.getenv("VID_END") then
DEFAULT_END = AT + tonumber(os.getenv("VID_STREAM_FRAMES") or "200") * 8 + 2000
elseif MODE == "ring" and not os.getenv("VID_END") then
-- Three phases, each with its own bounded wait (VID_RING_WAIT, 300).
DEFAULT_END = AT + 3 * tonumber(os.getenv("VID_RING_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")
local ENTRY_PC = tonumber(os.getenv("VID_ENTRY_PC") or "0x3F579970")
local PATH = os.getenv("VID_PAYLOAD") or "harness/udpstream/build/vidstream.bin"
local POOL_BYTES = tonumber(os.getenv("VID_POOL_BYTES") or "249856")
local STATIC_PTR = tonumber(os.getenv("VID_STATIC_PTR") or "0")
-- _start is at the linked entry point, not at the section origin: GSPLNK
-- places ENTRY.obj after the C object (see the map). build-stream.sh
-- publishes the address; ORIGIN is only where the raw .text is poked.
local ENTRY_PTR = tonumber(os.getenv("VID_ENTRY_PTR") or "0x018C0000")
local MARK_PAGE = 0x01810000
local POOL_PTR = MARK_PAGE + 0x20 * 8
local POOL_FREEB = MARK_PAGE + 0x24 * 8
local POOL_FREEA = MARK_PAGE + 0x28 * 8
local VID_ALIVE = MARK_PAGE + 0x2c * 8
-- Task 4: receive counters and (retired by Task 7) the injection cells.
local RX_OK = MARK_PAGE + 0x30 * 8
local RX_BAD = MARK_PAGE + 0x34 * 8
local RX_LAST_LEN = MARK_PAGE + 0x38 * 8
local RX_SLOT = MARK_PAGE + 0x3c * 8
local RX_SLOT_A = MARK_PAGE + 0x60 * 8
local RX_SLOT_B = MARK_PAGE + 0x64 * 8
-- Task 7: the RX ring record (frame_parse.h).
-- Cells are FOUR bytes apart (n increases by 4), so these are 0x10, 0x14,
-- 0x18, 0x1c, 0x40 and 0x48 -- not six adjacent n values, which would be one
-- byte apart and would overlap into zeros.
local RX_RING_BASE = MARK_PAGE + 0x10 * 8
local RX_RING_POS = MARK_PAGE + 0x14 * 8
local RX_RING_ACC = MARK_PAGE + 0x18 * 8
local RX_RING_ARMED = MARK_PAGE + 0x1c * 8
local RX_RING_DROPS = MARK_PAGE + 0x40 * 8
local RX_DGRAM = MARK_PAGE + 0x48 * 8
-- Task 5: the OS-repaint canary (frame_parse.h, ruling R12).
local OS_TOUCH = MARK_PAGE + 0x44 * 8
-- Task 6: the sender/keyframe cell and the status record.
local RX_EOF = MARK_PAGE + 0x78 * 8
local ST_SENT = MARK_PAGE + 0x7c * 8
local ST_FRAMES = MARK_PAGE + 0x80 * 8
local ST_BUF = MARK_PAGE + 0x84 * 8
local ST_TMD1 = MARK_PAGE + 0x88 * 8
local ST_INIT = MARK_PAGE + 0x8c * 8
local ST_TMLAST = MARK_PAGE + 0x90 * 8
-- Must match vidstream.c.
local SLOT_BYTES = 24576
-- The RX ring (Task 7), from vidstream.c: RX_DESC descriptors of 8 bytes each,
-- one RXBUFLEN-byte buffer per descriptor inside the armed slot.
local RX_DESC = 16
local RXBUFLEN = 1526
local ETH_HDR = 14
local ETH_FCS = 4
local RMD1_OWN = 0x8000
local RMD1_ENP = 0x0100
local RMD1_STP = 0x0200
-- The frame every mode delivers first: 8 rows of 1280 pixels, 1 bpp MSB-first
-- (160 bytes a row), each row repeated 8 times. 1280 B of payload -- the same
-- geometry vidsrc.py's ROW_BYTES produces, so Task 5 can blit it unchanged.
-- 1296 bytes is ONE datagram, which is what mode rx wants.
local RX_ROWS = 8
local RX_STRETCH = 8
local RX_ROW_BYTES = 160
local RX_PLEN = RX_ROWS * RX_ROW_BYTES
local RX_FRAME = 16 + RX_PLEN
-- 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; at the sender's
-- 1450-byte chunk it is 15 datagrams of the 16 descriptors, which is exactly
-- the "one descriptor to spare" the design claims.
local DRAW_ROWS = 128
local DRAW_STRETCH = 8
local DRAW_PLEN = DRAW_ROWS * RX_ROW_BYTES
local DRAW_FRAME = 16 + DRAW_PLEN
local DRAW_CHUNK = 1450
-- Stream mode (Task 6): how many frames the sender plays, how long to wait for
-- the final status once RX_EOF is set, and the frame's geometry. The terminal
-- reports every 64 accepted frames (STATUS_EVERY in vidstream.c), so 200 frames
-- produce reports at 64, 128, 192 and -- after RX_EOF -- at 200.
--
-- One frame is one datagram (the 8-row, 1296-byte frame from mode rx): it fits a
-- single 1526-byte buffer, so the ring position does not have to be a multiple
-- of anything and every descriptor can hold the same body. Re-poking 200 x 20
-- KB through the debugger would take minutes, which is why the stream mode does
-- not use the 128-row frame; modes draw and ring cover that path.
local STREAM_FRAMES = tonumber(os.getenv("VID_STREAM_FRAMES") or "200")
local STREAM_WAIT = tonumber(os.getenv("VID_STREAM_WAIT") or "900")
-- Emulated frames to give the chip to hand the last TX descriptor back after
-- the final status. It is a bounded safety net, not a routine delay: MAME's
-- LANCE model completes a transmit in len/m_bandwidth, where dinetwork.cpp
-- converts the chip's 10 Mbit/s into m_bandwidth = bandwidth << 17 bytes/s,
-- so a 60-byte frame takes 60/1310720 = 46 us emulated. Measured in
-- VT1200_LANCE_DMA_LOG for this run: 49 us from the descriptor read to the
-- write-back (t=35.992888 -> 35.992937). The payload's own bounded wait
-- normally sees that completion (ST_TMD1's OWN is clear), and this wait only
-- matters if the host is too slow to service the model's timer.
local STREAM_DRAIN = tonumber(os.getenv("VID_STREAM_DRAIN") or "900")
local STATUS_EVERY = 64
local STATUS_BYTES = 60 -- STATUS_LEN in vidstream.c
local STATUS_BODY = 14 + 16 -- Ethernet header + VS header
-- 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"
and MODE ~= "stream" and MODE ~= "ring" then
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
machine:exit()
return
end
-- CCITT-FALSE (polynomial 0x1021, init 0xFFFF, not reflected, no final xor).
-- frame.py, frame_parse.c and this loop are three independent readings of the
-- same spec; the harness signs its own frames rather than reusing a value the
-- payload computed, so the C parser accepting them is a real check. Since
-- ruling R14 the CRC input is the 12 header bytes only.
local function crc_add(crc, bytes, first, count)
for i = first, first + count - 1 do
crc = (crc ~ (bytes[i] << 8)) & 0xFFFF
for _ = 1, 8 do
if (crc & 0x8000) ~= 0 then
crc = ((crc << 1) ~ 0x1021) & 0xFFFF
else
crc = (crc << 1) & 0xFFFF
end
end
end
return crc
end
-- A complete frame, header and payload, laid out as frame.py writes it.
-- `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'
b[2] = 0x01 -- version
b[3] = kind
b[4] = frame_no & 0xFF
b[5] = (frame_no >> 8) & 0xFF
b[6] = plen & 0xFF
b[7] = (plen >> 8) & 0xFF
b[8] = rows
b[9] = stretch
b[10] = 0
b[11] = 0
for i = 0, plen - 1 do
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)
-- Header bytes 0..11 only (ruling R14): the payload is protected by each
-- datagram's Ethernet FCS, not by this CRC. frame.py, frame_parse.c and
-- this loop are three independent readings of the same rule.
b[12] = crc & 0xFF
b[13] = (crc >> 8) & 0xFF
b[14] = 0
b[15] = 0
return b
end
-- Mode rx's two frames. Both are ONE datagram (1296 + 14 = 1310 bytes, well
-- inside a 1526-byte buffer); the second carries a flipped header byte, inside
-- the 12 bytes the CRC covers. Since ruling R14 a flipped PAYLOAD byte is
-- ACCEPTED by fp_parse (the Ethernet FCS is the chip's job), so a payload flip
-- would no longer exercise the reject path; the payload-flip case is pinned by
-- the host tests instead.
local rx_valid = make_frame(1, 0x0021, RX_ROWS, RX_STRETCH, RX_PLEN)
local rx_corrupt = make_frame(1, 0x0022, RX_ROWS, RX_STRETCH, RX_PLEN)
rx_corrupt[4] = rx_corrupt[4] ~ 0x01 -- frame_no low byte, CRC left
local payload = nil
do
local f, err = io.open(PATH, "rb")
if not f then
print("VID ERROR: cannot open " .. PATH .. ": " .. tostring(err))
machine:exit()
return
end
payload = f:read("*a")
f:close()
print(string.format("VID: payload %s bytes=%d origin=%s", PATH, #payload, hex(ORIGIN)))
end
local frames = 0
local loaded = false
local entered = false
local armed = false
local direct = false
local alive0 = nil
local alive_last = nil
local injected = false
local injected_bad = false
local readback_a = false
local readback_b = false
local function load_payload()
local n = math.floor(#payload / 2)
local words = {}
local bad = 0
for i = 0, n - 1 do
local lo = payload:byte(i * 2 + 1)
local hi = payload:byte(i * 2 + 2)
words[i] = lo | (hi << 8)
prog:write_u16(ORIGIN + i * 16, words[i])
end
for i = 0, n - 1 do
if prog:read_u16(ORIGIN + i * 16) ~= words[i] then
bad = bad + 1
end
end
print(string.format("VID: loaded %d words at bit %s; readback %s (%d bad)",
n, hex(ORIGIN), bad == 0 and "OK" or "FAILED", bad))
-- Enter through the idle loop, exactly like hello.lua. The action only
-- assigns PC and resumes; the payload proves itself.
dbg:command("bpset " .. hex(ENTRY_PC) .. ",,{ pc = " .. hex(ENTRY_PTR) .. " ; g }")
armed = true
end
local function in_payload(pc)
return pc >= ORIGIN and pc < ORIGIN + #payload * 8
end
local function enter_direct(reason)
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
-- --- Task 7: playing the chip ----------------------------------------------
--
-- The payload owns a 16-descriptor LANCE RX ring; the emulated chip has no
-- network interface (ruling R11), so the harness plays the CHIP: it writes one
-- Ethernet frame into the buffer of the descriptor at RX_RING_POS and completes
-- that descriptor the way am79c90.cpp's recv_complete_cb() does -- RMD3 =
-- delivered length including the 4-byte FCS, RMD1 = STP | ENP with OWN clear.
-- A VS frame split over several datagrams fills consecutive descriptors in ring
-- order, which is the order the chip uses: INIT resets its ring position to 0
-- (am79c90.cpp device_start) and every completed packet advances it by one.
--
-- None of the work under test is done here: framing, header validation, slot
-- alternation, the fragment map and the paint are the payload's.
-- The terminal's Ethernet address (frame.py TERMINAL_MAC, from its address ROM).
local TERMINAL_MAC = { 0x08, 0x00, 0x2B, 0x16, 0x4B, 0x5A }
local function ring_ready()
return prog:read_u32(RX_RING_BASE) ~= 0
end
-- The armed slot's base (a BIT address), the next descriptor to fill, and which
-- slot it is.
local function ring_state()
local armed = prog:read_u32(RX_RING_ARMED)
local base = armed == 0 and prog:read_u32(RX_SLOT_A) or prog:read_u32(RX_SLOT_B)
return base, prog:read_u32(RX_RING_POS), armed
end
-- The bit address of descriptor i's RMD word at byte offset `off` (0 = RMD0
-- address low, 2 = RMD1, 4 = RMD2 length, 6 = RMD3 message count).
local function rmd_bit(ringb, i, off)
return ringb + i * 64 + off * 8
end
local function rmd1_of(ringb, i)
return prog:read_u16(rmd_bit(ringb, i, 2))
end
-- A descriptor's buffer address as a BYTE address (RMD1 bits 0-7 hold the high
-- byte, RMD0 the low 16 bits).
local function buf_addr(ringb, i)
return ((rmd1_of(ringb, i) & 0xFF) << 16) | prog:read_u16(rmd_bit(ringb, i, 0))
end
-- Write one Ethernet frame -- broadcast destination, the terminal's own source,
-- the stream's ethertype -- and `len` body bytes from `frame` at offset `off`
-- into the buffer whose bit address is `bufbit`. The 4-byte FCS the chip would
-- store after the frame is written as zero: nothing reads it (the payload
-- subtracts 4 from RMD3 and never looks at those bytes), so inventing a value
-- would only pretend the harness checked a CRC it does not check.
local function poke_packet(bufbit, frame, off, len)
local buf = bufbit // 8 -- BYTE address
local i
for i = 0, 5 do
prog:write_u8((buf + i) * 8, 0xFF)
end
for i = 0, 5 do
prog:write_u8((buf + 6 + i) * 8, TERMINAL_MAC[i + 1])
end
prog:write_u8((buf + 12) * 8, 0x88)
prog:write_u8((buf + 13) * 8, 0xB7)
for i = 0, len - 1 do
prog:write_u8((buf + ETH_HDR + i) * 8, frame[off + i])
end
for i = 0, ETH_FCS - 1 do
prog:write_u8((buf + ETH_HDR + len + i) * 8, 0)
end
end
-- Did the bytes that landed in descriptor `idx`'s buffer match the frame? The
-- checker for draw mode compares the screen against what was delivered, so a
-- delivery this harness corrupted would be blamed on the payload's blit -- hence
-- the readback, exactly as the Task 4/5 modes did it.
local function verify_chunk(bufbit, frame, off, len)
local buf = bufbit // 8 + ETH_HDR
local bad = 0
for i = 0, len - 1 do
if prog:read_u8((buf + i) * 8) ~= frame[off + i] then
bad = bad + 1
end
end
return bad == 0, bad
end
-- Complete descriptor `idx` the way the chip does. Returns false when the
-- descriptor is not armed: the ledger and the ring disagree and the caller must
-- not pretend a datagram was delivered.
local function complete_dgram(ringb, base, idx, len)
if (rmd1_of(ringb, idx) & RMD1_OWN) == 0 then
return false
end
local byte = base // 8 + idx * RXBUFLEN
prog:write_u16(rmd_bit(ringb, idx, 6), ETH_HDR + len + ETH_FCS)
prog:write_u16(rmd_bit(ringb, idx, 2),
RMD1_STP | RMD1_ENP | ((byte >> 16) & 0xFF))
return true
end
-- Deliver a whole VS frame (16-byte header + payload, `flen` bytes) as
-- datagrams of at most `chunk` bytes, in ring order from RX_RING_POS. Returns
-- the number of datagrams delivered, or nil when the ring is not ready or a
-- descriptor was not armed.
local function inject_frame(frame, flen, chunk)
if not ring_ready() then
return nil
end
local ringb = prog:read_u32(RX_RING_BASE)
local base, pos = ring_state()
local ndg = (flen + chunk - 1) // chunk
local k
for k = 0, ndg - 1 do
local idx = (pos + k) % RX_DESC
local off = k * chunk
local n = flen - off
if n > chunk then
n = chunk
end
local bufbit = base + idx * RXBUFLEN * 8
if (rmd1_of(ringb, idx) & RMD1_OWN) == 0 then
print(string.format("VID: descriptor %d is not armed; the ring and this harness disagree", idx))
return nil
end
poke_packet(bufbit, frame, off, n)
complete_dgram(ringb, base, idx, n)
end
return ndg
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 rx_ok_at = nil
-- How long to wait for the payload to accept the frame, in EMULATED FRAMES.
-- This hook only runs once a frame, so that is the resolution of the
-- measurement below as well as the timeout. VID_DRAW_WAIT exists so the
-- timeout path can be exercised on purpose (set it to 1).
local DRAW_WAIT = tonumber(os.getenv("VID_DRAW_WAIT") or "300")
-- Evidence freshness (ruling R15): remove this mode's dump files before the
-- 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 function clear_dump_files()
if MODE ~= "draw" and MODE ~= "stream" then
return
end
for _, name in ipairs(DRAW_FILES) do
os.remove(DUMP_DIR .. "/" .. name)
end
for _, name in ipairs(STREAM_FILES) do
os.remove(DUMP_DIR .. "/" .. name)
end
end
clear_dump_files()
local function draw_row_byte(i)
if (i // RX_ROW_BYTES) % 3 == 2 then
return 0x00
end
return 0xF0
end
local function inject_draw()
-- Negative control for the OS-repaint canary: clobber the sentinel word
-- (last screen row, column 1280) just before the frame is delivered. 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
local f = make_frame(1, 0x0051, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN,
draw_row_byte)
local ndg = inject_frame(f, DRAW_FRAME, DRAW_CHUNK)
if not ndg then
return false
end
draw_bytes = f
local base, pos = ring_state()
local okb, badb = true, 0
local k
for k = 0, ndg - 1 do
local idx = (pos + k) % RX_DESC
local off = k * DRAW_CHUNK
local n = DRAW_FRAME - off
if n > DRAW_CHUNK then
n = DRAW_CHUNK
end
local okc, badc = verify_chunk(base + idx * RXBUFLEN * 8, f, off, n)
badb = badb + badc
if not okc then
okb = false
end
end
print(string.format("VID: frame %d: delivered %d datagrams (%d B, valid, %d rows x %d stretch) into the ring",
frames, ndg, DRAW_FRAME, DRAW_ROWS, DRAW_STRETCH))
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
-- --- Task 6: stream mode ---------------------------------------------------
--
-- The harness plays the sender here, and the terminal's status frame is the
-- thing under test. The frames go in through the payload's own RX ring, exactly
-- as in modes rx, draw and ring, because the emulated LANCE has no network
-- interface (R11).
--
-- One frame is ONE datagram here: 8 rows of 160 bytes is a 1296-byte VS frame,
-- so it fits a single 1526-byte buffer. That is what keeps a 200-frame run
-- affordable and position-independent: every descriptor buffer in both slots
-- holds the same frame body, so whichever descriptor the ring is on, the harness
-- only has to rewrite that body's 16-byte header and complete the descriptor.
-- The multi-datagram path (a 128-row frame split over 15 or 16 datagrams) is
-- covered by modes draw and ring; this mode's subject is the status cadence, and
-- 200 accepted frames exercise the ring position advancing, wrapping and the
-- slots alternating 200 times.
-- The 16-byte VS header for one frame number, sealed over its first 12 bytes
-- (ruling R14). The body of each descriptor buffer is written once; only this
-- header changes per frame, so the harness is not slower than the payload.
local function header_bytes(kind, frame_no, rows, stretch, plen)
local b = {}
b[0] = 0x56
b[1] = 0x53
b[2] = 0x01
b[3] = kind
b[4] = frame_no & 0xFF
b[5] = (frame_no >> 8) & 0xFF
b[6] = plen & 0xFF
b[7] = (plen >> 8) & 0xFF
b[8] = rows
b[9] = stretch
b[10] = 0
b[11] = 0
local crc = crc_add(0xFFFF, b, 0, 12)
b[12] = crc & 0xFF
b[13] = (crc >> 8) & 0xFF
b[14] = 0
b[15] = 0
return b
end
local function read_bytes(base, n)
local chars = {}
local t = {}
for i = 0, n - 1 do
local v = prog:read_u8(base + i * 8)
t[i] = v
chars[#chars + 1] = string.char(v)
end
return table.concat(chars), t
end
local function le16(t, off)
return t[off] | (t[off + 1] << 8)
end
local function le32(t, off)
return t[off] | (t[off + 1] << 8) | (t[off + 2] << 16) | (t[off + 3] << 24)
end
local stream_prefilled = false
local stream_body = nil
local announced = 0
local eof_at = nil
local drain_from = nil
local st_sent_seen = 0
local bad_captures = 0
local captures = {}
-- Lay the stream frame's body into EVERY descriptor buffer of BOTH slots, once.
-- One frame is one datagram, so whichever descriptor the ring is on already
-- holds the right bytes and only the header has to change per frame.
local function stream_prefill()
if not ring_ready() then
return false
end
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
local k
if a == 0 or b == 0 then
return false
end
stream_body = make_frame(1, 0, RX_ROWS, RX_STRETCH, RX_PLEN,
draw_row_byte)
for k = 0, RX_DESC - 1 do
poke_packet(a + k * RXBUFLEN * 8, stream_body, 0, RX_FRAME)
poke_packet(b + k * RXBUFLEN * 8, stream_body, 0, RX_FRAME)
end
print(string.format("VID: frame %d: stream ring laid out (%d-row frame, %d B, one datagram per descriptor; A=%s B=%s); sender will deliver %d frames",
frames, RX_ROWS, RX_FRAME, hex(a), hex(b), STREAM_FRAMES))
return true
end
-- Deliver one frame: rewrite the header in the buffer of the descriptor at
-- RX_RING_POS and complete it. Returns false while that descriptor is not armed
-- (the payload has not finished with the previous frame), which is how the
-- sender learns the terminal's pace -- the role the ready flag played in Task 4.
local function inject_stream()
local ringb = prog:read_u32(RX_RING_BASE)
local base, pos = ring_state()
local hdr, k
if (rmd1_of(ringb, pos) & RMD1_OWN) == 0 then
return false -- not re-armed yet
end
hdr = header_bytes(1, announced & 0xFFFF, RX_ROWS, RX_STRETCH, RX_PLEN)
local hbuf = base // 8 + pos * RXBUFLEN + ETH_HDR
for k = 0, 15 do
prog:write_u8((hbuf + k) * 8, hdr[k])
end
complete_dgram(ringb, base, pos, RX_FRAME)
announced = announced + 1
return true
end
-- Read the status frame the payload last produced, when ST_SENT moves. The
-- payload publishes ST_SENT last, so a reader that sees it move reads a frame
-- that was built, transmitted and recorded. This Lua only decodes enough to
-- decide when to stop; the wire format is checked by
-- harness/mame/vidstream_status_check.py, which decodes the dumped bytes.
local function capture_status()
local sent = prog:read_u32(ST_SENT)
if sent == st_sent_seen then
return
end
st_sent_seen = sent
local base = prog:read_u32(ST_BUF)
if base == 0 then
return
end
local raw, t = read_bytes(base, STATUS_BYTES)
local rec = {
bytes = raw,
magic_ok = (t[14] == 0x56 and t[15] == 0x53),
kind = t[17],
payload_len = le16(t, 20),
frames_rx = le32(t, STATUS_BODY),
crc_err = le16(t, STATUS_BODY + 4),
drops = le16(t, STATUS_BODY + 6),
csr0 = le16(t, STATUS_BODY + 8),
heap_kb = le16(t, STATUS_BODY + 10),
}
-- Only a well-formed VS status frame counts. A payload without the
-- status path leaves ST_SENT/ST_BUF as whatever RAM held, so the first
-- "capture" would otherwise be garbage presented as a report -- and
-- "no status frames" is exactly what a Task 5 tree must say.
if not rec.magic_ok or rec.kind ~= 5 or rec.payload_len ~= 12 then
bad_captures = bad_captures + 1
print(string.format("VID: status capture %d is not a VS status frame (magic=%s kind=%d payload_len=%d); ignored",
bad_captures, rec.magic_ok and "VS" or "WRONG", rec.kind,
rec.payload_len))
return
end
captures[#captures + 1] = rec
print(string.format("VID: status %d: frames_rx=%d crc_err=%d drops=%d csr0=%04X heap_free_kb=%d kind=%d payload_len=%d",
#captures, rec.frames_rx, rec.crc_err, rec.drops, rec.csr0,
rec.heap_kb, rec.kind, rec.payload_len))
end
-- Dump the captured status frames and the run's counters, then end the run.
-- The verdict here is only "the run produced what the harness asked for"; the
-- bytes themselves are decoded independently by the checker, so a wrong field
-- offset or a bad CRC cannot be blessed by this Lua.
local function dump_stream(timed_out)
local chunks = {}
local last = captures[#captures]
local lines = {
string.format("INJECTED=%d", announced),
string.format("STATUS_COUNT=%d", #captures),
string.format("BAD_STATUS_FRAMES=%d", bad_captures),
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
string.format("ST_INIT=%d", prog:read_u32(ST_INIT)),
string.format("ST_TMD1=%d", prog:read_u32(ST_TMD1)),
string.format("EOF_SET=%d", eof_at and 1 or 0),
string.format("TIMEOUT=%d", timed_out and 1 or 0),
}
for _, rec in ipairs(captures) do
chunks[#chunks + 1] = rec.bytes
end
if last then
lines[#lines + 1] = string.format("LAST_FRAMES_RX=%d", last.frames_rx)
lines[#lines + 1] = string.format("LAST_CRC_ERR=%d", last.crc_err)
lines[#lines + 1] = string.format("LAST_DROPS=%d", last.drops)
lines[#lines + 1] = string.format("LAST_CSR0=%d", last.csr0)
lines[#lines + 1] = string.format("LAST_HEAP_KB=%d", last.heap_kb)
lines[#lines + 1] = string.format("LAST_KIND=%d", last.kind)
lines[#lines + 1] = string.format("LAST_PAYLOAD_LEN=%d", last.payload_len)
end
-- The descriptor's state now, and how long the chip took to finish it.
local tp = prog:read_u32(ST_TMLAST)
local tmd1_now = tp ~= 0 and prog:read_u16(tp) or 0
lines[#lines + 1] = string.format("TMD1_DRAINED=%d", tmd1_now)
lines[#lines + 1] = string.format("DRAIN_FRAMES=%d",
drain_from and (frames - drain_from) or -1)
local okb = write_file(DUMP_DIR .. "/stream-status.bin", table.concat(chunks))
local okt = write_file(DUMP_DIR .. "/stream-status.txt",
table.concat(lines, "\n") .. "\n")
print(string.format("VID: %d status frames captured (%d B) in %s/stream-status.bin",
#captures, #table.concat(chunks), DUMP_DIR))
local ok = okb and okt and not timed_out and bad_captures == 0
and #captures >= 3 and last ~= nil
and last.frames_rx == STREAM_FRAMES and last.drops == 0
and last.crc_err == 0
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
machine:exit()
end
-- --- Task 7: ring mode -----------------------------------------------------
--
-- The structural check of the payload's own receive ring. Nothing here looks at
-- pixels -- draw mode owns those. What this mode establishes is the transport's
-- contract: 16 descriptors armed for one slot, a completed frame moving them to
-- the other slot, and a frame larger than a slot rejected without a blit. That
-- is as far as an emulator with no network interface (R11) can go; on the unit
-- the same descriptors are filled by the chip's DMA instead of by this script.
local RING_WAIT = tonumber(os.getenv("VID_RING_WAIT") or "300")
local inspect_pool -- defined below, after the dumpers
local ring_phase = 0
local ring_phase_at = nil
local ring_fail = 0
local ring_lines = {}
local ring_vram0 = nil
local ring_ringb = 0
local ring_pos0 = 0
local ring_dgrams = 0
local function ring_check(cond, what)
if cond then
ring_lines[#ring_lines + 1] = "PASS " .. what
print("VID: ring PASS: " .. what)
else
ring_lines[#ring_lines + 1] = "FAIL " .. what
print("VID: ring FAIL: " .. what)
ring_fail = ring_fail + 1
end
return cond
end
-- Are all 16 descriptors armed for `base`? OWN set, buffer i at
-- base + i*1526, buffers 1526 bytes apart, RMD2 = 0x10000 - 1526.
local function ring_armed_for(base, what)
local want = base // 8
local i, badown, badaddr, badlen, badstride = 0, 0, 0, 0, 0
local prev = nil
for i = 0, RX_DESC - 1 do
if (rmd1_of(ring_ringb, i) & RMD1_OWN) == 0 then
badown = badown + 1
end
if buf_addr(ring_ringb, i) ~= want + i * RXBUFLEN then
badaddr = badaddr + 1
end
if prog:read_u16(rmd_bit(ring_ringb, i, 4)) ~= (0x10000 - RXBUFLEN) % 0x10000 then
badlen = badlen + 1
end
if prev ~= nil and buf_addr(ring_ringb, i) - prev ~= RXBUFLEN then
badstride = badstride + 1
end
prev = buf_addr(ring_ringb, i)
end
return ring_check(badown == 0 and badaddr == 0 and badlen == 0 and badstride == 0,
string.format("%s: 16 descriptors OWN-set, buffers 0x%06X + i*%d (own-bad %d, addr-bad %d, len-bad %d, stride-bad %d)",
what, want, RXBUFLEN, badown, badaddr, badlen, badstride))
end
-- The 32 bytes at the start of screen row 0. Enough to see a blit: the frame
-- ring mode delivers paints row 0 with 0xF0 bytes, so any paint changes them.
local function read_vram_head(n)
local chars = {}
for i = 0, n - 1 do
chars[#chars + 1] = string.char(prog:read_u8(VRAM_BASE + i * 8))
end
return table.concat(chars)
end
local function hex_bytes(s)
return (s:gsub(".", function(c) return string.format("%02X", c:byte()) end))
end
-- A VS frame whose header claims payload_len = 0xFFFF, i.e. 65551 bytes when the
-- slot is 24576. It can never complete, so the payload must reject it on the
-- header alone: RX_BAD ticks, nothing is painted, and the ring recovers.
local function oversize_frame()
local b = {}
local i
b[0] = 0x56
b[1] = 0x53
b[2] = 0x01
b[3] = 1
b[4] = 0x77
b[5] = 0x00
b[6] = 0xFF
b[7] = 0xFF -- payload_len = 65535
b[8] = DRAW_ROWS
b[9] = DRAW_STRETCH
b[10] = 0
b[11] = 0
local crc = crc_add(0xFFFF, b, 0, 12)
b[12] = crc & 0xFF
b[13] = (crc >> 8) & 0xFF
b[14] = 0
b[15] = 0
for i = 16, 16 + 1383 do
b[i] = 0xAA -- a body that would be visible if it painted
end
return b, 1400
end
local function dump_ring(timed_out)
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
local lines = {
string.format("RING_BASE=0x%X", ring_ringb),
string.format("RING_POS0=%d", ring_pos0),
string.format("RING_POS=%d", prog:read_u32(RX_RING_POS)),
string.format("SLOT_A=0x%X", a),
string.format("SLOT_B=0x%X", b),
string.format("RING_ARMED=%d", prog:read_u32(RX_RING_ARMED)),
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
string.format("RX_LAST_LEN=%d", prog:read_u32(RX_LAST_LEN)),
string.format("RX_SLOT=%d", prog:read_u32(RX_SLOT)),
string.format("RX_DGRAM=%d", prog:read_u32(RX_DGRAM)),
string.format("RX_RING_DROPS=%d", prog:read_u32(RX_RING_DROPS)),
string.format("RX_RING_ACC=%d", prog:read_u32(RX_RING_ACC)),
string.format("VRAM_ROW0_BEFORE=%s", ring_vram0 and hex_bytes(ring_vram0) or "-"),
string.format("VRAM_ROW0_AFTER=%s", hex_bytes(read_vram_head(32))),
string.format("RING_CHECKS_FAILED=%d", ring_fail),
string.format("TIMEOUT=%d", timed_out and 1 or 0),
}
for _, l in ipairs(ring_lines) do
lines[#lines + 1] = "CHECK " .. l
end
local okf = write_file(DUMP_DIR .. "/ring-status.txt",
table.concat(lines, "\n") .. "\n")
local ok = okf and not timed_out and ring_fail == 0
print(string.format("VID: ring: %d checks failed, %d datagrams consumed, RX_OK=%d RX_BAD=%d",
ring_fail, prog:read_u32(RX_DGRAM), prog:read_u32(RX_OK),
prog:read_u32(RX_BAD)))
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
machine:exit()
end
-- One call per emulated frame while mode ring is running. Returns true when the
-- run is over (the caller must not continue).
local function ring_step()
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
local ndg
if ring_phase == 0 then
if ring_phase_at == nil then
ring_phase_at = frames
end
if not ring_ready() then
if frames >= ring_phase_at + RING_WAIT then
ring_check(false, string.format(
"the payload published an RX ring (RX_RING_BASE=%s, POOL_PTR=%s, ST_INIT=%04X, heartbeat=%d)",
hex(prog:read_u32(RX_RING_BASE)),
hex(prog:read_u32(POOL_PTR)),
prog:read_u32(ST_INIT),
prog:read_u32(VID_ALIVE)))
dump_ring(true)
return true
end
return false
end
ring_ringb = prog:read_u32(RX_RING_BASE)
local _, pos0, armed0 = ring_state()
ring_pos0 = pos0
print(string.format("VID: frame %d: RX ring 0x%X armed on slot %d (pos %d), A=%s B=%s",
frames, ring_ringb, armed0, pos0, hex(a), hex(b)))
ring_check(a ~= 0 and (b - a) == SLOT_BYTES * 8,
"slot B follows slot A by SLOT_BYTES")
ring_check(armed0 == 0, "the ring starts armed on slot A")
ring_armed_for(a, "slot A")
-- One complete 128-row frame: 15 datagrams of 1450 bytes.
local f = make_frame(1, 0x0091, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN,
draw_row_byte)
ndg = inject_frame(f, DRAW_FRAME, DRAW_CHUNK)
ring_check(ndg == 15, string.format("the frame was delivered as 15 datagrams (got %s)",
tostring(ndg)))
ring_phase = 1
ring_phase_at = frames
return false
end
if ring_phase == 1 then
if prog:read_u32(RX_OK) < 1 then
if frames >= ring_phase_at + RING_WAIT then
print(string.format("VID: timed out waiting for the frame to be accepted (RX_OK=%d RX_BAD=%d RX_RING_ACC=%d)",
prog:read_u32(RX_OK), prog:read_u32(RX_BAD),
prog:read_u32(RX_RING_ACC)))
dump_ring(true)
return true
end
return false
end
ring_dgrams = prog:read_u32(RX_DGRAM)
ring_check(prog:read_u32(RX_SLOT) == 0, "the accepted frame came in slot A")
ring_check(prog:read_u32(RX_LAST_LEN) == DRAW_PLEN,
string.format("RX_LAST_LEN is the frame's payload_len (%d)", DRAW_PLEN))
ring_check(prog:read_u32(RX_RING_ACC) == 0,
"the frame left no bytes in progress")
ring_check(ring_dgrams >= 15, string.format("the consumer took all 15 datagrams (%d)", ring_dgrams))
ring_check(prog:read_u32(RX_RING_ARMED) == 1,
"the completion moved the write pointer to slot B")
ring_armed_for(b, "slot B after the frame")
ring_vram0 = read_vram_head(32)
-- Now the impossible frame, into the slot the ring is armed on (B).
local big, blen = oversize_frame()
ndg = inject_frame(big, blen, DRAW_CHUNK)
ring_check(ndg == 1, "the oversize frame was delivered as one datagram")
ring_phase = 2
ring_phase_at = frames
return false
end
if ring_phase == 2 then
if prog:read_u32(RX_BAD) < 1 then
if frames >= ring_phase_at + RING_WAIT then
print(string.format("VID: timed out waiting for the oversize frame to be rejected (RX_BAD=%d RX_RING_ACC=%d)",
prog:read_u32(RX_BAD), prog:read_u32(RX_RING_ACC)))
dump_ring(true)
return true
end
return false
end
ring_check(prog:read_u32(RX_BAD) == 1, "RX_BAD counts the oversize frame")
ring_check(prog:read_u32(RX_OK) == 1, "RX_OK did not move: the oversize frame was not accepted")
local after = read_vram_head(32)
ring_check(ring_vram0 ~= nil and after == ring_vram0,
"the screen did not change: the oversize frame was not blitted")
ring_check(prog:read_u32(RX_RING_ARMED) == 0,
"the rejection re-armed the ring on slot A")
ring_armed_for(a, "slot A after the rejection")
ring_check(prog:read_u32(RX_RING_DROPS) == 0,
"no frame was dropped for want of descriptors")
local pool_ok = inspect_pool()
ring_check(pool_ok, "the startup allocation is still the one under test")
dump_ring(false)
return true
end
return false
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
-- that a pixel mismatch would blame the blit for a transport failure. The
-- status file carries TIMEOUT=1 and the checker stops there.
local function dump_draw(timed_out)
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)
-- One frame's datagrams, consumed: the ring's replacement for Task 4's
-- "the ready flag is clear" signal.
local dgrams = prog:read_u32(RX_DGRAM)
local armed = prog:read_u32(RX_RING_ARMED)
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 parse_frames = -1
local ok = okv and okf
if rx_ok_at ~= nil then
parse_frames = rx_ok_at - draw_at
end
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_DGRAM=%d\nRING_ARMED=%d\nSENTINEL=%d\n"
.. "TIMEOUT=%d\nPARSE_FRAMES=%d\n",
rx_ok, rx_bad, os_touch, dgrams, armed, sent, timed_out and 1 or 0,
parse_frames))
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_DGRAM=%d SENTINEL=%04X",
rx_ok, rx_bad, os_touch, dgrams, sent))
if parse_frames >= 0 then
print(string.format("VID: receive-to-draw cost: last datagram delivered at frame %d, RX_OK first read at frame %d = %d emulated frames",
draw_at, rx_ok_at, parse_frames))
else
print("VID: receive-to-draw cost: no RX_OK observed (the frame was never accepted)")
end
if timed_out or rx_ok ~= 1 or rx_bad ~= 0 or dgrams ~= 15 or os_touch ~= 0 then
ok = false
end
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
machine:exit()
end
function inspect_pool()
local ptr = prog:read_u32(POOL_PTR)
local freeb = prog:read_u32(POOL_FREEB)
local freea = prog:read_u32(POOL_FREEA)
local alive = prog:read_u32(VID_ALIVE)
local pbyte = math.floor(ptr / 8)
local ok = true
print(string.format("VID: marker page %s: POOL_PTR=%s POOL_FREE(before)=%d POOL_FREE(after)=%d",
hex(MARK_PAGE), hex(ptr), freeb, freea))
print(string.format("VID: heartbeat %d (was %s) -- payload %s",
alive, alive_last and tostring(alive_last) or "unset",
(in_payload(cpu.state['PC'].value) or direct) and "still running" or "NOT RUNNING"))
if alive_last ~= nil and alive == alive_last then
ok = false
print("VID: heartbeat did not advance -> FAIL")
end
if ptr == 0 then
print(string.format("VID: pool pointer %s -> FAIL", hex(ptr)))
ok = false
elseif pbyte < 0x100000 or pbyte > 0x400000 then
print(string.format("VID: pool pointer %s (byte %s) outside 100000..400000 -> FAIL",
hex(ptr), hex(pbyte, 6)))
ok = false
else
print(string.format("VID: pool pointer in range: %s (byte %s) -> PASS",
hex(ptr), hex(pbyte, 6)))
end
-- Alignment: a 32-bit C value occupies two 16-bit words on the TMS34010,
-- so the storage needs 16-bit-word alignment. The firmware allocator
-- returns word-aligned bit pointers (observed 0x0090C8D0 = byte
-- 0x12191A, ptr % 16 == 0 but ptr % 32 == 16); a literal ptr % 32 == 0
-- would reject every correct allocation, so test the real guarantee.
if ptr ~= 0 then
if (ptr % 16) ~= 0 then
print(string.format("VID: pool pointer %s not 16-bit-word aligned -> FAIL", hex(ptr)))
ok = false
else
print(string.format("VID: pool pointer %s is 16-bit-word aligned -> PASS", hex(ptr)))
end
end
if (freeb & 1) == 1 then
-- malloc failed: the low bit of the before value is the static flag.
local before = freeb - 1
if STATIC_PTR ~= 0 and ptr == STATIC_PTR then
print(string.format("VID: static fallback at %s, flag set, heap untouched (%d -> %d bytes) -> PASS",
hex(ptr), before, freea))
else
print(string.format("VID: static fallback wrong: pointer %s, expected %s -> FAIL",
hex(ptr), hex(STATIC_PTR)))
ok = false
end
else
-- A real pool must not sit on top of the payload's own .text or .bss.
-- Both are above the poke origin, so "below the origin" covers both.
local origin_byte = math.floor(ORIGIN / 8)
if pbyte >= origin_byte then
print(string.format("VID: pool byte %s is not below the payload origin byte %s -> FAIL",
hex(pbyte, 6), hex(origin_byte, 6)))
ok = false
else
print(string.format("VID: pool byte %s is below the payload origin byte %s -> PASS",
hex(pbyte, 6), hex(origin_byte, 6)))
end
local drop = freeb - freea
local delta = drop - POOL_BYTES
if delta < 0 then delta = -delta end
if drop > 0 and delta <= 64 then
print(string.format("VID: heap free before/after differ by %d bytes (POOL_BYTES=%d, header delta=%d) -> PASS",
drop, POOL_BYTES, drop - POOL_BYTES))
else
print(string.format("VID: heap free before/after differ by %d bytes, expected ~%d -> FAIL",
drop, POOL_BYTES))
ok = false
end
end
return ok
end
local function inspect_rx()
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
local ok = true
if not injected or not injected_bad then
print("VID: both frames were not delivered -> FAIL")
return false
end
print(string.format("VID: slots A=%s B=%s (delta %d bits = %d bytes, expected %d)",
hex(a), hex(b), b - a, (b - a) / 8, SLOT_BYTES))
if a == 0 or (b - a) ~= SLOT_BYTES * 8 then
print("VID: slot B does not follow slot A by SLOT_BYTES -> FAIL")
ok = false
end
if readback_a and readback_b then
print("VID: both delivered frames read back byte for byte from their buffers -> PASS")
else
print(string.format("VID: delivered frame readback mismatch (A=%s B=%s) -> FAIL",
tostring(readback_a), tostring(readback_b)))
ok = false
end
local oka = prog:read_u32(RX_OK)
local bad = prog:read_u32(RX_BAD)
if oka == 1 and bad == 1 then
print(string.format("VID: RX_OK=%d RX_BAD=%d -> PASS", oka, bad))
else
print(string.format("VID: RX_OK=%d RX_BAD=%d -> FAIL (one valid frame, one corrupt)", oka, bad))
ok = false
end
local last = prog:read_u32(RX_LAST_LEN)
if last == RX_PLEN then
print(string.format("VID: RX_LAST_LEN=%d = payload_len of the accepted frame -> PASS", last))
else
print(string.format("VID: RX_LAST_LEN=%d, expected %d -> FAIL", last, RX_PLEN))
ok = false
end
local slot = prog:read_u32(RX_SLOT)
if slot == 0 then
print("VID: RX_SLOT=0 (slot A, where the valid frame went) -> PASS")
else
print(string.format("VID: RX_SLOT=%d, expected 0 (slot A) -> FAIL", slot))
ok = false
end
-- The ring is the receive path now: both frames were datagrams, and after
-- the rejected one the payload must have re-armed slot A with nothing in
-- progress.
local dg = prog:read_u32(RX_DGRAM)
if dg >= 2 then
print(string.format("VID: %d datagrams consumed (one per frame) -> PASS", dg))
else
print(string.format("VID: only %d datagrams consumed, expected 2 -> FAIL", dg))
ok = false
end
if prog:read_u32(RX_RING_ARMED) == 0 and prog:read_u32(RX_RING_ACC) == 0 then
print("VID: the ring is armed on slot A with no frame in progress -> PASS")
else
print(string.format("VID: ring armed=%d acc=%d, expected armed=0 acc=0 -> FAIL",
prog:read_u32(RX_RING_ARMED), prog:read_u32(RX_RING_ACC)))
ok = false
end
return ok
end
emu.register_frame_done(function()
frames = frames + 1
local pcv = cpu.state['PC'].value
if frames == AT and not loaded then
loaded = true
load_payload()
end
if loaded and not entered and in_payload(pcv) then
entered = true
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))
end
if loaded and not entered and armed and frames == AT + 10 then
enter_direct("breakpoint at " .. hex(ENTRY_PC) .. " did not fire")
entered = true
alive0 = prog:read_u32(VID_ALIVE)
end
-- Mode rx: the valid frame first, then the same frame with one header byte
-- flipped. Both go through the ring -- one datagram each -- and the second
-- waits for RX_OK, because the payload only re-arms the other slot once it
-- has finished with the first frame.
if entered and MODE == "rx" then
if not injected and ring_ready() then
local ndg = inject_frame(rx_valid, RX_FRAME, DRAW_CHUNK)
if ndg ~= nil then
local base, pos = ring_state()
injected = true
local okb, badb = verify_chunk(base + pos * RXBUFLEN * 8, rx_valid, 0, RX_FRAME)
readback_a = okb
print(string.format("VID: frame %d: delivered the valid frame as %d datagram(s) into slot %d (rx_ok=%s, %d bad)",
frames, ndg, prog:read_u32(RX_RING_ARMED), okb and "OK" or "FAILED", badb))
end
elseif injected and not injected_bad and prog:read_u32(RX_OK) >= 1 then
local ndg = inject_frame(rx_corrupt, RX_FRAME, DRAW_CHUNK)
if ndg ~= nil then
local base, pos = ring_state()
injected_bad = true
local okb, badb = verify_chunk(base + pos * RXBUFLEN * 8, rx_corrupt, 0, RX_FRAME)
readback_b = okb
print(string.format("VID: frame %d: delivered the header-flipped frame as %d datagram(s) into slot %d (rx_ok=%s, %d bad)",
frames, ndg, prog:read_u32(RX_RING_ARMED), okb and "OK" or "FAILED", badb))
end
end
end
-- Mode ring: the transport's structural check (see ring_step()).
if entered and MODE == "ring" then
if ring_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:
--
-- * the first frame at which RX_OK reads >= 1 is the PARSE COST
-- (the last datagram completed -> first RX_OK read, in emulated frames;
-- RX_OK is published after the paint, so this is the whole
-- receive-and-draw of one frame, not the parse alone);
-- * RX_OK having moved is the payload done with the frame -- that is the
-- screen to dump.
--
-- R15: the wait is bounded, and hitting the bound is reported as a
-- timeout, never as a pixel mismatch.
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 then
if rx_ok_at == nil and prog:read_u32(RX_OK) >= 1 then
rx_ok_at = frames
end
if rx_ok_at ~= nil and frames > rx_ok_at then
dump_draw(false)
return
end
if frames >= draw_at + DRAW_WAIT then
print(string.format("VID: timed out waiting for the frame: delivered at frame %d, RX_OK=%d after %d emulated frames",
draw_at, prog:read_u32(RX_OK), DRAW_WAIT))
dump_draw(true)
return
end
end
-- Stream mode: play the sender, one frame per emulated frame whenever the
-- ring has been re-armed. The cadence is NOT the sender's 30 fps: MAME's
-- frame is not the wire (R11), and what this run has to show is that every
-- delivered frame is accepted and that the terminal reports on time, not
-- that a wall clock was kept.
if entered and MODE == "stream" and not stream_prefilled then
stream_prefilled = stream_prefill()
end
if MODE == "stream" and stream_prefilled then
if announced < STREAM_FRAMES then
inject_stream()
elseif eof_at == nil then
-- The sender has stopped. Without this the last report would be
-- the 192nd frame's; RX_EOF is how a finite run asks for the
-- final count (frame_parse.h; harness-only).
prog:write_u32(RX_EOF, 1)
eof_at = frames
print(string.format("VID: frame %d: %d frames delivered; RX_EOF set, waiting for the final status",
frames, announced))
end
capture_status()
local last = captures[#captures]
if eof_at ~= nil and last ~= nil and last.frames_rx >= STREAM_FRAMES then
-- The final report is in. Wait for the chip to hand the last
-- descriptor back before calling the transmit done: a descriptor
-- the chip never took would mean the payload armed it for nothing.
-- The model completes it in ~46 us emulated (see STREAM_DRAIN);
-- the wait is a safety net for a slow host, not a delay.
if drain_from == nil then
drain_from = frames
end
local tp = prog:read_u32(ST_TMLAST)
local tmd1 = tp ~= 0 and prog:read_u16(tp) or 0xFFFF
if (tmd1 & 0x8000) == 0 then
print(string.format("VID: frame %d: last TX descriptor completed (TMD1=%04X, OWN clear)",
frames, tmd1))
dump_stream(false)
return
end
if frames >= drain_from + STREAM_DRAIN then
print(string.format("VID: timed out %d emulated frames after the final status: last TX descriptor still has OWN set (TMD1=%04X at %s)",
STREAM_DRAIN, tmd1, hex(tp)))
dump_stream(true)
return
end
end
if (eof_at ~= nil and frames >= eof_at + STREAM_WAIT)
or frames >= STOP_AT then
print(string.format("VID: timed out waiting for the final status: %d frames announced, %d status frames, last frames_rx=%s, RX_EOF set at %s",
announced, #captures,
last and tostring(last.frames_rx) or "none",
eof_at and tostring(eof_at) or "never"))
dump_stream(true)
return
end
end
if entered and frames % 10 == 0 and frames < STOP_AT then
alive_last = prog:read_u32(VID_ALIVE)
end
if MODE == "ring" and frames >= STOP_AT then
print(string.format("VID: ring mode did not finish (phase %d) by frame %d",
ring_phase, frames))
dump_ring(true)
return
end
if frames >= STOP_AT and MODE ~= "stream" and MODE ~= "ring"
and not (MODE == "draw" and draw_at ~= nil) then
if not entered then
print("VID: payload never entered -> FAIL")
print("VID: RESULT FAIL")
else
local ok, result = pcall(inspect_pool)
if not ok then
print("VID: ERROR " .. tostring(result))
print("VID: RESULT FAIL")
else
if MODE == "rx" then
local ok2, result2 = pcall(inspect_rx)
if not ok2 then
print("VID: ERROR " .. tostring(result2))
result = false
else
result = result and result2
end
end
print(result and "VID: RESULT PASS" or "VID: RESULT FAIL")
end
end
machine:exit()
end
end, "vt1200-vidstream")
dbg.visible_cpu = cpu
dbg.execution_state = 'run'