-- 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 payload 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. -- -- 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 or draw -- VID_AT load/entry frame (default 2400, the OS phase) -- VID_END inspection/exit frame (default VID_AT + 60) -- VID_DUMP_DIR where draw mode writes vram-draw.bin / frame-draw.bin / -- draw-status.txt (default /tmp/opencode) -- VID_SENTINEL_CLOBBER=1 draw mode only: overwrite the VRAM canary word -- before the frame is announced, so the payload must count -- an OS touch. The negative control for OS_TOUCH. -- VID_PAYLOAD raw .text image (default harness/udpstream/build/vidstream.bin) -- VID_ORIGIN payload load (poke) bit address (default 0x018C0000) -- VID_ENTRY_PTR _start bit address from vidstream.env (default VID_ORIGIN) -- 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") local STOP_AT = tonumber(os.getenv("VID_END") or tostring(AT + 60)) 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 -- 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 -- Screen memory: bit 0x04000000, 256-byte row pitch (the real unit's, see -- harness/c-hello/README.md; MAME's own renderer uses a different pitch and is -- not what this check reads). local VRAM_BASE = 0x04000000 local VRAM_ROW_BITS = 0x800 local VRAM_ROWS = 1024 local VRAM_ROW_BYTES = 256 local DUMP_DIR = os.getenv("VID_DUMP_DIR") or "/tmp/opencode" if MODE ~= "alloc" and MODE ~= "rx" and MODE ~= "draw" then print("VID ERROR: mode '" .. MODE .. "' is not implemented") machine:exit() return 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. 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) crc = crc_add(crc, b, 16, plen) 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
` 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) fb[16 + 100] = fb[16 + 100] ~ 0x01 -- payload byte flipped, 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 payload 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 function draw_row_byte(i) if (i // RX_ROW_BYTES) % 3 == 2 then return 0x00 end return 0xF0 end local function inject_draw() local a = prog:read_u32(RX_SLOT_A) if a == 0 then return false end local f = make_frame(1, 0x0051, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN, draw_row_byte) poke(a, f, DRAW_FRAME) draw_bytes = f prog:write_u32(RX_LEN_A, DRAW_FRAME) -- Negative control for the OS-repaint canary: clobber the sentinel word -- (last screen row, column 1280) just before the payload is told a frame -- is ready. The first blit must then count an OS touch. Off by default; -- see VID_SENTINEL_CLOBBER in the env list. if os.getenv("VID_SENTINEL_CLOBBER") then prog:write_u16(VRAM_BASE + (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280, 0x1234) print(string.format("VID: frame %d: sentinel clobbered to 1234 (canary control)", frames)) end prog:write_u32(RX_READY_A, 1) print(string.format("VID: frame %d: injected slot A (%s, %d B, valid, %d rows x %d stretch)", frames, hex(a), DRAW_FRAME, DRAW_ROWS, DRAW_STRETCH)) local okb, badb = verify(a, f, DRAW_FRAME) print(string.format("VID: frame %d: draw frame readback %s (%d bad)", frames, okb and "OK" or "FAILED", badb)) return true end -- Read the screen as memory: 1024 rows x 256 bytes from bit 0x04000000. -- read_u16 is used rather than read_u32 so the byte order (a TMS34010 word is -- little-endian on the wire, low byte first) is explicit rather than assumed. local function read_vram() local rows = {} for y = 0, VRAM_ROWS - 1 do local off = VRAM_BASE + y * VRAM_ROW_BITS local chars = {} for w = 0, VRAM_ROW_BYTES // 2 - 1 do local v = prog:read_u16(off + w * 16) chars[#chars + 1] = string.char(v & 0xFF, (v >> 8) & 0xFF) end rows[#rows + 1] = table.concat(chars) end return table.concat(rows) end local function write_file(path, s) local f, err = io.open(path, "wb") if not f then print("VID ERROR: cannot write " .. path .. ": " .. tostring(err)) return false end f:write(s) f:close() return true end -- Dump the screen and the payload's record for vidstream_check.py, then end -- the run. The pass/fail for pixels belongs to the checker; here we only -- report what the payload itself says (one accepted frame, no rejects, no -- repaint by the OS). local function dump_draw() local vram = read_vram() local chars = {} for i = 0, DRAW_FRAME - 1 do chars[#chars + 1] = string.char(draw_bytes[i]) end local frame = table.concat(chars) local okv = write_file(DUMP_DIR .. "/vram-draw.bin", vram) local okf = write_file(DUMP_DIR .. "/frame-draw.bin", frame) local rx_ok = prog:read_u32(RX_OK) local rx_bad = prog:read_u32(RX_BAD) local os_touch = prog:read_u32(OS_TOUCH) local ready = prog:read_u32(RX_READY_A) local alive = prog:read_u32(VID_ALIVE) local ptr = prog:read_u32(POOL_PTR) local slota = prog:read_u32(RX_SLOT_A) local pcv = cpu.state['PC'].value local sent = prog:read_u16(VRAM_BASE + (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280) local ok = okv and okf print(string.format("VID: heartbeat %d, PC=%s (%s)", alive, hex(pcv), in_payload(pcv) and "inside the payload" or "OUTSIDE the payload")) print(string.format("VID: POOL_PTR=%s RX_SLOT_A=%s (an unchanged pointer means " .. "the payload did not re-enter main)", hex(ptr), hex(slota))) write_file(DUMP_DIR .. "/draw-status.txt", string.format( "RX_OK=%d\nRX_BAD=%d\nOS_TOUCH=%d\nRX_READY_A=%d\nSENTINEL=%d\n", rx_ok, rx_bad, os_touch, ready, sent)) print(string.format("VID: frame %d: screen dumped to %s/vram-draw.bin (%d B), frame to %s/frame-draw.bin (%d B)", frames, DUMP_DIR, #vram, DUMP_DIR, DRAW_FRAME)) print(string.format("VID: RX_OK=%d RX_BAD=%d OS_TOUCH=%d RX_READY_A=%d SENTINEL=%04X", rx_ok, rx_bad, os_touch, ready, sent)) if rx_ok ~= 1 or rx_bad ~= 0 or ready ~= 0 or os_touch ~= 0 then ok = false end print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL") machine:exit() end local function inspect_pool() local ptr = prog:read_u32(POOL_PTR) local freeb = prog:read_u32(POOL_FREEB) 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 consume the slot -- (it clears RX_READY_A) and give it a few frames of slack before reading -- the screen back. The timeout is generous on purpose: fp_parse() runs a -- BITWISE CRC, and a 20 KB frame costs it ~40 emulated frames (measured), -- so an unlucky 10-frame wait would read the screen before the blit. -- A payload that never takes the frame still gets its screen dumped, so -- the checker reports the truth instead of the run hanging. if entered and MODE == "draw" and draw_at == nil then if inject_draw() then draw_at = frames end end if MODE == "draw" and draw_at ~= nil and frames > draw_at + 1 then if (prog:read_u32(RX_READY_A) == 0 and frames >= draw_at + 5) or frames >= draw_at + 300 then dump_draw() return end end if entered and frames % 10 == 0 and frames < STOP_AT then alive_last = prog:read_u32(VID_ALIVE) end if frames >= STOP_AT 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'