mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-21 05:30:20 +00:00
Introduction more general notion of update priority used to increase
weight of diffs that persist across multiple frames. For each frame, zero out update priority of bytes that no longer have a pending diff, and add the edit distance of the remaining diffs. Zero these out as opcodes are retired. Replace hamming distance with Damerau-Levenshtein distance of the encoded pixel colours in the byte, e.g. 0x2A --> GGG0 (taking into account the half-pixel) This has a couple of benefits over hamming distance of the bit patterns: - transposed pixels are weighted less (edit distance 1, not 2+ for Hamming) - coloured pixels are weighted equally as white pixels (not half as much) - weighting changes in palette bit that flip multiple pixel colours While I'm here, the RLE opcode should emit run_length - 1 so that we can encode runs of 256 bytes.
This commit is contained in:
parent
d3522c817f
commit
6e2c83c1e5
16
opcodes.py
16
opcodes.py
@ -1,4 +1,5 @@
|
||||
import enum
|
||||
import numpy as np
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
import screen
|
||||
@ -20,12 +21,13 @@ class State:
|
||||
"""Represents virtual machine state."""
|
||||
|
||||
def __init__(self, cycle_counter: CycleCounter,
|
||||
memmap: screen.MemoryMap):
|
||||
memmap: screen.MemoryMap, update_priority: np.array):
|
||||
self.page = 0x20
|
||||
self.content = 0x7f
|
||||
|
||||
self.memmap = memmap
|
||||
self.cycle_counter = cycle_counter
|
||||
self.update_priority = update_priority
|
||||
|
||||
def emit(self, last_opcode: "Opcode", opcode: "Opcode") -> Iterator[int]:
|
||||
cmd = opcode.emit_command(last_opcode, opcode)
|
||||
@ -127,6 +129,8 @@ class Store(Opcode):
|
||||
|
||||
def apply(self, state):
|
||||
state.memmap.write(state.page, self.offset, state.content)
|
||||
# TODO: screen page
|
||||
state.update_priority[state.page - 32, self.offset] = 0
|
||||
|
||||
|
||||
class SetContent(Opcode):
|
||||
@ -193,7 +197,7 @@ class RLE(Opcode):
|
||||
def emit_data(self):
|
||||
# print(" RLE @ %02x * %02x" % (self.start_offset, self.run_length))
|
||||
yield self.start_offset
|
||||
yield self.run_length
|
||||
yield self.run_length - 1
|
||||
|
||||
@property
|
||||
def cycles(self):
|
||||
@ -201,10 +205,10 @@ class RLE(Opcode):
|
||||
|
||||
def apply(self, state):
|
||||
for i in range(self.run_length):
|
||||
state.memmap.write(
|
||||
state.page, (self.start_offset + i) & 0xff,
|
||||
state.content
|
||||
)
|
||||
offset = (self.start_offset + i) & 0xff
|
||||
state.memmap.write(state.page, offset, state.content)
|
||||
# TODO: screen page
|
||||
state.update_priority[state.page - 32, offset] = 0
|
||||
|
||||
|
||||
class Tick(Opcode):
|
||||
|
22
scheduler.py
22
scheduler.py
@ -31,12 +31,12 @@ class HeuristicPageFirstScheduler(OpcodeScheduler):
|
||||
page_weights = collections.defaultdict(int)
|
||||
page_content_weights = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content, run_length = ch
|
||||
update_priority, page, offset, content, run_length = ch
|
||||
data.setdefault((page, content), list()).append(
|
||||
(xor_weight, run_length, offset))
|
||||
page_weights[page] += xor_weight
|
||||
(update_priority, run_length, offset))
|
||||
page_weights[page] += update_priority
|
||||
page_content_weights.setdefault(page, collections.defaultdict(
|
||||
int))[content] += xor_weight
|
||||
int))[content] += update_priority
|
||||
|
||||
# Weight each page and content within page by total xor weight and
|
||||
# traverse in this order, with a random nonce so that we don't
|
||||
@ -87,12 +87,12 @@ class HeuristicContentFirstScheduler(OpcodeScheduler):
|
||||
content_weights = collections.defaultdict(int)
|
||||
content_page_weights = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content, run_length = ch
|
||||
update_priority, page, offset, content, run_length = ch
|
||||
data.setdefault((page, content), list()).append(
|
||||
(xor_weight, run_length, offset))
|
||||
content_weights[content] += xor_weight
|
||||
(update_priority, run_length, offset))
|
||||
content_weights[content] += update_priority
|
||||
content_page_weights.setdefault(content, collections.defaultdict(
|
||||
int))[page] += xor_weight
|
||||
int))[page] += update_priority
|
||||
|
||||
# Weight each page and content within page by total xor weight and
|
||||
# traverse in this order
|
||||
@ -129,7 +129,7 @@ class OldHeuristicPageFirstScheduler(OpcodeScheduler):
|
||||
"""Group by page first then content byte.
|
||||
|
||||
This uses a deterministic order of pages and content bytes, and ignores
|
||||
xor_weight altogether
|
||||
update_priority altogether
|
||||
"""
|
||||
|
||||
# Median similarity: 0.854613 ( @ 15 fps, 10M output)
|
||||
@ -141,7 +141,7 @@ class OldHeuristicPageFirstScheduler(OpcodeScheduler):
|
||||
def schedule(self, changes):
|
||||
data = {}
|
||||
for ch in changes:
|
||||
xor_weight, page, offset, content, run_length = ch
|
||||
update_priority, page, offset, content, run_length = ch
|
||||
data.setdefault(page, {}).setdefault(content, set()).add(
|
||||
(run_length, offset))
|
||||
|
||||
@ -234,7 +234,7 @@ class OldHeuristicPageFirstScheduler(OpcodeScheduler):
|
||||
# # Heuristic: group by content byte first then page
|
||||
# data = {}
|
||||
# for ch in changes:
|
||||
# xor_weight, page, offset, content = ch
|
||||
# update_priority, page, offset, content = ch
|
||||
# data.setdefault(content, {}).setdefault(page, set()).add(offset)
|
||||
#
|
||||
# for content, page_offsets in data.items():
|
||||
|
113
video.py
113
video.py
@ -1,6 +1,9 @@
|
||||
import functools
|
||||
from typing import Iterator, Tuple, Iterable
|
||||
|
||||
import numpy as np
|
||||
from similarity.damerau import Damerau
|
||||
|
||||
import opcodes
|
||||
import scheduler
|
||||
import screen
|
||||
@ -14,6 +17,56 @@ def hamming_weight(n):
|
||||
return n
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def edit_weight(a: int, b: int, is_odd_offset: bool):
|
||||
d = Damerau()
|
||||
|
||||
a_pixels = byte_to_colour_string(a, is_odd_offset)
|
||||
b_pixels = byte_to_colour_string(b, is_odd_offset)
|
||||
|
||||
return d.distance(a_pixels, b_pixels)
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def byte_to_colour_string(b: int, is_odd_offset: bool) -> str:
|
||||
pixels = []
|
||||
|
||||
idx = 0
|
||||
if is_odd_offset:
|
||||
pixels.append("01"[b & 0x01])
|
||||
idx += 1
|
||||
|
||||
# K = black
|
||||
# G = green
|
||||
# V = violet
|
||||
# W = white
|
||||
palettes = (
|
||||
(
|
||||
"K", # 0x00
|
||||
"V", # 0x01
|
||||
"G", # 0x10
|
||||
"W" # 0x11
|
||||
), (
|
||||
"K", # 0x00
|
||||
"O", # 0x01
|
||||
"B", # 0x10
|
||||
"W" # 0x11
|
||||
)
|
||||
)
|
||||
palette = palettes[b & 0x80 != 0]
|
||||
|
||||
for _ in range(3):
|
||||
pixel = palette[(b >> idx) & 0b11]
|
||||
pixels.append(pixel)
|
||||
idx += 2
|
||||
|
||||
if not is_odd_offset:
|
||||
pixels.append("01"[b & 0x40 != 0])
|
||||
idx += 1
|
||||
|
||||
return "".join(pixels)
|
||||
|
||||
|
||||
class Video:
|
||||
"""Apple II screen memory map encoding a bitmapped frame."""
|
||||
|
||||
@ -32,7 +85,11 @@ class Video:
|
||||
|
||||
self.cycle_counter = opcodes.CycleCounter()
|
||||
|
||||
self.state = opcodes.State(self.cycle_counter, self.memory_map)
|
||||
# Accumulates pending edit weights across frames
|
||||
self.update_priority = np.zeros((32, 256), dtype=np.int)
|
||||
|
||||
self.state = opcodes.State(
|
||||
self.cycle_counter, self.memory_map, self.update_priority)
|
||||
|
||||
self.frame_rate = frame_rate
|
||||
self.stream_pos = 0
|
||||
@ -43,7 +100,8 @@ class Video:
|
||||
|
||||
self._last_op = opcodes.Nop()
|
||||
|
||||
def encode_frame(self, frame: screen.MemoryMap) -> Iterator[opcodes.Opcode]:
|
||||
def encode_frame(self, target: screen.MemoryMap) -> Iterator[
|
||||
opcodes.Opcode]:
|
||||
"""Update to match content of frame within provided budget.
|
||||
|
||||
Emits encoded byte stream for rendering the image.
|
||||
@ -66,14 +124,8 @@ class Video:
|
||||
it optimizes the bytestream.
|
||||
"""
|
||||
|
||||
# Target screen memory map for new frame
|
||||
target = frame
|
||||
|
||||
# Sort by highest xor weight and take the estimated number of change
|
||||
# operations
|
||||
# TODO: changes should be a class
|
||||
changes = sorted(list(self._index_changes(self.memory_map, target)),
|
||||
reverse=True)
|
||||
changes = self._index_changes(self.memory_map, target)
|
||||
|
||||
yield from self.scheduler.schedule(changes)
|
||||
|
||||
@ -92,7 +144,7 @@ class Video:
|
||||
num_changes_in_run = 0
|
||||
|
||||
# Total weight of differences accumulated in run
|
||||
total_xor_in_run = 0
|
||||
total_update_priority_in_run = 0
|
||||
|
||||
def end_run():
|
||||
# Decide if it's worth emitting as a run vs single stores
|
||||
@ -109,7 +161,9 @@ class Video:
|
||||
# )
|
||||
# print(run)
|
||||
yield (
|
||||
total_xor_in_run, start_offset, cur_content, run_length)
|
||||
total_update_priority_in_run, start_offset, cur_content,
|
||||
run_length
|
||||
)
|
||||
else:
|
||||
for ch in run:
|
||||
if ch[0]:
|
||||
@ -126,7 +180,7 @@ class Video:
|
||||
run = []
|
||||
run_length = 0
|
||||
num_changes_in_run = 0
|
||||
total_xor_in_run = 0
|
||||
total_update_priority_in_run = 0
|
||||
cur_content = tc
|
||||
|
||||
if cur_content is None:
|
||||
@ -136,7 +190,7 @@ class Video:
|
||||
run.append((bd, offset, tc, 1))
|
||||
if bd:
|
||||
num_changes_in_run += 1
|
||||
total_xor_in_run += bd
|
||||
total_update_priority_in_run += bd
|
||||
|
||||
if run:
|
||||
# End of run
|
||||
@ -149,22 +203,39 @@ class Video:
|
||||
) -> Iterator[Tuple[int, int, int, int, int]]:
|
||||
"""Transform encoded screen to sequence of change tuples.
|
||||
|
||||
Change tuple is (xor_weight, page, offset, content, run_length)
|
||||
Change tuple is (update_priority, page, offset, content, run_length)
|
||||
"""
|
||||
|
||||
# TODO: don't use 256 bytes if XMAX is smaller, or we may compute RLE
|
||||
# (with bit errors) over the full page!
|
||||
diff_weights = hamming_weight(source.page_offset ^ target.page_offset)
|
||||
diff_weights = np.zeros((32, 256), dtype=np.uint8)
|
||||
|
||||
it = np.nditer(
|
||||
source.page_offset ^ target.page_offset,
|
||||
flags=['multi_index'])
|
||||
while not it.finished:
|
||||
diff_weights[it.multi_index] = edit_weight(
|
||||
source.page_offset[it.multi_index],
|
||||
target.page_offset[it.multi_index],
|
||||
it.multi_index[1] % 2 == 1
|
||||
)
|
||||
it.iternext()
|
||||
|
||||
# Clear any update priority entries that have resolved themselves
|
||||
# with new frame
|
||||
self.update_priority[diff_weights == 0] = 0
|
||||
|
||||
self.update_priority += diff_weights
|
||||
|
||||
for page in range(32):
|
||||
for change in self._index_page(
|
||||
diff_weights[page], target.page_offset[page]):
|
||||
total_xor_in_run, start_offset, target_content, run_length = \
|
||||
change
|
||||
self.update_priority[page], target.page_offset[page]):
|
||||
(
|
||||
total_priority_in_run, start_offset, target_content,
|
||||
run_length
|
||||
) = change
|
||||
|
||||
# TODO: handle screen page
|
||||
yield (
|
||||
total_xor_in_run, page + 32, start_offset,
|
||||
total_priority_in_run, page + 32, start_offset,
|
||||
target_content, run_length
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user