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.
420 lines
17 KiB
Python
420 lines
17 KiB
Python
"""Assemble and statically verify frozen APU-P3/P4 microcode bundles."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import struct
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from apu_isa import (
|
|
CONTROL_OPCODES,
|
|
DEFERRED_OPCODES,
|
|
PREDICATES,
|
|
SCALAR_OPCODES,
|
|
APUMC_TARGETS,
|
|
BitstreamOpcode,
|
|
ControlOpcode,
|
|
EntropyOpcode,
|
|
Entry,
|
|
Instruction,
|
|
InstructionClass,
|
|
KernelOpcode,
|
|
LocalOpcode,
|
|
ScalarOpcode,
|
|
TransportOpcode,
|
|
abi_manifest,
|
|
build_apumc,
|
|
control_flow_report,
|
|
crc32_iso_hdlc,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Assembly:
|
|
bundle: bytes
|
|
symbols: dict[str, int]
|
|
instructions: list[Instruction]
|
|
entries: list[Entry]
|
|
build_id: int
|
|
target: str
|
|
table_payload: bytes
|
|
mc_abi: int
|
|
|
|
|
|
def _number(token: str) -> int:
|
|
return int(token, 0)
|
|
|
|
|
|
def _register(token: str) -> int:
|
|
match = re.fullmatch(r"[rR](\d+)", token)
|
|
if match is None or int(match.group(1)) > 15:
|
|
raise ValueError(f"invalid APU register {token}")
|
|
return int(match.group(1))
|
|
|
|
|
|
def _tokens(line: str) -> list[str]:
|
|
return [token for token in re.split(r"[\s,]+", line.strip()) if token]
|
|
|
|
|
|
def _predicate(tokens: list[str]) -> tuple[list[str], int]:
|
|
if tokens and tokens[-1].startswith("if="):
|
|
name = tokens[-1][3:].lower()
|
|
if name not in PREDICATES:
|
|
raise ValueError(f"unknown predicate {name}")
|
|
return tokens[:-1], PREDICATES[name]
|
|
return tokens, 0
|
|
|
|
|
|
def _encode_instruction(tokens: list[str], pc: int, labels: dict[str, int]) -> Instruction:
|
|
tokens, predicate = _predicate(tokens)
|
|
mnemonic = tokens[0].lower()
|
|
args = tokens[1:]
|
|
if mnemonic in CONTROL_OPCODES:
|
|
opcode = ControlOpcode(CONTROL_OPCODES[mnemonic])
|
|
values = {"predicate": predicate}
|
|
if opcode in (ControlOpcode.NOP, ControlOpcode.END, ControlOpcode.RET):
|
|
if args:
|
|
raise ValueError(f"{mnemonic} takes no operands")
|
|
elif opcode == ControlOpcode.TRAP:
|
|
if len(args) != 1:
|
|
raise ValueError("trap takes one immediate")
|
|
values["immediate"] = _number(args[0])
|
|
elif opcode in (ControlOpcode.JUMP_FWD, ControlOpcode.CALL_FWD):
|
|
if len(args) != 1:
|
|
raise ValueError(f"{mnemonic} takes one label")
|
|
values["immediate"] = labels[args[0]] - pc - 1
|
|
elif opcode == ControlOpcode.LOOP_SETUP:
|
|
if len(args) != 2:
|
|
raise ValueError("loop_setup takes a slot and count register")
|
|
values["aux"] = _number(args[0])
|
|
values["src0"] = _register(args[1])
|
|
elif opcode == ControlOpcode.LOOP_BACK:
|
|
if len(args) != 2:
|
|
raise ValueError("loop_back takes a slot and label")
|
|
values["aux"] = _number(args[0])
|
|
values["immediate"] = pc + 1 - labels[args[1]]
|
|
elif opcode == ControlOpcode.WAIT:
|
|
if len(args) != 1:
|
|
raise ValueError("wait takes one source")
|
|
values["aux"] = _number(args[0])
|
|
return Instruction(InstructionClass.CONTROL, opcode, **values)
|
|
|
|
if mnemonic in SCALAR_OPCODES:
|
|
opcode = ScalarOpcode(SCALAR_OPCODES[mnemonic])
|
|
values = {"predicate": predicate}
|
|
elif mnemonic in DEFERRED_OPCODES["bitstream"]:
|
|
opcode = BitstreamOpcode(DEFERRED_OPCODES["bitstream"].index(mnemonic))
|
|
values = {"predicate": predicate}
|
|
if opcode in (BitstreamOpcode.REFILL, BitstreamOpcode.PEEK, BitstreamOpcode.GET):
|
|
if len(args) != 2:
|
|
raise ValueError(f"{mnemonic} takes a destination and bit width")
|
|
values.update(dst=_register(args[0]), immediate=_number(args[1]))
|
|
elif opcode == BitstreamOpcode.SKIP:
|
|
if len(args) != 1:
|
|
raise ValueError("skip takes one bit width")
|
|
values["immediate"] = _number(args[0])
|
|
elif opcode == BitstreamOpcode.ALIGN:
|
|
if args:
|
|
raise ValueError("align takes no operands")
|
|
elif opcode == BitstreamOpcode.FRAME_SYNC:
|
|
if len(args) != 5:
|
|
raise ValueError("frame_sync takes dst, pattern, mask, width, and limit")
|
|
values.update(
|
|
dst=_register(args[0]),
|
|
src0=_register(args[1]),
|
|
src1=_register(args[2]),
|
|
aux=_number(args[3]),
|
|
immediate=_number(args[4]),
|
|
)
|
|
else:
|
|
if len(args) != 3:
|
|
raise ValueError(f"{mnemonic} takes dst, accumulator, and byte registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), src1=_register(args[2]))
|
|
return Instruction(InstructionClass.BITSTREAM, opcode, **values)
|
|
elif mnemonic in DEFERRED_OPCODES["entropy"]:
|
|
opcode = EntropyOpcode(DEFERRED_OPCODES["entropy"].index(mnemonic))
|
|
values = {"predicate": predicate}
|
|
if opcode <= EntropyOpcode.HUFF_QUAD:
|
|
if len(args) != 4:
|
|
raise ValueError(f"{mnemonic} takes dst, table, entries, and maximum length")
|
|
values.update(
|
|
dst=_register(args[0]),
|
|
src0=_register(args[1]),
|
|
src1=_register(args[2]),
|
|
aux=_number(args[3]),
|
|
)
|
|
elif opcode == EntropyOpcode.UNARY:
|
|
if len(args) != 3:
|
|
raise ValueError("unary takes dst, polarity, and maximum run")
|
|
values.update(dst=_register(args[0]), aux=_number(args[1]), immediate=_number(args[2]))
|
|
else:
|
|
if len(args) != 3:
|
|
raise ValueError(f"{mnemonic} takes three registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), src1=_register(args[2]))
|
|
return Instruction(InstructionClass.ENTROPY, opcode, **values)
|
|
elif mnemonic in DEFERRED_OPCODES["local"]:
|
|
opcode = LocalOpcode(DEFERRED_OPCODES["local"].index(mnemonic))
|
|
values = {"predicate": predicate}
|
|
if opcode == LocalOpcode.LD32:
|
|
if len(args) != 3:
|
|
raise ValueError("ld32 takes destination, address register, and offset")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), immediate=_number(args[2]) & 0xFFFF)
|
|
elif opcode == LocalOpcode.ST32:
|
|
if len(args) != 3:
|
|
raise ValueError("st32 takes address, value, and offset")
|
|
values.update(src0=_register(args[0]), src1=_register(args[1]), immediate=_number(args[2]) & 0xFFFF)
|
|
elif opcode in (LocalOpcode.TABLE8, LocalOpcode.TABLE16, LocalOpcode.TABLE32):
|
|
if len(args) != 3:
|
|
raise ValueError(f"{mnemonic} takes destination, base, and index registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), src1=_register(args[2]))
|
|
elif opcode == LocalOpcode.FIFO_POP:
|
|
if len(args) != 1:
|
|
raise ValueError("fifo_pop takes one destination")
|
|
values["dst"] = _register(args[0])
|
|
else:
|
|
if len(args) != 2:
|
|
raise ValueError("fifo_push takes data and metadata registers")
|
|
values.update(src0=_register(args[0]), src1=_register(args[1]))
|
|
return Instruction(InstructionClass.LOCAL, opcode, **values)
|
|
elif mnemonic in DEFERRED_OPCODES["kernel"]:
|
|
opcode = KernelOpcode(DEFERRED_OPCODES["kernel"].index(mnemonic))
|
|
if len(args) != 5:
|
|
raise ValueError(f"{mnemonic} takes output, input, parameter, aux, and count")
|
|
return Instruction(
|
|
InstructionClass.KERNEL,
|
|
opcode,
|
|
predicate=predicate,
|
|
dst=_register(args[0]),
|
|
src0=_register(args[1]),
|
|
src1=_register(args[2]),
|
|
aux=_number(args[3]),
|
|
immediate=_number(args[4]),
|
|
)
|
|
elif mnemonic in DEFERRED_OPCODES["transport"]:
|
|
opcode = TransportOpcode(DEFERRED_OPCODES["transport"].index(mnemonic))
|
|
values = {"predicate": predicate}
|
|
if opcode in (
|
|
TransportOpcode.INPUT_REFILL,
|
|
TransportOpcode.OUTPUT_COMMIT,
|
|
TransportOpcode.OUTPUT_STREAM,
|
|
):
|
|
if len(args) != 3:
|
|
raise ValueError(f"{mnemonic} takes destination, address, and count registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), src1=_register(args[2]))
|
|
elif opcode == TransportOpcode.DMA_WAIT:
|
|
if len(args) != 1:
|
|
raise ValueError("dma_wait takes one destination register")
|
|
values["dst"] = _register(args[0])
|
|
elif opcode == TransportOpcode.FRAME_COMMIT:
|
|
if len(args) != 2:
|
|
raise ValueError("frame_commit takes input and output count registers")
|
|
values.update(src0=_register(args[0]), src1=_register(args[1]))
|
|
elif opcode == TransportOpcode.JOB_RESULT:
|
|
if len(args) != 3:
|
|
raise ValueError("job_result takes code, detail, and stage")
|
|
values.update(src0=_register(args[0]), src1=_register(args[1]), aux=_number(args[2]))
|
|
else:
|
|
if len(args) != 1:
|
|
raise ValueError("event takes one IRQ mask")
|
|
values["immediate"] = _number(args[0])
|
|
return Instruction(InstructionClass.TRANSPORT, opcode, **values)
|
|
else:
|
|
raise ValueError(f"unknown mnemonic {mnemonic}")
|
|
|
|
if opcode == ScalarOpcode.MOV:
|
|
if len(args) != 2:
|
|
raise ValueError("mov takes two registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]))
|
|
elif opcode == ScalarOpcode.MOVI:
|
|
if len(args) != 2:
|
|
raise ValueError("movi takes one register and one immediate")
|
|
values.update(dst=_register(args[0]), immediate=_number(args[1]) & 0xFFFFFFFF)
|
|
elif opcode == ScalarOpcode.CMP:
|
|
if len(args) != 2:
|
|
raise ValueError("cmp takes two registers")
|
|
values.update(src0=_register(args[0]), src1=_register(args[1]))
|
|
elif opcode in (ScalarOpcode.MIN, ScalarOpcode.MAX):
|
|
if len(args) not in (3, 4):
|
|
raise ValueError(f"{mnemonic} takes three registers and an optional signed selector")
|
|
values.update(
|
|
dst=_register(args[0]),
|
|
src0=_register(args[1]),
|
|
src1=_register(args[2]),
|
|
aux=_number(args[3]) if len(args) == 4 else 0,
|
|
)
|
|
elif opcode == ScalarOpcode.SAT:
|
|
if len(args) != 3:
|
|
raise ValueError("sat takes a destination, source, and width/sign encoding")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), aux=_number(args[2]))
|
|
else:
|
|
if len(args) != 3:
|
|
raise ValueError(f"{mnemonic} takes three registers")
|
|
values.update(dst=_register(args[0]), src0=_register(args[1]), src1=_register(args[2]))
|
|
return Instruction(InstructionClass.SCALAR, opcode, **values)
|
|
|
|
|
|
def assemble(
|
|
source: str,
|
|
target: str = "p3",
|
|
table_payload_input: bytes | None = None,
|
|
mc_abi: int | None = None,
|
|
) -> Assembly:
|
|
if target not in APUMC_TARGETS:
|
|
raise ValueError(f"unknown APU target {target}")
|
|
lines = []
|
|
labels: dict[str, int] = {}
|
|
directives: list[list[str]] = []
|
|
pc = 0
|
|
for line_number, raw_line in enumerate(source.splitlines(), start=1):
|
|
line = raw_line.partition("#")[0].strip()
|
|
if not line:
|
|
continue
|
|
if line.endswith(":"):
|
|
label = line[:-1].strip()
|
|
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", label) or label in labels:
|
|
raise ValueError(f"line {line_number}: invalid or duplicate label {label}")
|
|
labels[label] = pc
|
|
elif line.startswith("."):
|
|
directives.append(_tokens(line))
|
|
else:
|
|
lines.append((line_number, _tokens(line)))
|
|
pc += 1
|
|
|
|
build_id = 0
|
|
entry_directives = []
|
|
table_words: list[int] = []
|
|
for directive in directives:
|
|
if directive[0] == ".build_id" and len(directive) == 2:
|
|
build_id = _number(directive[1])
|
|
elif directive[0] == ".entry" and len(directive) in (7, 12):
|
|
entry_directives.append(directive[1:])
|
|
elif directive[0] == ".table32" and len(directive) > 1:
|
|
table_words.extend(_number(value) & 0xFFFFFFFF for value in directive[1:])
|
|
else:
|
|
raise ValueError(f"invalid assembler directive {' '.join(directive)}")
|
|
if len(entry_directives) != 3:
|
|
raise ValueError("exactly three .entry directives are required")
|
|
|
|
instructions = []
|
|
for line_number, tokens in lines:
|
|
try:
|
|
instructions.append(_encode_instruction(tokens, len(instructions), labels))
|
|
except (IndexError, KeyError, ValueError) as error:
|
|
raise ValueError(f"line {line_number}: {error}") from error
|
|
|
|
entries = []
|
|
for values in entry_directives:
|
|
try:
|
|
optional = [0, 0, 0, 0, 0] if len(values) == 6 else [_number(item) for item in values[6:]]
|
|
entries.append(Entry(
|
|
format_id=_number(values[0]),
|
|
entry_pc=labels[values[1]],
|
|
first_pc=labels[values[2]],
|
|
last_pc=labels[values[3]],
|
|
max_loop_count=_number(values[4]),
|
|
max_retired=_number(values[5]),
|
|
scratch_base=optional[0],
|
|
scratch_bytes=optional[1],
|
|
primitive_mask=optional[2],
|
|
table_offset=optional[3],
|
|
table_bytes=optional[4],
|
|
))
|
|
except KeyError as error:
|
|
raise ValueError(f"unknown entry label {error.args[0]}") from error
|
|
entries.sort(key=lambda entry: entry.format_id)
|
|
if table_payload_input is not None and table_words:
|
|
raise ValueError("external table payload cannot be combined with .table32")
|
|
table_payload = (
|
|
table_payload_input
|
|
if table_payload_input is not None
|
|
else b"".join(struct.pack("<I", word) for word in table_words)
|
|
)
|
|
bundle = build_apumc(
|
|
instructions, entries, build_id, target=target, table_payload=table_payload, abi=mc_abi
|
|
)
|
|
selected_abi = mc_abi if mc_abi is not None else (0x00020000 if target == "p5" else 0x00010000)
|
|
return Assembly(
|
|
bundle,
|
|
dict(sorted(labels.items())),
|
|
instructions,
|
|
entries,
|
|
build_id,
|
|
target,
|
|
table_payload,
|
|
selected_abi,
|
|
)
|
|
|
|
|
|
def _artifact_data(assembly: Assembly) -> dict[str, object]:
|
|
instruction_limit = 4096 if assembly.mc_abi == 0x00020000 else 2048
|
|
return {
|
|
"abi": "2.0" if assembly.mc_abi == 0x00020000 else "1.0",
|
|
"mc_abi": f"0x{assembly.mc_abi:08x}",
|
|
"build_id": f"0x{assembly.build_id:016x}",
|
|
"bundle_bytes": len(assembly.bundle),
|
|
"instruction_count": len(assembly.instructions),
|
|
"maximum_instruction_words": instruction_limit,
|
|
"free_instruction_words": instruction_limit - len(assembly.instructions),
|
|
"payload_crc32": f"0x{crc32_iso_hdlc(assembly.bundle[64:]):08x}",
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("source", type=Path)
|
|
parser.add_argument("--target", choices=APUMC_TARGETS, default="p3")
|
|
parser.add_argument("--mc-abi", choices=("1.0", "2.0"))
|
|
parser.add_argument("-o", "--output", type=Path, required=True)
|
|
parser.add_argument("--symbols", type=Path)
|
|
parser.add_argument("--cfg-report", type=Path)
|
|
parser.add_argument("--primitive-manifest", type=Path)
|
|
parser.add_argument("--trace-input", type=Path)
|
|
parser.add_argument("--abi-input-manifest", type=Path)
|
|
parser.add_argument("--table-binary", type=Path)
|
|
args = parser.parse_args()
|
|
table_payload = args.table_binary.read_bytes() if args.table_binary is not None else None
|
|
selected_abi = None if args.mc_abi is None else (0x00010000 if args.mc_abi == "1.0" else 0x00020000)
|
|
assembly = assemble(args.source.read_text(encoding="utf-8"), args.target, table_payload, selected_abi)
|
|
args.output.write_bytes(assembly.bundle)
|
|
artifacts = {
|
|
args.symbols: assembly.symbols,
|
|
args.cfg_report: {
|
|
**_artifact_data(assembly),
|
|
"entries": [entry.__dict__ for entry in assembly.entries],
|
|
"control_flow": [
|
|
control_flow_report(
|
|
assembly.instructions, entry, assembly.target, assembly.mc_abi
|
|
)
|
|
for entry in assembly.entries
|
|
],
|
|
},
|
|
args.primitive_manifest: {
|
|
"target": assembly.target,
|
|
"implemented_mask": {
|
|
"p3": 0,
|
|
"p4": 0x0000FFFF,
|
|
"p5": 0x001FFFFF,
|
|
}[assembly.target],
|
|
"required_mask": __import__("functools").reduce(
|
|
int.__or__, (entry.primitive_mask for entry in assembly.entries), 0
|
|
),
|
|
"entry_masks": [entry.primitive_mask for entry in assembly.entries],
|
|
},
|
|
args.trace_input: {
|
|
"entries": [entry.entry_pc for entry in assembly.entries],
|
|
"instructions": [f"0x{item.encode():016x}" for item in assembly.instructions],
|
|
},
|
|
args.abi_input_manifest: {**_artifact_data(assembly), "canonical_abi": abi_manifest()},
|
|
}
|
|
for path, data in artifacts.items():
|
|
if path is not None:
|
|
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|