def test_diff_weights(self): fs = frame_grabber.FrameGrabber(mode=video_mode.VideoMode.DHGR) v = video.Video(fs, ticks_per_second=10000., mode=video_mode.VideoMode.DHGR) frame = screen.MemoryMap(screen_page=1) frame.page_offset[0, 0] = 0b1111111 frame.page_offset[0, 1] = 0b1010101 target_pixelmap = screen.DHGRBitmap(palette=palette.Palette.NTSC, main_memory=v.memory_map, aux_memory=frame) self.assertEqual(0b0000000000101010100000001111111000, target_pixelmap.packed[0, 0]) pal = palette.NTSCPalette diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) # Expect byte 0 to map to 0b0001111111000 expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0001111111000] # Expect byte 2 to map to 0b0001010101000 expect2 = target_pixelmap.edit_distances(pal.ID)[2][0b0001010101000] self.assertEqual(expect0, diff[0, 0]) self.assertEqual(expect2, diff[0, 1]) # Update aux frame v.aux_memory_map.page_offset = frame.page_offset v.pixelmap._pack() self.assertEqual(0b0000000000101010100000001111111000, v.pixelmap.packed[0, 0]) # Encode new aux frame frame = screen.MemoryMap(screen_page=1) frame.page_offset[0, 0] = 0b1101101 frame.page_offset[0, 1] = 0b0110110 target_pixelmap = screen.DHGRBitmap(main_memory=v.memory_map, aux_memory=frame, palette=pal.ID) self.assertEqual(0b0000000000011011000000001101101000, target_pixelmap.packed[0, 0]) diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) # Masked offset 0 changes from 0001111111000 to 0001101101000 expect0 = target_pixelmap.edit_distances( pal.ID)[0][0b00011111110000001101101000] # Masked offset 2 changes from 0001010101000 to 0000110110000 expect2 = target_pixelmap.edit_distances( pal.ID)[2][0b00010101010000000110110000] self.assertEqual(expect0, diff[0, 0]) self.assertEqual(expect2, diff[0, 1])
def __init__(self, frame_grabber: FrameGrabber, ticks_per_second: float, mode: VideoMode = VideoMode.HGR, palette: Palette = Palette.NTSC): self.mode = mode # type: VideoMode 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) # type: float self.frame_number = 0 # type: int self.palette = palette # type: Palette # 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 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, ) # Accumulates pending edit weights across frames self.update_priority = np.zeros((32, 256), dtype=np.int) if self.mode == mode.DHGR: self.aux_update_priority = np.zeros((32, 256), dtype=np.int)
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 dhgr = screen.DHGRBitmap( main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) self.assertEqual( 0b0001000011111010110000111110101000, dhgr.packed[0, 1] ) # Check footer on neighbouring byte self.assertEqual( 0b1010000000000000000000000000000000, dhgr.packed[0, 0] ) # Check header on neighbouring byte self.assertEqual( 0b0000000000000000000000000000000100, dhgr.packed[0, 2] ) # No other entries should be set self.assertEqual(3, np.count_nonzero(dhgr.packed))
def test_fix_array_neighbours(self): """Test that _fix_array_neighbours DTRT after masked_update.""" dhgr = screen.DHGRBitmap( main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) packed = dhgr.masked_update(0, dhgr.packed, np.uint8(0x7f)) dhgr._fix_array_neighbours(packed, 0) # Should propagate to all footers self.assertEqual( 0, np.count_nonzero( packed[packed != 0b1110000000000000000000001111111000] ) ) # Should not change headers/footers packed = dhgr.masked_update(1, packed, np.uint8(0b1010101)) dhgr._fix_array_neighbours(packed, 1) self.assertEqual( 0, np.count_nonzero( packed[packed != 0b1110000000000000010101011111111000] ) ) # Should propagate to all headers packed = dhgr.masked_update(3, packed, np.uint8(0b0110110)) dhgr._fix_array_neighbours(packed, 3) self.assertEqual( 0, np.count_nonzero( packed[packed != 0b1110110110000000010101011111111011] ) )
def test_mask_and_shift_data(self): """Verify that mask_and_shift_data extracts the right bit positions.""" int13_max = np.uint64(2 ** 13 - 1) int34_max = np.uint64(2 ** 34 - 1) dhgr = screen.DHGRBitmap( main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) for o in range(3): self.assertEqual( int13_max, dhgr.mask_and_shift_data( screen.DHGRBitmap.BYTE_MASKS[o], o ) ) # Now check complement, i.e. no bits taken from outside expected # range self.assertEqual( 0, dhgr.mask_and_shift_data( ~screen.DHGRBitmap.BYTE_MASKS[o] & int34_max, o ) )
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) self.assertEqual( 0b0001000011111010110000111110101000, dhgr.packed[0, 127] ) # Check footer on neighbouring byte self.assertEqual( 0b1010000000000000000000000000000000, dhgr.packed[0, 126] ) # No other entries should be set, in particular header should not # propagate to next row self.assertEqual(2, np.count_nonzero(dhgr.packed))
def _index_changes(self, source: screen.MemoryMap, target: screen.MemoryMap, update_priority: np.array, is_aux: True) -> Iterator[Tuple[int, int, List[int]]]: """Transform encoded screen to sequence of change tuples.""" if self.mode == VideoMode.DHGR: if is_aux: target_pixelmap = screen.DHGRBitmap( main_memory=self.memory_map, aux_memory=target, palette=self.palette) else: target_pixelmap = screen.DHGRBitmap( main_memory=target, aux_memory=self.aux_memory_map, palette=self.palette) else: target_pixelmap = screen.HGRBitmap(main_memory=target, palette=self.palette) 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])
def test_apply(self): """Test that apply() correctly updates neighbours.""" dhgr = screen.DHGRBitmap( main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) dhgr.apply(page=0, offset=0, is_aux=True, value=np.uint8(0xff)) self.assertEqual(0b1111111000, dhgr.packed[0, 0]) dhgr.apply(page=12, offset=36, is_aux=True, value=np.uint8(0xff)) # Neighbouring header self.assertEqual( 0, dhgr.packed[12, 19]) # Body self.assertEqual( 0b1111111000, dhgr.packed[12, 18]) # Neighbouring footer self.assertEqual( 0b1110000000000000000000000000000000, dhgr.packed[12, 17]) # Now update the next aux offset in same uint64 dhgr.apply(page=12, offset=37, is_aux=True, value=np.uint8(0xff)) # Neighbouring header self.assertEqual( 0, dhgr.packed[12, 19]) # Body self.assertEqual( 0b0000000111111100000001111111000, dhgr.packed[12, 18] ) # Neighbouring footer self.assertEqual( 0b1110000000000000000000000000000000, dhgr.packed[12, 17]) # Update offset 3, should propagate to next header dhgr.apply(page=12, offset=37, is_aux=False, value=np.uint8(0b1010101)) self.assertEqual( 0b101, dhgr.packed[12, 19]) self.assertEqual( 0b1010101111111100000001111111000, dhgr.packed[12, 18] ) self.assertEqual( 0b1110000000000000000000000000000000, dhgr.packed[12, 17]) dhgr.apply(page=12, offset=36, is_aux=False, value=np.uint8(0b0001101)) self.assertEqual( 0b101, dhgr.packed[12, 19]) self.assertEqual( 0b1010101111111100011011111111000, dhgr.packed[12, 18] ) self.assertEqual( 0b1110000000000000000000000000000000, dhgr.packed[12, 17]) # Change offset 0, should propagate to neighbouring footer dhgr.apply(page=12, offset=36, is_aux=True, value=np.uint8(0b0001101)) # Neighbouring header self.assertEqual( 0b101, dhgr.packed[12, 19]) self.assertEqual( 0b1010101111111100011010001101000, dhgr.packed[12, 18] ) # Neighbouring footer self.assertEqual( 0b1010000000000000000000000000000000, dhgr.packed[12, 17]) # Now propagate new header from neighbour onto (12, 18) dhgr.apply(page=12, offset=35, is_aux=False, value=np.uint8(0b1010101)) self.assertEqual( 0b1010101111111100011010001101101, dhgr.packed[12, 18] ) # Neighbouring footer self.assertEqual( 0b1011010101000000000000000000000000, dhgr.packed[12, 17])