118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""Regression tests: decode each mode's emitted VIC-II bytes and check they
|
|
reproduce the converter's own index image, and that every viewer assembles and
|
|
fits. Run with `pytest` or directly: `python tests/test_roundtrip.py`.
|
|
|
|
These tests exercise the byte-packing that the GUI preview deliberately does *not*
|
|
touch (the preview renders from the index image), so they are the safety net that
|
|
catches an encoding bug before it reaches a real C64.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from c64view import imageprep, palette as pal # noqa: E402
|
|
from c64view.convert import fli, hires, ifli, multicolor # noqa: E402
|
|
from c64view.viewer.assemble import SOURCES, build_viewer_prg, have_xa # noqa: E402
|
|
|
|
|
|
def _gradient(w, h):
|
|
yy, xx = np.mgrid[0:h, 0:w]
|
|
rgb = np.stack([(xx * 255 // w), (yy * 255 // h),
|
|
((xx + yy) * 255 // (w + h))], axis=-1)
|
|
return rgb.astype(np.uint8)
|
|
|
|
|
|
def _decode_mc(bitmap, screen, colram, bg):
|
|
dec = np.zeros((200, 160), np.uint8)
|
|
for cr in range(25):
|
|
for cc in range(40):
|
|
ci = cr * 40 + cc
|
|
lut = [bg, screen[ci] >> 4, screen[ci] & 0xF, colram[ci] & 0xF]
|
|
for r in range(8):
|
|
byte = bitmap[cr * 320 + cc * 8 + r]
|
|
for x in range(4):
|
|
dec[cr * 8 + r, cc * 4 + x] = lut[(byte >> (6 - 2 * x)) & 3]
|
|
return dec
|
|
|
|
|
|
def test_multicolor_roundtrip():
|
|
img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions())
|
|
c = multicolor.convert(img)
|
|
d = c.data
|
|
dec = _decode_mc(np.frombuffer(d[:8000], np.uint8),
|
|
np.frombuffer(d[8000:9000], np.uint8),
|
|
np.frombuffer(d[9000:10000], np.uint8), d[10000])
|
|
assert np.array_equal(dec, c.index_image)
|
|
|
|
|
|
def test_hires_roundtrip():
|
|
img = imageprep.prepare(_imgobj(320, 200), 320, 200, 1.0, imageprep.PrepOptions())
|
|
c = hires.convert(img)
|
|
bitmap = np.frombuffer(c.data[:8000], np.uint8)
|
|
screen = np.frombuffer(c.data[8000:9000], np.uint8)
|
|
dec = np.zeros((200, 320), np.uint8)
|
|
for cr in range(25):
|
|
for cc in range(40):
|
|
ci = cr * 40 + cc
|
|
fg, bgc = screen[ci] >> 4, screen[ci] & 0xF
|
|
for r in range(8):
|
|
byte = bitmap[cr * 320 + cc * 8 + r]
|
|
for x in range(8):
|
|
dec[cr * 8 + r, cc * 8 + x] = fg if (byte >> (7 - x)) & 1 else bgc
|
|
assert np.array_equal(dec, c.index_image)
|
|
|
|
|
|
def test_fli_roundtrip():
|
|
img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions())
|
|
c = fli.convert(img)
|
|
d = c.data
|
|
screens = [np.frombuffer(d[L * 1024:L * 1024 + 1000], np.uint8) for L in range(8)]
|
|
bitmap = np.frombuffer(d[8192:8192 + 8000], np.uint8)
|
|
colram = np.frombuffer(d[16384:16384 + 1000], np.uint8)
|
|
bg = d[17384]
|
|
dec = np.zeros((200, 160), np.uint8)
|
|
for cr in range(25):
|
|
for cc in range(40):
|
|
ci = cr * 40 + cc
|
|
for r in range(8):
|
|
sb = screens[r][ci]
|
|
lut = [bg, sb >> 4, sb & 0xF, colram[ci] & 0xF]
|
|
byte = bitmap[cr * 320 + cc * 8 + r]
|
|
for x in range(4):
|
|
dec[cr * 8 + r, cc * 4 + x] = lut[(byte >> (6 - 2 * x)) & 3]
|
|
assert np.array_equal(dec, c.index_image)
|
|
|
|
|
|
def test_interlace_blend_better():
|
|
"""Interlace blend error should beat plain multicolor on a gradient."""
|
|
img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions())
|
|
assert ifli.convert(img).error < multicolor.convert(img).error + 1e-6
|
|
assert len(ifli.convert(img).data) == 25577
|
|
|
|
|
|
def test_viewers_assemble_and_fit():
|
|
if not have_xa():
|
|
return # xa not installed; skip
|
|
sizes = {"hires": 9000, "multicolor": 10001, "fli": 17385,
|
|
"fli_ntsc": 17385, "interlace": 25577}
|
|
for key in SOURCES:
|
|
prg = build_viewer_prg(key, bytes(sizes[key]),
|
|
0x4000 if key.startswith("fli") else 0x2000)
|
|
assert prg[:2] == bytes([0x01, 0x08]) # PRG load address $0801
|
|
|
|
|
|
def _imgobj(w, h):
|
|
from PIL import Image
|
|
return Image.fromarray(_gradient(w, h), "RGB")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
for fn in fns:
|
|
fn()
|
|
print(f"PASS {fn.__name__}")
|
|
print(f"\nAll {len(fns)} tests passed.")
|