mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-08 10:49:24 +00:00
Remove unused code.
Add a new DHGRBitmap class that efficiently represents the DHGR interleaving of the (aux, main) MemoryMap as a sequence of 28-bit integers. This allows for easily extracting the 8-bit and 12-bit subsequences representing the DHGR pixels that are influenced when storing a byte at offsets 0..3 within the interleaved (aux, main, aux, main) sequence. Since we have precomputed all of the pairwise differences between these 8- and 12-bit values, this allows us to efficiently compute the edit distances between pairs of screen bytes (and/or arrays)
This commit is contained in:
parent
9c90665e96
commit
15c77f2465
@ -1,18 +1,11 @@
|
||||
"""Various representations of Apple II video display."""
|
||||
|
||||
import functools
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
# TODO: support DHGR
|
||||
|
||||
|
||||
def bitmap_similarity(a1: np.array, a2: np.array) -> float:
|
||||
"""Measure bitwise % similarity between two bitmap arrays"""
|
||||
bits_different = np.sum(np.logical_xor(a1, a2)).item()
|
||||
|
||||
return 1. - (bits_different / (np.shape(a1)[0] * np.shape(a1)[1]))
|
||||
|
||||
|
||||
def y_to_base_addr(y: int, page: int = 0) -> int:
|
||||
"""Maps y coordinate to base address on given screen page"""
|
||||
a = y // 64
|
||||
@ -30,6 +23,7 @@ Y_TO_BASE_ADDR = [
|
||||
]
|
||||
|
||||
# Array mapping (page, offset) to x (byte) and y coords respectively
|
||||
# TODO: is np.dtype(int) faster for these?
|
||||
PAGE_OFFSET_TO_X = np.zeros((32, 256), dtype=np.uint8)
|
||||
PAGE_OFFSET_TO_Y = np.zeros((32, 256), dtype=np.uint8)
|
||||
|
||||
@ -68,111 +62,6 @@ def _populate_mappings():
|
||||
_populate_mappings()
|
||||
|
||||
|
||||
class Bytemap:
|
||||
"""Bitmap array with horizontal pixels packed into bytes."""
|
||||
|
||||
def __init__(self, bytemap: np.array = None):
|
||||
self.bytemap = None # type: np.array
|
||||
if bytemap is not None:
|
||||
if bytemap.shape != (192, 40):
|
||||
raise ValueError("Unexpected shape: %r" % (bytemap.shape,))
|
||||
self.bytemap = bytemap
|
||||
else:
|
||||
self.bytemap = np.zeros((192, 40), dtype=np.uint8)
|
||||
|
||||
def to_memory_map(self, screen_page: int) -> "MemoryMap":
|
||||
# Numpy magic that constructs a new array indexed by (page, offset)
|
||||
# instead of (y, x).
|
||||
mmap = self.bytemap[PAGE_OFFSET_TO_Y, PAGE_OFFSET_TO_X]
|
||||
# Reset whatever values ended up in the screen holes after this mapping
|
||||
# (which came from default 0 values in PAGE_OFFSET_TO_X)
|
||||
mmap[SCREEN_HOLES] = 0
|
||||
return MemoryMap(screen_page, mmap)
|
||||
|
||||
|
||||
class Bitmap:
|
||||
XMAX = None # type: int
|
||||
YMAX = None # type: int
|
||||
|
||||
def __init__(self, bitmap: np.array = None):
|
||||
if bitmap is None:
|
||||
self.bitmap = np.zeros((self.YMAX, self.XMAX), dtype=bool)
|
||||
else:
|
||||
if bitmap.shape != (self.YMAX, self.XMAX):
|
||||
raise ValueError("Unexpected shape: %r" % (bitmap.shape,))
|
||||
self.bitmap = bitmap
|
||||
|
||||
def randomize(self) -> None:
|
||||
self.bitmap = np.random.randint(
|
||||
2, size=(self.YMAX, self.XMAX), dtype=bool)
|
||||
|
||||
@staticmethod
|
||||
def _to_bytemap(bitmap) -> Bytemap:
|
||||
# Insert zero column after every 7
|
||||
pixels = bitmap.copy()
|
||||
for i in range(pixels.shape[1] // 7 - 1, -1, -1):
|
||||
pixels = np.insert(pixels, (i + 1) * 7, False, axis=1)
|
||||
|
||||
# packbits is big-endian so we flip the array before and after to
|
||||
# invert this
|
||||
return Bytemap(
|
||||
np.flip(np.packbits(np.flip(pixels, axis=1), axis=1), axis=1))
|
||||
|
||||
def to_bytemap(self) -> Bytemap:
|
||||
return self._to_bytemap(self.bitmap)
|
||||
|
||||
def to_memory_map(self, screen_page: int) -> "MemoryMap":
|
||||
return self.to_bytemap().to_memory_map(screen_page)
|
||||
|
||||
@staticmethod
|
||||
def _from_bytemap(bytemap: Bytemap) -> np.array:
|
||||
bm = np.unpackbits(bytemap.bytemap, axis=1)
|
||||
bm = np.delete(bm, np.arange(0, bm.shape[1], 8), axis=1)
|
||||
|
||||
# Need to flip each 7-bit sequence
|
||||
reorder_cols = []
|
||||
for i in range(bm.shape[1] // 7):
|
||||
for j in range((i + 1) * 7 - 1, i * 7 - 1, -1):
|
||||
reorder_cols.append(j)
|
||||
bm = bm[:, reorder_cols]
|
||||
|
||||
return np.array(bm, dtype=np.bool)
|
||||
|
||||
@classmethod
|
||||
def from_bytemap(cls, bytemap: Bytemap) -> "Bitmap":
|
||||
return cls(cls._from_bytemap(bytemap))
|
||||
|
||||
|
||||
class HGR140Bitmap(Bitmap):
|
||||
XMAX = 140 # double-wide pixels to not worry about colour effects
|
||||
YMAX = 192
|
||||
|
||||
def to_bytemap(self) -> Bytemap:
|
||||
# Double each pixel horizontally
|
||||
return self._to_bytemap(np.repeat(self.bitmap, 2, axis=1))
|
||||
|
||||
@classmethod
|
||||
def from_bytemap(cls, bytemap: Bytemap) -> "HGR140Bitmap":
|
||||
# Undouble pixels
|
||||
bitmap = cls._from_bytemap(bytemap)
|
||||
bitmap = np.array(
|
||||
np.delete(bitmap, np.arange(0, bitmap.shape[1], 2), axis=1),
|
||||
dtype=np.bool
|
||||
)
|
||||
|
||||
return HGR140Bitmap(bitmap)
|
||||
|
||||
|
||||
class HGRBitmap(Bitmap):
|
||||
XMAX = 280
|
||||
YMAX = 192
|
||||
|
||||
|
||||
class DHGRBitmap(Bitmap):
|
||||
XMAX = 560
|
||||
YMAX = 192
|
||||
|
||||
|
||||
class FlatMemoryMap:
|
||||
"""Linear 8K representation of HGR screen memory."""
|
||||
|
||||
@ -223,11 +112,92 @@ class MemoryMap:
|
||||
def to_flat_memory_map(self) -> FlatMemoryMap:
|
||||
return FlatMemoryMap(self.screen_page, self.page_offset.reshape(8192))
|
||||
|
||||
def to_bytemap(self) -> Bytemap:
|
||||
bytemap = self.page_offset[X_Y_TO_PAGE, X_Y_TO_OFFSET]
|
||||
return Bytemap(bytemap)
|
||||
|
||||
def write(self, page: int, offset: int, val: int) -> None:
|
||||
"""Updates screen image to set (page, offset)=val (inc. screen holes)"""
|
||||
|
||||
self.page_offset[page - self._page_start][offset] = val
|
||||
|
||||
|
||||
class DHGRBitmap:
|
||||
BYTE_MASK32 = [
|
||||
# 3333333222222211111110000000 <- byte 0.3
|
||||
#
|
||||
# 33222222222211111111110000000000 <- bit pos in uint32
|
||||
# 10987654321098765432109876543210
|
||||
# 0000GGGGFFFFEEEEDDDDCCCCBBBBAAAA <- pixel A..G
|
||||
# 3210321032103210321032103210 <- bit pos in A..G pixel
|
||||
0b00000000000000000000000011111111, # byte 0 influences A,B
|
||||
0b00000000000000001111111111110000, # byte 1 influences B,C,D
|
||||
0b00000000111111111111000000000000, # byte 2 influences D,E,F
|
||||
0b00001111111100000000000000000000, # byte 3 influences F,G
|
||||
]
|
||||
|
||||
# How much to right-shift bits after masking to bring into int8/int12 range
|
||||
BYTE_SHIFTS = [0, 4, 12, 20]
|
||||
|
||||
# Load edit distance matrices for masked, shifted byte 0..3 values
|
||||
# TODO: should go somewhere else since we don't use it here at all
|
||||
with open("transcoder/edit_distance.pickle", "rb") as ed:
|
||||
edit_distances = pickle.load(ed)
|
||||
|
||||
def __init__(self, main_memory: MemoryMap, aux_memory: MemoryMap):
|
||||
self.main_memory = main_memory
|
||||
self.aux_memory = aux_memory
|
||||
|
||||
self.packed = np.empty(shape=(32, 128), dtype=np.uint32)
|
||||
self._pack()
|
||||
|
||||
def _pack(self):
|
||||
"""Interleave and pack aux and main memory into 28-bit uint32 array"""
|
||||
|
||||
# Palette bit is unused for DHGR so mask it out
|
||||
aux = (self.aux_memory.page_offset & 0x7f).astype(np.uint32)
|
||||
main = (self.main_memory.page_offset & 0x7f).astype(np.uint32)
|
||||
|
||||
# Interleave aux and main memory columns and pack 7-bit masked values
|
||||
# into a 28-bit value. This sequentially encodes 7 4-bit DHGR pixels.
|
||||
# See make_data_tables.py for more discussion about this representation.
|
||||
self.packed = (
|
||||
aux[:, 0::2] +
|
||||
(main[:, 0::2] << 7) +
|
||||
(aux[:, 1::2] << 14) +
|
||||
(main[:, 1::2] << 21)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@functools.lru_cache(None)
|
||||
def interleaved_byte_offset(x_byte: int, is_aux: bool) -> int:
|
||||
"""Returns 0..3 offset in ByteTuple for a given x_byte and is_aux"""
|
||||
is_odd = x_byte % 2 == 1
|
||||
if is_aux:
|
||||
if is_odd:
|
||||
return 2
|
||||
return 0
|
||||
else: # main memory
|
||||
if is_odd:
|
||||
return 3
|
||||
else:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def masked_update(byte_offset: int, old_value, new_value: int):
|
||||
# Mask out 7-bit value where update will go
|
||||
masked_value = old_value & ~(0x7f << (7 * byte_offset))
|
||||
|
||||
update = (new_value & 0x7f) << (7 * byte_offset)
|
||||
|
||||
return masked_value ^ update
|
||||
|
||||
def apply(self, page: int, offset: int, is_aux: bool, value: int):
|
||||
"""Update packed representation of changing main/aux memory."""
|
||||
|
||||
byte_offset = self.interleaved_byte_offset(offset, is_aux)
|
||||
packed_offset = offset // 2
|
||||
|
||||
self.packed[page, packed_offset] = self.masked_update(
|
||||
byte_offset, self.packed[page, packed_offset], value)
|
||||
|
||||
def mask_and_shift_data(self, data, byte_offset):
|
||||
"""Masks and shifts data into the 8 or 12-bit range."""
|
||||
return (data & self.BYTE_MASK32[byte_offset]) >> (
|
||||
self.BYTE_SHIFTS[byte_offset])
|
||||
|
188
transcoder/screen_test.py
Normal file
188
transcoder/screen_test.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Tests for the screen module."""
|
||||
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
import screen
|
||||
|
||||
|
||||
class TestDHGRBitmap(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.aux = screen.MemoryMap(screen_page=1)
|
||||
self.main = screen.MemoryMap(screen_page=1)
|
||||
|
||||
def test_pixel_packing(self):
|
||||
# PBBBAAAA
|
||||
self.aux.page_offset[0, 0] = 0b11110101
|
||||
# PDDCCCCB
|
||||
self.main.page_offset[0, 0] = 0b01000011
|
||||
# PFEEEEDD
|
||||
self.aux.page_offset[0, 1] = 0b11110101
|
||||
# PGGGGFFF
|
||||
self.main.page_offset[0, 1] = 0b01000011
|
||||
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux)
|
||||
|
||||
self.assertEqual(
|
||||
0b1000011111010110000111110101,
|
||||
dhgr.packed[0, 0]
|
||||
)
|
||||
|
||||
def test_interleaved_byte_offset(self):
|
||||
self.assertEqual(
|
||||
0,
|
||||
screen.DHGRBitmap.interleaved_byte_offset(0, is_aux=True)
|
||||
)
|
||||
self.assertEqual(
|
||||
1,
|
||||
screen.DHGRBitmap.interleaved_byte_offset(0, is_aux=False)
|
||||
)
|
||||
self.assertEqual(
|
||||
2,
|
||||
screen.DHGRBitmap.interleaved_byte_offset(1, is_aux=True)
|
||||
)
|
||||
self.assertEqual(
|
||||
3,
|
||||
screen.DHGRBitmap.interleaved_byte_offset(1, is_aux=False)
|
||||
)
|
||||
|
||||
def test_mask_and_shift_data(self):
|
||||
int8_max = 2 ** 8 - 1
|
||||
int12_max = 2 ** 12 - 1
|
||||
int32_max = 2 ** 32 - 1
|
||||
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux)
|
||||
|
||||
self.assertEqual(
|
||||
int8_max,
|
||||
dhgr.mask_and_shift_data(
|
||||
screen.DHGRBitmap.BYTE_MASK32[0], 0
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
int12_max,
|
||||
dhgr.mask_and_shift_data(
|
||||
screen.DHGRBitmap.BYTE_MASK32[1], 1
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
int12_max,
|
||||
dhgr.mask_and_shift_data(
|
||||
screen.DHGRBitmap.BYTE_MASK32[2], 2
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
int8_max,
|
||||
dhgr.mask_and_shift_data(
|
||||
screen.DHGRBitmap.BYTE_MASK32[3], 3
|
||||
)
|
||||
)
|
||||
|
||||
# Now check complement, i.e. no bits taken from outside expected range
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
dhgr.mask_and_shift_data(
|
||||
~screen.DHGRBitmap.BYTE_MASK32[0] & int32_max, 0
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
dhgr.mask_and_shift_data(
|
||||
~screen.DHGRBitmap.BYTE_MASK32[1] & int32_max, 1
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
dhgr.mask_and_shift_data(
|
||||
~screen.DHGRBitmap.BYTE_MASK32[2] & int32_max, 2
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
dhgr.mask_and_shift_data(
|
||||
~screen.DHGRBitmap.BYTE_MASK32[3] & int32_max, 3
|
||||
)
|
||||
)
|
||||
|
||||
def test_masked_update(self):
|
||||
self.assertEqual(
|
||||
0b0000000000000000000001111111,
|
||||
screen.DHGRBitmap.masked_update(0, 0x00000000, 0xff)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b0000000000000011111110000000,
|
||||
screen.DHGRBitmap.masked_update(1, 0x00000000, 0xff)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b0000000111111100000000000000,
|
||||
screen.DHGRBitmap.masked_update(2, 0x00000000, 0xff)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b1111111000000000000000000000,
|
||||
screen.DHGRBitmap.masked_update(3, 0x00000000, 0xff)
|
||||
)
|
||||
|
||||
# Now test masking out existing values
|
||||
|
||||
int28_max = 2 ** 28 - 1
|
||||
|
||||
self.assertEqual(
|
||||
0b1111111111111111111110000000,
|
||||
screen.DHGRBitmap.masked_update(0, int28_max, 0x00)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b1111111111111100000001111111,
|
||||
screen.DHGRBitmap.masked_update(1, int28_max, 0x00)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b1111111000000011111111111111,
|
||||
screen.DHGRBitmap.masked_update(2, int28_max, 0x00)
|
||||
)
|
||||
self.assertEqual(
|
||||
0b0000000111111111111111111111,
|
||||
screen.DHGRBitmap.masked_update(3, int28_max, 0x00)
|
||||
)
|
||||
|
||||
# Test that masked_update can broadcast to numpy arrays
|
||||
ary = np.zeros((2, 2), dtype=np.uint32)
|
||||
self.assertTrue(np.array_equal(
|
||||
np.array([[0x7f, 0x7f], [0x7f, 0x7f]], dtype=np.uint32),
|
||||
screen.DHGRBitmap.masked_update(0, ary, 0xff)
|
||||
))
|
||||
|
||||
def test_apply(self):
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux)
|
||||
|
||||
dhgr.apply(page=0, offset=0, is_aux=True, value=0xff)
|
||||
self.assertEqual(0x0000007f, dhgr.packed[0, 0])
|
||||
|
||||
dhgr.apply(page=12, offset=36, is_aux=True, value=0xff)
|
||||
self.assertEqual(0x0000007f, dhgr.packed[12, 18])
|
||||
|
||||
# Now update the next aux offset in same uint32
|
||||
dhgr.apply(page=12, offset=37, is_aux=True, value=0xff)
|
||||
self.assertEqual(
|
||||
0b0000000111111100000001111111,
|
||||
dhgr.packed[12, 18]
|
||||
)
|
||||
|
||||
dhgr.apply(page=12, offset=37, is_aux=False, value=0b1010101)
|
||||
self.assertEqual(
|
||||
0b1010101111111100000001111111,
|
||||
dhgr.packed[12, 18]
|
||||
)
|
||||
|
||||
dhgr.apply(page=12, offset=36, is_aux=False, value=0b0001101)
|
||||
self.assertEqual(
|
||||
0b1010101111111100011011111111,
|
||||
dhgr.packed[12, 18]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user