mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-21 20:29:21 +00:00
WIP - optimal encoding by computing all possible byte stores at all
possible (page, offset) and then choosing top 4 offsets for all content bytes. i.e. this should do better than the previous "greedy" approach in which we picked a single high-priority offset to resolve, and then tried to find 3 more than didn't introduce too much additional error. Major outstanding issue is that we need to also evaluate whether it's worth storing 1, 2 or 3 offsets as well, since sometimes this will have lower total error.
This commit is contained in:
parent
142dbe02fe
commit
1cd20a7615
@ -210,14 +210,14 @@ class Bitmap:
|
||||
body = self._body()
|
||||
|
||||
# Prepend last 3 bits of previous odd byte so we can correctly
|
||||
# decode the effective colours at the beginning of the 22-bit tuple
|
||||
# decode the effective colours at the beginning of the body tuple
|
||||
prev_col = np.roll(body, 1, axis=1).astype(np.uint64)
|
||||
header = self._make_header(prev_col)
|
||||
# Don't leak header across page boundaries
|
||||
header[:, 0] = 0
|
||||
|
||||
# Append first 3 bits of next even byte so we can correctly
|
||||
# decode the effective colours at the end of the 22-bit tuple
|
||||
# decode the effective colours at the end of the body tuple
|
||||
next_col = np.roll(body, -1, axis=1).astype(np.uint64)
|
||||
footer = self._make_footer(next_col)
|
||||
# Don't leak footer across page boundaries
|
||||
@ -314,26 +314,26 @@ class Bitmap:
|
||||
|
||||
return column_right
|
||||
|
||||
def _fix_array_neighbours(
|
||||
self,
|
||||
ary: np.ndarray,
|
||||
byte_offset: int
|
||||
) -> None:
|
||||
"""Fix up column headers/footers for all array entries."""
|
||||
|
||||
# TODO: don't leak header/footer across page boundaries
|
||||
|
||||
# Propagate new value into neighbouring byte headers/footers if
|
||||
# necessary
|
||||
if byte_offset == 0:
|
||||
# Need to also update the footer of the preceding column
|
||||
shifted_left = np.roll(ary, -1, axis=1)
|
||||
self._fix_column_left(ary, shifted_left)
|
||||
|
||||
elif byte_offset == (self.SCREEN_BYTES - 1):
|
||||
# Need to also update the header of the next column
|
||||
shifted_right = np.roll(ary, 1, axis=1)
|
||||
self._fix_column_right(ary, shifted_right)
|
||||
# def _fix_array_neighbours(
|
||||
# self,
|
||||
# ary: np.ndarray,
|
||||
# byte_offset: int
|
||||
# ) -> None:
|
||||
# """Fix up column headers/footers for all array entries."""
|
||||
#
|
||||
# # TODO: don't leak header/footer across page boundaries
|
||||
#
|
||||
# # Propagate new value into neighbouring byte headers/footers if
|
||||
# # necessary
|
||||
# if byte_offset == 0:
|
||||
# # Need to also update the footer of the preceding column
|
||||
# shifted_left = np.roll(ary, -1, axis=1)
|
||||
# self._fix_column_left(ary, shifted_left)
|
||||
#
|
||||
# elif byte_offset == (self.SCREEN_BYTES - 1):
|
||||
# # Need to also update the header of the next column
|
||||
# shifted_right = np.roll(ary, 1, axis=1)
|
||||
# self._fix_column_right(ary, shifted_right)
|
||||
|
||||
@classmethod
|
||||
@functools.lru_cache(None)
|
||||
@ -400,37 +400,14 @@ class Bitmap:
|
||||
is_aux: bool
|
||||
) -> np.ndarray:
|
||||
"""Compute edit distance matrix from source bitmap."""
|
||||
return self._diff_weights(source.packed, is_aux)
|
||||
|
||||
# TODO: unit test
|
||||
def _diff_weights(
|
||||
self,
|
||||
source_packed: np.ndarray,
|
||||
is_aux: bool,
|
||||
content: np.uint8 = None
|
||||
) -> np.ndarray:
|
||||
"""Computes edit distance matrix from source_packed to self.packed
|
||||
|
||||
If content is set, the distance will be computed as if this value
|
||||
was stored into each offset position of source_packed, i.e. to
|
||||
allow evaluating which offsets (if any) should be chosen for storing
|
||||
this content byte.
|
||||
"""
|
||||
|
||||
diff = np.ndarray((32, 256), dtype=np.int)
|
||||
|
||||
offsets = self._byte_offsets(is_aux)
|
||||
|
||||
dists = []
|
||||
for o in offsets:
|
||||
if content is not None:
|
||||
compare_packed = self.masked_update(o, source_packed, content)
|
||||
self._fix_array_neighbours(compare_packed, o)
|
||||
else:
|
||||
compare_packed = source_packed
|
||||
|
||||
# Pixels influenced by byte offset o
|
||||
source_pixels = self.mask_and_shift_data(compare_packed, o)
|
||||
source_pixels = self.mask_and_shift_data(source.packed, o)
|
||||
target_pixels = self.mask_and_shift_data(self.packed, o)
|
||||
|
||||
# Concatenate N-bit source and target into 2N-bit values
|
||||
@ -466,34 +443,57 @@ class Bitmap:
|
||||
continue
|
||||
ok = False
|
||||
print(p, o, bin(self.packed[p, o - 1]),
|
||||
bin(headers[p, o]),
|
||||
bin(headers[p, o]),
|
||||
bin(self.packed[p, o]),
|
||||
bin(self.packed[p, o + 1]), bin(footers[p, o]),
|
||||
bin(res[p, o])
|
||||
)
|
||||
assert ok
|
||||
|
||||
CONTENT_RANGE = None
|
||||
|
||||
# TODO: unit tests
|
||||
def compute_delta(
|
||||
self,
|
||||
content: int,
|
||||
diff_weights: np.ndarray,
|
||||
is_aux: bool
|
||||
) -> np.ndarray:
|
||||
def compute_delta(self, is_aux: bool) -> np.ndarray:
|
||||
"""Compute which content stores introduce the least additional error.
|
||||
|
||||
We compute the effect of storing content at all possible offsets
|
||||
within self.packed, and then subtract the previous diff weights.
|
||||
|
||||
Negative values indicate that the new content value is closer to the
|
||||
target than the current content.
|
||||
within self.packed, in terms of the new edit_distance to the target
|
||||
pixels.
|
||||
"""
|
||||
# TODO: use error edit distance?
|
||||
# Only need to consider 0x0 .. 0x7f content stores
|
||||
diff = np.ndarray((self.CONTENT_RANGE, 32, 256), dtype=np.int)
|
||||
|
||||
new_diff = self._diff_weights(self.packed, is_aux, content)
|
||||
all_content_bytes = np.arange(
|
||||
self.CONTENT_RANGE, dtype=np.uint64).reshape(
|
||||
(self.CONTENT_RANGE, 1))
|
||||
|
||||
# TODO: try different weightings
|
||||
return (new_diff * 5) - diff_weights
|
||||
def _target_masked(content, t, byte_offset):
|
||||
return self.masked_update(byte_offset, t, content)
|
||||
|
||||
offsets = self._byte_offsets(is_aux)
|
||||
|
||||
dists = []
|
||||
for o in offsets:
|
||||
compare_packed = np.apply_along_axis(
|
||||
_target_masked, 1, all_content_bytes, self.packed, o)
|
||||
# self.masked_update(o, self.packed, content)
|
||||
# self._fix_array_neighbours(compare_packed, o)
|
||||
|
||||
# Pixels influenced by byte offset 0
|
||||
source_pixels = self.mask_and_shift_data(compare_packed, o)
|
||||
target_pixels = self.mask_and_shift_data(self.packed, o)
|
||||
|
||||
# Concatenate N-bit source and target into 2N-bit values
|
||||
pair = (source_pixels << self.MASKED_BITS) + target_pixels
|
||||
dist = self.edit_distances(self.palette)[o][pair].reshape(
|
||||
pair.shape)
|
||||
dists.append(dist)
|
||||
|
||||
# Interleave even/odd columns
|
||||
diff[:, :, 0::2] = dists[0]
|
||||
diff[:, :, 1::2] = dists[1]
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
class HGRBitmap(Bitmap):
|
||||
@ -572,7 +572,7 @@ class HGRBitmap(Bitmap):
|
||||
#
|
||||
# From header: 3 bits (2 HGR pixels but might be shifted right by palette)
|
||||
# From body: 7 bits doubled, plus possible shift from palette bit
|
||||
MASKED_DOTS = np.uint64(18) # 3 + 7 + 7
|
||||
MASKED_DOTS = np.uint64(18) # 3 + 7 + 7 + 1
|
||||
|
||||
# List of bitmasks for extracting the subset of packed data corresponding
|
||||
# to bits influencing/influenced by a given byte offset. These must be
|
||||
@ -593,6 +593,9 @@ class HGRBitmap(Bitmap):
|
||||
# odd: 2 (3)
|
||||
PHASES = [1, 3]
|
||||
|
||||
# Need to consider all 0x0 .. 0xff content stores
|
||||
CONTENT_RANGE = 256
|
||||
|
||||
def __init__(self, palette: pal.Palette, main_memory: MemoryMap):
|
||||
super(HGRBitmap, self).__init__(palette, main_memory, None)
|
||||
|
||||
@ -850,7 +853,7 @@ class DHGRBitmap(Bitmap):
|
||||
np.uint64(0b0000000000000011111111111110000000), # byte 1 uint13 mask
|
||||
np.uint64(0b0000000111111111111100000000000000), # byte 2 uint13 mask
|
||||
np.uint64(0b1111111111111000000000000000000000), # byte 3 uint13 mask
|
||||
]
|
||||
] # XXX XXX
|
||||
|
||||
# How much to right-shift bits after masking, to bring into uint13 range
|
||||
BYTE_SHIFTS = [np.uint64(0), np.uint64(7), np.uint64(14), np.uint64(21)]
|
||||
@ -866,6 +869,9 @@ class DHGRBitmap(Bitmap):
|
||||
# MAIN 1: 1 (2)
|
||||
PHASES = [1, 0, 3, 2]
|
||||
|
||||
# Only need to consider 0x0 .. 0x7f content stores
|
||||
CONTENT_RANGE = 128
|
||||
|
||||
@staticmethod
|
||||
def _make_header(col: IntOrArray) -> IntOrArray:
|
||||
"""Extract upper 3 bits of body for header of next column."""
|
||||
|
@ -39,17 +39,16 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
def test_pixel_packing_offset_0(self):
|
||||
"""Screen byte packing happens correctly at offset 0."""
|
||||
|
||||
# PBBBAAAA
|
||||
self.aux.page_offset[0, 0] = 0b11110101
|
||||
# PDDCCCCB
|
||||
self.main.page_offset[0, 0] = 0b01000011
|
||||
# PFEEEEDD
|
||||
self.aux.page_offset[0, 1] = 0b11110101
|
||||
# PGGGGFFF
|
||||
self.main.page_offset[0, 1] = 0b01000011
|
||||
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC)
|
||||
# PBBBAAAA
|
||||
dhgr.apply(0, 0, True, np.uint8(0b11110101))
|
||||
# PDDCCCCB
|
||||
dhgr.apply(0, 0, False, np.uint8(0b01000011))
|
||||
# PFEEEEDD
|
||||
dhgr.apply(0, 1, True, np.uint8(0b11110101))
|
||||
# PGGGGFFF
|
||||
dhgr.apply(0, 1, False, np.uint8(0b01000011))
|
||||
|
||||
self.assertEqual(
|
||||
0b0001000011111010110000111110101000,
|
||||
@ -69,17 +68,25 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
def test_pixel_packing_offset_1(self):
|
||||
"""Screen byte packing happens correctly at offset 1."""
|
||||
|
||||
# PBBBAAAA
|
||||
self.aux.page_offset[0, 2] = 0b11110101
|
||||
# PDDCCCCB
|
||||
self.main.page_offset[0, 2] = 0b01000011
|
||||
# PFEEEEDD
|
||||
self.aux.page_offset[0, 3] = 0b11110101
|
||||
# PGGGGFFF
|
||||
self.main.page_offset[0, 3] = 0b01000011
|
||||
# # PBBBAAAA
|
||||
# self.aux.page_offset[0, 2] = 0b11110101
|
||||
# # PDDCCCCB
|
||||
# self.main.page_offset[0, 2] = 0b01000011
|
||||
# # PFEEEEDD
|
||||
# self.aux.page_offset[0, 3] = 0b11110101
|
||||
# # PGGGGFFF
|
||||
# self.main.page_offset[0, 3] = 0b01000011
|
||||
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC)
|
||||
# PBBBAAAA
|
||||
dhgr.apply(0, 2, True, np.uint8(0b11110101))
|
||||
# PDDCCCCB
|
||||
dhgr.apply(0, 2, False, np.uint8(0b01000011))
|
||||
# PFEEEEDD
|
||||
dhgr.apply(0, 3, True, np.uint8(0b11110101))
|
||||
# PGGGGFFF
|
||||
dhgr.apply(0, 3, False, np.uint8(0b01000011))
|
||||
|
||||
self.assertEqual(
|
||||
0b0001000011111010110000111110101000,
|
||||
@ -104,17 +111,17 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
def test_pixel_packing_offset_127(self):
|
||||
"""Screen byte packing happens correctly at offset 127."""
|
||||
|
||||
# PBBBAAAA
|
||||
self.aux.page_offset[0, 254] = 0b11110101
|
||||
# PDDCCCCB
|
||||
self.main.page_offset[0, 254] = 0b01000011
|
||||
# PFEEEEDD
|
||||
self.aux.page_offset[0, 255] = 0b11110101
|
||||
# PGGGGFFF
|
||||
self.main.page_offset[0, 255] = 0b01000011
|
||||
|
||||
dhgr = screen.DHGRBitmap(
|
||||
main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC)
|
||||
# PBBBAAAA
|
||||
dhgr.apply(0, 254, True, np.uint8(0b11110101))
|
||||
# PDDCCCCB
|
||||
dhgr.apply(0, 254, False, np.uint8(0b01000011))
|
||||
# PFEEEEDD
|
||||
dhgr.apply(0, 255, True, np.uint8(0b11110101))
|
||||
# PGGGGFFF
|
||||
dhgr.apply(0, 255, False, np.uint8(0b01000011))
|
||||
|
||||
self.assertEqual(
|
||||
0b0001000011111010110000111110101000,
|
||||
@ -277,6 +284,7 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
0b1110000000000000000000000000000000,
|
||||
dhgr.packed[12, 17])
|
||||
|
||||
# Update offset 2
|
||||
dhgr.apply(page=12, offset=36, is_aux=False, value=np.uint8(0b0001101))
|
||||
self.assertEqual(
|
||||
0b101,
|
||||
@ -315,6 +323,17 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
0b1011010101000000000000000000000000,
|
||||
dhgr.packed[12, 17])
|
||||
|
||||
# Now propagate new footer from neighbour onto (12, 18)
|
||||
dhgr.apply(page=12, offset=38, is_aux=True, value=np.uint8(0b1111001))
|
||||
self.assertEqual(
|
||||
0b0011010101111111100011010001101101,
|
||||
dhgr.packed[12, 18]
|
||||
)
|
||||
# Neighbouring header
|
||||
self.assertEqual(
|
||||
0b0000000000000000000000001111001101,
|
||||
dhgr.packed[12, 19])
|
||||
|
||||
def test_fix_array_neighbours(self):
|
||||
"""Test that _fix_array_neighbours DTRT after masked_update."""
|
||||
|
||||
@ -351,6 +370,15 @@ class TestDHGRBitmap(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
packed = dhgr.masked_update(0, dhgr.packed, np.uint8(0b101))
|
||||
dhgr._fix_array_neighbours(packed, 0)
|
||||
|
||||
# Should propagate to all footers
|
||||
self.assertEqual(
|
||||
0, np.count_nonzero(
|
||||
packed[packed != 0b1010000000000000000000000000101000]
|
||||
)
|
||||
)
|
||||
|
||||
class TestHGRBitmap(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
|
@ -16,8 +16,6 @@ from video_mode import VideoMode
|
||||
class Video:
|
||||
"""Encodes sequence of images into prioritized screen byte changes."""
|
||||
|
||||
CLOCK_SPEED = 1024 * 1024 # type: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame_grabber: FrameGrabber,
|
||||
@ -80,8 +78,9 @@ class Video:
|
||||
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
|
||||
# XXX why is this happening? Maybe because we're not scoring <4 stores
|
||||
if np.count_nonzero(memory_map.page_offset[screen.SCREEN_HOLES]):
|
||||
print("Someone stored in screen holes")
|
||||
|
||||
print("Similarity %f" % (update_priority.mean()))
|
||||
|
||||
@ -117,181 +116,424 @@ class Video:
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
priorities = self._heapify_priorities(update_priority)
|
||||
# priorities = self._heapify_priorities(update_priority)
|
||||
|
||||
content_deltas = {}
|
||||
content_deltas = 5 * target_pixelmap.compute_delta(is_aux)
|
||||
# print(content_deltas[:, 0, 0])
|
||||
|
||||
while priorities:
|
||||
pri, _, page, offset = heapq.heappop(priorities)
|
||||
# Only want to consider deltas that are < 0
|
||||
# content_deltas[content_deltas >= update_priority] = 0
|
||||
|
||||
assert not screen.SCREEN_HOLES[page, offset], (
|
||||
"Attempted to store into screen hole at (%d, %d)" % (
|
||||
page, offset))
|
||||
edit_distance = content_deltas - update_priority
|
||||
|
||||
# Check whether we've already cleared this diff while processing
|
||||
# an earlier opcode
|
||||
if update_priority[page, offset] == 0:
|
||||
# print(edit_distance[:, 0, 0])
|
||||
candidates = np.sum(edit_distance < 0)
|
||||
print("Candidates = %d" % candidates)
|
||||
|
||||
# We care about finding the 4 smallest elements for each (
|
||||
# content, page), but not their order.
|
||||
smallest_idx = np.argpartition(edit_distance, 3, axis=2)[:, :, :4]
|
||||
# smallest = # np.sort(
|
||||
smallest = np.take_along_axis(edit_distance, smallest_idx,
|
||||
axis=2) #, axis=2)
|
||||
while True:
|
||||
# Score should be sum of first 4 non-zero elements
|
||||
|
||||
# score = np.apply_along_axis(_score, 2, smallest)
|
||||
|
||||
# XXX turn into vector
|
||||
# scores = [
|
||||
# smallest[:, :, 0],
|
||||
# np.sum(smallest[:, :, :2], axis=2),
|
||||
# np.sum(smallest[:, :, :3], axis=2),
|
||||
# np.sum(smallest, axis=2)
|
||||
# ]
|
||||
|
||||
score = np.sum(smallest, axis=2)
|
||||
|
||||
idx = np.argmin(score, axis=None)
|
||||
#print([s.shape for s in scores])
|
||||
|
||||
# print(score[:, 0])
|
||||
# print(score.shape)
|
||||
# idx = np.array((
|
||||
# np.argmin(scores[0], axis=None),
|
||||
# np.argmin(scores[1], axis=None),
|
||||
# np.argmin(scores[2], axis=None),
|
||||
# np.argmin(scores[3], axis=None)
|
||||
# ))
|
||||
# contents, pages = np.unravel_index(idx, scores[0].shape)
|
||||
# #print(contents, pages)
|
||||
# best_scores = np.array([scores[i][contents[i], pages[i]] for i in
|
||||
# range(4)])
|
||||
# idx_argmin = np.argmin(best_scores)
|
||||
# #print(best_scores)
|
||||
# #print(idx_argmin)
|
||||
# num_offsets = idx_argmin + 1
|
||||
#
|
||||
# sc = best_scores[idx_argmin]
|
||||
# # print(sc)
|
||||
# content, page = contents[idx_argmin], pages[idx_argmin]
|
||||
#print(score.shape)
|
||||
# print("Taking %d args" % num_offsets)
|
||||
# TODO: also consider what happens if we only store 1, 2 or 3
|
||||
# offsets e.g. might only be a single pixel to fix up, as in
|
||||
# AppleVision video.
|
||||
|
||||
content, page = np.unravel_index(idx, score.shape)
|
||||
#print(content, page)
|
||||
sc = score[content, page]
|
||||
# print([s[content, page] for s in scores])
|
||||
# print(sc, content, page)
|
||||
if sc == 0:
|
||||
break
|
||||
assert sc < 0
|
||||
|
||||
# May not have 4 valid offsets so have to recompute explicitly
|
||||
# i.e. can't just use smallest_idx[content, page]
|
||||
nonzero_offsets = smallest[content, page] < 0
|
||||
offsets = smallest_idx[
|
||||
content, page, nonzero_offsets].tolist() # [:num_offsets]
|
||||
|
||||
# print(sc, content, page, offsets)
|
||||
|
||||
# TODO: uncomment once we are saving residual diff
|
||||
if any(diff_weights[page, o] == 0 for o in offsets):
|
||||
print("someone else got here first")
|
||||
continue
|
||||
|
||||
offsets = [offset]
|
||||
content = target.page_offset[page, offset]
|
||||
if self.mode == VideoMode.DHGR:
|
||||
# DHGR palette bit not expected to be set
|
||||
assert content < 0x80
|
||||
for o in offsets:
|
||||
# TODO: uncomment once we are saving residual diff
|
||||
assert edit_distance[content, page, o]
|
||||
|
||||
# Clear priority for the offset we're emitting
|
||||
update_priority[page, offset] = 0
|
||||
diff_weights[page, offset] = 0
|
||||
# TODO: move these all outside the loop & vectorize
|
||||
|
||||
# Update memory maps
|
||||
source.page_offset[page, offset] = content
|
||||
self.pixelmap.apply(page, offset, is_aux, content)
|
||||
# TODO: temporal error diffusion - push residual error into
|
||||
# next frame
|
||||
update_priority[page, o] = 0 # += content_deltas[content,
|
||||
# page, o]
|
||||
# assert update_priority[page, o] >= 0
|
||||
diff_weights[page, o] = 0
|
||||
|
||||
# Make sure we don't emit this offset as a side-effect of some
|
||||
# other offset later.
|
||||
for cd in content_deltas.values():
|
||||
cd[page, offset] = 0
|
||||
# TODO: what if we add another content_deltas entry later?
|
||||
# We might clobber it again
|
||||
content_deltas[:, page, o] = 0
|
||||
|
||||
# Need to find 3 more offsets to fill this opcode
|
||||
for err, o in self._compute_error(
|
||||
page,
|
||||
content,
|
||||
target_pixelmap,
|
||||
diff_weights,
|
||||
content_deltas,
|
||||
is_aux
|
||||
):
|
||||
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:
|
||||
# Someone already resolved this diff.
|
||||
continue
|
||||
|
||||
# Make sure we don't end up considering this (page, offset)
|
||||
# again until the next image frame. Even if a better match
|
||||
# comes along, it's probably better to fix up some other byte.
|
||||
# TODO: or should we recompute it with new error?
|
||||
for cd in content_deltas.values():
|
||||
cd[page, o] = 0
|
||||
|
||||
byte_offset = target_pixelmap.byte_offset(o, is_aux)
|
||||
old_packed = target_pixelmap.packed[page, o // 2]
|
||||
|
||||
p = target_pixelmap.byte_pair_difference(
|
||||
byte_offset, old_packed, content)
|
||||
|
||||
# Update priority for the offset we're emitting
|
||||
update_priority[page, o] = p
|
||||
edit_distance[:, page, o] = 0
|
||||
|
||||
# Update memory maps
|
||||
source.page_offset[page, o] = content
|
||||
self.pixelmap.apply(page, o, is_aux, content)
|
||||
self.pixelmap.apply(page, o, is_aux, np.uint8(content))
|
||||
|
||||
if p:
|
||||
# This content byte introduced an error, so put back on the
|
||||
# heap in case we can get back to fixing it exactly
|
||||
# during this frame. Otherwise we'll get to it later.
|
||||
heapq.heappush(
|
||||
priorities, (-p, random.getrandbits(8), page, o))
|
||||
|
||||
offsets.append(o)
|
||||
if len(offsets) == 3:
|
||||
break
|
||||
|
||||
# Pad to 4 if we didn't find enough
|
||||
# Pad to 4 if we didn't find enough
|
||||
for _ in range(len(offsets), 4):
|
||||
offsets.append(offsets[0])
|
||||
|
||||
self._repartition(edit_distance, smallest_idx, smallest, page,
|
||||
offsets)
|
||||
|
||||
yield (page + 32, content, offsets)
|
||||
|
||||
# # TODO: there is still a bug causing residual diffs when we have
|
||||
# # apparently run out of work to do
|
||||
if not np.array_equal(source.page_offset, target.page_offset):
|
||||
diffs = np.nonzero(source.page_offset != target.page_offset)
|
||||
for i in range(len(diffs[0])):
|
||||
diff_p = diffs[0][i]
|
||||
diff_o = diffs[1][i]
|
||||
|
||||
# For HGR, 0x00 or 0x7f may be visually equivalent to the same
|
||||
# bytes with high bit set (depending on neighbours), so skip
|
||||
# them
|
||||
if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \
|
||||
(target.page_offset[diff_p, diff_o] & 0x7f) == 0:
|
||||
continue
|
||||
|
||||
if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \
|
||||
(target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f:
|
||||
continue
|
||||
|
||||
print("Diff at (%d, %d): %d != %d" % (
|
||||
diff_p, diff_o, source.page_offset[diff_p, diff_o],
|
||||
target.page_offset[diff_p, diff_o]
|
||||
))
|
||||
# assert False
|
||||
print("Done")
|
||||
|
||||
# If we run out of things to do, pad forever
|
||||
content = target.page_offset[0, 0]
|
||||
while True:
|
||||
yield (32, content, [0, 0, 0, 0])
|
||||
|
||||
@staticmethod
|
||||
def _heapify_priorities(update_priority: np.array) -> List:
|
||||
"""Build priority queue of (page, offset) ordered by update priority."""
|
||||
def _repartition(
|
||||
self,
|
||||
edit_distance: np.ndarray,
|
||||
smallest_idx: np.ndarray,
|
||||
smallest: np.ndarray,
|
||||
page: int,
|
||||
offsets: int
|
||||
):
|
||||
sip = smallest_idx[:, page, :]
|
||||
contents, _ = (
|
||||
(sip == offsets[0]) |
|
||||
(sip == offsets[1]) |
|
||||
(sip == offsets[2]) |
|
||||
(sip == offsets[3])
|
||||
).nonzero()
|
||||
# print("Repartitioning %d" % len(contents))
|
||||
for content in contents:
|
||||
partition = np.argpartition(
|
||||
edit_distance[content, page], 3)[:4]
|
||||
smallest_idx[content, page] = partition
|
||||
smallest[content, page] = np.take(
|
||||
edit_distance[content, page], partition)
|
||||
|
||||
# Use numpy vectorization to efficiently compute the list of
|
||||
# (priority, random nonce, page, offset) tuples to be heapified.
|
||||
pages, offsets = update_priority.nonzero()
|
||||
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]),
|
||||
pages,
|
||||
offsets)
|
||||
).T.tolist()]
|
||||
return
|
||||
|
||||
heapq.heapify(priorities)
|
||||
return priorities
|
||||
def _compute_delta(
|
||||
self,
|
||||
target: screen.DHGRBitmap,
|
||||
old,
|
||||
is_aux: bool
|
||||
):
|
||||
# Only need to consider 0x0 .. 0x7f content stores
|
||||
diff = np.ndarray((128, 32, 256), dtype=np.int)
|
||||
|
||||
_OFFSETS = np.arange(256)
|
||||
all_content_bytes = np.arange(128).reshape((128, 1))
|
||||
|
||||
def _compute_error(self, page, content, target_pixelmap, diff_weights,
|
||||
content_deltas, is_aux):
|
||||
"""Build priority queue of other offsets at which to store content.
|
||||
# TODO: use error edit distance
|
||||
|
||||
Ordered by offsets which are closest to the target content value.
|
||||
"""
|
||||
# TODO: move this up into parent
|
||||
delta_screen = content_deltas.get(content)
|
||||
if delta_screen is None:
|
||||
delta_screen = target_pixelmap.compute_delta(
|
||||
content, diff_weights, is_aux)
|
||||
content_deltas[content] = delta_screen
|
||||
# def _shift8(s0, t0):
|
||||
# return (s0 << 8) + t0
|
||||
#
|
||||
# def _shift12(s0, t0):
|
||||
# return (s0 << 12) + t0
|
||||
|
||||
delta_page = delta_screen[page]
|
||||
cond = delta_page < 0
|
||||
candidate_offsets = self._OFFSETS[cond]
|
||||
priorities = delta_page[cond]
|
||||
def _target_masked(content, t, byte_offset):
|
||||
return target.masked_update(byte_offset, t, content)
|
||||
|
||||
deltas = [
|
||||
(priorities[i], random.getrandbits(8), candidate_offsets[i])
|
||||
for i in range(len(candidate_offsets))
|
||||
]
|
||||
heapq.heapify(deltas)
|
||||
if is_aux:
|
||||
# Pixels influenced by byte offset 0
|
||||
source_pixels0 = target.mask_and_shift_data(
|
||||
np.apply_along_axis(
|
||||
_target_masked, 1, all_content_bytes, target.packed,
|
||||
0), 0)
|
||||
target_pixels0 = target.mask_and_shift_data(target.packed,0)
|
||||
|
||||
while deltas:
|
||||
pri, _, o = heapq.heappop(deltas)
|
||||
assert pri < 0
|
||||
assert o <= 255
|
||||
# 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)
|
||||
|
||||
yield -pri, o
|
||||
# Pixels influenced by byte offset 2
|
||||
source_pixels2 = target.mask_and_shift_data(
|
||||
np.apply_along_axis(
|
||||
_target_masked, 1, all_content_bytes, target.packed,
|
||||
2), 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(
|
||||
np.apply_along_axis(
|
||||
_target_masked, 1, all_content_bytes, target.packed,
|
||||
1), 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(
|
||||
np.apply_along_axis(
|
||||
_target_masked, 1, all_content_bytes, target.packed,
|
||||
3), 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
|
||||
# 66% of the time this found enough to fill at 3 offsets
|
||||
# 18693 0
|
||||
# 14758 1
|
||||
# 12629 2
|
||||
# / 136804
|
||||
# and only 13% of the time found no candidates
|
||||
return diff
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# priorities = self._heapify_priorities(update_priority)
|
||||
#
|
||||
# content_deltas = {}
|
||||
#
|
||||
# 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:
|
||||
# continue
|
||||
#
|
||||
# offsets = [offset]
|
||||
# content = target.page_offset[page, offset]
|
||||
# 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
|
||||
# diff_weights[page, offset] = 0
|
||||
#
|
||||
# # Update memory maps
|
||||
# source.page_offset[page, offset] = content
|
||||
# self.pixelmap.apply(page, offset, is_aux, content)
|
||||
#
|
||||
# # Make sure we don't emit this offset as a side-effect of some
|
||||
# # other offset later.
|
||||
# for cd in content_deltas.values():
|
||||
# cd[page, offset] = 0
|
||||
# # TODO: what if we add another content_deltas entry later?
|
||||
# # We might clobber it again
|
||||
#
|
||||
# # Need to find 3 more offsets to fill this opcode
|
||||
# for err, o in self._compute_error(
|
||||
# page,
|
||||
# content,
|
||||
# target_pixelmap,
|
||||
# diff_weights,
|
||||
# content_deltas,
|
||||
# is_aux
|
||||
# ):
|
||||
# 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:
|
||||
# # Someone already resolved this diff.
|
||||
# continue
|
||||
#
|
||||
# # Make sure we don't end up considering this (page, offset)
|
||||
# # again until the next image frame. Even if a better match
|
||||
# # comes along, it's probably better to fix up some other byte.
|
||||
# # TODO: or should we recompute it with new error?
|
||||
# for cd in content_deltas.values():
|
||||
# cd[page, o] = 0
|
||||
#
|
||||
# byte_offset = target_pixelmap.byte_offset(o, is_aux)
|
||||
# old_packed = target_pixelmap.packed[page, o // 2]
|
||||
#
|
||||
# p = target_pixelmap.byte_pair_difference(
|
||||
# byte_offset, old_packed, content)
|
||||
#
|
||||
# # Update priority for the offset we're emitting
|
||||
# update_priority[page, o] = p
|
||||
#
|
||||
# source.page_offset[page, o] = content
|
||||
# self.pixelmap.apply(page, o, is_aux, content)
|
||||
#
|
||||
# if p:
|
||||
# # This content byte introduced an error, so put back on the
|
||||
# # heap in case we can get back to fixing it exactly
|
||||
# # during this frame. Otherwise we'll get to it later.
|
||||
# heapq.heappush(
|
||||
# priorities, (-p, random.getrandbits(8), page, o))
|
||||
#
|
||||
# offsets.append(o)
|
||||
# if len(offsets) == 3:
|
||||
# break
|
||||
#
|
||||
# # Pad to 4 if we didn't find enough
|
||||
# for _ in range(len(offsets), 4):
|
||||
# offsets.append(offsets[0])
|
||||
# yield (page + 32, content, offsets)
|
||||
#
|
||||
# # # TODO: there is still a bug causing residual diffs when we have
|
||||
# # # apparently run out of work to do
|
||||
# if not np.array_equal(source.page_offset, target.page_offset):
|
||||
# diffs = np.nonzero(source.page_offset != target.page_offset)
|
||||
# for i in range(len(diffs[0])):
|
||||
# diff_p = diffs[0][i]
|
||||
# diff_o = diffs[1][i]
|
||||
#
|
||||
# # For HGR, 0x00 or 0x7f may be visually equivalent to the same
|
||||
# # bytes with high bit set (depending on neighbours), so skip
|
||||
# # them
|
||||
# if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \
|
||||
# (target.page_offset[diff_p, diff_o] & 0x7f) == 0:
|
||||
# continue
|
||||
#
|
||||
# if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \
|
||||
# (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f:
|
||||
# continue
|
||||
#
|
||||
# print("Diff at (%d, %d): %d != %d" % (
|
||||
# diff_p, diff_o, source.page_offset[diff_p, diff_o],
|
||||
# target.page_offset[diff_p, diff_o]
|
||||
# ))
|
||||
# # assert False
|
||||
#
|
||||
# # If we run out of things to do, pad forever
|
||||
# content = target.page_offset[0, 0]
|
||||
# while True:
|
||||
# yield (32, content, [0, 0, 0, 0])
|
||||
#
|
||||
# @staticmethod
|
||||
# def _heapify_priorities(update_priority: np.array) -> List:
|
||||
# """Build priority queue of (page, offset) ordered by update priority."""
|
||||
#
|
||||
# # Use numpy vectorization to efficiently compute the list of
|
||||
# # (priority, random nonce, page, offset) tuples to be heapified.
|
||||
# pages, offsets = update_priority.nonzero()
|
||||
# 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]),
|
||||
# pages,
|
||||
# offsets)
|
||||
# ).T.tolist()]
|
||||
#
|
||||
# heapq.heapify(priorities)
|
||||
# return priorities
|
||||
#
|
||||
# _OFFSETS = np.arange(256)
|
||||
#
|
||||
# def _compute_error(self, page, content, target_pixelmap, diff_weights,
|
||||
# content_deltas, is_aux):
|
||||
# """Build priority queue of other offsets at which to store content.
|
||||
#
|
||||
# Ordered by offsets which are closest to the target content value.
|
||||
# """
|
||||
# # TODO: move this up into parent
|
||||
# delta_screen = content_deltas.get(content)
|
||||
# if delta_screen is None:
|
||||
# delta_screen = target_pixelmap.compute_delta(
|
||||
# content, diff_weights, is_aux)
|
||||
# content_deltas[content] = delta_screen
|
||||
#
|
||||
# delta_page = delta_screen[page]
|
||||
# cond = delta_page < 0
|
||||
# candidate_offsets = self._OFFSETS[cond]
|
||||
# priorities = delta_page[cond]
|
||||
#
|
||||
# deltas = [
|
||||
# (priorities[i], random.getrandbits(8), candidate_offsets[i])
|
||||
# for i in range(len(candidate_offsets))
|
||||
# ]
|
||||
# heapq.heapify(deltas)
|
||||
#
|
||||
# while deltas:
|
||||
# pri, _, o = heapq.heappop(deltas)
|
||||
# assert pri < 0
|
||||
# assert o <= 255
|
||||
#
|
||||
# yield -pri, o
|
||||
|
Loading…
Reference in New Issue
Block a user