diff --git a/transcoder/colours.py b/transcoder/colours.py index 7def093..691543d 100644 --- a/transcoder/colours.py +++ b/transcoder/colours.py @@ -1,25 +1,140 @@ -"""Apple II logical display colours.""" +"""Apple II nominal display colours, represented by 4-bit dot sequences. + +These are distinct from the effective colours that are actually displayed, +e.g. due to white/black coalescing and NTSC artifacting. +""" + +from typing import Tuple, Type import enum +import functools -class DHGRColours(enum.Enum): +def ror(int4: int, howmany: int) -> int: + """Rotate-right an int4 some number of times.""" + res = int4 + for _ in range(howmany): + res = _ror(res) + + return res + + +def _ror(int4: int) -> int: + return ((int4 & 0b1110) >> 1) ^ ((int4 & 0b0001) << 3) + + +def rol(int4: int, howmany: int) -> int: + """Rotate-left an int4 some number of times.""" + res = int4 + for _ in range(howmany): + res = _rol(res) + + return res + + +def _rol(int4: int) -> int: + return ((int4 & 0b0111) << 1) ^ ((int4 & 0b1000) >> 3) + + +class NominalColours(enum.Enum): + pass + + +class HGRColours(NominalColours): # Value is memory bit order, which is opposite to screen order (bits # ordered Left to Right on screen) BLACK = 0b0000 + MAGENTA = 0b0001 + BROWN = 0b1000 + ORANGE = 0b1001 # HGR colour + DARK_GREEN = 0b0100 + GREY1 = 0b0101 + GREEN = 0b1100 # HGR colour + YELLOW = 0b1101 + DARK_BLUE = 0b0010 + VIOLET = 0b0011 # HGR colour + GREY2 = 0b1010 + PINK = 0b1011 + MED_BLUE = 0b0110 # HGR colour + LIGHT_BLUE = 0b0111 + AQUA = 0b1110 + WHITE = 0b1111 + + +class DHGRColours(NominalColours): + # DHGR 4-bit memory representation is right-rotated from the HGR video + # representation. + BLACK = 0b0000 MAGENTA = 0b1000 BROWN = 0b0100 - ORANGE = 0b1100 + ORANGE = 0b1100 # HGR colour DARK_GREEN = 0b0010 GREY1 = 0b1010 - GREEN = 0b0110 + GREEN = 0b0110 # HGR colour YELLOW = 0b1110 DARK_BLUE = 0b0001 - VIOLET = 0b1001 + VIOLET = 0b1001 # HGR colour GREY2 = 0b0101 PINK = 0b1101 - MED_BLUE = 0b0011 + MED_BLUE = 0b0011 # HGR colour LIGHT_BLUE = 0b1011 AQUA = 0b0111 WHITE = 0b1111 + +# @functools.lru_cache(None) +# def int28_to_nominal_colour_pixels2(int28): +# return tuple( +# HGRColours( +# (int28 & (0b1111 << (4 * i))) >> (4 * i)) for i in range(7) +# ) + + +@functools.lru_cache(None) +def int34_to_nominal_colour_pixels( + int34: int, + colours: Type[NominalColours], + init_phase: int = 1 # Such that phase = 0 at start of 28-bit body +) -> Tuple[NominalColours]: + """Produce sequence of 31 nominal colour pixels via sliding 4-bit window. + + Includes the 3-bit header that represents the trailing 3 bits of the + previous 28-bit tuple. i.e. storing a byte in aux even columns will also + influence the colours of the previous main odd column. + + This naively models the NTSC colour artifacting. + + TODO: Use a more careful colour composition model to produce effective + pixel colours. + + TODO: DHGR vs HGR colour differences can be modeled by changing init_phase + """ + res = [] + + shifted = int34 + phase = init_phase + + # Omit trailing 3 bits which are only there to provide a trailer for + # bits 28..31 + for i in range(31): + colour = rol(shifted & 0b1111, phase) + res.append(colours(colour)) + + shifted >>= 1 + phase += 1 + if phase == 4: + phase = 0 + + return tuple(res) + + +@functools.lru_cache(None) +def int34_to_nominal_colour_pixel_values( + int34: int, + colours: Type[NominalColours], + init_phase: int = 1 # Such that phase = 0 at start of 28-bit body +) -> Tuple[int]: + return tuple(p.value for p in int34_to_nominal_colour_pixels( + int34, colours, init_phase + )) + diff --git a/transcoder/colours_test.py b/transcoder/colours_test.py new file mode 100644 index 0000000..0be88b2 --- /dev/null +++ b/transcoder/colours_test.py @@ -0,0 +1,107 @@ +import unittest + +import colours + +HGRColours = colours.HGRColours + + +class TestColours(unittest.TestCase): + + def test_int28_to_pixels(self): + self.assertEqual( + ( + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.DARK_BLUE, + HGRColours.MED_BLUE, + HGRColours.AQUA, + HGRColours.AQUA, + HGRColours.GREEN, + HGRColours.BROWN, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + ), + colours.int34_to_nominal_colour_pixels( + 0b00000000000000000000111000000000, HGRColours + ) + ) + + self.assertEqual( + ( + HGRColours.BLACK, + HGRColours.MAGENTA, + HGRColours.VIOLET, + HGRColours.LIGHT_BLUE, + HGRColours.WHITE, + HGRColours.AQUA, + HGRColours.GREEN, + HGRColours.BROWN, + HGRColours.BLACK, + HGRColours.MAGENTA, + HGRColours.VIOLET, + HGRColours.LIGHT_BLUE, + HGRColours.WHITE, + HGRColours.AQUA, + HGRColours.GREEN, + HGRColours.BROWN, + HGRColours.BLACK, + HGRColours.MAGENTA, + HGRColours.VIOLET, + HGRColours.LIGHT_BLUE, + HGRColours.WHITE, + HGRColours.AQUA, + HGRColours.GREEN, + HGRColours.BROWN, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK + ), + colours.int34_to_nominal_colour_pixels( + 0b0000111100001111000011110000, HGRColours + ) + ) + + +class TestRolRoR(unittest.TestCase): + def testRolOne(self): + self.assertEqual(0b1111, colours.rol(0b1111, 1)) + self.assertEqual(0b0001, colours.rol(0b1000, 1)) + self.assertEqual(0b1010, colours.rol(0b0101, 1)) + + def testRolMany(self): + self.assertEqual(0b1111, colours.rol(0b1111, 3)) + self.assertEqual(0b0010, colours.rol(0b1000, 2)) + self.assertEqual(0b0101, colours.rol(0b0101, 2)) + + def testRorOne(self): + self.assertEqual(0b1111, colours.ror(0b1111, 1)) + self.assertEqual(0b1000, colours.ror(0b0001, 1)) + self.assertEqual(0b0101, colours.ror(0b1010, 1)) + + def testRoRMany(self): + self.assertEqual(0b1111, colours.ror(0b1111, 3)) + self.assertEqual(0b1000, colours.ror(0b0010, 2)) + self.assertEqual(0b0101, colours.ror(0b0101, 2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/transcoder/make_data_tables.py b/transcoder/make_data_tables.py index 6f00280..fa917ae 100644 --- a/transcoder/make_data_tables.py +++ b/transcoder/make_data_tables.py @@ -1,6 +1,8 @@ import bz2 import functools import pickle +import time +import datetime from typing import Iterable, Type import colormath.color_conversions @@ -77,54 +79,8 @@ def pixel_char(i: int) -> str: @functools.lru_cache(None) -def pixel_string(pixels: Iterable[colours.DHGRColours]) -> str: - return "".join(pixel_char(p.value) for p in pixels) - - -@functools.lru_cache(None) -def pixels_influenced_by_byte_index( - pixels: str, - idx: int) -> str: - """Return subset of pixels that are influenced by given byte index (0..4)""" - start, end = { - 0: (0, 1), - 1: (1, 3), - 2: (3, 5), - 3: (5, 6) - }[idx] - - return pixels[start:end + 1] - - -@functools.lru_cache(None) -def int28_to_pixels(int28): - return tuple( - palette.DHGRColours( - (int28 & (0b1111 << (4 * i))) >> (4 * i)) for i in range(7) - ) - - -# TODO: these duplicates byte_mask32/byte_shift from DHGRBitmap - -# Map n-bit int into 32-bit masked value -def map_int8_to_mask32_0(int8): - assert 0 <= int8 < 2 ** 8, int8 - return int8 - - -def map_int12_to_mask32_1(int12): - assert 0 <= int12 < 2 ** 12, int12 - return int12 << 4 - - -def map_int12_to_mask32_2(int12): - assert 0 <= int12 < 2 ** 12, int12 - return int12 << 12 - - -def map_int8_to_mask32_3(int8): - assert 0 <= int8 < 2 ** 8, int8 - return int8 << 20 +def pixel_string(pixels: Iterable[int]) -> str: + return "".join(pixel_char(p) for p in pixels) class EditDistanceParams: @@ -179,7 +135,6 @@ def make_substitute_costs(pal: Type[palette.BasePalette]): return edp -@functools.lru_cache(None) def edit_distance( edp: EditDistanceParams, a: str, @@ -199,66 +154,70 @@ def edit_distance( def make_edit_distance(edp: EditDistanceParams): edit = [ - np.zeros(shape=(2 ** 16), dtype=np.int16), - np.zeros(shape=(2 ** 24), dtype=np.int16), - np.zeros(shape=(2 ** 24), dtype=np.int16), - np.zeros(shape=(2 ** 16), dtype=np.int16), + np.zeros(shape=(2 ** 26), dtype=np.uint16), + np.zeros(shape=(2 ** 26), dtype=np.uint16), + np.zeros(shape=(2 ** 26), dtype=np.uint16), + np.zeros(shape=(2 ** 26), dtype=np.uint16), ] - for i in range(2 ** 8): - print(i) - for j in range(2 ** 8): - pair = (i << 8) + j + start_time = time.time() - first = map_int8_to_mask32_0(i) - second = map_int8_to_mask32_0(j) + for i in range(2 ** 13): + if i > 1: + now = time.time() + eta = datetime.timedelta( + seconds=(now - start_time) * (2 ** 13 / i)) + print("%.2f%% (ETA %s)" % (100 * i / (2 ** 13), eta)) + for j in range(2 ** 13): + pair = (i << 13) + j - first_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(first)), 0) - second_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(second)), 0) + # Each DHGR byte offset has the same range of int13 possible + # values and nominal colour pixels, but with different initial + # phases: + # AUX 0: 0 (1 at start of 3-bit header) + # MAIN 0: 3 (0) + # AUX 1: 2 (3) + # MAIN 1: 1 (2) + first_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + i, colours.DHGRColours, init_phase=1) + ) + second_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + j, colours.DHGRColours, init_phase=1)) edit[0][pair] = edit_distance( edp, first_pixels, second_pixels, error=False) - first = map_int8_to_mask32_3(i) - second = map_int8_to_mask32_3(j) - - first_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(first)), 3) - second_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(second)), 3) - - edit[3][pair] = edit_distance( - edp, first_pixels, second_pixels, error=False) - - for i in range(2 ** 12): - print(i) - for j in range(2 ** 12): - pair = (i << 12) + j - - first = map_int12_to_mask32_1(i) - second = map_int12_to_mask32_1(j) - - first_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(first)), 1) - second_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(second)), 1) - + first_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + i, colours.DHGRColours, init_phase=0) + ) + second_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + j, colours.DHGRColours, init_phase=0)) edit[1][pair] = edit_distance( edp, first_pixels, second_pixels, error=False) - first = map_int12_to_mask32_2(i) - second = map_int12_to_mask32_2(j) - - first_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(first)), 2) - second_pixels = pixels_influenced_by_byte_index( - pixel_string(int28_to_pixels(second)), 2) - + first_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + i, colours.DHGRColours, init_phase=3) + ) + second_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + j, colours.DHGRColours, init_phase=3)) edit[2][pair] = edit_distance( edp, first_pixels, second_pixels, error=False) + first_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + i, colours.DHGRColours, init_phase=2) + ) + second_pixels = pixel_string( + colours.int34_to_nominal_colour_pixel_values( + j, colours.DHGRColours, init_phase=2)) + edit[3][pair] = edit_distance( + edp, first_pixels, second_pixels, error=False) return edit @@ -269,7 +228,7 @@ def main(): edit = make_edit_distance(edp) # TODO: error distance matrices - data = "transcoder/data/palette_%d_edit_distance.pickle" \ + data = "transcoder/data/DHGR_palette_%d_edit_distance.pickle" \ ".bz2" % p.ID.value with bz2.open(data, "wb", compresslevel=9) as out: pickle.dump(edit, out, protocol=pickle.HIGHEST_PROTOCOL) diff --git a/transcoder/make_data_tables_test.py b/transcoder/make_data_tables_test.py index b8d0d27..ab951e3 100644 --- a/transcoder/make_data_tables_test.py +++ b/transcoder/make_data_tables_test.py @@ -1,12 +1,12 @@ import unittest -from colours import DHGRColours +from colours import HGRColours import make_data_tables class TestMakeDataTables(unittest.TestCase): def test_pixel_string(self): - pixels = (DHGRColours.BLACK, DHGRColours.WHITE, DHGRColours.ORANGE) + pixels = (HGRColours.BLACK, HGRColours.WHITE, HGRColours.ORANGE) self.assertEqual("0FC", make_data_tables.pixel_string(pixels)) def test_pixels_influenced_by_byte_index(self): @@ -22,39 +22,6 @@ class TestMakeDataTables(unittest.TestCase): make_data_tables.pixels_influenced_by_byte_index(pixels, 1) ) - def test_int28_to_pixels(self): - self.assertEqual( - ( - DHGRColours.BLACK, - DHGRColours.BLACK, - DHGRColours.YELLOW, - DHGRColours.BLACK, - DHGRColours.BLACK, - DHGRColours.BLACK, - DHGRColours.BLACK, - ), - tuple( - make_data_tables.int28_to_pixels( - 0b00000000000000000000111000000000) - ) - ) - - self.assertEqual( - ( - DHGRColours.BLACK, - DHGRColours.WHITE, - DHGRColours.BLACK, - DHGRColours.WHITE, - DHGRColours.BLACK, - DHGRColours.WHITE, - DHGRColours.BLACK, - ), - tuple( - make_data_tables.int28_to_pixels( - 0b0000111100001111000011110000) - ) - ) - def test_map_to_mask32(self): byte_mask32 = [ # 33222222222211111111110000000000 <- bit pos in uint32 diff --git a/transcoder/palette.py b/transcoder/palette.py index a3d33e6..77f7058 100644 --- a/transcoder/palette.py +++ b/transcoder/palette.py @@ -3,7 +3,7 @@ from typing import Dict, Type import colormath.color_objects -from colours import DHGRColours +from colours import HGRColours # Type annotation RGB = colormath.color_objects.sRGBColor @@ -24,7 +24,7 @@ class BasePalette: ID = Palette.UNKNOWN # type: Palette # Palette RGB map - RGB = {} # type: Dict[DHGRColours: RGB] + RGB = {} # type: Dict[HGRColours: RGB] class NTSCPalette(BasePalette): @@ -32,22 +32,22 @@ class NTSCPalette(BasePalette): # Palette RGB values taken from BMP2DHGR's default NTSC palette RGB = { - DHGRColours.BLACK: rgb(0, 0, 0), - DHGRColours.MAGENTA: rgb(148, 12, 125), - DHGRColours.BROWN: rgb(99, 77, 0), - DHGRColours.ORANGE: rgb(249, 86, 29), - DHGRColours.DARK_GREEN: rgb(51, 111, 0), - DHGRColours.GREY1: rgb(126, 126, 126), - DHGRColours.GREEN: rgb(67, 200, 0), - DHGRColours.YELLOW: rgb(221, 206, 23), - DHGRColours.DARK_BLUE: rgb(32, 54, 212), - DHGRColours.VIOLET: rgb(188, 55, 255), - DHGRColours.GREY2: rgb(126, 126, 126), - DHGRColours.PINK: rgb(255, 129, 236), - DHGRColours.MED_BLUE: rgb(7, 168, 225), - DHGRColours.LIGHT_BLUE: rgb(158, 172, 255), - DHGRColours.AQUA: rgb(93, 248, 133), - DHGRColours.WHITE: rgb(255, 255, 255) + HGRColours.BLACK: rgb(0, 0, 0), + HGRColours.MAGENTA: rgb(148, 12, 125), + HGRColours.BROWN: rgb(99, 77, 0), + HGRColours.ORANGE: rgb(249, 86, 29), + HGRColours.DARK_GREEN: rgb(51, 111, 0), + HGRColours.GREY1: rgb(126, 126, 126), + HGRColours.GREEN: rgb(67, 200, 0), + HGRColours.YELLOW: rgb(221, 206, 23), + HGRColours.DARK_BLUE: rgb(32, 54, 212), + HGRColours.VIOLET: rgb(188, 55, 255), + HGRColours.GREY2: rgb(126, 126, 126), + HGRColours.PINK: rgb(255, 129, 236), + HGRColours.MED_BLUE: rgb(7, 168, 225), + HGRColours.LIGHT_BLUE: rgb(158, 172, 255), + HGRColours.AQUA: rgb(93, 248, 133), + HGRColours.WHITE: rgb(255, 255, 255) } @@ -56,22 +56,22 @@ class IIGSPalette(BasePalette): # Palette RGB values taken from BMP2DHGR's KEGS32 palette RGB = { - DHGRColours.BLACK: rgb(0, 0, 0), - DHGRColours.MAGENTA: rgb(221, 0, 51), - DHGRColours.BROWN: rgb(136, 85, 34), - DHGRColours.ORANGE: rgb(255, 102, 0), - DHGRColours.DARK_GREEN: rgb(0, 119, 0), - DHGRColours.GREY1: rgb(85, 85, 85), - DHGRColours.GREEN: rgb(0, 221, 0), - DHGRColours.YELLOW: rgb(255, 255, 0), - DHGRColours.DARK_BLUE: rgb(0, 0, 153), - DHGRColours.VIOLET: rgb(221, 0, 221), - DHGRColours.GREY2: rgb(170, 170, 170), - DHGRColours.PINK: rgb(255, 153, 136), - DHGRColours.MED_BLUE: rgb(34, 34, 255), - DHGRColours.LIGHT_BLUE: rgb(102, 170, 255), - DHGRColours.AQUA: rgb(0, 255, 153), - DHGRColours.WHITE: rgb(255, 255, 255) + HGRColours.BLACK: rgb(0, 0, 0), + HGRColours.MAGENTA: rgb(221, 0, 51), + HGRColours.BROWN: rgb(136, 85, 34), + HGRColours.ORANGE: rgb(255, 102, 0), + HGRColours.DARK_GREEN: rgb(0, 119, 0), + HGRColours.GREY1: rgb(85, 85, 85), + HGRColours.GREEN: rgb(0, 221, 0), + HGRColours.YELLOW: rgb(255, 255, 0), + HGRColours.DARK_BLUE: rgb(0, 0, 153), + HGRColours.VIOLET: rgb(221, 0, 221), + HGRColours.GREY2: rgb(170, 170, 170), + HGRColours.PINK: rgb(255, 153, 136), + HGRColours.MED_BLUE: rgb(34, 34, 255), + HGRColours.LIGHT_BLUE: rgb(102, 170, 255), + HGRColours.AQUA: rgb(0, 255, 153), + HGRColours.WHITE: rgb(255, 255, 255) } diff --git a/transcoder/screen.py b/transcoder/screen.py index 845cd04..63b6260 100644 --- a/transcoder/screen.py +++ b/transcoder/screen.py @@ -3,10 +3,11 @@ import bz2 import functools import pickle -from typing import Union, List +from typing import Union, List, Optional import numpy as np -import palette + +import palette as pal # Type annotation for cases where we may process either an int or a numpy array. IntOrArray = Union[int, np.ndarray] @@ -124,55 +125,334 @@ class MemoryMap: 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 - ] +@functools.lru_cache(None) +def _edit_distances(name: str, palette_id: pal.Palette) -> List[np.ndarray]: + """Load edit distance matrices for masked, shifted byte values. - # How much to right-shift bits after masking to bring into int8/int12 range - BYTE_SHIFTS = [0, 4, 12, 20] + This is defined at module level to be a singleton. + """ + data = "transcoder/data/%s_palette_%d_edit_distance.pickle.bz2" % ( + name, + palette_id.value + ) + with bz2.open(data, "rb") as ed: + return pickle.load(ed) # type: List[np.ndarray] - @staticmethod - @functools.lru_cache(None) - def edit_distances(palette_id: palette.Palette) -> List[np.ndarray]: - """Load edit distance matrices for masked, shifted byte 0..3 values.""" - data = "transcoder/data/palette_%d_edit_distance.pickle.bz2" % ( - palette_id.value - ) - with bz2.open(data, "rb") as ed: - return pickle.load(ed) # type: List[np.ndarray] - def __init__(self, main_memory: MemoryMap, aux_memory: MemoryMap): - self.main_memory = main_memory - self.aux_memory = aux_memory +class Bitmap: + """Packed 28-bit bitmap representation of (D)HGR screen memory. - self.packed = np.empty(shape=(32, 128), dtype=np.uint32) + The memory layout is still page-oriented, not linear y-x buffer but the + bit map is such that 20 consecutive entries linearly encode the 28*20 = + 560-bit monochrome dot positions that underlie both Mono and Colour ( + D)HGR screens. + + For Colour display the (nominal) colours are encoded as 4-bit pixels. + """ + + def __init__( + self, + palette: pal.Palette, + main_memory: MemoryMap, + aux_memory: Optional[MemoryMap] + ): + self.palette = palette # type: pal.Palette + self.main_memory = main_memory # type: MemoryMap + self.aux_memory = aux_memory # type: Optional[MemoryMap] + + self.packed = np.empty( + shape=(32, 128), dtype=np.uint64) # type: np.ndarray self._pack() def _pack(self) -> None: - """Interleave and pack aux and main memory into 28-bit uint32 array""" + """Pack MemoryMap into 34-bit representation.""" + raise NotImplementedError + + NAME = None + + @functools.lru_cache(None) + def edit_distances(self, palette_id: pal.Palette) -> List[np.ndarray]: + """Load edit distance matrices for masked, shifted byte values.""" + return _edit_distances(self.NAME, palette_id) + + def apply( + self, + page: int, + offset: np.uint8, + is_aux: bool, + value: np.uint8) -> None: + raise NotImplementedError + + @functools.lru_cache(None) + def byte_pair_difference( + self, + byte_offset: int, + old_packed: int, + content: int + ) -> int: + raise NotImplementedError + + def diff_weights( + self, + other: "DHGRBitmap", + is_aux: bool + ) -> np.ndarray: + raise NotImplementedError + + def compute_delta( + self, + content: int, + old: np.ndarray, + is_aux: bool + ) -> np.ndarray: + raise NotImplementedError + + +class HGRBitmap(Bitmap): + BYTE_MASK16 = [ + # 11111110000000 <- byte 0, 1 + # 1111110000000000 + # 5432109876543210 + # 00GGFFEEDDCCBBAA <- pixel A..G + 0b0000000011111111, + 0b0011111111000000 + ] + + # Representation + # + # 1111110000000000 + # 5432109876543210 + # PGGFFEEDPDCCBBAA + # + # Where palette bit influences all of the pixels in the byte + # + # Map to 3-bit pixels, i.e. 21-bit quantity + # + # 222211111111110000000000 + # 321098765432109876543210 + # 000PGGPFFPEEPDDPCCPBBPAA + BYTE_MASK32 = [ + 0b000000000000111111111111, + 0b000111111111111000000000 + ] + + # XXX 3-bit pixel isn't quite correct, e.g. the case of conflicting + # palette bits across byte boundary + # Also hard to interleave the palette bit in multiple places - could use + # a mapping array but maybe don't need to, can just use 8-bit values as is? + # But need contiguous representation for edit distance tables + + # P + # (0)00 --> 0.0. + # (0)01 --> 0.1. + # + # (1)01 --> .0.1 + # (1)11 --> .1.1 + # etc + + # + + BYTE_SHIFTS = [0, 9] + + NAME = 'HGR' + + def __init__(self, palette: pal.Palette, main_memory: MemoryMap): + super(HGRBitmap, self).__init__(palette, main_memory, None) + + def _pack(self) -> None: + """Pack main memory into (28+3)-bit uint64 array""" + + # 00000000001111111111222222222233 + # 01234567890123456789012345678901 + # AAAABBBBCCCCDDd + # AAAABBBBCCCCDd + # DDEEEEFFFFGGGGg + # dDDEEEEFFFFGGGg + + # Even, P0: store unshifted (0..14) + # Even, P1: store shifted << 1 (1..15) (only need 1..14) + + # Odd, P0: store shifted << 14 (14 .. 28) - set bit 14 as bit 0 of next + # byte + # Odd, p1: store shifted << 15 (15 .. 29) (only need 15 .. 28) - set + # bit 13 as bit 0 of next byte + + # Odd overflow only matters for even, P1 + # - bit 0 is either bit 14 if odd, P0 or bit 13 if odd, P1 + # - but these both come from the undoubled bit 6. + + main = self.main_memory.page_offset.astype(np.uint64) + + # Double 7-bit pixel data from a into 14-bit fat pixels, and extend MSB + # into 15-bits tohandle case when subsequent byte has palette bit set, + # i.e. is right-shifted by 1 dot. This only matters for even bytes + # with P=0 that are followed by odd bytes with P=1; in other cases + # this extra bit will be overwritten. + double = ( + # Bit pos 6 + ((main & 0x40) << 8) + ((main & 0x40) << 7) + ( + (main & 0x40) << 6)) + ( + # Bit pos 5 + ((main & 0x20) << 6) + ((main & 0x20) << 5)) + ( + # Bit pos 4 + ((main & 0x10) << 5) + ((main & 0x10) << 4)) + ( + # Bit pos 3 + ((main & 0x08) << 4) + ((main & 0x08) << 3)) + ( + # Bit pos 2 + ((main & 0x04) << 3) + ((main & 0x04) << 2)) + ( + # Bit pos 1 + ((main & 0x02) << 2) + ((main & 0x02) << 1)) + ( + # Bit pos 0 + ((main & 0x01) << 1) + (main & 0x01)) + + a_even = main[:, ::2] + a_odd = main[:, 1::2] + + double_even = double[:, ::2] + double_odd = double[:, 1::2] + + # Place even offsets at bits 1..15 (P=1) or 0..14 (P=0) + packed = np.where(a_even & 0x80, double_even << 1, double_even) + + # Place off offsets at bits 15..27 (P=1) or 14..27 (P=0) + packed = np.where( + a_odd & 0x80, + np.bitwise_xor( + np.bitwise_and(packed, (2 ** 15 - 1)), + double_odd << 15 + ), + np.bitwise_xor( + np.bitwise_and(packed, (2 ** 14 - 1)), + double_odd << 14 + ) + ) + + # Patch up even offsets with P=1 with extended bit from previous odd + # column + + previous_odd = np.roll(a_odd, 1, axis=1).astype(np.uint64) + + packed = np.where( + a_even & 0x80, + # Truncate to 28-bits and set bit 0 from bit 6 of previous byte + np.bitwise_xor( + np.bitwise_and(packed, (2 ** 28 - 2)), + (previous_odd & (1 << 6)) >> 6 + ), + # Truncate to 28-bits + np.bitwise_and(packed, (2 ** 28 - 1)) + ) + + # Append first 3 bits of next even byte so we can correctly + # decode the effective colours at the end of the 28-bit tuple + trailing = np.roll(packed, -1, axis=1).astype(np.uint64) + + packed = np.bitwise_xor( + packed, + (trailing & 0b111) << 28 + ) + + self.packed = packed + + @staticmethod + @functools.lru_cache(None) + def byte_offset(x_byte: int) -> int: + """Returns 0..1 offset in ByteTuple for a given x_byte,""" + is_odd = x_byte % 2 == 1 + + return 1 if is_odd else 0 + + @staticmethod + def masked_update( + byte_offset: int, + old_value: IntOrArray, + new_value: int) -> IntOrArray: + raise NotImplementedError + + def apply(self, page: int, offset: int, is_aux: bool, value: int) -> None: + """Update packed representation of changing main/aux memory.""" + + assert not is_aux + + # XXX fix + + byte_offset = self.byte_offset(offset) + packed_offset = offset // 2 + + self.packed[page, packed_offset] = self.masked_update( + byte_offset, self.packed[page, packed_offset], value) + + # XXXX Generic? + def mask_and_shift_data( + self, + data: IntOrArray, + byte_offset: int) -> IntOrArray: + """Masks and shifts data into the 8 or 12-bit range.""" + return (data & self.BYTE_MASK32[byte_offset]) >> ( + self.BYTE_SHIFTS[byte_offset]) + + +class DHGRBitmap(Bitmap): + # NOTE: See https://github.com/numpy/numpy/issues/2524 and related issues + # for why we have to cast things explicitly to np.uint64 - type promotion + # to uint64 is broken in numpy :( + + # 3-bit header + 28-bit body + 3-bit trailer + BYTE_MASK34 = [ + # 3333333222222211111110000000 <- byte 0.3 + # + # 3333222222222211111111110000000000 <- bit pos in uint64 + # 3210987654321098765432109876543210 + # tttGGGGFFFFEEEEDDDDCCCCBBBBAAAAhhh <- pixel A..G + # 3210321032103210321032103210 <- bit pos in A..G pixel + np.uint64(0b0000000000000000000001111111111111), # byte 0 int13 mask + np.uint64(0b0000000000000011111111111110000000), # byte 1 int13 mask + np.uint64(0b0000000111111111111100000000000000), # byte 2 int13 mask + np.uint64(0b1111111111111000000000000000000000), # byte 3 int13 mask + ] + + # How much to right-shift bits after masking to bring into int13 range + BYTE_SHIFTS = [np.uint64(0), np.uint64(7), np.uint64(14), np.uint64(21)] + + NAME = 'DHGR' + + def _pack(self) -> None: + """Interleave and pack aux and main memory into 34-bit uint64 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) + aux = (self.aux_memory.page_offset & 0x7f).astype(np.uint64) + main = (self.main_memory.page_offset & 0x7f).astype(np.uint64) # 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. + # into a 28-bit value, with 3-bit header and trailer. This + # sequentially encodes 7 4-bit DHGR pixels, together with the + # neighbouring 3 bits that are necessary to decode artifact colours. + # # 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) + packed = ( + (aux[:, 0::2] << 3) + + (main[:, 0::2] << 10) + + (aux[:, 1::2] << 17) + + (main[:, 1::2] << 24) + ) + + # Prepend last 3 bits of previous main odd byte so we can correctly + # decode the effective colours at the beginning of the 28-bit + # tuple + prevcol = np.roll(packed, 1, axis=1).astype(np.uint64) + + # Append first 3 bits of next aux even byte so we can correctly + # decode the effective colours at the end of the 28-bit tuple + nextcol = np.roll(packed, -1, axis=1).astype(np.uint64) + + self.packed = np.bitwise_xor( + np.bitwise_xor( + packed, + # Prepend last 3 bits of 28-bit body from previous column + (prevcol & (0b111 << 28)) >> 28 + ), + # Append first 3 bits of 28-bit body from next column + (nextcol & (0b111 << 3)) << 28 ) @staticmethod @@ -190,31 +470,207 @@ class DHGRBitmap: else: return 1 + # XXX test @staticmethod - def masked_update( + def masked_update_scalar( byte_offset: int, - old_value: IntOrArray, - new_value: int) -> IntOrArray: + old_value: np.uint64, + new_value: np.uint8) -> np.uint64: # Mask out 7-bit value where update will go - masked_value = old_value & ~(0x7f << (7 * byte_offset)) + masked_value = old_value & ( + ~np.uint64(0x7f << (7 * byte_offset + 3))) - update = (new_value & 0x7f) << (7 * byte_offset) + update = (new_value & np.uint64(0x7f)) << np.uint64( + 7 * byte_offset + 3) - return masked_value ^ update + new = masked_value ^ update + return new - def apply(self, page: int, offset: int, is_aux: bool, value: int) -> None: + # XXX test + @staticmethod + def masked_update_array( + byte_offset: int, + old_value: np.ndarray, + new_value: int) -> np.ndarray: + # Mask out 7-bit value where update will go + masked_value = old_value & ( + ~np.uint64(0x7f << (7 * byte_offset + 3))) + + update = (new_value & np.uint64(0x7f)) << np.uint64(7 * byte_offset + 3) + + new = masked_value ^ update + + # TODO: don't leak headers across screen rows. + + if byte_offset == 0: + # Need to also update the 3-bit trailer of the preceding column + + shifted = np.roll(new, -1, axis=1) + + new &= np.uint64(2 ** 31 - 1) + new ^= (shifted & np.uint64(0b111 << 3)) << np.uint64(28) + elif byte_offset == 3: + # Need to also update the 3-bit header of the next column + + shifted = np.roll(new, 1, axis=1) + + new &= np.uint64((2 ** 31 - 1) << 3) + new ^= (shifted & np.uint64(0b111 << 28)) >> np.uint64(28) + return new + + # XXX test + def apply( + self, + page: int, + offset: int, + is_aux: bool, + value: np.uint8) -> None: """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( + self.packed[page, packed_offset] = self.masked_update_scalar( byte_offset, self.packed[page, packed_offset], value) + # TODO: don't leak headers/trailers across screen rows. + if byte_offset == 0 and packed_offset > 0: + # Need to also update the 3-bit trailer of the preceding column + self.packed[page, packed_offset - 1] &= np.uint64(2 ** 31 - 1) + + self.packed[page, packed_offset - 1] ^= ( + (self.packed[page, packed_offset] & np.uint64(0b111 << 3)) + << np.uint64(28) + ) + elif byte_offset == 3 and packed_offset < 127: + # Need to also update the 3-bit header of the next column + self.packed[page, packed_offset + 1] &= np.uint64( + (2 ** 31 - 1) << 3) + + self.packed[page, packed_offset + 1] ^= ( + (self.packed[page, packed_offset] & np.uint64(0b111 << 28)) + >> np.uint64(28) + ) + def mask_and_shift_data( self, data: IntOrArray, byte_offset: int) -> IntOrArray: - """Masks and shifts data into the 8 or 12-bit range.""" - return (data & self.BYTE_MASK32[byte_offset]) >> ( + """Masks and shifts data into the 13-bit range.""" + res = (data & self.BYTE_MASK34[byte_offset]) >> ( self.BYTE_SHIFTS[byte_offset]) + assert np.all(res <= 2 ** 13) + return res + + @functools.lru_cache(None) + def byte_pair_difference( + self, + byte_offset: int, + old_packed: np.uint64, + content: np.uint8 + ) -> int: + + old_pixels = self.mask_and_shift_data( + old_packed, byte_offset) + new_pixels = self.mask_and_shift_data( + self.masked_update_scalar( + byte_offset, old_packed, content), byte_offset) + + pair = (old_pixels << np.uint64(13)) + new_pixels + + return self.edit_distances(self.palette)[byte_offset][pair] + + def diff_weights( + self, + source: "DHGRBitmap", + is_aux: bool + ) -> np.ndarray: + return self._diff_weights(source.packed, is_aux) + + def _diff_weights( + self, + source_packed: np.ndarray, + is_aux: bool + ) -> np.ndarray: + """Computes diff from source_packed to self.packed""" + diff = np.ndarray((32, 256), dtype=np.int) + + if is_aux: + offsets = [0, 2] + else: + offsets = [1, 3] + + dists = [] + for o in offsets: + # Pixels influenced by byte offset o + source_pixels = self.mask_and_shift_data(source_packed, o) + target_pixels = self.mask_and_shift_data(self.packed, o) + + # Concatenate 13-bit source and target into 26-bit values + pair = (source_pixels << np.uint64(13)) + target_pixels + dist = self.edit_distances(self.palette)[o][pair].reshape( + pair.shape) + dists.append(dist) + + diff[:, 0::2] = dists[0] + diff[:, 1::2] = dists[1] + + return diff + + def compute_delta( + self, + content: int, + old: np.ndarray, + is_aux: bool + ) -> np.ndarray: + # TODO: use error edit distance + + # XXX reuse code + + diff = np.ndarray((32, 256), dtype=np.int) + + if is_aux: + # Pixels influenced by byte offset 0 + source_pixels0 = self.mask_and_shift_data( + self.masked_update_array(0, self.packed, content), 0) + target_pixels0 = self.mask_and_shift_data(self.packed, 0) + + # Concatenate 13-bit source and target into 26-bit values + pair0 = (source_pixels0 << np.uint64(13)) + target_pixels0 + dist0 = self.edit_distances(self.palette)[0][pair0].reshape( + pair0.shape) + + # Pixels influenced by byte offset 2 + source_pixels2 = self.mask_and_shift_data( + self.masked_update_array(2, self.packed, content), 2) + target_pixels2 = self.mask_and_shift_data(self.packed, 2) + # Concatenate 13-bit source and target into 26-bit values + pair2 = (source_pixels2 << np.uint64(13)) + target_pixels2 + dist2 = self.edit_distances(self.palette)[2][pair2].reshape( + pair2.shape) + + diff[:, 0::2] = dist0 + diff[:, 1::2] = dist2 + + else: + # Pixels influenced by byte offset 1 + source_pixels1 = self.mask_and_shift_data( + self.masked_update_array(1, self.packed, content), 1) + target_pixels1 = self.mask_and_shift_data(self.packed, 1) + pair1 = (source_pixels1 << np.uint64(13)) + target_pixels1 + dist1 = self.edit_distances(self.palette)[1][pair1].reshape( + pair1.shape) + + # Pixels influenced by byte offset 3 + source_pixels3 = self.mask_and_shift_data( + self.masked_update_array(3, self.packed, content), 3) + target_pixels3 = self.mask_and_shift_data(self.packed, 3) + pair3 = (source_pixels3 << np.uint64(13)) + target_pixels3 + dist3 = self.edit_distances(self.palette)[3][pair3].reshape( + pair3.shape) + + diff[:, 0::2] = dist1 + diff[:, 1::2] = dist3 + + # TODO: try different weightings + return (diff * 5) - old diff --git a/transcoder/screen_test.py b/transcoder/screen_test.py index 0bd0dae..20bdff7 100644 --- a/transcoder/screen_test.py +++ b/transcoder/screen_test.py @@ -4,6 +4,7 @@ import unittest import numpy as np +import colours import screen @@ -184,5 +185,411 @@ class TestDHGRBitmap(unittest.TestCase): ) +def binary(a): + return np.vectorize("{:032b}".format)(a) + + +class TestHGRBitmap(unittest.TestCase): + def setUp(self) -> None: + self.main = screen.MemoryMap(screen_page=1) + + def test_pixel_packing_p0_p0(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b01000011 + # PGGFFEED + self.main.page_offset[0, 1] = 0b01000011 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b1100000000111111000000001111 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def test_pixel_packing_p0_p1(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b01000011 + # PGGFFEED + self.main.page_offset[0, 1] = 0b11000011 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b1000000001111111000000001111 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def test_pixel_packing_p1_p0(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000011 + # PGGFFEED + self.main.page_offset[0, 1] = 0b01000011 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b1100000000111110000000011110 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def test_pixel_packing_p1_p1(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000011 + # PGGFFEED + self.main.page_offset[0, 1] = 0b11000011 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b1000000001111110000000011110 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def test_pixel_packing_p1_promote_p0(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b00000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b01000000 + + # PDCCBBAA + self.main.page_offset[0, 2] = 0b10000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b0000000000000000000000000001 + got = hgr.packed[0, 1] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def test_pixel_packing_p1_promote_p1(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b00000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b11000000 + + # PDCCBBAA + self.main.page_offset[0, 2] = 0b10000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b0000000000000000000000000001 + got = hgr.packed[0, 1] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + def testNominalColours(self): + # PDCCBBAA + self.main.page_offset[0, 0] = 0b01010101 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00101010 + # PDCCBBAA + self.main.page_offset[0, 2] = 0b01010101 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b000110011001100110011001100110011 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + self.assertEqual( + ( + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + colours.HGRColours.VIOLET, + ), + colours.int34_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + # See Figure 8.15 from Sather, "Understanding the Apple IIe" + + def testNominalColoursSather1(self): + # Extend violet into light blue + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b01000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b10000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.LIGHT_BLUE, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather2(self): + # Cut off blue with black to produce dark blue + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.DARK_BLUE, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather3(self): + # Cut off blue with green to produce aqua + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000001 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.AQUA, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather4(self): + # Cut off white with black to produce pink + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11100000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b0000000000000011100000000000 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + # TODO: BROWN(0001)/VIOLET(1100) should reframe to PINK (1011) + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BROWN, + colours.HGRColours.VIOLET, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather5(self): + # Extend green into light brown + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b01000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b10000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b0000000000000111000000000000 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + # TODO: LIGHT_BLUE should reframe to PINK (1011) + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.LIGHT_BLUE, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather6(self): + # Cut off orange with black to produce dark brown + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b00000000000000010000000000000 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + # TODO: DARK_BLUE should reframe to DARK_BROWN + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.DARK_BLUE, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather7(self): + # Cut off orange with violet to produce pink + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11000000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000001 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b00000000000001110000000000000 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + # TODO: AQUA should reframe to PINK + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.AQUA, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], + colours.HGRColours) + ) + + def testNominalColoursSather8(self): + # Cut off white with black to produce aqua + + # PDCCBBAA + self.main.page_offset[0, 0] = 0b11100000 + # PGGFFEED + self.main.page_offset[0, 1] = 0b00000000 + + hgr = screen.HGRBitmap( + main_memory=self.main) + + want = 0b00000000000000011100000000000 + got = hgr.packed[0, 0] + + self.assertEqual( + want, got, "\n%s\n%s" % (binary(want), binary(got)) + ) + + # TODO: BROWN/VIOLET should reframe to AQUA + self.assertEqual( + ( + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BROWN, + colours.HGRColours.VIOLET, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + colours.HGRColours.BLACK, + ), + colours.int28_to_nominal_colour_pixels( + hgr.packed[0, 0], colours.HGRColours) + ) + + if __name__ == '__main__': unittest.main() diff --git a/transcoder/video.py b/transcoder/video.py index 043abc1..80cf050 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -1,6 +1,5 @@ """Encode a sequence of images as an optimized stream of screen changes.""" -import functools import heapq import random from typing import List, Iterator, Tuple @@ -30,7 +29,7 @@ class Video: self.frame_grabber = frame_grabber # type: FrameGrabber self.ticks_per_second = ticks_per_second # type: float self.ticks_per_frame = ( - self.ticks_per_second / frame_grabber.input_frame_rate + self.ticks_per_second / frame_grabber.input_frame_rate ) # type: float self.frame_number = 0 # type: int self.palette = palette # type: Palette @@ -43,6 +42,7 @@ class Video: screen_page=1) # type: screen.MemoryMap self.pixelmap = screen.DHGRBitmap( + palette=palette, main_memory=self.memory_map, aux_memory=self.aux_memory_map ) @@ -71,6 +71,10 @@ class Video: memory_map = self.memory_map update_priority = self.update_priority + # Make sure nothing is leaking into screen holes + assert np.count_nonzero( + memory_map.page_offset[screen.SCREEN_HOLES]) == 0 + print("Similarity %f" % (update_priority.mean())) yield from self._index_changes( @@ -88,23 +92,28 @@ class Video: if is_aux: target_pixelmap = screen.DHGRBitmap( main_memory=self.memory_map, - aux_memory=target + aux_memory=target, + palette=self.palette ) else: target_pixelmap = screen.DHGRBitmap( main_memory=target, - aux_memory=self.aux_memory_map + aux_memory=self.aux_memory_map, + palette=self.palette ) - diff_weights = self._diff_weights( - self.pixelmap, target_pixelmap, is_aux - ) + diff_weights = target_pixelmap.diff_weights(self.pixelmap, is_aux) + + # Don't bother storing into screen holes + diff_weights[screen.SCREEN_HOLES] = 0 # Clear any update priority entries that have resolved themselves # with new frame update_priority[diff_weights == 0] = 0 update_priority += diff_weights + assert np.count_nonzero(update_priority[screen.SCREEN_HOLES]) == 0 + priorities = self._heapify_priorities(update_priority) content_deltas = {} @@ -112,6 +121,10 @@ class Video: while priorities: pri, _, page, offset = heapq.heappop(priorities) + assert not screen.SCREEN_HOLES[page, offset], ( + "Attempted to store into screen hole at (%d, %d)" % ( + page, offset)) + # Check whether we've already cleared this diff while processing # an earlier opcode if update_priority[page, offset] == 0: @@ -119,7 +132,9 @@ class Video: offsets = [offset] content = target.page_offset[page, offset] - assert content < 0x80 # DHGR palette bit not expected to be set + if self.mode == VideoMode.DHGR: + # DHGR palette bit not expected to be set + assert content < 0x80 # Clear priority for the offset we're emitting update_priority[page, offset] = 0 @@ -147,6 +162,10 @@ class Video: ): assert o != offset + assert not screen.SCREEN_HOLES[page, o], ( + "Attempted to store into screen hole at (%d, %d)" % ( + page, o)) + if update_priority[page, o] == 0: # print("Skipping page=%d, offset=%d" % (page, o)) continue @@ -161,8 +180,8 @@ class Video: byte_offset = target_pixelmap.interleaved_byte_offset(o, is_aux) old_packed = target_pixelmap.packed[page, o // 2] - p = self._byte_pair_difference( - target_pixelmap, byte_offset, old_packed, content) + p = target_pixelmap.byte_pair_difference( + byte_offset, old_packed, content) # Update priority for the offset we're emitting update_priority[page, o] = p # 0 @@ -211,7 +230,7 @@ class Video: priorities = [tuple(data) for data in np.stack(( -update_priority[pages, offsets], # Don't use deterministic order for page, offset - np.random.randint(0, 2**8, size=pages.shape[0]), + np.random.randint(0, 2 ** 8, size=pages.shape[0]), pages, offsets) ).T.tolist()] @@ -219,136 +238,6 @@ class Video: heapq.heapify(priorities) return priorities - def _diff_weights( - self, - source: screen.DHGRBitmap, - target: screen.DHGRBitmap, - is_aux: bool - ): - diff = np.ndarray((32, 256), dtype=np.int) - - if is_aux: - # Pixels influenced by byte offset 0 - source_pixels0 = source.mask_and_shift_data(source.packed, 0) - target_pixels0 = target.mask_and_shift_data(target.packed, 0) - - # Concatenate 8-bit source and target into 16-bit values - pair0 = (source_pixels0 << 8) + target_pixels0 - dist0 = source.edit_distances(self.palette)[0][pair0].reshape( - pair0.shape) - - # Pixels influenced by byte offset 2 - source_pixels2 = source.mask_and_shift_data(source.packed, 2) - target_pixels2 = target.mask_and_shift_data(target.packed, 2) - # Concatenate 12-bit source and target into 24-bit values - pair2 = (source_pixels2 << 12) + target_pixels2 - dist2 = source.edit_distances(self.palette)[2][pair2].reshape( - pair2.shape) - - diff[:, 0::2] = dist0 - diff[:, 1::2] = dist2 - - else: - # Pixels influenced by byte offset 1 - source_pixels1 = source.mask_and_shift_data(source.packed, 1) - target_pixels1 = target.mask_and_shift_data(target.packed, 1) - pair1 = (source_pixels1 << 12) + target_pixels1 - dist1 = source.edit_distances(self.palette)[1][pair1].reshape( - pair1.shape) - - # Pixels influenced by byte offset 3 - source_pixels3 = source.mask_and_shift_data(source.packed, 3) - target_pixels3 = target.mask_and_shift_data(target.packed, 3) - pair3 = (source_pixels3 << 8) + target_pixels3 - dist3 = source.edit_distances(self.palette)[3][pair3].reshape( - pair3.shape) - - diff[:, 0::2] = dist1 - diff[:, 1::2] = dist3 - - return diff - - @functools.lru_cache(None) - def _byte_pair_difference( - self, - target_pixelmap, - byte_offset, - old_packed, - content - ): - - old_pixels = target_pixelmap.mask_and_shift_data( - old_packed, byte_offset) - new_pixels = target_pixelmap.mask_and_shift_data( - target_pixelmap.masked_update( - byte_offset, old_packed, content), byte_offset) - - if byte_offset == 0 or byte_offset == 3: - pair = (old_pixels << 8) + new_pixels - else: - pair = (old_pixels << 12) + new_pixels - - p = target_pixelmap.edit_distances(self.palette)[byte_offset][pair] - - return p - - def _compute_delta( - self, - content: int, - target: screen.DHGRBitmap, - old, - is_aux: bool - ): - diff = np.ndarray((32, 256), dtype=np.int) - - # TODO: use error edit distance - - if is_aux: - # Pixels influenced by byte offset 0 - source_pixels0 = target.mask_and_shift_data( - target.masked_update(0, target.packed, content), 0) - target_pixels0 = target.mask_and_shift_data(target.packed, 0) - - # Concatenate 8-bit source and target into 16-bit values - pair0 = (source_pixels0 << 8) + target_pixels0 - dist0 = target.edit_distances(self.palette)[0][pair0].reshape( - pair0.shape) - - # Pixels influenced by byte offset 2 - source_pixels2 = target.mask_and_shift_data( - target.masked_update(2, target.packed, content), 2) - target_pixels2 = target.mask_and_shift_data(target.packed, 2) - # Concatenate 12-bit source and target into 24-bit values - pair2 = (source_pixels2 << 12) + target_pixels2 - dist2 = target.edit_distances(self.palette)[2][pair2].reshape( - pair2.shape) - - diff[:, 0::2] = dist0 - diff[:, 1::2] = dist2 - - else: - # Pixels influenced by byte offset 1 - source_pixels1 = target.mask_and_shift_data( - target.masked_update(1, target.packed, content), 1) - target_pixels1 = target.mask_and_shift_data(target.packed, 1) - pair1 = (source_pixels1 << 12) + target_pixels1 - dist1 = target.edit_distances(self.palette)[1][pair1].reshape( - pair1.shape) - - # Pixels influenced by byte offset 3 - source_pixels3 = target.mask_and_shift_data( - target.masked_update(3, target.packed, content), 3) - target_pixels3 = target.mask_and_shift_data(target.packed, 3) - pair3 = (source_pixels3 << 8) + target_pixels3 - dist3 = target.edit_distances(self.palette)[3][pair3].reshape( - pair3.shape) - - diff[:, 0::2] = dist1 - diff[:, 1::2] = dist3 - - # TODO: try different weightings - return (diff * 5) - old - _OFFSETS = np.arange(256) def _compute_error(self, page, content, target_pixelmap, old_error, @@ -356,8 +245,8 @@ class Video: # TODO: move this up into parent delta_screen = content_deltas.get(content) if delta_screen is None: - delta_screen = self._compute_delta( - content, target_pixelmap, old_error, is_aux) + delta_screen = target_pixelmap.compute_delta( + content, old_error, is_aux) content_deltas[content] = delta_screen delta_page = delta_screen[page] @@ -374,6 +263,6 @@ class Video: while deltas: pri, _, o = heapq.heappop(deltas) assert pri < 0 - assert o < 255 + assert o <= 255 yield -pri, o diff --git a/transcoder/video_test.py b/transcoder/video_test.py index a4d58d0..3055ef6 100644 --- a/transcoder/video_test.py +++ b/transcoder/video_test.py @@ -21,6 +21,7 @@ class TestVideo(unittest.TestCase): frame.page_offset[0, 1] = 0b1010101 target_pixelmap = screen.DHGRBitmap( + palette = palette.Palette.NTSC, main_memory=v.memory_map, aux_memory=frame ) @@ -28,10 +29,10 @@ class TestVideo(unittest.TestCase): 0b0000000101010100000001111111, target_pixelmap.packed[0, 0]) - diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True) - pal = palette.NTSCPalette + diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) + # Expect byte 0 to map to 0b00000000 01111111 expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0000000001111111] @@ -63,7 +64,7 @@ class TestVideo(unittest.TestCase): target_pixelmap.packed[0, 0] ) - diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True) + diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) # Expect byte 0 to map to 0b01111111 01101101 expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0111111101101101]