Checkpoint WIP for easier comparison to dhgr branch:

- naive version of NTSC artifacting, it uses a sliding 4-bit window to
  assign a nominal (D)HGR colour to each dot position.  A more
  sophisticated/correct implementation would model the YIQ signal
  directly.

- Switch DHGRBitmap implementation to use a 34-bit representation of
  the 4-byte tuple, comprised of a 3-bit header and footer, plus
  4*7=28-bit body.  The headers/footers account for the influence on
  neighbouring tuples from the 4-bit NTSC window.

- With this model each screen byte influences 13 pixels, so we need to
  precompute 2^26 edit distances for all possible (source, target)
  13-bit sequences.

- Checkpointing not-yet-working HGR implementation.

- Add new unit tests but not yet all passing due to refactoring
This commit is contained in:
kris 2019-07-02 22:40:50 +01:00
parent e2a8bd9b4d
commit 666272a8fc
9 changed files with 1268 additions and 367 deletions

View File

@ -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 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 # Value is memory bit order, which is opposite to screen order (bits
# ordered Left to Right on screen) # ordered Left to Right on screen)
BLACK = 0b0000 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 MAGENTA = 0b1000
BROWN = 0b0100 BROWN = 0b0100
ORANGE = 0b1100 ORANGE = 0b1100 # HGR colour
DARK_GREEN = 0b0010 DARK_GREEN = 0b0010
GREY1 = 0b1010 GREY1 = 0b1010
GREEN = 0b0110 GREEN = 0b0110 # HGR colour
YELLOW = 0b1110 YELLOW = 0b1110
DARK_BLUE = 0b0001 DARK_BLUE = 0b0001
VIOLET = 0b1001 VIOLET = 0b1001 # HGR colour
GREY2 = 0b0101 GREY2 = 0b0101
PINK = 0b1101 PINK = 0b1101
MED_BLUE = 0b0011 MED_BLUE = 0b0011 # HGR colour
LIGHT_BLUE = 0b1011 LIGHT_BLUE = 0b1011
AQUA = 0b0111 AQUA = 0b0111
WHITE = 0b1111 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
))

107
transcoder/colours_test.py Normal file
View File

@ -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()

View File

@ -1,6 +1,8 @@
import bz2 import bz2
import functools import functools
import pickle import pickle
import time
import datetime
from typing import Iterable, Type from typing import Iterable, Type
import colormath.color_conversions import colormath.color_conversions
@ -77,54 +79,8 @@ def pixel_char(i: int) -> str:
@functools.lru_cache(None) @functools.lru_cache(None)
def pixel_string(pixels: Iterable[colours.DHGRColours]) -> str: def pixel_string(pixels: Iterable[int]) -> str:
return "".join(pixel_char(p.value) for p in pixels) return "".join(pixel_char(p) 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
class EditDistanceParams: class EditDistanceParams:
@ -179,7 +135,6 @@ def make_substitute_costs(pal: Type[palette.BasePalette]):
return edp return edp
@functools.lru_cache(None)
def edit_distance( def edit_distance(
edp: EditDistanceParams, edp: EditDistanceParams,
a: str, a: str,
@ -199,66 +154,70 @@ def edit_distance(
def make_edit_distance(edp: EditDistanceParams): def make_edit_distance(edp: EditDistanceParams):
edit = [ edit = [
np.zeros(shape=(2 ** 16), dtype=np.int16), np.zeros(shape=(2 ** 26), dtype=np.uint16),
np.zeros(shape=(2 ** 24), dtype=np.int16), np.zeros(shape=(2 ** 26), dtype=np.uint16),
np.zeros(shape=(2 ** 24), dtype=np.int16), np.zeros(shape=(2 ** 26), dtype=np.uint16),
np.zeros(shape=(2 ** 16), dtype=np.int16), np.zeros(shape=(2 ** 26), dtype=np.uint16),
] ]
for i in range(2 ** 8): start_time = time.time()
print(i)
for j in range(2 ** 8):
pair = (i << 8) + j
first = map_int8_to_mask32_0(i) for i in range(2 ** 13):
second = map_int8_to_mask32_0(j) 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( # Each DHGR byte offset has the same range of int13 possible
pixel_string(int28_to_pixels(first)), 0) # values and nominal colour pixels, but with different initial
second_pixels = pixels_influenced_by_byte_index( # phases:
pixel_string(int28_to_pixels(second)), 0) # 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( edit[0][pair] = edit_distance(
edp, first_pixels, second_pixels, error=False) edp, first_pixels, second_pixels, error=False)
first = map_int8_to_mask32_3(i) first_pixels = pixel_string(
second = map_int8_to_mask32_3(j) colours.int34_to_nominal_colour_pixel_values(
i, colours.DHGRColours, init_phase=0)
first_pixels = pixels_influenced_by_byte_index( )
pixel_string(int28_to_pixels(first)), 3) second_pixels = pixel_string(
second_pixels = pixels_influenced_by_byte_index( colours.int34_to_nominal_colour_pixel_values(
pixel_string(int28_to_pixels(second)), 3) j, colours.DHGRColours, init_phase=0))
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)
edit[1][pair] = edit_distance( edit[1][pair] = edit_distance(
edp, first_pixels, second_pixels, error=False) edp, first_pixels, second_pixels, error=False)
first = map_int12_to_mask32_2(i) first_pixels = pixel_string(
second = map_int12_to_mask32_2(j) colours.int34_to_nominal_colour_pixel_values(
i, colours.DHGRColours, init_phase=3)
first_pixels = pixels_influenced_by_byte_index( )
pixel_string(int28_to_pixels(first)), 2) second_pixels = pixel_string(
second_pixels = pixels_influenced_by_byte_index( colours.int34_to_nominal_colour_pixel_values(
pixel_string(int28_to_pixels(second)), 2) j, colours.DHGRColours, init_phase=3))
edit[2][pair] = edit_distance( edit[2][pair] = edit_distance(
edp, first_pixels, second_pixels, error=False) 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 return edit
@ -269,7 +228,7 @@ def main():
edit = make_edit_distance(edp) edit = make_edit_distance(edp)
# TODO: error distance matrices # 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 ".bz2" % p.ID.value
with bz2.open(data, "wb", compresslevel=9) as out: with bz2.open(data, "wb", compresslevel=9) as out:
pickle.dump(edit, out, protocol=pickle.HIGHEST_PROTOCOL) pickle.dump(edit, out, protocol=pickle.HIGHEST_PROTOCOL)

View File

@ -1,12 +1,12 @@
import unittest import unittest
from colours import DHGRColours from colours import HGRColours
import make_data_tables import make_data_tables
class TestMakeDataTables(unittest.TestCase): class TestMakeDataTables(unittest.TestCase):
def test_pixel_string(self): 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)) self.assertEqual("0FC", make_data_tables.pixel_string(pixels))
def test_pixels_influenced_by_byte_index(self): 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) 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): def test_map_to_mask32(self):
byte_mask32 = [ byte_mask32 = [
# 33222222222211111111110000000000 <- bit pos in uint32 # 33222222222211111111110000000000 <- bit pos in uint32

View File

@ -3,7 +3,7 @@ from typing import Dict, Type
import colormath.color_objects import colormath.color_objects
from colours import DHGRColours from colours import HGRColours
# Type annotation # Type annotation
RGB = colormath.color_objects.sRGBColor RGB = colormath.color_objects.sRGBColor
@ -24,7 +24,7 @@ class BasePalette:
ID = Palette.UNKNOWN # type: Palette ID = Palette.UNKNOWN # type: Palette
# Palette RGB map # Palette RGB map
RGB = {} # type: Dict[DHGRColours: RGB] RGB = {} # type: Dict[HGRColours: RGB]
class NTSCPalette(BasePalette): class NTSCPalette(BasePalette):
@ -32,22 +32,22 @@ class NTSCPalette(BasePalette):
# Palette RGB values taken from BMP2DHGR's default NTSC palette # Palette RGB values taken from BMP2DHGR's default NTSC palette
RGB = { RGB = {
DHGRColours.BLACK: rgb(0, 0, 0), HGRColours.BLACK: rgb(0, 0, 0),
DHGRColours.MAGENTA: rgb(148, 12, 125), HGRColours.MAGENTA: rgb(148, 12, 125),
DHGRColours.BROWN: rgb(99, 77, 0), HGRColours.BROWN: rgb(99, 77, 0),
DHGRColours.ORANGE: rgb(249, 86, 29), HGRColours.ORANGE: rgb(249, 86, 29),
DHGRColours.DARK_GREEN: rgb(51, 111, 0), HGRColours.DARK_GREEN: rgb(51, 111, 0),
DHGRColours.GREY1: rgb(126, 126, 126), HGRColours.GREY1: rgb(126, 126, 126),
DHGRColours.GREEN: rgb(67, 200, 0), HGRColours.GREEN: rgb(67, 200, 0),
DHGRColours.YELLOW: rgb(221, 206, 23), HGRColours.YELLOW: rgb(221, 206, 23),
DHGRColours.DARK_BLUE: rgb(32, 54, 212), HGRColours.DARK_BLUE: rgb(32, 54, 212),
DHGRColours.VIOLET: rgb(188, 55, 255), HGRColours.VIOLET: rgb(188, 55, 255),
DHGRColours.GREY2: rgb(126, 126, 126), HGRColours.GREY2: rgb(126, 126, 126),
DHGRColours.PINK: rgb(255, 129, 236), HGRColours.PINK: rgb(255, 129, 236),
DHGRColours.MED_BLUE: rgb(7, 168, 225), HGRColours.MED_BLUE: rgb(7, 168, 225),
DHGRColours.LIGHT_BLUE: rgb(158, 172, 255), HGRColours.LIGHT_BLUE: rgb(158, 172, 255),
DHGRColours.AQUA: rgb(93, 248, 133), HGRColours.AQUA: rgb(93, 248, 133),
DHGRColours.WHITE: rgb(255, 255, 255) HGRColours.WHITE: rgb(255, 255, 255)
} }
@ -56,22 +56,22 @@ class IIGSPalette(BasePalette):
# Palette RGB values taken from BMP2DHGR's KEGS32 palette # Palette RGB values taken from BMP2DHGR's KEGS32 palette
RGB = { RGB = {
DHGRColours.BLACK: rgb(0, 0, 0), HGRColours.BLACK: rgb(0, 0, 0),
DHGRColours.MAGENTA: rgb(221, 0, 51), HGRColours.MAGENTA: rgb(221, 0, 51),
DHGRColours.BROWN: rgb(136, 85, 34), HGRColours.BROWN: rgb(136, 85, 34),
DHGRColours.ORANGE: rgb(255, 102, 0), HGRColours.ORANGE: rgb(255, 102, 0),
DHGRColours.DARK_GREEN: rgb(0, 119, 0), HGRColours.DARK_GREEN: rgb(0, 119, 0),
DHGRColours.GREY1: rgb(85, 85, 85), HGRColours.GREY1: rgb(85, 85, 85),
DHGRColours.GREEN: rgb(0, 221, 0), HGRColours.GREEN: rgb(0, 221, 0),
DHGRColours.YELLOW: rgb(255, 255, 0), HGRColours.YELLOW: rgb(255, 255, 0),
DHGRColours.DARK_BLUE: rgb(0, 0, 153), HGRColours.DARK_BLUE: rgb(0, 0, 153),
DHGRColours.VIOLET: rgb(221, 0, 221), HGRColours.VIOLET: rgb(221, 0, 221),
DHGRColours.GREY2: rgb(170, 170, 170), HGRColours.GREY2: rgb(170, 170, 170),
DHGRColours.PINK: rgb(255, 153, 136), HGRColours.PINK: rgb(255, 153, 136),
DHGRColours.MED_BLUE: rgb(34, 34, 255), HGRColours.MED_BLUE: rgb(34, 34, 255),
DHGRColours.LIGHT_BLUE: rgb(102, 170, 255), HGRColours.LIGHT_BLUE: rgb(102, 170, 255),
DHGRColours.AQUA: rgb(0, 255, 153), HGRColours.AQUA: rgb(0, 255, 153),
DHGRColours.WHITE: rgb(255, 255, 255) HGRColours.WHITE: rgb(255, 255, 255)
} }

View File

@ -3,10 +3,11 @@
import bz2 import bz2
import functools import functools
import pickle import pickle
from typing import Union, List from typing import Union, List, Optional
import numpy as np 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. # Type annotation for cases where we may process either an int or a numpy array.
IntOrArray = Union[int, np.ndarray] IntOrArray = Union[int, np.ndarray]
@ -124,55 +125,334 @@ class MemoryMap:
self.page_offset[page - self._page_start][offset] = val self.page_offset[page - self._page_start][offset] = val
class DHGRBitmap: @functools.lru_cache(None)
BYTE_MASK32 = [ def _edit_distances(name: str, palette_id: pal.Palette) -> List[np.ndarray]:
# 3333333222222211111110000000 <- byte 0.3 """Load edit distance matrices for masked, shifted byte values.
#
# 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 This is defined at module level to be a singleton.
BYTE_SHIFTS = [0, 4, 12, 20] """
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): class Bitmap:
self.main_memory = main_memory """Packed 28-bit bitmap representation of (D)HGR screen memory.
self.aux_memory = aux_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() self._pack()
def _pack(self) -> None: 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 # Palette bit is unused for DHGR so mask it out
aux = (self.aux_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.uint32) main = (self.main_memory.page_offset & 0x7f).astype(np.uint64)
# Interleave aux and main memory columns and pack 7-bit masked values # 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. # See make_data_tables.py for more discussion about this representation.
self.packed = ( packed = (
aux[:, 0::2] + (aux[:, 0::2] << 3) +
(main[:, 0::2] << 7) + (main[:, 0::2] << 10) +
(aux[:, 1::2] << 14) + (aux[:, 1::2] << 17) +
(main[:, 1::2] << 21) (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 @staticmethod
@ -190,31 +470,207 @@ class DHGRBitmap:
else: else:
return 1 return 1
# XXX test
@staticmethod @staticmethod
def masked_update( def masked_update_scalar(
byte_offset: int, byte_offset: int,
old_value: IntOrArray, old_value: np.uint64,
new_value: int) -> IntOrArray: new_value: np.uint8) -> np.uint64:
# Mask out 7-bit value where update will go # 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.""" """Update packed representation of changing main/aux memory."""
byte_offset = self.interleaved_byte_offset(offset, is_aux) byte_offset = self.interleaved_byte_offset(offset, is_aux)
packed_offset = offset // 2 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) 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( def mask_and_shift_data(
self, self,
data: IntOrArray, data: IntOrArray,
byte_offset: int) -> IntOrArray: byte_offset: int) -> IntOrArray:
"""Masks and shifts data into the 8 or 12-bit range.""" """Masks and shifts data into the 13-bit range."""
return (data & self.BYTE_MASK32[byte_offset]) >> ( res = (data & self.BYTE_MASK34[byte_offset]) >> (
self.BYTE_SHIFTS[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

View File

@ -4,6 +4,7 @@ import unittest
import numpy as np import numpy as np
import colours
import screen 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,6 +1,5 @@
"""Encode a sequence of images as an optimized stream of screen changes.""" """Encode a sequence of images as an optimized stream of screen changes."""
import functools
import heapq import heapq
import random import random
from typing import List, Iterator, Tuple from typing import List, Iterator, Tuple
@ -30,7 +29,7 @@ class Video:
self.frame_grabber = frame_grabber # type: FrameGrabber self.frame_grabber = frame_grabber # type: FrameGrabber
self.ticks_per_second = ticks_per_second # type: float self.ticks_per_second = ticks_per_second # type: float
self.ticks_per_frame = ( self.ticks_per_frame = (
self.ticks_per_second / frame_grabber.input_frame_rate self.ticks_per_second / frame_grabber.input_frame_rate
) # type: float ) # type: float
self.frame_number = 0 # type: int self.frame_number = 0 # type: int
self.palette = palette # type: Palette self.palette = palette # type: Palette
@ -43,6 +42,7 @@ class Video:
screen_page=1) # type: screen.MemoryMap screen_page=1) # type: screen.MemoryMap
self.pixelmap = screen.DHGRBitmap( self.pixelmap = screen.DHGRBitmap(
palette=palette,
main_memory=self.memory_map, main_memory=self.memory_map,
aux_memory=self.aux_memory_map aux_memory=self.aux_memory_map
) )
@ -71,6 +71,10 @@ class Video:
memory_map = self.memory_map memory_map = self.memory_map
update_priority = self.update_priority 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())) print("Similarity %f" % (update_priority.mean()))
yield from self._index_changes( yield from self._index_changes(
@ -88,23 +92,28 @@ class Video:
if is_aux: if is_aux:
target_pixelmap = screen.DHGRBitmap( target_pixelmap = screen.DHGRBitmap(
main_memory=self.memory_map, main_memory=self.memory_map,
aux_memory=target aux_memory=target,
palette=self.palette
) )
else: else:
target_pixelmap = screen.DHGRBitmap( target_pixelmap = screen.DHGRBitmap(
main_memory=target, main_memory=target,
aux_memory=self.aux_memory_map aux_memory=self.aux_memory_map,
palette=self.palette
) )
diff_weights = self._diff_weights( diff_weights = target_pixelmap.diff_weights(self.pixelmap, is_aux)
self.pixelmap, target_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 # Clear any update priority entries that have resolved themselves
# with new frame # with new frame
update_priority[diff_weights == 0] = 0 update_priority[diff_weights == 0] = 0
update_priority += diff_weights update_priority += diff_weights
assert np.count_nonzero(update_priority[screen.SCREEN_HOLES]) == 0
priorities = self._heapify_priorities(update_priority) priorities = self._heapify_priorities(update_priority)
content_deltas = {} content_deltas = {}
@ -112,6 +121,10 @@ class Video:
while priorities: while priorities:
pri, _, page, offset = heapq.heappop(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 # Check whether we've already cleared this diff while processing
# an earlier opcode # an earlier opcode
if update_priority[page, offset] == 0: if update_priority[page, offset] == 0:
@ -119,7 +132,9 @@ class Video:
offsets = [offset] offsets = [offset]
content = target.page_offset[page, 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 # Clear priority for the offset we're emitting
update_priority[page, offset] = 0 update_priority[page, offset] = 0
@ -147,6 +162,10 @@ class Video:
): ):
assert o != offset 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: if update_priority[page, o] == 0:
# print("Skipping page=%d, offset=%d" % (page, o)) # print("Skipping page=%d, offset=%d" % (page, o))
continue continue
@ -161,8 +180,8 @@ class Video:
byte_offset = target_pixelmap.interleaved_byte_offset(o, is_aux) byte_offset = target_pixelmap.interleaved_byte_offset(o, is_aux)
old_packed = target_pixelmap.packed[page, o // 2] old_packed = target_pixelmap.packed[page, o // 2]
p = self._byte_pair_difference( p = target_pixelmap.byte_pair_difference(
target_pixelmap, byte_offset, old_packed, content) byte_offset, old_packed, content)
# Update priority for the offset we're emitting # Update priority for the offset we're emitting
update_priority[page, o] = p # 0 update_priority[page, o] = p # 0
@ -211,7 +230,7 @@ class Video:
priorities = [tuple(data) for data in np.stack(( priorities = [tuple(data) for data in np.stack((
-update_priority[pages, offsets], -update_priority[pages, offsets],
# Don't use deterministic order for page, offset # 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, pages,
offsets) offsets)
).T.tolist()] ).T.tolist()]
@ -219,136 +238,6 @@ class Video:
heapq.heapify(priorities) heapq.heapify(priorities)
return 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) _OFFSETS = np.arange(256)
def _compute_error(self, page, content, target_pixelmap, old_error, def _compute_error(self, page, content, target_pixelmap, old_error,
@ -356,8 +245,8 @@ class Video:
# TODO: move this up into parent # TODO: move this up into parent
delta_screen = content_deltas.get(content) delta_screen = content_deltas.get(content)
if delta_screen is None: if delta_screen is None:
delta_screen = self._compute_delta( delta_screen = target_pixelmap.compute_delta(
content, target_pixelmap, old_error, is_aux) content, old_error, is_aux)
content_deltas[content] = delta_screen content_deltas[content] = delta_screen
delta_page = delta_screen[page] delta_page = delta_screen[page]
@ -374,6 +263,6 @@ class Video:
while deltas: while deltas:
pri, _, o = heapq.heappop(deltas) pri, _, o = heapq.heappop(deltas)
assert pri < 0 assert pri < 0
assert o < 255 assert o <= 255
yield -pri, o yield -pri, o

View File

@ -21,6 +21,7 @@ class TestVideo(unittest.TestCase):
frame.page_offset[0, 1] = 0b1010101 frame.page_offset[0, 1] = 0b1010101
target_pixelmap = screen.DHGRBitmap( target_pixelmap = screen.DHGRBitmap(
palette = palette.Palette.NTSC,
main_memory=v.memory_map, main_memory=v.memory_map,
aux_memory=frame aux_memory=frame
) )
@ -28,10 +29,10 @@ class TestVideo(unittest.TestCase):
0b0000000101010100000001111111, 0b0000000101010100000001111111,
target_pixelmap.packed[0, 0]) target_pixelmap.packed[0, 0])
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
pal = palette.NTSCPalette pal = palette.NTSCPalette
diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True)
# Expect byte 0 to map to 0b00000000 01111111 # Expect byte 0 to map to 0b00000000 01111111
expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0000000001111111] expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0000000001111111]
@ -63,7 +64,7 @@ class TestVideo(unittest.TestCase):
target_pixelmap.packed[0, 0] 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 # Expect byte 0 to map to 0b01111111 01101101
expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0111111101101101] expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0111111101101101]