mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- Added new tests for FLAC encoding and decoding, including handling of rice parameters and partial LPC tail. - Introduced WAV PCM encoding tests to ensure correct behavior across various configurations. - Updated test cases to validate the handling of malformed FLAC files and metadata structures. - Enhanced the APU register tests to include expanded P5 profile capabilities and ensure accurate register descriptions. - Improved assertions for manifest checks in the APU release tests to validate ABI and instruction counts. - Refactored existing tests for clarity and maintainability, ensuring they align with the latest APU specifications.
739 lines
30 KiB
Python
739 lines
30 KiB
Python
"""Reference interpreter for the enabled APU-P3/P4/P5 microcode targets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from apu_isa import (
|
|
APUMC_ABI_V1,
|
|
APUMC_TARGETS,
|
|
BitstreamOpcode,
|
|
ControlOpcode,
|
|
EntropyOpcode,
|
|
Entry,
|
|
Instruction,
|
|
InstructionClass,
|
|
LocalOpcode,
|
|
ScalarOpcode,
|
|
TransportOpcode,
|
|
parse_apumc,
|
|
trap_detail,
|
|
validate_instruction,
|
|
)
|
|
from apu_primitives import PrimitiveBam, PrimitiveFault, PrimitiveResult
|
|
|
|
|
|
class SequencerTrap(RuntimeError):
|
|
def __init__(self, reason: int, pc: int, instruction: Instruction, detail: int | None = None):
|
|
self.reason = reason
|
|
self.pc = pc
|
|
self.instruction = instruction
|
|
self.detail = trap_detail(reason, pc, instruction) if detail is None else detail
|
|
super().__init__(f"APU sequencer trap {reason} at PC {pc}")
|
|
|
|
|
|
@dataclass
|
|
class PredicateInputs:
|
|
input_exhausted: bool = False
|
|
input_ready: bool = False
|
|
output_ready: bool = False
|
|
kernel_done: bool = False
|
|
transport_done: bool = False
|
|
|
|
|
|
@dataclass
|
|
class TransportCommand:
|
|
opcode: TransportOpcode
|
|
destination: int
|
|
offset: int
|
|
count: int
|
|
payload: bytes
|
|
cycles: int
|
|
|
|
|
|
@dataclass
|
|
class TransportModel:
|
|
input_data: bytes = b""
|
|
output_capacity: int = 0xFFFFFFFF
|
|
stream_output: bool = False
|
|
latency: int = 2
|
|
output_frame_bytes: int = 4
|
|
input_cursor: int = 0
|
|
output: bytearray = field(default_factory=bytearray)
|
|
frames: int = 0
|
|
logical_input: int = 0
|
|
logical_output: int = 0
|
|
result_code: int = 0
|
|
result_stage: int = 0
|
|
result_detail: int = 0
|
|
events: int = 0
|
|
pending: list[TransportCommand] = field(default_factory=list)
|
|
completed: list[tuple[int, int]] = field(default_factory=list)
|
|
output_commands: list[dict[str, int]] = field(default_factory=list)
|
|
|
|
@property
|
|
def idle(self) -> bool:
|
|
return not self.pending
|
|
|
|
def reset(self) -> None:
|
|
self.input_cursor = 0
|
|
self.output.clear()
|
|
self.frames = 0
|
|
self.logical_input = 0
|
|
self.logical_output = 0
|
|
self.result_code = 0
|
|
self.result_stage = 0
|
|
self.result_detail = 0
|
|
self.events = 0
|
|
self.pending.clear()
|
|
self.completed.clear()
|
|
self.output_commands.clear()
|
|
|
|
def abort(self) -> None:
|
|
self.pending.clear()
|
|
self.completed.clear()
|
|
|
|
def issue(
|
|
self,
|
|
instruction: Instruction,
|
|
registers: list[int],
|
|
entry: Entry,
|
|
primitives: PrimitiveBam,
|
|
) -> bool:
|
|
opcode = TransportOpcode(instruction.opcode)
|
|
offset = registers[instruction.src0]
|
|
count = registers[instruction.src1]
|
|
if opcode == TransportOpcode.INPUT_REFILL:
|
|
if (
|
|
count < 4
|
|
or count > 256
|
|
or count & 3
|
|
or offset & 3
|
|
or offset + count > entry.scratch_bytes
|
|
):
|
|
raise PrimitiveFault(11, 11, 5)
|
|
actual = min(count, len(self.input_data) - self.input_cursor)
|
|
needed = (actual + 3) // 4
|
|
if any(command.opcode == TransportOpcode.INPUT_REFILL for command in self.pending):
|
|
return False
|
|
if len(primitives.input_fifo.words) + needed > 64:
|
|
return False
|
|
payload = self.input_data[self.input_cursor : self.input_cursor + actual]
|
|
self.pending.append(
|
|
TransportCommand(
|
|
opcode, instruction.dst, offset, actual, payload, max(1, self.latency)
|
|
)
|
|
)
|
|
return True
|
|
if opcode in (TransportOpcode.OUTPUT_COMMIT, TransportOpcode.OUTPUT_STREAM):
|
|
if count < 1 or count > 256 or offset & 3 or offset + count > entry.scratch_bytes:
|
|
raise PrimitiveFault(11, 11, 5)
|
|
if count % self.output_frame_bytes or (
|
|
(opcode == TransportOpcode.OUTPUT_STREAM) != self.stream_output
|
|
):
|
|
raise PrimitiveFault(11, 11, 5)
|
|
if not primitives.output_fifo.empty:
|
|
raise PrimitiveFault(11, 11, 5)
|
|
if any(
|
|
command.opcode in (TransportOpcode.OUTPUT_COMMIT, TransportOpcode.OUTPUT_STREAM)
|
|
for command in self.pending
|
|
):
|
|
return False
|
|
if (
|
|
opcode == TransportOpcode.OUTPUT_COMMIT
|
|
and len(self.output) + count > self.output_capacity
|
|
):
|
|
raise PrimitiveFault(8, 8, 9)
|
|
address = entry.scratch_base + offset
|
|
payload = primitives.memory.read(address, count, entry)
|
|
self.output_commands.append({"opcode": int(opcode), "bytes": count})
|
|
self.pending.append(
|
|
TransportCommand(
|
|
opcode, instruction.dst, offset, count, payload, max(1, self.latency)
|
|
)
|
|
)
|
|
return True
|
|
if opcode == TransportOpcode.DMA_WAIT:
|
|
if not self.idle:
|
|
return False
|
|
self.completed.append((instruction.dst, 0))
|
|
elif opcode == TransportOpcode.FRAME_COMMIT:
|
|
if (
|
|
count != self.output_frame_bytes
|
|
or self.logical_input + offset > self.input_cursor
|
|
or self.logical_output + count > len(self.output)
|
|
):
|
|
raise PrimitiveFault(11, 11, 9)
|
|
self.logical_input = min(0xFFFFFFFF, self.logical_input + offset)
|
|
self.logical_output = min(0xFFFFFFFF, self.logical_output + count)
|
|
self.frames = min(0xFFFFFFFF, self.frames + 1)
|
|
self.completed.append((instruction.dst, 0))
|
|
elif opcode == TransportOpcode.JOB_RESULT:
|
|
code = offset & 0x3F
|
|
if offset >> 6 or code > 22:
|
|
raise PrimitiveFault(11, 11, 1)
|
|
self.result_code = code
|
|
self.result_stage = instruction.aux & 0xF
|
|
self.result_detail = count
|
|
self.completed.append((instruction.dst, 0))
|
|
else:
|
|
self.events |= instruction.immediate & 0x0C0
|
|
self.completed.append((instruction.dst, 0))
|
|
return True
|
|
|
|
def tick(self, entry: Entry, primitives: PrimitiveBam) -> None:
|
|
if not self.pending:
|
|
return
|
|
for command in self.pending:
|
|
command.cycles -= 1
|
|
ready = [command for command in self.pending if command.cycles == 0]
|
|
for command in ready:
|
|
if command.opcode == TransportOpcode.INPUT_REFILL:
|
|
primitives.memory.write(entry.scratch_base + command.offset, command.payload, entry)
|
|
for position in range(0, command.count, 4):
|
|
data = command.payload[position : position + 4]
|
|
last = self.input_cursor + position + len(data) == len(self.input_data)
|
|
primitives.input_fifo.push(
|
|
int.from_bytes(data.ljust(4, b"\0"), "little")
|
|
| (len(data) << 32)
|
|
| (int(last) << 40)
|
|
)
|
|
self.input_cursor += command.count
|
|
else:
|
|
self.output.extend(command.payload)
|
|
context_output = entry.scratch_base + entry.scratch_bytes - 64 + 44
|
|
primitives.memory.write(
|
|
context_output,
|
|
len(self.output).to_bytes(4, "little"),
|
|
entry,
|
|
)
|
|
self.completed.append((command.destination, command.count))
|
|
self.pending.remove(command)
|
|
|
|
|
|
@dataclass
|
|
class Machine:
|
|
instructions: list[Instruction]
|
|
entry: Entry
|
|
predicate_inputs: PredicateInputs = field(default_factory=PredicateInputs)
|
|
timeout: int = 0xFFFF
|
|
fetch_no_retirement_cycles: int = 2
|
|
stall_cycles: dict[int, int] = field(default_factory=dict)
|
|
registers: list[int] = field(default_factory=lambda: [0] * 16)
|
|
eq: bool = False
|
|
signed_lt: bool = False
|
|
unsigned_lt: bool = False
|
|
pc: int = 0
|
|
retired: int = 0
|
|
return_stack: list[int] = field(default_factory=list)
|
|
loops: dict[int, tuple[int, int]] = field(default_factory=dict)
|
|
watchdog: int = 0
|
|
trace: list[dict[str, object]] = field(default_factory=list)
|
|
target: str = "p3"
|
|
primitives: PrimitiveBam | None = None
|
|
transport: TransportModel | None = None
|
|
primitive_cycles: list[int] = field(default_factory=list)
|
|
kernel_result: PrimitiveResult | None = field(default=None, init=False)
|
|
kernel_fault: PrimitiveFault | None = field(default=None, init=False)
|
|
kernel_pending_fault: PrimitiveFault | None = field(default=None, init=False)
|
|
kernel_pending_dst: int | None = field(default=None, init=False)
|
|
kernel_cycles_remaining: int = field(default=0, init=False)
|
|
transport_pending_dst: set[int] = field(default_factory=set, init=False)
|
|
transport_fault: PrimitiveFault | None = field(default=None, init=False)
|
|
mc_abi: int = APUMC_ABI_V1
|
|
|
|
def __post_init__(self) -> None:
|
|
self.begin_entry(self.entry)
|
|
|
|
@staticmethod
|
|
def _signed(value: int) -> int:
|
|
return value if value < 0x80000000 else value - 0x100000000
|
|
|
|
def _predicate(self, predicate: int) -> bool:
|
|
input_ready = self.predicate_inputs.input_ready
|
|
output_ready = self.predicate_inputs.output_ready
|
|
kernel_done = self.predicate_inputs.kernel_done
|
|
input_exhausted = self.predicate_inputs.input_exhausted
|
|
if self.target in ("p4", "p5") and self.primitives is not None:
|
|
input_ready = not self.primitives.input_fifo.empty
|
|
output_ready = not self.primitives.output_fifo.full
|
|
kernel_done = self.primitives.kernel_done
|
|
input_exhausted = (
|
|
self.primitives.eof
|
|
and self.primitives.input_fifo.empty
|
|
and not self.primitives.bits
|
|
)
|
|
values = (
|
|
True,
|
|
self.eq,
|
|
not self.eq,
|
|
self.signed_lt,
|
|
not self.signed_lt,
|
|
self.unsigned_lt,
|
|
not self.unsigned_lt,
|
|
input_exhausted,
|
|
input_ready,
|
|
output_ready,
|
|
kernel_done,
|
|
self.transport.idle
|
|
if self.transport is not None
|
|
else self.predicate_inputs.transport_done,
|
|
)
|
|
if predicate >= len(values):
|
|
raise SequencerTrap(1, self.pc, self.instructions[self.pc])
|
|
return values[predicate]
|
|
|
|
def _write(self, register: int, value: int) -> None:
|
|
self.registers[register] = value & 0xFFFFFFFF
|
|
|
|
def _complete_kernel(self) -> None:
|
|
if self.kernel_pending_fault is not None:
|
|
self.kernel_fault = self.kernel_pending_fault
|
|
elif self.kernel_result is not None:
|
|
for register, value in self.kernel_result.writes.items():
|
|
self._write(register, value)
|
|
if self.primitives is not None:
|
|
self.primitives.kernel_done = True
|
|
self.kernel_result = None
|
|
self.kernel_pending_fault = None
|
|
self.kernel_pending_dst = None
|
|
self.kernel_cycles_remaining = 0
|
|
|
|
def _tick_kernel(self) -> None:
|
|
if self.kernel_pending_dst is None:
|
|
return
|
|
if self.kernel_cycles_remaining > 0:
|
|
self.kernel_cycles_remaining -= 1
|
|
if self.kernel_cycles_remaining == 0:
|
|
self._complete_kernel()
|
|
|
|
def _tick_transport(self) -> None:
|
|
if self.transport is None or self.primitives is None:
|
|
return
|
|
try:
|
|
self.transport.tick(self.entry, self.primitives)
|
|
except PrimitiveFault as error:
|
|
self.transport_fault = error
|
|
return
|
|
while self.transport.completed:
|
|
register, value = self.transport.completed.pop(0)
|
|
self._write(register, value)
|
|
self.transport_pending_dst.discard(register)
|
|
|
|
def _pending_dependency(self, instruction: Instruction) -> bool:
|
|
pending_registers = set(self.transport_pending_dst)
|
|
if self.kernel_pending_dst is not None:
|
|
pending_registers.add(self.kernel_pending_dst)
|
|
return any(
|
|
self._uses_pending_register(instruction, pending) for pending in pending_registers
|
|
)
|
|
|
|
@staticmethod
|
|
def _uses_pending_register(instruction: Instruction, pending: int) -> bool:
|
|
instruction_class = instruction.instruction_class
|
|
opcode = instruction.opcode
|
|
if instruction_class == InstructionClass.CONTROL:
|
|
if opcode == ControlOpcode.END:
|
|
return True
|
|
return opcode == ControlOpcode.LOOP_SETUP and instruction.src0 == pending
|
|
if instruction_class == InstructionClass.SCALAR:
|
|
if opcode == ScalarOpcode.MOV:
|
|
return instruction.src0 == pending or instruction.dst == pending
|
|
if opcode == ScalarOpcode.MOVI:
|
|
return instruction.dst == pending
|
|
if opcode == ScalarOpcode.CMP:
|
|
return instruction.src0 == pending or instruction.src1 == pending
|
|
if opcode == ScalarOpcode.SAT:
|
|
return instruction.src0 == pending or instruction.dst == pending
|
|
return (
|
|
instruction.src0 == pending
|
|
or instruction.src1 == pending
|
|
or instruction.dst == pending
|
|
)
|
|
return True
|
|
|
|
def begin_entry(self, entry: Entry | None = None) -> None:
|
|
if entry is not None:
|
|
self.entry = entry
|
|
self.pc = self.entry.entry_pc
|
|
self.retired = 0
|
|
self.registers = [0] * 16
|
|
self.eq = False
|
|
self.signed_lt = False
|
|
self.unsigned_lt = False
|
|
self.return_stack.clear()
|
|
self.loops.clear()
|
|
self.watchdog = 0
|
|
self.kernel_result = None
|
|
self.kernel_fault = None
|
|
self.kernel_pending_fault = None
|
|
self.kernel_pending_dst = None
|
|
self.kernel_cycles_remaining = 0
|
|
self.transport_pending_dst.clear()
|
|
self.transport_fault = None
|
|
if self.primitives is not None:
|
|
self.primitives.reset_mutable()
|
|
if self.transport is not None:
|
|
self.transport.reset()
|
|
|
|
def abort(self) -> None:
|
|
self.kernel_result = None
|
|
self.kernel_fault = None
|
|
self.kernel_pending_fault = None
|
|
self.kernel_pending_dst = None
|
|
self.kernel_cycles_remaining = 0
|
|
self.transport_pending_dst.clear()
|
|
self.transport_fault = None
|
|
if self.transport is not None:
|
|
self.transport.abort()
|
|
self.watchdog = 0
|
|
if self.primitives is not None:
|
|
self.primitives.reset_mutable()
|
|
|
|
def reset(self) -> None:
|
|
self.begin_entry(self.entry)
|
|
|
|
def _retire(self, instruction: Instruction, predicate_true: bool) -> None:
|
|
self.retired += 1
|
|
self.watchdog = 0
|
|
self.trace.append(
|
|
{
|
|
"pc": self.pc,
|
|
"instruction": f"0x{instruction.encode():016x}",
|
|
"predicate": predicate_true,
|
|
"retired": self.retired,
|
|
}
|
|
)
|
|
|
|
def _no_retirement_cycle(
|
|
self, instruction: Instruction, *, tick_kernel: bool = True, tick_transport: bool = True
|
|
) -> None:
|
|
if tick_kernel:
|
|
self._tick_kernel()
|
|
if tick_transport:
|
|
self._tick_transport()
|
|
self.watchdog += 1
|
|
if self.timeout <= 0 or self.watchdog >= self.timeout:
|
|
raise SequencerTrap(7, self.pc, instruction)
|
|
|
|
def _saturate(self, value: int, width: int, signed: bool) -> int:
|
|
width = 32 if width == 0 else width
|
|
if signed:
|
|
signed_value = self._signed(value)
|
|
maximum = (1 << (width - 1)) - 1
|
|
minimum = -(1 << (width - 1))
|
|
return min(max(signed_value, minimum), maximum) & 0xFFFFFFFF
|
|
return min(value, (1 << width) - 1)
|
|
|
|
def step(self) -> bool:
|
|
if self.pc < self.entry.first_pc or self.pc > self.entry.last_pc:
|
|
raise SequencerTrap(2, self.pc, Instruction(0, 0))
|
|
instruction = self.instructions[self.pc]
|
|
self._tick_kernel()
|
|
self._tick_transport()
|
|
if self.kernel_fault is not None:
|
|
fault = self.kernel_fault
|
|
self.kernel_fault = None
|
|
raise SequencerTrap(fault.reason, self.pc, instruction)
|
|
if self.transport_fault is not None:
|
|
fault = self.transport_fault
|
|
self.transport_fault = None
|
|
raise SequencerTrap(fault.reason, self.pc, instruction)
|
|
try:
|
|
validate_instruction(instruction, self.target, self.mc_abi)
|
|
except ValueError as error:
|
|
raise SequencerTrap(1, self.pc, instruction) from error
|
|
if self.retired >= self.entry.max_retired:
|
|
raise SequencerTrap(8, self.pc, instruction)
|
|
predicate_true = self._predicate(instruction.predicate)
|
|
if not predicate_true:
|
|
store_last = 0x7FF if self.mc_abi == APUMC_ABI_V1 else 0xFFF
|
|
if self.pc == self.entry.last_pc or self.pc == store_last:
|
|
raise SequencerTrap(2, self.pc, instruction)
|
|
self._retire(instruction, predicate_true)
|
|
self.pc += 1
|
|
return False
|
|
|
|
if self._pending_dependency(instruction):
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
|
|
next_pc = self.pc + 1
|
|
terminal = False
|
|
if instruction.instruction_class == InstructionClass.CONTROL:
|
|
opcode = ControlOpcode(instruction.opcode)
|
|
if opcode == ControlOpcode.END:
|
|
if self.transport is not None and not self.transport.idle:
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
terminal = True
|
|
elif opcode == ControlOpcode.TRAP:
|
|
raise SequencerTrap(10, self.pc, instruction, instruction.immediate)
|
|
elif opcode == ControlOpcode.JUMP_FWD:
|
|
next_pc += instruction.immediate
|
|
elif opcode == ControlOpcode.CALL_FWD:
|
|
if len(self.return_stack) == 4:
|
|
raise SequencerTrap(3, self.pc, instruction)
|
|
self.return_stack.append(next_pc)
|
|
next_pc += instruction.immediate
|
|
elif opcode == ControlOpcode.RET:
|
|
if not self.return_stack:
|
|
raise SequencerTrap(3, self.pc, instruction)
|
|
next_pc = self.return_stack.pop()
|
|
elif opcode == ControlOpcode.LOOP_SETUP:
|
|
slot = instruction.aux & 3
|
|
count = self.registers[instruction.src0] & 0xFFFF
|
|
if slot in self.loops or count == 0 or count > self.entry.max_loop_count:
|
|
raise SequencerTrap(4, self.pc, instruction)
|
|
self.loops[slot] = (count, next_pc)
|
|
elif opcode == ControlOpcode.LOOP_BACK:
|
|
slot = instruction.aux & 3
|
|
if slot not in self.loops:
|
|
raise SequencerTrap(4, self.pc, instruction)
|
|
count, start = self.loops[slot]
|
|
target = next_pc - instruction.immediate
|
|
if target != start or target < self.entry.first_pc:
|
|
raise SequencerTrap(4, self.pc, instruction)
|
|
if count > 1:
|
|
self.loops[slot] = (count - 1, start)
|
|
next_pc = target
|
|
else:
|
|
del self.loops[slot]
|
|
elif opcode == ControlOpcode.WAIT:
|
|
ready = False
|
|
if self.primitives is not None:
|
|
ready = (
|
|
(
|
|
instruction.aux == 0
|
|
and self.transport is not None
|
|
and self.transport.idle
|
|
)
|
|
or (instruction.aux == 1 and self.primitives.kernel_done)
|
|
or (instruction.aux == 2 and not self.primitives.input_fifo.empty)
|
|
or (instruction.aux == 3 and not self.primitives.output_fifo.full)
|
|
or (
|
|
instruction.aux == 4
|
|
and self.transport is not None
|
|
and self.transport.idle
|
|
)
|
|
or (
|
|
instruction.aux == 5
|
|
and self.transport is not None
|
|
and self.transport.idle
|
|
)
|
|
)
|
|
if not ready:
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
elif instruction.instruction_class == InstructionClass.SCALAR:
|
|
opcode = ScalarOpcode(instruction.opcode)
|
|
source0 = self.registers[instruction.src0]
|
|
source1 = self.registers[instruction.src1]
|
|
if opcode == ScalarOpcode.MOV:
|
|
self._write(instruction.dst, source0)
|
|
elif opcode == ScalarOpcode.MOVI:
|
|
self._write(instruction.dst, instruction.immediate)
|
|
elif opcode == ScalarOpcode.ADD:
|
|
self._write(instruction.dst, source0 + source1)
|
|
elif opcode == ScalarOpcode.SUB:
|
|
self._write(instruction.dst, source0 - source1)
|
|
elif opcode == ScalarOpcode.AND:
|
|
self._write(instruction.dst, source0 & source1)
|
|
elif opcode == ScalarOpcode.OR:
|
|
self._write(instruction.dst, source0 | source1)
|
|
elif opcode == ScalarOpcode.XOR:
|
|
self._write(instruction.dst, source0 ^ source1)
|
|
elif opcode == ScalarOpcode.SHL:
|
|
self._write(instruction.dst, source0 << (source1 & 31))
|
|
elif opcode == ScalarOpcode.SHR:
|
|
self._write(instruction.dst, source0 >> (source1 & 31))
|
|
elif opcode == ScalarOpcode.SAR:
|
|
self._write(instruction.dst, self._signed(source0) >> (source1 & 31))
|
|
elif opcode == ScalarOpcode.CMP:
|
|
self.eq = source0 == source1
|
|
self.signed_lt = self._signed(source0) < self._signed(source1)
|
|
self.unsigned_lt = source0 < source1
|
|
elif opcode in (ScalarOpcode.MIN, ScalarOpcode.MAX):
|
|
left = self._signed(source0) if instruction.aux else source0
|
|
right = self._signed(source1) if instruction.aux else source1
|
|
result = min(left, right) if opcode == ScalarOpcode.MIN else max(left, right)
|
|
self._write(instruction.dst, result)
|
|
elif opcode == ScalarOpcode.SAT:
|
|
self._write(
|
|
instruction.dst,
|
|
self._saturate(source0, instruction.aux & 0x1F, bool(instruction.aux & 0x20)),
|
|
)
|
|
elif instruction.instruction_class == InstructionClass.TRANSPORT:
|
|
if self.transport is None or self.primitives is None:
|
|
raise SequencerTrap(6, self.pc, instruction)
|
|
try:
|
|
accepted = self.transport.issue(
|
|
instruction, self.registers, self.entry, self.primitives
|
|
)
|
|
except PrimitiveFault as error:
|
|
raise SequencerTrap(error.reason, self.pc, instruction) from error
|
|
if not accepted:
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
if instruction.opcode <= TransportOpcode.OUTPUT_STREAM:
|
|
self.transport_pending_dst.add(instruction.dst)
|
|
else:
|
|
self._tick_transport()
|
|
else:
|
|
if self.primitives is None:
|
|
raise SequencerTrap(6, self.pc, instruction)
|
|
if instruction.instruction_class == InstructionClass.LOCAL:
|
|
opcode = LocalOpcode(instruction.opcode)
|
|
if (opcode == LocalOpcode.FIFO_POP and self.primitives.input_fifo.empty) or (
|
|
opcode == LocalOpcode.FIFO_PUSH and self.primitives.output_fifo.full
|
|
):
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
if instruction.instruction_class == InstructionClass.BITSTREAM:
|
|
opcode = BitstreamOpcode(instruction.opcode)
|
|
required = 0
|
|
if opcode in (
|
|
BitstreamOpcode.REFILL,
|
|
BitstreamOpcode.PEEK,
|
|
BitstreamOpcode.GET,
|
|
BitstreamOpcode.SKIP,
|
|
):
|
|
required = instruction.immediate & 0x3F
|
|
elif opcode == BitstreamOpcode.FRAME_SYNC:
|
|
required = instruction.aux & 0x1F
|
|
if required != 0:
|
|
self.primitives._refill(required)
|
|
if required != 0 and len(self.primitives.bits) < required and not self.primitives.eof:
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
if instruction.instruction_class == InstructionClass.ENTROPY:
|
|
opcode = EntropyOpcode(instruction.opcode)
|
|
if opcode <= EntropyOpcode.HUFF_QUAD:
|
|
required = instruction.aux
|
|
elif opcode == EntropyOpcode.UNARY:
|
|
required = instruction.immediate + 1
|
|
elif opcode == EntropyOpcode.SIGN_RESTORE:
|
|
required = (
|
|
0
|
|
if self.registers[instruction.src0] == 0
|
|
else (self.registers[instruction.src1] & 0x1F) + 1
|
|
)
|
|
else:
|
|
required = 1
|
|
self.primitives._refill(required)
|
|
if len(self.primitives.bits) < required and not self.primitives.eof:
|
|
self._no_retirement_cycle(instruction, tick_kernel=False, tick_transport=False)
|
|
return False
|
|
try:
|
|
if instruction.instruction_class == InstructionClass.KERNEL:
|
|
self.primitives.kernel_done = False
|
|
result = self.primitives.execute(instruction, self.registers, self.entry)
|
|
except PrimitiveFault as error:
|
|
if instruction.instruction_class != InstructionClass.KERNEL or error.reason == 5:
|
|
raise SequencerTrap(error.reason, self.pc, instruction) from error
|
|
result = PrimitiveResult(cycles=1, kernel_done=True)
|
|
self.kernel_pending_fault = error
|
|
self.primitive_cycles.append(result.cycles)
|
|
if instruction.instruction_class == InstructionClass.KERNEL:
|
|
self.primitives.kernel_done = False
|
|
self.kernel_result = result
|
|
self.kernel_pending_dst = instruction.dst
|
|
self.kernel_cycles_remaining = max(result.cycles - 1, 0)
|
|
if self.kernel_cycles_remaining == 0:
|
|
self._complete_kernel()
|
|
else:
|
|
for register, value in result.writes.items():
|
|
self._write(register, value)
|
|
for _ in range(max(result.cycles - 1, 0)):
|
|
self._no_retirement_cycle(instruction)
|
|
|
|
self._retire(instruction, predicate_true)
|
|
if not terminal:
|
|
if next_pc < self.entry.first_pc or next_pc > self.entry.last_pc:
|
|
raise SequencerTrap(2, self.pc, instruction)
|
|
self.pc = next_pc
|
|
return terminal
|
|
|
|
def run(self) -> dict[str, object]:
|
|
fetch_required = True
|
|
while True:
|
|
if self.pc < self.entry.first_pc or self.pc > self.entry.last_pc:
|
|
raise SequencerTrap(2, self.pc, Instruction(0, 0))
|
|
instruction = self.instructions[self.pc]
|
|
if fetch_required:
|
|
for _ in range(self.fetch_no_retirement_cycles):
|
|
self._no_retirement_cycle(instruction)
|
|
remaining_stalls = self.stall_cycles.get(self.pc, 0)
|
|
while remaining_stalls > 0:
|
|
self._no_retirement_cycle(instruction)
|
|
remaining_stalls -= 1
|
|
self.stall_cycles[self.pc] = 0
|
|
retired_before = self.retired
|
|
if self.step():
|
|
break
|
|
fetch_required = self.retired != retired_before
|
|
return {
|
|
"pc": self.pc,
|
|
"retired": self.retired,
|
|
"registers": self.registers,
|
|
"trace": self.trace,
|
|
"primitive_cycles": self.primitive_cycles,
|
|
"transport": None
|
|
if self.transport is None
|
|
else {
|
|
"input_used": self.transport.input_cursor,
|
|
"output": bytes(self.transport.output).hex(),
|
|
"frames": self.transport.frames,
|
|
"result_code": self.transport.result_code,
|
|
"result_stage": self.transport.result_stage,
|
|
"result_detail": self.transport.result_detail,
|
|
"events": self.transport.events,
|
|
"output_commands": self.transport.output_commands,
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("bundle", type=Path)
|
|
parser.add_argument("--target", choices=APUMC_TARGETS, default="p3")
|
|
parser.add_argument("--entry", type=int, choices=range(3), default=0)
|
|
parser.add_argument("--timeout", type=int, default=0xFFFF)
|
|
parser.add_argument("--input-exhausted", action="store_true")
|
|
parser.add_argument("--input-ready", action="store_true")
|
|
parser.add_argument("--output-ready", action="store_true")
|
|
parser.add_argument("--kernel-done", action="store_true")
|
|
parser.add_argument("--transport-done", action="store_true")
|
|
args = parser.parse_args()
|
|
instructions, entries, header = parse_apumc(args.bundle.read_bytes(), args.target)
|
|
inputs = PredicateInputs(
|
|
input_exhausted=args.input_exhausted,
|
|
input_ready=args.input_ready,
|
|
output_ready=args.output_ready,
|
|
kernel_done=args.kernel_done,
|
|
transport_done=args.transport_done,
|
|
)
|
|
print(
|
|
json.dumps(
|
|
Machine(
|
|
instructions,
|
|
entries[args.entry],
|
|
inputs,
|
|
args.timeout,
|
|
target=args.target,
|
|
primitives=PrimitiveBam() if args.target in ("p4", "p5") else None,
|
|
transport=TransportModel() if args.target == "p5" else None,
|
|
mc_abi=header[1],
|
|
).run(),
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|