forked from arti/vt1200
feat(tiles): dictionary storage and kind dispatch in the payload
The payload assembles KIND_DICT_CHUNK frames into a 32 KB dictionary at POOL_PTR + 2 * SLOT_BYTES and learns the tile kinds: * frame_parse.h gains FP_KIND_TILES_FULL (2), FP_KIND_TILES_DELTA (3) and FP_KIND_DICT_CHUNK (4), exactly frame.py's KINDS table, plus the marker cells DICT_READY/DICT_BYTES/DICT_BAD and TILES_DROP. A host test parses the header and compares it with KINDS so the two cannot drift apart. * dict_chunk() enforces an ascending, contiguous offset (no gap, overlap, duplicate or reorder; a mid-entry offset only as a continuation), a chunk that does not overrun the last entry, and no chunk after DICT_READY. A refused chunk leaves the prefix alone. Readiness is exactly 32768 bytes (256 whole entries), so a short upload never becomes ready. * The dispatcher routes STRIP to the blit, DICT_CHUNK to the assembler, and refuses TILES_FULL/TILES_DELTA without painting, counting them in TILES_DROP (Task 4 adds the tile blit). * A compile-time assertion refuses a POOL_BYTES that cannot hold the dictionary; the 1987 preprocessor does not fold macro arithmetic in #if, so it is a defined array whose negative length fails the build. * VT1200_MODE=dict injects both tile kinds before the dictionary, 23 contiguous chunks with a duplicate offset, a gap and an overrunning tail mixed in, and reads the stored 32 KB back byte for byte.
This commit is contained in:
1 parent
84349d6da3
commit
0b726fda3a
5 files changed
+817
-20
No files matched your search
@@ -7,6 +7,7 @@
|
||||
# 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
|
||||
# VT1200_MODE=dict harness/mame/run-vidstream.sh # dictionary upload + kinds
|
||||
# STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback
|
||||
#
|
||||
# Mode rx delivers one valid frame and one corrupted frame through the payload's
|
||||
@@ -34,6 +35,13 @@
|
||||
# carries no bytes) counted in RX_FCS_DROP with its frame abandoned. The verdict
|
||||
# is in the Lua; the evidence is $LOGDIR/ring-status.txt.
|
||||
#
|
||||
# Mode dict asserts the dictionary assembler and the kind dispatch: a TILES_FULL
|
||||
# frame before the dictionary is rejected without painting, a 32 KB upload in 23
|
||||
# KIND_DICT_CHUNK frames (with a duplicate offset and an overrunning tail chunk
|
||||
# refused) makes DICT_READY 1 and DICT_BYTES 32,768, and the stored bytes are
|
||||
# compared with what was sent. The verdict is in the Lua; the evidence is
|
||||
# $LOGDIR/dict-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
|
||||
# free-list head (0x1E8AC0) both read 0, so the malloc success path cannot be
|
||||
@@ -90,6 +98,9 @@ fi
|
||||
if [ "$MODE" = "ring" ]; then
|
||||
rm -f "$LOGDIR/ring-status.txt"
|
||||
fi
|
||||
if [ "$MODE" = "dict" ]; then
|
||||
rm -f "$LOGDIR/dict-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"
|
||||
|
||||
+450
-3
@@ -59,6 +59,16 @@
|
||||
-- It writes ring-status.txt and prints the verdict itself: these are structural
|
||||
-- facts about the descriptors, not pixels (draw mode owns the pixels).
|
||||
--
|
||||
-- Mode "dict" (Task 3) is the dictionary assembler and the kind dispatch. It
|
||||
-- sends a TILES_FULL (2) and a TILES_DELTA (3) frame before there is a
|
||||
-- dictionary and asserts both are rejected without painting; then a 32 KB
|
||||
-- KIND_DICT_CHUNK (4) upload in 23 chunks with a duplicate-offset chunk, a
|
||||
-- gapped-offset chunk and an overrunning tail chunk mixed in; then it reads the
|
||||
-- 32 KB back from POOL_PTR + 2 * SLOT_BYTES and compares it with what was sent.
|
||||
-- DICT_READY/DICT_BYTES/DICT_BAD are asserted after every chunk, so a
|
||||
-- premature "ready" fails at the chunk that caused it. Verdict and evidence:
|
||||
-- the printed checks and $DUMP_DIR/dict-status.txt.
|
||||
--
|
||||
-- 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
|
||||
@@ -78,10 +88,10 @@
|
||||
-- harness/udpstream/frame_parse.h.
|
||||
--
|
||||
-- Env:
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream or ring
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream, ring or dict
|
||||
-- VID_AT load/entry frame (default 2400, the OS phase)
|
||||
-- VID_END inspection/exit frame (default VID_AT + 60)
|
||||
-- VID_DUMP_DIR where draw, stream and ring modes write their dumps
|
||||
-- VID_DUMP_DIR where draw, stream, ring and dict modes write their dumps
|
||||
-- (default /tmp/opencode)
|
||||
-- VID_SENTINEL_CLOBBER=1 draw mode only: overwrite the VRAM canary word
|
||||
-- before the frame is delivered, so the payload must count
|
||||
@@ -95,6 +105,8 @@
|
||||
-- status after RX_EOF (default 900)
|
||||
-- VID_RING_WAIT ring mode only: emulated frames to wait for each ring
|
||||
-- transition (default 300)
|
||||
-- VID_DICT_WAIT dict mode only: emulated frames to wait for each dictionary
|
||||
-- chunk to be consumed (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)
|
||||
@@ -127,6 +139,9 @@ if MODE == "stream" and not os.getenv("VID_END") then
|
||||
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
|
||||
elseif MODE == "dict" and not os.getenv("VID_END") then
|
||||
-- Five phases, each with its own bounded wait (VID_DICT_WAIT, 300).
|
||||
DEFAULT_END = AT + 5 * tonumber(os.getenv("VID_DICT_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")
|
||||
@@ -177,6 +192,14 @@ local ST_TMD1 = MARK_PAGE + 0x88 * 8
|
||||
local ST_INIT = MARK_PAGE + 0x8c * 8
|
||||
local ST_TMLAST = MARK_PAGE + 0x90 * 8
|
||||
|
||||
-- Task 3: the dictionary record and the tile-frame drop count (frame_parse.h).
|
||||
-- Cells 0x50..0x5c fill the free gap between RX_FCS_DROP (0x4c) and RX_SLOT_A
|
||||
-- (0x60), at the same four-byte spacing every other cell uses.
|
||||
local DICT_READY = MARK_PAGE + 0x50 * 8
|
||||
local DICT_BYTES = MARK_PAGE + 0x54 * 8
|
||||
local DICT_BAD = MARK_PAGE + 0x58 * 8
|
||||
local TILES_DROP = MARK_PAGE + 0x5c * 8
|
||||
|
||||
-- Must match vidstream.c.
|
||||
local SLOT_BYTES = 24576
|
||||
|
||||
@@ -249,7 +272,7 @@ local VRAM_ROW_BYTES = 256
|
||||
local DUMP_DIR = os.getenv("VID_DUMP_DIR") or "/tmp/opencode"
|
||||
|
||||
if MODE ~= "alloc" and MODE ~= "rx" and MODE ~= "draw"
|
||||
and MODE ~= "stream" and MODE ~= "ring" then
|
||||
and MODE ~= "stream" and MODE ~= "ring" and MODE ~= "dict" then
|
||||
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
|
||||
machine:exit()
|
||||
return
|
||||
@@ -564,6 +587,10 @@ local DRAW_FILES = { "vram-draw.bin", "frame-draw.bin", "draw-status.txt" }
|
||||
local STREAM_FILES = { "stream-status.bin", "stream-status.txt" }
|
||||
|
||||
local function clear_dump_files()
|
||||
if MODE == "dict" then
|
||||
os.remove(DUMP_DIR .. "/dict-status.txt")
|
||||
return
|
||||
end
|
||||
if MODE ~= "draw" and MODE ~= "stream" then
|
||||
return
|
||||
end
|
||||
@@ -1154,6 +1181,411 @@ local function ring_step()
|
||||
return false
|
||||
end
|
||||
|
||||
-- --- Task 3: the dictionary upload and kind dispatch -------------------------
|
||||
--
|
||||
-- Mode "dict" is the dictionary assembler's check. It plays the sender with
|
||||
-- tiledict.py's wire format -- KIND_DICT_CHUNK (4) frames, each a u16
|
||||
-- little-endian dictionary offset followed by up to 1,448 dictionary bytes --
|
||||
-- and asserts what the payload must do with them:
|
||||
--
|
||||
-- * a TILES_FULL (2) and a TILES_DELTA (3) frame that arrive before the
|
||||
-- dictionary is complete are rejected without painting: TILES_DROP ticks
|
||||
-- twice, RX_OK stays 0 and the screen does not change;
|
||||
-- * 23 contiguous chunks make DICT_BYTES 32,768 and DICT_READY 1, and the
|
||||
-- 32 KB the payload holds is byte for byte what was sent;
|
||||
-- * a chunk whose offset is not the next one is refused with DICT_BAD and the
|
||||
-- upload continues from where it was: a duplicate (overlapping/descending)
|
||||
-- and a gap are both exercised;
|
||||
-- * a tail chunk that would overrun the last 128-byte entry is refused the
|
||||
-- same way -- the "partial final entry" Task 2's review carried;
|
||||
-- * DICT_READY stays 0 after every short prefix: it is set by the chunk that
|
||||
-- completes exactly 256 entries, not by the first one.
|
||||
--
|
||||
-- The dictionary is the payload's own copy at POOL_PTR + 2 * SLOT_BYTES (the
|
||||
-- plan's placement, which Task 4 consumes). This script reads it back from
|
||||
-- there rather than asking the payload where it put it, so a wrong base is a
|
||||
-- failure of this check rather than something both ends could agree on.
|
||||
|
||||
local KIND_TILES_FULL = 2
|
||||
local KIND_TILES_DELTA = 3
|
||||
local KIND_DICT_CHUNK = 4
|
||||
|
||||
-- Must match vidstream.c and tiledict.py.
|
||||
local DICT_ENTRY = 128
|
||||
local DICT_SIZE = 32768 -- 256 entries x 128 B
|
||||
local DICT_CHUNK_BODY = 1448 -- tiledict.MAX_BODY
|
||||
local DICT_OFF = 2 * SLOT_BYTES -- 0xC000: after the two slots
|
||||
|
||||
local DICT_WAIT = tonumber(os.getenv("VID_DICT_WAIT") or "300")
|
||||
local dict_phase = 0
|
||||
local dict_phase_at = nil
|
||||
local dict_fail = 0
|
||||
local dict_lines = {}
|
||||
local dict_vram0 = nil
|
||||
local dict_pos = 0 -- RX_RING_POS expected once consumed
|
||||
local dict_plan = nil
|
||||
local dict_at = 0
|
||||
local dict_last = nil -- the plan entry awaiting its settle
|
||||
|
||||
-- The dictionary pattern: a distinct byte for every position, and every
|
||||
-- 128-byte entry different from its neighbours. A constant fill would let a
|
||||
-- wrong entry stride, a reversed copy or a chunk stored twice all pass, and a
|
||||
-- mismatch here would then be evidence of nothing.
|
||||
local function dict_byte(i)
|
||||
return (i * 13 + (i // DICT_ENTRY) * 7 + 3) & 0xFF
|
||||
end
|
||||
|
||||
-- One KIND_DICT_CHUNK frame: offset u16 little-endian, then `body` dictionary
|
||||
-- bytes taken from `off`. Bytes past the dictionary are filled with 0xEE so
|
||||
-- the oversize case carries a recognisable body rather than a repeat.
|
||||
local function dict_chunk_frame(off, body)
|
||||
local f = make_frame(KIND_DICT_CHUNK, 0, 0, 0, 2 + body, function(k)
|
||||
local p
|
||||
|
||||
if k == 0 then return off & 0xFF end
|
||||
if k == 1 then return (off >> 8) & 0xFF end
|
||||
p = off + k - 2
|
||||
if p >= DICT_SIZE then return 0xEE end
|
||||
return dict_byte(p)
|
||||
end)
|
||||
return f, 16 + 2 + body
|
||||
end
|
||||
|
||||
local function dict_chunk(i)
|
||||
local off = i * DICT_CHUNK_BODY
|
||||
local n = DICT_SIZE - off
|
||||
|
||||
if n > DICT_CHUNK_BODY then
|
||||
n = DICT_CHUNK_BODY
|
||||
end
|
||||
return dict_chunk_frame(off, n)
|
||||
end
|
||||
|
||||
local function dict_check(cond, what)
|
||||
if cond then
|
||||
dict_lines[#dict_lines + 1] = "PASS " .. what
|
||||
print("VID: dict PASS: " .. what)
|
||||
else
|
||||
dict_lines[#dict_lines + 1] = "FAIL " .. what
|
||||
print("VID: dict FAIL: " .. what)
|
||||
dict_fail = dict_fail + 1
|
||||
end
|
||||
return cond
|
||||
end
|
||||
|
||||
-- The plan: 11 chunks, a duplicate-offset chunk, a gapped chunk, chunks
|
||||
-- 11..21, a tail chunk that overruns entry 255, then the chunk that completes
|
||||
-- the dictionary. Each entry carries what the record must read once the
|
||||
-- payload has consumed it, so "premature" DICT_READY and a miscounted
|
||||
-- DICT_BYTES/DICT_BAD fail at the chunk that caused them instead of at the end
|
||||
-- of the run.
|
||||
local function dict_build_plan()
|
||||
local plan = {}
|
||||
local i, f, n, off
|
||||
|
||||
for i = 0, 10 do
|
||||
f, n = dict_chunk(i)
|
||||
plan[#plan + 1] = { f = f, n = n,
|
||||
bytes = (i + 1) * DICT_CHUNK_BODY, bad = 0, ready = 0,
|
||||
label = "chunk " .. i }
|
||||
end
|
||||
off = 2 * DICT_CHUNK_BODY
|
||||
f, n = dict_chunk_frame(off, DICT_CHUNK_BODY)
|
||||
plan[#plan + 1] = { f = f, n = n, bytes = 11 * DICT_CHUNK_BODY,
|
||||
bad = 1, ready = 0,
|
||||
label = string.format("chunk at duplicate offset %d (must be refused)", off) }
|
||||
-- A gap: the offset skips ahead of the prefix instead of repeating it.
|
||||
off = 31 * DICT_CHUNK_BODY
|
||||
f, n = dict_chunk_frame(off, DICT_CHUNK_BODY)
|
||||
plan[#plan + 1] = { f = f, n = n, bytes = 11 * DICT_CHUNK_BODY,
|
||||
bad = 2, ready = 0,
|
||||
label = string.format("chunk at gapped offset %d (must be refused)", off) }
|
||||
for i = 11, 21 do
|
||||
f, n = dict_chunk(i)
|
||||
plan[#plan + 1] = { f = f, n = n,
|
||||
bytes = (i + 1) * DICT_CHUNK_BODY, bad = 2, ready = 0,
|
||||
label = "chunk " .. i }
|
||||
end
|
||||
off = 22 * DICT_CHUNK_BODY
|
||||
f, n = dict_chunk_frame(off, DICT_CHUNK_BODY)
|
||||
plan[#plan + 1] = { f = f, n = n, bytes = 22 * DICT_CHUNK_BODY,
|
||||
bad = 3, ready = 0,
|
||||
label = string.format("tail chunk at %d overrunning entry 255 (must be refused)", off) }
|
||||
f, n = dict_chunk(22)
|
||||
plan[#plan + 1] = { f = f, n = n, bytes = DICT_SIZE, bad = 3, ready = 1,
|
||||
label = "chunk 22 (completes 256 entries)" }
|
||||
return plan
|
||||
end
|
||||
|
||||
-- The payload has finished with the last injection.
|
||||
--
|
||||
-- RX_DGRAM is NOT the signal: rx_poll() bumps it before it processes the
|
||||
-- datagram, so a read between the bump and the frame completion sees the count
|
||||
-- advanced while DICT_BYTES is still the old one (measured: the first run read
|
||||
-- DICT_BYTES=0 for a chunk it had just stored). RX_RING_POS and RX_RING_ACC
|
||||
-- are published together at the END of rx_poll(), after the frame has been
|
||||
-- dispatched, so POS naming the descriptor after the last injected one AND no
|
||||
-- frame in progress is a consistent "done" -- the DICT_* writes precede both
|
||||
-- in program order.
|
||||
local function dict_settled()
|
||||
return prog:read_u32(RX_RING_POS) == dict_pos
|
||||
and prog:read_u32(RX_RING_ACC) == 0
|
||||
end
|
||||
|
||||
local function dict_take(a)
|
||||
local ndg = inject_frame(a.f, a.n, DRAW_CHUNK)
|
||||
|
||||
if ndg == nil then
|
||||
return false
|
||||
end
|
||||
-- Cumulative: only ever called when the previous batch is settled, so
|
||||
-- dict_pos is RX_RING_POS at the moment of injection, and two frames can
|
||||
-- be handed over back to back.
|
||||
dict_pos = (dict_pos + ndg) % RX_DESC
|
||||
dict_last = a
|
||||
return true
|
||||
end
|
||||
|
||||
-- Read the 32 KB back from POOL_PTR + 2 * SLOT_BYTES and compare it with the
|
||||
-- pattern that was sent. Returns ok, first bad byte (-1 if none) and why not.
|
||||
local function dict_verify()
|
||||
local base = prog:read_u32(POOL_PTR)
|
||||
local bit, bad, first, i, v, want
|
||||
|
||||
if base == 0 then
|
||||
return false, -1, "POOL_PTR is zero"
|
||||
end
|
||||
bit = base + DICT_OFF * 8
|
||||
bad = 0
|
||||
first = -1
|
||||
for i = 0, DICT_SIZE // 2 - 1 do
|
||||
v = prog:read_u16(bit + i * 16)
|
||||
want = dict_byte(i * 2) | (dict_byte(i * 2 + 1) << 8)
|
||||
if v ~= want then
|
||||
bad = bad + 1
|
||||
if first < 0 then
|
||||
first = i * 2
|
||||
end
|
||||
end
|
||||
end
|
||||
return bad == 0, first, nil
|
||||
end
|
||||
|
||||
local function dict_dump(timed_out)
|
||||
local lines = {
|
||||
string.format("DICT_READY=%d", prog:read_u32(DICT_READY)),
|
||||
string.format("DICT_BYTES=%d", prog:read_u32(DICT_BYTES)),
|
||||
string.format("DICT_BAD=%d", prog:read_u32(DICT_BAD)),
|
||||
string.format("TILES_DROP=%d", prog:read_u32(TILES_DROP)),
|
||||
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
|
||||
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
|
||||
string.format("RX_DGRAM=%d", prog:read_u32(RX_DGRAM)),
|
||||
string.format("POOL_PTR=0x%X", prog:read_u32(POOL_PTR)),
|
||||
string.format("DICT_AT=0x%X", prog:read_u32(POOL_PTR) + DICT_OFF * 8),
|
||||
string.format("DICT_SIZE=%d", DICT_SIZE),
|
||||
string.format("VRAM_ROW0_BEFORE=%s", dict_vram0 and hex_bytes(dict_vram0) or "-"),
|
||||
string.format("VRAM_ROW0_AFTER=%s", hex_bytes(read_vram_head(32))),
|
||||
string.format("DICT_CHECKS_FAILED=%d", dict_fail),
|
||||
string.format("TIMEOUT=%d", timed_out and 1 or 0),
|
||||
}
|
||||
for _, l in ipairs(dict_lines) do
|
||||
lines[#lines + 1] = "CHECK " .. l
|
||||
end
|
||||
local okf = write_file(DUMP_DIR .. "/dict-status.txt",
|
||||
table.concat(lines, "\n") .. "\n")
|
||||
local ok = okf and not timed_out and dict_fail == 0
|
||||
print(string.format("VID: dict: %d checks failed; DICT_READY=%d DICT_BYTES=%d DICT_BAD=%d TILES_DROP=%d",
|
||||
dict_fail, prog:read_u32(DICT_READY), prog:read_u32(DICT_BYTES),
|
||||
prog:read_u32(DICT_BAD), prog:read_u32(TILES_DROP)))
|
||||
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
|
||||
machine:exit()
|
||||
end
|
||||
|
||||
-- One call per emulated frame. Returns true when the run is over.
|
||||
local function dict_step()
|
||||
local a, f, n, okv, first, why, after
|
||||
|
||||
if dict_phase == 0 then
|
||||
if dict_phase_at == nil then
|
||||
dict_phase_at = frames
|
||||
end
|
||||
if not ring_ready() then
|
||||
if frames >= dict_phase_at + DICT_WAIT then
|
||||
dict_check(false, string.format(
|
||||
"the payload published an RX ring (RX_RING_BASE=%s, ST_INIT=%04X)",
|
||||
hex(prog:read_u32(RX_RING_BASE)), prog:read_u32(ST_INIT)))
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
dict_vram0 = read_vram_head(32)
|
||||
local _, pos0 = ring_state()
|
||||
dict_pos = pos0
|
||||
a = { f = make_frame(KIND_TILES_FULL, 0x0061, 0, 0,
|
||||
DRAW_ROWS * RX_ROW_BYTES,
|
||||
function(i) return (i * 5 + 1) & 0xFF end),
|
||||
n = 16 + DRAW_ROWS * RX_ROW_BYTES }
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted the pre-dictionary TILES_FULL frame")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
dict_phase = 1
|
||||
dict_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if dict_phase == 1 then
|
||||
if not dict_settled() then
|
||||
if frames >= dict_phase_at + DICT_WAIT then
|
||||
dict_check(false, "the pre-dictionary TILES_FULL frame was consumed")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
dict_check(prog:read_u32(TILES_DROP) == 1,
|
||||
string.format("a TILES_FULL before the dictionary is rejected and counted (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
dict_check(prog:read_u32(DICT_READY) == 0, "DICT_READY is still 0")
|
||||
dict_check(prog:read_u32(RX_OK) == 0,
|
||||
"RX_OK did not move: the tile frame painted nothing")
|
||||
after = read_vram_head(32)
|
||||
dict_check(dict_vram0 ~= nil and after == dict_vram0,
|
||||
"the screen did not change: the tile frame was not blitted")
|
||||
dict_check(prog:read_u32(DICT_BYTES) == 0, "DICT_BYTES is still 0")
|
||||
-- The other tile kind, also before there is anything to decode with.
|
||||
-- A TILES_DELTA is 8 bytes of changed-tile list here; nothing may
|
||||
-- read it. One frame per settle: the payload re-arms the whole ring
|
||||
-- on the other slot when it completes a frame, so a frame injected
|
||||
-- behind another would be wiped by that re-arm.
|
||||
a = { f = make_frame(KIND_TILES_DELTA, 0x0063, 0, 0, 8,
|
||||
function(i) return (i * 3 + 1) & 0xFF end),
|
||||
n = 16 + 8 }
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted the pre-dictionary TILES_DELTA frame")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
dict_phase = 2
|
||||
dict_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if dict_phase == 2 then
|
||||
if not dict_settled() then
|
||||
if frames >= dict_phase_at + DICT_WAIT then
|
||||
dict_check(false, "the pre-dictionary TILES_DELTA frame was consumed")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
dict_check(prog:read_u32(TILES_DROP) == 2,
|
||||
string.format("TILES_FULL and TILES_DELTA before the dictionary are both rejected and counted (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
dict_check(prog:read_u32(DICT_READY) == 0,
|
||||
"DICT_READY is still 0 after both tile frames")
|
||||
dict_check(prog:read_u32(RX_OK) == 0,
|
||||
"RX_OK did not move: neither tile frame painted anything")
|
||||
after = read_vram_head(32)
|
||||
dict_check(dict_vram0 ~= nil and after == dict_vram0,
|
||||
"the screen did not change: neither tile frame was blitted")
|
||||
dict_check(prog:read_u32(DICT_BYTES) == 0, "DICT_BYTES is still 0")
|
||||
dict_plan = dict_build_plan()
|
||||
dict_at = 0
|
||||
dict_last = nil
|
||||
dict_phase = 3
|
||||
return false
|
||||
end
|
||||
|
||||
if dict_phase == 3 then
|
||||
if dict_last ~= nil then
|
||||
if not dict_settled() then
|
||||
if frames >= dict_phase_at + DICT_WAIT then
|
||||
dict_check(false, string.format(
|
||||
"the payload consumed '%s' (DICT_BYTES=%d RX_RING_POS=%d expected %d RX_RING_ACC=%d RX_DGRAM=%d)",
|
||||
dict_last.label, prog:read_u32(DICT_BYTES),
|
||||
prog:read_u32(RX_RING_POS), dict_pos,
|
||||
prog:read_u32(RX_RING_ACC), prog:read_u32(RX_DGRAM)))
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
dict_check(prog:read_u32(DICT_BYTES) == dict_last.bytes,
|
||||
string.format("%s: DICT_BYTES=%d (expected %d)",
|
||||
dict_last.label, prog:read_u32(DICT_BYTES), dict_last.bytes))
|
||||
dict_check(prog:read_u32(DICT_BAD) == dict_last.bad,
|
||||
string.format("%s: DICT_BAD=%d (expected %d)",
|
||||
dict_last.label, prog:read_u32(DICT_BAD), dict_last.bad))
|
||||
dict_check(prog:read_u32(DICT_READY) == dict_last.ready,
|
||||
string.format("%s: DICT_READY=%d (expected %d)",
|
||||
dict_last.label, prog:read_u32(DICT_READY), dict_last.ready))
|
||||
dict_last = nil
|
||||
end
|
||||
if dict_at >= #dict_plan then
|
||||
-- The upload is complete. Check the bytes the payload actually
|
||||
-- holds, not just its own count of them.
|
||||
okv, first, why = dict_verify()
|
||||
dict_check(okv, why or string.format(
|
||||
"the 32 KB at POOL_PTR + %d is the dictionary sent (first bad byte %d)",
|
||||
DICT_OFF, first))
|
||||
dict_check(inspect_pool(),
|
||||
"the startup allocation is still the one under test")
|
||||
-- A tile frame after the dictionary is complete: Task 4 paints
|
||||
-- it. This tree has no tile blit, so it is reported rather than
|
||||
-- asserted -- the assertion above (before DICT_READY) is Task
|
||||
-- 3's; the paint is Task 4's.
|
||||
a = { f = make_frame(KIND_TILES_FULL, 0x0062, 0, 0,
|
||||
DRAW_ROWS * RX_ROW_BYTES,
|
||||
function(i) return 0x5A end),
|
||||
n = 16 + DRAW_ROWS * RX_ROW_BYTES,
|
||||
label = nil }
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted the post-dictionary TILES_FULL frame")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
dict_phase = 4
|
||||
dict_phase_at = frames
|
||||
return false
|
||||
end
|
||||
dict_at = dict_at + 1
|
||||
dict_phase_at = frames
|
||||
a = dict_plan[dict_at]
|
||||
if not dict_take(a) then
|
||||
dict_check(false, "the ring accepted '" .. a.label .. "'")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if dict_phase == 4 then
|
||||
if not dict_settled() then
|
||||
if frames >= dict_phase_at + DICT_WAIT then
|
||||
dict_check(false, "the post-dictionary TILES_FULL frame was consumed")
|
||||
dict_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
dict_check(prog:read_u32(DICT_READY) == 1
|
||||
and prog:read_u32(DICT_BYTES) == DICT_SIZE,
|
||||
"a TILES_FULL after DICT_READY leaves the dictionary record alone")
|
||||
dict_lines[#dict_lines + 1] = string.format(
|
||||
"INFO post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints this frame; this tree has no tile blit)",
|
||||
prog:read_u32(TILES_DROP), prog:read_u32(RX_OK))
|
||||
print(string.format("VID: dict: post-dictionary TILES_FULL: TILES_DROP=%d RX_OK=%d (Task 4 paints it)",
|
||||
prog:read_u32(TILES_DROP), prog:read_u32(RX_OK)))
|
||||
dict_dump(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
|
||||
@@ -1431,6 +1863,13 @@ emu.register_frame_done(function()
|
||||
end
|
||||
end
|
||||
|
||||
-- Mode dict: the dictionary upload and the tile-frame dispatch (dict_step()).
|
||||
if entered and MODE == "dict" then
|
||||
if dict_step() then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw mode: inject once, then wait for the payload to accept the frame.
|
||||
-- Two separate observations, because they answer different questions:
|
||||
--
|
||||
@@ -1534,7 +1973,15 @@ emu.register_frame_done(function()
|
||||
return
|
||||
end
|
||||
|
||||
if MODE == "dict" and frames >= STOP_AT then
|
||||
print(string.format("VID: dict mode did not finish (phase %d) by frame %d",
|
||||
dict_phase, frames))
|
||||
dict_dump(true)
|
||||
return
|
||||
end
|
||||
|
||||
if frames >= STOP_AT and MODE ~= "stream" and MODE ~= "ring"
|
||||
and MODE ~= "dict"
|
||||
and not (MODE == "draw" and draw_at ~= nil) then
|
||||
if not entered then
|
||||
print("VID: payload never entered -> FAIL")
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
* ------ ---- ------------------------------------------------------
|
||||
* 0 2 magic, ASCII 'V','S'
|
||||
* 2 1 version, 0x01
|
||||
* 3 1 kind (KIND_STRIP = 1, KIND_STATUS = 5)
|
||||
* 3 1 kind (see the FP_KIND_* block below; the numbers are exactly
|
||||
* frame.py's KINDS table)
|
||||
* 4 2 frame_no, u16 little-endian
|
||||
* 6 2 payload_len, u16 little-endian
|
||||
* 8 1 rows
|
||||
@@ -48,14 +49,31 @@
|
||||
* RX_FCS_DROP +0x260 0x01810260 datagrams the chip flagged bad
|
||||
* (no bytes delivered; each
|
||||
* abandons its frame)
|
||||
* DICT_READY +0x280 0x01810280 1 = the 32 KB dictionary is
|
||||
* complete (Task 3)
|
||||
* DICT_BYTES +0x2A0 0x018102A0 dictionary bytes accepted, a
|
||||
* contiguous prefix from entry 0
|
||||
* DICT_BAD +0x2C0 0x018102C0 dictionary chunks refused
|
||||
* (gap/overlap/duplicate offset,
|
||||
* overrun, or after ready)
|
||||
* TILES_DROP +0x2E0 0x018102E0 tile frames that did not become
|
||||
* pixels (no dictionary yet, or
|
||||
* no tile paint in this tree)
|
||||
* 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
|
||||
* VID_ALIVE +0x60 0x01810160 heartbeat while the loop runs
|
||||
* RX_OK +0x80 0x01810180 frames accepted by fp_parse
|
||||
* RX_BAD +0xA0 0x018101A0 frames rejected by fp_parse
|
||||
* RX_LAST_LEN +0xC0 0x018101C0 payload_len of the last accepted
|
||||
* RX_SLOT +0xE0 0x018101E0 slot of the last accepted (0=A)
|
||||
* RX_OK +0x80 0x01810180 frames that reached the screen
|
||||
* (a strip painted; Task 4 adds
|
||||
* tiles). A dictionary chunk
|
||||
* is counted by DICT_BYTES, not
|
||||
* here: it is not a picture.
|
||||
* RX_BAD +0xA0 0x018101A0 frames the receiver refused
|
||||
* (fp_parse, or a kind the
|
||||
* dispatcher cannot act on)
|
||||
* RX_LAST_LEN +0xC0 0x018101C0 payload_len of the last frame
|
||||
* that reached the screen
|
||||
* RX_SLOT +0xE0 0x018101E0 slot of that frame (0=A)
|
||||
* OS_TOUCH +0x220 0x01810220 times the VRAM canary was not ours
|
||||
*
|
||||
* NOTE ON OWNERSHIP OF THE PAGE: nothing allocates this page. The firmware's
|
||||
@@ -115,6 +133,44 @@
|
||||
* further accepted frame; nothing reads it in a continuous stream.
|
||||
* ---------------------------------------------------------------------------
|
||||
*
|
||||
* Task 3: the frame kinds and the dictionary upload.
|
||||
*
|
||||
* The kind byte is frame.py's KINDS table, written down twice -- there in
|
||||
* Python, here in C -- so the numbers below must be changed in both places at
|
||||
* once. harness/udpstream/tests/test_frame.py parses this file and compares
|
||||
* the two tables, so a renumbering on one side fails a host test rather than
|
||||
* silently mis-dispatching on the wire.
|
||||
*
|
||||
* FP_KIND_STRIP 1 packed 1-bpp rows, blitted (scheme A)
|
||||
* FP_KIND_TILES_FULL 2 a full 40x32 screen of dictionary indices
|
||||
* FP_KIND_TILES_DELTA 3 only the tiles that changed
|
||||
* FP_KIND_DICT_CHUNK 4 one slice of the 32x32 tile dictionary
|
||||
* FP_KIND_STATUS 5 the terminal's report, the other way
|
||||
*
|
||||
* A DICT_CHUNK's payload (tiledict.py writes it) is
|
||||
*
|
||||
* offset 2 u16 little-endian: dictionary position of the first body byte
|
||||
* body n dictionary bytes, at most 1448 (1450-byte payload budget)
|
||||
*
|
||||
* The dictionary is 256 entries of 128 bytes (32 KB) and lives in the payload's
|
||||
* own pool at POOL_PTR + 2 * SLOT_BYTES -- right after the slot pair, the
|
||||
* placement Task 4 consumes. The assembler in vidstream.c refuses, and counts
|
||||
* in DICT_BAD: an offset that is not the next contiguous byte (gaps,
|
||||
* overlaps, duplicates, reordering), an offset inside an entry that is not a
|
||||
* continuation of the accepted prefix (the 128-byte rule), a chunk that would
|
||||
* overrun the last entry, and any chunk after DICT_READY (the dictionary is
|
||||
* fixed for the clip -- ruling R1 of plan 2). A refused chunk leaves the
|
||||
* prefix alone, so the sender's next good chunk still lands. DICT_READY is
|
||||
* set by the chunk that makes DICT_BYTES exactly 32768: 256 whole entries, so
|
||||
* a short upload is never complete and never paints anything.
|
||||
*
|
||||
* Until DICT_READY a TILES_FULL/TILES_DELTA frame cannot be decoded at all;
|
||||
* it is refused without touching VRAM and counted in TILES_DROP. Those are
|
||||
* dispatch rules of the receiver, not of fp_parse(): the header is well formed
|
||||
* and fp_parse() accepts it, which is exactly why the dispatcher must not
|
||||
* forget it.
|
||||
* ---------------------------------------------------------------------------
|
||||
*
|
||||
* K&R C only. The 1987 toolchain in build-stream.sh compiles this header, so
|
||||
* there are no ANSI prototypes, no '//' comments and no 'const' (romapi.h is
|
||||
* written the same way): the parameter lists live in the comments and in the
|
||||
@@ -136,8 +192,11 @@
|
||||
#define FP_MAGIC1 0x53 /* 'S' */
|
||||
#define FP_VERSION 1
|
||||
|
||||
#define FP_KIND_STRIP 1
|
||||
#define FP_KIND_STATUS 5
|
||||
#define FP_KIND_STRIP 1
|
||||
#define FP_KIND_TILES_FULL 2
|
||||
#define FP_KIND_TILES_DELTA 3
|
||||
#define FP_KIND_DICT_CHUNK 4
|
||||
#define FP_KIND_STATUS 5
|
||||
|
||||
/* Marker page cells; see the table above. */
|
||||
#define MARK_PAGE 0x01810000
|
||||
@@ -172,6 +231,14 @@
|
||||
#define RX_DGRAM MARK_CELL(0x48)
|
||||
#define RX_FCS_DROP MARK_CELL(0x4c)
|
||||
|
||||
/* Task 3: the dictionary record and the tile-frame drop count. Cells 0x50 to
|
||||
* 0x5c fill the gap between RX_FCS_DROP and RX_SLOT_A at the same four-byte
|
||||
* spacing every other cell uses. */
|
||||
#define DICT_READY MARK_CELL(0x50)
|
||||
#define DICT_BYTES MARK_CELL(0x54)
|
||||
#define DICT_BAD MARK_CELL(0x58)
|
||||
#define TILES_DROP MARK_CELL(0x5c)
|
||||
|
||||
#define RX_SLOT_A MARK_CELL(0x60)
|
||||
#define RX_SLOT_B MARK_CELL(0x64)
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import struct
|
||||
import pytest
|
||||
|
||||
from harness.udpstream.frame import (CRC_OFFSET, HEADER_SIZE, KIND_STATUS,
|
||||
KIND_STRIP, MAGIC, VERSION, STATUS_SIZE,
|
||||
KIND_STRIP, KINDS, MAGIC, VERSION,
|
||||
STATUS_SIZE,
|
||||
BROADCAST, ETHERTYPE, TERMINAL_MAC,
|
||||
crc16, ethernet_frame, pack_header,
|
||||
pack_status, seal_header, unpack_header,
|
||||
@@ -288,3 +289,27 @@ def test_ethernet_frame_pads_to_the_minimum_only_at_the_end():
|
||||
BROADCAST + TERMINAL_MAC + b"\x88\xB7" + long)
|
||||
# Off by default: the chunking layer decides, and only for the last chunk.
|
||||
assert len(ethernet_frame(b"VS" + bytes(10), src=TERMINAL_MAC)) == 26
|
||||
|
||||
|
||||
def test_payload_header_kinds_match_the_wire_table():
|
||||
"""The terminal's frame_parse.h mirrors this module's KINDS table.
|
||||
|
||||
The kind byte is written by frame.py and read by the payload, so the same
|
||||
number is written down in Python and in C. A renumbering on one side would
|
||||
silently mis-dispatch every frame on the other: a TILES_FULL would be
|
||||
painted as a strip, or a DICT_CHUNK would be counted as a frame. No MAME
|
||||
check would notice until the kind it exercises itself changed, which is why
|
||||
this parses the C header rather than trusting the comment that says the two
|
||||
agree.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
header = (pathlib.Path(__file__).resolve().parents[1] / "frame_parse.h")
|
||||
text = header.read_text()
|
||||
found = {name: int(value) for name, value in
|
||||
re.findall(r"^#define\s+FP_KIND_(\w+)\s+(\d+)\b", text, re.M)}
|
||||
assert found, "frame_parse.h has no FP_KIND_* definitions"
|
||||
assert found == KINDS
|
||||
# And the status kind the payload builds is the one frame.py reads.
|
||||
assert found["STATUS"] == KIND_STATUS
|
||||
@@ -13,17 +13,33 @@
|
||||
* 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.
|
||||
* Plan 2's Task 3 (this one) adds the tile dictionary: KIND_DICT_CHUNK frames
|
||||
* are assembled into a 32 KB block after the two slots, and the dispatcher
|
||||
* learns the TILES_FULL/TILES_DELTA kinds so a frame that arrives before the
|
||||
* dictionary is complete is refused and counted instead of being mistaken for
|
||||
* a strip.
|
||||
*
|
||||
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
|
||||
*
|
||||
* +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
|
||||
* +0x0C000 dictionary 32768 bytes (256 x 128 B, Task 3)
|
||||
* +0x14000 framebuffer 163840 bytes (scheme A reserve; see below)
|
||||
* +0x3C000 LANCE block 4096 bytes (init block, TX ring, status
|
||||
* frame, RX ring)
|
||||
*
|
||||
* The LANCE block is what was slack. Its ring bases are aligned at run time
|
||||
* The dictionary sits at POOL_PTR + 2 * SLOT_BYTES, which is the placement the
|
||||
* plan fixes and plan 2's Task 4 consumes. The 160 KB framebuffer reserve
|
||||
* follows it: this decoder paints straight from the RX fragments (there is no
|
||||
* per-frame CPU copy, spec 8.1) and never touches that region, so it is slack
|
||||
* today, but the layout adds up to exactly POOL_BYTES -- 24576 + 24576 + 32768
|
||||
* + 163840 + 4096 = 249856. dict_slack_check below pins the part Task 3
|
||||
* depends on (the slot pair and the 32 KB dictionary); the LANCE block's
|
||||
* offsets at 0x3C000 were fixed before it and are not re-checked here. Had
|
||||
* the dictionary gone after the framebuffer, the plan's base would not be
|
||||
* POOL_PTR + 2 * SLOT_BYTES and Task 4 would read 160 KB of zeroes.
|
||||
*
|
||||
* The LANCE block is the 4 KB tail. 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 TX descriptors, the RX ring after that.
|
||||
*
|
||||
@@ -75,6 +91,9 @@
|
||||
* 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.
|
||||
* Plan 2's Task 3 takes cells 0x50 (DICT_READY), 0x54 (DICT_BYTES), 0x58
|
||||
* (DICT_BAD) and 0x5c (TILES_DROP) -- the gap between RX_FCS_DROP and
|
||||
* RX_SLOT_A, still at four-byte spacing.
|
||||
*
|
||||
* K&R C only: no prototypes, no '//', declarations before statements.
|
||||
*/
|
||||
@@ -112,6 +131,41 @@
|
||||
*/
|
||||
#define SLOT_BYTES 24576
|
||||
|
||||
/*
|
||||
* Plan 2's Task 3: the tile dictionary.
|
||||
*
|
||||
* 256 entries of 32x32 1-bpp tiles, 128 bytes each = 32 KB, held in the
|
||||
* startup pool right after the slot pair -- POOL_PTR + 2 * SLOT_BYTES, the
|
||||
* placement the plan fixes and the tile blit (Task 4) consumes. The chunks
|
||||
* arrive as KIND_DICT_CHUNK frames whose payload is a u16 little-endian
|
||||
* offset plus up to 1448 dictionary bytes (tiledict.py); dict_chunk() is the
|
||||
* assembler and states the rules it enforces.
|
||||
*
|
||||
* The pool layout in the header comment is exactly POOL_BYTES, so a build with
|
||||
* a smaller request cannot hold the dictionary: dict_slack_check below refuses
|
||||
* it at build time, and the run-time guard in main() refuses to point the
|
||||
* assembler at memory outside the allocation. (POOL_BYTES is generated into
|
||||
* poolcfg.h by build-stream.sh; the fallback pool is STATIC_BYTES, the same
|
||||
* size.)
|
||||
*/
|
||||
#define DICT_OFF (2 * SLOT_BYTES) /* 0x0C000: after the two slots */
|
||||
#define DICT_ENTRY 128 /* one 32x32 1-bpp tile, in bytes */
|
||||
#define DICT_SIZE (DICT_ENTRY * 256) /* 256 entries = 32 KB */
|
||||
|
||||
/*
|
||||
* The arithmetic check the brief asks for, as a compile-time assertion.
|
||||
*
|
||||
* A #if here does NOT work: this preprocessor compiled the file with
|
||||
* POOL_BYTES = 50000 and no complaint, so it does not fold the macro
|
||||
* arithmetic in #if. An `extern' array with a negative length is accepted
|
||||
* too (measured). A DEFINED array whose length is 1 or -1 does fail the
|
||||
* build (measured: POOL_BYTES=50000 -> "1 Error, No Warnings" and no
|
||||
* VIDSTRM.OUT), at the cost of one byte of .bss on a good build. The
|
||||
* dictionary must fit after the slot pair, which is the arithmetic below; the
|
||||
* full layout is exactly POOL_BYTES (header comment).
|
||||
*/
|
||||
char dict_slack_check[(POOL_BYTES >= (DICT_OFF + DICT_SIZE)) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* Task 6: the terminal's status frame.
|
||||
*
|
||||
@@ -274,6 +328,27 @@ static unsigned int rx_frag_off[RX_DESC];
|
||||
static unsigned int rx_frag_ptr[RX_DESC];
|
||||
static unsigned int rx_frag_len[RX_DESC];
|
||||
|
||||
/*
|
||||
* Plan 2's Task 3 state. Like every other static here it is cleared by main()
|
||||
* before use: the toolchain never runs .cinit.
|
||||
*
|
||||
* dict_base bit address of the dictionary in the pool (POOL_PTR + DICT_OFF)
|
||||
* dict_bytes bytes accepted, always a contiguous prefix from entry 0
|
||||
* dict_bad chunks refused (gap/overlap/duplicate, overrun, after ready)
|
||||
* dict_ready 1 once dict_bytes is exactly DICT_SIZE
|
||||
* tiles_drop tile frames that could not become pixels
|
||||
*
|
||||
* dict_bytes is the assembler's only cursor. It is not a copy of anything the
|
||||
* sender says: the offset in a chunk is checked against it, never trusted, so a
|
||||
* missing or replayed chunk is visible as an offset mismatch rather than a hole
|
||||
* in the dictionary.
|
||||
*/
|
||||
static unsigned int dict_base;
|
||||
static unsigned int dict_bytes;
|
||||
static unsigned int dict_bad;
|
||||
static unsigned int dict_ready;
|
||||
static unsigned int tiles_drop;
|
||||
|
||||
/*
|
||||
* blit_row(src, daddr_xy, dydx)
|
||||
*
|
||||
@@ -380,6 +455,129 @@ fp_header *h;
|
||||
vid_eint_();
|
||||
}
|
||||
|
||||
/*
|
||||
* frag_copy(dst, pos, n) -- copy n frame bytes starting at frame offset `pos`
|
||||
* into memory at bit address `dst`. 1 on success, 0 when the fragment table
|
||||
* does not hold the whole range (a truncated frame or a gap).
|
||||
*
|
||||
* The frame's bytes are not contiguous in the slot: each datagram's body sits
|
||||
* 14 bytes into its own 1526-byte buffer, so a dictionary chunk that spans two
|
||||
* datagrams -- every 1450-byte chunk does, since 16 + 1450 = 1466 -- has to be
|
||||
* stored piece by piece. paint() walks the same table for the blit, so this is
|
||||
* one reading of the fragment map, not a second one.
|
||||
*/
|
||||
static int frag_copy(dst, pos, n)
|
||||
unsigned int dst;
|
||||
unsigned int pos;
|
||||
unsigned int n;
|
||||
{
|
||||
unsigned int k, f, room, i;
|
||||
|
||||
k = 0;
|
||||
while (n > 0) {
|
||||
while (k < rx_nfrag
|
||||
&& pos >= rx_frag_off[k] + rx_frag_len[k]) {
|
||||
k = k + 1;
|
||||
}
|
||||
if (k >= rx_nfrag) {
|
||||
return 0; /* truncated */
|
||||
}
|
||||
if (pos < rx_frag_off[k]) {
|
||||
return 0; /* a gap: never copy across it */
|
||||
}
|
||||
f = pos - rx_frag_off[k];
|
||||
room = rx_frag_len[k] - f;
|
||||
if (room > n) {
|
||||
room = n;
|
||||
}
|
||||
for (i = 0; i < room; i++) {
|
||||
*(unsigned char *)(dst + i * 8) =
|
||||
*(unsigned char *)(rx_frag_ptr[k] + (f + i) * 8);
|
||||
}
|
||||
dst = dst + room * 8;
|
||||
pos = pos + room;
|
||||
n = n - room;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* One refused dictionary chunk: count it and leave the accepted prefix alone,
|
||||
* so the sender's next good chunk still lands. */
|
||||
static void dict_refuse()
|
||||
{
|
||||
dict_bad = dict_bad + 1;
|
||||
*(unsigned int *)DICT_BAD = dict_bad;
|
||||
}
|
||||
|
||||
/*
|
||||
* dict_chunk(h) -- assemble one slice of the 32 KB tile dictionary.
|
||||
*
|
||||
* The wire form (tiledict.py, restated in frame_parse.h) is the VS header,
|
||||
* then a u16 little-endian dictionary offset, then up to 1448 dictionary bytes.
|
||||
* The rules, and what each refuses:
|
||||
*
|
||||
* payload_len >= 3 the offset and at least one dictionary byte.
|
||||
* off == dict_bytes ascending and contiguous. A gap, an overlap, a
|
||||
* duplicate and a reordered chunk all fail this one
|
||||
* test. It is also the 128-byte rule Task 2's
|
||||
* review carried: an offset inside an entry is legal
|
||||
* only as a continuation of the accepted prefix --
|
||||
* which is exactly "off equals the prefix end" -- and
|
||||
* a first chunk that does not begin at entry 0 fails
|
||||
* it too.
|
||||
* off + body <= DICT_SIZE past the last entry; the tail chunk that would
|
||||
* overrun 32,768 is the "partial final entry" case.
|
||||
* frag_copy() succeeds the bytes are really in the fragment table.
|
||||
*
|
||||
* DICT_READY is set only by the chunk that makes dict_bytes exactly DICT_SIZE:
|
||||
* 256 whole entries. A short upload is therefore never ready, so a tile frame
|
||||
* that follows it is still refused -- the wire has no end-of-upload marker to
|
||||
* do that earlier.
|
||||
*
|
||||
* After DICT_READY a further chunk is refused rather than restarting the
|
||||
* upload: plan ruling R1 makes the dictionary fixed for the clip, and a
|
||||
* re-upload would need a coordinated paint stall the protocol does not have.
|
||||
*
|
||||
* The offset is read out of the first fragment, whose body always holds the VS
|
||||
* header and the first bytes after it; frag_copy() then handles the body
|
||||
* proper, which may cross into the next datagram.
|
||||
*/
|
||||
static void dict_chunk(h)
|
||||
fp_header *h;
|
||||
{
|
||||
unsigned int off, body, p;
|
||||
|
||||
if (dict_ready || dict_base == 0 || h->payload_len < 3) {
|
||||
dict_refuse();
|
||||
return;
|
||||
}
|
||||
if (rx_frag_len[0] < h->payload_off + 2) {
|
||||
dict_refuse();
|
||||
return;
|
||||
}
|
||||
p = rx_frag_ptr[0] + h->payload_off * 8;
|
||||
off = (unsigned int)*(unsigned char *)p
|
||||
| ((unsigned int)*(unsigned char *)(p + 8) << 8);
|
||||
body = h->payload_len - 2;
|
||||
if (off != dict_bytes || off + body > DICT_SIZE) {
|
||||
dict_refuse();
|
||||
return;
|
||||
}
|
||||
if (!frag_copy(dict_base + off * 8, h->payload_off + 2, body)) {
|
||||
dict_refuse();
|
||||
return;
|
||||
}
|
||||
dict_bytes = off + body;
|
||||
*(unsigned int *)DICT_BYTES = dict_bytes;
|
||||
*(unsigned int *)DICT_BAD = dict_bad;
|
||||
if (dict_bytes == DICT_SIZE) {
|
||||
dict_ready = 1;
|
||||
/* Published last, after DICT_BYTES: a reader that sees readiness
|
||||
* sees the complete record. */
|
||||
*(unsigned int *)DICT_READY = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The LANCE, transmit only. The sequence is lance.c's, and the comment on
|
||||
* each step names what makes it necessary (harness/mame/README.md rule 4: a
|
||||
@@ -629,12 +827,37 @@ unsigned int i;
|
||||
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;
|
||||
if (h.kind == FP_KIND_STRIP) {
|
||||
*(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 if (h.kind == FP_KIND_DICT_CHUNK) {
|
||||
/* A chunk is not a picture: it is counted by DICT_BYTES
|
||||
* and DICT_BAD, never by RX_OK. */
|
||||
dict_chunk(&h);
|
||||
} else if (h.kind == FP_KIND_TILES_FULL
|
||||
|| h.kind == FP_KIND_TILES_DELTA) {
|
||||
/*
|
||||
* Task 4 turns these into pixels. This tree has no tile
|
||||
* blit, and the point of the branch is that such a frame
|
||||
* is never silently forgotten: before DICT_READY there is
|
||||
* nothing to decode with, and after it this tree still
|
||||
* has no paint, so both are counted and neither touches
|
||||
* VRAM. (A frame that arrived before the dictionary and
|
||||
* was dropped cannot leave a half-painted screen: the
|
||||
* branch draws nothing at all.)
|
||||
*/
|
||||
tiles_drop = tiles_drop + 1;
|
||||
*(unsigned int *)TILES_DROP = tiles_drop;
|
||||
} else {
|
||||
/* A well-formed header whose kind nothing here dispatches
|
||||
* (KIND_STATUS is the terminal's own, travelling the other
|
||||
* way): received and refused, so it is a bad frame. */
|
||||
*(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1;
|
||||
}
|
||||
} else {
|
||||
*(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1;
|
||||
}
|
||||
@@ -979,6 +1202,18 @@ int main()
|
||||
*(unsigned int *)RX_DGRAM = 0;
|
||||
*(unsigned int *)OS_TOUCH = 0;
|
||||
|
||||
/* Plan 2's Task 3: the dictionary record starts empty and is published
|
||||
* as zero before the pool is known, so a reader never sees an earlier
|
||||
* run's readiness on a pool that has no dictionary behind it. */
|
||||
*(unsigned int *)DICT_READY = 0;
|
||||
*(unsigned int *)DICT_BYTES = 0;
|
||||
*(unsigned int *)DICT_BAD = 0;
|
||||
*(unsigned int *)TILES_DROP = 0;
|
||||
dict_ready = 0;
|
||||
dict_bytes = 0;
|
||||
dict_bad = 0;
|
||||
tiles_drop = 0;
|
||||
|
||||
/* Arm the VRAM canary before any frame is accepted, so a repaint
|
||||
* between now and the first blit is counted rather than mistaken for
|
||||
* our own last frame. */
|
||||
@@ -1004,6 +1239,18 @@ int main()
|
||||
slot_base[1] = ptr + SLOT_BYTES * 8;
|
||||
*(unsigned int *)RX_SLOT_A = slot_base[0];
|
||||
*(unsigned int *)RX_SLOT_B = slot_base[1];
|
||||
/* Plan 2's Task 3: the dictionary follows the slot pair. It is derived
|
||||
* from the allocation, never published separately -- POOL_PTR is the
|
||||
* record, and the pool layout above fixes the offset. build-stream.sh
|
||||
* asserts at build time that the request can hold it (dict_slack_check
|
||||
* above); this is the run-time half, for a payload built another way: a
|
||||
* pool too small for the dictionary leaves dict_base 0, every chunk is
|
||||
* refused and nothing is written past the allocation. */
|
||||
if (POOL_BYTES >= DICT_OFF + DICT_SIZE) {
|
||||
dict_base = ptr + DICT_OFF * 8;
|
||||
} else {
|
||||
dict_base = 0;
|
||||
}
|
||||
*(unsigned int *)POOL_PTR = ptr; /* publish last: a complete record */
|
||||
|
||||
/* Task 6: the status back-channel. The record starts empty, then the
|
||||
|
||||
Reference in new issue
Block a user