Proof of concept DHGR encoding/playback

- Every time we process an ACK opcode, toggle page 1/page 2 soft
  switches to steer subsequent writes between MAIN and AUX memory
- while I'm here, squeeze out some unnecessary operations from the
  buffer management

On the player side, this is implemented by maintaining two screen
memory maps, and alternating between opcode streams for each of them.
This is using entirely the wrong colour model for errors, but
surprisingly it already works pretty well in practise (and the frame
rate is acceptable on test videos)

DHGR/HGR could be made runtime selectable by adding a header byte that
determines whether to set the DHGR soft switches before initiating
the decode loop.

While I'm in here, fix op_terminate to clear keyboard strobe before
waiting.
This commit is contained in:
kris 2019-03-27 21:37:06 +00:00
parent d4a27444c6
commit 10fa4bc72d
4 changed files with 97 additions and 65 deletions

View File

@ -304,8 +304,23 @@ exit_parmtable:
init_mainloop:
JSR hgr ; nukes the startup code we placed in HGR segment
STA $C050 ; GRAPHICS
STA $C057 ; HIRES
STA $C05E ; DHR
STA $C00D ; 80 COLUMN MODE
STA $C001 ; 80STOREON
; Clear aux screen
STA $C055 ;
LDA #$20
JSR $F3EA
STA fullscr
STA $C054 ; MAIN memory active
; establish invariant expected by decode loop
LDX #$00
@ -1256,11 +1271,11 @@ op_tick_64 63
op_tick_66 63
op_terminate:
; Wait for keypress
LDA KBDSTRB ; clear strobe
@0: ; Wait for keypress
LDA KBD
BMI @1 ; key pressed
BPL op_terminate
@1: LDA KBDSTRB ; clear strobe
BPL @0
@1: ; key pressed
JMP exit
; Manage W5100 socket buffer and ACK TCP stream.
@ -1268,30 +1283,35 @@ op_terminate:
; In order to simplify the buffer management we expect this ACK opcode to consume
; the last 4 bytes in a 2K "TCP frame". i.e. we can assume that we need to consume
; exactly 2K from the W5100 socket buffer.
;
; TODO: actually we are underrunning by 2 bytes currently, we've only consumed 2
; bytes of 4 by this point.
op_ack:
BIT tick ; 4
LDA WDATA ; 4 dummy read of second-last byte in TCP frame
; allow flip-flopping the PAGE1/PAGE2 soft switches to steer writes to MAIN/AUX screens
; actually this allows touching any $C0XX soft-switch, in case that is useful somehow
LDA WDATA ; 4
STA @D+1 ; 4
@D:
STA $C054 ; 4 low-byte is modified
LDA WDATA ; 4 dummy read of last byte in TCP frame
CLC ; 2
LDA #>S0RXRD ; 2 NEED HIGH BYTE HERE
STA WADRH ; 4
LDA #<S0RXRD ; 2
LDX #<S0RXRD ; 2
STX WADRL ; 4
STA WADRL ; 4
NOP ; 2
BIT tick ; 4 (36) ; does not affect Carry bit
; No need to read/modify low byte since it is always guaranteed to be 0 (since we are at the end of a 2K frame)
LDA WDATA ; 4 HIGH BYTE
LDX WDATA ; 4 LOW BYTE ; not sure if needed -- but we have cycles to spare so who cares!
ADC #$08 ; 2 ADD HIGH BYTE OF RECEIVED SIZE
BIT tick ; 4 (36)
TAY ; 2 SAVE
LDA #<S0RXRD ; 2
STA WADRL ; 4 Might not be needed, but have cycles to spare
STY WDATA ; 4 SEND HIGH BYTE
STX WDATA ; 4 SEND LOW BYTE
ADC #$08 ; 2 Add high byte of received size (always constant
STX WADRL ; 4 Reset address pointer, still have it in X
STA WDATA ; 4 Store high byte (no need to store low byte since it's 0)
; SEND THE RECV COMMAND
LDA #<S0CR ; 2
@ -1300,6 +1320,7 @@ op_ack:
STA WDATA ; 4
NOP ; 2 ; see, we even have cycles left over!
NOP ; 2
JMP CHECKRECV ; 3 (37 with following BIT tick)
@ -1313,21 +1334,4 @@ CLOSECONN:
LDA #SCDISCON ; DISCONNECT
STA WDATA ; SEND COMMAND
; CHECK FOR CLOSED STATUS
;CHECKCLOSED:
; LDX #0
;@L:
; LDA #<S0SR
; STA WADRL
; LDA WDATA
; BEQ ISCLOSED
; NOP
; NOP
; NOP
; INX
; BNE @L ; DON'T WAIT FOREVER
;ISCLOSED:
; RTS ; SOCKET IS CLOSED
.endproc

View File

@ -36,6 +36,8 @@ class Movie:
self.video.update_priority
)
self.aux_memory_bank = False
def encode(self) -> Iterator[opcodes.Opcode]:
"""
@ -47,18 +49,23 @@ class Movie:
for au in self.audio.audio_stream():
self.cycles += self.audio.cycles_per_tick
if self.video.tick(self.cycles):
video_frame = next(video_frames)
main, aux = next(video_frames)
if ((self.video.frame_number - 1) % self.every_n_video_frames
== 0):
print("Starting frame %d" % self.video.frame_number)
video_seq = self.video.encode_frame(video_frame)
main_seq = self.video.encode_frame(
main, self.video.memory_map, self.video.update_priority)
aux_seq = self.video.encode_frame(
aux, self.video.aux_memory_map,
self.video.aux_update_priority)
# au has range -15 .. 16 (step=1)
# Tick cycles are units of 2
tick = au * 2 # -30 .. 32 (step=2)
tick += 34 # 4 .. 66 (step=2)
(page, content, offsets) = next(video_seq)
(page, content, offsets) = next(
aux_seq if self.aux_memory_bank else main_seq)
yield opcodes.TICK_OPCODES[(tick, page)](content, offsets)
@ -86,7 +93,9 @@ class Movie:
socket_pos = self.stream_pos % 2048
if socket_pos >= 2044:
# 2 dummy bytes + 2 address bytes for next opcode
yield from self._emit_bytes(opcodes.Ack())
yield from self._emit_bytes(opcodes.Ack(self.aux_memory_bank))
# Flip-flop between MAIN and AUX banks
self.aux_memory_bank = not self.aux_memory_bank
yield from self._emit_bytes(op)
yield from self.done()

View File

@ -76,13 +76,18 @@ class Ack(Opcode):
"""Instructs player to perform TCP stream + buffer management."""
COMMAND = OpcodeCommand.ACK
def __init__(self, aux_active: bool):
self.aux_active = aux_active
def emit_data(self) -> Iterator[int]:
# Dummy bytes to pad out TCP frame
yield 0xff
# Flip $C054 or $C055 soft-switches to steer subsequent writes to
# MAIN/AUX screen memory
yield 0x54 if self.aux_active else 0x55
# Dummy byte to pad out TCP frame
yield 0xff
def __data_eq__(self, other):
return True
return self.aux_active == other.aux_active
class BaseTick(Opcode):

View File

@ -45,10 +45,14 @@ class Video:
# Initialize empty screen
self.memory_map = screen.MemoryMap(
screen_page=1) # type: screen.MemoryMap
self.aux_memory_map = screen.MemoryMap(
screen_page=1) # type: screen.MemoryMap
# Accumulates pending edit weights across frames
self.update_priority = np.zeros((32, 256), dtype=np.int64)
self.aux_update_priority = np.zeros((32, 256), dtype=np.int64)
def tick(self, cycles: int) -> bool:
if cycles > (self.cycles_per_frame * self.frame_number):
self.frame_number += 1
@ -110,51 +114,61 @@ class Video:
def worker():
"""Invoke bmp2dhr to encode input image frames and push to queue."""
for _idx, _frame in enumerate(self._frame_grabber()):
outfile = "%s/%08dC.BIN" % (frame_dir, _idx)
mainfile = "%s/%08d.BIN" % (frame_dir, _idx)
auxfile = "%s/%08d.AUX" % (frame_dir, _idx)
bmpfile = "%s/%08d.bmp" % (frame_dir, _idx)
try:
os.stat(outfile)
os.stat(mainfile)
os.stat(auxfile)
except FileNotFoundError:
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
_frame.save(bmpfile)
subprocess.call(
["/usr/local/bin/bmp2dhr", bmpfile, "hgr", "D9"])
["/usr/local/bin/bmp2dhr", bmpfile, "dhgr", "P0", "A",
"D9"])
os.remove(bmpfile)
_frame = np.fromfile(outfile, dtype=np.uint8)
q.put(_frame)
main = np.fromfile(mainfile, dtype=np.uint8)
aux = np.fromfile(auxfile, dtype=np.uint8)
q.put((main, aux))
q.put(None)
q.put((None, None))
t = threading.Thread(target=worker, daemon=True)
t.start()
while True:
frame = q.get()
if frame is None:
main, aux = q.get()
if main is None:
break
yield screen.FlatMemoryMap(
screen_page=1, data=frame).to_memory_map()
yield (
screen.FlatMemoryMap(screen_page=1, data=main).to_memory_map(),
screen.FlatMemoryMap(screen_page=1, data=aux).to_memory_map()
)
q.task_done()
t.join()
def encode_frame(
self, target: screen.MemoryMap
self, target: screen.MemoryMap,
memory_map: screen.MemoryMap,
update_priority: np.array,
) -> Iterator[opcodes.Opcode]:
"""Update to match content of frame within provided budget."""
print("Similarity %f" % (self.update_priority.mean()))
yield from self._index_changes(self.memory_map, target)
print("Similarity %f" % (update_priority.mean()))
yield from self._index_changes(memory_map, target, update_priority)
def _index_changes(
self,
source: screen.MemoryMap,
target: screen.MemoryMap
target: screen.MemoryMap,
update_priority: np.array
) -> Iterator[Tuple[int, int, List[int]]]:
"""Transform encoded screen to sequence of change tuples."""
@ -162,16 +176,16 @@ class Video:
# Clear any update priority entries that have resolved themselves
# with new frame
self.update_priority[diff_weights == 0] = 0
update_priority[diff_weights == 0] = 0
# Halve existing weights to increase bias to new diffs.
# In particular this means that existing updates with diff 1 will
# become diff 0, i.e. will only be prioritized if they are still
# diffs in the new frame.
# self.update_priority >>= 1
self.update_priority += diff_weights
update_priority += diff_weights
priorities = self._heapify_priorities()
priorities = self._heapify_priorities(update_priority)
content_deltas = {}
@ -179,15 +193,15 @@ class Video:
_, _, page, offset = heapq.heappop(priorities)
# Check whether we've already cleared this diff while processing
# an earlier opcode
if self.update_priority[page, offset] == 0:
if update_priority[page, offset] == 0:
continue
offsets = [offset]
content = target.page_offset[page, offset]
# Clear priority for the offset we're emitting
self.update_priority[page, offset] = 0
self.memory_map.page_offset[page, offset] = content
update_priority[page, offset] = 0
source.page_offset[page, offset] = content
diff_weights[page, offset] = 0
# Make sure we don't emit this offset as a side-effect of some
@ -212,9 +226,9 @@ class Video:
error=False)
# Update priority for the offset we're emitting
self.update_priority[page, o] = p # 0
update_priority[page, o] = p # 0
self.memory_map.page_offset[page, o] = content
source.page_offset[page, o] = content
if p:
# This content byte introduced an error, so put back on the
@ -242,9 +256,9 @@ class Video:
return edit_distance.screen_edit_distance(
source.page_offset, target.page_offset)
def _heapify_priorities(self) -> List:
def _heapify_priorities(self, update_priority: np.array) -> List:
priorities = []
it = np.nditer(self.update_priority, flags=['multi_index'])
it = np.nditer(update_priority, flags=['multi_index'])
while not it.finished:
priority = it[0]
if not priority: