mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-21 05:30:20 +00:00
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:
parent
e2a8bd9b4d
commit
666272a8fc
@ -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
|
||||
))
|
||||
|
||||
|
107
transcoder/colours_test.py
Normal file
107
transcoder/colours_test.py
Normal 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()
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user