Files
vt1200/harness/udpstream/tests/test_tilecost.py
T

185 lines
6.8 KiB
Python

"""Tests for the tile dictionary builder and the frame quantiser.
The plan's two tests come first, verbatim. The extras pin down what the plan
text leaves implicit but the later tasks depend on:
* the tile layout -- 32 rows of four bytes, row-major, MSB first -- so a
dictionary entry is exactly the bytes the terminal's ``PIXBLT B,XY`` reads
from ``dict + index*128``;
* the index frame's size. The plan says ``quantise`` returns 1,280 index bytes
(a 40x32 screen), but its own test feeds a 128-row frame, which is only 160
tiles. A buffer shorter than the screen is therefore the *top* of the screen
and the rows it does not cover quantise the all-zero tile; that convention is
pinned here rather than left to the caller to discover;
* the Hamming tie-break: equal distance resolves to the lowest index;
* validation, so a mis-sized frame is an error here and not a mis-painted
screen later.
"""
import random
import pytest
from harness.udpstream.tilecost import build_dictionary, quantise
#: One source row of the 1280-px screen, as ``vidsrc.Rows.pack_row`` packs it.
ROW_BYTES = 160
#: A 32-px tile is four bytes wide, so a screen row is 40 tiles.
COLS = 40
#: Dictionary entry size: 32 rows of four bytes.
TILE_BYTES = 128
def _frame(seed):
r = random.Random(seed)
return bytes(r.randrange(256) for _ in range(128 * 1280 // 8))
def test_dictionary_is_deterministic_and_indexable():
frames = [_frame(i) for i in range(4)]
a = build_dictionary(frames, k=8, seed=7)
b = build_dictionary(frames, k=8, seed=7)
assert a == b and len(a) == 8 * 128
def test_quantise_returns_1280_indices_within_range():
frames = [_frame(i) for i in range(4)]
d = build_dictionary(frames, k=8, seed=7)
idx = quantise(frames[0], d)
assert len(idx) == 1280 and max(idx) < 8
# --- layout --------------------------------------------------------------
def _screen_of(patterns, seed=0):
"""Build a 1280x1024 frame whose 32x32 tiles are drawn from ``patterns``."""
r = random.Random(seed)
screen = bytearray(ROW_BYTES * 1024)
for ty in range(32):
for tx in range(COLS):
tile = patterns[r.randrange(len(patterns))]
for row in range(32):
off = (ty * 32 + row) * ROW_BYTES + tx * 4
screen[off:off + 4] = tile[row * 4:row * 4 + 4]
return bytes(screen)
def _render(indices, dictionary):
"""Blit an index frame the way the terminal will: one block per index."""
screen = bytearray(ROW_BYTES * 1024)
for ty in range(32):
for tx in range(COLS):
base = indices[ty * COLS + tx] * TILE_BYTES
for row in range(32):
off = (ty * 32 + row) * ROW_BYTES + tx * 4
screen[off:off + 4] = dictionary[base + row * 4:
base + row * 4 + 4]
return bytes(screen)
def test_quantise_picks_the_byte_block_the_blitter_reads():
"""Tile (tx, ty) is rows ty*32..+31, bytes tx*4..tx*4+4 of each row."""
frame = bytearray(ROW_BYTES * 32) # one tile row: 40 tiles
tiles = []
for tx in range(COLS):
tile = bytearray()
for row in range(32):
block = bytes((tx * 7 + row * 3 + c) & 0xFF for c in range(4))
frame[row * ROW_BYTES + tx * 4:row * ROW_BYTES + tx * 4 + 4] = block
tile += block
tiles.append(bytes(tile))
idx = quantise(bytes(frame), b"".join(tiles))
# Every tile has distance 0 to its own dictionary entry, so the first row of
# the index frame names them in order -- and nothing else could.
assert idx[:COLS] == bytes(range(COLS))
def test_rows_below_a_short_frame_quantise_the_zero_tile():
"""A 32-row frame is the top of the screen; everything below is black."""
white = bytes([0xFF]) * TILE_BYTES
black = bytes(TILE_BYTES)
dictionary = white + black # index 0 white, 1 black
frame = bytes([0xFF]) * (ROW_BYTES * 32) # one all-white tile row
idx = quantise(frame, dictionary)
assert len(idx) == 1280
assert idx[:COLS] == bytes([0]) * COLS
assert idx[COLS:] == bytes([1]) * (1280 - COLS)
def test_equal_distance_resolves_to_the_lowest_index():
white = bytes([0xFF]) * TILE_BYTES
black = bytes(TILE_BYTES)
dictionary = white + black + black # entries 1 and 2 are identical
frame = bytes(ROW_BYTES * 32) # all black
idx = quantise(frame, dictionary)
assert set(idx) == {1}
# The same tie with the duplicate *first*: an exact match at index 0 must
# stop the search, not be overtaken by a later exact match.
dictionary = black + white + black
idx = quantise(frame, dictionary)
assert set(idx) == {0}
def test_a_dictionary_of_the_frames_own_tiles_reconstructs_the_frame():
rng = random.Random(3)
patterns = [bytes(rng.randrange(256) for _ in range(TILE_BYTES))
for _ in range(5)]
frames = [_screen_of(patterns, seed=s) for s in range(3)]
d = build_dictionary(frames, k=5, seed=1)
assert len(d) == 5 * TILE_BYTES
for frame in frames:
assert _render(quantise(frame, d), d) == frame
def test_build_dictionary_does_not_depend_on_frame_order():
"""Candidates are named by content, so frame order cannot matter."""
frames = [_frame(i) for i in range(4)]
assert (build_dictionary(frames, k=4, seed=2)
== build_dictionary(list(reversed(frames)), k=4, seed=2))
def test_build_dictionary_of_a_single_repeated_tile_is_that_tile():
"""The centroid is data, not a seeded sample: one distinct tile stays."""
pattern = bytes((i * 37 + 11) & 0xFF for i in range(TILE_BYTES))
frame = _screen_of([pattern], seed=0)
assert build_dictionary([frame], k=1, seed=0) == pattern
# --- validation ----------------------------------------------------------
@pytest.mark.parametrize("k", [0, -1, 257, 1000])
def test_build_dictionary_rejects_k_that_is_not_an_index_byte(k):
with pytest.raises(ValueError):
build_dictionary([_frame(0)], k=k)
def test_build_dictionary_rejects_no_frames():
with pytest.raises(ValueError):
build_dictionary([], k=8)
def test_frames_must_be_whole_160_byte_rows_of_whole_32_row_tiles():
d = bytes(TILE_BYTES)
with pytest.raises(ValueError):
quantise(bytes(ROW_BYTES * 32 + 1), d) # half a row
with pytest.raises(ValueError):
quantise(bytes(ROW_BYTES * 33), d) # half a tile row
with pytest.raises(ValueError):
quantise(bytes(ROW_BYTES * 1056), d) # below the 1024-row screen
with pytest.raises(ValueError):
build_dictionary([bytes(ROW_BYTES * 31)], k=1)
def test_the_dictionary_is_a_whole_number_of_tiles_of_at_most_256():
frame = bytes(ROW_BYTES * 32)
for bad in (b"", bytes(TILE_BYTES + 1), bytes(257 * TILE_BYTES)):
with pytest.raises(ValueError):
quantise(frame, bad)