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: init_mainloop:
JSR hgr ; nukes the startup code we placed in HGR segment 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 fullscr
STA $C054 ; MAIN memory active
; establish invariant expected by decode loop ; establish invariant expected by decode loop
LDX #$00 LDX #$00
@ -1256,11 +1271,11 @@ op_tick_64 63
op_tick_66 63 op_tick_66 63
op_terminate: op_terminate:
; Wait for keypress LDA KBDSTRB ; clear strobe
@0: ; Wait for keypress
LDA KBD LDA KBD
BMI @1 ; key pressed BPL @0
BPL op_terminate @1: ; key pressed
@1: LDA KBDSTRB ; clear strobe
JMP exit JMP exit
; Manage W5100 socket buffer and ACK TCP stream. ; 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 ; 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 ; 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. ; 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: op_ack:
BIT tick ; 4 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 LDA WDATA ; 4 dummy read of last byte in TCP frame
CLC ; 2 CLC ; 2
LDA #>S0RXRD ; 2 NEED HIGH BYTE HERE LDA #>S0RXRD ; 2 NEED HIGH BYTE HERE
STA WADRH ; 4 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 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 ADC #$08 ; 2 Add high byte of received size (always constant
BIT tick ; 4 (36) STX WADRL ; 4 Reset address pointer, still have it in X
TAY ; 2 SAVE STA WDATA ; 4 Store high byte (no need to store low byte since it's 0)
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
; SEND THE RECV COMMAND ; SEND THE RECV COMMAND
LDA #<S0CR ; 2 LDA #<S0CR ; 2
@ -1300,6 +1320,7 @@ op_ack:
STA WDATA ; 4 STA WDATA ; 4
NOP ; 2 ; see, we even have cycles left over! NOP ; 2 ; see, we even have cycles left over!
NOP ; 2
JMP CHECKRECV ; 3 (37 with following BIT tick) JMP CHECKRECV ; 3 (37 with following BIT tick)
@ -1313,21 +1334,4 @@ CLOSECONN:
LDA #SCDISCON ; DISCONNECT LDA #SCDISCON ; DISCONNECT
STA WDATA ; SEND COMMAND 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 .endproc

View File

@ -36,6 +36,8 @@ class Movie:
self.video.update_priority self.video.update_priority
) )
self.aux_memory_bank = False
def encode(self) -> Iterator[opcodes.Opcode]: def encode(self) -> Iterator[opcodes.Opcode]:
""" """
@ -47,18 +49,23 @@ class Movie:
for au in self.audio.audio_stream(): for au in self.audio.audio_stream():
self.cycles += self.audio.cycles_per_tick self.cycles += self.audio.cycles_per_tick
if self.video.tick(self.cycles): 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 if ((self.video.frame_number - 1) % self.every_n_video_frames
== 0): == 0):
print("Starting frame %d" % self.video.frame_number) 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) # au has range -15 .. 16 (step=1)
# Tick cycles are units of 2 # Tick cycles are units of 2
tick = au * 2 # -30 .. 32 (step=2) tick = au * 2 # -30 .. 32 (step=2)
tick += 34 # 4 .. 66 (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) yield opcodes.TICK_OPCODES[(tick, page)](content, offsets)
@ -86,7 +93,9 @@ class Movie:
socket_pos = self.stream_pos % 2048 socket_pos = self.stream_pos % 2048
if socket_pos >= 2044: if socket_pos >= 2044:
# 2 dummy bytes + 2 address bytes for next opcode # 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._emit_bytes(op)
yield from self.done() yield from self.done()

View File

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

View File

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