Checkpoint

This commit is contained in:
kris
2023-01-22 00:49:23 +00:00
parent d131b48661
commit 1b640705d2
7 changed files with 337 additions and 102 deletions

View File

@@ -71,6 +71,13 @@ class DHGRColours(NominalColours):
WHITE = 0b1111
class MonoColours(NominalColours):
"""XXX """
BLACK = 0b0
WHITE = 0b1
def ror(int4: int, howmany: int) -> int:
"""Rotate-right an int4 some number of times."""
res = int4
@@ -147,3 +154,34 @@ def dots_to_nominal_colour_pixel_values(
num_bits, dots, colours, init_phase
))
@functools.lru_cache(None)
def dots_to_mono_pixels(
num_bits: int,
dots: int,
colours: Type[MonoColours],
) -> Tuple[MonoColours]:
"""Sequence of num_bits mono pixels.
"""
res = []
shifted = dots
for i in range(num_bits):
colour = shifted & 0b1
res.append(colours(colour))
shifted >>= 1
return tuple(res)
@functools.lru_cache(None)
def dots_to_mono_pixel_values(
num_bits: int,
dots: int,
colours: Type[MonoColours],
) -> Tuple[int]:
""""Sequence of num_bits nominal colour values via sliding 4-bit window."""
return tuple(p.value for p in dots_to_mono_pixels(num_bits, dots, colours))

View File

@@ -81,24 +81,28 @@ class FileFrameGrabber(FrameGrabber):
self.filename, self.video_mode, self.palette)
os.makedirs(frame_dir, exist_ok=True)
global _converter
if self.video_mode == VideoMode.DHGR_MONO:
_converter = DHGRMonoFrameConverter(frame_dir)
converter = DHGRMonoFrameConverter(frame_dir)
elif self.video_mode == VideoMode.DHGR:
# XXX support palette
_converter = DHGRFrameConverter(frame_dir=frame_dir)
converter = DHGRFrameConverter(frame_dir=frame_dir)
elif self.video_mode == VideoMode.HGR:
_converter = HGRFrameConverter(
converter = HGRFrameConverter(
frame_dir=frame_dir, palette_value=self.palette.value)
pool = multiprocessing.Pool(10)
for main, aux in pool.imap(_converter.convert, self._frame_extractor(
pool = multiprocessing.Pool(
10, initializer=init_converter, initargs=(converter,))
for main, aux in pool.imap(converter.convert, self._frame_extractor(
frame_dir), chunksize=1):
main_map = screen.FlatMemoryMap(
screen_page=1, data=main).to_memory_map()
aux_map = screen.FlatMemoryMap(
screen_page=1, data=aux).to_memory_map() if aux else None
if aux is not None:
aux_map = screen.FlatMemoryMap(
screen_page=1, data=aux).to_memory_map()
else:
aux_map = None
# print("main %s" % main_map.page_offset)
# print("aux %s" % aux_map.page_offset)
yield main_map, aux_map
@@ -106,6 +110,11 @@ class FileFrameGrabber(FrameGrabber):
_converter = None # type:Optional[FrameConverter]
def init_converter(converter):
global _converter
_converter = converter
class FrameConverter:
def __init__(self, frame_dir):
self.frame_dir = frame_dir
@@ -210,7 +219,9 @@ class DHGRMonoFrameConverter(FrameConverter):
os.stat(dhrfile)
except FileNotFoundError:
subprocess.call([
"python", "convert.py", "dhr_mono", bmpfile, dhrfile
# XXX
"python3.10", "/Volumes/Stuff/apple2/dither/convert.py",
"dhr_mono", "--no-show-output", bmpfile, dhrfile
])
# os.remove(bmpfile)

View File

@@ -39,7 +39,8 @@ class EditDistanceParams:
# Smallest substitution value is ~20 from palette.diff_matrices, i.e.
# we always prefer to transpose 2 pixels rather than substituting colours.
# TODO: is quality really better allowing transposes?
transpose_costs = np.ones((128, 128), dtype=np.float64)
# XXX is 1 appropriate weight for mono?
transpose_costs = np.ones((128, 128), dtype=np.float64) * 50
# These will be filled in later
substitute_costs = np.zeros((128, 128), dtype=np.float64)
@@ -58,8 +59,8 @@ def compute_diff_matrix(pal: Type[palette.BasePalette]):
Specifically CIE2000 delta values for this palette.
"""
dm = np.ndarray(shape=(16, 16), dtype=np.int32)
palette_size = len(pal.RGB)
dm = np.ndarray(shape=(palette_size, palette_size), dtype=np.int32)
for colour1, a in pal.RGB.items():
alab = colormath.color_conversions.convert_color(
a, colormath.color_objects.LabColor)
@@ -78,9 +79,11 @@ def compute_substitute_costs(pal: Type[palette.BasePalette]):
diff_matrix = compute_diff_matrix(pal)
palette_size = len(pal.RGB)
# Penalty for changing colour
for i, c in enumerate(PIXEL_CHARS):
for j, d in enumerate(PIXEL_CHARS):
for i, c in enumerate(PIXEL_CHARS[:palette_size]):
for j, d in enumerate(PIXEL_CHARS[:palette_size]):
cost = diff_matrix[i, j]
edp.substitute_costs[(ord(c), ord(d))] = cost
edp.substitute_costs[(ord(d), ord(c))] = cost
@@ -112,7 +115,7 @@ def edit_distance(
def compute_edit_distance(
edp: EditDistanceParams,
bitmap_cls: Type[screen.Bitmap],
nominal_colours: Type[colours.NominalColours]
nominal_colours: Type[colours.NominalColours], is_mono: bool = False
) -> np.ndarray:
"""Computes edit distance matrix between all pairs of pixel strings.
@@ -146,12 +149,18 @@ def compute_edit_distance(
for o, ph in enumerate(bitmap_cls.PHASES):
# Compute this in the outer loop since it's invariant under j
first_dots = bitmap_cls.to_dots(i, byte_offset=o)
first_pixels = pixel_string(
colours.dots_to_nominal_colour_pixel_values(
num_dots, first_dots, nominal_colours,
init_phase=ph)
)
if is_mono:
first_pixel_values = colours.dots_to_mono_pixel_values(
num_dots, first_dots, nominal_colours)
else:
first_pixel_values = (
colours.dots_to_nominal_colour_pixel_values(
num_dots, first_dots, nominal_colours,
init_phase=ph)
)
first_pixels = pixel_string(first_pixel_values)
# Matrix is symmetrical with zero diagonal so only need to compute
# upper triangle
for j in range(i):
@@ -164,11 +173,19 @@ def compute_edit_distance(
pair = pair_base + np.uint64(j)
second_dots = bitmap_cls.to_dots(j, byte_offset=o)
second_pixels = pixel_string(
colours.dots_to_nominal_colour_pixel_values(
num_dots, second_dots, nominal_colours,
init_phase=ph)
)
if is_mono:
second_pixel_values = colours.dots_to_mono_pixel_values(
num_dots, second_dots, nominal_colours)
else:
second_pixel_values = (
colours.dots_to_nominal_colour_pixel_values(
num_dots, second_dots, nominal_colours,
init_phase=ph)
)
second_pixels = pixel_string(second_pixel_values)
edit[o, pair] = edit_distance(
edp, first_pixels, second_pixels, error=False)
@@ -183,7 +200,9 @@ def make_edit_distance(
):
"""Write file containing (D)HGR edit distance matrix for a palette."""
dist = compute_edit_distance(edp, bitmap_cls, nominal_colours)
is_mono = pal.ID == palette.Palette.MONO
dist = compute_edit_distance(edp, bitmap_cls, nominal_colours, is_mono)
data = "transcoder/data/%s_palette_%d_edit_distance.npz" % (
bitmap_cls.NAME, pal.ID.value)
np.savez_compressed(data, edit_distance=dist)
@@ -192,12 +211,16 @@ def make_edit_distance(
def main():
for p in palette.PALETTES.values():
print("Processing palette %s" % p)
# TODO: still worth using error distance matrices?
edp = compute_substitute_costs(p)
# TODO: still worth using error distance matrices?
make_edit_distance(p, edp, screen.HGRBitmap, colours.HGRColours)
make_edit_distance(p, edp, screen.DHGRBitmap, colours.DHGRColours)
if p.ID == palette.Palette.MONO:
make_edit_distance(p, edp, screen.DHGRMonoBitmap,
colours.MonoColours)
else:
#make_edit_distance(p, edp, screen.HGRBitmap, colours.HGRColours)
#make_edit_distance(p, edp, screen.DHGRBitmap, colours.DHGRColours)
pass
if __name__ == "__main__":

View File

@@ -85,8 +85,7 @@ class Movie:
palette=self.palette
)
elif self.video_mode == VideoMode.DHGR_MONO:
# XXX
target_pixelmap = screen.DHGRBitmap(
target_pixelmap = screen.DHGRMonoBitmap(
main_memory=main,
aux_memory=aux,
palette=self.palette

View File

@@ -5,7 +5,7 @@ from typing import Dict, Type
import colormath.color_objects
from colours import HGRColours
from colours import HGRColours, MonoColours
# Type annotation
RGB = colormath.color_objects.sRGBColor
@@ -18,9 +18,11 @@ def rgb(r, g, b):
class Palette(enum.Enum):
"""BMP2DHR palette numbers."""
# XXX don't use BMP2DHR palette values here
UNKNOWN = -1
IIGS = 0
NTSC = 5
MONO = 1
class BasePalette:
@@ -54,6 +56,15 @@ class NTSCPalette(BasePalette):
}
class MonoPalette(BasePalette):
ID = Palette.MONO
RGB = {
MonoColours.BLACK: rgb(0, 0, 0),
MonoColours.WHITE: rgb(255, 255, 255)
}
class IIGSPalette(BasePalette):
ID = Palette.IIGS
@@ -80,5 +91,6 @@ class IIGSPalette(BasePalette):
PALETTES = {
Palette.IIGS: IIGSPalette,
Palette.NTSC: NTSCPalette
Palette.NTSC: NTSCPalette,
Palette.MONO: MonoPalette
} # type: Dict[Palette, Type[BasePalette]]

View File

@@ -184,7 +184,8 @@ class Bitmap:
self.SCREEN_BYTES = np.uint64(len(self.BYTE_MASKS)) # type: np.uint64
self.packed = np.empty(
shape=(32, 128), dtype=np.uint64) # type: np.ndarray
shape=(32, np.uint64(256) // self.SCREEN_BYTES), dtype=np.uint64
) # type: np.ndarray
self._pack()
# TODO: don't leak headers/footers across screen rows. We should be using
@@ -213,17 +214,24 @@ class Bitmap:
# decode the effective colours at the beginning of the 22-bit 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
if header.shape[1]:
# 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
next_col = np.roll(body, -1, axis=1).astype(np.uint64)
footer = self._make_footer(next_col)
# Don't leak footer across page boundaries
footer[:, -1] = 0
if footer.shape[1]:
# Don't leak footer across page boundaries
footer[:, -1] = 0
self.packed = header ^ body ^ footer
packed = body
if header.shape[1]:
packed ^= header
if footer.shape[1]:
packed ^= footer
self.packed = packed
@staticmethod
def masked_update(
@@ -262,12 +270,14 @@ class Bitmap:
"""Update packed representation of changing main/aux memory."""
byte_offset = self.byte_offset(offset, is_aux)
packed_offset = offset // 2
packed_offset = int(offset // self.SCREEN_BYTES)
self.packed[page, packed_offset] = self.masked_update(
byte_offset, self.packed[page, packed_offset], value)
self._fix_scalar_neighbours(page, packed_offset, byte_offset)
assert self.packed[page, packed_offset] < 128
if is_aux:
self.aux_memory.write(page, offset, value)
else:
@@ -280,12 +290,15 @@ class Bitmap:
byte_offset: int) -> None:
"""Fix up column headers/footers when updating a (page, offset)."""
if byte_offset == 0 and offset > 0:
if byte_offset == 0 and offset > 0 and self.HEADER_BITS:
self.packed[page, offset - 1] = self._fix_column_left(
self.packed[page, offset - 1],
self.packed[page, offset]
)
elif byte_offset == (self.SCREEN_BYTES - 1) and offset < 127:
elif (
byte_offset == (self.SCREEN_BYTES - 1) and offset < 127 and
self.FOOTER_BITS
):
# Need to also update the 3-bit header of the next column
self.packed[page, offset + 1] = self._fix_column_right(
self.packed[page, offset + 1],
@@ -330,12 +343,12 @@ class Bitmap:
# Propagate new value into neighbouring byte headers/footers if
# necessary
if byte_offset == 0:
if byte_offset == 0 and self.HEADER_BITS:
# 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):
elif byte_offset == (self.SCREEN_BYTES - 1) and self.FOOTER_BITS:
# 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)
@@ -429,22 +442,32 @@ class Bitmap:
if content is not None:
compare_packed = self.masked_update(o, source_packed, content)
self._fix_array_neighbours(compare_packed, o)
# print("content = %s" % content)
else:
compare_packed = source_packed
# print("source_packed %s" % source_packed)
# print("compare %s" % compare_packed)
# print("packed %s" % self.packed)
# Pixels influenced by byte offset o
source_pixels = self.mask_and_shift_data(compare_packed, o)
target_pixels = self.mask_and_shift_data(self.packed, o)
aux_offset = 0 if is_aux else 1 # XXX
source_pixels = self.mask_and_shift_data(
compare_packed[:, aux_offset::2], o)
target_pixels = self.mask_and_shift_data(
self.packed[:, aux_offset::2], 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)
# print(source_pixels, target_pixels)
# print(dist)
# assert False
# Interleave even/odd columns
diff[:, 0::2] = dists[0]
diff[:, 1::2] = dists[1]
for i in range(len(offsets)):
# Interleave columns
diff[:, i::len(offsets)] = dists[i]
return diff
@@ -468,28 +491,35 @@ class Bitmap:
diff = np.ndarray((256,), dtype=np.int32)
offsets = self._byte_offsets(is_aux)
# print(source_packed, target_packed)
# assert source_packed.dtype == np.uint64, source_packed.dtype
# assert target_packed.dtype == np.uint64, target_packed.dtype
dists = []
for o in offsets:
if content is not None:
compare_packed = self.masked_update(o, source_packed, content)
# print(content, source_packed, compare_packed)
self._fix_array_neighbours(compare_packed, o)
# print(compare_packed)
else:
compare_packed = source_packed
# Pixels influenced by byte offset o
source_pixels = self.mask_and_shift_data(compare_packed, o)
target_pixels = self.mask_and_shift_data(target_packed, o)
aux_offset = 0 if is_aux else 1 # XXX
source_pixels = self.mask_and_shift_data(
compare_packed[:, aux_offset::2], o)
target_pixels = self.mask_and_shift_data(
target_packed[:, aux_offset::2], 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]
for i in range(len(offsets)):
# Interleave columns
diff[i::len(offsets)] = dists[i]
return diff
@@ -1005,3 +1035,118 @@ class DHGRBitmap(Bitmap):
update = (new_value & np.uint64(0x7f)) << np.uint64(
7 * byte_offset + 3)
return masked_value ^ update
class DHGRMonoBitmap(Bitmap):
"""Packed bitmap representation of DHGR mono screen memory.
Unlike the colour cases, where the display colour of a pixel is influenced
by the pixels to the left of it, the mono representation is trivial.
With this masked representation, we can precompute an edit distance for the
pixel changes resulting from all possible DHGR byte stores, see
make_edit_distance.py.
XXX
The edit distance matrix is encoded by concatenating the 13-bit source
and target masked values into a 26-bit pair, which indexes into the
edit_distance array to give the corresponding edit distance.
"""
NAME = 'DHGR_MONO'
# Packed representation is 0 + 14 + 0 = 14 bits
HEADER_BITS = np.uint64(0)
BODY_BITS = np.uint64(7)
FOOTER_BITS = np.uint64(0)
# Masked representation selecting the influence of each byte offset
MASKED_BITS = np.uint64(7) # 7-bit body + 0-bit header + 0-bit footer
# Masking is 1:1 with screen dots
MASKED_DOTS = np.uint64(7)
BYTE_MASKS = [
np.uint64(0b1111111),
]
BYTE_SHIFTS = [np.uint64(0)]
PHASES = [0]
@staticmethod
def _make_header(col: IntOrArray) -> IntOrArray:
"""Extract upper XXX bits of body for header of next column."""
# XXX return None
if isinstance(col, np.ndarray):
return np.zeros((col.shape[0], 0), dtype=col.dtype)
return 0
def _body(self) -> np.ndarray:
"""Pack related screen bytes into an efficient representation.
For DHGR we first strip off the (unused) palette bit to produce
7-bit values, then interleave aux and main memory columns and pack
these 7-bit values into 28-bits. This sequentially encodes 7 4-bit
DHGR pixels, which is the "repeating unit" of the DHGR screen, and
in a form that is convenient to operate on.
We also shift to make room for the 3-bit header.
"""
# Palette bit is unused for DHGR so mask it out
aux = (self.aux_memory.page_offset & 0x7f).astype(np.uint8)
main = (self.main_memory.page_offset & 0x7f).astype(np.uint8)
body = np.empty((aux.shape[0], aux.shape[1] + main.shape[1]),
dtype=np.uint8)
body[:, 0::2] = aux
body[:, 1::2] = main
# print(body)
return body
@staticmethod
def _make_footer(col: IntOrArray) -> IntOrArray:
"""Extract lower XXX bits of body for footer of previous column."""
# XXX return None
if isinstance(col, np.ndarray):
return np.zeros((col.shape[0], 0), dtype=col.dtype)
return 0
@staticmethod
@functools.lru_cache(None)
def byte_offset(page_offset: int, is_aux: bool) -> int:
"""Returns 0..3 packed byte offset for a given page_offset and is_aux"""
return 0
@staticmethod
@functools.lru_cache(None)
def _byte_offsets(is_aux: bool) -> Tuple[int, int]:
return (0,)
@classmethod
def to_dots(cls, masked_val: int, byte_offset: int) -> int:
"""Convert masked representation to bit sequence of display dots.
For DHGR the 13-bit masked value is already a 13-bit dot sequence
so no need to transform it.
"""
return masked_val
@staticmethod
def masked_update(
byte_offset: int,
old_value: IntOrArray,
new_value: np.uint8) -> IntOrArray:
"""Update int/array to store new value at byte_offset in every entry.
Does not patch up headers/footers of neighbouring columns.
"""
if isinstance(old_value, np.ndarray):
res = np.empty_like(old_value)
res.fill(new_value & np.uint64(0x7f))
return res
else:
return new_value & np.uint64(0x7f)

View File

@@ -37,24 +37,31 @@ class Video:
# Initialize empty screen
self.memory_map = screen.MemoryMap(
screen_page=1) # type: screen.MemoryMap
if self.mode == mode.DHGR:
if self.mode in {VideoMode.DHGR, VideoMode.DHGR_MONO}:
self.aux_memory_map = screen.MemoryMap(
screen_page=1) # type: screen.MemoryMap
self.pixelmap = screen.DHGRBitmap(
palette=palette,
main_memory=self.memory_map,
aux_memory=self.aux_memory_map
)
if self.mode == VideoMode.DHGR:
self.pixelmap = screen.DHGRBitmap(
palette=palette,
main_memory=self.memory_map,
aux_memory=self.aux_memory_map
)
else:
self.pixelmap = screen.DHGRMonoBitmap(
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,
)
# print("pix %s" % self.pixelmap.packed)
# Accumulates pending edit weights across frames
self.update_priority = np.zeros((32, 256), dtype=np.int32)
if self.mode == mode.DHGR:
if self.mode in {VideoMode.DHGR, VideoMode.DHGR_MONO}:
self.aux_update_priority = np.zeros((32, 256), dtype=np.int32)
# Indicates whether we have run out of work for the main/aux banks.
@@ -101,7 +108,7 @@ class Video:
) -> Iterator[Tuple[int, int, List[int]]]:
"""Transform encoded screen to sequence of change tuples."""
if self.mode == VideoMode.DHGR and is_aux:
if self.mode in {VideoMode.DHGR, VideoMode.DHGR_MONO} and is_aux:
target = target_pixelmap.aux_memory
else:
target = target_pixelmap.main_memory
@@ -132,7 +139,7 @@ class Video:
offsets = [offset]
content = target.page_offset[page, offset]
if self.mode == VideoMode.DHGR:
if self.mode in {VideoMode.DHGR, VideoMode.DHGR_MONO}:
# DHGR palette bit not expected to be set
assert content < 0x80
@@ -206,44 +213,44 @@ class Video:
# deterministic point in time when we can assert that all diffs should
# have been resolved.
# TODO: add flag to enable debug assertions
# 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 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
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
# If we run out of things to do, pad forever
content = target.page_offset[0, 0]