mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-11-02 12:23:17 +02:00
The tile pipeline packed every row MSB first: pixel i -> bit 7 - i % 8. The
encoder and tile_check.py shared that convention, so every MAME replay check
passed while a display showed each group of eight pixels mirrored.
Evidence, from the real clip's own replay dump (vram-replay-200.bin, frame 200
of Bad Apple, 1,228,800 pixels):
decoded MSB first 1,215 mismatched <- what the pipeline produced
decoded LSB first 3,113 mismatched <- the mirrored picture, a sawtooth
and both the real unit and MAME decode LSB first: dwt/screenshot.py --real
("LSB-first bits", measured on live VRAM) and vt1200.cpp's
(vram[bit >> 4] >> (bit & 15)) & 1. A zoomed edge of that dump showed the
comb of triangles; after this change the same edge is a smooth curve.
vidsrc.Rows.pack_row now sets bit i % 8 of byte i // 8. The test that pins it
was written first and watched fail: it decodes the packed row with the
hardware's rule, which no MSB-first packer can pass. The two older tests that
asserted MSB first were rewritten -- they were asserting the bug -- and the
synthetic clip now packs through a bit-reversal table, so the committed cache
is regenerated (PSNR 29.47 -> 31.30 dB). The layout claims in tilecost.py,
vram_tile.asm and make_tiles.load_clip are corrected so the clip format states
the order instead of leaving it to whoever writes the frames.
Separately, MAME's renderer reads 128-byte rows while a payload targeting the
real unit writes 256-byte rows (vram_tile.asm DPTCH=0x800; c-hello/README.md:
"a payload writing at row*2048 bits lands on scanline row"). VT1200_VRAM_REAL=1
now draws the framebuffer at 2048 bits / 1280 px from x=0. Off by default: the
Level-1 error text is only legible at the derived 128-byte pitch, and that is
what the faithful boot needs. The branch only touches the bitmap, so the
regression baseline is unmoved (PROG_FRAMES=200 -> PC=3FDCE400, samples=198,
3 distinct PCs), and the default frame-3000 render of the Terminal Manager is
unchanged.
Verified end to end on the 3,292-frame Bad Apple clip: RX_OK=3196, TILES_DROP=0,
3292/3292 frames dumped and matched by tile_check.py, and the dumps decode to
1,379 / 4,125 mismatched pixels of 1,228,800 (quantisation) under the hardware
rule versus 3,101 / 9,793 under MSB. MAME's own frame dumps with
VT1200_VRAM_REAL=1 show the clip unsheared.
Revert note for the driver: delete the m_render_real branch and its getenv and
vt1200.cpp returns to the measured 128-byte renderer byte for byte. No second
reader reviewed that citation; it is quoted in the comment for the next one.
186 lines
6.9 KiB
Python
186 lines
6.9 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, LSB first (bit 0 of a
|
|
byte is the leftmost pixel, the scanout order) -- 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)
|