mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2025-01-02 05:30:05 +00:00
10fa4bc72d
- 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.
183 lines
4.8 KiB
Python
183 lines
4.8 KiB
Python
"""Opcodes representing discrete operations of video player."""
|
|
|
|
import enum
|
|
from typing import Iterator, Tuple
|
|
|
|
import symbol_table
|
|
from machine import Machine
|
|
|
|
|
|
def _op_cmds():
|
|
"""Construct names of player opcodes."""
|
|
|
|
op_cmds = [
|
|
"TERMINATE",
|
|
"NOP",
|
|
"ACK",
|
|
]
|
|
for tick in range(4, 68, 2):
|
|
for page in range(32, 64):
|
|
op_cmds.append("TICK_%d_PAGE_%d" % (tick, page))
|
|
return op_cmds
|
|
|
|
|
|
OpcodeCommand = enum.Enum("OpcodeCommand", _op_cmds())
|
|
|
|
|
|
class Opcode:
|
|
"""Base class for opcodes."""
|
|
COMMAND = None # type: OpcodeCommand
|
|
|
|
# Offset of start byte of player opcode implementation
|
|
_START = None # type: int
|
|
|
|
def __repr__(self):
|
|
return "Opcode(%s)" % self.COMMAND.name
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, self.__class__):
|
|
return False
|
|
|
|
return self.__data_eq__(other)
|
|
|
|
def __data_eq__(self, other):
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def emit_command(opcode: "Opcode") -> Iterator[int]:
|
|
# Emit address of next opcode
|
|
yield opcode._START >> 8
|
|
yield opcode._START & 0xff
|
|
|
|
def emit_data(self) -> Iterator[int]:
|
|
return
|
|
|
|
def apply(self, state: Machine):
|
|
pass
|
|
|
|
|
|
class Nop(Opcode):
|
|
"""NOP pad opcode that does nothing except vector to the next one."""
|
|
COMMAND = OpcodeCommand.NOP
|
|
|
|
def __data_eq__(self, other):
|
|
return True
|
|
|
|
|
|
class Terminate(Opcode):
|
|
"""Terminates video playback."""
|
|
COMMAND = OpcodeCommand.TERMINATE
|
|
|
|
def __data_eq__(self, other):
|
|
return True
|
|
|
|
|
|
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]:
|
|
# 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 self.aux_active == other.aux_active
|
|
|
|
|
|
class BaseTick(Opcode):
|
|
"""Base class for "fat" audio + video opcode.
|
|
|
|
Each such opcode is specialized for a particular HiRes graphics page,
|
|
and speaker duty cycle count. The opcode also stores the provided
|
|
content byte at 4 offsets on this graphics page.
|
|
"""
|
|
|
|
def __init__(self, content: int, offsets: Tuple):
|
|
self.content = content
|
|
if len(offsets) != 4:
|
|
raise ValueError("Wrong number of offsets: %d != 4" % len(offsets))
|
|
self.offsets = offsets
|
|
|
|
def __data_eq__(self, other):
|
|
return self.content == other.content and self.offsets == other.offsets
|
|
|
|
def emit_data(self):
|
|
yield self.content # content
|
|
yield from self.offsets
|
|
|
|
|
|
def _make_tick_opcodes():
|
|
# Dynamically construct classes for each of the tick opcodes.
|
|
tick_opcodes = {}
|
|
|
|
for _tick in range(4, 68, 2):
|
|
for _page in range(32, 64):
|
|
tick_opcodes[(_tick, _page)] = type(
|
|
"Tick%dPage%d" % (_tick, _page),
|
|
(BaseTick,),
|
|
{
|
|
"COMMAND": OpcodeCommand["TICK_%d_PAGE_%d" % (_tick, _page)]
|
|
}
|
|
)
|
|
return tick_opcodes
|
|
|
|
|
|
TICK_OPCODES = _make_tick_opcodes()
|
|
|
|
|
|
def _parse_symbol_table():
|
|
"""Read symbol table from video player debug file."""
|
|
|
|
opcode_data = {}
|
|
for name, data in symbol_table.SymbolTable(
|
|
"player/iivision.dbg").parse().items():
|
|
if name.startswith("\"op_"):
|
|
op_name = name[4:-1]
|
|
start_addr = int(data["val"], 16)
|
|
|
|
opcode_data.setdefault(op_name, {})["start"] = start_addr
|
|
|
|
opcode_addrs = []
|
|
for op_name, addrs in opcode_data.items():
|
|
for op in OpcodeCommand:
|
|
if op.name.lower() != op_name:
|
|
continue
|
|
opcode_addrs.append((op, addrs["start"]))
|
|
|
|
return sorted(opcode_addrs, key=lambda x: x[1])
|
|
|
|
|
|
def _fill_opcode_addresses():
|
|
"""Populate _START on opcodes from symbol table."""
|
|
|
|
_OPCODE_ADDRS = _parse_symbol_table()
|
|
_OPCODE_CLASSES = {
|
|
OpcodeCommand.TERMINATE: Terminate,
|
|
OpcodeCommand.NOP: Nop,
|
|
OpcodeCommand.ACK: Ack,
|
|
}
|
|
|
|
for _tick in range(4, 68, 2):
|
|
for _page in range(32, 64):
|
|
_tickop = OpcodeCommand["TICK_%d_PAGE_%d" % (_tick, _page)]
|
|
_OPCODE_CLASSES[_tickop] = TICK_OPCODES[(_tick, _page)]
|
|
for op, start in _OPCODE_ADDRS:
|
|
cls = _OPCODE_CLASSES[op]
|
|
cls._START = start
|
|
|
|
for op, cls in _OPCODE_CLASSES.items():
|
|
if not cls._START:
|
|
raise ValueError(
|
|
"Unable to find opcode address for %s in player debug symbols"
|
|
% op
|
|
)
|
|
|
|
|
|
_fill_opcode_addresses()
|