mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-30 15:29:26 +00:00
Refactor the world
This commit is contained in:
parent
84611ad5e3
commit
36fc34d26d
84
main.py
84
main.py
@ -3,7 +3,9 @@ import skvideo.datasets
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
import opcodes
|
||||
import screen
|
||||
import video
|
||||
|
||||
CYCLES = 1024 * 1024
|
||||
MAX_OUT = 20 * 1024
|
||||
@ -45,113 +47,121 @@ APPLE_FPS = 10
|
||||
# stores=2222, content changes=339, page changes=88
|
||||
|
||||
# Optimized new
|
||||
# Fullness = 1.384560, cycles = 90738/104857 budget
|
||||
# Fullness = 1.384560, cycle_counter = 90738/104857 budget
|
||||
# stores=1872, content changes=15, page changes=352
|
||||
# Frame 0, 2606 bytes, similarity = 0.849219
|
||||
# Fullness = 1.452588, cycles = 110009/104857 budget
|
||||
# Fullness = 1.452588, cycle_counter = 110009/104857 budget
|
||||
# stores=2163, content changes=28, page changes=472
|
||||
# Frame 3, 3163 bytes, similarity = 0.924256
|
||||
# Fullness = 1.577072, cycles = 113843/104857 budget
|
||||
# Fullness = 1.577072, cycle_counter = 113843/104857 budget
|
||||
# stores=2062, content changes=30, page changes=577
|
||||
# Frame 6, 3276 bytes, similarity = 0.939918
|
||||
# Fullness = 1.597466, cycles = 106213/104857 budget
|
||||
# Fullness = 1.597466, cycle_counter = 106213/104857 budget
|
||||
# stores=1899, content changes=29, page changes=550
|
||||
# Frame 9, 3057 bytes, similarity = 0.928274
|
||||
# Fullness = 1.615001, cycles = 106008/104857 budget
|
||||
# Fullness = 1.615001, cycle_counter = 106008/104857 budget
|
||||
# stores=1875, content changes=27, page changes=561
|
||||
# Frame 12, 3051 bytes, similarity = 0.933854
|
||||
# Fullness = 1.639691, cycles = 106460/104857 budget
|
||||
# Fullness = 1.639691, cycle_counter = 106460/104857 budget
|
||||
# stores=1855, content changes=30, page changes=575
|
||||
# Frame 15, 3065 bytes, similarity = 0.929725
|
||||
# Fullness = 1.635406, cycles = 104583/104857 budget
|
||||
# Fullness = 1.635406, cycle_counter = 104583/104857 budget
|
||||
# stores=1827, content changes=30, page changes=562
|
||||
|
||||
# TSP solver
|
||||
# Fullness = 1.336189, cycles = 87568/104857 budget
|
||||
# Fullness = 1.336189, cycle_counter = 87568/104857 budget
|
||||
# stores=1872, content changes=320, page changes=32
|
||||
# Frame 0, 2576 bytes, similarity = 0.849219
|
||||
# Fullness = 1.386065, cycles = 108771/104857 budget
|
||||
# Fullness = 1.386065, cycle_counter = 108771/104857 budget
|
||||
# stores=2242, content changes=452, page changes=33
|
||||
# Frame 3, 3212 bytes, similarity = 0.927604
|
||||
# Fullness = 1.482284, cycles = 112136/104857 budget
|
||||
# Fullness = 1.482284, cycle_counter = 112136/104857 budget
|
||||
# stores=2161, content changes=552, page changes=33
|
||||
# Frame 6, 3331 bytes, similarity = 0.943415
|
||||
# Fullness = 1.501014, cycles = 106182/104857 budget
|
||||
# Fullness = 1.501014, cycle_counter = 106182/104857 budget
|
||||
# stores=2021, content changes=535, page changes=33
|
||||
# Frame 9, 3157 bytes, similarity = 0.934263
|
||||
# Fullness = 1.523818, cycles = 106450/104857 budget
|
||||
# Fullness = 1.523818, cycle_counter = 106450/104857 budget
|
||||
# stores=1995, content changes=554, page changes=33
|
||||
# Frame 12, 3169 bytes, similarity = 0.939844
|
||||
# Fullness = 1.543029, cycles = 106179/104857 budget
|
||||
# Fullness = 1.543029, cycle_counter = 106179/104857 budget
|
||||
# stores=1966, content changes=566, page changes=33
|
||||
# Frame 15, 3164 bytes, similarity = 0.935231
|
||||
# Fullness = 1.538659, cycles = 104560/104857 budget
|
||||
# Fullness = 1.538659, cycle_counter = 104560/104857 budget
|
||||
# stores=1941, content changes=554, page changes=33
|
||||
|
||||
# page first
|
||||
# Fullness = 1.366463, cycles = 89552/104857 budget
|
||||
# Fullness = 1.366463, cycle_counter = 89552/104857 budget
|
||||
# stores=1872, content changes=352, page changes=32
|
||||
# Frame 0, 2640 bytes, similarity = 0.849219
|
||||
# Fullness = 1.413155, cycles = 108440/104857 budget
|
||||
# Fullness = 1.413155, cycle_counter = 108440/104857 budget
|
||||
# stores=2192, content changes=476, page changes=32
|
||||
# Frame 3, 3208 bytes, similarity = 0.925744
|
||||
# Fullness = 1.516888, cycles = 112554/104857 budget
|
||||
# Fullness = 1.516888, cycle_counter = 112554/104857 budget
|
||||
# stores=2120, content changes=583, page changes=32
|
||||
# Frame 6, 3350 bytes, similarity = 0.942187
|
||||
# Fullness = 1.535086, cycles = 106115/104857 budget
|
||||
# Fullness = 1.535086, cycle_counter = 106115/104857 budget
|
||||
# stores=1975, content changes=561, page changes=32
|
||||
# Frame 9, 3161 bytes, similarity = 0.932106
|
||||
# Fullness = 1.553913, cycles = 106143/104857 budget
|
||||
# Fullness = 1.553913, cycle_counter = 106143/104857 budget
|
||||
# stores=1951, content changes=575, page changes=32
|
||||
# Frame 12, 3165 bytes, similarity = 0.937835
|
||||
# Fullness = 1.571548, cycles = 106047/104857 budget
|
||||
# Fullness = 1.571548, cycle_counter = 106047/104857 budget
|
||||
# stores=1927, content changes=587, page changes=32
|
||||
# Frame 15, 3165 bytes, similarity = 0.933259
|
||||
# Fullness = 1.572792, cycles = 104940/104857 budget
|
||||
# Fullness = 1.572792, cycle_counter = 104940/104857 budget
|
||||
# stores=1906, content changes=581, page changes=32
|
||||
|
||||
def main():
|
||||
s = screen.Screen()
|
||||
|
||||
decoder = screen.Screen()
|
||||
s = video.Video()
|
||||
|
||||
videogen = skvideo.io.vreader("CoffeeCup-H264-75.mov")
|
||||
with open("out.bin", "wb") as out:
|
||||
bytes_out = 0
|
||||
|
||||
# Estimated opcode overhead, i.e. ratio of extra cycles from opcodes
|
||||
# Estimated opcode overhead, i.e. ratio of extra cycle_counter from
|
||||
# opcodes
|
||||
fullness = 1.6
|
||||
|
||||
screen_cls = screen.HGRBitmap
|
||||
|
||||
# Assert that the opcode stream reconstructs the same screen
|
||||
ds = video.Video()
|
||||
decoder = opcodes.Decoder(ds.state)
|
||||
|
||||
for idx, frame in enumerate(videogen):
|
||||
if idx % (VIDEO_FPS // APPLE_FPS):
|
||||
continue
|
||||
|
||||
im = Image.fromarray(frame)
|
||||
im = im.resize((screen.Frame.XMAX, screen.Frame.YMAX))
|
||||
im = im.resize((screen_cls.XMAX, screen_cls.YMAX))
|
||||
im = im.convert("1")
|
||||
im = np.array(im)
|
||||
# im.show()
|
||||
|
||||
f = screen.Frame(im)
|
||||
f = screen_cls(im)
|
||||
|
||||
cycle_budget = int(CYCLES / APPLE_FPS)
|
||||
stream = bytes(s.update(f, cycle_budget, fullness))
|
||||
|
||||
fullness *= s.cycles / cycle_budget
|
||||
print("Fullness = %f, cycles = %d/%d budget" % (
|
||||
fullness, s.cycles, cycle_budget))
|
||||
#print(" ".join("%02x(%02d)" % (b, b) for b in stream))
|
||||
|
||||
fullness *= s.cycle_counter.cycles / cycle_budget
|
||||
print("Fullness = %f, cycle_counter = %d/%d budget" % (
|
||||
fullness, s.cycle_counter.cycles, cycle_budget))
|
||||
|
||||
# Assert that the opcode stream reconstructs the same screen
|
||||
(num_content_stores, num_content_changes,
|
||||
num_page_changes, num_rle_bytes) = decoder.from_stream(iter(
|
||||
stream))
|
||||
assert np.array_equal(decoder.screen, s.screen)
|
||||
num_page_changes, num_rle_bytes) = decoder.decode_stream(
|
||||
iter(stream))
|
||||
assert np.array_equal(ds.screen.bytemap, s.screen.bytemap), (
|
||||
ds.screen.bytemap ^ s.screen.bytemap)
|
||||
print("stores=%d, content changes=%d, page changes=%d, "
|
||||
"rle_bytes=%d" % (
|
||||
num_content_stores, num_content_changes,
|
||||
num_page_changes, num_rle_bytes))
|
||||
|
||||
# print(" ".join("%02x(%02d)" % (b, b) for b in stream))
|
||||
# assert that the screen decodes to the original bitmap
|
||||
bm = s.to_bitmap()
|
||||
bm = screen_cls.from_bytemap(s.screen).bitmap
|
||||
|
||||
# print(np.array(im)[0:5,0:5])
|
||||
# print(bm[0:5,0:5])
|
||||
@ -171,7 +181,7 @@ def main():
|
||||
break
|
||||
|
||||
print("Frame %d, %d bytes, similarity = %f" % (
|
||||
idx, len(stream), s.similarity(im, bm)))
|
||||
idx, len(stream), screen.bitmap_similarity(im, bm)))
|
||||
out.write(stream)
|
||||
|
||||
out.write(bytes(s.done()))
|
||||
|
54
memory_map.py
Normal file
54
memory_map.py
Normal file
@ -0,0 +1,54 @@
|
||||
from typing import Tuple
|
||||
|
||||
import screen
|
||||
|
||||
|
||||
def y_to_base_addr(y: int, page: int = 0) -> int:
|
||||
"""Maps y coordinate to base address on given screen page"""
|
||||
a = y // 64
|
||||
d = y - 64 * a
|
||||
b = d // 8
|
||||
c = d - 8 * b
|
||||
|
||||
addr = 8192 * (page + 1) + 1024 * c + 128 * b + 40 * a
|
||||
return addr
|
||||
|
||||
|
||||
class MemoryMap:
|
||||
"""Memory map representing screen memory."""
|
||||
|
||||
# TODO: support DHGR
|
||||
|
||||
Y_TO_BASE_ADDR = [
|
||||
[y_to_base_addr(y, screen_page) for y in range(192)]
|
||||
for screen_page in (0, 1)
|
||||
]
|
||||
|
||||
ADDR_TO_COORDS = {}
|
||||
for p in range(2):
|
||||
for y in range(192):
|
||||
for x in range(40):
|
||||
a = Y_TO_BASE_ADDR[p][y] + x
|
||||
ADDR_TO_COORDS[a] = (p, y, x)
|
||||
|
||||
def __init__(self, screen_page: int, bytemap: screen.Bytemap):
|
||||
self.screen_page = screen_page # type: int
|
||||
self.bytemap = bytemap
|
||||
|
||||
def to_page_offset(self, x_byte: int, y: int) -> Tuple[int, int]:
|
||||
y_base = self.Y_TO_BASE_ADDR[self.screen_page][y]
|
||||
page = y_base >> 8
|
||||
|
||||
# print("y=%d -> page=%02x" % (y, page))
|
||||
offset = y_base - (page << 8) + x_byte
|
||||
return page, offset
|
||||
|
||||
def write(self, addr: int, val: int) -> None:
|
||||
"""Updates screen image to set 0xaddr ^= val"""
|
||||
try:
|
||||
_, y, x = self.ADDR_TO_COORDS[addr]
|
||||
except KeyError:
|
||||
# TODO: filter out screen holes
|
||||
# print("Attempt to write to invalid offset %04x" % addr)
|
||||
return
|
||||
self.bytemap.bytemap[y][x] = val
|
194
opcodes.py
Normal file
194
opcodes.py
Normal file
@ -0,0 +1,194 @@
|
||||
import enum
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
import memory_map
|
||||
|
||||
|
||||
class CycleCounter:
|
||||
def __init__(self):
|
||||
self.cycles = 0 # type:int
|
||||
|
||||
def tick(self, cycles: int) -> None:
|
||||
self.cycles += cycles
|
||||
|
||||
def reset(self) -> None:
|
||||
self.cycles = 0
|
||||
|
||||
|
||||
class State:
|
||||
"""Represents virtual machine state."""
|
||||
|
||||
def __init__(self, cycle_counter: CycleCounter,
|
||||
memmap: memory_map.MemoryMap):
|
||||
self.page = 0x20
|
||||
self.content = 0x7f
|
||||
|
||||
self.memmap = memmap
|
||||
self.cycle_counter = cycle_counter
|
||||
|
||||
def emit(self, opcode: "Opcode") -> Iterator[int]:
|
||||
cmd = opcode.emit_command()
|
||||
if cmd:
|
||||
yield from cmd
|
||||
data = opcode.emit_data()
|
||||
if data:
|
||||
yield from data
|
||||
|
||||
# Update changes in memory map, if any
|
||||
opcode.apply(self)
|
||||
|
||||
# Tick 6502 CPU
|
||||
self.cycle_counter.tick(opcode.cycles)
|
||||
|
||||
|
||||
class OpcodeCommand(enum.Enum):
|
||||
OFFSET = 0x00
|
||||
SET_CONTENT = 0xfb # set new data byte to write
|
||||
SET_PAGE = 0xfc
|
||||
RLE = 0xfd
|
||||
TICK = 0xfe # tick speaker
|
||||
TERMINATE = 0xff
|
||||
|
||||
|
||||
class Opcode:
|
||||
COMMAND = None # type: OpcodeCommand
|
||||
_CYCLES = None # type: int
|
||||
|
||||
@property
|
||||
def cycles(self) -> int:
|
||||
return self._CYCLES
|
||||
|
||||
def emit_command(self) -> Iterator[int]:
|
||||
yield self.COMMAND.value
|
||||
|
||||
def emit_data(self) -> Iterator[int]:
|
||||
return
|
||||
|
||||
def apply(self, state: State):
|
||||
pass
|
||||
|
||||
|
||||
class Offset(Opcode):
|
||||
COMMAND = OpcodeCommand.OFFSET
|
||||
_CYCLES = 36
|
||||
|
||||
def __init__(self, offset: int):
|
||||
if offset < 0 or offset >255:
|
||||
raise ValueError("Invalid offset: %d" % offset)
|
||||
|
||||
self.offset = offset
|
||||
|
||||
def emit_command(self):
|
||||
return
|
||||
|
||||
def emit_data(self):
|
||||
yield self.offset
|
||||
|
||||
def apply(self, state):
|
||||
state.memmap.write(state.page << 8 | self.offset, state.content)
|
||||
|
||||
|
||||
class SetPage(Opcode):
|
||||
COMMAND = OpcodeCommand.SET_PAGE
|
||||
_CYCLES = 73
|
||||
|
||||
def __init__(self, page:int):
|
||||
self.page = page
|
||||
|
||||
def emit_data(self):
|
||||
yield self.page
|
||||
|
||||
def apply(self, state: State):
|
||||
state.page = self.page
|
||||
|
||||
|
||||
class SetContent(Opcode):
|
||||
COMMAND = OpcodeCommand.SET_CONTENT
|
||||
_CYCLES = 62
|
||||
|
||||
def __init__(self, content: int):
|
||||
self.content = content
|
||||
|
||||
def emit_data(self):
|
||||
yield self.content
|
||||
|
||||
def apply(self, state: State):
|
||||
state.content = self.content
|
||||
|
||||
|
||||
class RLE(Opcode):
|
||||
COMMAND = OpcodeCommand.RLE
|
||||
|
||||
def __init__(self, start_offset: int, run_length: int):
|
||||
self.start_offset = start_offset
|
||||
self.run_length = run_length
|
||||
|
||||
def emit_data(self):
|
||||
yield self.start_offset
|
||||
yield self.run_length
|
||||
|
||||
@property
|
||||
def cycles(self):
|
||||
return 98 + 9 * self.run_length
|
||||
|
||||
def apply(self, state):
|
||||
for i in range(self.run_length):
|
||||
state.memmap.write(
|
||||
state.page << 8 | ((self.start_offset + i) & 0xff),
|
||||
state.content
|
||||
)
|
||||
|
||||
|
||||
class Tick(Opcode):
|
||||
COMMAND = OpcodeCommand.TICK
|
||||
_CYCLES = 50
|
||||
|
||||
|
||||
class Terminate(Opcode):
|
||||
COMMAND = OpcodeCommand.TERMINATE
|
||||
_CYCLES = 50
|
||||
|
||||
|
||||
class Decoder:
|
||||
def __init__(self, state: State):
|
||||
self.state = state # type: State
|
||||
|
||||
def decode_stream(self, stream: Iterator[int]) -> Tuple[int, int, int, int]:
|
||||
"""Replay an opcode stream to build a screen image."""
|
||||
num_content_changes = 0
|
||||
num_page_changes = 0
|
||||
num_content_stores = 0
|
||||
num_rle_bytes = 0
|
||||
|
||||
terminate = False
|
||||
for b in stream:
|
||||
if b == OpcodeCommand.SET_CONTENT.value:
|
||||
content = next(stream)
|
||||
op = SetContent(content)
|
||||
num_content_changes += 1
|
||||
elif b == OpcodeCommand.SET_PAGE.value:
|
||||
page = next(stream)
|
||||
op = SetPage(page)
|
||||
num_page_changes += 1
|
||||
elif b == OpcodeCommand.RLE.value:
|
||||
offset = next(stream)
|
||||
run_length = next(stream)
|
||||
num_rle_bytes += run_length
|
||||
op = RLE(offset, run_length)
|
||||
elif b == OpcodeCommand.TICK.value:
|
||||
op = Tick()
|
||||
elif b == OpcodeCommand.TERMINATE.value:
|
||||
op = Terminate()
|
||||
terminate = True
|
||||
else:
|
||||
op = Offset(b)
|
||||
num_content_stores += 1
|
||||
|
||||
op.apply(self.state)
|
||||
if terminate:
|
||||
break
|
||||
|
||||
return (
|
||||
num_content_stores, num_content_changes, num_page_changes,
|
||||
num_rle_bytes
|
||||
)
|
125
scheduler.py
Normal file
125
scheduler.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""Opcode schedulers."""
|
||||
|
||||
from typing import Iterator
|
||||
|
||||
import opcodes
|
||||
|
||||
|
||||
class OpcodeScheduler:
|
||||
def schedule(self, changes) -> Iterator[opcodes.Opcode]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HeuristicPageFirstScheduler(OpcodeScheduler):
|
||||
"""Group by page first then content byte."""
|
||||
|
||||
def schedule(self, changes):
|
||||
data = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content, run_length = ch
|
||||
data.setdefault(page, {}).setdefault(content, set()).add(
|
||||
(run_length, offset))
|
||||
|
||||
for page, content_offsets in data.items():
|
||||
yield opcodes.SetPage(page)
|
||||
for content, offsets in content_offsets.items():
|
||||
yield opcodes.SetContent(content)
|
||||
|
||||
# print("page %d content %d offsets %s" % (page, content,
|
||||
# offsets))
|
||||
for (run_length, offset) in sorted(offsets, reverse=True):
|
||||
if run_length > 1:
|
||||
# print("Offset %d run length %d" % (
|
||||
# offset, run_length))
|
||||
yield opcodes.RLE(offset, run_length)
|
||||
else:
|
||||
yield opcodes.Offset(offset)
|
||||
|
||||
#
|
||||
# def _tsp_opcode_scheduler(self, changes):
|
||||
# # Build distance matrix for pairs of changes based on number of
|
||||
# # opcodes it would cost for opcodes to emit target change given source
|
||||
#
|
||||
# dist = np.zeros(shape=(len(changes), len(changes)), dtype=np.int)
|
||||
# for i1, ch1 in enumerate(changes):
|
||||
# _, page1, _, content1 = ch1
|
||||
# for i2, ch2 in enumerate(changes):
|
||||
# if ch1 == ch2:
|
||||
# continue
|
||||
# _, page2, _, content2 = ch2
|
||||
#
|
||||
# cost = self.CYCLES[0] # Emit the target content byte
|
||||
# if page1 != page2:
|
||||
# cost += self.CYCLES[OpcodeCommand.SET_PAGE]
|
||||
# if content1 != content2:
|
||||
# cost += self.CYCLES[OpcodeCommand.SET_CONTENT]
|
||||
#
|
||||
# dist[i1][i2] = cost
|
||||
# dist[i2][i1] = cost
|
||||
#
|
||||
# def create_distance_callback(dist_matrix):
|
||||
# # Create a callback to calculate distances between cities.
|
||||
#
|
||||
# def distance_callback(from_node, to_node):
|
||||
# return int(dist_matrix[from_node][to_node])
|
||||
#
|
||||
# return distance_callback
|
||||
#
|
||||
# routing = pywrapcp.RoutingModel(len(changes), 1, 0)
|
||||
# search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
|
||||
# # Create the distance callback.
|
||||
# dist_callback = create_distance_callback(dist)
|
||||
# routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
|
||||
#
|
||||
# assignment = routing.SolveWithParameters(search_parameters)
|
||||
# if assignment:
|
||||
# # Solution distance.
|
||||
# print("Total cycle_counter: " + str(assignment.ObjectiveValue()))
|
||||
# # Display the solution.
|
||||
# # Only one route here; otherwise iterate from 0 to
|
||||
# # routing.vehicles() - 1
|
||||
# route_number = 0
|
||||
# index = routing.Start(
|
||||
# route_number) # Index of the variable for the starting node.
|
||||
# page = 0x20
|
||||
# content = 0x7f
|
||||
# # TODO: I think this will end by visiting the origin node which
|
||||
# # is not what we want
|
||||
# while not routing.IsEnd(index):
|
||||
# _, new_page, offset, new_content = changes[index]
|
||||
#
|
||||
# if new_page != page:
|
||||
# page = new_page
|
||||
# yield self._emit(OpcodeCommand.SET_PAGE)
|
||||
# yield page
|
||||
#
|
||||
# if new_content != content:
|
||||
# content = new_content
|
||||
# yield self._emit(OpcodeCommand.SET_CONTENT)
|
||||
# yield content
|
||||
#
|
||||
# self._write(page << 8 | offset, content)
|
||||
# yield self._emit(offset)
|
||||
#
|
||||
# index = assignment.Value(routing.NextVar(index))
|
||||
# else:
|
||||
# raise ValueError('No solution found.')
|
||||
#
|
||||
# def _heuristic_opcode_scheduler(self, changes):
|
||||
# # Heuristic: group by content byte first then page
|
||||
# data = {}
|
||||
# for ch in changes:
|
||||
# xor_weight, page, offset, content = ch
|
||||
# data.setdefault(content, {}).setdefault(page, set()).add(offset)
|
||||
#
|
||||
# for content, page_offsets in data.items():
|
||||
# yield self._emit(OpcodeCommand.SET_CONTENT)
|
||||
# yield content
|
||||
# for page, offsets in page_offsets.items():
|
||||
# yield self._emit(OpcodeCommand.SET_PAGE)
|
||||
# yield page
|
||||
#
|
||||
# for offset in offsets:
|
||||
# self._write(page << 8 | offset, content)
|
||||
# yield self._emit(offset)
|
||||
#
|
483
screen.py
483
screen.py
@ -1,100 +1,34 @@
|
||||
"""Screen module represents Apple II video display."""
|
||||
|
||||
from collections import defaultdict
|
||||
import functools
|
||||
import enum
|
||||
from typing import List, Set, Iterator, Union, Tuple
|
||||
|
||||
from ortools.constraint_solver import pywrapcp
|
||||
from ortools.constraint_solver import routing_enums_pb2
|
||||
# from ortools.constraint_solver import pywrapcp
|
||||
# from ortools.constraint_solver import routing_enums_pb2
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def hamming_weight(n: int) -> int:
|
||||
"""Compute hamming weight of 8-bit int"""
|
||||
n = (n & 0x55) + ((n & 0xAA) >> 1)
|
||||
n = (n & 0x33) + ((n & 0xCC) >> 2)
|
||||
n = (n & 0x0F) + ((n & 0xF0) >> 4)
|
||||
return n
|
||||
def bitmap_similarity(a1: np.array, a2: np.array) -> float:
|
||||
"""Measure bitwise % similarity between two bitmap arrays"""
|
||||
bits_different = np.asscalar(np.sum(np.logical_xor(a1, a2)))
|
||||
|
||||
return 1 - (bits_different / (np.shape(a1)[0] * np.shape(a1)[1]))
|
||||
|
||||
|
||||
def y_to_base_addr(y: int, page: int = 0) -> int:
|
||||
"""Maps y coordinate to base address on given screen page"""
|
||||
a = y // 64
|
||||
d = y - 64 * a
|
||||
b = d // 8
|
||||
c = d - 8 * b
|
||||
class Bytemap:
|
||||
"""Bitmap array with horizontal pixels packed into bytes."""
|
||||
|
||||
addr = 8192 * (page + 1) + 1024 * c + 128 * b + 40 * a
|
||||
return addr
|
||||
def __init__(self, bitmap: np.array):
|
||||
self.ymax = bitmap.shape[0] # type: int
|
||||
self.xmax = bitmap.shape[1] # type: int
|
||||
if self.xmax % 7 != 0:
|
||||
raise ValueError(
|
||||
"Bitmap x dimension not divisible by 7: %d" % self.xmax)
|
||||
|
||||
self._unpacked_bitmap = bitmap
|
||||
|
||||
# TODO: fill out other byte opcodes
|
||||
class Opcode(enum.Enum):
|
||||
SET_CONTENT = 0xfb # set new data byte to write
|
||||
SET_PAGE = 0xfc
|
||||
RLE = 0xfd
|
||||
TICK = 0xfe # tick speaker
|
||||
END_FRAME = 0xff
|
||||
self.bytemap = self._pack() # type: np.array
|
||||
|
||||
|
||||
class Frame:
|
||||
"""Bitmapped screen frame."""
|
||||
|
||||
XMAX = 280 # 140 # double-wide pixels to not worry about colour
|
||||
# effects
|
||||
YMAX = 192
|
||||
|
||||
def __init__(self, bitmap: np.array = None):
|
||||
if bitmap is None:
|
||||
self.bitmap = np.zeros((self.YMAX, self.XMAX), dtype=bool)
|
||||
else:
|
||||
self.bitmap = bitmap
|
||||
|
||||
def randomize(self):
|
||||
self.bitmap = np.random.randint(
|
||||
2, size=(self.YMAX, self.XMAX), dtype=bool)
|
||||
|
||||
|
||||
class Screen:
|
||||
"""Apple II screen memory map encoding a bitmapped frame."""
|
||||
|
||||
Y_TO_BASE_ADDR = [
|
||||
[y_to_base_addr(y, page) for y in range(192)] for page in (0, 1)
|
||||
]
|
||||
|
||||
ADDR_TO_COORDS = {}
|
||||
for p in range(2):
|
||||
for y in range(192):
|
||||
for x in range(40):
|
||||
a = Y_TO_BASE_ADDR[p][y] + x
|
||||
ADDR_TO_COORDS[a] = (p, y, x)
|
||||
|
||||
CYCLES = defaultdict(lambda: 36) # fast-path cycle count
|
||||
CYCLES.update({
|
||||
Opcode.SET_CONTENT: 62,
|
||||
Opcode.SET_PAGE: 73,
|
||||
Opcode.RLE: 98, # + 9 * N
|
||||
Opcode.TICK: 50,
|
||||
Opcode.END_FRAME: 50
|
||||
})
|
||||
|
||||
def __init__(self, page: int = 0):
|
||||
self.screen = self._encode(Frame().bitmap) # initialize empty
|
||||
self.page = page
|
||||
self.cycles = 0
|
||||
|
||||
@staticmethod
|
||||
def _encode(bitmap: np.array) -> np.array:
|
||||
"""Encode bitmapped screen as apple II memory map.
|
||||
|
||||
Rows are y-coordinates, Columns are byte-packed x-values
|
||||
"""
|
||||
|
||||
# Double each pixel horizontally
|
||||
pixels = bitmap # np.repeat(bitmap, 2, axis=1)
|
||||
def _pack(self) -> np.array:
|
||||
pixels = self._unpacked_bitmap
|
||||
|
||||
# Insert zero column after every 7
|
||||
for i in range(pixels.shape[1] // 7 - 1, -1, -1):
|
||||
@ -104,299 +38,9 @@ class Screen:
|
||||
# invert this
|
||||
return np.flip(np.packbits(np.flip(pixels, axis=1), axis=1), axis=1)
|
||||
|
||||
def update(self, frame: Frame,
|
||||
cycle_budget: int, fullness: float) -> Iterator[int]:
|
||||
"""Update to match content of frame within provided budget.
|
||||
|
||||
Emits encoded byte stream for rendering the image.
|
||||
|
||||
The byte stream consists of offsets against a selected page (e.g. $20xx)
|
||||
at which to write a selected content byte. Those selections are
|
||||
controlled by special opcodes emitted to the stream
|
||||
|
||||
Opcodes:
|
||||
SET_CONTENT - new byte to write to screen contents
|
||||
SET_PAGE - set new page to offset against (e.g. $20xx)
|
||||
TICK - tick the speaker
|
||||
DONE - terminate the video decoding
|
||||
|
||||
In order to "make room" for these opcodes we make use of the fact that
|
||||
each page has 2 sets of 8-byte "screen holes", at page offsets
|
||||
0x78-0x7f and 0xf8-0xff. Currently we only use the latter range as
|
||||
this allows for efficient matching in the critical path of the decoder.
|
||||
|
||||
We group by offsets from page boundary (cf some other more
|
||||
optimal starting point) because STA (..),y has 1 extra cycle if
|
||||
crossing the page boundary. Though maybe this would be worthwhile if
|
||||
it optimizes the bytestream.
|
||||
"""
|
||||
|
||||
self.cycles = 0
|
||||
# Target screen memory map for new frame
|
||||
target = self._encode(frame.bitmap)
|
||||
|
||||
# Estimate number of opcodes that will end up fitting in the cycle
|
||||
# budget.
|
||||
est_opcodes = int(cycle_budget / fullness / self.CYCLES[0])
|
||||
|
||||
# Sort by highest xor weight and take the estimated number of change
|
||||
# operations
|
||||
changes = list(
|
||||
sorted(self.index_changes(self.screen, target), reverse=True)
|
||||
)[:est_opcodes]
|
||||
|
||||
for b in self._heuristic_page_first_opcode_scheduler(changes):
|
||||
yield b
|
||||
|
||||
def index_changes(self, source: np.array,
|
||||
target: np.array) -> Set[Tuple[int, int, int, int, int]]:
|
||||
"""Transform encoded screen to sequence of change tuples.
|
||||
|
||||
Change tuple is (xor_weight, page, offset, content)
|
||||
"""
|
||||
|
||||
# Compute difference from current frame
|
||||
deltas = np.bitwise_xor(self.screen, target)
|
||||
deltas = np.ma.masked_array(deltas, np.logical_not(deltas))
|
||||
|
||||
changes = set()
|
||||
# TODO: don't use 256 bytes if XMAX is smaller, or we may compute RLE
|
||||
# over the full page!
|
||||
memmap = defaultdict(lambda: [(0, 0, 0)] * 256)
|
||||
|
||||
it = np.nditer(target, flags=['multi_index'])
|
||||
while not it.finished:
|
||||
y, x_byte = it.multi_index
|
||||
|
||||
|
||||
y_base = self.Y_TO_BASE_ADDR[self.page][y]
|
||||
page = y_base >> 8
|
||||
|
||||
# print("y=%d -> page=%02x" % (y, page))
|
||||
offset = y_base - (page << 8) + x_byte
|
||||
|
||||
src_content = source[y][x_byte]
|
||||
target_content = np.asscalar(it[0])
|
||||
|
||||
bits_different = hamming_weight(src_content ^ target_content)
|
||||
|
||||
memmap[page][offset] = (bits_different, src_content, target_content)
|
||||
it.iternext()
|
||||
|
||||
for page, offsets in memmap.items():
|
||||
cur_content = None
|
||||
run_length = 0
|
||||
maybe_run = []
|
||||
for offset, data in enumerate(offsets):
|
||||
bits_different, src_content, target_content = data
|
||||
|
||||
# TODO: allowing odd bit errors introduces colour error
|
||||
if maybe_run and hamming_weight(
|
||||
cur_content ^ target_content) > 2:
|
||||
# End of run
|
||||
|
||||
# Decide if it's worth emitting as a run vs single stores
|
||||
|
||||
# Number of changes in run for which >0 bits differ
|
||||
num_changes = len([c for c in maybe_run if c[0]])
|
||||
run_cost = self.CYCLES[Opcode.RLE] + run_length * 9
|
||||
single_cost = self.CYCLES[0] * num_changes
|
||||
#print("Run of %d cheaper than %d singles" % (
|
||||
# run_length, num_changes))
|
||||
|
||||
# TODO: don't allow too much error to accumulate
|
||||
|
||||
if run_cost < single_cost:
|
||||
# Compute median bit value over run
|
||||
median_bits = np.median(
|
||||
np.vstack(
|
||||
np.unpackbits(
|
||||
np.array(r[3], dtype=np.uint8)
|
||||
)
|
||||
for r in maybe_run
|
||||
), axis=0
|
||||
) > 0.5
|
||||
|
||||
typical_content = np.asscalar(np.packbits(median_bits))
|
||||
|
||||
total_xor = sum(ch[0] for ch in maybe_run)
|
||||
start_offset = maybe_run[0][2]
|
||||
|
||||
change = (total_xor, page, start_offset,
|
||||
typical_content, run_length)
|
||||
# print("Found run of %d * %2x at %2x:%2x" % (
|
||||
# run_length, cur_content, page, offset - run_length)
|
||||
# )
|
||||
#print(maybe_run)
|
||||
#print("change =", change)
|
||||
changes.add(change)
|
||||
else:
|
||||
changes.update(ch for ch in maybe_run if ch[0])
|
||||
maybe_run = []
|
||||
run_length = 0
|
||||
cur_content = target_content
|
||||
|
||||
if cur_content is None:
|
||||
cur_content = target_content
|
||||
|
||||
run_length += 1
|
||||
# if bits_different != 0:
|
||||
# # Only accumulate bytes for which src != target content
|
||||
maybe_run.append(
|
||||
(bits_different, page, offset, target_content, 1))
|
||||
|
||||
return changes
|
||||
|
||||
def _heuristic_page_first_opcode_scheduler(self, changes):
|
||||
# Heuristic: group by page first then content byte
|
||||
data = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content, run_length = ch
|
||||
data.setdefault(page, {}).setdefault(content, set()).add(
|
||||
(run_length, offset))
|
||||
|
||||
for page, content_offsets in data.items():
|
||||
for b in self._emit(Opcode.SET_PAGE, page):
|
||||
yield b
|
||||
for content, offsets in content_offsets.items():
|
||||
for b in self._emit(Opcode.SET_CONTENT, content):
|
||||
yield b
|
||||
|
||||
# print("page %d content %d offsets %s" % (page, content,
|
||||
# offsets))
|
||||
for (run_length, offset) in sorted(offsets, reverse=True):
|
||||
if run_length > 1:
|
||||
# print("Offset %d run length %d" % (offset, run_length))
|
||||
for b in self._emit(Opcode.RLE, offset, run_length):
|
||||
yield b
|
||||
for i in range(run_length):
|
||||
self._write((page << 8 | offset) + i, content)
|
||||
else:
|
||||
for b in self._emit(offset):
|
||||
yield b
|
||||
self._write(page << 8 | offset, content)
|
||||
|
||||
def _tsp_opcode_scheduler(self, changes):
|
||||
# Build distance matrix for pairs of changes based on number of
|
||||
# opcodes it would cost for opcodes to emit target change given source
|
||||
|
||||
dist = np.zeros(shape=(len(changes), len(changes)), dtype=np.int)
|
||||
for i1, ch1 in enumerate(changes):
|
||||
_, page1, _, content1 = ch1
|
||||
for i2, ch2 in enumerate(changes):
|
||||
if ch1 == ch2:
|
||||
continue
|
||||
_, page2, _, content2 = ch2
|
||||
|
||||
cost = self.CYCLES[0] # Emit the target content byte
|
||||
if page1 != page2:
|
||||
cost += self.CYCLES[Opcode.SET_PAGE]
|
||||
if content1 != content2:
|
||||
cost += self.CYCLES[Opcode.SET_CONTENT]
|
||||
|
||||
dist[i1][i2] = cost
|
||||
dist[i2][i1] = cost
|
||||
|
||||
def create_distance_callback(dist_matrix):
|
||||
# Create a callback to calculate distances between cities.
|
||||
|
||||
def distance_callback(from_node, to_node):
|
||||
return int(dist_matrix[from_node][to_node])
|
||||
|
||||
return distance_callback
|
||||
|
||||
routing = pywrapcp.RoutingModel(len(changes), 1, 0)
|
||||
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
|
||||
# Create the distance callback.
|
||||
dist_callback = create_distance_callback(dist)
|
||||
routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
|
||||
|
||||
assignment = routing.SolveWithParameters(search_parameters)
|
||||
if assignment:
|
||||
# Solution distance.
|
||||
print("Total cycles: " + str(assignment.ObjectiveValue()))
|
||||
# Display the solution.
|
||||
# Only one route here; otherwise iterate from 0 to routing.vehicles() - 1
|
||||
route_number = 0
|
||||
index = routing.Start(
|
||||
route_number) # Index of the variable for the starting node.
|
||||
page = 0x20
|
||||
content = 0x7f
|
||||
# TODO: I think this will end by visiting the origin node which
|
||||
# is not what we want
|
||||
while not routing.IsEnd(index):
|
||||
_, new_page, offset, new_content = changes[index]
|
||||
|
||||
if new_page != page:
|
||||
page = new_page
|
||||
yield self._emit(Opcode.SET_PAGE)
|
||||
yield page
|
||||
|
||||
if new_content != content:
|
||||
content = new_content
|
||||
yield self._emit(Opcode.SET_CONTENT)
|
||||
yield content
|
||||
|
||||
self._write(page << 8 | offset, content)
|
||||
yield self._emit(offset)
|
||||
|
||||
index = assignment.Value(routing.NextVar(index))
|
||||
else:
|
||||
raise ValueError('No solution found.')
|
||||
|
||||
def _heuristic_opcode_scheduler(self, changes):
|
||||
# Heuristic: group by content byte first then page
|
||||
data = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content = ch
|
||||
data.setdefault(content, {}).setdefault(page, set()).add(offset)
|
||||
|
||||
for content, page_offsets in data.items():
|
||||
yield self._emit(Opcode.SET_CONTENT)
|
||||
yield content
|
||||
for page, offsets in page_offsets.items():
|
||||
yield self._emit(Opcode.SET_PAGE)
|
||||
yield page
|
||||
|
||||
for offset in offsets:
|
||||
self._write(page << 8 | offset, content)
|
||||
yield self._emit(offset)
|
||||
|
||||
def _emit(self, opcode: Union[Opcode, int], *data) -> List[int]:
|
||||
if opcode == Opcode.RLE:
|
||||
run_length = data[1]
|
||||
self.cycles += 9 * run_length
|
||||
self.cycles += self.CYCLES[opcode]
|
||||
|
||||
opcode_byte = opcode.value if opcode in Opcode else opcode
|
||||
return [opcode_byte] + list(data)
|
||||
|
||||
@staticmethod
|
||||
def similarity(a1: np.array, a2: np.array) -> float:
|
||||
"""Measure bitwise % similarity between two arrays"""
|
||||
bits_different = np.asscalar(np.sum(np.logical_xor(a1, a2)))
|
||||
|
||||
return 1 - (bits_different / (np.shape(a1)[0] * np.shape(a1)[1]))
|
||||
|
||||
def done(self) -> Iterator[int]:
|
||||
"""Terminate opcode stream."""
|
||||
|
||||
for b in self._emit(Opcode.END_FRAME):
|
||||
yield b
|
||||
|
||||
def _write(self, addr: int, val: int) -> None:
|
||||
"""Updates screen image to set 0xaddr ^= val"""
|
||||
try:
|
||||
_, y, x = self.ADDR_TO_COORDS[addr]
|
||||
except KeyError:
|
||||
# TODO: filter out screen holes
|
||||
# print("Attempt to write to invalid offset %04x" % addr)
|
||||
return
|
||||
self.screen[y][x] = val
|
||||
|
||||
def to_bitmap(self) -> np.array:
|
||||
def unpack(self) -> np.array:
|
||||
"""Convert packed screen representation to bitmap."""
|
||||
bm = np.unpackbits(self.screen, axis=1)
|
||||
bm = np.unpackbits(self.bytemap, axis=1)
|
||||
bm = np.delete(bm, np.arange(0, bm.shape[1], 8), axis=1)
|
||||
|
||||
# Need to flip each 7-bit sequence
|
||||
@ -406,44 +50,55 @@ class Screen:
|
||||
reorder_cols.append(j)
|
||||
bm = bm[:, reorder_cols]
|
||||
|
||||
# Undouble pixels
|
||||
# return np.array(np.delete(bm, np.arange(0, bm.shape[1], 2), axis=1),
|
||||
# dtype=np.bool)
|
||||
return np.array(bm, dtype=np.bool)
|
||||
|
||||
def from_stream(self, stream: Iterator[int]) -> Tuple[int, int, int]:
|
||||
"""Replay an opcode stream to build a screen image."""
|
||||
page = 0x20
|
||||
content = 0x7f
|
||||
num_content_changes = 0
|
||||
num_page_changes = 0
|
||||
num_content_stores = 0
|
||||
num_rle_bytes = 0
|
||||
for b in stream:
|
||||
if b == Opcode.SET_CONTENT.value:
|
||||
content = next(stream)
|
||||
num_content_changes += 1
|
||||
continue
|
||||
elif b == Opcode.SET_PAGE.value:
|
||||
page = next(stream)
|
||||
num_page_changes += 1
|
||||
continue
|
||||
elif b == Opcode.RLE.value:
|
||||
offset = next(stream)
|
||||
rle = next(stream)
|
||||
num_rle_bytes += rle
|
||||
for i in range(rle):
|
||||
self._write(page << 8 | ((offset + i) & 0xff), content)
|
||||
continue
|
||||
elif b == Opcode.TICK.value:
|
||||
continue
|
||||
elif b == Opcode.END_FRAME.value:
|
||||
break
|
||||
|
||||
num_content_stores += 1
|
||||
self._write(page << 8 | b, content)
|
||||
class Bitmap:
|
||||
XMAX = None # type: int
|
||||
YMAX = None # type: int
|
||||
|
||||
return (
|
||||
num_content_stores, num_content_changes, num_page_changes,
|
||||
num_rle_bytes
|
||||
)
|
||||
def __init__(self, bitmap: np.array = None):
|
||||
if bitmap is None:
|
||||
self.bitmap = np.zeros((self.YMAX, self.XMAX), dtype=bool)
|
||||
else:
|
||||
self.bitmap = bitmap
|
||||
|
||||
def randomize(self) -> None:
|
||||
self.bitmap = np.random.randint(
|
||||
2, size=(self.YMAX, self.XMAX), dtype=bool)
|
||||
|
||||
def pack(self):
|
||||
return Bytemap(self.bitmap)
|
||||
|
||||
@classmethod
|
||||
def from_bytemap(cls, bytemap: Bytemap):
|
||||
return cls(bytemap.unpack())
|
||||
|
||||
|
||||
class HGR140Bitmap(Bitmap):
|
||||
XMAX = 140 # double-wide pixels to not worry about colour effects
|
||||
YMAX = 192
|
||||
|
||||
def pack(self):
|
||||
# Double each pixel horizontally
|
||||
return Bytemap(np.repeat(self.bitmap, 2, axis=1))
|
||||
|
||||
@classmethod
|
||||
def from_bytemap(cls, bytemap: Bytemap):
|
||||
# Undouble pixels
|
||||
bm = bytemap.unpack()
|
||||
return cls(
|
||||
np.array(
|
||||
np.delete(bm, np.arange(0, bm.shape[1], 2), axis=1),
|
||||
dtype=np.bool
|
||||
))
|
||||
|
||||
|
||||
class HGRBitmap(Bitmap):
|
||||
XMAX = 280
|
||||
YMAX = 192
|
||||
|
||||
|
||||
class DHGRBitmap(Bitmap):
|
||||
XMAX = 560
|
||||
YMAX = 192
|
||||
|
26
server.py
Normal file
26
server.py
Normal file
@ -0,0 +1,26 @@
|
||||
import socketserver
|
||||
|
||||
ADDR = "192.168.1.15"
|
||||
PORT = 20000
|
||||
|
||||
|
||||
class ChunkHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
print("sending %d" % i)
|
||||
self.request.sendall(bytes([i % (128 - 32) + 32] * 256))
|
||||
|
||||
|
||||
def main():
|
||||
with socketserver.TCPServer(
|
||||
(ADDR, PORT), ChunkHandler, bind_and_activate=False) as server:
|
||||
server.allow_reuse_address = True
|
||||
server.server_bind()
|
||||
server.server_activate()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
180
video.py
Normal file
180
video.py
Normal file
@ -0,0 +1,180 @@
|
||||
import functools
|
||||
from collections import defaultdict
|
||||
from typing import Iterator, Set, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
import opcodes
|
||||
import scheduler
|
||||
import memory_map
|
||||
import screen
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def hamming_weight(n: int) -> int:
|
||||
"""Compute hamming weight of 8-bit int"""
|
||||
n = (n & 0x55) + ((n & 0xAA) >> 1)
|
||||
n = (n & 0x33) + ((n & 0xCC) >> 2)
|
||||
n = (n & 0x0F) + ((n & 0xF0) >> 4)
|
||||
return n
|
||||
|
||||
|
||||
class Video:
|
||||
"""Apple II screen memory map encoding a bitmapped frame."""
|
||||
|
||||
def __init__(self, screen_page: int = 0,
|
||||
opcode_scheduler: scheduler.OpcodeScheduler = None):
|
||||
self.screen_page = screen_page
|
||||
|
||||
# Initialize empty
|
||||
self.screen = screen.HGRBitmap().pack() # type: screen.Bytemap
|
||||
|
||||
self.memory_map = memory_map.MemoryMap(screen_page, self.screen)
|
||||
|
||||
self.cycle_counter = opcodes.CycleCounter()
|
||||
self.state = opcodes.State(self.cycle_counter, self.memory_map)
|
||||
|
||||
self.scheduler = (
|
||||
opcode_scheduler or scheduler.HeuristicPageFirstScheduler())
|
||||
|
||||
def update(self, frame: screen.Bitmap,
|
||||
cycle_budget: int, fullness: float) -> Iterator[int]:
|
||||
"""Update to match content of frame within provided budget.
|
||||
|
||||
Emits encoded byte stream for rendering the image.
|
||||
|
||||
The byte stream consists of offsets against a selected page (e.g. $20xx)
|
||||
at which to write a selected content byte. Those selections are
|
||||
controlled by special opcodes emitted to the stream
|
||||
|
||||
Opcodes:
|
||||
SET_CONTENT - new byte to write to screen contents
|
||||
SET_PAGE - set new page to offset against (e.g. $20xx)
|
||||
TICK - tick the speaker
|
||||
DONE - terminate the video decoding
|
||||
|
||||
In order to "make room" for these opcodes we make use of the fact that
|
||||
each page has 2 sets of 8-byte "screen holes", at page offsets
|
||||
0x78-0x7f and 0xf8-0xff. Currently we only use the latter range as
|
||||
this allows for efficient matching in the critical path of the decoder.
|
||||
|
||||
We group by offsets from page boundary (cf some other more
|
||||
optimal starting point) because STA (..),y has 1 extra cycle if
|
||||
crossing the page boundary. Though maybe this would be worthwhile if
|
||||
it optimizes the bytestream.
|
||||
"""
|
||||
|
||||
self.cycle_counter.reset()
|
||||
# Target screen memory map for new frame
|
||||
target = frame.pack()
|
||||
|
||||
# Estimate number of opcodes that will end up fitting in the cycle
|
||||
# budget.
|
||||
byte_cycles = opcodes.Offset(0).cycles
|
||||
est_opcodes = int(cycle_budget / fullness / byte_cycles)
|
||||
|
||||
# Sort by highest xor weight and take the estimated number of change
|
||||
# operations
|
||||
# TODO: changes should be a class
|
||||
changes = list(
|
||||
sorted(self.index_changes(self.screen, target), reverse=True)
|
||||
)[:est_opcodes]
|
||||
|
||||
for op in self.scheduler.schedule(changes):
|
||||
yield from self.state.emit(op)
|
||||
|
||||
def index_changes(self, source: screen.Bytemap,
|
||||
target: screen.Bytemap) -> Set[
|
||||
Tuple[int, int, int, int, int]]:
|
||||
"""Transform encoded screen to sequence of change tuples.
|
||||
|
||||
Change tuple is (xor_weight, page, offset, content)
|
||||
"""
|
||||
|
||||
changes = set()
|
||||
# TODO: don't use 256 bytes if XMAX is smaller, or we may compute RLE
|
||||
# over the full page!
|
||||
memmap = defaultdict(lambda: [(0, 0, 0)] * 256)
|
||||
|
||||
it = np.nditer(target.bytemap, flags=['multi_index'])
|
||||
while not it.finished:
|
||||
y, x_byte = it.multi_index
|
||||
|
||||
page, offset = self.memory_map.to_page_offset(x_byte, y)
|
||||
|
||||
src_content = source.bytemap[y][x_byte]
|
||||
target_content = np.asscalar(it[0])
|
||||
|
||||
bits_different = hamming_weight(src_content ^ target_content)
|
||||
|
||||
memmap[page][offset] = (bits_different, src_content, target_content)
|
||||
it.iternext()
|
||||
|
||||
byte_cycles = opcodes.Offset(0).cycles
|
||||
|
||||
for page, offsets in memmap.items():
|
||||
cur_content = None
|
||||
run_length = 0
|
||||
maybe_run = []
|
||||
for offset, data in enumerate(offsets):
|
||||
bits_different, src_content, target_content = data
|
||||
|
||||
# TODO: allowing odd bit errors introduces colour error
|
||||
if maybe_run and hamming_weight(
|
||||
cur_content ^ target_content) > 2:
|
||||
# End of run
|
||||
|
||||
# Decide if it's worth emitting as a run vs single stores
|
||||
|
||||
# Number of changes in run for which >0 bits differ
|
||||
num_changes = len([c for c in maybe_run if c[0]])
|
||||
run_cost = opcodes.RLE(0, run_length).cycles
|
||||
single_cost = byte_cycles * num_changes
|
||||
# print("Run of %d cheaper than %d singles" % (
|
||||
# run_length, num_changes))
|
||||
|
||||
# TODO: don't allow too much error to accumulate
|
||||
|
||||
if run_cost < single_cost:
|
||||
# Compute median bit value over run
|
||||
median_bits = np.median(
|
||||
np.vstack(
|
||||
np.unpackbits(
|
||||
np.array(r[3], dtype=np.uint8)
|
||||
)
|
||||
for r in maybe_run
|
||||
), axis=0
|
||||
) > 0.5
|
||||
|
||||
typical_content = np.asscalar(np.packbits(median_bits))
|
||||
|
||||
total_xor = sum(ch[0] for ch in maybe_run)
|
||||
start_offset = maybe_run[0][2]
|
||||
|
||||
change = (total_xor, page, start_offset,
|
||||
typical_content, run_length)
|
||||
# print("Found run of %d * %2x at %2x:%2x" % (
|
||||
# run_length, cur_content, page, offset - run_length)
|
||||
# )
|
||||
# print(maybe_run)
|
||||
# print("change =", change)
|
||||
changes.add(change)
|
||||
else:
|
||||
changes.update(ch for ch in maybe_run if ch[0])
|
||||
maybe_run = []
|
||||
run_length = 0
|
||||
cur_content = target_content
|
||||
|
||||
if cur_content is None:
|
||||
cur_content = target_content
|
||||
|
||||
run_length += 1
|
||||
maybe_run.append(
|
||||
(bits_different, page, offset, target_content, 1))
|
||||
|
||||
return changes
|
||||
|
||||
def done(self) -> Iterator[int]:
|
||||
"""Terminate opcode stream."""
|
||||
|
||||
yield from self.state.emit(opcodes.Terminate())
|
Loading…
Reference in New Issue
Block a user