forked from arti/vt1200
feat(tiles): delta frames and periodic keyframes
This commit is contained in:
1 parent
2fa232ba56
commit
8f8454461d
6 files changed
+1214
-19
No files matched your search
@@ -9,6 +9,7 @@
|
||||
# VT1200_MODE=ring harness/mame/run-vidstream.sh # the RX ring's structure
|
||||
# VT1200_MODE=dict harness/mame/run-vidstream.sh # dictionary upload + kinds
|
||||
# VT1200_MODE=tiles harness/mame/run-vidstream.sh # the 32x32 tile blit
|
||||
# VT1200_MODE=delta harness/mame/run-vidstream.sh # deltas and keyframes
|
||||
# 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
|
||||
@@ -52,6 +53,19 @@
|
||||
# vram_tile.asm is the negative control: the frame is accepted and the pixels
|
||||
# are wrong, which only the checker can see.
|
||||
#
|
||||
# Mode delta asserts the changed-tile list: a TILES_FULL keyframe is painted,
|
||||
# then a one-tile TILES_DELTA must change exactly that 32x32 block and nothing
|
||||
# else, with RX_OK/TILES_DROP/RX_LAST_LEN saying so. Two negative controls
|
||||
# follow -- a delta naming a position off the 1,280-tile grid, and one whose
|
||||
# count disagrees with its length -- and each must be refused (TILES_DROP, no
|
||||
# RX_OK) with ZERO bytes of the screen changed; a good delta after them proves
|
||||
# the refusal left no latch behind. The Lua holds the before/after dumps in
|
||||
# memory and compares them itself: the assertion is a relation between two
|
||||
# screens, not a rendering to re-derive, and it is why the first negative
|
||||
# control's valid first entry matters (a decoder that painted as it decoded
|
||||
# would have painted it). Evidence: $LOGDIR/vram-delta-*.bin and
|
||||
# delta-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
|
||||
@@ -115,6 +129,12 @@ if [ "$MODE" = "tiles" ]; then
|
||||
rm -f "$LOGDIR/vram-tiles.bin" "$LOGDIR/dict-tiles.bin" \
|
||||
"$LOGDIR/indices-tiles.bin" "$LOGDIR/tiles-status.txt"
|
||||
fi
|
||||
if [ "$MODE" = "delta" ]; then
|
||||
rm -f "$LOGDIR/vram-delta-before.bin" "$LOGDIR/vram-delta-after.bin" \
|
||||
"$LOGDIR/vram-delta-reject.bin" "$LOGDIR/vram-delta-recover.bin" \
|
||||
"$LOGDIR/dict-delta.bin" "$LOGDIR/indices-delta.bin" \
|
||||
"$LOGDIR/delta-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"
|
||||
|
||||
+674
-3
@@ -81,6 +81,19 @@
|
||||
-- vram_tile.asm: the frame is then accepted (RX_OK moves) and the checker must
|
||||
-- FAIL on the picture.
|
||||
--
|
||||
-- Mode "delta" (Task 5) is the changed-tile list and the keyframe cadence. It
|
||||
-- uploads the same dictionary, paints one TILES_FULL keyframe, then delivers a
|
||||
-- one-tile TILES_DELTA and compares the screen before and after: all 128 bytes
|
||||
-- of that 32x32 block must change and NOT ONE byte outside it. The negative
|
||||
-- controls are a delta naming a position off the 1,280-tile grid (its first
|
||||
-- entry is valid on purpose, so a decoder that painted as it read would paint
|
||||
-- it) and a delta whose count disagrees with its length; both must be refused
|
||||
-- in TILES_DROP with zero pixels changed, and a good delta afterwards proves
|
||||
-- the refusal left no latch. Unlike tiles mode the assertion is a comparison
|
||||
-- of two screens, so this Lua makes it: the property is relational, not a
|
||||
-- rendering the geometry could be re-derived from. Evidence:
|
||||
-- vram-delta-{before,after,reject,recover}.bin and delta-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
|
||||
@@ -100,7 +113,8 @@
|
||||
-- harness/udpstream/frame_parse.h.
|
||||
--
|
||||
-- Env:
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream, ring, dict or tiles
|
||||
-- VT1200_MODE alloc (default), rx, draw, stream, ring, dict, tiles or
|
||||
-- delta
|
||||
-- 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, ring, dict and tiles modes write their
|
||||
@@ -121,6 +135,8 @@
|
||||
-- chunk to be consumed (default 300)
|
||||
-- VID_TILE_WAIT tiles mode only: emulated frames to wait for a dictionary
|
||||
-- chunk or the tile frame to be consumed (default 300)
|
||||
-- VID_DELTA_WAIT delta mode only: emulated frames to wait for a dictionary
|
||||
-- chunk or a delta frame 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)
|
||||
@@ -161,6 +177,11 @@ elseif MODE == "tiles" and not os.getenv("VID_END") then
|
||||
-- frame, each bounded (VID_TILE_WAIT, 300); the first timeout dumps and
|
||||
-- exits, so this is only the catastrophic-stall backstop.
|
||||
DEFAULT_END = AT + 26 * tonumber(os.getenv("VID_TILE_WAIT") or "300") + 120
|
||||
elseif MODE == "delta" and not os.getenv("VID_END") then
|
||||
-- Same shape as tiles mode plus the keyframe and the three deltas: one
|
||||
-- ring wait, 23 dictionary chunks and five frames, each bounded by
|
||||
-- VID_DELTA_WAIT.
|
||||
DEFAULT_END = AT + 30 * tonumber(os.getenv("VID_DELTA_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")
|
||||
@@ -292,7 +313,7 @@ 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" and MODE ~= "dict"
|
||||
and MODE ~= "tiles" then
|
||||
and MODE ~= "tiles" and MODE ~= "delta" then
|
||||
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
|
||||
machine:exit()
|
||||
return
|
||||
@@ -607,6 +628,9 @@ local DRAW_FILES = { "vram-draw.bin", "frame-draw.bin", "draw-status.txt" }
|
||||
local STREAM_FILES = { "stream-status.bin", "stream-status.txt" }
|
||||
local TILE_FILES = { "vram-tiles.bin", "dict-tiles.bin", "indices-tiles.bin",
|
||||
"tiles-status.txt" }
|
||||
local DELTA_FILES = { "vram-delta-before.bin", "vram-delta-after.bin",
|
||||
"vram-delta-reject.bin", "vram-delta-recover.bin", "dict-delta.bin",
|
||||
"indices-delta.bin", "delta-status.txt" }
|
||||
|
||||
local function clear_dump_files()
|
||||
if MODE == "dict" then
|
||||
@@ -619,6 +643,12 @@ local function clear_dump_files()
|
||||
end
|
||||
return
|
||||
end
|
||||
if MODE == "delta" then
|
||||
for _, name in ipairs(DELTA_FILES) do
|
||||
os.remove(DUMP_DIR .. "/" .. name)
|
||||
end
|
||||
return
|
||||
end
|
||||
if MODE ~= "draw" and MODE ~= "stream" then
|
||||
return
|
||||
end
|
||||
@@ -1940,6 +1970,633 @@ return {
|
||||
}
|
||||
end)()
|
||||
|
||||
-- --- Task 5: delta frames and keyframes ------------------------------------
|
||||
--
|
||||
-- Mode "delta" is the changed-tile list, checked RELATIVELY: the screen is
|
||||
-- read before and after each frame and only the 32x32 block the frame names may
|
||||
-- differ. That is the property a delta decoder must have -- a delta is a
|
||||
-- promise not to touch the other 1,248 tiles -- and it is not something
|
||||
-- tile_check.py can see, because tile_check.py renders one whole frame.
|
||||
--
|
||||
-- The sequence:
|
||||
--
|
||||
-- 1. upload the same 32 KB dictionary tiles mode uses (every entry and every
|
||||
-- row distinct) and read it back byte for byte;
|
||||
-- 2. paint ONE TILES_FULL keyframe (index = (i*7 + 3) & 0xFF, as in tiles
|
||||
-- mode) and dump the screen as vram-delta-before.bin -- the reference;
|
||||
-- 3. deliver ONE TILES_DELTA naming tile (tx=11, ty=7) with a new entry;
|
||||
-- dump vram-delta-after.bin and assert RX_OK moved, TILES_DROP did not,
|
||||
-- exactly that block's 128 bytes changed, they now equal dictionary[new],
|
||||
-- and no byte outside the block changed;
|
||||
-- 4. negative control A: a delta whose FIRST entry is on the grid and whose
|
||||
-- second position is 1,280 (off the grid). A decoder that painted as it
|
||||
-- decoded would paint the first tile and report success; the assertions
|
||||
-- are TILES_DROP +1, RX_OK and RX_LAST_LEN unchanged, and ZERO bytes of
|
||||
-- the active area changed since the previous dump. The first entry is
|
||||
-- what makes "nothing painted" mean validated-first, not happened-to-fail;
|
||||
-- 5. negative control B: a count that disagrees with the payload length (two
|
||||
-- entries claimed, one carried). Same assertions -- the list may not run
|
||||
-- past the frame;
|
||||
-- 6. recover: one more valid delta at a different tile, so the refusals left
|
||||
-- no latch behind; the assertions are the same shape as step 3.
|
||||
--
|
||||
-- The active area is screen rows 0..1023, bytes 0..159 of each 256-byte row.
|
||||
-- Bytes 160..255 are outside every tile and belong to the resident OS, exactly
|
||||
-- as tile_check.py says; the canary word at row 1023 / column 1280 is asserted
|
||||
-- separately (SENTINEL) and OS_TOUCH must stay 0.
|
||||
--
|
||||
-- This Lua asserts the pixels itself, because the property is a comparison of
|
||||
-- two dumps rather than a rendering the geometry could be re-derived from: it
|
||||
-- holds both screens in memory, and writing a third file for a checker to
|
||||
-- diff would only move the same string comparison outside the run.
|
||||
--
|
||||
-- The mode's own state lives in a closure for the same reason tiles mode's
|
||||
-- does: Lua 5.3 allows 200 locals in the main chunk of a file this size. The
|
||||
-- dictionary and chunk helpers are duplicated here rather than shared, so this
|
||||
-- mode cannot change tiles mode's behaviour by being added.
|
||||
local delta = (function()
|
||||
local TILE_PIXELS = 32
|
||||
local TILE_COLS = 40
|
||||
local TILE_ROWS = 32
|
||||
local TILES_PER_FRAME = TILE_COLS * TILE_ROWS
|
||||
local TILE_DICT_SIZE = 32768
|
||||
local TILE_DICT_BYTES = 128
|
||||
local DICT_CHUNK_BODY = 1448 -- tiledict.MAX_BODY
|
||||
local DICT_OFF = 2 * SLOT_BYTES -- 0xC000: after the two slots
|
||||
local DELTA_HDR = 2 -- the u16 count, frame_parse.h
|
||||
local ACTIVE_ROW = 160 -- 40 tiles x 4 bytes: the pixels tiles cover
|
||||
local DELTA_WAIT = tonumber(os.getenv("VID_DELTA_WAIT") or "300")
|
||||
|
||||
-- Tile positions this mode uses. TOUCH and RECOVER are ordinary interior
|
||||
-- tiles (a corner would also show a swapped tx/ty, which tiles mode already
|
||||
-- covers); BAD_POS is the entry a progressively-painting decoder would paint
|
||||
-- before it met the off-grid position.
|
||||
local TOUCH = 7 * TILE_COLS + 11 -- (tx 11, ty 7)
|
||||
local BAD_POS = 3 * TILE_COLS + 22 -- (tx 22, ty 3)
|
||||
local RECOVER = 20 * TILE_COLS + 3 -- (tx 3, ty 20)
|
||||
local BAD_OUT = TILES_PER_FRAME -- 1,280: the first position off the grid
|
||||
|
||||
-- The dictionary: every 128-byte entry and every row inside it differs from
|
||||
-- its neighbours, the same pattern tiles mode uses. A delta that painted the
|
||||
-- wrong entry, or a blit that re-read row 0, cannot match.
|
||||
local function tile_byte(entry, row, col)
|
||||
return (entry * 37 + row * 11 + col * 3 + 5) & 0xFF
|
||||
end
|
||||
|
||||
local function tile_dictionary()
|
||||
local out = {}
|
||||
for e = 0, 255 do
|
||||
local entry = {}
|
||||
for r = 0, TILE_PIXELS - 1 do
|
||||
for c = 0, 3 do
|
||||
entry[#entry + 1] = string.char(tile_byte(e, r, c))
|
||||
end
|
||||
end
|
||||
out[#out + 1] = table.concat(entry)
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
local TILE_DICT = tile_dictionary()
|
||||
|
||||
local function tile_index(i)
|
||||
return (i * 7 + 3) & 0xFF
|
||||
end
|
||||
|
||||
local TILE_INDICES = (function()
|
||||
local out = {}
|
||||
for i = 0, TILES_PER_FRAME - 1 do
|
||||
out[#out + 1] = string.char(tile_index(i))
|
||||
end
|
||||
return table.concat(out)
|
||||
end)()
|
||||
|
||||
local delta_phase = 0
|
||||
local delta_phase_at = nil
|
||||
local delta_fail = 0
|
||||
local delta_lines = {}
|
||||
local delta_pos = 0 -- RX_RING_POS expected once the last frame settles
|
||||
local delta_last = 0 -- datagrams the frame in flight is waiting on
|
||||
local delta_at = 0
|
||||
local delta_dict_matched = 0
|
||||
local delta_before = nil -- the screen after the keyframe
|
||||
local delta_prev = nil -- the screen after the previous accepted frame
|
||||
local delta_after = nil
|
||||
local delta_reject = nil
|
||||
local delta_recover = nil
|
||||
local delta_last_len = 0
|
||||
|
||||
local function delta_chunk_frame(off, body)
|
||||
local f = make_frame(KIND_DICT_CHUNK, 0, 0, 0, 2 + body, function(k)
|
||||
if k == 0 then return off & 0xFF end
|
||||
if k == 1 then return (off >> 8) & 0xFF end
|
||||
return TILE_DICT:byte(off + k - 1)
|
||||
end)
|
||||
return f, 16 + 2 + body
|
||||
end
|
||||
|
||||
local function delta_chunk(i)
|
||||
local off = i * DICT_CHUNK_BODY
|
||||
local n = TILE_DICT_SIZE - off
|
||||
|
||||
if n > DICT_CHUNK_BODY then
|
||||
n = DICT_CHUNK_BODY
|
||||
end
|
||||
return delta_chunk_frame(off, n)
|
||||
end
|
||||
|
||||
-- One TILES_DELTA payload (frame_parse.h): u16 count, `count` u16 positions,
|
||||
-- `count` one-byte entries. `positions` and `values` are Lua arrays of the
|
||||
-- same length, strictly ascending by construction at every call site here.
|
||||
local function delta_bytes(positions, values)
|
||||
local n = #positions
|
||||
local b = {}
|
||||
b[1] = n & 0xFF
|
||||
b[2] = (n >> 8) & 0xFF
|
||||
for i = 1, n do
|
||||
b[#b + 1] = positions[i] & 0xFF
|
||||
b[#b + 1] = (positions[i] >> 8) & 0xFF
|
||||
end
|
||||
for i = 1, n do
|
||||
b[#b + 1] = values[i] & 0xFF
|
||||
end
|
||||
return b
|
||||
end
|
||||
|
||||
-- A raw payload, for the negatives a well-formed encoder would never produce.
|
||||
local function delta_frame_of(payload, frame_no)
|
||||
return make_frame(KIND_TILES_DELTA, frame_no, 0, 0, #payload, function(k)
|
||||
return payload[k + 1]
|
||||
end), 16 + #payload
|
||||
end
|
||||
|
||||
local function delta_send(positions, values, frame_no)
|
||||
local payload = delta_bytes(positions, values)
|
||||
|
||||
return delta_frame_of(payload, frame_no)
|
||||
end
|
||||
|
||||
local function delta_check(cond, what)
|
||||
if cond then
|
||||
delta_lines[#delta_lines + 1] = "PASS " .. what
|
||||
print("VID: delta PASS: " .. what)
|
||||
else
|
||||
delta_lines[#delta_lines + 1] = "FAIL " .. what
|
||||
print("VID: delta FAIL: " .. what)
|
||||
delta_fail = delta_fail + 1
|
||||
end
|
||||
return cond
|
||||
end
|
||||
|
||||
local function delta_settled()
|
||||
return prog:read_u32(RX_RING_POS) == delta_pos
|
||||
and prog:read_u32(RX_RING_ACC) == 0
|
||||
end
|
||||
|
||||
local function delta_take(f, n)
|
||||
local ndg = inject_frame(f, n, DRAW_CHUNK)
|
||||
|
||||
if ndg == nil then
|
||||
return false
|
||||
end
|
||||
delta_pos = (delta_pos + ndg) % RX_DESC
|
||||
delta_last = ndg
|
||||
return true
|
||||
end
|
||||
|
||||
local function delta_verify_dict()
|
||||
local base = prog:read_u32(POOL_PTR)
|
||||
local bit, bad, first, i, v, want
|
||||
|
||||
if base == 0 then
|
||||
return false, -1
|
||||
end
|
||||
bit = base + DICT_OFF * 8
|
||||
bad = 0
|
||||
first = -1
|
||||
for i = 0, TILE_DICT_SIZE // 2 - 1 do
|
||||
v = prog:read_u16(bit + i * 16)
|
||||
want = TILE_DICT:byte(i * 2 + 1) | (TILE_DICT:byte(i * 2 + 2) << 8)
|
||||
if v ~= want then
|
||||
bad = bad + 1
|
||||
if first < 0 then
|
||||
first = i * 2
|
||||
end
|
||||
end
|
||||
end
|
||||
return bad == 0, first
|
||||
end
|
||||
|
||||
-- Compare two screens over the active area (rows 0..1023, bytes 0..159 of
|
||||
-- each row) and classify every differing byte: inside the 32x32 block at
|
||||
-- (tx, ty), or outside it. `first` is the row*256+x of the first byte found
|
||||
-- outside the block, for a message that names where a stray write landed.
|
||||
--
|
||||
-- The whole 256-byte row is not compared: bytes 160..255 are outside every
|
||||
-- tile and the OS owns them, which is why tile_check.py stops at 160 too.
|
||||
local function block_diff(a, b, tx, ty)
|
||||
local x0 = tx * 4
|
||||
local y0 = ty * TILE_PIXELS
|
||||
local inside, outside, first = 0, 0, -1
|
||||
local y, x
|
||||
|
||||
for y = 0, VRAM_ROWS - 1 do
|
||||
local row = y * VRAM_ROW_BYTES
|
||||
local inband = y >= y0 and y < y0 + TILE_PIXELS
|
||||
if inband then
|
||||
-- Outside the block's four-byte column: everything to its left
|
||||
-- and right. Substring compare first, so a clean row costs two
|
||||
-- compares and no byte loop.
|
||||
if a:sub(row + 1, row + x0) ~= b:sub(row + 1, row + x0)
|
||||
or a:sub(row + x0 + 5, row + ACTIVE_ROW)
|
||||
~= b:sub(row + x0 + 5, row + ACTIVE_ROW) then
|
||||
for x = 0, ACTIVE_ROW - 1 do
|
||||
if (x < x0 or x >= x0 + 4)
|
||||
and a:byte(row + x + 1) ~= b:byte(row + x + 1) then
|
||||
outside = outside + 1
|
||||
if first < 0 then
|
||||
first = row + x
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- A row outside the tile band has no block on it: every active
|
||||
-- byte of it is outside, the block's four columns included.
|
||||
if a:sub(row + 1, row + ACTIVE_ROW)
|
||||
~= b:sub(row + 1, row + ACTIVE_ROW) then
|
||||
for x = 0, ACTIVE_ROW - 1 do
|
||||
if a:byte(row + x + 1) ~= b:byte(row + x + 1) then
|
||||
outside = outside + 1
|
||||
if first < 0 then
|
||||
first = row + x
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for y = y0, y0 + TILE_PIXELS - 1 do
|
||||
local row = y * VRAM_ROW_BYTES
|
||||
for x = x0, x0 + 3 do
|
||||
if a:byte(row + x + 1) ~= b:byte(row + x + 1) then
|
||||
inside = inside + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return inside, outside, first
|
||||
end
|
||||
|
||||
-- The block at (tx, ty) of screen `v` against dictionary entry `entry`.
|
||||
local function block_equals(v, tx, ty, entry)
|
||||
local x0 = tx * 4
|
||||
local base = entry * TILE_DICT_BYTES
|
||||
local y, x
|
||||
|
||||
for y = 0, TILE_PIXELS - 1 do
|
||||
local row = (ty * TILE_PIXELS + y) * VRAM_ROW_BYTES
|
||||
for x = 0, 3 do
|
||||
if v:byte(row + x0 + x + 1)
|
||||
~= TILE_DICT:byte(base + y * 4 + x + 1) then
|
||||
return false, y * 4 + x
|
||||
end
|
||||
end
|
||||
end
|
||||
return true, -1
|
||||
end
|
||||
|
||||
local function delta_dump(timed_out)
|
||||
local ok = write_file(DUMP_DIR .. "/dict-delta.bin", TILE_DICT)
|
||||
ok = write_file(DUMP_DIR .. "/indices-delta.bin", TILE_INDICES) and ok
|
||||
if delta_before then
|
||||
ok = write_file(DUMP_DIR .. "/vram-delta-before.bin", delta_before) and ok
|
||||
end
|
||||
if delta_after then
|
||||
ok = write_file(DUMP_DIR .. "/vram-delta-after.bin", delta_after) and ok
|
||||
end
|
||||
if delta_reject then
|
||||
ok = write_file(DUMP_DIR .. "/vram-delta-reject.bin", delta_reject) and ok
|
||||
end
|
||||
if delta_recover then
|
||||
ok = write_file(DUMP_DIR .. "/vram-delta-recover.bin", delta_recover) and ok
|
||||
end
|
||||
local lines = {
|
||||
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
|
||||
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
|
||||
string.format("TILES_DROP=%d", prog:read_u32(TILES_DROP)),
|
||||
string.format("RX_LAST_LEN=%d", prog:read_u32(RX_LAST_LEN)),
|
||||
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("DICT_MATCHED=%d", delta_dict_matched),
|
||||
string.format("OS_TOUCH=%d", prog:read_u32(OS_TOUCH)),
|
||||
string.format("SENTINEL=%d", prog:read_u16(VRAM_BASE
|
||||
+ (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280)),
|
||||
string.format("DELTA_CHECKS_FAILED=%d", delta_fail),
|
||||
string.format("TIMEOUT=%d", timed_out and 1 or 0),
|
||||
}
|
||||
for _, l in ipairs(delta_lines) do
|
||||
lines[#lines + 1] = "CHECK " .. l
|
||||
end
|
||||
ok = write_file(DUMP_DIR .. "/delta-status.txt",
|
||||
table.concat(lines, "\n") .. "\n") and ok
|
||||
print(string.format("VID: delta: %d checks failed; RX_OK=%d TILES_DROP=%d RX_LAST_LEN=%d",
|
||||
delta_fail, prog:read_u32(RX_OK), prog:read_u32(TILES_DROP),
|
||||
prog:read_u32(RX_LAST_LEN)))
|
||||
print(ok and not timed_out and delta_fail == 0
|
||||
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 delta_step()
|
||||
local f, n, okv, first, inside, outside, stray, okb, v
|
||||
|
||||
if delta_phase == 0 then
|
||||
if delta_phase_at == nil then
|
||||
delta_phase_at = frames
|
||||
end
|
||||
if not ring_ready() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_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)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
local _, pos0 = ring_state()
|
||||
delta_pos = pos0
|
||||
delta_check(prog:read_u32(RX_OK) == 0,
|
||||
string.format("no frame had reached the screen before the dictionary (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
delta_phase = 1
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 1 then
|
||||
if delta_last ~= 0 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the frame at phase 1 (RX_RING_POS=%d expected %d)",
|
||||
prog:read_u32(RX_RING_POS), delta_pos))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_last = 0
|
||||
end
|
||||
if delta_at >= 23 then
|
||||
delta_check(prog:read_u32(DICT_READY) == 1
|
||||
and prog:read_u32(DICT_BYTES) == TILE_DICT_SIZE,
|
||||
string.format("the 23 chunks made DICT_READY=1 and DICT_BYTES=%d",
|
||||
prog:read_u32(DICT_BYTES)))
|
||||
delta_check(prog:read_u32(DICT_BAD) == 0,
|
||||
string.format("DICT_BAD=0 after the upload (DICT_BAD=%d)",
|
||||
prog:read_u32(DICT_BAD)))
|
||||
okv, first = delta_verify_dict()
|
||||
delta_dict_matched = okv and 1 or 0
|
||||
delta_check(okv, string.format(
|
||||
"the 32 KB at POOL_PTR + %d is the dictionary sent (first bad byte %d)",
|
||||
DICT_OFF, first))
|
||||
f = make_frame(KIND_TILES_FULL, 0x0090, 0, 0, TILES_PER_FRAME,
|
||||
function(i) return tile_index(i) end)
|
||||
if not delta_take(f, 16 + TILES_PER_FRAME) then
|
||||
delta_check(false, "the ring accepted the TILES_FULL keyframe")
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_phase = 2
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
f, n = delta_chunk(delta_at)
|
||||
if not delta_take(f, n) then
|
||||
delta_check(false, string.format("the ring accepted dictionary chunk %d", delta_at))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_at = delta_at + 1
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 2 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the keyframe (RX_OK=%d TILES_DROP=%d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_check(prog:read_u32(RX_OK) == 1,
|
||||
string.format("the TILES_FULL keyframe reached the screen (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
delta_check(prog:read_u32(TILES_DROP) == 0,
|
||||
string.format("no tile frame was dropped (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
delta_check(prog:read_u32(RX_BAD) == 0,
|
||||
string.format("no frame was refused (RX_BAD=%d)", prog:read_u32(RX_BAD)))
|
||||
delta_before = read_vram()
|
||||
delta_prev = delta_before
|
||||
-- The delta under test: one tile, a different dictionary entry.
|
||||
f, n = delta_send({ TOUCH }, { (tile_index(TOUCH) + 1) & 0xFF }, 0x0091)
|
||||
if not delta_take(f, n) then
|
||||
delta_check(false, "the ring accepted the one-tile delta")
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_phase = 3
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 3 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the one-tile delta (RX_OK=%d TILES_DROP=%d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_check(prog:read_u32(RX_OK) == 2,
|
||||
string.format("the delta reached the screen (RX_OK=%d)", prog:read_u32(RX_OK)))
|
||||
delta_check(prog:read_u32(TILES_DROP) == 0,
|
||||
string.format("no tile frame was dropped (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
delta_check(prog:read_u32(RX_LAST_LEN) == 2 + 3,
|
||||
string.format("RX_LAST_LEN is the delta's payload_len (%d, expected 5)",
|
||||
prog:read_u32(RX_LAST_LEN)))
|
||||
delta_last_len = prog:read_u32(RX_LAST_LEN)
|
||||
delta_after = read_vram()
|
||||
inside, outside, stray = block_diff(delta_before, delta_after,
|
||||
TOUCH % TILE_COLS, TOUCH // TILE_COLS)
|
||||
delta_check(inside == 128, string.format(
|
||||
"all 128 bytes of the 32x32 block at tile %d changed (%d)", TOUCH, inside))
|
||||
delta_check(outside == 0, string.format(
|
||||
"no byte outside that block changed (first stray write at %d/%d)",
|
||||
stray, ACTIVE_ROW))
|
||||
okv, first = block_equals(delta_after, TOUCH % TILE_COLS, TOUCH // TILE_COLS,
|
||||
(tile_index(TOUCH) + 1) & 0xFF)
|
||||
delta_check(okv, string.format(
|
||||
"the block is dictionary entry %d (first bad byte %d)",
|
||||
(tile_index(TOUCH) + 1) & 0xFF, first))
|
||||
delta_prev = delta_after
|
||||
-- Negative control A: a valid tile followed by a position off the grid.
|
||||
f, n = delta_send({ BAD_POS, BAD_OUT },
|
||||
{ (tile_index(BAD_POS) + 3) & 0xFF, 0x11 }, 0x0092)
|
||||
if not delta_take(f, n) then
|
||||
delta_check(false, "the ring accepted the off-grid delta")
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_phase = 4
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 4 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the off-grid delta (RX_OK=%d TILES_DROP=%d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_check(prog:read_u32(TILES_DROP) == 1,
|
||||
string.format("the off-grid delta was refused (TILES_DROP=%d, expected 1)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
delta_check(prog:read_u32(RX_OK) == 2,
|
||||
string.format("the off-grid delta did not reach the screen (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
delta_check(prog:read_u32(RX_LAST_LEN) == delta_last_len,
|
||||
string.format("RX_LAST_LEN still names the last painted delta (%d, expected %d)",
|
||||
prog:read_u32(RX_LAST_LEN), delta_last_len))
|
||||
delta_reject = read_vram()
|
||||
inside, outside, stray = block_diff(delta_prev, delta_reject,
|
||||
BAD_POS % TILE_COLS, BAD_POS // TILE_COLS)
|
||||
delta_check(inside == 0, string.format(
|
||||
"the off-grid delta painted no part of its valid tile %d (%d bytes)",
|
||||
BAD_POS, inside))
|
||||
delta_check(outside == 0, string.format(
|
||||
"the off-grid delta changed nothing else (first stray write at %d/%d)",
|
||||
stray, ACTIVE_ROW))
|
||||
delta_prev = delta_reject
|
||||
-- Negative control B: two entries claimed, one carried.
|
||||
f, n = delta_frame_of({ 0x02, 0x00, BAD_POS & 0xFF, 0x00, 0x22 }, 0x0093)
|
||||
if not delta_take(f, n) then
|
||||
delta_check(false, "the ring accepted the truncation delta")
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_phase = 5
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 5 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the truncation delta (RX_OK=%d TILES_DROP=%d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_check(prog:read_u32(TILES_DROP) == 2,
|
||||
string.format("the truncated delta was refused (TILES_DROP=%d, expected 2)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
delta_check(prog:read_u32(RX_OK) == 2,
|
||||
string.format("the truncated delta did not reach the screen (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
v = read_vram()
|
||||
inside, outside, stray = block_diff(delta_prev, v,
|
||||
BAD_POS % TILE_COLS, BAD_POS // TILE_COLS)
|
||||
delta_check(inside == 0 and outside == 0, string.format(
|
||||
"the truncated delta changed nothing (%d inside, %d outside, first %d/%d)",
|
||||
inside, outside, stray, ACTIVE_ROW))
|
||||
delta_reject = v
|
||||
delta_prev = v
|
||||
-- Recovery: a good delta after two refusals.
|
||||
f, n = delta_send({ RECOVER }, { (tile_index(RECOVER) + 2) & 0xFF }, 0x0094)
|
||||
if not delta_take(f, n) then
|
||||
delta_check(false, "the ring accepted the recovery delta")
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
delta_phase = 6
|
||||
delta_phase_at = frames
|
||||
return false
|
||||
end
|
||||
|
||||
if delta_phase == 6 then
|
||||
if not delta_settled() then
|
||||
if frames >= delta_phase_at + DELTA_WAIT then
|
||||
delta_check(false, string.format(
|
||||
"the payload consumed the recovery delta (RX_OK=%d TILES_DROP=%d)",
|
||||
prog:read_u32(RX_OK), prog:read_u32(TILES_DROP)))
|
||||
delta_dump(true)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
delta_check(prog:read_u32(RX_OK) == 3,
|
||||
string.format("the recovery delta reached the screen (RX_OK=%d)",
|
||||
prog:read_u32(RX_OK)))
|
||||
delta_check(prog:read_u32(TILES_DROP) == 2,
|
||||
string.format("the refusals left no further drop (TILES_DROP=%d)",
|
||||
prog:read_u32(TILES_DROP)))
|
||||
delta_recover = read_vram()
|
||||
inside, outside, stray = block_diff(delta_prev, delta_recover,
|
||||
RECOVER % TILE_COLS, RECOVER // TILE_COLS)
|
||||
delta_check(inside == 128, string.format(
|
||||
"all 128 bytes of tile %d changed (%d)", RECOVER, inside))
|
||||
delta_check(outside == 0, string.format(
|
||||
"no byte outside that block changed (first stray write at %d/%d)",
|
||||
stray, ACTIVE_ROW))
|
||||
okv, first = block_equals(delta_recover, RECOVER % TILE_COLS,
|
||||
RECOVER // TILE_COLS, (tile_index(RECOVER) + 2) & 0xFF)
|
||||
delta_check(okv, string.format(
|
||||
"the block is dictionary entry %d (first bad byte %d)",
|
||||
(tile_index(RECOVER) + 2) & 0xFF, first))
|
||||
delta_check(prog:read_u32(DICT_READY) == 1
|
||||
and prog:read_u32(DICT_BYTES) == TILE_DICT_SIZE
|
||||
and prog:read_u32(DICT_BAD) == 0,
|
||||
"the deltas left the dictionary record alone")
|
||||
okb = prog:read_u16(VRAM_BASE + (VRAM_ROWS - 1) * VRAM_ROW_BITS + 1280)
|
||||
delta_check(okb == 0xA5C3, string.format(
|
||||
"the VRAM canary survives the deltas (SENTINEL=%04X)", okb))
|
||||
delta_check(prog:read_u32(OS_TOUCH) == 0,
|
||||
string.format("the OS did not repaint over the deltas (OS_TOUCH=%d)",
|
||||
prog:read_u32(OS_TOUCH)))
|
||||
delta_dump(false)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return {
|
||||
step = delta_step,
|
||||
phase = function() return delta_phase end,
|
||||
dump = function(timed_out) delta_dump(timed_out) end,
|
||||
}
|
||||
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
|
||||
@@ -2231,6 +2888,13 @@ emu.register_frame_done(function()
|
||||
end
|
||||
end
|
||||
|
||||
-- Mode delta: changed-tile lists and the periodic keyframe (delta.step()).
|
||||
if entered and MODE == "delta" then
|
||||
if delta.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:
|
||||
--
|
||||
@@ -2348,8 +3012,15 @@ emu.register_frame_done(function()
|
||||
return
|
||||
end
|
||||
|
||||
if MODE == "delta" and frames >= STOP_AT then
|
||||
print(string.format("VID: delta mode did not finish (phase %d) by frame %d",
|
||||
delta.phase(), frames))
|
||||
delta.dump(true)
|
||||
return
|
||||
end
|
||||
|
||||
if frames >= STOP_AT and MODE ~= "stream" and MODE ~= "ring"
|
||||
and MODE ~= "dict" and MODE ~= "tiles"
|
||||
and MODE ~= "dict" and MODE ~= "tiles" and MODE ~= "delta"
|
||||
and not (MODE == "draw" and draw_at ~= nil) then
|
||||
if not entered then
|
||||
print("VID: payload never entered -> FAIL")
|
||||
|
||||
@@ -171,6 +171,49 @@
|
||||
* forget it.
|
||||
* ---------------------------------------------------------------------------
|
||||
*
|
||||
* Task 4/5: the tile payloads, written down here because this is the wire
|
||||
* format's one C statement (Task 4's review carried the TILES_FULL layout to
|
||||
* this task for exactly this reason). Both kinds are decoded against the
|
||||
* dictionary at POOL_PTR + 2 * SLOT_BYTES -- 32,768 bytes, 256 entries of 128
|
||||
* bytes -- and only after DICT_READY. The screen is a 40 x 32 grid of 32x32
|
||||
* tiles: 1,280 of them, tile (tx, ty) at row-major position ty * 40 + tx.
|
||||
*
|
||||
* TILES_FULL (kind 2) is the keyframe: the whole screen, one dictionary index
|
||||
* per tile, in one byte each.
|
||||
*
|
||||
* offset size field
|
||||
* 0 1280 index[ty * 40 + tx], dictionary entry 0..255
|
||||
*
|
||||
* The payload is EXACTLY 1,280 bytes. A short frame does not name every tile
|
||||
* and a longer one is not this kind's wire form, so both are refused rather
|
||||
* than painted as a partial screen. (A 256-entry dictionary is exactly what
|
||||
* an index byte can name, which is why the entry count is fixed at 256 and not
|
||||
* a per-clip parameter on the wire.)
|
||||
*
|
||||
* TILES_DELTA (kind 3) is the same grid with only the tiles that changed:
|
||||
*
|
||||
* offset size field
|
||||
* 0 2 count, u16 little-endian, 1..1280
|
||||
* 2 2 * count position[k], u16 little-endian, row-major 0..1279,
|
||||
* STRICTLY ASCENDING
|
||||
* 2 + 2*count count index[k], one byte each: the new dictionary entry
|
||||
*
|
||||
* and payload_len is EXACTLY 2 + 3 * count. The position is two bytes because
|
||||
* the grid has 1,280 tiles while an entry needs one: 256 fits a byte, 1,280
|
||||
* does not. The receiver refuses, in TILES_DROP and without painting anything,
|
||||
* a count of 0 or above 1,280, a length that disagrees with the count (the list
|
||||
* may not run past the frame), a position off the grid, a repeated or
|
||||
* descending position, and (defensively: one byte cannot express it) an entry
|
||||
* above 255. It validates the WHOLE list before the first blit, so one bad
|
||||
* entry cannot leave half a delta on the screen; positions being ascending is
|
||||
* what makes that possible with one cursor and no per-tile division.
|
||||
*
|
||||
* The keyframe cadence is the host's: KEYFRAME_EVERY = 60 frames (tiledict.py,
|
||||
* ruling R2), plus a keyframe whenever the status frame's `drops` rises. The
|
||||
* terminal does not choose it and has no "delta lost" signal of its own -- a
|
||||
* delta that never arrives is silence.
|
||||
* ---------------------------------------------------------------------------
|
||||
*
|
||||
* 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
|
||||
@@ -198,6 +241,12 @@
|
||||
#define FP_KIND_DICT_CHUNK 4
|
||||
#define FP_KIND_STATUS 5
|
||||
|
||||
/* Task 4/5: the tile wire layout (see the comment block above). A keyframe is
|
||||
* one index byte per tile; a delta opens with a u16 count and names each
|
||||
* changed tile with a u16 position and a one-byte entry. */
|
||||
#define FP_TILES_SCREEN 1280 /* 40 x 32 tiles, one index byte each */
|
||||
#define FP_DELTA_HDR 2 /* bytes of a delta's u16 count */
|
||||
|
||||
/* Marker page cells; see the table above. */
|
||||
#define MARK_PAGE 0x01810000
|
||||
#define MARK_CELL(n) (MARK_PAGE + (n) * 8)
|
||||
|
||||
@@ -17,17 +17,29 @@ leaves implicit but the terminal's receiver (Task 3) depends on:
|
||||
Reassembly is asserted byte for byte: concatenating the bodies in arrival order
|
||||
must give back the input, and the last offset plus the last body length must be
|
||||
the end of the dictionary.
|
||||
|
||||
The last group of tests is Task 5's delta encoding, and it pins the *payload*,
|
||||
not just the host round trip: the C decoder (``vidstream.c``'s ``paint_delta``)
|
||||
reads the same layout, so what is asserted here is what ``frame_parse.h``
|
||||
writes down. ``unpack_delta`` is the host's reading of that layout and shares
|
||||
none of the encoder's code, so a delta that round-trips through it is not being
|
||||
blessed by its own writer.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from harness.udpstream.frame import (HEADER_SIZE, KIND_DICT_CHUNK,
|
||||
unpack_header)
|
||||
from harness.udpstream.tiledict import (MAX_PAYLOAD, dict_chunks, payload_body,
|
||||
payload_offset)
|
||||
from harness.udpstream.tiledict import (KEYFRAME_EVERY, MAX_PAYLOAD,
|
||||
delta_frame, dict_chunks, keyframe_due,
|
||||
payload_body, payload_offset,
|
||||
unpack_delta)
|
||||
|
||||
#: A whole dictionary: 256 entries of 128 bytes.
|
||||
DICTIONARY = bytes(range(256)) * 128
|
||||
|
||||
#: One full-screen index frame: 1,280 row-major tile indices (40 x 32).
|
||||
FRAME = bytes(range(256)) * 5 # exactly 1,280 bytes
|
||||
|
||||
|
||||
def test_chunks_cover_the_dictionary_once_in_order():
|
||||
d = bytes(range(256)) * 128 # 32 KB
|
||||
@@ -101,3 +113,114 @@ def test_payload_helpers_reject_a_truncated_payload():
|
||||
header = unpack_header(packet)
|
||||
with pytest.raises(ValueError):
|
||||
payload_body(bytes(packet[:HEADER_SIZE + header["payload_len"] - 1]))
|
||||
|
||||
|
||||
# --- Task 5: delta frames and keyframe cadence ---------------------------
|
||||
#
|
||||
# The layout pinned here is the one frame_parse.h writes down for the C
|
||||
# decoder: a u16 count, `count` u16 row-major tile positions and `count`
|
||||
# one-byte dictionary entries. Positions are two bytes because a screen has
|
||||
# 1,280 tiles and one byte cannot name tile 1279 -- the *entry* fits in a byte
|
||||
# (256 of them), the *position* does not. That is the one layout fact this
|
||||
# group of tests exists to stop from drifting on one side.
|
||||
|
||||
def test_delta_of_two_changed_tiles_lists_exactly_those_two_indices():
|
||||
"""The brief's test: two tiles changed, two entries and no others."""
|
||||
cur = bytearray(FRAME)
|
||||
cur[7] = 0x80
|
||||
cur[1000] = 0xFF
|
||||
|
||||
payload = delta_frame(FRAME, bytes(cur))
|
||||
|
||||
assert payload is not None
|
||||
assert unpack_delta(payload) == [(7, 0x80), (1000, 0xFF)]
|
||||
# 2 bytes of count, two positions and two values.
|
||||
assert len(payload) == 2 + 2 * 2 + 2
|
||||
|
||||
|
||||
def test_identical_frames_produce_no_delta():
|
||||
assert delta_frame(FRAME, FRAME) is None
|
||||
assert delta_frame(FRAME, bytes(FRAME)) is None
|
||||
|
||||
|
||||
def test_a_keyframe_is_due_at_frame_60_and_120():
|
||||
assert KEYFRAME_EVERY == 60
|
||||
assert keyframe_due(60) is True
|
||||
assert keyframe_due(120) is True
|
||||
# Frame 0 has no previous frame, so the first frame is a keyframe too; the
|
||||
# 59 frames between two keyframes are deltas.
|
||||
assert keyframe_due(0) is True
|
||||
assert not any(keyframe_due(n) for n in range(1, 60))
|
||||
assert not any(keyframe_due(n) for n in range(61, 120))
|
||||
|
||||
|
||||
def test_a_reported_drop_forces_a_keyframe():
|
||||
"""The status frame's `drops` is cumulative: any increase means a loss."""
|
||||
assert keyframe_due(7, drops=3, seen_drops=2) is True
|
||||
assert keyframe_due(7, drops=2, seen_drops=2) is False
|
||||
assert keyframe_due(7, drops=0, seen_drops=0) is False
|
||||
|
||||
|
||||
def test_delta_positions_are_strictly_ascending_in_scan_order():
|
||||
cur = bytearray(FRAME)
|
||||
for position in (1279, 0, 640, 639):
|
||||
cur[position] = (cur[position] + 1) & 0xFF
|
||||
|
||||
payload = delta_frame(FRAME, bytes(cur))
|
||||
|
||||
assert [p for p, _ in unpack_delta(payload)] == [0, 639, 640, 1279]
|
||||
|
||||
|
||||
def test_a_whole_screen_delta_is_the_largest_frame_and_stays_one_slot():
|
||||
cur = bytes((b + 1) & 0xFF for b in FRAME)
|
||||
|
||||
payload = delta_frame(FRAME, cur)
|
||||
|
||||
assert len(payload) == 2 + 3 * 1280
|
||||
assert unpack_delta(payload) == [(i, cur[i]) for i in range(1280)]
|
||||
# A delta can never be larger than 2 + 3 * 1,280 = 3,842 bytes; both that
|
||||
# and a 1,280-byte keyframe fit the payload's 24 KB slot many times over.
|
||||
assert len(payload) == 3842
|
||||
|
||||
|
||||
def test_encoder_rejects_a_frame_that_is_not_1280_index_bytes():
|
||||
with pytest.raises(ValueError):
|
||||
delta_frame(FRAME[:-1], FRAME)
|
||||
with pytest.raises(ValueError):
|
||||
delta_frame(FRAME, FRAME + b"\x00")
|
||||
with pytest.raises(TypeError):
|
||||
delta_frame(FRAME, "not bytes")
|
||||
|
||||
|
||||
def test_decoder_rejects_a_list_that_disagrees_with_the_length():
|
||||
"""`payload_len` is authoritative: the list cannot run past it."""
|
||||
changed = bytes([FRAME[0] ^ 0xFF]) + FRAME[1:]
|
||||
good = delta_frame(FRAME, changed)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(good[:-1]) # one value missing
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(good + b"\x00") # a tail outside the list
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(b"") # no count field at all
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(b"\x00\x00") # count 0: the encoder sends None
|
||||
|
||||
|
||||
def test_decoder_rejects_a_tile_position_outside_the_grid():
|
||||
"""A position >= 1,280 must be refused, never wrapped onto another tile."""
|
||||
assert unpack_delta(b"\x01\x00" + (1279).to_bytes(2, "little") + b"\x00") \
|
||||
== [(1279, 0)]
|
||||
|
||||
for position in (1280, 5000, 0xFFFF):
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(b"\x01\x00"
|
||||
+ position.to_bytes(2, "little") + b"\x00")
|
||||
|
||||
|
||||
def test_decoder_rejects_a_repeated_or_descending_position():
|
||||
"""Strictly ascending: a tile is named once, in scan order."""
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(b"\x02\x00" + bytes([5, 0, 5, 0]) + b"\x00\x01")
|
||||
with pytest.raises(ValueError):
|
||||
unpack_delta(b"\x02\x00" + bytes([6, 0, 5, 0]) + b"\x00\x01")
|
||||
@@ -30,6 +30,31 @@ frames. The ``frame_no`` field is not what orders the chunks; the offset is.
|
||||
The payload's ``frame_parse.h`` mirrors :data:`harness.udpstream.frame.KINDS`,
|
||||
which is where ``KIND_DICT_CHUNK`` is defined.
|
||||
|
||||
Delta frames
|
||||
------------
|
||||
|
||||
The same module carries Task 5's changed-tile list, because it is the other
|
||||
half of the tile wire format and the two layouts share the dictionary:
|
||||
|
||||
KIND_TILES_FULL (2) 1,280 dictionary indices, row-major, one byte each
|
||||
KIND_TILES_DELTA (3) a u16 count, then `count` u16 positions and
|
||||
`count` one-byte dictionary entries
|
||||
|
||||
A position is a row-major tile index 0..1,279, so it needs **two** bytes while
|
||||
one dictionary entry fits in one -- the entry space is 256, the tile grid is
|
||||
1,280. :func:`delta_frame` writes the payload, :func:`unpack_delta` reads it
|
||||
back with the same rules the C decoder enforces (exact length, positions on the
|
||||
grid and strictly ascending), and :func:`keyframe_due` is the cadence rule:
|
||||
a ``TILES_FULL`` whenever the host has no previous frame, every
|
||||
:data:`KEYFRAME_EVERY` frames, and as soon as the terminal's status frame
|
||||
reports a drop.
|
||||
|
||||
Positions travel separately from values (rather than interleaved as pairs) for
|
||||
one reason: a receiver can validate the whole position list before it touches a
|
||||
pixel, so a frame with one bad position paints **nothing**. Interleaved pairs
|
||||
invite a decoder that paints as it reads, which is the failure the harness's
|
||||
off-grid negative control exists to catch.
|
||||
|
||||
Pure standard library.
|
||||
"""
|
||||
|
||||
@@ -37,10 +62,12 @@ import struct
|
||||
|
||||
from harness.udpstream.frame import (HEADER_SIZE, KIND_DICT_CHUNK, MAGIC,
|
||||
VERSION, pack_header, unpack_header)
|
||||
from harness.udpstream.tilecost import TILES_PER_SCREEN
|
||||
|
||||
__all__ = [
|
||||
"MAX_BODY", "MAX_DICT_BYTES", "MAX_PAYLOAD", "OFFSET_SIZE",
|
||||
"dict_chunks", "payload_body", "payload_offset",
|
||||
"DELTA_HDR", "KEYFRAME_EVERY", "MAX_BODY", "MAX_DICT_BYTES", "MAX_PAYLOAD",
|
||||
"OFFSET_SIZE", "delta_frame", "dict_chunks", "keyframe_due",
|
||||
"payload_body", "payload_offset", "unpack_delta",
|
||||
]
|
||||
|
||||
#: Largest chunk payload in one packet, offset included. The same 1450-byte
|
||||
@@ -57,7 +84,16 @@ MAX_BODY = MAX_PAYLOAD - OFFSET_SIZE # 1,448
|
||||
#: offset 65,536 is still addressable: its last chunk's offset fits.
|
||||
MAX_DICT_BYTES = 0x10000
|
||||
|
||||
#: A delta payload opens with the u16 entry count (see frame_parse.h).
|
||||
DELTA_HDR = 2
|
||||
|
||||
#: Send a full frame (``TILES_FULL``) this often, whether or not anything was
|
||||
#: lost. Ruling R2 of the plan: 60 frames is ~2 s at the sender's 30 fps, so a
|
||||
#: desynchronised screen can never stay wrong for longer than that.
|
||||
KEYFRAME_EVERY = 60
|
||||
|
||||
_OFFSET = struct.Struct("<H")
|
||||
_COUNT = struct.Struct("<H")
|
||||
|
||||
|
||||
def _dict_payload(packet: bytes) -> bytes:
|
||||
@@ -128,3 +164,119 @@ def payload_offset(packet: bytes) -> int:
|
||||
def payload_body(packet: bytes) -> bytes:
|
||||
"""The dictionary bytes ``packet`` carries, offset excluded."""
|
||||
return _dict_payload(packet)[OFFSET_SIZE:]
|
||||
|
||||
|
||||
# --- Task 5: changed-tile lists -------------------------------------------
|
||||
|
||||
def _indices(name: str, frame) -> bytes:
|
||||
"""Range-check one full-screen index frame and return it as ``bytes``."""
|
||||
if not isinstance(frame, (bytes, bytearray, memoryview)):
|
||||
raise TypeError("%s is bytes-like, got %s"
|
||||
% (name, type(frame).__name__))
|
||||
data = bytes(frame)
|
||||
if len(data) != TILES_PER_SCREEN:
|
||||
raise ValueError("%s is one index byte per tile: %d bytes, got %d"
|
||||
% (name, TILES_PER_SCREEN, len(data)))
|
||||
return data
|
||||
|
||||
|
||||
def delta_frame(prev: bytes, cur: bytes) -> bytes | None:
|
||||
"""Return the ``KIND_TILES_DELTA`` payload from ``prev`` to ``cur``.
|
||||
|
||||
``None`` when the frames are identical: there is nothing to put on the
|
||||
wire, and an empty list is not a frame the receiver should have to accept.
|
||||
Otherwise the payload is
|
||||
|
||||
u16 count | count * u16 position | count * u8 dictionary entry
|
||||
|
||||
with positions in ascending order (a scan of the row-major grid produces
|
||||
them that way) and each entry the *new* value for that tile. Everything is
|
||||
recomputed from the two frames -- nothing is carried over from a previous
|
||||
delta -- so a delta lost in transit cannot corrupt the next one; it is the
|
||||
periodic keyframe that repairs the screen.
|
||||
"""
|
||||
before = _indices("prev", prev)
|
||||
after = _indices("cur", cur)
|
||||
|
||||
positions = [i for i in range(TILES_PER_SCREEN) if before[i] != after[i]]
|
||||
if not positions:
|
||||
return None
|
||||
values = bytes(after[i] for i in positions)
|
||||
return (_COUNT.pack(len(positions))
|
||||
+ b"".join(_COUNT.pack(i) for i in positions)
|
||||
+ values)
|
||||
|
||||
|
||||
def unpack_delta(payload: bytes) -> list[tuple[int, int]]:
|
||||
"""Decode a ``KIND_TILES_DELTA`` payload, or raise ``ValueError``.
|
||||
|
||||
The rules are the C decoder's (``paint_delta`` in ``vidstream.c``), and
|
||||
they are the reason a delta cannot corrupt the screen:
|
||||
|
||||
* the payload holds exactly ``2 + 3 * count`` bytes, so the list cannot run
|
||||
past the frame the header described;
|
||||
* ``count`` is at least 1 and at most :data:`TILES_PER_SCREEN`;
|
||||
* every position is on the 40 x 32 grid, so no blit is ever aimed past the
|
||||
last tile;
|
||||
* positions are strictly ascending, so no tile is named twice and a
|
||||
reordered list is a refusal rather than a different picture.
|
||||
|
||||
Each value is a byte and therefore already a dictionary entry, so the
|
||||
"entry index < 256" rule needs no check on this side.
|
||||
"""
|
||||
if not isinstance(payload, (bytes, bytearray, memoryview)):
|
||||
raise TypeError("a delta payload is bytes-like, got %s"
|
||||
% type(payload).__name__)
|
||||
body = bytes(payload)
|
||||
if len(body) < DELTA_HDR:
|
||||
raise ValueError("a delta payload opens with a %d-byte count, got %d"
|
||||
% (DELTA_HDR, len(body)))
|
||||
count = _COUNT.unpack_from(body, 0)[0]
|
||||
if not 1 <= count <= TILES_PER_SCREEN:
|
||||
raise ValueError("a delta lists 1..%d tiles, got %d"
|
||||
% (TILES_PER_SCREEN, count))
|
||||
if len(body) != DELTA_HDR + 3 * count:
|
||||
raise ValueError("a %d-entry delta is %d bytes, got %d"
|
||||
% (count, DELTA_HDR + 3 * count, len(body)))
|
||||
|
||||
positions = []
|
||||
previous = -1
|
||||
for k in range(count):
|
||||
position = _COUNT.unpack_from(body, DELTA_HDR + 2 * k)[0]
|
||||
if position >= TILES_PER_SCREEN:
|
||||
raise ValueError("tile position %d is off the %d-tile grid"
|
||||
% (position, TILES_PER_SCREEN))
|
||||
if position <= previous:
|
||||
raise ValueError("tile positions must be strictly ascending: "
|
||||
"%d follows %d" % (position, previous))
|
||||
previous = position
|
||||
positions.append(position)
|
||||
values = body[DELTA_HDR + 2 * count:]
|
||||
return list(zip(positions, values))
|
||||
|
||||
|
||||
def keyframe_due(frame_no: int, drops: int = 0, seen_drops: int = 0,
|
||||
every: int = KEYFRAME_EVERY) -> bool:
|
||||
"""True when the host must send a ``TILES_FULL`` instead of a delta.
|
||||
|
||||
Two triggers, and both are "the terminal may not have what the host thinks
|
||||
it has":
|
||||
|
||||
* ``frame_no`` is a multiple of ``every`` -- a fixed cadence that bounds
|
||||
how long a desynchronised screen can stay wrong (ruling R2). Frame 0
|
||||
is in this set because there is no previous frame to delta against;
|
||||
* ``drops`` rose since ``seen_drops``: the status frame's counter is
|
||||
cumulative, so any increase means at least one frame never reached the
|
||||
screen and the host's idea of the screen is stale.
|
||||
|
||||
``crc_err`` is deliberately not a trigger: a refused frame never painted,
|
||||
so it cannot have desynchronised anything, and the cadence repairs a drop
|
||||
the terminal never counted.
|
||||
"""
|
||||
if (not isinstance(frame_no, int) or isinstance(frame_no, bool)
|
||||
or frame_no < 0):
|
||||
raise ValueError("frame_no is a non-negative int, got %r"
|
||||
% (frame_no,))
|
||||
if not isinstance(every, int) or isinstance(every, bool) or every < 1:
|
||||
raise ValueError("every is a positive int, got %r" % (every,))
|
||||
return frame_no % every == 0 or drops > seen_drops
|
||||
+192
-12
@@ -19,7 +19,8 @@
|
||||
* dictionary is complete is refused and counted instead of being mistaken for
|
||||
* a strip. Plan 2's Task 4 paints TILES_FULL: one PIXBLT B,XY per 32x32 tile
|
||||
* from the dictionary (vram_tile.asm), 1,280 of them per frame inside one
|
||||
* dint/eint pair. TILES_DELTA is still counted, not painted (Task 5).
|
||||
* dint/eint pair. Plan 2's Task 5 decodes TILES_DELTA: a validated changed-tile
|
||||
* list painted by the same per-tile primitive, only the tiles it names.
|
||||
*
|
||||
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
|
||||
*
|
||||
@@ -152,7 +153,8 @@
|
||||
*/
|
||||
#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 */
|
||||
#define DICT_ENTRIES 256 /* entries: an index is one byte */
|
||||
#define DICT_SIZE (DICT_ENTRY * DICT_ENTRIES) /* 256 entries = 32 KB */
|
||||
|
||||
/*
|
||||
* Task 4: a TILES_FULL frame is 1,280 dictionary indices, row-major, one byte
|
||||
@@ -183,6 +185,14 @@
|
||||
*/
|
||||
char dict_slack_check[(POOL_BYTES >= (DICT_OFF + DICT_SIZE)) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* Task 5: the wire's tile count and the decoder's geometry must be the same
|
||||
* grid. frame_parse.h pins it (FP_TILES_SCREEN, the layout block) and this is
|
||||
* the build-time half: a disagreement is a compile error rather than a paint
|
||||
* that lands one tile off. Same trick as dict_slack_check above.
|
||||
*/
|
||||
char tile_grid_check[(TILES_SCREEN == FP_TILES_SCREEN) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* Task 6: the terminal's status frame.
|
||||
*
|
||||
@@ -582,6 +592,173 @@ fp_header *h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* frag_get(pos, vp) -- one frame byte at frame offset `pos`. 1 on success, 0
|
||||
* when the fragment table does not hold it (a gap, or a frame shorter than the
|
||||
* position asked for).
|
||||
*
|
||||
* paint_delta() reads single bytes at offsets its own arithmetic derives -- the
|
||||
* count, each position and each value -- where frag_copy() moves one contiguous
|
||||
* run. Same table, same rules: no copy across a gap and nothing past the last
|
||||
* delivered byte.
|
||||
*/
|
||||
static int frag_get(pos, vp)
|
||||
unsigned int pos;
|
||||
unsigned int *vp;
|
||||
{
|
||||
unsigned int k;
|
||||
|
||||
k = 0;
|
||||
while (k < rx_nfrag && pos >= rx_frag_off[k] + rx_frag_len[k]) {
|
||||
k = k + 1;
|
||||
}
|
||||
if (k >= rx_nfrag) {
|
||||
return 0;
|
||||
}
|
||||
if (pos < rx_frag_off[k]) {
|
||||
return 0;
|
||||
}
|
||||
*vp = (unsigned int)*(unsigned char *)
|
||||
(rx_frag_ptr[k] + (pos - rx_frag_off[k]) * 8);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* paint_delta(h) -- draw one accepted TILES_DELTA frame. 1 when the list was
|
||||
* decoded and painted, 0 when the frame was refused (the caller counts that in
|
||||
* TILES_DROP and VRAM is untouched).
|
||||
*
|
||||
* The wire form (frame_parse.h, tiledict.py's delta_frame) is
|
||||
*
|
||||
* count u16 | count x position u16 | count x entry u8
|
||||
*
|
||||
* with payload_len exactly 2 + 3 * count. A position is the row-major tile
|
||||
* index ty * TILE_COLS + tx and must be strictly ascending; an entry is a
|
||||
* dictionary index, one byte.
|
||||
*
|
||||
* Two passes, and the first one is the safety property: EVERY position and
|
||||
* entry is read and bounds-checked before the first blit. A frame with one bad
|
||||
* entry therefore paints nothing at all, where a decoder that painted as it
|
||||
* read would leave half a delta on the screen -- and half a delta is worse than
|
||||
* none, because the next delta is computed against a frame this side never had.
|
||||
*
|
||||
* Refused (nothing painted), in the order the checks run:
|
||||
* - no complete dictionary (DICT_READY 0 or no base), as for TILES_FULL;
|
||||
* - payload_len < 2: no count field;
|
||||
* - count 0 or above TILES_SCREEN: an empty list is not sent (the encoder
|
||||
* returns None) and a screen cannot have more changed tiles than tiles;
|
||||
* - payload_len != 2 + 3 * count: the list would run past the frame, or the
|
||||
* frame carries bytes that are not part of the list;
|
||||
* - a position >= TILES_SCREEN: off the 40x32 grid (a blit aimed there would
|
||||
* write past the last tile band);
|
||||
* - a position not greater than the previous one: the list is not canonical,
|
||||
* and a repeated tile would be painted twice for no reason. This is also
|
||||
* what lets the paint pass run with one cursor: strictly ascending means
|
||||
* the nested grid loop visits every named tile exactly once;
|
||||
* - an entry >= DICT_ENTRIES: one byte cannot express it, so this check is
|
||||
* dead today, but it is the bound that keeps a future wider encoding from
|
||||
* reading past the 32 KB dictionary. It is written out rather than left to
|
||||
* the byte width.
|
||||
*
|
||||
* The paint pass walks the 40x32 grid once, comparing the running tile position
|
||||
* with the next named one -- no division, no search, and the same
|
||||
* one-PIXBLT-per-tile primitive (and dint/eint pair and canary) as
|
||||
* paint_tiles(). A tile the list does not name is not touched at all, which is
|
||||
* the whole point of a delta.
|
||||
*
|
||||
* Because the list was validated a moment ago from the same slot, the reads in
|
||||
* the paint pass cannot fail; their return values are ignored deliberately, and
|
||||
* the slot cannot be written while this frame is being decoded (rx_switch()
|
||||
* armed the OTHER slot before the dispatch).
|
||||
*/
|
||||
static int paint_delta(h)
|
||||
fp_header *h;
|
||||
{
|
||||
unsigned int count, k, pos, val, prev, lo, hi, sent;
|
||||
unsigned int tile, tx, ty;
|
||||
|
||||
if (!dict_ready || dict_base == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (h->payload_len < FP_DELTA_HDR) {
|
||||
return 0;
|
||||
}
|
||||
if (!frag_get(h->payload_off, &lo)
|
||||
|| !frag_get(h->payload_off + 1, &hi)) {
|
||||
return 0;
|
||||
}
|
||||
count = lo | (hi << 8);
|
||||
if (count == 0 || count > TILES_SCREEN) {
|
||||
return 0;
|
||||
}
|
||||
if (h->payload_len != FP_DELTA_HDR + count * 3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Pass 1: the whole list, before anything is painted. */
|
||||
prev = 0;
|
||||
for (k = 0; k < count; k++) {
|
||||
if (!frag_get(h->payload_off + FP_DELTA_HDR + k * 2, &lo)
|
||||
|| !frag_get(h->payload_off + FP_DELTA_HDR + k * 2 + 1,
|
||||
&hi)) {
|
||||
return 0;
|
||||
}
|
||||
pos = lo | (hi << 8);
|
||||
if (pos >= TILES_SCREEN) {
|
||||
return 0;
|
||||
}
|
||||
if (k > 0 && pos <= prev) {
|
||||
return 0;
|
||||
}
|
||||
prev = pos;
|
||||
if (!frag_get(h->payload_off + FP_DELTA_HDR + count * 2 + k,
|
||||
&val)) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= DICT_ENTRIES) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* The same canary rule as paint_tiles(): outside every tile, so an
|
||||
* unchanged value means nobody painted over this frame's screen. */
|
||||
sent = (unsigned int)*(unsigned short *)SENT_ADDR;
|
||||
if (sent != SENT_VALUE) {
|
||||
*(unsigned int *)OS_TOUCH = *(unsigned int *)OS_TOUCH + 1;
|
||||
*(unsigned short *)SENT_ADDR = SENT_VALUE;
|
||||
}
|
||||
|
||||
/* Pass 2: walk the grid, paint the named tiles. `pos` holds the next
|
||||
* named position; `tile` is the grid position the loop is on. */
|
||||
vid_dint_();
|
||||
frag_get(h->payload_off + FP_DELTA_HDR, &lo);
|
||||
frag_get(h->payload_off + FP_DELTA_HDR + 1, &hi);
|
||||
pos = lo | (hi << 8);
|
||||
k = 0;
|
||||
tile = 0;
|
||||
for (ty = 0; ty < TILE_ROWS; ty++) {
|
||||
for (tx = 0; tx < TILE_COLS; tx++) {
|
||||
if (k < count && pos == tile) {
|
||||
frag_get(h->payload_off + FP_DELTA_HDR
|
||||
+ count * 2 + k, &val);
|
||||
tile_blit(dict_base + (val << TILE_SHIFT),
|
||||
tx, ty);
|
||||
k = k + 1;
|
||||
if (k < count) {
|
||||
frag_get(h->payload_off + FP_DELTA_HDR
|
||||
+ k * 2, &lo);
|
||||
frag_get(h->payload_off + FP_DELTA_HDR
|
||||
+ k * 2 + 1, &hi);
|
||||
pos = lo | (hi << 8);
|
||||
}
|
||||
}
|
||||
tile = tile + 1;
|
||||
}
|
||||
}
|
||||
vid_eint_();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
@@ -986,17 +1163,20 @@ unsigned int i;
|
||||
}
|
||||
} else if (h.kind == FP_KIND_TILES_DELTA) {
|
||||
/*
|
||||
* Task 5 decodes deltas. This tree has no delta
|
||||
* decoder, and the point of the branch is that such a
|
||||
* frame is never silently forgotten: it is counted in
|
||||
* TILES_DROP and does not touch VRAM, so a delta frame
|
||||
* cannot leave half a picture on the screen. (A delta
|
||||
* frame that arrives before the dictionary is dropped
|
||||
* here too; the dictionary is fixed for the clip, so
|
||||
* DICT_READY is the same condition either way.)
|
||||
* Task 5: a changed-tile list. paint_delta() validates
|
||||
* the whole list before it paints, so a refused frame
|
||||
* touches no pixel; a frame it accepts moves the same
|
||||
* record cells a TILES_FULL does, and only then.
|
||||
*/
|
||||
tiles_drop = tiles_drop + 1;
|
||||
*(unsigned int *)TILES_DROP = tiles_drop;
|
||||
if (paint_delta(&h)) {
|
||||
*(unsigned int *)RX_LAST_LEN = h.payload_len;
|
||||
*(unsigned int *)RX_SLOT = slot;
|
||||
*(unsigned int *)RX_OK =
|
||||
*(unsigned int *)RX_OK + 1;
|
||||
} else {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user