forked from arti/vt1200
1065 lines
39 KiB
Lua
1065 lines
39 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 writes a VALID frame
|
|
-- into slot A and the same frame with one header byte flipped into slot B,
|
|
-- raises each slot's ready flag (the role the LANCE receive interrupt plays on
|
|
-- the real transport), and asserts RX_OK == 1 && RX_BAD == 1, that RX_LAST_LEN
|
|
-- 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.
|
|
--
|
|
-- Mode "stream" (Task 6) is the status frame. It plays the *sender*: 200 strip
|
|
-- frames, one per emulated frame whenever a slot is free, alternating slots.
|
|
-- 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.
|
|
--
|
|
-- 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
|
|
-- AGENTS.md rule 4 requires to be quoted and independently read. The DMA path
|
|
-- is proven in Task 7, on the unit.
|
|
--
|
|
-- 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_* cells below, is in harness/udpstream/frame_parse.h.
|
|
--
|
|
-- Env:
|
|
-- VT1200_MODE alloc (default), rx, draw or stream
|
|
-- VID_AT load/entry frame (default 2400, the OS phase)
|
|
-- VID_END inspection/exit frame (default VID_AT + 60)
|
|
-- VID_DUMP_DIR where draw and stream modes write their dumps
|
|
-- (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_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_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
|
|
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 the injection interface (frame_parse.h).
|
|
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
|
|
local RX_READY_A = MARK_PAGE + 0x68 * 8
|
|
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
|
|
|
|
-- 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 injected frame: 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.
|
|
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.
|
|
local DRAW_ROWS = 128
|
|
local DRAW_STRETCH = 8
|
|
local DRAW_PLEN = DRAW_ROWS * RX_ROW_BYTES
|
|
local DRAW_FRAME = 16 + DRAW_PLEN
|
|
|
|
-- 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 status 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.
|
|
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" 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
|
|
|
|
local function poke(base, bytes, n)
|
|
for i = 0, n - 1 do
|
|
prog:write_u8(base + i * 8, bytes[i])
|
|
end
|
|
end
|
|
|
|
local function verify(base, bytes, n)
|
|
local bad = 0
|
|
for i = 0, n - 1 do
|
|
if prog:read_u8(base + i * 8) ~= bytes[i] then
|
|
bad = bad + 1
|
|
end
|
|
end
|
|
return bad == 0, bad
|
|
end
|
|
|
|
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 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
|
|
|
|
-- 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
|
|
-- malloc has not finished and the caller retries next frame.
|
|
local function inject()
|
|
local a = prog:read_u32(RX_SLOT_A)
|
|
local b = prog:read_u32(RX_SLOT_B)
|
|
|
|
if a == 0 or b == 0 then
|
|
return false
|
|
end
|
|
|
|
local fa = make_frame(1, 0x0021, RX_ROWS, RX_STRETCH, RX_PLEN)
|
|
poke(a, fa, RX_FRAME)
|
|
readback_a = verify(a, fa, RX_FRAME)
|
|
|
|
local fb = make_frame(1, 0x0022, RX_ROWS, RX_STRETCH, RX_PLEN)
|
|
-- Corrupt a 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.
|
|
fb[4] = fb[4] ~ 0x01 -- frame_no low byte, CRC left alone
|
|
poke(b, fb, RX_FRAME)
|
|
readback_b = verify(b, fb, RX_FRAME)
|
|
|
|
prog:write_u32(RX_LEN_A, RX_FRAME)
|
|
prog:write_u32(RX_LEN_B, RX_FRAME)
|
|
prog:write_u32(RX_READY_A, 1)
|
|
prog:write_u32(RX_READY_B, 1)
|
|
|
|
print(string.format("VID: frame %d: injected slot A (%s, %d B, valid) and slot B (%s, %d B, one header byte flipped)",
|
|
frames, hex(a), RX_FRAME, hex(b), RX_FRAME))
|
|
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 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()
|
|
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
|
|
|
|
-- --- Task 6: stream mode ---------------------------------------------------
|
|
--
|
|
-- The harness plays the sender here, and the terminal's status frame is the
|
|
-- thing under test. Nothing in this section pretends to be a NIC: the frames
|
|
-- are poked into the slots the payload published, exactly as in modes rx and
|
|
-- draw, because the emulated LANCE has no network interface (R11).
|
|
|
|
-- The 16-byte VS header for one frame number, sealed over its first 12 bytes
|
|
-- (ruling R14). The body of each slot 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 announced = 0
|
|
local eof_at = nil
|
|
local drain_from = nil
|
|
local st_sent_seen = 0
|
|
local bad_captures = 0
|
|
local captures = {}
|
|
|
|
-- Fill both slots' bodies once. Slot A holds the draw-mode pattern, slot B
|
|
-- its inverse, so a decoder that read the wrong slot (or cached one body) can
|
|
-- never pass a frame check. The headers are rewritten per frame.
|
|
local function stream_prefill()
|
|
local a = prog:read_u32(RX_SLOT_A)
|
|
local b = prog:read_u32(RX_SLOT_B)
|
|
|
|
if a == 0 or b == 0 then
|
|
return false
|
|
end
|
|
|
|
local fa = make_frame(1, 0, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN, draw_row_byte)
|
|
poke(a, fa, DRAW_FRAME)
|
|
local fb = make_frame(1, 0, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN,
|
|
function(i)
|
|
if draw_row_byte(i) == 0xF0 then
|
|
return 0x0F
|
|
end
|
|
return 0xFF
|
|
end)
|
|
poke(b, fb, DRAW_FRAME)
|
|
print(string.format("VID: frame %d: stream slots prefilled (A=%s, B=%s, %d B each); sender will announce %d frames",
|
|
frames, hex(a), hex(b), DRAW_FRAME, STREAM_FRAMES))
|
|
return true
|
|
end
|
|
|
|
-- Announce one frame in whichever slot is free. A busy slot is not
|
|
-- overwritten: the payload sets the ready flag and clears it when it is done
|
|
-- with the slot, so this is how the sender learns the terminal's pace.
|
|
local function inject_stream()
|
|
local slot = announced % 2
|
|
local ready = prog:read_u32(slot == 0 and RX_READY_A or RX_READY_B)
|
|
|
|
if ready ~= 0 then
|
|
return false
|
|
end
|
|
|
|
local base = prog:read_u32(slot == 0 and RX_SLOT_A or RX_SLOT_B)
|
|
if base == 0 then
|
|
return false
|
|
end
|
|
|
|
poke(base, header_bytes(1, announced & 0xFFFF, DRAW_ROWS, DRAW_STRETCH,
|
|
DRAW_PLEN), 16)
|
|
if slot == 0 then
|
|
prog:write_u32(RX_LEN_A, DRAW_FRAME)
|
|
prog:write_u32(RX_READY_A, 1)
|
|
else
|
|
prog:write_u32(RX_LEN_B, DRAW_FRAME)
|
|
prog:write_u32(RX_READY_B, 1)
|
|
end
|
|
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
|
|
|
|
-- 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 report
|
|
-- what the payload itself says (one accepted frame, no rejects, no repaint by
|
|
-- the OS) plus the measured parse cost.
|
|
--
|
|
-- `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)
|
|
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 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_READY_A=%d\nSENTINEL=%d\n"
|
|
.. "TIMEOUT=%d\nPARSE_FRAMES=%d\n",
|
|
rx_ok, rx_bad, os_touch, ready, 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_READY_A=%d SENTINEL=%04X",
|
|
rx_ok, rx_bad, os_touch, ready, sent))
|
|
if parse_frames >= 0 then
|
|
print(string.format("VID: parse cost: RX_READY_A written at frame %d, RX_OK first read at frame %d = %d emulated frames",
|
|
draw_at, rx_ok_at, parse_frames))
|
|
else
|
|
print("VID: parse cost: no RX_OK observed (the frame was never accepted)")
|
|
end
|
|
|
|
if timed_out or 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)
|
|
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 rxa = prog:read_u32(RX_READY_A)
|
|
local rxb = prog:read_u32(RX_READY_B)
|
|
local ok = true
|
|
|
|
if not injected then
|
|
print("VID: no frame was ever injected -> 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 injected frames read back byte for byte -> PASS")
|
|
else
|
|
print(string.format("VID: injected 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
|
|
|
|
if rxa == 0 and rxb == 0 then
|
|
print("VID: the payload cleared both ready flags -> PASS")
|
|
else
|
|
print(string.format("VID: ready flags A=%d B=%d, both must be cleared -> FAIL", rxa, rxb))
|
|
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
|
|
|
|
-- Inject as soon as the payload has entered AND published the slots: the
|
|
-- slot addresses are written straight after the malloc, so a non-zero
|
|
-- value there means the receive loop is about to run.
|
|
if entered and MODE == "rx" and not injected then
|
|
if prog:read_u32(RX_SLOT_A) ~= 0 and prog:read_u32(RX_SLOT_B) ~= 0 then
|
|
injected = inject()
|
|
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
|
|
-- (RX_READY_A write -> first RX_OK read, in emulated frames);
|
|
-- * RX_READY_A == 0 is the payload done with the slot, i.e. after
|
|
-- paint_frame() returned -- 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 prog:read_u32(RX_READY_A) == 0
|
|
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: announced 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 announced frame per emulated frame
|
|
-- whenever a slot is free. 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
|
|
-- announced 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, like RX_READY_*).
|
|
prog:write_u32(RX_EOF, 1)
|
|
eof_at = frames
|
|
print(string.format("VID: frame %d: %d frames announced; 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 frames >= STOP_AT and MODE ~= "stream"
|
|
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'
|