2019-03-21 16:42:47 +00:00
|
|
|
"""Encode a sequence of images as an optimized stream of screen changes."""
|
|
|
|
|
2019-03-07 23:07:24 +00:00
|
|
|
import heapq
|
2019-03-21 16:24:40 +00:00
|
|
|
import random
|
2019-03-14 22:08:50 +00:00
|
|
|
from typing import List, Iterator, Tuple
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-03-04 23:09:00 +00:00
|
|
|
import numpy as np
|
|
|
|
|
2019-03-21 16:24:40 +00:00
|
|
|
import opcodes
|
|
|
|
import screen
|
2019-06-14 20:59:39 +00:00
|
|
|
from frame_grabber import FrameGrabber
|
2019-06-15 20:02:00 +00:00
|
|
|
from palette import Palette
|
2019-06-14 20:59:39 +00:00
|
|
|
from video_mode import VideoMode
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
class Video:
|
2019-07-11 22:40:00 +00:00
|
|
|
"""Encodes sequence of images into prioritized screen byte changes."""
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
CLOCK_SPEED = 1024 * 1024 # type: int
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2019-06-14 20:59:39 +00:00
|
|
|
frame_grabber: FrameGrabber,
|
2019-06-21 21:08:22 +00:00
|
|
|
ticks_per_second: float,
|
2019-06-15 20:02:00 +00:00
|
|
|
mode: VideoMode = VideoMode.HGR,
|
2019-06-19 21:10:15 +00:00
|
|
|
palette: Palette = Palette.NTSC
|
2019-06-13 23:12:26 +00:00
|
|
|
):
|
2019-06-14 20:59:39 +00:00
|
|
|
self.mode = mode # type: VideoMode
|
|
|
|
self.frame_grabber = frame_grabber # type: FrameGrabber
|
2023-01-17 21:39:05 +00:00
|
|
|
self.ticks_per_second = float(ticks_per_second) # type: float
|
2019-06-13 23:12:26 +00:00
|
|
|
self.ticks_per_frame = (
|
2019-07-02 21:40:50 +00:00
|
|
|
self.ticks_per_second / frame_grabber.input_frame_rate
|
2019-06-21 21:08:22 +00:00
|
|
|
) # type: float
|
2019-06-13 23:12:26 +00:00
|
|
|
self.frame_number = 0 # type: int
|
2019-06-15 20:02:00 +00:00
|
|
|
self.palette = palette # type: Palette
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
# Initialize empty screen
|
|
|
|
self.memory_map = screen.MemoryMap(
|
|
|
|
screen_page=1) # type: screen.MemoryMap
|
|
|
|
if self.mode == mode.DHGR:
|
|
|
|
self.aux_memory_map = screen.MemoryMap(
|
|
|
|
screen_page=1) # type: screen.MemoryMap
|
|
|
|
|
2019-07-07 20:12:10 +00:00
|
|
|
self.pixelmap = screen.DHGRBitmap(
|
|
|
|
palette=palette,
|
|
|
|
main_memory=self.memory_map,
|
|
|
|
aux_memory=self.aux_memory_map
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.pixelmap = screen.HGRBitmap(
|
|
|
|
palette=palette,
|
|
|
|
main_memory=self.memory_map,
|
|
|
|
)
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
# Accumulates pending edit weights across frames
|
2023-01-17 12:26:25 +00:00
|
|
|
self.update_priority = np.zeros((32, 256), dtype=np.int32)
|
2019-06-13 23:12:26 +00:00
|
|
|
if self.mode == mode.DHGR:
|
2023-01-17 12:26:25 +00:00
|
|
|
self.aux_update_priority = np.zeros((32, 256), dtype=np.int32)
|
2019-06-13 23:12:26 +00:00
|
|
|
|
2023-01-17 21:39:05 +00:00
|
|
|
# Indicates whether we have run out of work for the main/aux banks.
|
|
|
|
# Key is True for aux bank and False for main bank
|
|
|
|
self.out_of_work = {True: False, False: False}
|
|
|
|
|
2019-06-19 21:28:31 +00:00
|
|
|
def tick(self, ticks: int) -> bool:
|
2019-07-11 22:40:00 +00:00
|
|
|
"""Keep track of when it is time for a new image frame."""
|
|
|
|
|
2019-06-19 21:28:31 +00:00
|
|
|
if ticks >= (self.ticks_per_frame * self.frame_number):
|
2019-06-13 23:12:26 +00:00
|
|
|
self.frame_number += 1
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2019-03-10 22:42:31 +00:00
|
|
|
def encode_frame(
|
2019-06-13 23:12:26 +00:00
|
|
|
self,
|
2023-01-17 21:39:05 +00:00
|
|
|
target: screen.Bitmap,
|
2019-06-13 23:12:26 +00:00
|
|
|
is_aux: bool,
|
2019-03-10 22:42:31 +00:00
|
|
|
) -> Iterator[opcodes.Opcode]:
|
2019-07-11 22:40:00 +00:00
|
|
|
"""Converge towards target frame in priority order of edit distance."""
|
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
if is_aux:
|
|
|
|
memory_map = self.aux_memory_map
|
|
|
|
update_priority = self.aux_update_priority
|
|
|
|
else:
|
|
|
|
memory_map = self.memory_map
|
|
|
|
update_priority = self.update_priority
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-07-02 21:40:50 +00:00
|
|
|
# Make sure nothing is leaking into screen holes
|
|
|
|
assert np.count_nonzero(
|
|
|
|
memory_map.page_offset[screen.SCREEN_HOLES]) == 0
|
|
|
|
|
2019-03-27 21:37:06 +00:00
|
|
|
print("Similarity %f" % (update_priority.mean()))
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
yield from self._index_changes(
|
|
|
|
memory_map, target, update_priority, is_aux)
|
2019-02-23 23:52:25 +00:00
|
|
|
|
2019-03-10 22:42:31 +00:00
|
|
|
def _index_changes(
|
|
|
|
self,
|
|
|
|
source: screen.MemoryMap,
|
2023-01-17 21:39:05 +00:00
|
|
|
target_pixelmap: screen.Bitmap,
|
2019-06-13 23:12:26 +00:00
|
|
|
update_priority: np.array,
|
2023-01-17 21:39:05 +00:00
|
|
|
is_aux: bool
|
2019-03-14 22:32:52 +00:00
|
|
|
) -> Iterator[Tuple[int, int, List[int]]]:
|
|
|
|
"""Transform encoded screen to sequence of change tuples."""
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2023-01-17 21:39:05 +00:00
|
|
|
if self.mode == VideoMode.DHGR and is_aux:
|
|
|
|
target = target_pixelmap.aux_memory
|
2019-06-13 23:12:26 +00:00
|
|
|
else:
|
2023-01-17 21:39:05 +00:00
|
|
|
target = target_pixelmap.main_memory
|
2019-06-13 23:12:26 +00:00
|
|
|
|
2019-07-02 21:40:50 +00:00
|
|
|
diff_weights = target_pixelmap.diff_weights(self.pixelmap, is_aux)
|
|
|
|
# Don't bother storing into screen holes
|
|
|
|
diff_weights[screen.SCREEN_HOLES] = 0
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-03-14 22:32:52 +00:00
|
|
|
# Clear any update priority entries that have resolved themselves
|
2019-03-10 22:42:31 +00:00
|
|
|
# with new frame
|
2019-03-27 21:37:06 +00:00
|
|
|
update_priority[diff_weights == 0] = 0
|
|
|
|
update_priority += diff_weights
|
2023-01-17 21:39:05 +00:00
|
|
|
assert np.all(update_priority >= 0)
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-03-27 21:37:06 +00:00
|
|
|
priorities = self._heapify_priorities(update_priority)
|
2019-03-14 22:08:50 +00:00
|
|
|
|
2019-03-10 22:42:31 +00:00
|
|
|
while priorities:
|
2019-06-13 23:12:26 +00:00
|
|
|
pri, _, page, offset = heapq.heappop(priorities)
|
|
|
|
|
2019-07-02 21:40:50 +00:00
|
|
|
assert not screen.SCREEN_HOLES[page, offset], (
|
2019-07-04 14:21:20 +00:00
|
|
|
"Attempted to store into screen hole at (%d, %d)" % (
|
2019-07-02 21:40:50 +00:00
|
|
|
page, offset))
|
|
|
|
|
2019-03-10 22:42:31 +00:00
|
|
|
# Check whether we've already cleared this diff while processing
|
|
|
|
# an earlier opcode
|
2019-03-27 21:37:06 +00:00
|
|
|
if update_priority[page, offset] == 0:
|
2019-03-07 23:07:24 +00:00
|
|
|
continue
|
2019-03-10 22:42:31 +00:00
|
|
|
|
2019-03-07 23:07:24 +00:00
|
|
|
offsets = [offset]
|
|
|
|
content = target.page_offset[page, offset]
|
2019-07-02 21:40:50 +00:00
|
|
|
if self.mode == VideoMode.DHGR:
|
|
|
|
# DHGR palette bit not expected to be set
|
|
|
|
assert content < 0x80
|
2019-03-07 23:07:24 +00:00
|
|
|
|
|
|
|
# Clear priority for the offset we're emitting
|
2019-03-27 21:37:06 +00:00
|
|
|
update_priority[page, offset] = 0
|
2019-03-21 22:56:45 +00:00
|
|
|
diff_weights[page, offset] = 0
|
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
# Update memory maps
|
|
|
|
self.pixelmap.apply(page, offset, is_aux, content)
|
|
|
|
|
2019-03-07 23:07:24 +00:00
|
|
|
# Need to find 3 more offsets to fill this opcode
|
2019-06-13 23:12:26 +00:00
|
|
|
for err, o in self._compute_error(
|
2019-03-10 22:42:31 +00:00
|
|
|
page,
|
2019-03-07 23:07:24 +00:00
|
|
|
content,
|
2019-06-13 23:12:26 +00:00
|
|
|
target_pixelmap,
|
2019-03-14 22:08:50 +00:00
|
|
|
diff_weights,
|
2019-06-13 23:12:26 +00:00
|
|
|
is_aux
|
2019-03-10 22:42:31 +00:00
|
|
|
):
|
2019-06-13 23:12:26 +00:00
|
|
|
assert o != offset
|
2019-07-02 21:40:50 +00:00
|
|
|
assert not screen.SCREEN_HOLES[page, o], (
|
|
|
|
"Attempted to store into screen hole at (%d, %d)" % (
|
|
|
|
page, o))
|
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
if update_priority[page, o] == 0:
|
2019-07-11 22:40:00 +00:00
|
|
|
# Someone already resolved this diff.
|
2019-06-13 23:12:26 +00:00
|
|
|
continue
|
2019-03-21 22:56:45 +00:00
|
|
|
|
2019-07-04 14:21:20 +00:00
|
|
|
byte_offset = target_pixelmap.byte_offset(o, is_aux)
|
2019-06-13 23:12:26 +00:00
|
|
|
old_packed = target_pixelmap.packed[page, o // 2]
|
|
|
|
|
2019-07-02 21:40:50 +00:00
|
|
|
p = target_pixelmap.byte_pair_difference(
|
|
|
|
byte_offset, old_packed, content)
|
2019-03-21 22:56:45 +00:00
|
|
|
|
2019-03-21 15:57:09 +00:00
|
|
|
# Update priority for the offset we're emitting
|
2019-07-11 22:40:00 +00:00
|
|
|
update_priority[page, o] = p
|
2019-03-21 22:56:45 +00:00
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
self.pixelmap.apply(page, o, is_aux, content)
|
2019-03-21 22:56:45 +00:00
|
|
|
if p:
|
|
|
|
# This content byte introduced an error, so put back on the
|
|
|
|
# heap in case we can get back to fixing it exactly
|
2023-01-17 21:39:05 +00:00
|
|
|
# during this frame. Otherwise, we'll get to it later.
|
2019-03-21 22:56:45 +00:00
|
|
|
heapq.heappush(
|
2019-07-11 22:40:00 +00:00
|
|
|
priorities, (-p, random.getrandbits(8), page, o))
|
2019-06-13 23:12:26 +00:00
|
|
|
|
|
|
|
offsets.append(o)
|
|
|
|
if len(offsets) == 3:
|
|
|
|
break
|
2019-03-10 22:42:31 +00:00
|
|
|
|
|
|
|
# Pad to 4 if we didn't find enough
|
2019-03-07 23:07:24 +00:00
|
|
|
for _ in range(len(offsets), 4):
|
|
|
|
offsets.append(offsets[0])
|
2023-01-17 21:39:05 +00:00
|
|
|
yield page + 32, content, offsets
|
|
|
|
|
|
|
|
self.out_of_work[is_aux] = True
|
|
|
|
|
|
|
|
# These debugging assertions validate that when we are out of work,
|
|
|
|
# our source and target representations should be identical.
|
|
|
|
#
|
|
|
|
# They only work correctly for palettes that do not have identical
|
|
|
|
# colours (e.g. IIGS but not NTSC which has two identical greys).
|
|
|
|
#
|
|
|
|
# The problem is that if we have substituted one grey for the other
|
|
|
|
# there may be no diff if they are part of an extended run of greys.
|
|
|
|
#
|
|
|
|
# The only difference is at the end of the run where these produce
|
|
|
|
# different artifact colours, but this may only be visible in the
|
|
|
|
# other bank.
|
|
|
|
#
|
|
|
|
# It may take several iterations of main/aux before we will notice and
|
|
|
|
# correct all of these differences. That means we don't have a
|
|
|
|
# deterministic point in time when we can assert that all diffs should
|
|
|
|
# have been resolved.
|
|
|
|
# TODO: add flag to enable debug assertions
|
2023-01-17 21:39:05 +00:00
|
|
|
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've finished both main and aux pages, there should be no residual
|
|
|
|
# diffs in packed representation
|
|
|
|
all_done = self.out_of_work[True] and self.out_of_work[False]
|
|
|
|
if all_done and not np.array_equal(self.pixelmap.packed,
|
|
|
|
target_pixelmap.packed):
|
|
|
|
diffs = np.nonzero(
|
|
|
|
self.pixelmap.packed != target_pixelmap.packed)
|
|
|
|
print("is_aux: %s" % is_aux)
|
|
|
|
for i in range(len(diffs[0])):
|
|
|
|
diff_p = diffs[0][i]
|
|
|
|
diff_o = diffs[1][i]
|
|
|
|
print("(%d, %d): got %d want %d" % (
|
|
|
|
diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o],
|
|
|
|
target_pixelmap.packed[diff_p, diff_o]))
|
|
|
|
assert False
|
2019-06-13 23:12:26 +00:00
|
|
|
|
2019-03-10 22:42:31 +00:00
|
|
|
# If we run out of things to do, pad forever
|
2019-06-13 23:12:26 +00:00
|
|
|
content = target.page_offset[0, 0]
|
2019-03-10 22:42:31 +00:00
|
|
|
while True:
|
2023-01-17 21:39:05 +00:00
|
|
|
yield 32, content, [0, 0, 0, 0]
|
2019-03-14 22:32:52 +00:00
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
@staticmethod
|
|
|
|
def _heapify_priorities(update_priority: np.array) -> List:
|
2019-07-11 22:40:00 +00:00
|
|
|
"""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.
|
2019-06-13 23:12:26 +00:00
|
|
|
pages, offsets = update_priority.nonzero()
|
|
|
|
priorities = [tuple(data) for data in np.stack((
|
|
|
|
-update_priority[pages, offsets],
|
2023-01-17 21:39:05 +00:00
|
|
|
# Don't use deterministic order for page, offset. Otherwise,
|
|
|
|
# we get the "venetian blind" effect when filling large blocks of
|
|
|
|
# colour.
|
2019-07-02 21:40:50 +00:00
|
|
|
np.random.randint(0, 2 ** 8, size=pages.shape[0]),
|
2019-06-13 23:12:26 +00:00
|
|
|
pages,
|
|
|
|
offsets)
|
|
|
|
).T.tolist()]
|
|
|
|
|
|
|
|
heapq.heapify(priorities)
|
|
|
|
return priorities
|
|
|
|
|
2019-03-14 22:32:52 +00:00
|
|
|
_OFFSETS = np.arange(256)
|
|
|
|
|
2023-01-17 21:39:05 +00:00
|
|
|
def _compute_error(
|
|
|
|
self, page, content, target_pixelmap, diff_weights, is_aux):
|
2019-07-11 22:40:00 +00:00
|
|
|
"""Build priority queue of other offsets at which to store content.
|
|
|
|
|
|
|
|
Ordered by offsets which are closest to the target content value.
|
|
|
|
"""
|
2023-01-17 21:39:05 +00:00
|
|
|
delta_page = target_pixelmap.compute_delta_page(
|
|
|
|
page, content, diff_weights[page, :], is_aux)
|
2019-03-14 22:32:52 +00:00
|
|
|
cond = delta_page < 0
|
|
|
|
candidate_offsets = self._OFFSETS[cond]
|
2019-03-21 22:56:45 +00:00
|
|
|
priorities = delta_page[cond]
|
2019-03-14 22:32:52 +00:00
|
|
|
|
2023-01-17 21:39:05 +00:00
|
|
|
# Don't use deterministic order for page, offset. Otherwise,
|
|
|
|
# we get the "venetian blind" effect when filling large blocks of
|
|
|
|
# colour.
|
2019-06-13 23:12:26 +00:00
|
|
|
deltas = [
|
2019-07-11 22:40:00 +00:00
|
|
|
(priorities[i], random.getrandbits(8), candidate_offsets[i])
|
2019-03-14 22:32:52 +00:00
|
|
|
for i in range(len(candidate_offsets))
|
|
|
|
]
|
2019-06-13 23:12:26 +00:00
|
|
|
heapq.heapify(deltas)
|
2019-03-21 22:56:45 +00:00
|
|
|
|
2019-06-13 23:12:26 +00:00
|
|
|
while deltas:
|
2023-01-17 21:39:05 +00:00
|
|
|
pri, _, offset = heapq.heappop(deltas)
|
2019-06-13 23:12:26 +00:00
|
|
|
assert pri < 0
|
2023-01-17 21:39:05 +00:00
|
|
|
assert 0 <= offset <= 255
|
2019-03-14 22:32:52 +00:00
|
|
|
|
2023-01-17 21:39:05 +00:00
|
|
|
yield -pri, offset
|