diff --git a/harness/mame/run-vidstream.sh b/harness/mame/run-vidstream.sh index 1bbaac9..2034c18 100755 --- a/harness/mame/run-vidstream.sh +++ b/harness/mame/run-vidstream.sh @@ -6,24 +6,32 @@ # VT1200_MODE=rx harness/mame/run-vidstream.sh # alloc + the receive path # VT1200_MODE=draw harness/mame/run-vidstream.sh # alloc + rx + the blit # VT1200_MODE=stream harness/mame/run-vidstream.sh # the status back-channel +# VT1200_MODE=ring harness/mame/run-vidstream.sh # the RX ring's structure # STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback # -# Mode rx injects one valid frame and one corrupted frame with the debugger and -# asserts RX_OK == 1 && RX_BAD == 1 (see vidstream.lua and the task-4 report). +# Mode rx delivers one valid frame and one corrupted frame through the payload's +# own LANCE RX ring and asserts RX_OK == 1 && RX_BAD == 1 (see vidstream.lua and +# the task-7 report). # -# Mode draw injects one 128-row strip frame (stretch 8 = 1024 screen rows), lets -# the payload blit it, and dumps the screen and the delivered frame; this script -# then runs harness/mame/vidstream_check.py, which compares every screen row -# against its source row. The pixel verdict is the checker's, not the Lua's. +# Mode draw delivers one 128-row strip frame (stretch 8 = 1024 screen rows), +# lets the payload blit it, and dumps the screen and the delivered frame; this +# script then runs harness/mame/vidstream_check.py, which compares every screen +# row against its source row. The pixel verdict is the checker's, not the Lua's. # # Mode stream plays the sender: 200 strip frames, then RX_EOF so the terminal -# reports the final count. The payload takes the LANCE over for transmit and -# builds a KIND_STATUS frame every 64 accepted frames; the Lua reads those -# frames back from the payload's own transmit buffer and this script runs -# harness/mame/vidstream_status_check.py, which decodes them with the host -# sender's wire format. There is no network in this run: the emulated LANCE has -# no interface (ruling R11), so the evidence is the buffer's bytes plus the -# completed descriptor, and the wire path belongs to Task 7. +# reports the final count. The payload takes the LANCE over for receive and +# transmit and builds a KIND_STATUS frame every 64 accepted frames; the Lua +# reads those frames back from the payload's own transmit buffer and this +# script runs harness/mame/vidstream_status_check.py, which decodes them with +# the host sender's wire format. There is no network in this run: the emulated +# LANCE has no interface (ruling R11), so the evidence is the buffer's bytes +# plus the completed descriptor, and the wire path needs the unit. +# +# Mode ring asserts the transport's structure: 16 descriptors armed with OWN set +# for slot A, a completed frame moving them to slot B, and a header claiming +# more than SLOT_BYTES rejected without a blit (RX_BAD moves, RX_OK does not, +# the screen does not change). The verdict is in the Lua; the evidence is +# $LOGDIR/ring-status.txt. # # The run uses VT1200_SKIP_LEVEL1=1: the payload's heap is only initialised in # the OS phase. In the diagnostics menu the free counter (0x1E8BA0) and the @@ -78,6 +86,9 @@ fi if [ "$MODE" = "stream" ]; then rm -f "$LOGDIR/stream-status.bin" "$LOGDIR/stream-status.txt" fi +if [ "$MODE" = "ring" ]; then + rm -f "$LOGDIR/ring-status.txt" +fi # Absolute: the Lua runs with MAME's cwd, so a relative default only resolves # when the caller happens to be at the repo root. VID_PAYLOAD="$ROOT/harness/udpstream/build/vidstream.bin" diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua index c6d7895..dd784d6 100644 --- a/harness/mame/vidstream.lua +++ b/harness/mame/vidstream.lua @@ -18,56 +18,73 @@ -- * 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 "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 injects ONE valid 128-row strip +-- 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 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. +-- 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 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. +-- 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. -- --- 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. +-- 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_* cells below, is in harness/udpstream/frame_parse.h. +-- including the RX_* and RX_RING_* cells below, is in +-- harness/udpstream/frame_parse.h. -- -- Env: --- VT1200_MODE alloc (default), rx, draw or stream +-- 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 and stream modes write their dumps +-- 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 announced, so the payload must count +-- 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 @@ -76,6 +93,8 @@ -- (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) @@ -83,6 +102,7 @@ -- 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 @@ -104,6 +124,9 @@ local AT = tonumber(os.getenv("VID_AT") or "2400") 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") @@ -122,17 +145,24 @@ 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). +-- 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 -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 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 @@ -149,9 +179,20 @@ 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. +-- 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 @@ -159,18 +200,28 @@ 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. +-- 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 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. +-- 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 @@ -194,7 +245,8 @@ 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 +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 @@ -254,21 +306,15 @@ local function make_frame(kind, frame_no, rows, stretch, plen, fill) 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 +-- 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 @@ -291,6 +337,7 @@ 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 @@ -347,41 +394,135 @@ local function disarm_entry() 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) +-- --- 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. - if a == 0 or b == 0 then +-- 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 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)) + 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 @@ -424,31 +565,42 @@ local function draw_row_byte(i) 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. + -- (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 - 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) + + 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 @@ -486,13 +638,23 @@ 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). +-- 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 slot is written once; only this header --- changes per frame, so the harness is not slower than the payload. +-- (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 @@ -535,6 +697,7 @@ local function le32(t, off) end local stream_prefilled = false +local stream_body = nil local announced = 0 local eof_at = nil local drain_from = nil @@ -542,57 +705,49 @@ 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. +-- 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 - - 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)) + 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 --- 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. +-- 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 slot = announced % 2 - local ready = prog:read_u32(slot == 0 and RX_READY_A or RX_READY_B) + local ringb = prog:read_u32(RX_RING_BASE) + local base, pos = ring_state() + local hdr, k - if ready ~= 0 then - return false + if (rmd1_of(ringb, pos) & RMD1_OWN) == 0 then + return false -- not re-armed yet 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) + 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 @@ -696,10 +851,245 @@ local function dump_stream(timed_out) 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. +-- --- 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 @@ -717,7 +1107,10 @@ local function dump_draw(timed_out) 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) + -- 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) @@ -737,30 +1130,30 @@ local function dump_draw(timed_out) .. "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=%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, ready, sent, timed_out and 1 or 0, + 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_READY_A=%d SENTINEL=%04X", - rx_ok, rx_bad, os_touch, ready, sent)) + 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: parse cost: RX_READY_A written at frame %d, RX_OK first read at frame %d = %d emulated frames", + 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: parse cost: no RX_OK observed (the frame was never accepted)") + 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 ready ~= 0 or os_touch ~= 0 then + 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 -local function inspect_pool() +function inspect_pool() local ptr = prog:read_u32(POOL_PTR) local freeb = prog:read_u32(POOL_FREEB) local freea = prog:read_u32(POOL_FREEA) @@ -847,12 +1240,10 @@ 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") + if not injected or not injected_bad then + print("VID: both frames were not delivered -> FAIL") return false end @@ -864,9 +1255,9 @@ local function inspect_rx() end if readback_a and readback_b then - print("VID: both injected frames read back byte for byte -> PASS") + print("VID: both delivered frames read back byte for byte from their buffers -> PASS") else - print(string.format("VID: injected frame readback mismatch (A=%s B=%s) -> FAIL", + print(string.format("VID: delivered frame readback mismatch (A=%s B=%s) -> FAIL", tostring(readback_a), tostring(readback_b))) ok = false end @@ -896,10 +1287,22 @@ local function inspect_rx() ok = false end - if rxa == 0 and rxb == 0 then - print("VID: the payload cleared both ready flags -> PASS") + -- 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: ready flags A=%d B=%d, both must be cleared -> FAIL", rxa, rxb)) + 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 @@ -929,12 +1332,38 @@ emu.register_frame_done(function() 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() + -- 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 @@ -942,9 +1371,11 @@ emu.register_frame_done(function() -- 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. + -- (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. @@ -958,23 +1389,22 @@ emu.register_frame_done(function() 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 + 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: announced at frame %d, RX_OK=%d after %d emulated frames", + 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 announced frame per emulated frame - -- whenever a slot is free. The cadence is NOT the sender's 30 fps: MAME's + -- 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 - -- announced frame is accepted and that the terminal reports on time, not + -- 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() @@ -986,10 +1416,10 @@ emu.register_frame_done(function() 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_*). + -- final count (frame_parse.h; harness-only). 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", + print(string.format("VID: frame %d: %d frames delivered; RX_EOF set, waiting for the final status", frames, announced)) end capture_status() @@ -1033,7 +1463,14 @@ emu.register_frame_done(function() alive_last = prog:read_u32(VID_ALIVE) end - if frames >= STOP_AT and MODE ~= "stream" + 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") diff --git a/harness/mame/vidstream_check.py b/harness/mame/vidstream_check.py index ed087cd..fe99b34 100755 --- a/harness/mame/vidstream_check.py +++ b/harness/mame/vidstream_check.py @@ -106,8 +106,8 @@ def main(): # --- a timed-out wait is not a pixel verdict (ruling R15) ------------- if status.get("TIMEOUT", 0) != 0: print("FAIL: timed out waiting for the frame (RX_OK=%r, " - "RX_READY_A=%r: the payload did not finish with the slot)" - % (status.get("RX_OK"), status.get("RX_READY_A"))) + "RX_DGRAM=%r: the payload did not finish with the frame)" + % (status.get("RX_OK"), status.get("RX_DGRAM"))) print("vidstream_check: FAIL (timeout)") return 1 diff --git a/harness/udpstream/frame_parse.h b/harness/udpstream/frame_parse.h index 7628bdf..a0d69ac 100644 --- a/harness/udpstream/frame_parse.h +++ b/harness/udpstream/frame_parse.h @@ -38,6 +38,13 @@ * * cell page offset bit address meaning * ------------ ----------- ----------- -------------------------------- + * RX_RING_BASE +0x080 0x01810080 bit address of the RX ring + * RX_RING_POS +0x0A0 0x018100A0 next descriptor to consume (the + * chip's ring position) + * RX_RING_ACC +0x0C0 0x018100C0 bytes of the frame arriving + * RX_RING_ARMED +0x0E0 0x018100E0 slot the ring is armed on (0=A) + * RX_RING_DROPS +0x200 0x01810200 frames too big for the ring + * RX_DGRAM +0x240 0x01810240 datagrams consumed * POOL_PTR +0x00 0x01810100 bit pointer to the 244 KB pool * POOL_FREEB +0x20 0x01810120 heap free bytes before malloc * POOL_FREEA +0x40 0x01810140 heap free bytes after malloc @@ -54,20 +61,26 @@ * is at cell 0x44 -- free between the RX_* counters and the injection cells * below (ruling R12). * - * Debugger injection interface (Task 4; the harness plays the receive path): + * Debugger injection interface (Task 4; retired by Task 7's ring): * * RX_SLOT_A +0x200 0x01810300 bit address of slot A * RX_SLOT_B +0x220 0x01810320 bit address of slot B - * RX_READY_A +0x240 0x01810340 1 = slot A holds RX_LEN_A bytes - * RX_LEN_A +0x260 0x01810360 bytes in slot A - * RX_READY_B +0x280 0x01810380 1 = slot B holds RX_LEN_B bytes - * RX_LEN_B +0x2A0 0x018103A0 bytes in slot B * - * The RX_READY_x and RX_LEN_x cells are NOT part of the wire protocol. In the real - * transport the LANCE receive interrupt fills a slot and raises that slot's - * ready flag; MAME cannot hand this emulated LANCE an Ethernet frame (the - * driver attaches no network interface -- ruling R11), so the debugger writes - * the slot and raises the flag instead. The payload clears it. + * Task 4 received frames through a "slot ready" flag (RX_READY_A/B plus a byte + * count) that the debugger raised after poking a frame into a slot. Task 7 + * removed those cells: the payload now owns a real LANCE RX ring and the + * handshake is the descriptor's OWN bit, per descriptor, with the chip clearing + * it and the payload setting it again only after reading the buffer. There is + * no level-triggered flag for two writers to race on, which was Task 4's + * Important finding (a). + * + * The harness therefore plays the CHIP: it writes a datagram into the buffer of + * the descriptor at RX_RING_POS (and the ones after it, for a multi-datagram + * frame) and completes it the way am79c90.cpp does -- RMD3 = delivered length + * including the FCS, RMD1 = STP | ENP with OWN clear. RX_ARMED says which slot + * those descriptors point into. The two slot addresses above remain published + * because they are the payload's own record of where its buffers are. + * --------------------------------------------------------------------------- * * Task 6 adds one more cell of the same kind and the status record the harness * reads back: @@ -82,12 +95,12 @@ * ST_TMLAST +0x480 0x01810480 bit address of the last armed * TX descriptor's TMD1 word * - * RX_EOF is harness-only in exactly the same sense as RX_READY_x: it says "the - * sender has stopped announcing frames", which on a real wire is silence and - * carries no signal. It exists so a finite run can ask for a final status - * instead of waiting for the count to reach the next multiple of 64. The - * payload reports as soon as it sees it, and keeps reporting for each further - * accepted frame; nothing reads it in a continuous stream. + * RX_EOF is harness-only in exactly the same sense as the retired injection + * flag was: it says "the sender has stopped announcing frames", which on a real + * wire is silence and carries no signal. It exists so a finite run can ask for + * a final status instead of waiting for the count to reach the next multiple of + * 64. The payload reports as soon as it sees it, and keeps reporting for each + * further accepted frame; nothing reads it in a continuous stream. * --------------------------------------------------------------------------- * * K&R C only. The 1987 toolchain in build-stream.sh compiles this header, so @@ -130,12 +143,24 @@ #define OS_TOUCH MARK_CELL(0x44) +/* + * Task 7: the RX ring record. Task 4's injection cells (0x68..0x74) are gone; + * these take the free page prefix R12 left for the RX work, at the SAME 4-byte + * spacing every other cell uses (n increases by 4 per cell -- 0x10, 0x14, 0x18 + * and 0x1C are four cells, and a 32-bit store at 0x01810080 does not reach + * 0x01810088). Reading "n = 0x10..0x15" as five adjacent cells is the mistake + * this comment exists to prevent: those four cells would be ONE BYTE apart and + * the 32-bit stores would overlap into zeros. + */ +#define RX_RING_BASE MARK_CELL(0x10) +#define RX_RING_POS MARK_CELL(0x14) +#define RX_RING_ACC MARK_CELL(0x18) +#define RX_RING_ARMED MARK_CELL(0x1c) +#define RX_RING_DROPS MARK_CELL(0x40) +#define RX_DGRAM MARK_CELL(0x48) + #define RX_SLOT_A MARK_CELL(0x60) #define RX_SLOT_B MARK_CELL(0x64) -#define RX_READY_A MARK_CELL(0x68) -#define RX_LEN_A MARK_CELL(0x6c) -#define RX_READY_B MARK_CELL(0x70) -#define RX_LEN_B MARK_CELL(0x74) /* Task 6: the sender/keyframe control cell and the status record. */ #define RX_EOF MARK_CELL(0x78) diff --git a/harness/udpstream/test-stream.sh b/harness/udpstream/test-stream.sh new file mode 100755 index 0000000..7117a36 --- /dev/null +++ b/harness/udpstream/test-stream.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# test-stream.sh -- the unit measurement: drive the stream into the terminal and +# report what it received, one CSV line per 30-frame block. +# +# sudo ./test-stream.sh [rows] [stretch] +# +# sudo ./test-stream.sh enp197s0f3u1 08:00:2b:16:4b:5a 128 8 +# sudo ./test-stream.sh enp197s0f3u1 ff:ff:ff:ff:ff:ff 128 8 +# +# is the host NIC the terminal is plugged into (a dedicated port or a +# hub -- the terminal answers with broadcast status frames on the same wire), +# the destination address frames are sent to: the terminal's own +# 08:00:2b:16:4b:5a, or broadcast, which its LANCE accepts with an empty filter. +# [rows] and [stretch] are the strip geometry (default 128 x 8 = the whole +# screen, ruling R3); rows*stretch must be 1024 or the encoder refuses. +# +# WHAT IT RUNS. harness/udpstream/vidsend.py paced at 30 fps for 300 frames, +# with the terminal's status frames read back on the same interface. vidsend +# prints its own CSV (frame,elapsed_s,fps,bytes) every 30 frames and its status +# reports as prose; both are line-flushed, so this script captures them in one +# stream and merges them in arrival order into: +# +# frame,elapsed_s,fps,bytes,frames_rx,delivery_pct,drops,csr0 +# +# frames_rx/drops/csr0 are the LAST status the terminal had sent at that point +# (a status only arrives every 64 accepted frames, so the first two blocks +# repeat the same report). delivery_pct = 100 * frames_rx / frame, i.e. what +# the terminal says it has finished against what the host has sent -- it sits +# just under 100 % while a frame is in flight, and a real loss shows as a step. +# +# Exit status: vidsend.py's (0 only if a status frame arrived and the last one +# reports drops == 0 and crc_err == 0), or 2/3 for a missing prerequisite. +# +# WHAT THIS CANNOT SHOW FROM HERE: OS_TOUCH (spec 8.9's VRAM canary, marker bit +# 0x01810220) and the terminal's free heap before the run are on the terminal, +# not in the status frame. Read them on the terminal with its own debugger +# alongside a run and record them with this CSV; an OS repaint during the run +# also shows up here as a fall in delivery_pct. +# +# The three unit runs (plan Task 7 step 6): +# +# 1. sudo ./test-stream.sh 08:00:2b:16:4b:5a 128 8 +# 2. the same with the blit forced to PIXBLT L,XY instead of B,XY +# (harness/udpstream/vram.asm's vram_blit_ vs vram_b.asm's vram_blit_b_: +# swap the entry point in vidstream.c's blit_row and rebuild), to measure +# what the unit costs per destination word +# 3. a 6,300-frame soak (~3.5 min at 30 fps): FRAMES=6300 +# +# These need the physical terminal (ruling R4, R16) and have NOT been run. +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +SENDER="$HERE/vidsend.py" + +IFACE=${1:-} +MAC=${2:-} +ROWS=${3:-128} +STRETCH=${4:-8} +FRAMES=${FRAMES:-300} +FPS=${FPS:-30} +REPORT=${REPORT:-30} + +if [ -z "$IFACE" ] || [ -z "$MAC" ]; then + echo "usage: sudo $0 [rows] [stretch]" >&2 + echo " e.g. sudo $0 enp197s0f3u1 08:00:2b:16:4b:5a 128 8" >&2 + exit 2 +fi + +# Fail loudly rather than send nothing and call it a run. +if [ ! -f "$SENDER" ]; then + echo "test-stream.sh: no sender at $SENDER (this script drives vidsend.py)" >&2 + exit 3 +fi +if [ ! -d "/sys/class/net/$IFACE" ]; then + echo "test-stream.sh: no interface '$IFACE' on this host (have: $(ls /sys/class/net | tr '\n' ' '))" >&2 + exit 3 +fi +if [ "$(id -u)" -ne 0 ]; then + echo "test-stream.sh: raw Ethernet needs CAP_NET_RAW; re-run with sudo" >&2 + exit 3 +fi +case "$MAC" in + *:*:*:*:*:*) ;; + *) echo "test-stream.sh: '$MAC' is not a MAC address (aa:bb:cc:dd:ee:ff)" >&2 + exit 2 ;; +esac + +OUT=${OUT:-/tmp/opencode/test-stream.log} +mkdir -p "$(dirname "$OUT")" +echo "test-stream.sh: $IFACE -> $MAC, $ROWS rows x $STRETCH, $FRAMES frames at $FPS fps" +echo "test-stream.sh: capture $OUT" + +set +e +python3 "$SENDER" --iface "$IFACE" --dst "$MAC" --rows "$ROWS" \ + --stretch "$STRETCH" --fps "$FPS" --frames "$FRAMES" \ + --report-every "$REPORT" > "$OUT" 2>&1 +STATUS=$? +set -e +if [ "$STATUS" -ne 0 ] && [ ! -s "$OUT" ]; then + echo "test-stream.sh: vidsend.py failed with status $STATUS and wrote nothing" >&2 + exit "$STATUS" +fi + +# Merge the sender's CSV rows with the terminal's status reports, in the order +# they arrived. Both are printed line by line with a flush, so the order in +# this one file IS the chronological order; no timestamps are needed. +set +e +python3 - "$OUT" <<'PY' +import re, sys + +path = sys.argv[1] +row = re.compile(r"^(\d+),([\d.]+),([\d.]+),(\d+)$") +status = re.compile( + r"^status (\d+): frame_no=(\d+) frames_rx=(\d+) crc_err=(\d+) " + r"drops=(\d+) csr0=0x([0-9A-Fa-f]+) heap_free_kb=(\d+)$") + +last = None # the most recent status, as a dict +reports = 0 +print("frame,elapsed_s,fps,bytes,frames_rx,delivery_pct,drops,csr0") +bad = 0 +with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.rstrip("\n") + m = status.match(line) + if m: + reports += 1 + last = dict(frames_rx=int(m.group(3)), crc_err=int(m.group(4)), + drops=int(m.group(5)), csr0=int(m.group(6), 16)) + continue + m = row.match(line) + if not m: + continue + frame = int(m.group(1)) + if last is None: + print("%d,%s,%s,%s,,,," % (frame, m.group(2), m.group(3), + m.group(4))) + continue + pct = 100.0 * last["frames_rx"] / frame if frame else 0.0 + print("%d,%s,%s,%s,%d,%.1f,%d,0x%04X" + % (frame, m.group(2), m.group(3), m.group(4), + last["frames_rx"], pct, last["drops"], last["csr0"])) +if reports == 0: + print("test-stream.sh: no status frame arrived, so delivery and drops are " + "unknown (the terminal said nothing)", file=sys.stderr) + bad = 1 +sys.exit(bad) +PY +MERGE=$? +set -e + +echo "test-stream.sh: vidsend.py status $STATUS, merge status $MERGE" +echo "test-stream.sh: OS_TOUCH and the terminal's heap are NOT in this table:" \ + "read marker bit 0x01810220 and POOL_FREE* on the terminal and record them" \ + "with this CSV." +if [ "$STATUS" -ne 0 ] || [ "$MERGE" -ne 0 ]; then + exit 1 +fi +exit 0 diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c index 48d2560..edff933 100644 --- a/harness/udpstream/vidstream.c +++ b/harness/udpstream/vidstream.c @@ -2,32 +2,50 @@ * vidstream.c -- terminal side of the VT1200 video stream. * * Task 3 claimed the working set with ONE malloc at startup and published - * where it landed. Task 4 added the receive side: two 24 KB frame slots at the - * head of that pool, and a poll loop that hands each completed slot to - * fp_parse() (frame_parse.c, shared with the host unit test) and counts what + * where it landed. Task 4 added the receive representation: two 24 KB frame + * slots at the head of that pool and a poll loop that hands a completed frame + * to fp_parse() (frame_parse.c, shared with the host unit test) and counts what * it accepted. Task 5 blits an accepted strip frame to the screen: one * PIXBLT B,XY per source row, repeated down the row `stretch` times by * SPTCH=0 (vram_b.asm). Task 6 adds the back-channel: the LANCE is taken over * for transmit and a KIND_STATUS frame is built and sent every 64 accepted - * frames (send_status() below). + * frames (send_status() below). Task 7 replaces Task 4's debugger-injected + * buffer with the payload's own LANCE RX ring: sixteen descriptors whose + * buffers tile a slot, armed per slot, consumed in ring order, so the chip + * DMAs the wire bytes into the memory fp_parse() and the blit read. * * Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB): * - * +0x00000 frame slot A 24576 bytes - * +0x06000 frame slot B 24576 bytes + * +0x00000 frame slot A 24576 bytes (16 x 1526 B RX buffers) + * +0x06000 frame slot B 24576 bytes (16 x 1526 B RX buffers) * +0x0C000 framebuffer 163840 bytes (scheme A, Task 5) * +0x34000 dictionary 32768 bytes - * +0x3C000 LANCE block 4096 bytes (Task 6: init block, TX ring, - * status frame) + * +0x3C000 LANCE block 4096 bytes (init block, TX ring, status + * frame, RX ring) * - * The LANCE block is what was slack. Its ring base is aligned at run time + * The LANCE block is what was slack. Its ring bases are aligned at run time * (lance_start() explains why the offset alone is not enough), and the status - * frame sits right after the 8 descriptors. + * frame sits right after the 8 TX descriptors, the RX ring after that. * - * A slot holds one complete frame -- 16-byte header and payload -- exactly as - * it arrives on the wire, so the bytes fp_parse() validates are the bytes the - * blit reads. Neither slot is ever split across a ring wrap: the two ring - * entries' descriptors are re-armed per slot at frame start. + * A slot is 16 packet buffers of RXBUFLEN = 1526 bytes (16 x 1526 = 24416, so + * it fits 24576 with 160 bytes to spare). The chip writes ONE Ethernet frame + * per buffer -- 14-byte header, the VS frame's chunk, and the FCS it appends -- + * so a VS frame arrives as a series of datagrams in ring order, and the frame's + * bytes are NOT contiguous in the slot: they are 14 bytes into each buffer, + * 1526 bytes apart. This payload therefore keeps a fragment table (offset, + * buffer, length per datagram) and `paint()` reads each source row across the + * fragments that hold it; there is no per-frame CPU copy of the picture (the + * design's "DMA, no CPU copy" -- spec 8.1). fp_parse() runs over the first + * fragment's body, which holds the VS header, with the accumulated length as + * its bound. + * + * Descriptors are re-armed per slot: when a frame completes, all 16 are pointed + * at the OTHER slot BEFORE the picture is painted, so the chip can fill N+1 + * while we draw N and can never write into the slot being painted. A frame + * that needs more than 16 datagrams cannot be received at all: the ring is + * exhausted, the partial frame is dropped and counted (RX_RING_DROPS). 128 + * rows of 160 bytes is 20,496 bytes = 15 datagrams, so the design's strip frame + * fits with one descriptor to spare. * * Screen geometry. VRAM is at bit 0x04000000 and a screen row is 0x800 bits * (256 bytes), the real unit's pitch (harness/c-hello/README.md). MAME's own @@ -53,8 +71,10 @@ * marker named 0x018100NN -> bit 0x01810000 + NN * 8 * * (POOL_PTR = 0x01810100, ...). Task 5's OS_TOUCH is 0x01810220 (ruling R12), - * i.e. cell 0x44 -- free between the RX_* counters and the injection cells. - * vidstream.lua uses the same rule. + * i.e. cell 0x44 -- free between the RX_* counters and Task 4's retired + * injection cells. Task 7's ring record takes cells 0x10, 0x14, 0x18, 0x1c, + * 0x40 and 0x48 (the free page prefix R12 left for the RX work, at the same + * four-byte spacing every other cell uses); vidstream.lua uses the same rule. * * K&R C only: no prototypes, no '//', declarations before statements. */ @@ -95,35 +115,30 @@ /* * Task 6: the terminal's status frame. * - * The payload takes the LANCE over -- STOP, its own init block and TX ring, - * INIT, IDON, STRT -- and every 64 accepted frames builds a 60-byte Ethernet - * frame in its own buffer and hands it to the chip with a TX descriptor. The - * descriptor setup is lance.c's (harness/udpstream/lance.c): the ring words are - * TMD0/TMD1/TMD2/TMD3 at byte offsets 0/2/4/6, the buffer address's high byte - * lives in TMD1, and the length is (0x10000 - bytes) in TMD2. This is NOT a - * second driver: it is the same init/descriptor sequence, with one buffer - * instead of a loopback probe's. + * The payload takes the LANCE over -- STOP, its own init block, RX and TX + * rings, INIT, IDON, STRT -- and every 64 accepted frames builds a 60-byte + * Ethernet frame in its own buffer and hands it to the chip with a TX + * descriptor. The descriptor setup is lance.c's (harness/udpstream/lance.c): + * the ring words are TMD0/TMD1/TMD2/TMD3 at byte offsets 0/2/4/6, the buffer + * address's high byte lives in TMD1, and the length is (0x10000 - bytes) in + * TMD2. This is NOT a second driver: it is the same init/descriptor sequence, + * with one buffer instead of a loopback probe's. * * Where the pieces live: the pool's last 4 KB was slack, so the LANCE block - * goes there -- 24 bytes of init block, 64 bytes of TX ring (8 descriptors) and - * the 60-byte status frame, all aligned at run time in lance_start(). All of - * it is memory this payload owns, which is the point: the firmware's own rings - * are in low memory with 32 descriptors (see lance.c's note on the OS init - * block), and arming a slot in a ring whose position the chip owns is not - * something this payload can do reliably. + * goes there -- 24 bytes of init block, 64 bytes of TX ring (8 descriptors), the + * 60-byte status frame and the RX ring (16 descriptors), all aligned at run + * time in lance_start(). All of it is memory this payload owns, which is the + * point: the firmware's own rings are in low memory with 32 descriptors (see + * lance.c's note on the OS init block), and arming a slot in a ring whose + * position the chip owns is not something this payload can do reliably. * - * The receiver is DISABLED (MODE_DRX) on purpose, and that is a gap rather - * than a design: the receive path in this task is the harness injection - * interface (RX_READY_A/B), the real transport chunks a frame into 15 - * datagrams, and a ring of one-descriptor-per-datagram that reassembles them - * does not exist yet (ruling R11 covers why MAME cannot exercise it either). - * Arming an RX ring we have no buffers for would let the chip DMA a datagram - * into address 0, so disabling the receiver is the honest state. - * - * So `drops` in the status body is wired to the chip's own counter -- CSR0's - * MISS bit, one per missed-packet event, cleared after each report -- and with - * the receiver disabled it can only be 0 here. What it must not be is - * invented: the field is the chip's, so an RX ring inherits it unchanged. + * Task 7 arms the receiver (MODE = 0, so CSR0's STRT sets RXON) against those + * 16 descriptors. `drops` in the status body stays the chip's own counter -- + * CSR0's MISS bit, one per missed-packet event, cleared after each report -- + * plus the payload's own count of frames the ring was too small to finish + * (RX_RING_DROPS). Both mean "a frame that arrived and did not reach the + * screen"; the chip's field is not replaced, and the payload's own count is + * added to it rather than invented. */ #define LANCE_RAP 0x05900000 /* bit address of the register-select port */ #define LANCE_RDP 0x05800000 /* bit address of the data port */ @@ -135,12 +150,27 @@ #define CSR0_IDON 0x0100 #define CSR0_MISS 0x1000 /* no RX descriptor: a missed-packet event */ -#define MODE_DRX 0x0001 /* disable the receiver (see above) */ - #define TMD1_OWN 0x8000 #define TMD1_ENP 0x0100 #define TMD1_STP 0x0200 +/* + * The RX ring (Task 7). RX_DESC descriptors of 8 bytes each, each pointing at + * its own RXBUFLEN-byte buffer inside the armed slot. RXBUFLEN is one full + * 1514-byte Ethernet frame plus margin, the figure lance_cmd.c uses on the + * unit: a datagram from vidsrc.py is 14 + 1450 = 1464 bytes, the chip appends + * the 4-byte FCS, writes it after the frame and reports the total in RMD3. + * RX_DESC * RXBUFLEN = 24416 bytes, which fits a 24576-byte slot. + */ +#define RX_DESC 16 +#define RXBUFLEN 1526 + +#define RMD1_OWN 0x8000 /* the chip owns the descriptor */ +#define RMD1_ENP 0x0100 /* end of packet */ +#define RMD1_STP 0x0200 /* start of packet */ + +#define ETH_FCS 4 /* the chip stores this after the frame */ + #define L_INIT_OFF 0x3C000 /* 12-word init block (24 bytes) */ #define L_RING_OFF 0x3C040 /* 8 TX descriptors, 8 bytes each */ #define L_RING_LEN 8 /* TLEN code 3 = 2^3 descriptors */ @@ -191,12 +221,47 @@ static unsigned int heap_bytes() */ static unsigned int lance_ok; /* 1 once the LANCE is ours */ static unsigned int lance_txring; /* bit address of the TX ring */ +static unsigned int lance_rxring; /* bit address of the RX ring */ static unsigned int status_buf; /* bit address of the status frame */ static unsigned int lance_txslot; /* next TX descriptor to arm */ static unsigned int status_seq; /* reports sent, the status frame_no*/ static unsigned int status_seen; /* frames_rx at the last report */ static unsigned int drops_total; /* CSR0 MISS events, cumulative */ +/* + * Task 7 receive state. + * + * rx_armed the slot the ring's 16 descriptors point into (0 = A, 1 = B). + * Only ever written together with a full re-arm, so a descriptor + * never points at a slot the payload is not expecting. + * rx_pos the next descriptor the consumer expects. The chip fills + * descriptors strictly in ring order (am79c90.cpp's + * recv_complete_cb advances m_rx_ring_pos by one per packet and + * INIT resets it to 0), and this payload consumes them in that + * same order, so rx_pos *is* the chip's position -- there is no + * second, shadow copy of it that could drift. It is published as + * RX_RING_POS so the MAME check can inject at the right descriptor + * without guessing. + * rx_acc bytes of the current VS frame accumulated so far, i.e. the sum + * of the delivered datagram bodies. It doubles as the "still + * receiving" flag: 0 means no frame is in progress. + * rx_nfrag fragments recorded for the frame being received. + * rx_frag_* one entry per datagram: where its body sits in the slot, which + * frame byte it starts at, and how many body bytes it delivered. + * paint() walks this instead of copying the picture into a + * contiguous buffer. + * RX_RING_DROPS counts frames abandoned because the ring ran out of descriptors + * before the frame completed: a frame larger than 16 datagrams cannot be + * received at all, so it is dropped and reported rather than half-painted. + */ +static unsigned int rx_armed; +static unsigned int rx_pos; +static unsigned int rx_acc; +static unsigned int rx_nfrag; +static unsigned int rx_frag_off[RX_DESC]; +static unsigned int rx_frag_ptr[RX_DESC]; +static unsigned int rx_frag_len[RX_DESC]; + /* * blit_row(src, daddr_xy, dydx) * @@ -220,23 +285,29 @@ unsigned int dydx; } /* - * paint_frame(slot, h) + * paint(h) -- draw one accepted strip frame from the fragment table. * - * Draw one accepted strip frame: source row r goes down screen rows - * r*stretch .. r*stretch+stretch-1. A frame the screen cannot hold is not - * drawn: zero rows or zero stretch, more than SCREEN_ROWS screen rows, or a - * payload shorter than the rows it claims (fp_parse() has already proved that - * payload_len bytes arrived, so that bound is real). A refused frame never - * touches VRAM, so a malformed header cannot paint garbage over a good one. + * Source row r goes down screen rows r*stretch .. r*stretch+stretch-1. The + * frame's bytes do not sit contiguously in the slot (each datagram's body is 14 + * bytes into its own buffer), so a row is cut into the pieces the fragments + * hold it in: the row starts at frame byte payload_off + r*ROW_BYTES, and each + * piece is one PIXBLT from the fragment that holds it, with the destination x + * advanced by the bytes already drawn. A 1450-byte datagram holds 9 rows and a + * fraction, so only about one row in nine is split at all. + * + * A frame the screen cannot hold is not drawn: zero rows or zero stretch, more + * than SCREEN_ROWS screen rows, or a payload shorter than the rows it claims + * (fp_parse() and the accumulator have already proved that payload_len body + * bytes arrived, so that bound is real). A refused frame never touches VRAM, + * so a malformed header cannot paint garbage over a good one. * * The whole burst runs with interrupts masked (spec 8.9) so the resident OS * cannot repaint between two of our source rows and tear the picture. */ -static void paint_frame(slot, h) -unsigned int slot; +static void paint(h) fp_header *h; { - unsigned int r, rows, stretch, src, sent; + unsigned int r, rows, stretch, sent, pos, done, k, f, room, n; rows = h->rows; stretch = h->stretch; @@ -249,6 +320,9 @@ fp_header *h; if (rows * ROW_BYTES > h->payload_len) { return; } + if (rx_nfrag == 0) { + return; + } /* The canary word lives outside every pixel this frame writes. If it * is not ours any more, something else painted VRAM: count it and put @@ -261,11 +335,35 @@ fp_header *h; } vid_dint_(); - src = slot + h->payload_off * 8; + pos = h->payload_off; /* the frame's first body byte */ + k = 0; for (r = 0; r < rows; r++) { - blit_row(src + r * ROW_PIXELS, - (r * stretch) << 16, - (stretch << 16) | ROW_PIXELS); + done = 0; + while (done < ROW_BYTES) { + while (k < rx_nfrag + && pos >= rx_frag_off[k] + rx_frag_len[k]) { + k = k + 1; + } + if (k >= rx_nfrag) { + vid_eint_(); + return; /* truncated: stop drawing */ + } + if (pos < rx_frag_off[k]) { + vid_eint_(); + return; /* a gap: never paint across it */ + } + f = pos - rx_frag_off[k]; + room = rx_frag_len[k] - f; + n = ROW_BYTES - done; + if (room < n) { + n = room; + } + blit_row(rx_frag_ptr[k] + f * 8, + ((r * stretch) << 16) | (done * 8), + (stretch << 16) | (n * 8)); + done = done + n; + pos = pos + n; + } } vid_eint_(); } @@ -329,8 +427,202 @@ unsigned int reg; return v; } +/* The two slots' bit addresses, published by main() and never changed. */ +static unsigned int slot_base[2]; + /* - * lance_start(ptr) -- take the LANCE over for transmit. 1 on success. + * rx_arm(slot) -- point all 16 descriptors at `slot`, ownership set last. + * + * The chip masks the ring ADDRESS it is given (RING_ADDR_MASK 0xFFFFF8, + * am79c90.h) but uses a descriptor's buffer address as written, in RMD1 bits + * 0-7 (high byte) and RMD0 (low 16). NetBSD's am7990_rint() re-arms a used + * descriptor with "length first, ownership last" and leaves the address alone; + * here the address changes on every frame (the slot alternates), so RMD0 is + * written too -- and before RMD1, because a descriptor with OWN set and a stale + * RMD0 would DMA the next packet into the other slot, which is the one being + * painted. + * + * RMD2 is the buffer size as a negative two's complement count (0x10000 - + * RXBUFLEN), the form am79c90's get_buf_length() decodes and lance_cmd.c arms + * with. RMD3 is the message count the chip fills in; zeroing it first means a + * descriptor completed by anything but the chip reads as a zero-length packet + * rather than as the previous frame's length. + */ +static void rx_arm(slot) +unsigned int slot; +{ + unsigned int i, cb, base; + + base = slot_base[slot]; + for (i = 0; i < RX_DESC; i++) { + cb = (base >> 3) + i * RXBUFLEN; + w16(lance_rxring + i * 64 + 0, cb & 0xFFFF); + w16(lance_rxring + i * 64 + 48, 0); + w16(lance_rxring + i * 64 + 32, (0x10000 - RXBUFLEN) & 0xFFFF); + w16(lance_rxring + i * 64 + 16, + RMD1_OWN | ((cb >> 16) & 0xFF)); + } +} + +/* + * rx_switch() -- arm the other slot. Called at the end of a frame, before the + * picture is drawn, so the chip can fill N+1 while we paint N and can never + * write into the slot being read (spec 8.1's overlap). + */ +static void rx_switch() +{ + rx_armed = 1 - rx_armed; + rx_arm(rx_armed); +} + +/* + * rx_dgram(i) -- consume descriptor i, which the chip has completed. + * + * One Ethernet frame per buffer, so STP and ENP must both be set. A packet + * longer than one buffer would be chained across descriptors, and every + * reference driver (4.4BSD pmax if_le.c, NetBSD am7990.c, Linux 7990.c) drops a + * chained frame rather than reassembling it -- lance_cmd.c cites the same rule + * -- so a chained descriptor contributes nothing here. RMD3 is the delivered + * length INCLUDING the 4-byte FCS the chip appends, so the body is RMD3 minus + * the Ethernet header and the FCS. + * + * The datagram is appended to the fragment table and the frame's own + * payload_len decides when the frame is complete (spec 8.1: payload_len plus + * the bytes actually delivered are authoritative, not the descriptor count). + * RMD3 is per datagram here, because each datagram is its own Ethernet packet. + */ +static void rx_dgram(i) +unsigned int i; +{ + unsigned int rmd1, mlen, len, src, plen, total, lim, slot; + fp_header h; + + rmd1 = r16(lance_rxring + i * 64 + 16); + mlen = r16(lance_rxring + i * 64 + 48) & 0x0FFF; + src = slot_base[rx_armed] + i * RXBUFLEN * 8 + ETH_HDR_LEN * 8; + len = 0; + if ((rmd1 & (RMD1_STP | RMD1_ENP)) == (RMD1_STP | RMD1_ENP) + && mlen > ETH_HDR_LEN + ETH_FCS + && mlen - ETH_HDR_LEN - ETH_FCS <= RXBUFLEN - ETH_HDR_LEN) { + len = mlen - ETH_HDR_LEN - ETH_FCS; + } + if (len == 0) { + return; /* runt or chained: nothing appended */ + } + if (rx_acc == 0) { + /* A frame starts here. Only a VS frame is ours: the chip + * accepts broadcast and its own address, so other traffic can + * land in this ring, and fp_parse() would count every such + * datagram as a bad frame. Resync on the magic instead. */ + if (len < 2 + || *(unsigned char *)src != FP_MAGIC0 + || *(unsigned char *)(src + 8) != FP_MAGIC1) { + return; + } + } + + rx_frag_off[rx_nfrag] = rx_acc; + rx_frag_ptr[rx_nfrag] = src; + rx_frag_len[rx_nfrag] = len; + rx_nfrag = rx_nfrag + 1; + rx_acc = rx_acc + len; + + if (rx_acc < FP_HEADER_SIZE) { + return; + } + plen = (unsigned int)*(unsigned char *)(rx_frag_ptr[0] + 6 * 8) + | ((unsigned int)*(unsigned char *)(rx_frag_ptr[0] + 7 * 8) << 8); + total = FP_HEADER_SIZE + plen; + if (total > SLOT_BYTES) { + /* The header claims a frame larger than the slot: it can never be + * completed, and no part of it is drawn. Reject it now, then hand + * the OTHER slot to the chip: all 16 descriptors are re-armed, + * including the one this datagram arrived in, so the sender's next + * frame starts on a clean ring. What is left of the rejected frame + * is ignored, exactly as any frame that does not open with the + * magic is. */ + *(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1; + rx_acc = 0; + rx_nfrag = 0; + rx_switch(); + return; + } + if (rx_acc < total) { + return; /* still arriving */ + } + + /* The frame is complete. Hand the OTHER slot to the chip first, so + * nothing it receives from now on lands in the picture below. */ + slot = rx_armed; + rx_switch(); + + /* + * Task 4 finding (b): the length handed to fp_parse() is clamped to the + * slot. rx_acc is bounded by RX_DESC * RXBUFLEN and the claim above, + * but fp_parse() walks a caller-supplied length, and a slot-sized bound + * is the contract both of them can be read against. + */ + lim = rx_acc; + if (lim > SLOT_BYTES) { + lim = SLOT_BYTES; + } + if (fp_parse((unsigned char *)rx_frag_ptr[0], lim, &h)) { + *(unsigned int *)RX_LAST_LEN = h.payload_len; + *(unsigned int *)RX_SLOT = slot; + paint(&h); + /* Published after the paint: a reader that sees RX_OK move + * sees a picture that is already on the screen. */ + *(unsigned int *)RX_OK = *(unsigned int *)RX_OK + 1; + } else { + *(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1; + } + rx_acc = 0; + rx_nfrag = 0; +} + +/* + * rx_poll() -- drain every descriptor the chip has completed, in ring order. + * + * Task 4 finding (a): there is no level-triggered "frame ready" cell any more. + * The handshake is the OWN bit, per descriptor: the chip clears it when the + * packet is in the buffer and this side sets it again (in rx_arm) only after + * reading the descriptor's data, so there is no shared flag for two writers to + * race on. `rx_pos` is the only cursor and it advances only after a descriptor + * has been read; the chip advances its own position by one per packet and INIT + * resets both to 0, so consuming in ring order keeps the two in step. + * + * A frame that uses the whole ring without completing is dropped: the ring + * cannot hold it, so it can never complete, and leaving the partial frame in + * place would make every later frame look like its tail. + */ +static void rx_poll() +{ + unsigned int i, n; + + for (n = 0; n < RX_DESC; n++) { + i = rx_pos; + if (r16(lance_rxring + i * 64 + 16) & RMD1_OWN) { + break; /* the chip has not completed it */ + } + rx_pos = (i + 1) & (RX_DESC - 1); + *(unsigned int *)RX_DGRAM = *(unsigned int *)RX_DGRAM + 1; + rx_dgram(i); + if (rx_nfrag == RX_DESC && rx_acc != 0) { + *(unsigned int *)RX_RING_DROPS = + *(unsigned int *)RX_RING_DROPS + 1; + rx_acc = 0; + rx_nfrag = 0; + rx_switch(); + } + } + *(unsigned int *)RX_RING_POS = rx_pos; + *(unsigned int *)RX_RING_ACC = rx_acc; + *(unsigned int *)RX_RING_ARMED = rx_armed; +} + +/* + * lance_start(ptr) -- take the LANCE over for receive and transmit. 1 on + * success. * * STOP comes first because the init block address (CSR1/CSR2/CSR3) is only * writable while the chip is stopped -- am79c90.cpp regs_w guards each of them @@ -344,11 +636,18 @@ unsigned int reg; * ring word is the address's bits 16-23 in its low byte and the length code * (2^code descriptors) in bits 13-15; the pool sits above 64 KB, so unlike * lance.c's low-memory rings the high byte is not zero and has to be written. + * + * Task 7 removed MODE_DRX and gave the receiver a ring, because that is what a + * receive path is: MODE = 0 (no loopback, no transmitter disable, FCS kept and + * appended) and STRT afterwards sets RXON as well as TXON (am79c90.cpp's STRT + * handler: "if (m_mode & MODE_DRX) clear RXON else set RXON"). The descriptors + * are armed for slot A before INIT, so the chip's first packet -- which can + * arrive the instant STRT lands -- finds a buffer. */ static int lance_start(ptr) unsigned int ptr; { - unsigned int init, ring, buf, cb, i, idon; + unsigned int init, ring, buf, rxring, cb, i, idon; /* * Alignment is the CHIP's requirement, not a convenience. The Am7990 @@ -365,12 +664,14 @@ unsigned int ptr; init = (ptr + L_INIT_OFF * 8 + 0x0F) & 0xFFFFFFF0; /* 16-bit words */ ring = (ptr + L_RING_OFF * 8 + 0x3F) & 0xFFFFFFC0; /* 8-byte */ buf = ring + L_RING_LEN * 64; /* after the ring */ + rxring = (buf + STATUS_LEN * 8 + 0x3F) & 0xFFFFFFC0; /* after status */ lance_txring = ring; status_buf = buf; + lance_rxring = rxring; lance_csr_w(0, CSR0_STOP); - w16(init + 0 * 16, MODE_DRX); /* no loopback, FCS appended */ + w16(init + 0 * 16, 0); /* MODE: no loopback, FCS on */ w16(init + 1 * 16, 0x0008); /* PADR 08:00:2b:16:4b:5a */ w16(init + 2 * 16, 0x162b); w16(init + 3 * 16, 0x5a4b); @@ -378,13 +679,14 @@ unsigned int ptr; w16(init + 5 * 16, 0); w16(init + 6 * 16, 0); w16(init + 7 * 16, 0); - w16(init + 8 * 16, 0); /* RX ring: none (MODE_DRX) */ - w16(init + 9 * 16, 0); + cb = rxring >> 3; /* BYTE address of the RX ring */ + w16(init + 8 * 16, cb & 0xFFFF); + w16(init + 9 * 16, ((cb >> 16) & 0xFF) | (4 << 13)); /* 16 desc */ cb = ring >> 3; /* BYTE address of the ring */ w16(init + 10 * 16, cb & 0xFFFF); w16(init + 11 * 16, ((cb >> 16) & 0xFF) | (3 << 13)); - /* Every descriptor points at the one status buffer; TMD1's OWN stays + /* Every TX descriptor points at the one status buffer; TMD1's OWN stays * clear until a frame is ready, and TMD2 holds the length as a two's * complement (0x10000 - 60), lance.c's descriptor layout. */ cb = buf >> 3; @@ -398,6 +700,12 @@ unsigned int ptr; wb(buf + i * 8, 0); } + /* The RX ring, armed for slot A: this is the state the first packet + * finds, and it is what the MAME ring check asserts. */ + rx_armed = 0; + rx_arm(rx_armed); + *(unsigned int *)RX_RING_BASE = rxring; + lance_csr_w(1, (init >> 3) & 0xFFFF); lance_csr_w(2, ((init >> 3) >> 16) & 0xFF); lance_csr_w(3, 0); @@ -415,8 +723,9 @@ unsigned int ptr; return 0; } - /* STRT turns the transmitter on; no INEA, so no interrupt reaches the - * firmware's network ISR while this payload owns the chip. */ + /* STRT turns the receiver and the transmitter on; no INEA, so no + * interrupt reaches the firmware's network ISR while this payload owns + * the chip. RXON is what makes the descriptors armed above live. */ lance_csr_w(0, CSR0_STRT); for (i = 0; i < 30; i++) { } @@ -440,7 +749,10 @@ unsigned int ptr; * crc_err is RX_BAD: the frames fp_parse() rejected (header CRC, magic, * version or length). Since ruling R14 a corrupted *body* cannot appear * here -- the LANCE validated each datagram's FCS before the bytes were - * stored -- so this is the only CRC error the terminal can count. + * stored -- so this is the only CRC error the terminal can count. drops is + * the chip's CSR0 MISS events plus RX_RING_DROPS (frames too large for the 16 + * descriptors, Task 7); both are frames that arrived and did not reach the + * screen, and neither number is inferred from the other. * * Returns 1 when the frame was handed to the chip, 0 when the ring slot was * still owned and nothing was armed. Overwriting a descriptor the chip still @@ -451,7 +763,7 @@ unsigned int ptr; static int send_status(frames_rx) unsigned int frames_rx; { - unsigned int i, crc, csr0, tmd1, off, hb, miss, bad, free_kb; + unsigned int i, crc, csr0, tmd1, off, hb, miss, bad, rdrops, free_kb; hb = status_buf; @@ -494,6 +806,10 @@ unsigned int frames_rx; } bad = *(unsigned int *)RX_BAD; + /* Both drop counts are frames that arrived and did not reach the screen: + * the chip's missed-packet events (no descriptor free) and the payload's + * own count of frames too large for the ring. */ + rdrops = *(unsigned int *)RX_RING_DROPS; free_kb = heap_bytes() >> 10; off = hb + STAT_BODY * 8; wb(off + 0 * 8, frames_rx & 0xFF); @@ -502,8 +818,8 @@ unsigned int frames_rx; wb(off + 3 * 8, (frames_rx >> 24) & 0xFF); wb(off + 4 * 8, bad & 0xFF); wb(off + 5 * 8, (bad >> 8) & 0xFF); - wb(off + 6 * 8, drops_total & 0xFF); - wb(off + 7 * 8, (drops_total >> 8) & 0xFF); + wb(off + 6 * 8, (drops_total + rdrops) & 0xFF); + wb(off + 7 * 8, ((drops_total + rdrops) >> 8) & 0xFF); wb(off + 8 * 8, csr0 & 0xFF); wb(off + 9 * 8, (csr0 >> 8) & 0xFF); wb(off + 10 * 8, free_kb & 0xFF); @@ -554,67 +870,6 @@ unsigned int frames_rx; return 1; } -/* - * Handle one slot if the receive path has raised its ready flag. - * - * In the real transport the LANCE receive interrupt fills the slot and sets - * the flag; MAME cannot hand this emulated LANCE an Ethernet frame (the - * driver attaches no network interface -- ruling R11), so in the harness - * vidstream.lua writes the slot and sets the flag instead. The payload does - * not care which of the two did it. - * - * `len` is the number of bytes the transport actually delivered. fp_parse() - * rejects a frame whose payload_len wants more than that, which is the rule - * that makes payload_len plus the received length authoritative rather than a - * descriptor field. - */ -static void rx_poll(slot) -unsigned int slot; -{ - unsigned int readyp, lenp, basep, base, len, ready; - fp_header h; - int ok; - - if (slot == 0) { - readyp = RX_READY_A; - lenp = RX_LEN_A; - basep = RX_SLOT_A; - } else { - readyp = RX_READY_B; - lenp = RX_LEN_B; - basep = RX_SLOT_B; - } - - ready = *(unsigned int *)readyp; - if (ready == 0) { - return; - } - - base = *(unsigned int *)basep; - len = *(unsigned int *)lenp; - - ok = 0; - if (base != 0) { - ok = fp_parse((unsigned char *)base, len, &h); - } - - if (ok) { - *(unsigned int *)RX_OK = *(unsigned int *)RX_OK + 1; - *(unsigned int *)RX_LAST_LEN = h.payload_len; - *(unsigned int *)RX_SLOT = slot; - /* Only an accepted frame reaches VRAM: the bytes just validated - * are the bytes the blit reads, and a corrupt frame can never - * paint over a good one. */ - paint_frame(base, &h); - } else { - *(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1; - } - - /* Release the slot last: the checks read the counters, and a slot - * freed before its counter moved would look like a dropped frame. */ - *(unsigned int *)readyp = 0; -} - int main() { unsigned int before, after, ptr, frames, sent; @@ -625,18 +880,21 @@ int main() *(unsigned int *)POOL_FREEA = 0; /* Receive record: counters and the two slots are cleared before anything - * is published, so a reader never sees a stale ready flag or a slot - * address that has no allocation behind it. */ + * is published, so a reader never sees a counter from a previous run or a + * slot address that has no allocation behind it. The ring record is + * published by lance_start() once the descriptors are armed. */ *(unsigned int *)RX_OK = 0; *(unsigned int *)RX_BAD = 0; *(unsigned int *)RX_LAST_LEN = 0; *(unsigned int *)RX_SLOT = 0; *(unsigned int *)RX_SLOT_A = 0; *(unsigned int *)RX_SLOT_B = 0; - *(unsigned int *)RX_READY_A = 0; - *(unsigned int *)RX_LEN_A = 0; - *(unsigned int *)RX_READY_B = 0; - *(unsigned int *)RX_LEN_B = 0; + *(unsigned int *)RX_RING_BASE = 0; + *(unsigned int *)RX_RING_POS = 0; + *(unsigned int *)RX_RING_ACC = 0; + *(unsigned int *)RX_RING_ARMED = 0; + *(unsigned int *)RX_RING_DROPS = 0; + *(unsigned int *)RX_DGRAM = 0; *(unsigned int *)OS_TOUCH = 0; /* Arm the VRAM canary before any frame is accepted, so a repaint @@ -660,14 +918,17 @@ int main() /* Slot B follows slot A in the same pool. Both are published before * POOL_PTR so the harness can find them from either one. */ - *(unsigned int *)RX_SLOT_A = ptr; - *(unsigned int *)RX_SLOT_B = ptr + SLOT_BYTES * 8; + slot_base[0] = ptr; + slot_base[1] = ptr + SLOT_BYTES * 8; + *(unsigned int *)RX_SLOT_A = slot_base[0]; + *(unsigned int *)RX_SLOT_B = slot_base[1]; *(unsigned int *)POOL_PTR = ptr; /* publish last: a complete record */ /* Task 6: the status back-channel. The record starts empty, then the - * LANCE is taken over for transmit with interrupts masked -- the OS's - * drivers were using this chip a moment ago, and a network ISR running - * in the middle of the ring setup would see a half-written descriptor. */ + * LANCE is taken over for receive and transmit with interrupts masked -- + * the OS's drivers were using this chip a moment ago, and a network ISR + * running in the middle of the ring setup would see a half-written + * descriptor. */ *(unsigned int *)RX_EOF = 0; *(unsigned int *)ST_SENT = 0; *(unsigned int *)ST_FRAMES = 0; @@ -680,6 +941,10 @@ int main() status_seq = 0; status_seen = 0; drops_total = 0; + rx_armed = 0; + rx_pos = 0; + rx_acc = 0; + rx_nfrag = 0; vid_dint_(); lance_ok = lance_start(ptr); @@ -688,8 +953,7 @@ int main() for (;;) { *(unsigned int *)VID_ALIVE = *(unsigned int *)VID_ALIVE + 1; - rx_poll(0); - rx_poll(1); + rx_poll(); /* Report every STATUS_EVERY accepted frames, and -- once the * harness says the sender has stopped -- for every further