import os
import sys
import json
import csv
import time

VERSION = "V7-direct-config-trace-v100-gcc-version-sync-gdr142"
TRACE_LEN = 20


# Inlined from compiled_opcodes.txt (107 entries in v19).  This is intentionally
# metadata/fallback width information, not a full semantic implementation of the
# generated SLEIGH pseudocode.  The targeted hand-written handlers below remain
# authoritative for the GDR-8050L E7/HL trace path.
INLINED_SLIPSTREAM_OPCODES = {
    0x00: ('clr DREG_S0_HI', 1),
    0x01: ('mov DREG_S1_HI, MEM_ABS16_S2', 1),
    0x02: ('movbu DREG_S1_HI, MEM8_ABS16_S2', 1),
    0x03: ('movhu DREG_S1_HI, MEM16_ABS16_S2', 1),
    0x10: ('extb DREG_S0_LO', 1),
    0x14: ('extbu DREG_S0_LO', 1),
    0x18: ('exth DREG_S0_LO', 1),
    0x1C: ('exthu DREG_S0_LO', 1),
    0x20: ('add SIMM8_B1, AREG_S1_LO', 1),
    0x24: ('mov IMM16_B23, AREG_S1_LO', 1),
    0x28: ('add SIMM8_B1, DREG_S1_LO', 1),
    0x2C: ('mov SIMM16_B23, DREG_S1_LO', 1),
    0x30: ('mov MEM_ABS16_S2, DREG_S1_LO', 1),
    0x34: ('movbu MEM8_ABS16_S2, DREG_S1_LO', 1),
    0x38: ('movhu MEM16_ABS16_S2, DREG_S1_LO', 1),
    0x3C: ('mov SP, AREG_S0_LO', 1),
    0x40: ('inc DREG_S0_HI', 1),
    0x41: ('inc AREG_S0_HI', 1),
    0x42: ('mov DREG_S1_HI, MEM_SP_S1', 1),
    0x43: ('mov AREG_S1_HI, MEM_SP_S1', 1),
    0x50: ('inc4 AREG_S0_LO', 1),
    0x54: ('asl2', 1),
    0x58: ('mov MEM_SP_S1, DREG_S1_LO', 1),
    0x5C: ('mov MEM_SP_S1, AREG_S1_LO', 1),
    0x60: ('mov DREG_S0_HI, MEM_AREG_S0_LO', 1),
    0x70: ('mov MEM_AREG_S0_LO, DREG_S0_HI', 1),
    0x80: ('mov SIMM8_B1, DREG_S1_EQ', 1),
    0x81: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x82: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x83: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x84: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x86: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x87: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x88: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x89: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x8B: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x8C: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x8D: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x8E: ('mov DREG_S0_HI, DREG_S0_LO', 1),
    0x90: ('mov IMM8_B1, AREG_S1_EQ', 1),
    0x91: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x92: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x93: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x94: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x96: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x97: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x98: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x99: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x9B: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x9C: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x9D: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0x9E: ('mov AREG_S0_HI, AREG_S0_LO', 1),
    0xA0: ('cmp SIMM8_B1, DREG_S1_EQ', 1),
    0xA1: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA2: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA3: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA4: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA6: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA7: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA8: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xA9: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xAB: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xAC: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xAD: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xAE: ('cmp DREG_S0_HI, DREG_S0_LO', 1),
    0xB0: ('cmp IMM8_B1, AREG_S1_EQ', 1),
    0xB1: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB2: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB3: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB4: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB6: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB7: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB8: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xB9: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xBB: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xBC: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xBD: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xBE: ('cmp AREG_S0_HI, AREG_S0_LO', 1),
    0xCB: ('nop', 1),
    0xCE: ('movm', 2),
    0xCF: ('movm', 2),
    0xD0: ('llt', 1),
    0xD1: ('lgt', 1),
    0xD2: ('lge', 1),
    0xD3: ('lle', 1),
    0xD4: ('lcs', 1),
    0xD5: ('lhi', 1),
    0xD6: ('lcc', 1),
    0xD7: ('lls', 1),
    0xD8: ('leq', 1),
    0xD9: ('lne', 1),
    0xDA: ('lra', 1),
    0xDB: ('setlb', 1),
    0xE0: ('add DREG_S0_HI, DREG_S0_LO', 1),
    0xF0: ('fleq', 2),
    0xF2: ('mul DREG_D0_HI, DREG_D0_LO', 2),
    0xF5: ('udf20', 2),
    0xF6: ('mcst9', 2),
    0xF7: ('add_add RREG_B3_HI, RREG_B3_LO, RREG_B2_HI, RREG_B2_LO', 4),
    0xF8: ('movm', 3),
    0xF9: ('fabs FSM0_D6', 3),
    0xFA: ('and', 4),
    0xFB: ('ftoi FSN3_D10, FSN1_D7', 4),
    0xFC: ('and', 6),
    0xFD: ('mul SIMM24_B345, RREG_RN02_EQ', 6),
    0xFE: ('fcmp IMM32HI8_B3456, FSM3_D789', 7),
    0xFF: ('break', 1)
}


def parse_int(value, default=0):
    """Parse int/hex strings used in JSON settings."""
    if value is None:
        return default
    if isinstance(value, int):
        return value
    if isinstance(value, str):
        s = value.strip().replace("_", "")
        if not s:
            return default
        return int(s, 0)
    return int(value)


def hex_bytes(data):
    return "".join(f"{b & 0xFF:02X}" for b in data)


def resolve_workspace_path(workspace, path_value):
    """Resolve JSON paths portably across Windows/Linux/macOS test runners."""
    raw = str(path_value or "")
    if not raw:
        return raw
    normalized = raw.replace("\\", os.sep).replace("/", os.sep)
    if os.path.isabs(normalized):
        return os.path.normpath(normalized)
    return os.path.normpath(os.path.join(workspace, normalized))


def version_token(text):
    """Return the most-specific vNN token from an emulator/config string.

    The emulator VERSION strings are prefixed with a major line such as
    ``V7-...-v28-...``.  The old parser returned the first match (``v7``),
    which rejected matching v28 JSON configs.  Prefer the last vNN token so
    package/config version checks track the harness generation.
    """
    s = str(text or "").lower()
    matches = []
    for i, ch in enumerate(s):
        if ch != "v":
            continue
        j = i + 1
        digits = []
        while j < len(s) and s[j].isdigit():
            digits.append(s[j])
            j += 1
        if digits:
            matches.append("v" + "".join(digits))
    return matches[-1] if matches else ""


class TraceRecorder:
    """JSON-configured breakpoint/watchpoint/event recorder."""
    CSV_FIELDS = [
        "seq", "cycle", "event", "name", "pc", "addr", "addr_physical",
        "size", "value", "bytes_hex", "d0", "d1", "d2", "d3", "a0", "a1",
        "a2", "a3", "sp", "notes"
    ]

    def __init__(self, config=None):
        self.config = config or {}
        self.enabled = bool(self.config.get("enabled", False))
        self.console = bool(self.config.get("console", False))
        self.seq = 0
        self.current_cycle = 0
        self.breakpoints = {}
        self.watchpoints = []
        self.synthetic_hooks = []
        self._done_hooks = set()
        self.csv_fp = None
        self.csv_writer = None
        self.base_dir = self.config.get("base_dir") or os.getcwd()
        self.event_counts = {}

        # v28: compact transaction ledger for the DF00/DF28 packet queue.
        self.transaction_ledger = self.config.get("transaction_ledger", {}) or {}
        self.transaction_ledger_enabled = bool(self.transaction_ledger.get("enabled", False))
        self.transaction_console = bool(self.transaction_ledger.get("console", self.console))
        default_pcs = ["0x90022388", "0x900223CF"]
        self.queue_submit_pcs = {parse_int(x) & 0xFFFFFFFF for x in self.transaction_ledger.get("submit_pcs", default_pcs)}
        self.queue_payload_addr = parse_int(self.transaction_ledger.get("payload_addr", "0x0000DF00"))
        self.queue_opcode_addr = parse_int(self.transaction_ledger.get("opcode_addr", "0x0000DF28"))
        qlens = {0x5D: 3, 0x59: 4, 0x51: 14, 0x20: 2}
        for k, v in (self.transaction_ledger.get("lengths", {}) or {}).items():
            qlens[parse_int(k) & 0xFF] = parse_int(v)
        self.queue_lengths = qlens
        self.queue_max_len = parse_int(self.transaction_ledger.get("max_len", 16), 16) if isinstance(self.transaction_ledger.get("max_len", 16), str) else int(self.transaction_ledger.get("max_len", 16))
        self._queue_seen = set()

        # v28: low-noise register-pointer taint markers for target buffers.
        self.pointer_tracking = self.config.get("pointer_tracking", {}) or {}
        self.pointer_tracking_enabled = bool(self.pointer_tracking.get("enabled", False))
        self.pointer_values = {parse_int(x) & 0xFFFFFFFF for x in self.pointer_tracking.get("values", [])}
        self.pointer_ranges = []
        for r in self.pointer_tracking.get("ranges", []):
            start = parse_int(r.get("start", r.get("address", r.get("addr")))) & 0xFFFFFFFF
            if "end" in r:
                end = parse_int(r["end"]) & 0xFFFFFFFF
            else:
                size = parse_int(r.get("size", 1), 1)
                end = (start + size - 1) & 0xFFFFFFFF
            name = r.get("name", f"0x{start:08X}-0x{end:08X}")
            self.pointer_ranges.append((start, end, name))
        self._last_pointer_hits = {}

        for bp in self.config.get("breakpoints", []):
            if not bp.get("enabled", True):
                continue
            addr = parse_int(bp.get("address", bp.get("addr")))
            self.breakpoints.setdefault(addr, []).append(bp)

        for wp in self.config.get("watchpoints", []):
            if not wp.get("enabled", True):
                continue
            start = parse_int(wp.get("start", wp.get("address", wp.get("addr"))))
            if "end" in wp:
                end = parse_int(wp["end"])
            else:
                size = parse_int(wp.get("size", 1), 1)
                end = start + size - 1
            kind = str(wp.get("kind", "write")).lower()
            wp2 = dict(wp)
            wp2["start_int"] = start
            wp2["end_int"] = end
            wp2["kind"] = kind
            self.watchpoints.append(wp2)

        for hook in self.config.get("synthetic_hooks", []):
            if hook.get("enabled", True):
                self.synthetic_hooks.append(dict(hook))

        if self.enabled:
            out_csv = self.config.get("out_csv") or self.config.get("csv")
            if out_csv:
                if not os.path.isabs(out_csv):
                    out_csv = os.path.join(self.base_dir, out_csv)
                os.makedirs(os.path.dirname(os.path.abspath(out_csv)), exist_ok=True)
                self.csv_fp = open(out_csv, "w", newline="", encoding="utf-8")
                self.csv_writer = csv.DictWriter(self.csv_fp, fieldnames=self.CSV_FIELDS)
                self.csv_writer.writeheader()

    def close(self):
        if self.csv_fp:
            self.csv_fp.flush()
            self.csv_fp.close()
            self.csv_fp = None

    def _reg_snapshot(self, cpu):
        if cpu is None:
            return {}
        return {
            "d0": f"0x{cpu.d[0]:08X}", "d1": f"0x{cpu.d[1]:08X}",
            "d2": f"0x{cpu.d[2]:08X}", "d3": f"0x{cpu.d[3]:08X}",
            "a0": f"0x{cpu.a[0]:08X}", "a1": f"0x{cpu.a[1]:08X}",
            "a2": f"0x{cpu.a[2]:08X}", "a3": f"0x{cpu.a[3]:08X}",
            "sp": f"0x{cpu.sp:08X}",
        }

    def emit(self, event, name="", pc=None, addr=None, addr_physical=None,
             size=None, value=None, bytes_hex_str="", cpu=None, notes=""):
        if not self.enabled:
            return
        self.seq += 1
        self.event_counts[event] = self.event_counts.get(event, 0) + 1
        row = {k: "" for k in self.CSV_FIELDS}
        row.update({
            "seq": self.seq,
            "cycle": self.current_cycle,
            "event": event,
            "name": name or "",
            "pc": f"0x{pc:08X}" if isinstance(pc, int) else (pc or ""),
            "addr": f"0x{addr:08X}" if isinstance(addr, int) else (addr or ""),
            "addr_physical": f"0x{addr_physical:08X}" if isinstance(addr_physical, int) else (addr_physical or ""),
            "size": size if size is not None else "",
            "value": f"0x{value:X}" if isinstance(value, int) else (value or ""),
            "bytes_hex": bytes_hex_str or "",
            "notes": notes or "",
        })
        row.update(self._reg_snapshot(cpu))
        if self.csv_writer:
            self.csv_writer.writerow(row)
        if self.console:
            print(f"[TRACE] {row['cycle']} {event} {name} pc={row['pc']} addr={row['addr']} bytes={row['bytes_hex']} {notes}")

    def eval_expr(self, expr, cpu, mem=None, default=0):
        if expr is None:
            return default
        if isinstance(expr, int):
            return expr
        s = str(expr).strip().lower().replace(" ", "")
        regs = {
            "d0": cpu.d[0], "d1": cpu.d[1], "d2": cpu.d[2], "d3": cpu.d[3],
            "a0": cpu.a[0], "a1": cpu.a[1], "a2": cpu.a[2], "a3": cpu.a[3],
            "sp": cpu.sp, "pc": cpu.pc,
        }
        if "|" in s:
            v = 0
            for part in s.split("|"):
                v |= self.eval_expr(part, cpu, mem, 0)
            return v & 0xFFFFFFFF
        for op in ("+", "-"):
            if op in s[1:]:
                lhs, rhs = s.split(op, 1)
                base = self.eval_expr(lhs, cpu, mem, default)
                off = self.eval_expr(rhs, cpu, mem, 0)
                return (base + off) & 0xFFFFFFFF if op == "+" else (base - off) & 0xFFFFFFFF
        if s in regs:
            return regs[s]
        return parse_int(s, default)

    def _capture_notes(self, bp, cpu, mem):
        parts = []
        for cap in bp.get("capture_memory", []):
            name = cap.get("name") or cap.get("expr") or "mem"
            addr = self.eval_expr(cap.get("expr", cap.get("address", cap.get("addr"))), cpu, mem)
            length = self.eval_expr(cap.get("length", cap.get("len", 16)), cpu, mem, 16)
            length = max(0, min(int(length), int(cap.get("max_len", 256))))
            data = [mem.read_byte(addr + i) for i in range(length)] if length else []
            parts.append(f"{name}@0x{addr:08X}[{length}]={hex_bytes(data)}")
        return "; ".join(parts)

    def before_instruction(self, cycle, pc, cpu, mem):
        self.current_cycle = cycle
        if not self.enabled:
            return
        for bp in self.breakpoints.get(pc, []):
            notes = self._capture_notes(bp, cpu, mem)
            self.emit("break", bp.get("name", f"bp_0x{pc:08X}"), pc=pc, cpu=cpu, notes=notes)
        self.maybe_emit_queue_submit(pc, cpu, mem)
        self._run_synthetic_hooks(pc, cpu, mem)

    def _run_synthetic_hooks(self, pc, cpu, mem):
        for hook in self.synthetic_hooks:
            name = hook.get("name", "synthetic_hook")
            if hook.get("once", False) and name in self._done_hooks:
                continue
            trigger_pc = parse_int(hook.get("pc", hook.get("address", 0)))
            if pc != trigger_pc:
                continue
            cond = hook.get("when", {})
            ok = True
            if "mem8" in cond:
                a = parse_int(cond["mem8"])
                got = mem.read_byte(a)
                want = parse_int(cond.get("equals", cond.get("value", got)))
                ok = (got == (want & 0xFF))
            if not ok:
                continue
            for action in hook.get("actions", []):
                if "write_memory" in action:
                    wm = action["write_memory"]
                    addr = parse_int(wm.get("address", wm.get("addr")))
                    data_hex = str(wm.get("data_hex", "")).replace(" ", "")
                    data = bytes.fromhex(data_hex)
                    for i, b in enumerate(data):
                        mem.write_byte(addr + i, b)
                    self.emit("synthetic_write", name, pc=pc, addr=addr, size=len(data),
                              bytes_hex_str=data_hex.upper(), cpu=cpu,
                              notes=action.get("description", "synthetic memory write"))
                if "set_registers" in action:
                    updates = action["set_registers"] or {}
                    notes = []
                    for reg, raw in updates.items():
                        r = str(reg).strip().lower()
                        val = self.eval_expr(raw, cpu, mem, 0) & 0xFFFFFFFF
                        if r.startswith("d") and r[1:].isdigit() and 0 <= int(r[1:]) <= 3:
                            cpu.d[int(r[1:])] = val
                        elif r.startswith("a") and r[1:].isdigit() and 0 <= int(r[1:]) <= 3:
                            cpu.a[int(r[1:])] = val
                        elif r == "sp":
                            cpu.sp = val
                        elif r == "pc":
                            cpu.pc = val
                        else:
                            continue
                        notes.append(f"{r}=0x{val:08X}")
                    self.emit("synthetic_regs", name, pc=pc, cpu=cpu,
                              notes=(action.get("description", "synthetic register update") + "; " + ", ".join(notes)).strip())
                if "set_pc" in action:
                    val = self.eval_expr(action["set_pc"], cpu, mem, 0) & 0xFFFFFFFF
                    cpu.pc = val
                    self.emit("synthetic_pc", name, pc=pc, cpu=cpu,
                              notes=action.get("description", f"synthetic pc=0x{val:08X}"))
            if hook.get("once", False):
                self._done_hooks.add(name)

    def check_watch(self, kind, logical_addr, physical_addr, size, value, bytes_hex_str, pc, cpu):
        if not self.enabled:
            return
        k = kind.lower()
        end_logical = logical_addr + max(size, 1) - 1
        end_physical = physical_addr + max(size, 1) - 1
        for wp in self.watchpoints:
            wk = wp.get("kind", "write")
            if wk not in (k, "access", "readwrite", "rw"):
                continue
            ws, we = wp["start_int"], wp["end_int"]
            hit_logical = not (end_logical < ws or logical_addr > we)
            hit_phys = not (end_physical < ws or physical_addr > we)
            if hit_logical or hit_phys:
                self.emit(f"watch_{k}", wp.get("name", f"watch_{ws:08X}"), pc=pc,
                          addr=logical_addr, addr_physical=physical_addr, size=size,
                          value=value, bytes_hex_str=bytes_hex_str, cpu=cpu,
                          notes=wp.get("description", ""))

    def _queue_len_for_opcode(self, opcode):
        return max(0, min(int(self.queue_lengths.get(opcode & 0xFF, self.queue_max_len)), self.queue_max_len))

    def _decode_queue_notes(self, opcode, payload):
        parts = [f"opcode=0x{opcode:02X}", f"len={len(payload)}"]
        if opcode == 0x51 and len(payload) >= 14:
            subcode = payload[0]
            ptr = payload[10] | (payload[11] << 8) | (payload[12] << 16) | (payload[13] << 24)
            parts.append(f"subcode=0x{subcode:02X}")
            parts.append(f"ptr=0x{ptr:08X}")
        return "; ".join(parts)

    def maybe_emit_queue_submit(self, pc, cpu, mem):
        if not (self.enabled and self.transaction_ledger_enabled):
            return
        pc &= 0xFFFFFFFF
        if pc not in self.queue_submit_pcs:
            return
        opcode = mem.read_byte(self.queue_opcode_addr) & 0xFF
        length = self._queue_len_for_opcode(opcode)
        payload = [mem.read_byte(self.queue_payload_addr + i) for i in range(length)]
        key = (self.current_cycle, pc, opcode, hex_bytes(payload))
        if key in self._queue_seen:
            return
        self._queue_seen.add(key)
        notes = self._decode_queue_notes(opcode, payload)
        self.emit("queue_submit", f"queue_0x{opcode:02X}", pc=pc, addr=self.queue_payload_addr,
                  size=length, value=opcode, bytes_hex_str=hex_bytes(payload), cpu=cpu, notes=notes)
        if self.transaction_console:
            print(f"[QUEUE] pc=0x{pc:08X} {notes} payload={hex_bytes(payload)}")

    def _pointer_hit_name(self, value):
        v = value & 0xFFFFFFFF
        if v in self.pointer_values:
            return f"exact_0x{v:08X}"
        for start, end, name in self.pointer_ranges:
            if start <= v <= end:
                return name
        return ""

    def after_instruction(self, cycle, pc, cpu, mem):
        self.current_cycle = cycle
        if not (self.enabled and self.pointer_tracking_enabled):
            return
        regs = [
            ("d0", cpu.d[0]), ("d1", cpu.d[1]), ("d2", cpu.d[2]), ("d3", cpu.d[3]),
            ("a0", cpu.a[0]), ("a1", cpu.a[1]), ("a2", cpu.a[2]), ("a3", cpu.a[3]),
            ("sp", cpu.sp),
        ]
        for reg, value in regs:
            name = self._pointer_hit_name(value)
            last = self._last_pointer_hits.get(reg)
            cur = (value & 0xFFFFFFFF, name) if name else None
            if cur and cur != last:
                self.emit("ptr_taint", f"{reg}_{name}", pc=pc, addr=value & 0xFFFFFFFF,
                          value=value & 0xFFFFFFFF, cpu=cpu, notes=f"{reg}=0x{value & 0xFFFFFFFF:08X} matches {name}")
            self._last_pointer_hits[reg] = cur

class DSPPeripheral:
    def __init__(self, name, base_addr, size):
        self.name = name
        self.base_addr = base_addr
        self.size = size
        self.registers = {}
        
    def read_byte(self, address):
        # When the CPU polls a hardware register, we return 0xFF by default.
        # In hardware, returning all 1s (0xFF) usually satisfies "Ready" or "Ack" bit-tests.
        val = self.registers.get(address, 0xFF)
        print(f"[DSP] Read 0x{val:02X} from {self.name} at 0x{address:08X}")
        return val
        
    def write_byte(self, address, value):
        # We store what the CPU writes so we can trace the exact laser commands!
        print(f"[DSP] Write 0x{value:02X} to {self.name} at 0x{address:08X}")
        self.registers[address] = value

class MemoryController:
    def __init__(self, rom_base=0x90000000):
        self.memory = {} 
        self.rom_base = rom_base
        self.mem_trace = []
        self.peripherals = []
        self.trace_recorder = None
        self.cpu_ref = None
        self.aliases = []
        self.read_overrides = []
        self.gcc_transfer_model = None
        self.gcc_dma_status_model = None
        self.gdr_packet_data_in_model = None
        self.gdr_packet_source_stream_model = None
        self.gdr_hardware_model = None
        self.gdr_packet_session_model = None


    def set_gdr_packet_session_model(self, model):
        """Install a persistent multi-command GDR packet-session model.

        v123 keeps the firmware alive across more than one SCSI/ATAPI packet.
        Earlier probes drove exactly one packet with static read overrides, then
        stopped after the firmware returned to the main loop.  This model owns
        the DF30..DF37 packet latch, DF90..DF97 DATA OUT FIFO, 0x01C8 DATA IN
        source stream, and per-command completion advance.  It advances to the
        next command only after the current command has passed the packet timer
        checkpoint and returned to the main loop.
        """
        cfg = model or {}
        enabled = bool(cfg.get("enabled", bool(cfg)))

        def _parse_bytes(src):
            if src is None:
                return []
            if isinstance(src, (bytes, bytearray)):
                return [int(x) & 0xFF for x in src]
            if isinstance(src, str):
                parts = src.replace(",", " ").split()
            elif isinstance(src, list):
                parts = src
            else:
                parts = [src]
            out = []
            for x in parts:
                try:
                    token = str(x).strip()
                    if not token:
                        continue
                    if token == "...":
                        break
                    if token.lower().startswith(("0x", "-0x")):
                        out.append(parse_int(token) & 0xFF)
                    else:
                        out.append(int(token, 16) & 0xFF)
                except Exception:
                    try:
                        out.append(parse_int(x) & 0xFF)
                    except Exception:
                        pass
            return out

        cmds = []
        for i, raw in enumerate(cfg.get("commands", []) or []):
            cdb = _parse_bytes(raw.get("cdb", raw.get("cdb_hex", raw.get("packet_cdb"))))
            data_in = _parse_bytes(raw.get("data_in", raw.get("data_in_hex", raw.get("payload", raw.get("payload_hex")))))
            data_out = _parse_bytes(raw.get("data_out", raw.get("data_out_hex", raw.get("host_payload", raw.get("host_payload_hex")))))
            direction = str(raw.get("direction", "in" if data_in else "out" if data_out else "none")).strip().lower()
            cmds.append({
                "index": i,
                "tag": str(raw.get("tag", raw.get("name", f"cmd{i:02d}"))),
                "title": str(raw.get("title", raw.get("tag", f"cmd{i:02d}"))),
                "direction": direction,
                "cdb": [x & 0xFF for x in cdb],
                "data_in": [x & 0xFF for x in data_in],
                "data_out": [x & 0xFF for x in data_out],
                "state": str(raw.get("state", "")),
            })
        firmware_ingress_cfg = dict(cfg.get("firmware_ingress", {}) or {})
        self.gdr_packet_session_model = {
            "enabled": enabled and bool(cmds),
            "emit_events": bool(cfg.get("emit_events", True)),
            "commands": cmds,
            "current": 0,
            "armed": False,
            "phase_started": False,
            "timer_done_seen": False,
            "completed": False,
            "session_done": False,
            "irq_pulse_pending": True,
            "source_index": 0,
            "data_out_index": 0,
            "df70_stream": [],
            "ready": False,
            "submitted": False,
            "packet_frame_delivered": False,
            "packet_tail_delivered": False,
            "d950_post_frame_reads": 0,
            "data_phase_request_seen": False,
            "data_phase_irq_pending": False,
            "data_phase_irq_latched_low_read": False,
            "data_phase_irq_pulsed": False,
            "transfer_helper_seen": False,
            "data_phase_hw_completed": False,
            "packet_flags": parse_int(cfg.get("packet_flags", "0x20")) & 0xFF,
            "df30_base": parse_int(cfg.get("df30_base", "0x0000DF30")) & 0xFFFFFFFF,
            "df90_base": parse_int(cfg.get("df90_base", "0x0000DF90")) & 0xFFFFFFFF,
            "df70_base": parse_int(cfg.get("df70_base", "0x0000DF70")) & 0xFFFFFFFF,
            "df2a_addr": parse_int(cfg.get("df2a_addr", "0x0000DF2A")) & 0xFFFFFFFF,
            "source_addr": parse_int(cfg.get("source_addr", "0x000001C8")) & 0xFFFFFFFF,
            "source_window": max(1, parse_int(cfg.get("source_window", "0x08"), 8)),
            "main_loop_pc": parse_int(cfg.get("main_loop_pc", "0x90006608")) & 0xFFFFFFFF,
            "timer_done_pc": parse_int(cfg.get("timer_done_pc", "0x90008824")) & 0xFFFFFFFF,
            "stop_on_complete": bool(cfg.get("stop_on_complete", True)),
            "armed_cycle": 0,
            "host_event_gate": cfg.get("host_event_gate", {}) or {},
            "firmware_ingress": {
                "enabled": bool(firmware_ingress_cfg.get("enabled", False)),
                "entry_pc": parse_int(firmware_ingress_cfg.get("entry_pc", "0x9001E4E5")) & 0xFFFFFFFF,
                "trigger_pc": parse_int(firmware_ingress_cfg.get("trigger_pc", "0x90006608")) & 0xFFFFFFFF,
                "return_pc": firmware_ingress_cfg.get("return_pc"),
                "frame_size": parse_int(firmware_ingress_cfg.get("frame_size", "0x08")) & 0xFF,
                "regs_mask": parse_int(firmware_ingress_cfg.get("regs_mask", "0x00")) & 0xFF,
                "opcodes": [parse_int(x) & 0xFF for x in (firmware_ingress_cfg.get("opcodes", [0x28, 0xA8, 0xBE]) or [])],
                "seed_dfa0": bool(firmware_ingress_cfg.get("seed_dfa0", True)),
                "only_after_unlock": bool(firmware_ingress_cfg.get("only_after_unlock", False)),
                "mark_phase_started": bool(firmware_ingress_cfg.get("mark_phase_started", False)),
                "emit_05ec_source_event": bool(firmware_ingress_cfg.get("emit_05ec_source_event", False)),
                "use_05ec_source_for_read": bool(firmware_ingress_cfg.get("use_05ec_source_for_read", False)),
            },
        }
        if self.gdr_packet_session_model["enabled"]:
            print(f"[*] GDR persistent packet session enabled: commands={len(cmds)} main_loop=0x{self.gdr_packet_session_model['main_loop_pc']:08X} timer_done=0x{self.gdr_packet_session_model['timer_done_pc']:08X}.")
            self._gdr_packet_session_arm(0, reason="initial")

    def _gdr_packet_session_current(self):
        model = getattr(self, "gdr_packet_session_model", None)
        if not model or not model.get("enabled", False):
            return None, None
        idx = int(model.get("current", 0))
        cmds = model.get("commands") or []
        if idx < 0 or idx >= len(cmds):
            return model, None
        return model, cmds[idx]

    def _gdr_packet_session_emit(self, event, name, *, addr=None, size=None, value=None, bytes_list=None, notes=""):
        model = getattr(self, "gdr_packet_session_model", None)
        if not model or not model.get("emit_events", True) or not self.trace_recorder:
            return
        pc = self.cpu_ref.pc if self.cpu_ref else 0
        bhex = ""
        if bytes_list is not None:
            bhex = " ".join(f"{int(b) & 0xFF:02X}" for b in bytes_list)
        self.trace_recorder.emit(event, name, pc=pc, addr=addr, size=size, value=value, bytes_hex_str=bhex, cpu=self.cpu_ref, notes=notes)

    def _gdr_packet_session_arm(self, idx, reason="advance"):
        model = getattr(self, "gdr_packet_session_model", None)
        if not model or not model.get("enabled", False):
            return
        cmds = model.get("commands") or []
        if idx >= len(cmds):
            model["session_done"] = True
            model["armed"] = False
            self._gdr_packet_session_emit("gdr_session_complete", "all_commands_complete", notes=f"commands={len(cmds)}")
            if model.get("stop_on_complete", True) and self.cpu_ref is not None:
                setattr(self.cpu_ref, "stop_requested", True)
                setattr(self.cpu_ref, "stop_reason", "gdr_persistent_session_complete")
            return
        model.update({
            "current": idx,
            "armed": True,
            "phase_started": False,
            "timer_done_seen": False,
            "completed": False,
            "source_index": 0,
            "data_out_index": 0,
            "df70_stream": [],
            "ready": False,
            "submitted": False,
            "packet_frame_delivered": False,
            "packet_tail_delivered": False,
            "d950_post_frame_reads": 0,
            "data_phase_request_seen": False,
            "data_phase_irq_pending": False,
            "data_phase_irq_latched_low_read": False,
            "data_phase_irq_pulsed": False,
            "transfer_helper_seen": False,
            "data_phase_hw_completed": False,
            "firmware_ingress_ran": False,
            "firmware_lba_source_emitted": False,
            "irq_pulse_pending": True,
            "armed_cycle": self._gdr_hardware_current_cycle(),
        })
        cmd = cmds[idx]
        gate = model.get("host_event_gate") or {}
        if gate.get("enabled"):
            # v124 diagnostic only: seed observable host-event state for the next-command re-entry
            # path so we can see which additional firmware gate prevents packet parsing.
            for addr_s, val_s in (gate.get("seed_mem8") or {}).items():
                try:
                    a = self.normalize_address(parse_int(addr_s))
                    v = parse_int(val_s) & 0xFF
                    self.memory[a] = v
                    self._gdr_packet_session_emit("gdr_session_host_gate_seed", cmd.get("tag", "cmd"), addr=a, size=1, value=v, bytes_list=[v], notes=f"index={idx}; reason={reason}")
                except Exception:
                    pass
        self._gdr_packet_session_emit(
            "gdr_session_command_arm", cmd.get("tag", f"cmd{idx:02d}"),
            bytes_list=cmd.get("cdb", []), value=idx,
            notes=f"index={idx}; direction={cmd.get('direction')}; reason={reason}; data_in_len={len(cmd.get('data_in') or [])}; data_out_len={len(cmd.get('data_out') or [])}"
        )

    def _gdr_packet_session_cmd_lba(self, cmd):
        cdb = [int(x) & 0xFF for x in (cmd.get("cdb") or [])]
        if len(cdb) >= 10 and cdb[0] == 0x28:
            return ((cdb[2] << 24) | (cdb[3] << 16) | (cdb[4] << 8) | cdb[5]) & 0xFFFFFFFF
        if len(cdb) >= 12 and cdb[0] == 0xA8:
            return ((cdb[2] << 24) | (cdb[3] << 16) | (cdb[4] << 8) | cdb[5]) & 0xFFFFFFFF
        return None

    def _gdr_packet_session_emit_firmware_lba_source(self, cmd, *, reason="data_phase"):
        """Emit proof evidence tying the active READ data phase to firmware 0x05EC.

        v173 proved the ingress routine can write 0x05EC for the active CDB.
        v174 makes that state an explicit source-decision surface for READ data
        phases, without claiming that the lower-level packet FIFO route is natural
        yet.  The event is emitted only for READ-family commands when enabled.
        """
        model = getattr(self, "gdr_packet_session_model", None) or {}
        ingress = model.get("firmware_ingress") or {}
        if not (ingress.get("emit_05ec_source_event") or ingress.get("use_05ec_source_for_read")):
            return None
        cdb_lba = self._gdr_packet_session_cmd_lba(cmd)
        if cdb_lba is None:
            return None
        try:
            fw_lba = self.read_dword(0x000005EC) & 0xFFFFFFFF
        except Exception:
            fw_lba = 0
        # This bridge window delta is model metadata for comparison only; the
        # event's proof value is the firmware-written host/source LBA in 0x05EC.
        bridge_iso = (fw_lba + 306080) & 0xFFFFFFFF
        matched = (fw_lba == cdb_lba)
        self._gdr_packet_session_emit(
            "gdr_session_firmware_lba_source", cmd.get("tag", "cmd"),
            addr=0x000005EC, size=4, value=fw_lba,
            bytes_list=[fw_lba & 0xFF, (fw_lba >> 8) & 0xFF, (fw_lba >> 16) & 0xFF, (fw_lba >> 24) & 0xFF],
            notes=f"index={cmd.get('index')}; reason={reason}; cdb_lba={cdb_lba}; firmware_05ec_lba={fw_lba}; match={1 if matched else 0}; bridge_iso_lba={bridge_iso}"
        )
        return fw_lba

    def _gdr_packet_session_seed_dfa0(self, cmd):
        """Seed the firmware-visible DFA0..DFAB packet/CDB latch from the active command."""
        cdb = [int(x) & 0xFF for x in (cmd.get("cdb") or [])]
        if len(cdb) < 12:
            cdb = cdb + [0] * (12 - len(cdb))
        for i, b in enumerate(cdb[:12]):
            self.memory[self.normalize_address(0x0000DFA0 + i)] = b & 0xFF
        return cdb[:12]

    def _gdr_packet_session_try_firmware_ingress_call(self, cycle, pc):
        """Optionally splice the live persistent command route through the firmware
        READ packet-ingress routine.

        This is a proof/route-discovery mode.  It does not use sidecar LBA math;
        it seeds DFA0..DFAB with the host CDB and performs an architectural call
        into the firmware candidate at 0x9001E4E5, returning to the current PC.
        The normal persistent session then continues, so v170/v171 validators can
        prove whether firmware wrote 0x05EC/0x05F0/0x06F1 for the same command
        window.
        """
        model, cmd = self._gdr_packet_session_current()
        if not model or cmd is None:
            return False
        ingress = model.get("firmware_ingress") or {}
        if not ingress.get("enabled") or model.get("firmware_ingress_ran"):
            return False
        if (pc & 0xFFFFFFFF) != (int(ingress.get("trigger_pc", 0x90006608)) & 0xFFFFFFFF):
            return False
        cdb = [int(x) & 0xFF for x in (cmd.get("cdb") or [])]
        if not cdb:
            return False
        allowed = set(int(x) & 0xFF for x in (ingress.get("opcodes") or []))
        if allowed and (cdb[0] & 0xFF) not in allowed:
            return False
        if ingress.get("only_after_unlock") and int(cmd.get("index", 0)) < 9:
            return False
        if ingress.get("seed_dfa0", True):
            cdb12 = self._gdr_packet_session_seed_dfa0(cmd)
        else:
            cdb12 = (cdb + [0] * 12)[:12]

        old_sp = int(getattr(self.cpu_ref, "sp", 0)) & 0xFFFFFFFF
        frame_size = int(ingress.get("frame_size", 8)) & 0xFF
        regs_mask = int(ingress.get("regs_mask", 0)) & 0xFF
        return_pc_raw = ingress.get("return_pc")
        return_pc = (pc if return_pc_raw is None else parse_int(return_pc_raw)) & 0xFFFFFFFF
        entry_pc = int(ingress.get("entry_pc", 0x9001E4E5)) & 0xFFFFFFFF

        # Minimal architectural CALL frame matching the emulator's CALL/RET model.
        self.write_dword(old_sp, return_pc)
        # No register-save bits are used by default, but honor regs_mask for
        # exploratory variants if future traces require it.
        try:
            regs = []
            order = [
                (0x80, "d", 2), (0x40, "d", 3), (0x20, "a", 2), (0x10, "a", 3),
                (0x08, "d", 0), (0x04, "d", 1), (0x02, "a", 0), (0x01, "a", 1),
            ]
            for bit, kind, idx in order:
                if regs_mask & bit:
                    val = self.cpu_ref.d[idx] if kind == "d" else self.cpu_ref.a[idx]
                    regs.append((kind, idx, val & 0xFFFFFFFF))
            for n, (_kind, _idx, val) in enumerate(regs, start=1):
                self.write_dword((old_sp - 4*n) & 0xFFFFFFFF, val)
        except Exception:
            pass
        self.cpu_ref.sp = (old_sp - frame_size) & 0xFFFFFFFF
        self.cpu_ref.mdr = return_pc
        self.cpu_ref.pc = entry_pc
        model["firmware_ingress_ran"] = True
        if ingress.get("mark_phase_started"):
            model["phase_started"] = True
        self._gdr_packet_session_emit(
            "gdr_session_firmware_ingress_call", cmd.get("tag", "cmd"),
            addr=0x0000DFA0, size=len(cdb12), value=cmd.get("index", 0), bytes_list=cdb12,
            notes=f"index={cmd.get('index')}; opcode=0x{cdb12[0]:02X}; entry=0x{entry_pc:08X}; return=0x{return_pc:08X}; sp=0x{old_sp:08X}->0x{self.cpu_ref.sp:08X}"
        )
        return True

    def gdr_packet_session_before_instruction(self, cycle, pc):
        model, cmd = self._gdr_packet_session_current()
        if not model or cmd is None:
            return
        pc &= 0xFFFFFFFF
        if self._gdr_packet_session_try_firmware_ingress_call(cycle, pc):
            return
        # v174: after the firmware ingress microcall returns to the trigger PC,
        # 0x05EC contains the firmware-assembled LBA for this command.  Emit the
        # source-decision event here, not earlier at D108, so it cannot lag one
        # command behind the active CDB.
        ingress = model.get("firmware_ingress") or {}
        if (pc == int(ingress.get("trigger_pc", model.get("main_loop_pc", 0x90006608))) & 0xFFFFFFFF
            and model.get("firmware_ingress_ran") and not model.get("firmware_lba_source_emitted")):
            self._gdr_packet_session_emit_firmware_lba_source(cmd, reason="post_firmware_ingress")
            model["firmware_lba_source_emitted"] = True
        if pc == model.get("timer_done_pc") and model.get("phase_started"):
            if not model.get("timer_done_seen"):
                model["timer_done_seen"] = True
                self._gdr_packet_session_emit("gdr_session_timer_done", cmd.get("tag", "cmd"), value=cmd.get("index", 0), notes=f"index={cmd.get('index')} cycle={cycle}")
        if pc == model.get("main_loop_pc") and model.get("phase_started") and model.get("timer_done_seen") and not model.get("completed"):
            model["completed"] = True
            idx = int(model.get("current", 0))
            self._gdr_packet_session_emit("gdr_session_command_complete", cmd.get("tag", f"cmd{idx:02d}"), value=idx, notes=f"index={idx}; cycle={cycle}")
            self._gdr_packet_session_arm(idx + 1, reason="post_timer_main_loop")

        # v131 baseline: command reached 0x900222FF/0x90022301/0x9001CD4D
        # but never reached the low-level DATA IN/OUT transfer helper.  0x9001CD4D
        # writes D108 from the just-submitted DF00 request, which appears to be
        # the boundary where the host-interface block should raise another service
        # edge.  Mark that edge here so the next main-loop 0x002C read can see a
        # second short IRQ pulse instead of falling into idle scheduling forever.
        gate = model.get("host_event_gate") or {}
        if gate.get("enabled") and int(cmd.get("index", 0)) > 0 and pc == 0x9001CD4D and model.get("packet_tail_delivered") and not model.get("data_phase_request_seen"):
            model["data_phase_request_seen"] = True
            if bool(gate.get("irq_after_d108_data_phase", True)):
                model["data_phase_irq_pending"] = True
                model["data_phase_irq_latched_low_read"] = False
                model["data_phase_irq_pulsed"] = False
            self._gdr_packet_session_emit("gdr_session_data_phase_request", cmd.get("tag", "cmd"), value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; pc=0x{pc:08X}; irq_after_d108={1 if model.get('data_phase_irq_pending') else 0}")

        if pc == 0x900099EA and model.get("data_phase_request_seen") and not model.get("transfer_helper_seen"):
            model["transfer_helper_seen"] = True
            self._gdr_packet_session_emit("gdr_session_transfer_helper", cmd.get("tag", "cmd"), value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; pc=0x{pc:08X}")

    def _gdr_packet_session_read_byte(self, logical, physical):
        model, cmd = self._gdr_packet_session_current()
        if not model or cmd is None or model.get("session_done"):
            return None
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        addrs = {addr_l, addr_p}
        df30 = model.get("df30_base", 0x0000DF30)
        df90 = model.get("df90_base", 0x0000DF90)
        source_addr = model.get("source_addr", 0x000001C8)
        source_window = model.get("source_window", 8)

        def _off(base, width):
            for a in addrs:
                if base <= a < base + width:
                    return a - base
            return None

        # v129: a second short service/IRQ pulse may be needed after D108 is
        # written for the data phase.  Keep this separate from the initial
        # command-present pulse so the trace tells us which edge is being used.
        if pc == model.get("main_loop_pc", 0x90006608) and model.get("data_phase_irq_pending") and model.get("phase_started") and not model.get("submitted"):
            if 0x0000002C in addrs:
                model["data_phase_irq_latched_low_read"] = True
                return 0x01
            if 0x0000002D in addrs:
                model["data_phase_irq_pending"] = False
                model["data_phase_irq_pulsed"] = True
                self._gdr_packet_session_emit("gdr_session_data_phase_irq_pulse", cmd.get("tag", "cmd"), addr=0x0000002C, size=2, value=0x0401, bytes_list=[0x01, 0x04], notes=f"index={cmd.get('index')}; data_phase_service_pulse=1; low_read={1 if model.get('data_phase_irq_latched_low_read') else 0}")
                return 0x04

        # One-shot main-loop service/IRQ pulse.  After a command completes,
        # the next packet must look like fresh host/service work.  The reset
        # path naturally reaches the first packet, but persistent sessions need
        # a conservative 0x002C bit-0x0400 pulse at the main-loop test.
        if pc == model.get("main_loop_pc", 0x90006608) and model.get("irq_pulse_pending") and not model.get("phase_started"):
            if 0x0000002C in addrs:
                return 0x01
            if 0x0000002D in addrs:
                model["irq_pulse_pending"] = False
                self._gdr_packet_session_emit("gdr_session_irq_pulse", cmd.get("tag", "cmd"), addr=0x0000002C, size=2, value=0x0401, bytes_list=[0x01, 0x04], notes=f"index={cmd.get('index')}; main_loop_service_pulse=1")
                return 0x04

        # v125a: model D950 as a small packet-frame status state, not just a
        # one-shot bit.  For a new command the firmware needs bit 0x10 once to
        # enter 0x9002425D and copy the FE packet frame.  After that frame has
        # been consumed, successful single-command traces show D950 settling at
        # 0x40, not zero.  Returning 0x40 avoids re-copying the same FE frame
        # while preserving the post-frame packet/status condition used by the
        # downstream command dispatcher.
        gate = model.get("host_event_gate") or {}
        if gate.get("enabled") and int(cmd.get("index", 0)) > 0 and 0x0000D950 in addrs:
            def _gate_val_top(name, default):
                try:
                    return parse_int(gate.get(name, default)) & 0xFF
                except Exception:
                    return parse_int(default) & 0xFF
            def _post_frame_d950_value():
                if not model.get("packet_frame_delivered"):
                    return None
                # v127/v129: 0x40 looks like a post-frame status edge/window, not a
                # level to hold forever.  v126 proved holding it forever causes
                # repeated tail copies.  Let the runner choose how many post-frame
                # reads should still see 0x40 before D950 decays to 0.
                limit = -1
                try:
                    limit = parse_int(gate.get("d950_zero_after_post_frame_reads", "-1"))
                except Exception:
                    limit = -1
                if bool(gate.get("clear_d950_after_tail", False)) and model.get("packet_tail_delivered"):
                    return 0x00
                reads = int(model.get("d950_post_frame_reads", 0))
                if limit >= 0 and reads >= limit:
                    return 0x00
                model["d950_post_frame_reads"] = reads + 1
                return _gate_val_top("post_frame_d950_value", "0x40")

            if pc in (0x900210E1, 0x900210EE):
                post = _post_frame_d950_value()
                v = _gate_val_top("force_d950_value", "0x10") if post is None else post
                self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x0000D950, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; d950_packet_frame_available={0 if model.get('packet_frame_delivered') else 1}; post_frame={1 if model.get('packet_frame_delivered') else 0}; tail={1 if model.get('packet_tail_delivered') else 0}; post_reads={model.get('d950_post_frame_reads',0)}; pc=0x{pc:08X}")
                return v
            if pc in (0x90021112, 0x9002111F):
                post = _post_frame_d950_value()
                v = 0x00 if post is None else post
                self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x0000D950, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; d950_packet_frame_post_status; tail={1 if model.get('packet_tail_delivered') else 0}; post_reads={model.get('d950_post_frame_reads',0)}; pc=0x{pc:08X}")
                return v



        # v125b: clear the stale DA18 bit-2 condition for commands after the
        # first.  If DA18&0x04 remains set from the prior command, 0x9001B6D3
        # routes to 0x9001C89B, which sets 0x05B1 bit 0x04 and never schedules
        # the normal packet data path.  New host packet presentation should
        # behave like the successful single-command path and fall through to
        # 0x9001C83E (0x05B1|=0x20).
        if gate.get("enabled") and int(cmd.get("index", 0)) > 0 and 0x0000DA18 in addrs and pc == 0x9001B6D3:
            v = 0x00
            self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x0000DA18, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; force_da18_normal_packet_path")
            return v

        # v125a: keep the FE packet-frame path on the same post-copy branch seen
        # in successful one-shot traces.  If 0x0278 has retained a large value
        # from earlier initialization, 0x900242B3 can return before testing the
        # 0x07E9 packet flags and before calling 0x9001B6D3/0x9001C83E.
        if gate.get("enabled") and int(cmd.get("index", 0)) > 0 and 0x00000278 in addrs and pc == 0x900242B3:
            v = 0x00
            self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x00000278, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; force_0278_low_for_fe_branch")
            return v

        # v126 trial: once a packet frame has been consumed, clear the synthetic
        # host-service selector.  Leaving 0x08A8 at 0x04 causes the scheduler to
        # keep re-entering the packet gate and replaying the CDB tail forever.
        if gate.get("enabled") and gate.get("clear_08a8_after_frame", False) and int(cmd.get("index", 0)) > 0 and 0x000008A8 in addrs and model.get("phase_started") and pc in (0x900240EC, 0x900240FA):
            v = 0x00
            try:
                self.memory[self.normalize_address(0x000008A8)] = v
            except Exception:
                pass
            self._gdr_packet_session_emit("gdr_session_host_gate_clear", cmd.get("tag", "cmd"), addr=0x000008A8, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; clear_08a8_after_packet_frame; pc=0x{pc:08X}")
            return v

        # v124 re-entry diagnostic host-event gate.  Command 0 enters via normal
        # reset/startup state.  Commands after the first need an asynchronous
        # host-command-present edge.  These optional read responses expose the
        # gate chain around 0x9001BBAC -> 0x900240EC without enabling full
        # instruction logging.  They are diagnostics, not final hardware truth.
        if gate.get("enabled") and int(cmd.get("index", 0)) > 0 and not model.get("phase_started"):
            def _gate_val(name, default):
                try:
                    return parse_int(gate.get(name, default)) & 0xFF
                except Exception:
                    return parse_int(default) & 0xFF
            if gate.get("force_016d_active", True) and 0x0000016D in addrs and pc == 0x90008C97:
                v = _gate_val("force_016d_value", "0x01")
                self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x0000016D, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; force_016d_active")
                return v
            if gate.get("force_00de_pollable", True) and 0x000000DE in addrs and pc in (0x9001BBAC, 0x9001BBD2, 0x9001BBD9, 0x9001EC57):
                v = _gate_val("force_00de_value", "0x01")
                self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x000000DE, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; force_00de_pollable; pc=0x{pc:08X}")
                return v
            if gate.get("force_08a8") is not None and 0x000008A8 in addrs and pc == 0x900240EC:
                v = _gate_val("force_08a8", "0x04")
                self._gdr_packet_session_emit("gdr_session_host_gate", cmd.get("tag", "cmd"), addr=0x000008A8, size=1, value=v, bytes_list=[v], notes=f"index={cmd.get('index')}; force_08a8_service_state")
                return v
        # Packet phase FE precheck and 8-byte frame copy.
        if df30 in addrs and pc == 0x900241E4:
            return 0x00
        if df30 in addrs and pc in (0x90024335, 0x90024346, 0x9002426D):
            model["phase_started"] = True
            return 0xFE
        copy_pcs = [0x90024283, 0x90024289, 0x9002428F, 0x90024295, 0x9002429B, 0x900242A1, 0x900242A7, 0x900242AD]
        if pc in copy_pcs:
            lane = _off(df30, 8)
            if lane is not None and not model.get("packet_frame_delivered"):
                cdb = cmd.get("cdb") or []
                frame = [0xFE, model.get("packet_flags", 0x20), 0, 0, 0, 0, 0, 0]
                for i, b in enumerate(cdb[:6], start=2):
                    if i < len(frame):
                        frame[i] = b & 0xFF
                model["phase_started"] = True
                if lane == 0:
                    self._gdr_packet_session_emit("gdr_session_packet_phase", cmd.get("tag", "cmd"), bytes_list=frame, value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; frame=FE")
                if lane == 7:
                    model["packet_frame_delivered"] = True
                    # Successful independent traces show D950 written 0x80 then
                    # 0x40 around 0x9001B708/0x9001B711 after the FE frame.
                    # Keep the visible status at 0x40 instead of clearing it.
                    self.memory[self.normalize_address(0x0000D950)] = parse_int((model.get("host_event_gate") or {}).get("post_frame_d950_value", "0x40")) & 0xFF
                    clear_note = ""
                    if gate.get("clear_08a8_after_frame", False):
                        # The service selector is an edge, not a level.  Clear it as soon as
                        # the packet frame has been accepted so command processing can run.
                        try:
                            self.memory[self.normalize_address(0x000008A8)] = 0x00
                        except Exception:
                            pass
                        clear_note = "; clear_08a8"
                    self._gdr_packet_session_emit("gdr_session_packet_frame_done", cmd.get("tag", "cmd"), addr=0x0000D950, size=1, value=self.memory[self.normalize_address(0x0000D950)], bytes_list=[self.memory[self.normalize_address(0x0000D950)]], notes=f"index={cmd.get('index')}; post_frame_d950{clear_note}")
                return frame[lane] & 0xFF
        tail_pcs = [0x90021169, 0x9002116F, 0x90021175, 0x9002117B, 0x90021181, 0x90021187, 0x9002118D, 0x90021193]
        if pc in tail_pcs:
            lane = _off(df30, 8)
            if lane is not None:
                # v129: some paths, especially the reset/first-command path, reach the
                # CDB tail-copy sequence without taking the synthetic FE frame-copy path.
                # Treat any observed tail copy as the packet phase having started so the
                # later timer-done/main-loop checkpoint can complete and advance the
                # persistent session. v129 showed DATA IN could succeed while phase_started
                # stayed false, preventing command completion/arm of command 2.
                model["phase_started"] = True
                cdb = cmd.get("cdb") or []
                tail = [0] * 8
                for i, b in enumerate(cdb[6:14]):
                    if i < len(tail):
                        tail[i] = b & 0xFF
                if lane == 0:
                    self._gdr_packet_session_emit("gdr_session_packet_tail", cmd.get("tag", "cmd"), bytes_list=tail, value=cmd.get("index", 0), notes=f"index={cmd.get('index')}")
                if lane == 7 and not model.get("packet_tail_delivered"):
                    model["packet_tail_delivered"] = True
                    clear_notes = []
                    if gate.get("clear_08a8_after_tail", False):
                        try:
                            self.memory[self.normalize_address(0x000008A8)] = 0x00
                        except Exception:
                            pass
                        clear_notes.append("08a8")
                    if gate.get("clear_d950_after_tail", False):
                        try:
                            self.memory[self.normalize_address(0x0000D950)] = 0x00
                        except Exception:
                            pass
                        clear_notes.append("d950")
                    if gate.get("seed_dfa0_after_packet_tail", False):
                        cdb12 = self._gdr_packet_session_seed_dfa0(cmd)
                        self._gdr_packet_session_emit("gdr_session_dfa0_latch", cmd.get("tag", "cmd"), addr=0x0000DFA0, size=len(cdb12), value=cmd.get("index", 0), bytes_list=cdb12, notes=f"index={cmd.get('index')}; reason=packet_tail_done")
                    self._gdr_packet_session_emit("gdr_session_packet_tail_done", cmd.get("tag", "cmd"), bytes_list=tail, value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; clear={'/'.join(clear_notes) if clear_notes else 'none'}")
                return tail[lane] & 0xFF

        # DATA IN: firmware reads 0x01C8 source stream, then writes to DF70.
        if str(cmd.get("direction", "")).lower() == "in" and pc == 0x900099FF:
            lane = _off(source_addr, source_window)
            if lane is not None:
                data = cmd.get("data_in") or []
                idx = int(model.get("source_index", 0))
                val = (data[idx] if idx < len(data) else 0) & 0xFF
                model["source_index"] = idx + 1
                self._gdr_packet_session_emit("gdr_session_data_in_source", cmd.get("tag", "cmd"), addr=addr_l, size=1, value=val, bytes_list=[val], notes=f"index={cmd.get('index')}; stream_index={idx}; expected_len={len(data)}")
                return val

        # DATA OUT: firmware reads host bytes from DF90..DF97 in FIFO order.
        if str(cmd.get("direction", "")).lower() == "out" and pc in (0x90009A59, 0x90009A5F, 0x90009A65, 0x90009A6B, 0x90009A71, 0x90009A77, 0x90009A7D, 0x90009A83):
            lane = _off(df90, 8)
            if lane is not None:
                data = cmd.get("data_out") or []
                idx = int(model.get("data_out_index", 0))
                val = (data[idx] if idx < len(data) else 0) & 0xFF
                model["data_out_index"] = idx + 1
                self._gdr_packet_session_emit("gdr_session_data_out_read", cmd.get("tag", "cmd"), addr=addr_l, size=1, value=val, bytes_list=[val], notes=f"index={cmd.get('index')}; stream_index={idx}; expected_len={len(data)}")
                return val
        return None

    def _gdr_packet_session_write_byte(self, logical, physical, val):
        model, cmd = self._gdr_packet_session_current()
        if not model or cmd is None:
            return
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        addrs = {addr_l, addr_p}
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF

        # v131 baseline: in the persistent post-FE path the firmware writes
        # a data-phase descriptor to D108 at 0x9001CD59 instead of entering the
        # CPU-side 0x900099EA transfer helper used by the reset/startup path.
        # Treat the D108 high-byte write as a host-interface/DMA data-phase
        # completion edge.  This is a baseline host-interface hardware model: it emits
        # the same byte-stream events the verifier uses, marks the command as
        # submitted, and lets the next visit to the main loop advance the
        # persistent command queue without resetting firmware state.
        gate = model.get("host_event_gate") or {}
        if (gate.get("enabled") and bool(gate.get("complete_data_phase_on_d108", True))
            and int(cmd.get("index", 0)) > 0 and model.get("data_phase_request_seen")
            and not model.get("data_phase_hw_completed") and pc == 0x9001CD59
            and 0x0000D109 in addrs):
            direction = str(cmd.get("direction", "")).lower()
            if direction == "in":
                data = [int(x) & 0xFF for x in (cmd.get("data_in") or [])]
                base0 = model.get("source_addr", 0x000001C8)
                win = max(1, int(model.get("source_window", 8)))
                for i, b in enumerate(data):
                    self._gdr_packet_session_emit("gdr_session_data_in_source", cmd.get("tag", "cmd"), addr=(base0 + (i % win)), size=1, value=b, bytes_list=[b], notes=f"index={cmd.get('index')}; stream_index={i}; expected_len={len(data)}; d108_hw=1")
                model["source_index"] = len(data)
                model["df70_stream"] = list(data)
                if data and not model.get("ready"):
                    model["ready"] = True
                    ml = min(8, len(data))
                    self._gdr_packet_session_emit("gdr_data_in_ready", cmd.get("tag", "cmd"), addr=model.get("df70_base", 0x0000DF70), size=ml, value=ml, bytes_list=data[:ml], notes=f"index={cmd.get('index')}; declared_length={len(data)}; persistent_session=1; d108_hw=1")
                if not model.get("submitted"):
                    model["submitted"] = True
                    self._gdr_packet_session_emit("gdr_data_in_submit", cmd.get("tag", "cmd"), addr=model.get("df2a_addr", 0x0000DF2A), size=1, value=0xA4, bytes_list=[0xA4], notes=f"index={cmd.get('index')}; persistent_session=1; d108_hw=1")
            elif direction == "out":
                data = [int(x) & 0xFF for x in (cmd.get("data_out") or [])]
                base0 = model.get("df90_base", 0x0000DF90)
                for i, b in enumerate(data):
                    self._gdr_packet_session_emit("gdr_session_data_out_read", cmd.get("tag", "cmd"), addr=(base0 + (i % 8)), size=1, value=b, bytes_list=[b], notes=f"index={cmd.get('index')}; stream_index={i}; expected_len={len(data)}; d108_hw=1")
                model["data_out_index"] = len(data)
                model["submitted"] = True
            model["data_phase_hw_completed"] = True
            model["timer_done_seen"] = True
            self._gdr_packet_session_emit("gdr_session_d108_hw_data_phase_complete", cmd.get("tag", "cmd"), addr=0x0000D108, size=2, value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; direction={direction}; d108_hw=1")
            self._gdr_packet_session_emit("gdr_session_timer_done", cmd.get("tag", "cmd"), value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; d108_hw=1")

        base = model.get("df70_base", 0x0000DF70)
        df2a = model.get("df2a_addr", 0x0000DF2A)
        data = cmd.get("data_in") or []
        if any(base <= a < base + 0x40 for a in addrs):
            stream = model.setdefault("df70_stream", [])
            stream.append(val & 0xFF)
            match_len = min(8, len(data)) if data else 0
            if match_len and len(stream) >= match_len and not model.get("ready"):
                if stream[:match_len] == data[:match_len]:
                    model["ready"] = True
                    self._gdr_packet_session_emit("gdr_data_in_ready", cmd.get("tag", "cmd"), addr=base, size=match_len, value=match_len, bytes_list=data[:match_len], notes=f"index={cmd.get('index')}; declared_length={len(data)}; persistent_session=1")
        if df2a in addrs and model.get("ready") and not model.get("submitted"):
            model["submitted"] = True
            self._gdr_packet_session_emit("gdr_data_in_submit", cmd.get("tag", "cmd"), addr=df2a, size=1, value=val & 0xFF, bytes_list=[val & 0xFF], notes=f"index={cmd.get('index')}; persistent_session=1")

    def set_gdr_hardware_model(self, model):
        """Install a small GDR-8050L MMIO/timer model.

        v118 starts moving the GDR packet harness away from hard-coded
        PC-qualified timer/status read overrides.  This is intentionally a
        conservative peripheral shim, not a complete MN103S timer block yet:

        * DC54/DC55 is exposed as a 16-bit free-running down counter derived
          from the emulator instruction cycle.  The firmware delay helpers
          snapshot DC54 and then wait until (start-current) exceeds a threshold;
          a fast synthetic tick rate lets those loops complete without per-PC
          overrides.
        * D904/D905 packet/mailbox status bytes are supplied by a PC-scoped
          map matching the known GDR packet service helpers.  Keeping these in
          one model makes the remaining non-timer hardware assumptions visible
          and removable later.
        """
        cfg = model or {}
        enabled = bool(cfg.get("enabled", bool(cfg)))
        timer_cfg = cfg.get("timer", {}) or {}
        status_cfg = cfg.get("packet_status", cfg.get("status", {}) or {}) or {}

        def _parse_pc_value_map(src, defaults):
            out = {}
            if src is None:
                src = defaults
            if isinstance(src, dict):
                iterable = src.items()
            else:
                iterable = []
                for item in src or []:
                    if isinstance(item, dict):
                        iterable.append((item.get("pc"), item.get("value", item.get("val", 0))))
            for k, v in iterable:
                try:
                    out[parse_int(k) & 0xFFFFFFFF] = parse_int(v) & 0xFF
                except Exception:
                    pass
            return out

        # Defaults mirror the legacy GDR packet-full-cdb read overrides, but
        # they now live in one explicit hardware model instead of many one-off
        # read_override entries.
        d905_defaults = {
            "0x900099F0": "0x03",  # transfer helper: source ready
            "0x90009A12": "0x01",  # DF2A accepted/acknowledged
            "0x90009A8A": "0x00",  # completion clear
        }
        d904_defaults = {
            "0x9002113E": "0x02",  # packet service loop ready
            "0x9002232B": "0x02",
            "0x900223C0": "0x00",
        }
        self.gdr_hardware_model = {
            "enabled": enabled,
            "emit_events": bool(cfg.get("emit_events", False)),
            "timer_enabled": bool(timer_cfg.get("enabled", cfg.get("timer_enabled", enabled))),
            "timer_addr": parse_int(timer_cfg.get("addr", cfg.get("timer_addr", "0x0000DC54"))) & 0xFFFFFFFF,
            "timer_base": parse_int(timer_cfg.get("base", cfg.get("timer_base", "0xF000"))) & 0xFFFF,
            "ticks_per_cycle": max(1, parse_int(timer_cfg.get("ticks_per_cycle", cfg.get("timer_ticks_per_cycle", "0x2000")), 0x2000)) & 0xFFFFFFFF,
            "timer_latch_cycle": None,
            "timer_latch_word": None,
            "status_enabled": bool(status_cfg.get("enabled", cfg.get("status_enabled", enabled))),
            "d904_addr": parse_int(status_cfg.get("d904_addr", "0x0000D904")) & 0xFFFFFFFF,
            "d905_addr": parse_int(status_cfg.get("d905_addr", "0x0000D905")) & 0xFFFFFFFF,
            "d904_by_pc": _parse_pc_value_map(status_cfg.get("d904_by_pc"), d904_defaults),
            "d905_by_pc": _parse_pc_value_map(status_cfg.get("d905_by_pc"), d905_defaults),
        }
        if enabled:
            timer_note = "on" if self.gdr_hardware_model.get("timer_enabled") else "off"
            status_note = "on" if self.gdr_hardware_model.get("status_enabled") else "off"
            print(f"[*] GDR hardware model enabled: timer={timer_note} DC54 ticks/cycle=0x{self.gdr_hardware_model['ticks_per_cycle']:X}; packet_status={status_note}.")

    def _gdr_hardware_current_cycle(self):
        tr = getattr(self, "trace_recorder", None)
        if tr is not None:
            try:
                return int(getattr(tr, "current_cycle", 0))
            except Exception:
                return 0
        return 0

    def _gdr_hardware_model_read_byte(self, logical, physical):
        model = getattr(self, "gdr_hardware_model", None)
        if not model or not model.get("enabled", False):
            return None
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        addrs = {addr_l, addr_p}

        if model.get("status_enabled", False):
            d905 = model.get("d905_addr", 0x0000D905)
            d904 = model.get("d904_addr", 0x0000D904)
            value = None
            source = None
            if d905 in addrs and pc in (model.get("d905_by_pc") or {}):
                value = (model.get("d905_by_pc") or {}).get(pc, 0) & 0xFF
                source = "d905_packet_status"
            elif d904 in addrs and pc in (model.get("d904_by_pc") or {}):
                value = (model.get("d904_by_pc") or {}).get(pc, 0) & 0xFF
                source = "d904_packet_status"
            if value is not None:
                if self.trace_recorder and model.get("emit_events", False):
                    self.trace_recorder.emit("gdr_hw_status_read", source, pc=pc, addr=(d905 if source.startswith("d905") else d904), size=1, value=value, bytes_hex_str=f"{value:02X}", cpu=self.cpu_ref)
                return value

        if model.get("timer_enabled", False):
            base = model.get("timer_addr", 0x0000DC54)
            if base in addrs or ((base + 1) & 0xFFFFFFFF) in addrs:
                cyc = self._gdr_hardware_current_cycle()
                if model.get("timer_latch_cycle") != cyc:
                    word = (int(model.get("timer_base", 0xF000)) - (cyc * int(model.get("ticks_per_cycle", 0x2000)))) & 0xFFFF
                    model["timer_latch_cycle"] = cyc
                    model["timer_latch_word"] = word
                word = int(model.get("timer_latch_word", 0)) & 0xFFFF
                if base in addrs:
                    val = word & 0xFF
                    timer_addr = base
                else:
                    val = (word >> 8) & 0xFF
                    timer_addr = (base + 1) & 0xFFFFFFFF
                if self.trace_recorder and model.get("emit_events", False):
                    self.trace_recorder.emit("gdr_hw_timer_read", "dc54_free_running_downcounter", pc=pc, addr=timer_addr, size=1, value=val, bytes_hex_str=f"{val:02X}", cpu=self.cpu_ref, notes=f"word=0x{word:04X}")
                return val
        return None

    def set_gdr_packet_data_in_model(self, model):
        """Install a narrow GDR-8050L packet data-in mailbox model.

        The observed GDR READ CAPACITY path copies drive-to-host payload bytes
        from the firmware source buffer at 0x01C8 into the host-facing DF70
        window, then submits a DF2A command byte (usually A4).  Earlier tests
        proved the payload bytes were being generated correctly, but the trace
        had no explicit semantic marker saying that the packet data-in phase had
        completed.  This model is intentionally small and data-driven: it watches
        DF70.. for a configured payload prefix, records completion, optionally
        applies conservative mailbox writes, and can request a clean stop after
        DF2A submission for bounded capture runs.
        """
        cfg = model or {}

        def _parse_bytes(src):
            if src is None:
                return []
            if isinstance(src, str):
                parts = src.replace(",", " ").split()
            elif isinstance(src, list):
                parts = src
            else:
                parts = [src]
            out = []
            for x in parts:
                try:
                    token = str(x).strip()
                    if token and not token.lower().startswith(("0x", "-0x")):
                        out.append(int(token, 16) & 0xFF)
                    else:
                        out.append(parse_int(token) & 0xFF)
                except Exception:
                    pass
            return out

        payload = _parse_bytes(cfg.get("bytes", cfg.get("payload_bytes", cfg.get("first_64_hex"))))
        enabled = bool(cfg.get("enabled", bool(payload) or bool(cfg)))
        length = parse_int(cfg.get("length", len(payload)), len(payload))
        # v115: large packet data-in responses can be submitted in repeated small
        # DF70 bursts.  Allow the semantic marker to match only the staged prefix
        # instead of requiring the entire response length in one burst.
        match_length = parse_int(
            cfg.get("match_length", cfg.get("match_len", cfg.get("prefix_match_length", len(payload)))),
            len(payload),
        )
        if payload:
            match_length = max(1, min(int(match_length), len(payload)))
        else:
            match_length = 0
        match_payload = payload[:match_length]
        consume_pcs_src = cfg.get("consume_d905_pcs", cfg.get("d905_consume_pcs", ["0x900099F0", "0x900099DC"]))
        if not isinstance(consume_pcs_src, list):
            consume_pcs_src = [consume_pcs_src]
        consume_pcs = set()
        for pc_token in consume_pcs_src:
            try:
                consume_pcs.add(parse_int(pc_token) & 0xFFFFFFFF)
            except Exception:
                pass
        self.gdr_packet_data_in_model = {
            "enabled": enabled,
            "payload": payload,
            "match_payload": match_payload,
            "match_length": match_length,
            "length": length,
            "df70_base": parse_int(cfg.get("df70_base", "0x0000DF70")) & 0xFFFFFFFF,
            "df2a_addr": parse_int(cfg.get("df2a_addr", "0x0000DF2A")) & 0xFFFFFFFF,
            "d905_addr": parse_int(cfg.get("d905_addr", "0x0000D905")) & 0xFFFFFFFF,
            "consume_after_submit": bool(cfg.get("consume_after_submit", cfg.get("consume_packet_after_submit", False))),
            "consume_d905_pcs": consume_pcs,
            "consume_d905_value": parse_int(cfg.get("consume_d905_value", "0x00")) & 0xFF,
            "consumed_reads": 0,
            "writes": {},
            "seen_prefix": False,
            "submitted": False,
            "submit_count": 0,
            "stop_on_submit": bool(cfg.get("stop_on_submit", cfg.get("stop_after_submit", False))),
            "emit_events": bool(cfg.get("emit_events", True)),
            "completion_writes": cfg.get("completion_writes", cfg.get("side_effect_writes", [])) or [],
            "stream_match": bool(cfg.get("stream_match", cfg.get("match_as_stream", False))),
        }
        if enabled:
            prefix_note = " ".join(f"{b:02X}" for b in match_payload[:min(len(match_payload), 16)])
            stop_note = " stop-on-submit" if self.gdr_packet_data_in_model.get("stop_on_submit") else ""
            full_note = "" if match_length == len(payload) else f"; full_prefix_bytes={len(payload)}"
            print(f"[*] GDR packet data-in model enabled: DF70 match {match_length} byte(s), declared length={length}{full_note}; first={prefix_note}{stop_note}.")

    def _gdr_packet_data_in_apply_writes(self):
        model = getattr(self, "gdr_packet_data_in_model", None)
        if not model:
            return
        for item in model.get("completion_writes", []) or []:
            try:
                addr = parse_int(item.get("address", item.get("addr"))) & 0xFFFFFFFF
                val = parse_int(item.get("value", item.get("val", 0))) & 0xFF
            except Exception:
                continue
            phys = self.normalize_address(addr)
            self.memory[phys] = val
            self.mem_trace.append(f"GDR-DATA-IN: W8 0x{phys:08X} <- 0x{val:02X}")
            if self.trace_recorder and model.get("emit_events", True):
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.emit("gdr_data_in_side_effect", f"gdr_write_{addr:08X}", pc=pc, addr=addr, size=1, value=val, bytes_hex_str=f"{val:02X}", cpu=self.cpu_ref)

    def _gdr_packet_data_in_model_write_byte(self, logical, physical, val):
        model = getattr(self, "gdr_packet_data_in_model", None)
        if not model or not model.get("enabled", False):
            return
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        addrs = {addr_l, addr_p}
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF

        # v131 baseline: in the persistent post-FE path the firmware writes
        # a data-phase descriptor to D108 at 0x9001CD59 instead of entering the
        # CPU-side 0x900099EA transfer helper used by the reset/startup path.
        # Treat the D108 high-byte write as a host-interface/DMA data-phase
        # completion edge.  This is a baseline host-interface hardware model: it emits
        # the same byte-stream events the verifier uses, marks the command as
        # submitted, and lets the next visit to the main loop advance the
        # persistent command queue without resetting firmware state.
        gate = model.get("host_event_gate") or {}
        if (gate.get("enabled") and bool(gate.get("complete_data_phase_on_d108", True))
            and int(cmd.get("index", 0)) > 0 and model.get("data_phase_request_seen")
            and not model.get("data_phase_hw_completed") and pc == 0x9001CD59
            and 0x0000D109 in addrs):
            direction = str(cmd.get("direction", "")).lower()
            if direction == "in":
                data = [int(x) & 0xFF for x in (cmd.get("data_in") or [])]
                base0 = model.get("source_addr", 0x000001C8)
                win = max(1, int(model.get("source_window", 8)))
                for i, b in enumerate(data):
                    self._gdr_packet_session_emit("gdr_session_data_in_source", cmd.get("tag", "cmd"), addr=(base0 + (i % win)), size=1, value=b, bytes_list=[b], notes=f"index={cmd.get('index')}; stream_index={i}; expected_len={len(data)}; d108_hw=1")
                model["source_index"] = len(data)
                model["df70_stream"] = list(data)
                if data and not model.get("ready"):
                    model["ready"] = True
                    ml = min(8, len(data))
                    self._gdr_packet_session_emit("gdr_data_in_ready", cmd.get("tag", "cmd"), addr=model.get("df70_base", 0x0000DF70), size=ml, value=ml, bytes_list=data[:ml], notes=f"index={cmd.get('index')}; declared_length={len(data)}; persistent_session=1; d108_hw=1")
                if not model.get("submitted"):
                    model["submitted"] = True
                    self._gdr_packet_session_emit("gdr_data_in_submit", cmd.get("tag", "cmd"), addr=model.get("df2a_addr", 0x0000DF2A), size=1, value=0xA4, bytes_list=[0xA4], notes=f"index={cmd.get('index')}; persistent_session=1; d108_hw=1")
            elif direction == "out":
                data = [int(x) & 0xFF for x in (cmd.get("data_out") or [])]
                base0 = model.get("df90_base", 0x0000DF90)
                for i, b in enumerate(data):
                    self._gdr_packet_session_emit("gdr_session_data_out_read", cmd.get("tag", "cmd"), addr=(base0 + (i % 8)), size=1, value=b, bytes_list=[b], notes=f"index={cmd.get('index')}; stream_index={i}; expected_len={len(data)}; d108_hw=1")
                model["data_out_index"] = len(data)
                model["submitted"] = True
            model["data_phase_hw_completed"] = True
            model["timer_done_seen"] = True
            self._gdr_packet_session_emit("gdr_session_d108_hw_data_phase_complete", cmd.get("tag", "cmd"), addr=0x0000D108, size=2, value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; direction={direction}; d108_hw=1")
            self._gdr_packet_session_emit("gdr_session_timer_done", cmd.get("tag", "cmd"), value=cmd.get("index", 0), notes=f"index={cmd.get('index')}; d108_hw=1")

        base = model.get("df70_base", 0x0000DF70)
        payload = model.get("match_payload") or model.get("payload") or []
        match_length = int(model.get("match_length", len(payload)) or len(payload))
        v = val & 0xFF
        if payload and (base <= addr_l < base + match_length or base <= addr_p < base + match_length):
            if model.get("stream_match", False):
                stream_writes = model.setdefault("write_stream", [])
                stream_writes.append(v)
                if len(stream_writes) >= match_length and not model.get("seen_prefix", False):
                    got = [int(x) & 0xFF for x in stream_writes[:match_length]]
                    if got == payload[:match_length]:
                        model["seen_prefix"] = True
                        self._gdr_packet_data_in_apply_writes()
                        if self.trace_recorder and model.get("emit_events", True):
                            pc = self.cpu_ref.pc if self.cpu_ref else 0
                            notes = f"declared_length={model.get('length')}; match_length={match_length}; stream_match=1"
                            self.trace_recorder.emit("gdr_data_in_ready", "df70_payload_prefix", pc=pc, addr=base, size=match_length, value=match_length, bytes_hex_str=" ".join(f"{b:02X}" for b in payload[:match_length]), cpu=self.cpu_ref, notes=notes)
            else:
                addr = addr_l if base <= addr_l < base + match_length else addr_p
                off = addr - base
                model.setdefault("writes", {})[off] = v
                if all(i in model["writes"] for i in range(match_length)):
                    got = [model["writes"].get(i, 0) & 0xFF for i in range(match_length)]
                    if got == payload[:match_length] and not model.get("seen_prefix", False):
                        model["seen_prefix"] = True
                        self._gdr_packet_data_in_apply_writes()
                        if self.trace_recorder and model.get("emit_events", True):
                            pc = self.cpu_ref.pc if self.cpu_ref else 0
                            notes = f"declared_length={model.get('length')}; match_length={match_length}"
                            self.trace_recorder.emit("gdr_data_in_ready", "df70_payload_prefix", pc=pc, addr=base, size=match_length, value=match_length, bytes_hex_str=" ".join(f"{b:02X}" for b in payload[:match_length]), cpu=self.cpu_ref, notes=notes)
        df2a = model.get("df2a_addr", 0x0000DF2A)
        if df2a in addrs and model.get("seen_prefix", False) and not model.get("submitted", False):
            model["submitted"] = True
            model["submit_count"] = int(model.get("submit_count", 0)) + 1
            if self.trace_recorder and model.get("emit_events", True):
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.emit("gdr_data_in_submit", "df2a_after_df70", pc=pc, addr=df2a, size=1, value=v, bytes_hex_str=f"{v:02X}", cpu=self.cpu_ref, notes=f"submit_count={model['submit_count']}")
            if model.get("stop_on_submit") and self.cpu_ref is not None:
                setattr(self.cpu_ref, "stop_requested", True)
                setattr(self.cpu_ref, "stop_reason", f"gdr_packet_data_in_submit_0x{v:02X}")

    def _gdr_packet_data_in_model_read_byte(self, logical, physical):
        model = getattr(self, "gdr_packet_data_in_model", None)
        if not model or not model.get("enabled", False):
            return None
        if not model.get("submitted", False) or not model.get("consume_after_submit", True):
            return None
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        d905 = model.get("d905_addr", 0x0000D905)
        if d905 not in (addr_l, addr_p):
            return None
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        pcs = model.get("consume_d905_pcs") or set()
        if pcs and pc not in pcs:
            return None
        val = model.get("consume_d905_value", 0) & 0xFF
        model["consumed_reads"] = int(model.get("consumed_reads", 0)) + 1
        if self.trace_recorder and model.get("emit_events", True):
            self.trace_recorder.emit("gdr_data_in_consumed", "d905_after_submit", pc=pc, addr=d905, size=1, value=val, bytes_hex_str=f"{val:02X}", cpu=self.cpu_ref, notes=f"consumed_reads={model['consumed_reads']}")
        return val

    def set_gdr_packet_source_stream_model(self, model):
        """Install a sequential source-buffer model for GDR packet data-in.

        The firmware helper at 0x900099FF reads from a small 0x01C8 source
        window and writes to DF70.  Large Xbox responses are delivered as many
        short bursts, including an initial two-byte prelude.  Address-local
        read overrides can drift whenever the helper emits a partial burst, so
        this model returns a single global stream byte on each matching source
        read: prelude bytes first, then the full configured payload.
        """
        cfg = model or {}

        def _parse_bytes(src):
            if src is None:
                return []
            if isinstance(src, str):
                parts = src.replace(",", " ").split()
            elif isinstance(src, list):
                parts = src
            else:
                parts = [src]
            out = []
            for x in parts:
                try:
                    token = str(x).strip()
                    if token == "...":
                        break
                    if token and not token.lower().startswith(("0x", "-0x")):
                        out.append(int(token, 16) & 0xFF)
                    else:
                        out.append(parse_int(token) & 0xFF)
                except Exception:
                    pass
            return out

        payload = _parse_bytes(cfg.get("bytes", cfg.get("payload_bytes", cfg.get("full_hex", cfg.get("first_64_hex")))))
        enabled = bool(cfg.get("enabled", bool(payload) or bool(cfg)))
        source_addr = parse_int(cfg.get("source_addr", cfg.get("addr", "0x000001C8"))) & 0xFFFFFFFF
        window_size = max(1, parse_int(cfg.get("window_size", cfg.get("source_window_size", "0x08")), 8))
        prelude_len = max(0, min(parse_int(cfg.get("prelude_len", cfg.get("source_prelude_len", "0x02")), 2), len(payload)))
        pcs_src = cfg.get("pcs", cfg.get("pc", ["0x900099FF"]))
        if not isinstance(pcs_src, list):
            pcs_src = [pcs_src]
        pcs = set()
        for pc_token in pcs_src:
            try:
                pcs.add(parse_int(pc_token) & 0xFFFFFFFF)
            except Exception:
                pass
        def _as_bool(v, default=True):
            if v is None:
                return default
            if isinstance(v, bool):
                return v
            return str(v).strip().lower() not in ("0", "false", "no", "off", "none")

        prepend_prelude = _as_bool(cfg.get("prepend_prelude", cfg.get("source_prepend_prelude", True)), True)
        stream = (payload[:prelude_len] + payload) if prepend_prelude else list(payload)
        self.gdr_packet_source_stream_model = {
            "enabled": enabled,
            "payload": payload,
            "stream": stream,
            "index": 0,
            "source_addr": source_addr,
            "window_size": window_size,
            "pcs": pcs,
            "pad_value": parse_int(cfg.get("pad_value", "0x00")) & 0xFF,
            "prepend_prelude": prepend_prelude,
            "emit_events": bool(cfg.get("emit_events", False)),
        }
        if enabled:
            prefix = " ".join(f"{b:02X}" for b in payload[:min(16, len(payload))])
            mode = "prelude+payload" if prepend_prelude else "payload-only"
            print(f"[*] GDR packet source stream model enabled: len={len(payload)} prelude={prelude_len} stream={mode} source=0x{source_addr:08X} window={window_size}; first={prefix}.")

    def _gdr_packet_source_stream_model_read_byte(self, logical, physical):
        model = getattr(self, "gdr_packet_source_stream_model", None)
        if not model or not model.get("enabled", False):
            return None
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        base = model.get("source_addr", 0x000001C8)
        window = model.get("window_size", 8)
        if not (base <= addr_l < base + window or base <= addr_p < base + window):
            return None
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        pcs = model.get("pcs") or set()
        if pcs and pc not in pcs:
            return None
        stream = model.get("stream") or []
        idx = int(model.get("index", 0))
        if idx < len(stream):
            val = stream[idx] & 0xFF
        else:
            val = model.get("pad_value", 0) & 0xFF
        model["index"] = idx + 1
        if self.trace_recorder and model.get("emit_events", False):
            self.trace_recorder.emit("gdr_source_stream_read", "packet_data_in_source", pc=pc, addr=logical, size=1, value=val, bytes_hex_str=f"{val:02X}", cpu=self.cpu_ref, notes=f"stream_index={idx}")
        return val

    def set_read_overrides(self, overrides):
        self.read_overrides = []
        for item in overrides or []:
            try:
                addr = parse_int(item.get("address", item.get("addr"))) & 0xFFFFFFFF
            except Exception:
                continue

            values_src = item.get("values", None)
            if values_src is None:
                values_src = item.get("sequence", None)
            if values_src is not None:
                if not isinstance(values_src, list):
                    values_src = [values_src]
                try:
                    values = [parse_int(x) & 0xFF for x in values_src]
                except Exception:
                    continue
                if not values:
                    continue
            else:
                try:
                    values = [parse_int(item.get("value", item.get("data", 0))) & 0xFF]
                except Exception:
                    continue

            pcs = item.get("pcs", item.get("pc", None))
            if pcs is None:
                pcset = None
            else:
                if not isinstance(pcs, list):
                    pcs = [pcs]
                pcset = {parse_int(x) & 0xFFFFFFFF for x in pcs}
            self.read_overrides.append({
                "address": addr,
                "values": values,
                "index": 0,
                "repeat_last": bool(item.get("repeat_last", True)),
                "advance": bool(item.get("advance", True)),
                "pcs": pcset,
                "name": str(item.get("name", "read_override")),
            })

    def set_gcc_dma_status_model(self, model):
        """Install a narrow GCC DMA/status-read model for HL subcode sweep probes.

        The subcode D/F paths use the common GCC DMA helper around 0x90021CFE
        and 0x90023648.  The firmware first waits for D904&0x30 == 0x20, then
        later waits for D904 bit 0x10 to become set.  This model returns the
        observed ready/complete status values by PC, replacing local ad-hoc
        read_overrides used during the sweep.
        """
        cfg = model or {}
        explicit_enabled = "enabled" in cfg
        enabled = bool(cfg.get("enabled", bool(cfg) or bool(cfg.get("_present", False))))
        first_pc = parse_int(cfg.get("first_ready_pc", "0x90021D02")) & 0xFFFFFFFF
        complete_pc = parse_int(cfg.get("complete_ready_pc", "0x90023649")) & 0xFFFFFFFF
        self.gcc_dma_status_model = {
            "enabled": enabled,
            "implicit_enabled": enabled and not explicit_enabled,
            "first_ready_pc": first_pc,
            "complete_ready_pc": complete_pc,
            "first_ready_value": parse_int(cfg.get("first_ready_value", "0x20")) & 0xFF,
            "complete_ready_value": parse_int(cfg.get("complete_ready_value", "0x30")) & 0xFF,
        }
        if enabled:
            enable_note = "implicit" if self.gcc_dma_status_model.get("implicit_enabled") else "explicit"
            print(f"[*] GCC DMA status model enabled: D904 first-ready=0x{self.gcc_dma_status_model['first_ready_value']:02X} at 0x{first_pc:08X}; complete=0x{self.gcc_dma_status_model['complete_ready_value']:02X} at 0x{complete_pc:08X}; dma enable={enable_note}.")

    def _gcc_dma_status_model_read_byte(self, logical, physical):
        model = getattr(self, "gcc_dma_status_model", None)
        if not model or not model.get("enabled"):
            return None
        pc = self.cpu_ref.pc & 0xFFFFFFFF if self.cpu_ref else 0
        if logical == 0x0000D904 or physical == 0x0000D904:
            if pc == model.get("first_ready_pc"):
                return model.get("first_ready_value", 0x20)
            if pc == model.get("complete_ready_pc"):
                return model.get("complete_ready_value", 0x30)
        return None

    def set_gcc_transfer_model(self, model):
        """Install a narrow GCC-4241N transfer-status/completion model.

        This replaces the older JSON read_overrides for the GCC E7/HL
        subcode-2 transfer helper with a small stateful hardware shim.  It is
        intentionally narrow: it only models the status poll PCs and the DF94
        completion dword read used by the observed GCC transfer routine.

        v48 added a byte-stream mode: configs may provide completion_bytes
        instead of pre-packed completion_words. v50 added source-mapped mode:
        configs may provide source_bytes plus optional source_offsets, so the
        model keys completions from the source offset that firmware stages in
        DF72 before writing the command byte to DF2A. v51 allows configs to set
        completion_read_pc to "any" so DF94 readout is address-driven rather
        than PC-qualified. v52 adds an address/state-driven D905/D955 status
        mode, selected with status_read_pc="any". v53 makes address-driven
        status/completion plus first-seen source binding the default for enabled
        GCC transfer models, so configs can name only source_bytes. v55 keeps
        the same model and removes the explicit 0x002C default/non-disc-path
        clear injection from active GCC configs. v56 moves active GCC E7/HL
        command staging out of raw memory_injections and into a semantic
        gcc_e7_hl_command config block. v57 makes the fixed E7/HL/subcode-2
        fields default inside that semantic command block, so active configs
        only name the fields that vary: transfer_length and, for boundary
        probes, source_offset. v58 makes both semantic blocks implicitly enabled
        when present (or when transfer source bytes are present), removing the
        remaining explicit enable toggles from active configs. v59 accepts the
        GCC transfer model as a top-level semantic block, decoupling the active
        hardware-transfer model from the trace block. v60 adds a single
        top-level gcc_e7_hl_subcode2 descriptor that expands into the semantic
        command staging plus transfer model for active GCC subcode-2 probes.
        v62 adds a generic top-level gcc_e7_hl descriptor for all observed
        HL subcodes and auto-enables the narrow DMA status model for subcodes
        D/F. v63 keeps that descriptor and adds broader local length-sweep
        probes for host-output subcodes 0, 1, and 4.
        """
        cfg = model or {}
        config_scope = str(cfg.get("_config_scope", "trace")).strip() or "trace"
        explicit_enabled = "enabled" in cfg

        def _parse_byte_list(src):
            if src is None:
                return None
            if isinstance(src, str):
                parts = src.replace(",", " ").split()
            elif isinstance(src, list):
                parts = src
            else:
                parts = [src]
            out = []
            for b in parts:
                try:
                    out.append(parse_int(b) & 0xFF)
                except Exception:
                    continue
            return out

        source_byte_stream = _parse_byte_list(cfg.get("source_bytes", cfg.get("source_byte_stream", None)))
        byte_stream = source_byte_stream
        source_mapped = source_byte_stream is not None
        if byte_stream is None:
            byte_stream = _parse_byte_list(cfg.get("completion_bytes", cfg.get("bytes", None)))
        has_transfer_payload = byte_stream is not None or "completion_words" in cfg or "words" in cfg
        enabled = bool(cfg.get("enabled", has_transfer_payload))
        implicit_enabled = enabled and not explicit_enabled and has_transfer_payload

        if byte_stream is not None:
            words = []
            for i in range(0, len(byte_stream), 2):
                lo = byte_stream[i] if i < len(byte_stream) else 0
                hi = byte_stream[i + 1] if i + 1 < len(byte_stream) else 0
                words.append((lo | (hi << 8)) & 0xFFFFFFFF)
        else:
            words_src = cfg.get("completion_words", cfg.get("words", [])) or []
            if not isinstance(words_src, list):
                words_src = [words_src]
            words = []
            for w in words_src:
                try:
                    words.append(parse_int(w) & 0xFFFFFFFF)
                except Exception:
                    continue

        source_word_map = {}
        source_binding = str(cfg.get("source_binding", cfg.get("source_map_mode", ""))).strip().lower()
        offs_src = cfg.get("source_offsets", None) if source_mapped else None
        if source_mapped:
            if offs_src is None:
                if source_binding in ("", "auto"):
                    source_binding = "first_seen"
                if source_binding not in ("first_seen", "firmware_staged", "dynamic"):
                    offsets = list(range(len(words)))
                    for off, word in zip(offsets, words):
                        source_word_map[off & 0xFFFFFFFF] = word & 0xFFFFFFFF
            else:
                if source_binding in ("", "auto"):
                    source_binding = "explicit_offsets"
                if not isinstance(offs_src, list):
                    offs_src = [offs_src]
                offsets = []
                for x in offs_src:
                    try:
                        offsets.append(parse_int(x) & 0xFFFFFFFF)
                    except Exception:
                        pass
                for off, word in zip(offsets, words):
                    source_word_map[off & 0xFFFFFFFF] = word & 0xFFFFFFFF
        if not source_binding:
            source_binding = "sequential"

        completion_read_pc_cfg = cfg.get("completion_read_pc", "any" if enabled else "0x9000C34F")
        completion_read_any = False
        if completion_read_pc_cfg is None:
            completion_read_any = True
            completion_read_pc = None
        elif isinstance(completion_read_pc_cfg, str) and completion_read_pc_cfg.strip().lower() in ("", "any", "all", "*", "none", "unqualified", "address"):
            completion_read_any = True
            completion_read_pc = None
        else:
            completion_read_pc = parse_int(completion_read_pc_cfg) & 0xFFFFFFFF

        status_read_pc_cfg = cfg.get("status_read_pc", cfg.get("status_poll_pc", "any" if enabled else "pc_scoped"))
        status_read_any = False
        if status_read_pc_cfg is None:
            status_read_any = True
        elif isinstance(status_read_pc_cfg, str) and status_read_pc_cfg.strip().lower() in ("", "any", "all", "*", "none", "unqualified", "address", "address_state"):
            status_read_any = True

        self.gcc_transfer_model = {
            "enabled": enabled,
            "implicit_enabled": implicit_enabled,
            "status_model": bool(cfg.get("status_model", True)),
            "status_read_any": status_read_any,
            "status_phase": str(cfg.get("status_initial_phase", "precheck")).strip().lower() or "precheck",
            "completion_read_pc": completion_read_pc,
            "completion_read_any": completion_read_any,
            "words": words,
            "source_word_map": source_word_map,
            "source_mapped": source_mapped,
            "source_byte_stream_len": len(source_byte_stream) if source_byte_stream is not None else None,
            "byte_stream_len": len(byte_stream) if byte_stream is not None else None,
            "source_binding": source_binding,
            "source_next_index": 0,
            "index": 0,
            "repeat_last": bool(cfg.get("repeat_last", True)),
            "active_word": None,
            "active_index": None,
            "staged_df70": [0, 0],
            "staged_df72": [0, 0],
            "last_source_offset": None,
            "submitted_source_offsets": [],
            "config_scope": config_scope,
        }
        if enabled:
            byte_note = ""
            if source_byte_stream is not None:
                map_note = "dynamic" if source_binding in ("first_seen", "firmware_staged", "dynamic") and not source_word_map else f"{len(source_word_map)} offset(s)"
                byte_note = f"; source byte stream={len(source_byte_stream)} byte(s); source map={map_note}; source binding={source_binding}"
            elif byte_stream is not None:
                byte_note = f"; completion byte stream={len(byte_stream)} byte(s)"
            completion_note = "any" if self.gcc_transfer_model.get("completion_read_any") else f"0x{self.gcc_transfer_model.get('completion_read_pc', 0):08X}"
            status_note = "any" if self.gcc_transfer_model.get("status_read_any") else "pc-scoped"
            enable_note = "implicit" if self.gcc_transfer_model.get("implicit_enabled") else "explicit"
            scope_note = self.gcc_transfer_model.get("config_scope", "trace")
            print(f"[*] GCC transfer model enabled: {len(words)} completion word(s){byte_note}; completion read={completion_note}; status read={status_note}; status model={'on' if self.gcc_transfer_model['status_model'] else 'off'}; transfer enable={enable_note}; transfer config scope={scope_note}.")

    def _gcc_transfer_model_write_byte(self, logical, physical, val):
        model = getattr(self, "gcc_transfer_model", None)
        if not model or not model.get("enabled", False):
            return
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        addrs = {addr_l, addr_p}
        v = val & 0xFF
        if 0x0000DF70 in addrs:
            model["staged_df70"][0] = v
        elif 0x0000DF71 in addrs:
            model["staged_df70"][1] = v
        elif 0x0000DF72 in addrs:
            model["staged_df72"][0] = v
        elif 0x0000DF73 in addrs:
            model["staged_df72"][1] = v
        elif 0x0000DF2A in addrs:
            # Observed GCC helper writes the command byte after staging the
            # source selector in DF72.  Capture that selector so the following
            # DF94 completion read is driven by firmware's command, not by a
            # blind completion-read counter.  In v51 first-seen mode, each new
            # firmware-staged selector is bound to the next configured source
            # word, so boundary tests no longer need source_offsets lists.
            src = (model["staged_df72"][0] | (model["staged_df72"][1] << 8)) & 0xFFFFFFFF
            model["last_source_offset"] = src
            model.setdefault("submitted_source_offsets", []).append(src)
            if model.get("status_read_any", False):
                model["status_phase"] = "accepted"
            if model.get("source_mapped") and model.get("source_binding") in ("first_seen", "firmware_staged", "dynamic"):
                source_map = model.setdefault("source_word_map", {})
                if src not in source_map:
                    words = model.get("words") or [0]
                    idx = int(model.get("source_next_index", 0))
                    if idx >= len(words):
                        idx = len(words) - 1 if model.get("repeat_last", True) and words else 0
                    source_map[src] = (words[idx] if words else 0) & 0xFFFFFFFF
                    if model.get("source_next_index", 0) < len(words):
                        model["source_next_index"] = model.get("source_next_index", 0) + 1

    def _gcc_transfer_model_read_byte(self, logical, physical):
        model = getattr(self, "gcc_transfer_model", None)
        if not model or not model.get("enabled", False):
            return None
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        addr_l = logical & 0xFFFFFFFF
        addr_p = physical & 0xFFFFFFFF
        addrs = {addr_l, addr_p}

        if model.get("status_model", True):
            # GCC helper status polls observed in the E7/HL subcode-2 path.
            # v52 can run these as an address/state-driven model instead of
            # qualifying every status read by firmware PC.  The state sequence is:
            # first D905 read is the pre-transfer error check (0), then ready
            # (0x02), DF2A command write moves to accepted (0x01), D955 completion
            # read moves to clear (0), then the next transfer is ready again.
            if model.get("status_read_any", False):
                if 0x0000D905 in addrs:
                    phase = str(model.get("status_phase", "precheck"))
                    if phase == "precheck":
                        model["status_phase"] = "ready"
                        return 0x00
                    if phase == "accepted":
                        model["status_phase"] = "completion"
                        return 0x01
                    if phase == "clear":
                        model["status_phase"] = "ready"
                        return 0x00
                    return 0x02
                if 0x0000D955 in addrs:
                    model["status_phase"] = "clear"
                    return 0x80
            else:
                if 0x0000D905 in addrs:
                    if pc == 0x9000C364:  # pre-transfer secondary error check
                        return 0x00
                    if pc == 0x9000C374:  # ready to accept transfer command
                        return 0x02
                    if pc == 0x9000C3BF:  # command accepted/acknowledged
                        return 0x01
                    if pc == 0x9000C3E0:  # ack clear after completion readout
                        return 0x00
                if 0x0000D955 in addrs and pc == 0x9000C3C9:
                    return 0x80

        completion_pc = model.get("completion_read_pc")
        if model.get("completion_read_any", False) or completion_pc is None or pc == completion_pc:
            base = 0x0000DF94
            if base <= addr_p <= base + 3 or base <= addr_l <= base + 3:
                addr = addr_p if base <= addr_p <= base + 3 else addr_l
                off = addr - base
                if off == 0 or model.get("active_word") is None:
                    words = model.get("words") or [0]
                    idx = int(model.get("index", 0))
                    word = None
                    if model.get("source_mapped"):
                        src = model.get("last_source_offset")
                        source_map = model.get("source_word_map") or {}
                        if src in source_map:
                            word = source_map[src]
                        elif model.get("repeat_last", True) and source_map:
                            # Keep old repeat-last behavior as a safety net for
                            # manual experiments beyond the supplied source map.
                            word = list(source_map.values())[-1]
                    if word is None:
                        if idx >= len(words):
                            if model.get("repeat_last", True):
                                idx = len(words) - 1
                            else:
                                idx = 0
                                word = 0
                                model["active_word"] = word
                                model["active_index"] = idx
                                return (word >> (off * 8)) & 0xFF
                        word = words[idx] if words else 0
                    model["active_word"] = word
                    model["active_index"] = idx
                word = model.get("active_word") or 0
                val = (word >> (off * 8)) & 0xFF
                if off == 3:
                    idx = int(model.get("active_index", model.get("index", 0)) or 0)
                    if idx < len(model.get("words") or []):
                        model["index"] = idx + 1
                    model["active_word"] = None
                    model["active_index"] = None
                return val
        return None

    def _override_read_byte(self, logical, physical):
        if not self.read_overrides:
            return None
        pc = (self.cpu_ref.pc if self.cpu_ref else 0) & 0xFFFFFFFF
        for item in self.read_overrides:
            if item["address"] not in (logical & 0xFFFFFFFF, physical & 0xFFFFFFFF):
                continue
            pcs = item.get("pcs")
            if pcs is not None and pc not in pcs:
                continue
            values = item.get("values") or [0]
            idx = int(item.get("index", 0))
            if idx >= len(values):
                if not item.get("repeat_last", True):
                    return None
                idx = len(values) - 1
            val = values[idx]
            if item.get("advance", True) and item.get("index", 0) < len(values):
                item["index"] = item.get("index", 0) + 1
            return val
        return None

    def set_trace_recorder(self, recorder, cpu_ref=None):
        self.trace_recorder = recorder
        self.cpu_ref = cpu_ref

    def add_alias(self, from_start, to_start, size, name=""):
        self.aliases.append({"from": int(from_start), "to": int(to_start), "size": int(size), "name": name})

    def normalize_address(self, address):
        a = address & 0xFFFFFFFF
        for al in self.aliases:
            start = al["from"]
            end = start + al["size"]
            if start <= a < end:
                return (al["to"] + (a - start)) & 0xFFFFFFFF
        return a
        
    def attach_peripheral(self, peripheral):
        self.peripherals.append(peripheral)
        print(f"[*] Attached Peripheral '{peripheral.name}' at 0x{peripheral.base_addr:08X} (Size: 0x{peripheral.size:X})")
        
    def write_byte(self, address, value):
        val = value & 0xFF
        logical = address & 0xFFFFFFFF
        physical = self.normalize_address(logical)
        self._gcc_transfer_model_write_byte(logical, physical, val)
        self._gdr_packet_session_write_byte(logical, physical, val)
        self._gdr_packet_data_in_model_write_byte(logical, physical, val)
        
        # Route to peripheral if it falls in MMIO range after alias translation.
        for p in self.peripherals:
            if p.base_addr <= physical < (p.base_addr + p.size):
                p.write_byte(physical, val)
                self.mem_trace.append(f"W8: [HW {p.name}] 0x{logical:08X}/0x{physical:08X} <- 0x{val:02X}")
                if self.trace_recorder:
                    pc = self.cpu_ref.pc if self.cpu_ref else 0
                    self.trace_recorder.check_watch("write", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
                return
                
        self.memory[physical] = val
        if logical != physical:
            self.mem_trace.append(f"W8: 0x{logical:08X}/0x{physical:08X} <- 0x{val:02X}")
        else:
            self.mem_trace.append(f"W8: 0x{physical:08X} <- 0x{val:02X}")
        if self.trace_recorder:
            pc = self.cpu_ref.pc if self.cpu_ref else 0
            self.trace_recorder.check_watch("write", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
        
    def read_byte(self, address):
        logical = address & 0xFFFFFFFF
        physical = self.normalize_address(logical)
        transfer_model_value = self._gcc_transfer_model_read_byte(logical, physical)
        if transfer_model_value is not None:
            val = transfer_model_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        dma_model_value = self._gcc_dma_status_model_read_byte(logical, physical)
        if dma_model_value is not None:
            val = dma_model_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        gdr_session_value = self._gdr_packet_session_read_byte(logical, physical)
        if gdr_session_value is not None:
            val = gdr_session_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        gdr_source_stream_value = self._gdr_packet_source_stream_model_read_byte(logical, physical)
        if gdr_source_stream_value is not None:
            val = gdr_source_stream_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        gdr_model_value = self._gdr_packet_data_in_model_read_byte(logical, physical)
        if gdr_model_value is not None:
            val = gdr_model_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        gdr_hw_value = self._gdr_hardware_model_read_byte(logical, physical)
        if gdr_hw_value is not None:
            val = gdr_hw_value & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        override = self._override_read_byte(logical, physical)
        if override is not None:
            val = override & 0xFF
            if self.trace_recorder:
                pc = self.cpu_ref.pc if self.cpu_ref else 0
                self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
            return val
        # Route to peripheral if it falls in MMIO range after alias translation.
        for p in self.peripherals:
            if p.base_addr <= physical < (p.base_addr + p.size):
                val = p.read_byte(physical)
                if self.trace_recorder:
                    pc = self.cpu_ref.pc if self.cpu_ref else 0
                    self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
                return val
                
        val = self.memory.get(physical, 0x00)
        if self.trace_recorder:
            pc = self.cpu_ref.pc if self.cpu_ref else 0
            self.trace_recorder.check_watch("read", logical, physical, 1, val, f"{val:02X}", pc, self.cpu_ref)
        return val
        
    def read_word(self, address):
        lo = self.read_byte(address)
        hi = self.read_byte(address + 1)
        return (hi << 8) | lo
        
    def read_dword(self, address):
        b0 = self.read_byte(address)
        b1 = self.read_byte(address + 1)
        b2 = self.read_byte(address + 2)
        b3 = self.read_byte(address + 3)
        return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0
    
    def write_word(self, address, value):
        val = value & 0xFFFF
        self.write_byte(address, val & 0xFF)
        self.write_byte(address + 1, (val >> 8) & 0xFF)
        self.mem_trace.append(f"W16: 0x{address:08X} <- 0x{val:04X}")
        
    def write_dword(self, address, value):
        val = value & 0xFFFFFFFF
        self.write_byte(address, val & 0xFF)
        self.write_byte(address + 1, (val >> 8) & 0xFF)
        self.write_byte(address + 2, (val >> 16) & 0xFF)
        self.write_byte(address + 3, (val >> 24) & 0xFF)
        self.mem_trace.append(f"W32: 0x{address:08X} <- 0x{val:08X}")

    def load_bin(self, path, base_addr, skip_bytes=0):
        with open(path, 'rb') as f:
            f.seek(skip_bytes)
            data = f.read()
            # Fast load directly into the dictionary
            for i, b in enumerate(data):
                self.memory[base_addr + i] = b
            return len(data)

class CPUState:
    def __init__(self):
        self.d = [0, 0, 0, 0]
        self.a = [0, 0, 0, 0]
        # Mock the Mask ROM stack initialization
        self.sp = 0x003FF000
        self.pc = 0
        self.psw = 0
        self.mdr = 0
        self.lir = 0
        self.lar = 0
        self.zf = False
        self.nf = False
        self.cf = False
        self.vf = False
        
    def dump(self):
        print("\n--- CPU STATE DUMP ---")
        print(f"PC: 0x{self.pc:08X}  SP: 0x{self.sp:08X}")
        print(f"D0: 0x{self.d[0]:08X}  D1: 0x{self.d[1]:08X}")
        print(f"D2: 0x{self.d[2]:08X}  D3: 0x{self.d[3]:08X}")
        print(f"A0: 0x{self.a[0]:08X}  A1: 0x{self.a[1]:08X}")
        print(f"A2: 0x{self.a[2]:08X}  A3: 0x{self.a[3]:08X}")
        print(f"ZF: {self.zf:<6}      NF: {self.nf:<6}")
        print(f"CF: {self.cf:<6}      VF: {self.vf:<6}")
        print("----------------------\n")

class MN103SEmulator:
    def __init__(self, rom_base=0x40000000, sleigh_path=None, trace_config=None):
        self.mem = MemoryController(rom_base)
        self.cpu = CPUState()
        self.running = False
        self.trace_log = []
        self.cycles = 0
        self.trace_config = trace_config or {}
        # Default preserves the historical emulator behavior.  Long persistent
        # sessions can set trace.live_instruction_log=false to avoid printing
        # every executed instruction; packet/session CSV events still record the
        # important hardware transcript.
        self.live_instruction_log = bool(self.trace_config.get("live_instruction_log", self.trace_config.get("debug_instructions", True)))
        self.trace_recorder = TraceRecorder(self.trace_config)
        self.mem.set_trace_recorder(self.trace_recorder, self.cpu)
        self.mem.set_gcc_transfer_model(self.trace_config.get("gcc_transfer_model", {}) or {})
        self.mem.set_gcc_dma_status_model(self.trace_config.get("gcc_dma_status_model", {}) or {})
        self.mem.set_gdr_hardware_model(self.trace_config.get("gdr_hardware_model", {}) or {})
        self.mem.set_gdr_packet_session_model(self.trace_config.get("gdr_packet_session_model", {}) or {})
        self.mem.set_gdr_packet_data_in_model(self.trace_config.get("gdr_packet_data_in_model", {}) or {})
        self.mem.set_gdr_packet_source_stream_model(self.trace_config.get("gdr_packet_source_stream_model", {}) or {})
        self.mem.set_read_overrides(self.trace_config.get("read_overrides", []) or [])
        self.stop_conditions = []
        self.stop_hit_counts = {}
        for item in (self.trace_config.get("stop_conditions", []) or []):
            try:
                pc = parse_int(item.get("pc", item.get("address"))) & 0xFFFFFFFF
            except Exception:
                continue
            try:
                hit_count = int(item.get("hit_count", item.get("count", 1)))
            except Exception:
                hit_count = 1
            try:
                min_cycle = int(item.get("min_cycle", 0))
            except Exception:
                min_cycle = 0
            self.stop_conditions.append({
                "pc": pc,
                "hit_count": max(1, hit_count),
                "min_cycle": max(0, min_cycle),
                "name": str(item.get("name", f"stop_0x{pc:08X}")),
            })
        call_policy = self.trace_config.get("call_policy", {}) or {}
        self.skip_call_targets = {parse_int(x) & 0xFFFFFFFF for x in call_policy.get("skip_targets", [])}
        self.call_return_values = {}
        for k, v in (call_policy.get("return_values", {}) or {}).items():
            self.call_return_values[parse_int(k) & 0xFFFFFFFF] = dict(v)
        self.call_side_effects = {}
        for k, actions in (call_policy.get("side_effects", {}) or {}).items():
            if isinstance(actions, dict):
                actions = [actions]
            self.call_side_effects[parse_int(k) & 0xFFFFFFFF] = list(actions or [])

        # Minimal MOVM scratch stack used by the ROM PSW guard wrappers at
        # 0x90006464/0x90006470.  The emulator needs faithful register
        # preservation across the wrapper body, not a full block-transfer model.
        self._movm_other_stack = []

        self.sleigh_dict = self._load_sleigh_dictionary(sleigh_path)

    def _load_sleigh_dictionary(self, path=None):
        """Return the compiled fallback opcode table embedded in this file.

        v20 removes the external compiled_opcodes.txt runtime dependency so the
        emulator can be distributed as a single Python module plus JSON tests.
        """
        op_dict = dict(INLINED_SLIPSTREAM_OPCODES)
        print(f"[*] Inline Slipstream Engine Online: {len(op_dict)} fallback opcodes built in.")
        return op_dict

    def _set_nz_flags(self, value):
        value &= 0xFFFFFFFF
        self.cpu.zf = (value == 0)
        self.cpu.nf = (value & 0x80000000) != 0

    def _s8(self, value):
        value &= 0xFF
        return value - 0x100 if value & 0x80 else value

    def _s16(self, value):
        value &= 0xFFFF
        return value - 0x10000 if value & 0x8000 else value

    def _s32(self, value):
        value &= 0xFFFFFFFF
        return value - 0x100000000 if value & 0x80000000 else value

    def _add_u32(self, lhs, rhs, carry=0, set_flags=True):
        lhs &= 0xFFFFFFFF
        rhs &= 0xFFFFFFFF
        carry &= 1
        total = lhs + rhs + carry
        res = total & 0xFFFFFFFF
        if set_flags:
            self._set_nz_flags(res)
            self.cpu.cf = total > 0xFFFFFFFF
            sl, sr, so = (lhs >> 31) & 1, (rhs >> 31) & 1, (res >> 31) & 1
            self.cpu.vf = (sl == sr) and (sl != so)
        return res

    def _sub_u32(self, lhs, rhs, borrow=0, set_flags=True):
        lhs &= 0xFFFFFFFF
        rhs &= 0xFFFFFFFF
        borrow &= 1
        subtrahend = (rhs + borrow) & 0x1FFFFFFFF
        res = (lhs - subtrahend) & 0xFFFFFFFF
        if set_flags:
            self._set_nz_flags(res)
            self.cpu.cf = lhs < subtrahend
            sl, sr, so = (lhs >> 31) & 1, (rhs >> 31) & 1, (res >> 31) & 1
            self.cpu.vf = (sl != sr) and (sl != so)
        return res

    def _cmp_u32(self, lhs, rhs):
        return self._sub_u32(lhs, rhs, 0, set_flags=True)

    def _bit_mask8(self, value):
        value &= 0xFF
        # Firmware and the available disassembler listings use the FE immediate
        # as a byte mask (for example 0x40), while register forms naturally use
        # low-bit indexes.  Treat powers/byte masks literally and compact bit
        # indexes as 1 << n.
        if value < 8:
            return (1 << value) & 0xFF
        return value

    def _lcc_condition(self, opcode):
        """Return the loop-branch condition for D0..DA Lcc opcodes."""
        zf = bool(getattr(self.cpu, 'zf', False))
        nf = bool(getattr(self.cpu, 'nf', False))
        cf = bool(getattr(self.cpu, 'cf', False))
        vf = bool(getattr(self.cpu, 'vf', False))
        idx = opcode & 0x0F
        if idx == 0x0:   # LLT
            return nf != vf
        if idx == 0x1:   # LGT
            return (not zf) and (nf == vf)
        if idx == 0x2:   # LGE
            return nf == vf
        if idx == 0x3:   # LLE
            return zf or (nf != vf)
        if idx == 0x4:   # LCS
            return cf
        if idx == 0x5:   # LHI
            return (not cf) and (not zf)
        if idx == 0x6:   # LCC
            return not cf
        if idx == 0x7:   # LLS
            return cf or zf
        if idx == 0x8:   # LEQ
            return zf
        if idx == 0x9:   # LNE
            return not zf
        if idx == 0xA:   # LRA
            return True
        return False

    def _lcc_name(self, opcode):
        return {
            0xD0: 'llt', 0xD1: 'lgt', 0xD2: 'lge', 0xD3: 'lle',
            0xD4: 'lcs', 0xD5: 'lhi', 0xD6: 'lcc', 0xD7: 'lls',
            0xD8: 'leq', 0xD9: 'lne', 0xDA: 'lra',
        }.get(opcode, 'l??')

    def _regs_from_callret_mask(self, mask):
        """Decode the compact MN103S CALL/RET/MOVM register mask subset.

        The donor firmware uses the common high-nibble forms:
          0xA0 -> [d2, a2]
          0xF0 -> [d2, d3, a2, a3]
          0x30 -> [a2, a3]
          0x00 -> []

        Panasonic documents CALL as saving PC at mem32(SP), selected regs at
        mem32(SP-4), mem32(SP-8), ... and then SP-imm8 -> SP. RET does the
        inverse: SP+imm8 -> SP, restore selected regs from SP-4... and load
        PC from mem32(SP). This targeted decoder implements the mask bits used
        by the GDR-8050L E7/HL path while keeping unknown low-bit forms benign.
        """
        order = [
            (0x80, 'D', 2),
            (0x40, 'D', 3),
            (0x20, 'A', 2),
            (0x10, 'A', 3),
            (0x08, 'D', 0),
            (0x04, 'D', 1),
            (0x02, 'A', 0),
            (0x01, 'A', 1),
        ]
        return [(kind, idx) for bit, kind, idx in order if (mask & bit)]

    def _get_reg_value(self, kind, idx):
        if kind == 'D':
            return self.cpu.d[idx] & 0xFFFFFFFF
        if kind == 'A':
            return self.cpu.a[idx] & 0xFFFFFFFF
        if kind == 'MDR':
            return getattr(self.cpu, 'mdr', 0) & 0xFFFFFFFF
        if kind == 'LIR':
            return getattr(self.cpu, 'lir', 0) & 0xFFFFFFFF
        if kind == 'LAR':
            return getattr(self.cpu, 'lar', 0) & 0xFFFFFFFF
        return 0

    def _set_reg_value(self, kind, idx, value):
        value &= 0xFFFFFFFF
        if kind == 'D':
            self.cpu.d[idx] = value
        elif kind == 'A':
            self.cpu.a[idx] = value
        elif kind == 'MDR':
            self.cpu.mdr = value
        elif kind == 'LIR':
            self.cpu.lir = value
        elif kind == 'LAR':
            self.cpu.lar = value

    def _format_reg_list(self, regs):
        if not regs:
            return "[]"
        return "[" + ",".join(f"{k.lower()}{i}" if k in ('D','A') else k.lower() for k, i in regs) + "]"

    def _perform_call_frame(self, return_pc, regs_mask, frame_size):
        """Architectural high-performance CALL frame setup."""
        old_sp = self.cpu.sp & 0xFFFFFFFF
        regs = self._regs_from_callret_mask(regs_mask)
        self.mem.write_dword(old_sp, return_pc & 0xFFFFFFFF)
        for n, (kind, idx) in enumerate(regs, start=1):
            self.mem.write_dword((old_sp - 4*n) & 0xFFFFFFFF, self._get_reg_value(kind, idx))
        self.cpu.sp = (old_sp - (frame_size & 0xFF)) & 0xFFFFFFFF
        self.cpu.mdr = return_pc & 0xFFFFFFFF
        return regs, old_sp

    def _perform_ret_frame(self, regs_mask, frame_size, use_mdr=False):
        """Architectural RET/RETF frame teardown.

        RET loads PC from mem32(SP+imm8). RETF loads PC from MDR. Both restore
        selected registers from the caller frame above the deallocated SP.
        """
        regs = self._regs_from_callret_mask(regs_mask)
        old_sp = (self.cpu.sp + (frame_size & 0xFF)) & 0xFFFFFFFF
        target = (self.cpu.mdr if use_mdr else self.mem.read_dword(old_sp)) & 0xFFFFFFFF
        for n, (kind, idx) in enumerate(regs, start=1):
            self._set_reg_value(kind, idx, self.mem.read_dword((old_sp - 4*n) & 0xFFFFFFFF))
        self.cpu.sp = old_sp
        return target, regs, old_sp

    def _apply_call_return_values(self, target):
        vals = self.call_return_values.get(target & 0xFFFFFFFF, {})
        for reg, value in vals.items():
            v = parse_int(value) & 0xFFFFFFFF
            r = str(reg).strip().upper()
            if r.startswith('D') and r[1:].isdigit():
                idx = int(r[1:])
                if 0 <= idx < 4:
                    self.cpu.d[idx] = v
            elif r.startswith('A') and r[1:].isdigit():
                idx = int(r[1:])
                if 0 <= idx < 4:
                    self.cpu.a[idx] = v
            elif r == 'SP':
                self.cpu.sp = v

    def _eval_runtime_expr(self, expr, default=0):
        """Small JSON expression evaluator for call-policy side effects.

        Supports register names, integer/hex strings, simple + / - arithmetic,
        and bitwise OR used for cached SRAM aliases such as d0|0x80000000.
        """
        if expr is None:
            return default
        if isinstance(expr, int):
            return expr & 0xFFFFFFFF
        s = str(expr).strip().lower().replace(" ", "")
        if not s:
            return default
        if "|" in s:
            v = 0
            for part in s.split("|"):
                v |= self._eval_runtime_expr(part, 0)
            return v & 0xFFFFFFFF
        # Handle one left-to-right + or - expression, enough for sp+8/a0+N.
        for op in ("+", "-"):
            if op in s[1:]:
                left, right = s.split(op, 1)
                a = self._eval_runtime_expr(left, 0)
                b = self._eval_runtime_expr(right, 0)
                return ((a + b) if op == "+" else (a - b)) & 0xFFFFFFFF
        regs = {
            "d0": self.cpu.d[0], "d1": self.cpu.d[1], "d2": self.cpu.d[2], "d3": self.cpu.d[3],
            "a0": self.cpu.a[0], "a1": self.cpu.a[1], "a2": self.cpu.a[2], "a3": self.cpu.a[3],
            "sp": self.cpu.sp, "pc": self.cpu.pc, "mdr": self.cpu.mdr,
        }
        if s in regs:
            return regs[s] & 0xFFFFFFFF
        return parse_int(s, default) & 0xFFFFFFFF

    def _apply_call_side_effects(self, target):
        actions = self.call_side_effects.get(target & 0xFFFFFFFF, [])
        for action in actions:
            kind = str(action.get("kind", action.get("op", ""))).lower()
            if kind == "copy_memory":
                src = self._eval_runtime_expr(action.get("src", action.get("source")))
                dst = self._eval_runtime_expr(action.get("dst", action.get("dest")))
                length = self._eval_runtime_expr(action.get("length", action.get("len", 0)))
                length = max(0, min(length, int(action.get("max_len", 4096))))
                data = [self.mem.read_byte(src + i) for i in range(length)]
                for i, b in enumerate(data):
                    self.mem.write_byte(dst + i, b)
                note = action.get("description") or f"copy 0x{src:08X}->0x{dst:08X} len={length}"
                self.trace_log.append(f"[CALL SIDE EFFECT] 0x{target:08X}: {note}")
                if self.trace_recorder:
                    self.trace_recorder.emit("call_side_effect", action.get("name", f"side_0x{target:08X}"),
                                             pc=self.cpu.pc, addr=dst, size=length,
                                             bytes_hex_str=hex_bytes(data), cpu=self.cpu, notes=note)
            elif kind == "write_memory":
                addr = self._eval_runtime_expr(action.get("address", action.get("addr")))
                data_hex = str(action.get("data_hex", "")).replace(" ", "")
                data = bytes.fromhex(data_hex)
                for i, b in enumerate(data):
                    self.mem.write_byte(addr + i, b)
                note = action.get("description") or f"write 0x{addr:08X} len={len(data)}"
                self.trace_log.append(f"[CALL SIDE EFFECT] 0x{target:08X}: {note}")
                if self.trace_recorder:
                    self.trace_recorder.emit("call_side_effect", action.get("name", f"side_0x{target:08X}"),
                                             pc=self.cpu.pc, addr=addr, size=len(data),
                                             bytes_hex_str=hex_bytes(data), cpu=self.cpu, notes=note)

    def _maybe_skip_call(self, target, return_pc, text):
        target &= 0xFFFFFFFF
        if target in self.skip_call_targets:
            self._apply_call_side_effects(target)
            self._apply_call_return_values(target)
            self.trace_log.append(text + " [SKIPPED BY JSON CALL_POLICY]")
            if self.trace_recorder:
                self.trace_recorder.emit("call_skip", f"skip_0x{target:08X}", pc=self.cpu.pc, addr=target, cpu=self.cpu)
            return True
        return False

    def _maybe_stop_condition(self, pc):
        if not self.stop_conditions:
            return False
        for cond in self.stop_conditions:
            if pc != cond["pc"] or self.cycles < cond["min_cycle"]:
                continue
            name = cond["name"]
            count = self.stop_hit_counts.get(name, 0) + 1
            self.stop_hit_counts[name] = count
            if count >= cond["hit_count"]:
                msg = f"0x{pc:08X}: [STOP] {name} hit {count} at cycle {self.cycles}"
                self.trace_log.append(msg)
                print(f"[*] Stop condition reached: {name} pc=0x{pc:08X} hit={count} cycle={self.cycles}")
                self.running = False
                return True
        return False

    def step(self):
        pc = self.cpu.pc
        if self.trace_recorder:
            self.trace_recorder.before_instruction(self.cycles, pc, self.cpu, self.mem)
        self.mem.gdr_packet_session_before_instruction(self.cycles, pc)
        # Hardware/session shims may deliberately redirect PC for proof-route
        # microcalls.  Re-read PC before stop-condition and opcode fetch so the
        # redirected firmware instruction executes in this cycle.
        pc = self.cpu.pc
        if self._maybe_stop_condition(pc):
            return
        if getattr(self.cpu, "stop_requested", False):
            reason = getattr(self.cpu, "stop_reason", "requested")
            self.trace_log.append(f"0x{pc:08X}: [STOP] {reason}")
            print(f"[*] Stop requested by hardware model: {reason} pc=0x{pc:08X} cycle={self.cycles}")
            self.running = False
            return
        b0 = self.mem.read_byte(pc)

        # 1. Take a Pre-Execution Snapshot
        old_d = list(self.cpu.d)
        old_a = list(self.cpu.a)
        old_sp = self.cpu.sp
        old_flags = (self.cpu.zf, self.cpu.nf, self.cpu.cf, self.cpu.vf)
        
        # Clear the memory trace buffer and record starting log size
        self.mem.mem_trace.clear()
        log_len = len(self.trace_log)

        # 2. Execute the Instruction
        if b0 >= 0xF0:
            cmd_size = self.dispatch_extension(b0)
        else:
            cmd_size = self.dispatch_base(b0)

        self.cpu.pc += cmd_size

        # 3. Calculate the State Diff
        diffs = []
        
        # Check Registers
        for i in range(4):
            if old_d[i] != self.cpu.d[i]: diffs.append(f"d{i}=0x{self.cpu.d[i]:X}")
            if old_a[i] != self.cpu.a[i]: diffs.append(f"a{i}=0x{self.cpu.a[i]:X}")
        if old_sp != self.cpu.sp: diffs.append(f"sp=0x{self.cpu.sp:X}")

        # Check Flags
        if old_flags != (self.cpu.zf, self.cpu.nf, self.cpu.cf, self.cpu.vf):
            flgs = "".join([
                "Z" if self.cpu.zf else "z",
                "N" if self.cpu.nf else "n",
                "C" if self.cpu.cf else "c",
                "V" if self.cpu.vf else "v"
            ])
            diffs.append(f"flg={flgs}")

        # Add Memory Writes
        diffs.extend(self.mem.mem_trace)

        # 4. Inject the Diff into the existing log line
        if len(self.trace_log) > log_len:
            base_line = self.trace_log[-1]
            if diffs:
                diff_str = " [" + ", ".join(diffs) + "]"
                # Format to a fixed column width (50 chars) for clean alignment
                self.trace_log[-1] = f"{base_line:<50}{diff_str}"
                
            # Live instruction output is useful for tiny probes but too slow
            # for persistent sessions and future iSCSI use.
            if getattr(self, "live_instruction_log", True):
                print(self.trace_log[-1])

        if self.trace_recorder:
            self.trace_recorder.after_instruction(self.cycles, pc, self.cpu, self.mem)

    # =========================================================================
    # HELPER MACROS (Derived from IDA ins.hpp)
    # =========================================================================
    def _reg_d(self, val): return val & 3               # Extracts D0-D3 (Destination)
    def _reg_a(self, val): return val & 3               # Extracts A0-A3 (Destination)
    def _reg_dm(self, val): return (val >> 2) & 3       # Extracts D0-D3 (Source)
    def _reg_am(self, val): return (val >> 2) & 3       # Extracts A0-A3 (Source)

    def _pack_psw(self):
        # Keep the emulator's explicit flag booleans reflected into low PSW bits
        # without disturbing high control bits manipulated by firmware and/or
        # JSON probes.  Exact bit positions are only needed for round-tripping
        # through the ROM's save/restore wrappers in the current harnesses.
        v = self.cpu.psw & 0xFFFFFFF0
        if self.cpu.zf: v |= 0x1
        if self.cpu.nf: v |= 0x2
        if self.cpu.cf: v |= 0x4
        if self.cpu.vf: v |= 0x8
        self.cpu.psw = v & 0xFFFFFFFF
        return self.cpu.psw

    def _unpack_psw(self, value):
        self.cpu.psw = value & 0xFFFFFFFF
        self.cpu.zf = bool(value & 0x1)
        self.cpu.nf = bool(value & 0x2)
        self.cpu.cf = bool(value & 0x4)
        self.cpu.vf = bool(value & 0x8)

    def _push_movm_other(self):
        self._movm_other_stack.append((
            list(self.cpu.d), list(self.cpu.a), self.cpu.mdr, self.cpu.lir,
            self.cpu.lar, self.cpu.psw, self.cpu.zf, self.cpu.nf, self.cpu.cf, self.cpu.vf,
        ))

    def _pop_movm_other(self):
        if not self._movm_other_stack:
            return False
        d, a, mdr, lir, lar, psw, zf, nf, cf, vf = self._movm_other_stack.pop()
        self.cpu.d[:] = d
        self.cpu.a[:] = a
        self.cpu.mdr = mdr
        self.cpu.lir = lir
        self.cpu.lar = lar
        self.cpu.psw = psw
        self.cpu.zf, self.cpu.nf, self.cpu.cf, self.cpu.vf = zf, nf, cf, vf
        return True

    # =========================================================================
    # PART 1: BASE DISPATCHER (0x00 to 0xEF)
    # =========================================================================
    def dispatch_base(self, b0):
        pc = self.cpu.pc
        
        # =====================================================================
        # EXACT ALIGNMENT TIER 1 (Standard MN103S Matrix)
        # =====================================================================
        
        # 0x00 - 0x0F: NOPs and Absolute Memory Stores
        if b0 in [0x00, 0x04, 0x08, 0x0C]: # clr Dn
            dn = (b0 >> 2) & 3
            self.cpu.d[dn] = 0
            self.cpu.zf = True
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          clr d{dn}")
            return 1
            
        elif b0 in [0x01, 0x05, 0x09, 0x0D]: # mov Dn, (abs16) [32-bit Store]
            dn = (b0 >> 2) & 3
            abs16 = self.mem.read_word(pc + 1)
            self.mem.write_dword(abs16, self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    mov d{dn}, (0x{abs16:04X})")
            return 3
            
        elif b0 in [0x02, 0x06, 0x0A, 0x0E]: # movbu Dn, (abs16) [8-bit Store]
            dn = (b0 >> 2) & 3
            abs16 = self.mem.read_word(pc + 1)
            self.mem.write_byte(abs16, self.cpu.d[dn] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    movbu d{dn}, (0x{abs16:04X})")
            return 3
            
        elif b0 in [0x03, 0x07, 0x0B, 0x0F]: # movhu Dn, (abs16) [16-bit Store]
            dn = (b0 >> 2) & 3
            abs16 = self.mem.read_word(pc + 1)
            self.mem.write_word(abs16, self.cpu.d[dn] & 0xFFFF)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    movhu d{dn}, (0x{abs16:04X})")
            return 3
        
        # =====================================================================
        # 0x14 - 0x17: extbu Dn (Zero extend byte to 32-bit)
        # =====================================================================
        elif b0 >= 0x14 and b0 <= 0x17:
            dn = b0 & 3
            self.cpu.d[dn] &= 0xFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          extbu d{dn}")
            return 1

        # 0x18 - 0x1B: exth Dn (Sign extend halfword to 32-bit)
        elif b0 >= 0x18 and b0 <= 0x1B:
            dn = b0 & 3
            self.cpu.d[dn] = self._s16(self.cpu.d[dn]) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          exth d{dn}")
            return 1
        
        # =====================================================================
        # 0x28 - 0x2B: add imm8, Dn
        # =====================================================================
        elif b0 >= 0x28 and b0 <= 0x2B:
            dn = b0 & 3
            imm8 = self.mem.read_byte(pc + 1)
            # Handle signed 8-bit immediate
            if imm8 > 127: imm8 -= 256
            
            res = self.cpu.d[dn] + imm8
            self.cpu.d[dn] = res & 0xFFFFFFFF
            
            # Update Flags (Crucial for loop evaluation!)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = (self.cpu.d[dn] & 0x80000000) != 0
            
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       add {imm8}, d{dn}")
            return 2
        
        # 0x10 - 0x13: extb Dn (Sign extend byte to 32-bit)
        elif b0 >= 0x10 and b0 <= 0x13:
            dn = b0 & 3
            val = self.cpu.d[dn] & 0xFF
            if val > 127: val -= 256 # Sign extend
            self.cpu.d[dn] = val & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          extb d{dn}")
            return 1

        # 0x1C - 0x1F: Sign Extensions
        elif b0 >= 0x1C and b0 <= 0x1F: # exthu Dn
            dn = b0 & 3
            self.cpu.d[dn] &= 0xFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          exthu d{dn}")
            return 1

        # 0x20 - 0x2F: Immediates
        elif b0 >= 0x20 and b0 <= 0x23: # add imm8, An
            an = b0 & 3
            imm8 = self.mem.read_byte(pc + 1)
            if imm8 > 127: imm8 -= 256
            self.cpu.a[an] = (self.cpu.a[an] + imm8) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {imm8&0xFF:02X}       add {imm8}, a{an}")
            return 2
        elif b0 >= 0x24 and b0 <= 0x27: # mov imm16, An
            an = b0 & 3
            imm16 = self.mem.read_word(pc + 1)
            self.cpu.a[an] = imm16
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {imm16&0xFF:02X} {imm16>>8:02X}    mov {imm16}, a{an}")
            return 3
        elif b0 >= 0x2C and b0 <= 0x2F: # mov imm16, Dn
            dn = b0 & 3
            imm16 = self.mem.read_word(pc + 1)
            self.cpu.d[dn] = imm16
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {imm16&0xFF:02X} {imm16>>8:02X}    mov {imm16}, d{dn}")
            return 3

        # 0x30 - 0x3F: Immediate Memory Loads and SP moves
        elif b0 >= 0x30 and b0 <= 0x33: # mov (abs16), Dn
            dn = b0 & 3
            abs16 = self.mem.read_word(pc + 1)
            self.cpu.d[dn] = self.mem.read_dword(abs16)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    mov (0x{abs16:04X}), d{dn}")
            return 3
        elif b0 >= 0x34 and b0 <= 0x37: # movbu (abs16), Dn
            dn = b0 & 3
            abs16 = self.mem.read_word(pc + 1)
            self.cpu.d[dn] = self.mem.read_byte(abs16)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    movbu (0x{abs16:04X}), d{dn}")
            return 3
        elif b0 >= 0x38 and b0 <= 0x3B: # movhu (abs16), Dn
            dn = b0 & 3
            abs16 = self.mem.read_word(pc + 1)
            self.cpu.d[dn] = self.mem.read_word(abs16)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {abs16&0xFF:02X} {abs16>>8:02X}    movhu (0x{abs16:04X}), d{dn}")
            return 3
        elif b0 >= 0x3C and b0 <= 0x3F: # mov SP, An
            an = b0 & 3
            self.cpu.a[an] = self.cpu.sp
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          mov sp, a{an}")
            return 1

        # 0x42/46/4A/4E and 0x43/47/4B/4F: 32-bit stores to (disp8, SP)
        elif b0 in [0x42, 0x46, 0x4A, 0x4E]: # mov Dn, (disp8, SP)
            dn = (b0 - 0x42) // 4
            off = self.mem.read_byte(pc + 1)
            self.mem.write_dword((self.cpu.sp + off) & 0xFFFFFFFF, self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {off:02X}       mov d{dn}, ({off},sp)")
            return 2
        elif b0 in [0x43, 0x47, 0x4B, 0x4F]: # mov An, (disp8, SP)
            an = (b0 - 0x43) // 4
            off = self.mem.read_byte(pc + 1)
            self.mem.write_dword((self.cpu.sp + off) & 0xFFFFFFFF, self.cpu.a[an])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {off:02X}       mov a{an}, ({off},sp)")
            return 2

        # 0x40 - 0x53: inc Dn / inc An
        elif b0 in [0x40, 0x44, 0x48, 0x4C]: # inc Dn
            dn = (b0 - 0x40) // 4
            self.cpu.d[dn] = (self.cpu.d[dn] + 1) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          inc d{dn}")
            return 1
            
        elif b0 in [0x41, 0x45, 0x49, 0x4D]: # inc An
            an = (b0 - 0x41) // 4
            self.cpu.a[an] = (self.cpu.a[an] + 1) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          inc a{an}")
            return 1
            
        # 0x50 - 0x53: inc4 An
        elif b0 >= 0x50 and b0 <= 0x53: # inc4 An
            an = b0 & 3
            self.cpu.a[an] = (self.cpu.a[an] + 4) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          inc4 a{an}")
            return 1

        elif b0 >= 0x54 and b0 <= 0x57: # asl2 Dn
            dn = b0 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] << 2) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          asl2 d{dn}")
            return 1

        elif b0 >= 0x58 and b0 <= 0x5B: # mov (disp8, SP), Dn
            dn = b0 & 3
            off = self.mem.read_byte(pc + 1)
            self.cpu.d[dn] = self.mem.read_dword((self.cpu.sp + off) & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {off:02X}       mov ({off},sp), d{dn}")
            return 2

        elif b0 >= 0x5C and b0 <= 0x5F: # mov (disp8, SP), An
            an = b0 & 3
            off = self.mem.read_byte(pc + 1)
            self.cpu.a[an] = self.mem.read_dword((self.cpu.sp + off) & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {off:02X}       mov ({off},sp), a{an}")
            return 2

        # 0x60 - 0x7F: Pointer Indirection
        elif b0 >= 0x60 and b0 <= 0x6F: # mov Dm, (An)
            dm = (b0 >> 2) & 3
            an = b0 & 3
            self.mem.write_dword(self.cpu.a[an], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          mov d{dm}, (a{an})")
            return 1
        elif b0 >= 0x70 and b0 <= 0x7F: # mov (An), Dm
            dm = (b0 >> 2) & 3
            an = b0 & 3
            self.cpu.d[dm] = self.mem.read_dword(self.cpu.a[an])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          mov (a{an}), d{dm}")
            return 1

        # =====================================================================
        # 0x80 - 0xBF: REGISTER/IMMEDIATE MOVES AND COMPARES (ana.cpp compliant)
        # =====================================================================
        elif b0 >= 0x80 and b0 <= 0x8F:
            dm = (b0 >> 2) & 3
            dn = b0 & 3
            if dm == dn: # mov imm8, Dn
                imm8 = self.mem.read_byte(pc + 1)
                if imm8 > 127: imm8 -= 256
                self.cpu.d[dn] = imm8 & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       mov {imm8}, d{dn}")
                return 2
            else: # mov Dm, Dn
                self.cpu.d[dn] = self.cpu.d[dm]
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          mov d{dm}, d{dn}")
                return 1

        elif b0 >= 0x90 and b0 <= 0x9F:
            am = (b0 >> 2) & 3
            an = b0 & 3
            if am == an: # mov imm8, An
                imm8 = self.mem.read_byte(pc + 1)
                if imm8 > 127: imm8 -= 256
                self.cpu.a[an] = imm8 & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       mov {imm8}, a{an}")
                return 2
            else: # mov Am, An
                self.cpu.a[an] = self.cpu.a[am]
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          mov a{am}, a{an}")
                return 1

        elif b0 >= 0xA0 and b0 <= 0xAF:
            dm = (b0 >> 2) & 3
            dn = b0 & 3
            if dm == dn: # cmp imm8, Dn
                imm8 = self.mem.read_byte(pc + 1)
                if imm8 > 127: imm8 -= 256
                res = (self.cpu.d[dn] - imm8) & 0xFFFFFFFF
                self.cpu.zf = (res == 0)
                self.cpu.nf = (res & 0x80000000) != 0
                self.cpu.cf = (imm8 > self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       cmp {imm8}, d{dn}")
                return 2
            else: # cmp Dm, Dn
                val_dn = self.cpu.d[dn]
                val_dm = self.cpu.d[dm]
                res = (val_dn - val_dm) & 0xFFFFFFFF
                self.cpu.zf = (res == 0)
                self.cpu.nf = (res & 0x80000000) != 0
                self.cpu.cf = (val_dm > val_dn)
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          cmp d{dm}, d{dn}")
                return 1

        elif b0 >= 0xB0 and b0 <= 0xBF:
            am = (b0 >> 2) & 3
            an = b0 & 3
            if am == an: # cmp imm8, An
                imm8 = self.mem.read_byte(pc + 1)
                if imm8 > 127: imm8 -= 256
                res = (self.cpu.a[an] - imm8) & 0xFFFFFFFF
                self.cpu.zf = (res == 0)
                self.cpu.nf = (res & 0x80000000) != 0
                self.cpu.cf = (imm8 > self.cpu.a[an])
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       cmp {imm8}, a{an}")
                return 2
            else: # cmp Am, An
                val_an = self.cpu.a[an]
                val_am = self.cpu.a[am]
                res = (val_an - val_am) & 0xFFFFFFFF
                self.cpu.zf = (res == 0)
                self.cpu.nf = (res & 0x80000000) != 0
                self.cpu.cf = (val_am > val_an)
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          cmp a{am}, a{an}")
                return 1

        elif 0xE0 <= b0 <= 0xEF:  # add Dm,Dn compact 1-byte form
            dm = (b0 >> 2) & 3
            dn = b0 & 3
            self.cpu.d[dn] = self._add_u32(self.cpu.d[dn], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          add d{dm}, d{dn}")
            return 1
        
        # =====================================================================
        # 0x90 - 0x9F: 32-bit Register Arithmetic (add / sub)
        # =====================================================================
        elif b0 >= 0x90 and b0 <= 0x93: # add Dm, Dn
            dm = b0 & 3
            b1 = self.mem.read_byte(pc + 1)
            dn = (b1 >> 2) & 3
            
            res = self.cpu.d[dn] + self.cpu.d[dm]
            self.cpu.d[dn] = res & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {b1:02X}          add d{dm}, d{dn}")
            return 2
            
        elif b0 >= 0x94 and b0 <= 0x97: # sub Dm, Dn
            dm = b0 & 3
            b1 = self.mem.read_byte(pc + 1)
            dn = (b1 >> 2) & 3
            
            res = self.cpu.d[dn] - self.cpu.d[dm]
            self.cpu.d[dn] = res & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {b1:02X}          sub d{dm}, d{dn}")
            return 2
        
        # =====================================================================
        # MOVM special register group used by firmware PSW save/restore wrappers
        # =====================================================================
        elif b0 == 0xCF:
            b1 = self.mem.read_byte(pc + 1)
            if b1 == 0x08:
                self._push_movm_other()
                self.trace_log.append(f"0x{pc:08X}: CF 08          movm [other],(sp)")
                return 2
            regs = self._regs_from_callret_mask(b1)
            for n, (kind, idx) in enumerate(regs):
                self.mem.write_dword((self.cpu.sp + 4*n) & 0xFFFFFFFF, self._get_reg_value(kind, idx))
            self.trace_log.append(f"0x{pc:08X}: CF {b1:02X}          movm {self._format_reg_list(regs)},(sp)")
            return 2

        elif b0 == 0xCE:
            b1 = self.mem.read_byte(pc + 1)
            if b1 == 0x08:
                restored = self._pop_movm_other()
                note = "" if restored else " [WARN: empty movm stack]"
                self.trace_log.append(f"0x{pc:08X}: CE 08          movm (sp),[other]{note}")
                return 2
            regs = self._regs_from_callret_mask(b1)
            for n, (kind, idx) in enumerate(regs):
                self._set_reg_value(kind, idx, self.mem.read_dword((self.cpu.sp + 4*n) & 0xFFFFFFFF))
            self.trace_log.append(f"0x{pc:08X}: CE {b1:02X}          movm (sp),{self._format_reg_list(regs)}")
            return 2

        # =====================================================================
        # 16-BIT RELATIVE BRANCH (0xCC)
        # =====================================================================
        elif b0 == 0xCC: # jmp (d16, PC)
            rel16 = self.mem.read_word(pc + 1)
            if rel16 > 32767: rel16 -= 65536
            target = (pc + rel16) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: CC {self.mem.read_byte(pc+1):02X} {self.mem.read_byte(pc+2):02X}       jmp -> 0x{target:08X}")
            self.cpu.pc = target
            return 0 # PC explicitly updated
        
        # =====================================================================
        # 16-BIT RELATIVE CALL (0xCD)
        # =====================================================================
        elif b0 == 0xCD: # call (d16, PC), 5-byte high-performance CALL form
            rel16 = self.mem.read_word(pc + 1)
            if rel16 > 32767: rel16 -= 65536
            target = (pc + rel16) & 0xFFFFFFFF
            regs_mask = self.mem.read_byte(pc + 3)
            frame_size = self.mem.read_byte(pc + 4)
            regs_preview = self._format_reg_list(self._regs_from_callret_mask(regs_mask))
            text = f"0x{pc:08X}: CD {self.mem.read_byte(pc+1):02X} {self.mem.read_byte(pc+2):02X} {regs_mask:02X} {frame_size:02X}    call -> 0x{target:08X} {regs_preview},{frame_size}"
            if self._maybe_skip_call(target, pc + 5, text):
                return 5
            regs, old_sp = self._perform_call_frame(pc + 5, regs_mask, frame_size)
            self.trace_log.append(text + f" [frame old_sp=0x{old_sp:08X} new_sp=0x{self.cpu.sp:08X}]")
            self.cpu.pc = target
            return 0

        # =====================================================================
        # 8-BIT RELATIVE BRANCHES (0xC0 - 0xCA)
        # =====================================================================
        elif b0 >= 0xC0 and b0 <= 0xCA: 
            rel8 = self.mem.read_byte(pc + 1)
            if rel8 > 127: rel8 -= 256
            
            # Safely fetch flags (defaulting to False if not yet initialized)
            zf = getattr(self.cpu, 'zf', False)
            nf = getattr(self.cpu, 'nf', False)
            cf = getattr(self.cpu, 'cf', False)
            vf = getattr(self.cpu, 'vf', False)
            
            taken = False
            mnemonic = "b??"
            
            if b0 == 0xC0: 
                taken = (nf != vf); mnemonic = "blt"
            elif b0 == 0xC1: 
                taken = (not zf) and (nf == vf); mnemonic = "bgt"
            elif b0 == 0xC2: 
                taken = (nf == vf); mnemonic = "bge"
            elif b0 == 0xC3: 
                taken = zf or (nf != vf); mnemonic = "ble"
            elif b0 == 0xC4: 
                taken = cf; mnemonic = "bcs"
            elif b0 == 0xC5: 
                taken = (not cf) and (not zf); mnemonic = "bhi"
            elif b0 == 0xC6: 
                taken = not cf; mnemonic = "bcc"
            elif b0 == 0xC7: 
                taken = cf or zf; mnemonic = "bls"
            elif b0 == 0xC8: 
                taken = zf; mnemonic = "beq"
            elif b0 == 0xC9: 
                taken = not zf; mnemonic = "bne"
            elif b0 == 0xCA: 
                taken = True; mnemonic = "bra"
            
            if taken:
                target = pc + rel8
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       {mnemonic} -> 0x{target:08X} [TAKEN]")
                self.cpu.pc = target
                return 0 # PC explicitly updated
            else:
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {self.mem.read_byte(pc+1):02X}       {mnemonic} -> 0x{pc + rel8:08X} [SKIPPED]")
                return 2
            
        # =====================================================================
        # 32-bit relative jump/call (0xDC/0xDD).
        # =====================================================================
        elif b0 == 0xDC:
            rel32 = self.mem.read_dword(pc + 1)
            if rel32 & 0x80000000:
                rel32 -= 0x100000000
            target = (pc + rel32) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: DC {self.mem.read_byte(pc+1):02X} {self.mem.read_byte(pc+2):02X} {self.mem.read_byte(pc+3):02X} {self.mem.read_byte(pc+4):02X} jmp -> 0x{target:08X}")
            self.cpu.pc = target
            return 0

        # Default is JSON-skipped for targeted trace harnesses; otherwise it calls.
        elif b0 == 0xDD:
            rel32 = self.mem.read_dword(pc + 1)
            if rel32 & 0x80000000:
                rel32 -= 0x100000000
            target = (pc + rel32) & 0xFFFFFFFF
            regs_mask = self.mem.read_byte(pc + 5)
            frame_size = self.mem.read_byte(pc + 6)
            regs_preview = self._format_reg_list(self._regs_from_callret_mask(regs_mask))
            text = f"0x{pc:08X}: DD {self.mem.read_byte(pc+1):02X} {self.mem.read_byte(pc+2):02X} {self.mem.read_byte(pc+3):02X} {self.mem.read_byte(pc+4):02X} {regs_mask:02X} {frame_size:02X} call -> 0x{target:08X} {regs_preview},{frame_size}"
            if self._maybe_skip_call(target, pc + 7, text):
                return 7
            regs, old_sp = self._perform_call_frame(pc + 7, regs_mask, frame_size)
            self.trace_log.append(text + f" [frame old_sp=0x{old_sp:08X} new_sp=0x{self.cpu.sp:08X}]")
            self.cpu.pc = target
            return 0

        # =====================================================================
        # RETURN INSTRUCTIONS (0xDE, 0xDF)
        # =====================================================================
        elif b0 == 0xDE or b0 == 0xDF: # retf / ret, 3-byte high-performance forms
            regs_mask = self.mem.read_byte(pc + 1)
            frame_size = self.mem.read_byte(pc + 2)
            use_mdr = (b0 == 0xDE)
            target, regs, old_sp = self._perform_ret_frame(regs_mask, frame_size, use_mdr=use_mdr)
            regs_text = self._format_reg_list(regs)
            mnemonic = "retf" if use_mdr else "ret"
            if target == 0 or old_sp == 0:
                self.trace_log.append(f"0x{pc:08X}: {b0:02X} {regs_mask:02X} {frame_size:02X}    {mnemonic} {regs_text},{frame_size} (HALT: no return target)")
                print(f"\n[*] Execution reached return with no target (SP={self.cpu.sp}). Halting safely.")
                self.running = False
                return 0
            self.trace_log.append(f"0x{pc:08X}: {b0:02X} {regs_mask:02X} {frame_size:02X}    {mnemonic} {regs_text},{frame_size} -> 0x{target:08X} [sp=0x{self.cpu.sp:08X}]")
            self.cpu.pc = target
            return 0

        # =====================================================================
        # LOOP BRANCH SETUP/LOOP BRANCHES (0xD0-0xDB)
        # =====================================================================
        elif 0xD0 <= b0 <= 0xDA:
            mnemonic = self._lcc_name(b0)
            taken = self._lcc_condition(b0)
            target = ((self.cpu.lar - 4) & 0xFFFFFFFF)
            if taken:
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          {mnemonic} -> 0x{target:08X} [TAKEN]")
                self.cpu.pc = target
                return 0
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          {mnemonic} -> 0x{target:08X} [SKIPPED]")
            return 1

        elif b0 == 0xDB:
            self.cpu.lir = self.mem.read_dword(pc + 1)
            self.cpu.lar = (pc + 5) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: DB          setlb [lir=0x{self.cpu.lir:08X}, lar=0x{self.cpu.lar:08X}]")
            return 1

        # =====================================================================
        # 0xCB: NOP.  Earlier versions treated this as a generic one-byte
        # fallback; implementing it directly removes false unknown noise from
        # PSW guard wrappers and copy helpers.
        # =====================================================================
        elif b0 == 0xCB:
            self.trace_log.append(f"0x{pc:08X}: CB          nop")
            return 1

        # =====================================================================
        # TIER 2: IDA MODULE ALIGNMENT SAFETY NET
        # Forces unmapped opcodes to advance by their architectural width
        # =====================================================================
        elif b0 in [0x01, 0x02, 0x03, 0x05, 0x09, 0x0A, 0x0B, 0x0D, 0x0E, 0x0F]:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 3-BYTE]")
            return 3
        elif b0 >= 0x10 and b0 <= 0x1F:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
        elif b0 >= 0x20 and b0 <= 0x2B:
            if b0 >= 0x24 and b0 <= 0x27:
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 3-BYTE]")
                return 3
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 2-BYTE]")
            return 2
        elif b0 >= 0x2C and b0 <= 0x3B:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 3-BYTE]")
            return 3
        elif b0 >= 0x40 and b0 <= 0x7F:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
        
        elif b0 >= 0x80 and b0 <= 0xBF:
            if b0 in [0x80, 0x90, 0xA0, 0xB0]:
                self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 2-BYTE]")
                return 2
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
        elif b0 >= 0xC0 and b0 <= 0xCA:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 2-BYTE]")
            return 2
        elif b0 in [0xCB, 0xDB]:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
        elif b0 in [0xCD, 0xCE, 0xCF]:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 3-BYTE]")
            return 3
        elif b0 >= 0xD0 and b0 <= 0xDA:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
        elif b0 in [0xDC, 0xDD]:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 5-BYTE]")
            return 5
        elif b0 in [0xDE, 0xDF]:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 3-BYTE]")
            return 3
        elif b0 >= 0xE8 and b0 <= 0xEF: # add Dm, Dn
            dm = (b0 >> 2) & 3
            dn = b0 & 3
            lhs = self.cpu.d[dn] & 0xFFFFFFFF
            rhs = self.cpu.d[dm] & 0xFFFFFFFF
            res = (lhs + rhs) & 0xFFFFFFFF
            self.cpu.d[dn] = res
            self._set_nz_flags(res)
            self.cpu.cf = (lhs + rhs) > 0xFFFFFFFF
            self.cpu.vf = False
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          add d{dm}, d{dn}")
            return 1

        elif b0 >= 0xE0 and b0 <= 0xEF:
            self.trace_log.append(f"0x{pc:08X}: {b0:02X}          [GENERIC 1-BYTE]")
            return 1
            
        raise NotImplementedError(f"Base Opcode 0x{b0:02X} not mapped properly at 0x{pc:08X}")

    # =========================================================================
    # PART 2: EXTENSION DISPATCHER (0xF0 to 0xFF)
    # =========================================================================
    def dispatch_extension(self, b0):
        # Maps to the ana_main() extF0 - extFF switch table
        ext_handlers = {
            0xF0: self.extF0, 0xF1: self.extF1, 0xF2: self.extF2,
            0xF3: self.extF3, 0xF4: self.extF4, 0xF5: self.extF5,
            0xF6: self.extF6, 0xF7: self.extF7, 0xF8: self.extF8,
            0xF9: self.extF9, 0xFA: self.extFA, 0xFB: self.extFB,
            0xFC: self.extFC, 0xFD: self.extFD, 0xFE: self.extFE,
            0xFF: self.extFF
        }
        
        if b0 in ext_handlers:
            return ext_handlers[b0](b0)
        
        # Absolute failsafe (should not be reached if map is intact)
        return 1

    # -------------------------------------------------------------
    # 2-Byte Formats (F0 - F6)
    # -------------------------------------------------------------
    def extF0(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        hi = (b1 >> 4) & 0x0F
        lo = b1 & 0x0F
        an = lo & 3
        am = (lo >> 2) & 3
        dn = (lo >> 2) & 3
        dm = (lo >> 2) & 3

        # F0 00-0F: mov (Am), An
        if hi == 0x0:
            self.cpu.a[dn] = self.mem.read_dword(self.cpu.a[an])
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          mov (a{an}), a{dn}")
            return 2

        # F0 10-1F: mov Am, (An)
        if hi == 0x1:
            self.mem.write_dword(self.cpu.a[an], self.cpu.a[dm] & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          mov a{dm}, (a{an})")
            return 2

        # F0 40-4F: movbu (An), Dn
        if hi == 0x4:
            self.cpu.d[dn] = self.mem.read_byte(self.cpu.a[an])
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          movbu (a{an}), d{dn}")
            return 2

        # F0 50-5F: movbu Dm, (An)
        if hi == 0x5:
            self.mem.write_byte(self.cpu.a[an], self.cpu.d[dm] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          movbu d{dm}, (a{an})")
            return 2

        # F0 60-6F: movhu (An), Dn
        if hi == 0x6:
            self.cpu.d[dn] = self.mem.read_word(self.cpu.a[an])
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          movhu (a{an}), d{dn}")
            return 2

        # F0 70-7F: movhu Dm, (An)
        if hi == 0x7:
            self.mem.write_word(self.cpu.a[an], self.cpu.d[dm] & 0xFFFF)
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          movhu d{dm}, (a{an})")
            return 2

        # F0 80-8F / 90-9F: bset/bclr Dm,(An).  The bit selector comes
        # from Dm's low bits; ZF reflects the old masked bit state.
        if hi in (0x8, 0x9):
            bit = self.cpu.d[dm] & 0x07
            mask = 1 << bit
            old = self.mem.read_byte(self.cpu.a[an])
            self.cpu.zf = ((old & mask) == 0)
            self.cpu.nf = False
            if hi == 0x8:
                self.mem.write_byte(self.cpu.a[an], old | mask)
                self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          bset d{dm}, (a{an})")
            else:
                self.mem.write_byte(self.cpu.a[an], old & (~mask & 0xFF))
                self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          bclr d{dm}, (a{an})")
            return 2

        # F0 F0-F3: CALLS (An).
        #
        # CALLS is not the same as JSR. The MN103S instruction manual defines
        # CALLS (An) as:
        #     PC+2 -> mem32(SP), PC+2 -> MDR, An -> PC
        # i.e. it stores the return PC at the current SP and updates MDR, but
        # does not decrement SP. This is why firmware called through CALLS can
        # still return with a normal RET [],imm frame teardown.
        if 0xF0 <= b1 <= 0xF3:
            an = b1 & 3
            target = self.cpu.a[an] & 0xFFFFFFFF
            return_pc = (pc + 2) & 0xFFFFFFFF
            sp = self.cpu.sp & 0xFFFFFFFF
            text = f"0x{pc:08X}: F0 {b1:02X}          calls (a{an}) -> 0x{target:08X}"
            if self._maybe_skip_call(target, return_pc, text):
                self.mem.write_dword(sp, return_pc)
                self.cpu.mdr = return_pc
                return 2
            self.mem.write_dword(sp, return_pc)
            self.cpu.mdr = return_pc
            self.cpu.pc = target
            self.trace_log.append(text + f" [ret=0x{return_pc:08X}, sp=0x{sp:08X}, mdr=0x{return_pc:08X}]")
            return 0

        # F0 F4-F7: JMP (An).
        if 0xF4 <= b1 <= 0xF7:
            an = b1 & 3
            target = self.cpu.a[an] & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          jmp (a{an}) -> 0x{target:08X}")
            self.cpu.pc = target
            return 0

        if b1 == 0xFC:
            target = self.cpu.mdr & 0xFFFFFFFF
            if target == 0:
                self.trace_log.append(f"0x{pc:08X}: F0 FC          rets (HALT: MDR empty)")
                self.running = False
                return 0
            self.trace_log.append(f"0x{pc:08X}: F0 FC          rets -> 0x{target:08X}")
            self.cpu.pc = target
            return 0

        if b1 == 0xFD:
            # RTI: restore PSW then PC from the interrupt frame.
            self._unpack_psw(self.mem.read_word(self.cpu.sp))
            target = self.mem.read_dword(self.cpu.sp + 4)
            self.cpu.sp = (self.cpu.sp + 8) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: F0 FD          rti -> 0x{target:08X}")
            self.cpu.pc = target
            return 0

        if b1 == 0xFE:
            self.trace_log.append(f"0x{pc:08X}: F0 FE          trap (HALT)")
            self.running = False
            return 2

        self.trace_log.append(f"0x{pc:08X}: F0 {b1:02X}          [F0 EXTENSION/INVALID]")
        return 2

    def extF1(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        hi = (b1 >> 4) & 0x0F
        dm = (b1 >> 2) & 3
        dn = b1 & 3
        am = dm
        an = dn

        if hi == 0x0:  # sub Dm,Dn
            self.cpu.d[dn] = self._sub_u32(self.cpu.d[dn], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          sub d{dm}, d{dn}")
            return 2
        if hi == 0x1:  # sub Am,Dn
            self.cpu.d[dn] = self._sub_u32(self.cpu.d[dn], self.cpu.a[am])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          sub a{am}, d{dn}")
            return 2
        if hi == 0x2:  # sub Dm,An
            self.cpu.a[an] = self._sub_u32(self.cpu.a[an], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          sub d{dm}, a{an}")
            return 2
        if hi == 0x3:  # sub Am,An
            self.cpu.a[an] = self._sub_u32(self.cpu.a[an], self.cpu.a[am])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          sub a{am}, a{an}")
            return 2
        if hi == 0x4:  # addc Dm,Dn
            self.cpu.d[dn] = self._add_u32(self.cpu.d[dn], self.cpu.d[dm], 1 if self.cpu.cf else 0)
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          addc d{dm}, d{dn}")
            return 2
        if hi == 0x5:  # add Am,Dn
            self.cpu.d[dn] = self._add_u32(self.cpu.d[dn], self.cpu.a[am])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          add a{am}, d{dn}")
            return 2
        if hi == 0x6:  # add Dm,An
            self.cpu.a[an] = self._add_u32(self.cpu.a[an], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          add d{dm}, a{an}")
            return 2
        if hi == 0x7:  # add Am,An
            self.cpu.a[an] = self._add_u32(self.cpu.a[an], self.cpu.a[am])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          add a{am}, a{an}")
            return 2
        if hi == 0x8:  # subc Dm,Dn
            self.cpu.d[dn] = self._sub_u32(self.cpu.d[dn], self.cpu.d[dm], 1 if self.cpu.cf else 0)
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          subc d{dm}, d{dn}")
            return 2
        if hi == 0x9:  # cmp Am,Dn
            self._cmp_u32(self.cpu.d[dn], self.cpu.a[am])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          cmp a{am}, d{dn}")
            return 2
        if hi == 0xA:  # cmp Dm,An
            self._cmp_u32(self.cpu.a[an], self.cpu.d[dm])
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          cmp d{dm}, a{an}")
            return 2
        if hi == 0xD:  # mov Am,Dn
            self.cpu.d[dn] = self.cpu.a[am] & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          mov a{am}, d{dn}")
            return 2
        if hi == 0xE:  # mov Dm,An
            self.cpu.a[an] = self.cpu.d[dm] & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          mov d{dm}, a{an}")
            return 2

        self.trace_log.append(f"0x{pc:08X}: F1 {b1:02X}          [F1 EXTENSION/INVALID]")
        return 2

    def extF2(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        hi = b1 >> 4
        lo = b1 & 0x0F
        dm = (b1 >> 2) & 3
        dn = b1 & 3

        def s32(v):
            v &= 0xFFFFFFFF
            return v - 0x100000000 if (v & 0x80000000) else v

        def s64(v):
            v &= 0xFFFFFFFFFFFFFFFF
            return v - 0x10000000000000000 if (v & 0x8000000000000000) else v

        # F2 0x-2x: register logical operations Dm,Dn.
        if hi == 0x0:
            self.cpu.d[dn] = (self.cpu.d[dn] & self.cpu.d[dm]) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          and d{dm}, d{dn}")
            return 2
        if hi == 0x1:
            self.cpu.d[dn] = (self.cpu.d[dn] | self.cpu.d[dm]) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          or d{dm}, d{dn}")
            return 2
        if hi == 0x2:
            self.cpu.d[dn] = (self.cpu.d[dn] ^ self.cpu.d[dm]) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          xor d{dm}, d{dn}")
            return 2

        # F2 30-33: NOT Dn.
        if hi == 0x3 and lo < 4:
            self.cpu.d[dn] = (~self.cpu.d[dn]) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          not d{dn}")
            return 2

        # F2 4x/5x: signed/unsigned multiply.  Result is split across {MDR,Dn}.
        if hi == 0x4:
            prod = (s32(self.cpu.d[dn]) * s32(self.cpu.d[dm])) & 0xFFFFFFFFFFFFFFFF
            self.cpu.d[dn] = prod & 0xFFFFFFFF
            self.cpu.mdr = (prod >> 32) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mul d{dm}, d{dn}")
            return 2
        if hi == 0x5:
            prod = ((self.cpu.d[dn] & 0xFFFFFFFF) * (self.cpu.d[dm] & 0xFFFFFFFF)) & 0xFFFFFFFFFFFFFFFF
            self.cpu.d[dn] = prod & 0xFFFFFFFF
            self.cpu.mdr = (prod >> 32) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mulu d{dm}, d{dn}")
            return 2

        # F2 6x/7x: signed/unsigned division.  Dividend is {MDR,Dn}; quotient -> Dn, remainder -> MDR.
        if hi == 0x6:
            divisor = s32(self.cpu.d[dm])
            dividend = s64(((self.cpu.mdr & 0xFFFFFFFF) << 32) | (self.cpu.d[dn] & 0xFFFFFFFF))
            if divisor == 0:
                self.cpu.vf = True
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          div d{dm}, d{dn} (divide-by-zero)")
                return 2
            q_abs = abs(dividend) // abs(divisor)
            q = -q_abs if ((dividend < 0) ^ (divisor < 0)) else q_abs
            r = dividend - q * divisor
            if q < -0x80000000 or q > 0x7FFFFFFF:
                self.cpu.vf = True
            else:
                self.cpu.vf = False
                self.cpu.d[dn] = q & 0xFFFFFFFF
                self.cpu.mdr = r & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          div d{dm}, d{dn}")
            return 2
        if hi == 0x7:
            divisor = self.cpu.d[dm] & 0xFFFFFFFF
            dividend = ((self.cpu.mdr & 0xFFFFFFFF) << 32) | (self.cpu.d[dn] & 0xFFFFFFFF)
            if divisor == 0:
                self.cpu.vf = True
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          divu d{dm}, d{dn} (divide-by-zero)")
                return 2
            q = dividend // divisor
            r = dividend % divisor
            if q > 0xFFFFFFFF:
                self.cpu.vf = True
            else:
                self.cpu.vf = False
                self.cpu.d[dn] = q & 0xFFFFFFFF
                self.cpu.mdr = r & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          divu d{dm}, d{dn}")
            return 2

        # F2 80-87: ROL/ROR Dn.
        if hi == 0x8 and ((b1 >> 2) & 3) in (0, 1):
            v = self.cpu.d[dn] & 0xFFFFFFFF
            if ((b1 >> 2) & 3) == 0:
                self.cpu.cf = bool(v & 0x80000000)
                self.cpu.d[dn] = ((v << 1) | (1 if self.cpu.cf else 0)) & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          rol d{dn}")
            else:
                self.cpu.cf = bool(v & 0x1)
                self.cpu.d[dn] = ((v >> 1) | (0x80000000 if self.cpu.cf else 0)) & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          ror d{dn}")
            self._set_nz_flags(self.cpu.d[dn])
            return 2

        # F2 9x-Bx: register-count shifts, count from low 5 bits of Dm.
        if hi in (0x9, 0xA, 0xB):
            count = self.cpu.d[dm] & 0x1F
            v = self.cpu.d[dn] & 0xFFFFFFFF
            if count:
                if hi == 0x9:
                    self.cpu.cf = bool((v << (count - 1)) & 0x80000000)
                    v = (v << count) & 0xFFFFFFFF
                    op = "asl"
                elif hi == 0xA:
                    self.cpu.cf = bool((v >> (count - 1)) & 0x1)
                    v = (v >> count) & 0xFFFFFFFF
                    op = "lsr"
                else:
                    self.cpu.cf = bool((v >> (count - 1)) & 0x1)
                    v = (s32(v) >> count) & 0xFFFFFFFF
                    op = "asr"
                self.cpu.d[dn] = v
                self._set_nz_flags(v)
            else:
                op = {0x9: "asl", 0xA: "lsr", 0xB: "asr"}[hi]
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          {op} d{dm}, d{dn}")
            return 2

        # F2 D0-D3: EXT Dn; sign-extend Dn to 64-bit by updating MDR only.
        if hi == 0xD and lo < 4:
            self.cpu.mdr = 0xFFFFFFFF if (self.cpu.d[dn] & 0x80000000) else 0x00000000
            self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          ext d{dn}")
            return 2

        # F2 E0-E3/E4-E7: MOV MDR/PSW,Dn.
        if hi == 0xE:
            selector = (b1 >> 2) & 3
            if selector == 0:
                self.cpu.d[dn] = self.cpu.mdr & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mov mdr, d{dn}")
                return 2
            if selector == 1:
                self.cpu.d[dn] = self._pack_psw()
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mov psw, d{dn}")
                return 2

        # F2 F0/F4/F8/FC: MOV An,SP.  F2 F2/F6/FA/FE: MOV Dn,MDR.  F2 F3/F7/FB/FF: MOV Dn,PSW.
        if hi == 0xF:
            selector = b1 & 3
            rn = (b1 >> 2) & 3
            if selector == 0:
                self.cpu.sp = self.cpu.a[rn] & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mov a{rn}, sp")
                return 2
            if selector == 2:
                self.cpu.mdr = self.cpu.d[rn] & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mov d{rn}, mdr")
                return 2
            if selector == 3:
                self._unpack_psw(self.cpu.d[rn])
                self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          mov d{rn}, psw")
                return 2

        self.trace_log.append(f"0x{pc:08X}: F2 {b1:02X}          [F2 EXTENSION]")
        return 2

    def extF3(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        hi = (b1 >> 4) & 0x0F
        lo = b1 & 0x0F
        an = lo & 0x03
        di = (lo >> 2) & 0x03
        dn = hi & 0x03
        dm = hi & 0x03
        addr = (self.cpu.a[an] + self.cpu.d[di]) & 0xFFFFFFFF

        if hi <= 0x3:  # mov (Di,Am),Dn
            self.cpu.d[dn] = self.mem.read_dword(addr)
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F3 {b1:02X}          mov (d{di},a{an}), d{dn}     [d{dn}=0x{self.cpu.d[dn]:X}]")
            return 2
        if hi <= 0x7:  # mov Dm,(Di,An)
            self.mem.write_dword(addr, self.cpu.d[dm] & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: F3 {b1:02X}          mov d{dm}, (d{di},a{an})")
            return 2
        if hi <= 0xB:  # mov (Di,Am),An
            dst = hi & 0x03
            self.cpu.a[dst] = self.mem.read_dword(addr)
            self.trace_log.append(f"0x{pc:08X}: F3 {b1:02X}          mov (d{di},a{an}), a{dst}     [a{dst}=0x{self.cpu.a[dst]:X}]")
            return 2

        src = hi & 0x03  # mov Am,(Di,An)
        self.mem.write_dword(addr, self.cpu.a[src] & 0xFFFFFFFF)
        self.trace_log.append(f"0x{pc:08X}: F3 {b1:02X}          mov a{src}, (d{di},a{an})")
        return 2

    def extF4(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        hi = (b1 >> 4) & 0x0F
        lo = b1 & 0x0F
        an = lo & 0x03
        di = (lo >> 2) & 0x03
        dn = hi & 0x03
        dm = hi & 0x03
        addr = (self.cpu.a[an] + self.cpu.d[di]) & 0xFFFFFFFF

        if hi <= 0x3:  # movbu (Di,Am),Dn
            self.cpu.d[dn] = self.mem.read_byte(addr)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F4 {b1:02X}          movbu (d{di},a{an}), d{dn}   [d{dn}=0x{self.cpu.d[dn]:X}]")
            return 2
        if hi <= 0x7:  # movbu Dm,(Di,An)
            self.mem.write_byte(addr, self.cpu.d[dm] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: F4 {b1:02X}          movbu d{dm}, (d{di},a{an})")
            return 2
        if hi <= 0xB:  # movhu (Di,Am),Dn
            dst = hi & 0x03
            self.cpu.d[dst] = self.mem.read_word(addr)
            self.cpu.zf = (self.cpu.d[dst] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F4 {b1:02X}          movhu (d{di},a{an}), d{dst}  [d{dst}=0x{self.cpu.d[dst]:X}]")
            return 2

        src = hi & 0x03  # movhu Dm,(Di,An)
        self.mem.write_word(addr, self.cpu.d[src] & 0xFFFF)
        self.trace_log.append(f"0x{pc:08X}: F4 {b1:02X}          movhu d{src}, (d{di},a{an})")
        return 2

    def extF5(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        self.trace_log.append(f"0x{self.cpu.pc:08X}: F5 {b1:02X}          [F5 EXTENSION]")
        return 2

    def extF6(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        self.trace_log.append(f"0x{self.cpu.pc:08X}: F6 {b1:02X}          [F6 EXTENSION]")
        return 2

    def extF7(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        # F7 is often a 4-byte instruction in AM33 extensions
        self.trace_log.append(f"0x{self.cpu.pc:08X}: F7 {b1:02X} .. ..    [F7 EXTENSION]")
        return 4

    # -------------------------------------------------------------
    # 3-Byte Formats (F8, F9)
    # -------------------------------------------------------------
    def extF8(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        b2 = self.mem.read_byte(pc + 2)

        # General F8 d8 displacement memory forms.  The IDA MN103 module
        # decodes these by high nibble: 0x0..0x7 are register-memory
        # moves with signed 8-bit displacement from An/Am; 0x9 and 0xB
        # cover SP byte/halfword stack slots.
        disp_s8 = b2 if b2 < 0x80 else b2 - 0x100
        hi = (b1 >> 4) & 0x0F
        lo = b1 & 0x0F
        an = lo & 0x03
        dm = (lo >> 2) & 0x03
        dn = dm

        if hi == 0x0:  # mov (d8,Am),Dn
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.cpu.d[dn] = self.mem.read_dword(addr)
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov ({disp_s8},a{an}), d{dn}")
            return 3

        if hi == 0x1:  # mov Dm,(d8,An)
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.mem.write_dword(addr, self.cpu.d[dm] & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov d{dm}, ({disp_s8},a{an})")
            return 3

        if hi == 0x2:  # mov (d8,Am),An
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.cpu.a[dn] = self.mem.read_dword(addr)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov ({disp_s8},a{an}), a{dn}")
            return 3

        if hi == 0x3:  # mov Am,(d8,An)
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.mem.write_dword(addr, self.cpu.a[dm] & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov a{dm}, ({disp_s8},a{an})")
            return 3

        if hi == 0x4:  # movbu (d8,Am),Dn
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.cpu.d[dn] = self.mem.read_byte(addr)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movbu ({disp_s8},a{an}), d{dn}")
            return 3

        if hi == 0x5:  # movbu Dm,(d8,An)
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.mem.write_byte(addr, self.cpu.d[dm] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movbu d{dm}, ({disp_s8},a{an})")
            return 3

        if hi == 0x6:  # movhu (d8,Am),Dn
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.cpu.d[dn] = self.mem.read_word(addr)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movhu ({disp_s8},a{an}), d{dn}")
            return 3

        if hi == 0x7:  # movhu Dm,(d8,An)
            addr = (self.cpu.a[an] + disp_s8) & 0xFFFFFFFF
            self.mem.write_word(addr, self.cpu.d[dm] & 0xFFFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movhu d{dm}, ({disp_s8},a{an})")
            return 3

        if hi == 0x9 and lo in (0x2, 0x6, 0xA, 0xE):
            dn = (lo >> 2) & 0x03
            self.mem.write_byte((self.cpu.sp + b2) & 0xFFFFFFFF, self.cpu.d[dn] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movbu d{dn}, ({b2},sp)")
            return 3

        if hi == 0x9 and lo in (0x3, 0x7, 0xB, 0xF):
            dn = (lo >> 2) & 0x03
            self.mem.write_word((self.cpu.sp + b2) & 0xFFFFFFFF, self.cpu.d[dn] & 0xFFFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movhu d{dn}, ({b2},sp)")
            return 3

        if hi == 0xB and lo in (0x8, 0x9, 0xA, 0xB):
            dn = lo & 0x03
            self.cpu.d[dn] = self.mem.read_byte((self.cpu.sp + b2) & 0xFFFFFFFF)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movbu ({b2},sp), d{dn}")
            return 3

        if hi == 0xB and lo in (0xC, 0xD, 0xE, 0xF):
            dn = lo & 0x03
            self.cpu.d[dn] = self.mem.read_word((self.cpu.sp + b2) & 0xFFFFFFFF)
            self.cpu.zf = (self.cpu.d[dn] == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       movhu ({b2},sp), d{dn}")
            return 3

        if 0xE0 <= b1 <= 0xE3:
            dn = b1 & 3
            self.cpu.d[dn] &= b2
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       and {b2}, d{dn}")
            return 3

        if 0xE4 <= b1 <= 0xE7:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] | b2) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       or {b2}, d{dn}")
            return 3

        if 0xE8 <= b1 <= 0xEB:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] ^ b2) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       xor {b2}, d{dn}")
            return 3

        if 0xEC <= b1 <= 0xEF:
            dn = b1 & 3
            self.cpu.zf = ((self.cpu.d[dn] & b2) == 0)
            self.cpu.nf = False
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       btst {b2}, d{dn}")
            return 3

        if 0xC0 <= b1 <= 0xC3:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] << b2) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       asl {b2}, d{dn}")
            return 3
        if 0xC4 <= b1 <= 0xC7:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] & 0xFFFFFFFF) >> b2
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       lsr {b2}, d{dn}")
            return 3
        if 0xC8 <= b1 <= 0xCB:
            dn = b1 & 3
            v = self.cpu.d[dn] & 0xFFFFFFFF
            if v & 0x80000000:
                v = (v - 0x100000000) >> b2
            else:
                v >>= b2
            self.cpu.d[dn] = v & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       asr {b2}, d{dn}")
            return 3

        if hi == 0xF and lo <= 0x3:  # mov (d8,An),SP
            addr = (self.cpu.a[an] + b2) & 0xFFFFFFFF
            self.cpu.sp = self.mem.read_dword(addr)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov ({b2},a{an}), sp")
            return 3

        if hi == 0xF and 0x4 <= lo <= 0x7:  # mov SP,(d8,An)
            addr = (self.cpu.a[an] + b2) & 0xFFFFFFFF
            self.mem.write_dword(addr, self.cpu.sp & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       mov sp, ({b2},a{an})")
            return 3

        if b1 == 0xFE:  # add imm8,SP
            imm8 = b2 if b2 < 0x80 else b2 - 0x100
            self.cpu.sp = (self.cpu.sp + imm8) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: F8 FE {b2:02X}       add {imm8}, sp")
            return 3

        self.trace_log.append(f"0x{pc:08X}: F8 {b1:02X} {b2:02X}       [F8 EXTENSION]")
        return 3

    def extF9(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        b2 = self.mem.read_byte(self.cpu.pc + 2)
        self.trace_log.append(f"0x{self.cpu.pc:08X}: F9 {b1:02X} {b2:02X}       [F9 EXTENSION]")
        return 3

    # -------------------------------------------------------------
    # 4-Byte Formats (FA, FB)
    # -------------------------------------------------------------
    def extFA(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        b2 = self.mem.read_byte(pc + 2)
        b3 = self.mem.read_byte(pc + 3)
        imm16 = b2 | (b3 << 8)
        if b1 == 0xFC:
            self._pack_psw()
            self.cpu.psw &= imm16
            self.trace_log.append(f"0x{pc:08X}: FA FC {b2:02X} {b3:02X}    and 0x{imm16:04X}, psw")
            return 4

        if b1 == 0xFD:
            self._pack_psw()
            self.cpu.psw |= imm16
            self.trace_log.append(f"0x{pc:08X}: FA FD {b2:02X} {b3:02X}    or 0x{imm16:04X}, psw")
            return 4

        if (b1 >> 4) == 0x8 and (b1 & 0x03) == 0x0:  # mov An,(abs16)
            an = (b1 >> 2) & 0x03
            self.mem.write_dword(imm16, self.cpu.a[an] & 0xFFFFFFFF)
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    mov a{an}, (0x{imm16:04X})")
            return 4

        if (b1 >> 4) == 0xA and ((b1 >> 2) & 0x03) == 0x0:  # mov (abs16),An
            an = b1 & 0x03
            self.cpu.a[an] = self.mem.read_dword(imm16)
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    mov (0x{imm16:04X}), a{an}    [a{an}=0x{self.cpu.a[an]:X}]")
            return 4

        # FA C0..CF: add/sub/cmp/mov imm16,Dn.
        if 0xC0 <= b1 <= 0xCF:
            group = (b1 >> 2) & 0x03
            dn = b1 & 3
            simm16 = self._s16(imm16)
            if group == 0:
                self.cpu.d[dn] = self._add_u32(self.cpu.d[dn], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    add {simm16}, d{dn}")
            elif group == 1:
                self.cpu.d[dn] = self._sub_u32(self.cpu.d[dn], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    sub {simm16}, d{dn}")
            elif group == 2:
                self._cmp_u32(self.cpu.d[dn], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    cmp {imm16}, d{dn}")
            else:
                self.cpu.d[dn] = imm16
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    mov 0x{imm16:04X}, d{dn}")
            return 4

        # FA D0..DF: add/sub/cmp/mov imm16,An.
        if 0xD0 <= b1 <= 0xDF:
            group = (b1 >> 2) & 0x03
            an = b1 & 3
            simm16 = self._s16(imm16)
            if group == 0:
                self.cpu.a[an] = self._add_u32(self.cpu.a[an], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    add {simm16}, a{an}")
            elif group == 1:
                self.cpu.a[an] = self._sub_u32(self.cpu.a[an], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    sub {simm16}, a{an}")
            elif group == 2:
                self._cmp_u32(self.cpu.a[an], imm16)
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    cmp {imm16}, a{an}")
            else:
                self.cpu.a[an] = imm16
                self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    mov 0x{imm16:04X}, a{an}")
            return 4

        if 0xE0 <= b1 <= 0xE3:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] & imm16) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    and 0x{imm16:04X}, d{dn}")
            return 4

        if 0xE4 <= b1 <= 0xE7:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] | imm16) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    or 0x{imm16:04X}, d{dn}")
            return 4

        if 0xE8 <= b1 <= 0xEB:
            dn = b1 & 3
            self.cpu.d[dn] = (self.cpu.d[dn] ^ imm16) & 0xFFFFFFFF
            self._set_nz_flags(self.cpu.d[dn])
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    xor 0x{imm16:04X}, d{dn}")
            return 4

        # FA EC..EF: btst imm16,Dn. This appears in the GDR-8050L HL path at
        # 0x90026682 as "btst 0x8000,d0".  v18 treated it as unknown, leaving
        # stale flags for the following conditional branch.
        if 0xEC <= b1 <= 0xEF:
            dn = b1 & 3
            self.cpu.zf = ((self.cpu.d[dn] & imm16) == 0)
            self.cpu.nf = False
            self.cpu.vf = False
            self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    btst 0x{imm16:04X}, d{dn}")
            return 4

        self.trace_log.append(f"0x{pc:08X}: FA {b1:02X} {b2:02X} {b3:02X}    [FA EXTENSION]")
        return 4

    def extFB(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        b2 = self.mem.read_byte(self.cpu.pc + 2)
        b3 = self.mem.read_byte(self.cpu.pc + 3)
        self.trace_log.append(f"0x{self.cpu.pc:08X}: FB {b1:02X} {b2:02X} {b3:02X}    [FB EXTENSION]")
        return 4

    # -------------------------------------------------------------
    # 5/6-Byte Formats (FC, FD)
    # -------------------------------------------------------------
    def extFC(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        b2 = self.mem.read_byte(pc + 2)
        b3 = self.mem.read_byte(pc + 3)
        b4 = self.mem.read_byte(pc + 4)
        b5 = self.mem.read_byte(pc + 5)
        imm32 = b2 | (b3 << 8) | (b4 << 16) | (b5 << 24)
        simm32 = imm32 - 0x100000000 if (imm32 & 0x80000000) else imm32

        # FC A0..AF: absolute 32-bit loads.  v93 reached a RAM-resident
        # flash/readback loop at 0x800003E2 using FC A8/A9; the IDA MN103
        # module decodes those as MOVBU (abs32),D0/D1.  Implement the whole
        # documented abs32 load group so these loops read real ROM/RAM bytes
        # instead of leaving stale register values.
        if 0xA0 <= b1 <= 0xAF:
            subgrp = (b1 >> 2) & 0x03
            rn = b1 & 0x03
            if subgrp == 0:  # mov (abs32),An
                self.cpu.a[rn] = self.mem.read_dword(imm32)
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov (0x{imm32:08X}), a{rn} [a{rn}=0x{self.cpu.a[rn]:X}]")
            elif subgrp == 1:  # mov (abs32),Dn
                self.cpu.d[rn] = self.mem.read_dword(imm32)
                self._set_nz_flags(self.cpu.d[rn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov (0x{imm32:08X}), d{rn} [d{rn}=0x{self.cpu.d[rn]:X}]")
            elif subgrp == 2:  # movbu (abs32),Dn
                self.cpu.d[rn] = self.mem.read_byte(imm32)
                self._set_nz_flags(self.cpu.d[rn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} movbu (0x{imm32:08X}), d{rn} [d{rn}=0x{self.cpu.d[rn]:X}]")
            else:  # movhu (abs32),Dn
                self.cpu.d[rn] = self.mem.read_word(imm32)
                self._set_nz_flags(self.cpu.d[rn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} movhu (0x{imm32:08X}), d{rn} [d{rn}=0x{self.cpu.d[rn]:X}]")
            return 6

        # FC 80..8F: absolute 32-bit stores.  These pair with the FC A0..AF
        # loads and are useful for flash/RAM helper loops.
        if 0x80 <= b1 <= 0x8F:
            subgrp = b1 & 0x03
            rn = (b1 >> 2) & 0x03
            if subgrp == 0:  # mov An,(abs32)
                self.mem.write_dword(imm32, self.cpu.a[rn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov a{rn}, (0x{imm32:08X})")
            elif subgrp == 1:  # mov Dn,(abs32)
                self.mem.write_dword(imm32, self.cpu.d[rn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov d{rn}, (0x{imm32:08X})")
            elif subgrp == 2:  # movbu Dn,(abs32)
                self.mem.write_byte(imm32, self.cpu.d[rn] & 0xFF)
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} movbu d{rn}, (0x{imm32:08X})")
            else:  # movhu Dn,(abs32)
                self.mem.write_word(imm32, self.cpu.d[rn] & 0xFFFF)
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} movhu d{rn}, (0x{imm32:08X})")
            return 6

        # FC C0..CF: add/sub/cmp/mov imm32,Dn
        if 0xC0 <= b1 <= 0xCF:
            group = (b1 >> 2) & 0x03
            dn = b1 & 0x03
            if group == 0:  # add imm32,Dn
                self.cpu.d[dn] = (self.cpu.d[dn] + imm32) & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} add {simm32}, d{dn}")
            elif group == 1:  # sub imm32,Dn
                self.cpu.d[dn] = (self.cpu.d[dn] - imm32) & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} sub {simm32}, d{dn}")
            elif group == 2:  # cmp imm32,Dn
                self._cmp_u32(self.cpu.d[dn], imm32)
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} cmp 0x{imm32:08X}, d{dn}")
            else:  # mov imm32,Dn
                self.cpu.d[dn] = imm32
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov 0x{imm32:08X}, d{dn}")
            return 6

        # FC D0..DF: add/sub/cmp/mov imm32,An
        if 0xD0 <= b1 <= 0xDF:
            group = (b1 >> 2) & 0x03
            an = b1 & 0x03
            if group == 0:  # add imm32,An
                self.cpu.a[an] = (self.cpu.a[an] + imm32) & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} add {simm32}, a{an}")
            elif group == 1:  # sub imm32,An
                self.cpu.a[an] = (self.cpu.a[an] - imm32) & 0xFFFFFFFF
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} sub {simm32}, a{an}")
            elif group == 2:  # cmp imm32,An
                self._cmp_u32(self.cpu.a[an], imm32)
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} cmp 0x{imm32:08X}, a{an}")
            else:  # mov imm32,An
                self.cpu.a[an] = imm32
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} mov 0x{imm32:08X}, a{an}")
            return 6

        # FC E0..EF: and/or/xor/btst imm32,Dn
        if 0xE0 <= b1 <= 0xEF:
            group = (b1 >> 2) & 0x03
            dn = b1 & 0x03
            if group == 0:  # and imm32,Dn
                self.cpu.d[dn] = (self.cpu.d[dn] & imm32) & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} and 0x{imm32:08X}, d{dn}")
            elif group == 1:  # or imm32,Dn
                self.cpu.d[dn] = (self.cpu.d[dn] | imm32) & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} or 0x{imm32:08X}, d{dn}")
            elif group == 2:  # xor imm32,Dn
                self.cpu.d[dn] = (self.cpu.d[dn] ^ imm32) & 0xFFFFFFFF
                self._set_nz_flags(self.cpu.d[dn])
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} xor 0x{imm32:08X}, d{dn}")
            else:  # btst imm32,Dn
                self.cpu.zf = ((self.cpu.d[dn] & imm32) == 0)
                self.cpu.nf = False
                self.cpu.vf = False
                self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} btst 0x{imm32:08X}, d{dn}")
            return 6

        if b1 == 0x82:
            self.mem.write_byte(imm32, self.cpu.d[0] & 0xFF)
            self.trace_log.append(f"0x{pc:08X}: FC 82 {b2:02X} {b3:02X} {b4:02X} {b5:02X} movbu d0, (0x{imm32:08X})")
            return 6

        if b1 == 0xFE:
            self.cpu.sp = (self.cpu.sp + simm32) & 0xFFFFFFFF
            self.trace_log.append(f"0x{pc:08X}: FC FE {b2:02X} {b3:02X} {b4:02X} {b5:02X} add {simm32}, sp")
            return 6

        if b1 == 0xFF:
            # CALLS (d32,PC): store return at current SP, update MDR, jump.
            target = (pc + simm32) & 0xFFFFFFFF
            return_pc = (pc + 6) & 0xFFFFFFFF
            sp = self.cpu.sp & 0xFFFFFFFF
            text = f"0x{pc:08X}: FC FF {b2:02X} {b3:02X} {b4:02X} {b5:02X} calls -> 0x{target:08X}"
            if self._maybe_skip_call(target, return_pc, text):
                self.mem.write_dword(sp, return_pc)
                self.cpu.mdr = return_pc
                return 6
            self.mem.write_dword(sp, return_pc)
            self.cpu.mdr = return_pc
            self.cpu.pc = target
            self.trace_log.append(text + f" [ret=0x{return_pc:08X}, sp=0x{sp:08X}, mdr=0x{return_pc:08X}]")
            return 0

        self.trace_log.append(f"0x{pc:08X}: FC {b1:02X} {b2:02X} {b3:02X} {b4:02X} {b5:02X} [FC EXTENSION]")
        return 6

    def extFD(self, b0):
        b1 = self.mem.read_byte(self.cpu.pc + 1)
        self.trace_log.append(f"0x{self.cpu.pc:08X}: FD {b1:02X} .. .. .. [FD EXTENSION]")
        return 6

    # -------------------------------------------------------------
    # 7-Byte Formats (FE)
    # -------------------------------------------------------------
    def extFE(self, b0):
        pc = self.cpu.pc
        b1 = self.mem.read_byte(pc + 1)
        op = b1 & 0x0F

        def do_bitop(addr, imm, size):
            mask = self._bit_mask8(imm)
            old = self.mem.read_byte(addr)
            self.cpu.zf = ((old & mask) == 0)
            self.cpu.nf = False
            self.cpu.vf = False
            if op == 0x0:
                self.mem.write_byte(addr, old | mask)
                mnem = "bset/btst"
            elif op == 0x1:
                self.mem.write_byte(addr, old & (~mask & 0xFF))
                mnem = "bclr/btst"
            else:
                mnem = "btst"
            self.trace_log.append(f"0x{pc:08X}: FE {b1:02X}          {mnem} 0x{imm & 0xFF:02X},(0x{addr:08X})")
            return size

        # FE 00/01/02: bset/bclr/btst imm8,(abs32)
        if (b1 >> 4) == 0x0 and op in (0x0, 0x1, 0x2):
            abs32 = self.mem.read_dword(pc + 2)
            imm = self.mem.read_byte(pc + 6)
            return do_bitop(abs32, imm, 7)

        # FE 80/81/82: bset/bclr/btst imm8,(abs16)
        if (b1 >> 4) == 0x8 and op in (0x0, 0x1, 0x2):
            abs16 = self.mem.read_word(pc + 2)
            imm = self.mem.read_byte(pc + 4)
            return do_bitop(abs16, imm, 5)

        self.trace_log.append(f"0x{pc:08X}: FE {b1:02X} .. .. .. [FE EXTENSION/INVALID]")
        return 5

    # -------------------------------------------------------------
    # Break / Trap (FF)
    # -------------------------------------------------------------
    def extFF(self, b0):
        self.trace_log.append(f"0x{self.cpu.pc:08X}: FF          break")
        self.running = False
        return 1

    def run(self, max_cycles=500000):
        self.running = True
        cycles = 0
        print(f"[*] Max cycles ({max_cycles}).")
        print(f"[*] Emulation started at PC: 0x{self.cpu.pc:08X}")
        sys.stdout.flush()
        try:
            while self.running and cycles < max_cycles:
                self.cycles = cycles
                self.step()
                cycles += 1

            if self.running and cycles >= max_cycles:
                print(f"[*] Max cycles ({max_cycles}) reached.")
            else:
                print(f"[*] Emulation stopped after {cycles} cycles.")
            self.cpu.dump()
        except NotImplementedError as e:
            print(f"\n[!] EMULATOR HALTED: {e}")
            print(f"[*] Cycles executed: {cycles}")
            self.cpu.dump()
        except Exception as e:
            print(f"\n[!] CRITICAL FAULT: {e}")
            self.cpu.dump()

        if self.trace_recorder:
            if getattr(self.trace_recorder, "event_counts", None):
                summary = ", ".join(f"{k}={v}" for k, v in sorted(self.trace_recorder.event_counts.items()))
                print(f"[*] Trace event summary: {summary}")
            self.trace_recorder.close()
            
        if TRACE_LEN > 0 and self.trace_log:
            print(f"\n--- TRACE LOG (Last {TRACE_LEN}) ---")
            for trace in self.trace_log[-TRACE_LEN:]:
                print(trace)

if __name__ == "__main__":
    print("="*80)
    print(f"                 MN103S Emulator ({VERSION})")
    print("="*80)
    workspace = r"D:\vscode-workspace\xbox-dvd-tools-v2"
    scripts_dir = os.path.join(workspace, "scripts")
    test_file = os.path.join(scripts_dir, "tools", "mn103s_emu", "test_dl_servo_GCC4241N.json")
    if len(sys.argv) > 1:
        test_file = sys.argv[1]
        
    if not os.path.exists(test_file):
        print(f"[-] FATAL: Test configuration '{test_file}' not found.")
        sys.exit(1)
        
    with open(test_file, 'r') as f:
        test_config = json.load(f)
        
    print(f"[*] Loading Test: {test_config.get('test_name', 'Unknown Test')}")
    print(f"[*] Active config: {os.path.basename(os.path.abspath(test_file))}")
    expected_config_version = str(test_config.get("config_version", test_config.get("requires_emulator_version", ""))).strip()
    emu_tok = version_token(VERSION)
    cfg_tok = version_token(expected_config_version)
    if cfg_tok and emu_tok and cfg_tok != emu_tok:
        if not bool(test_config.get("allow_version_mismatch", False)):
            print(f"[-] FATAL: Config version {expected_config_version!r} does not match emulator {VERSION!r}.")
            print("[-] Extract the matching package ZIP or set allow_version_mismatch=true only for manual experiments.")
            sys.exit(2)
    elif not cfg_tok:
        print("[!] WARNING: Test config has no config_version/requires_emulator_version marker; stale-config checks are limited.")
    else:
        print(f"[*] Config version check OK: {cfg_tok}")
    workspace = os.environ.get("XDVD_WORKSPACE_OVERRIDE") or test_config.get("workspace", os.environ.get("XDVD_WORKSPACE", workspace))
    scripts_dir = test_config.get("scripts_dir", os.path.join(workspace, "scripts"))

    # v72: direct-config-first target description.
    #
    # Older configs may still point at scripts/shared/fleet_profiles.json, but
    # the emulator must not depend on generated Stage6/Stage6A profiles.  A test
    # JSON with custom_bin/custom_base_addr/entry_point is sufficient.  Optional
    # target_profile data is now only a compatibility fallback for legacy configs.
    default_memory_map = {
        "RESET": "0x90000020",
        "ROM_BASE": test_config.get("custom_base_addr", "0x90000000"),
        "SRAM_BASE": "0x00000000",
        "SRAM_ALIAS": "0x80000000",
    }
    target_drive = test_config.get("target_profile", test_config.get("target_name", "direct_config"))
    profile = {
        "config": {
            "base_addr": test_config.get("custom_base_addr", "0x90000000"),
            "bin_rel": test_config.get("custom_bin", ""),
            "plaintext_bin_rel": test_config.get("custom_bin", ""),
        },
        "memory_map": dict(default_memory_map),
    }

    profile_path = test_config.get("fleet_profiles")
    if profile_path:
        profile_path = resolve_workspace_path(workspace, profile_path)
        if not os.path.exists(profile_path):
            print(f"[-] FATAL: Optional hardware database requested but not found: {profile_path}")
            sys.exit(1)
        with open(profile_path, 'r', encoding='utf-8') as f:
            profiles = json.load(f)
        if target_drive not in profiles:
            print(f"[-] FATAL: Drive profile '{target_drive}' not found in requested hardware database.")
            sys.exit(1)
        legacy_profile = profiles[target_drive]
        profile["config"].update(legacy_profile.get("config", {}))
        profile["memory_map"].update(legacy_profile.get("memory_map", {}))
        print(f"[*] Optional legacy target profile loaded: {target_drive}")
    else:
        print(f"[*] Direct target config active: {target_drive}")

    # 3. Parse Addresses and Paths (direct config with optional legacy fallback)
    if "custom_bin" in test_config:
        bin_path = resolve_workspace_path(workspace, test_config["custom_bin"])
        ROM_BASE = int(test_config.get("custom_base_addr", profile["config"].get("base_addr", "0x90000000")), 16)
        print(f"[*] Custom Binary Override Active: {bin_path}")
    else:
        bin_rel = profile["config"].get("plaintext_bin_rel") or profile["config"].get("bin_rel")
        if not bin_rel:
            print("[-] FATAL: Test config must provide custom_bin, or an explicit fleet_profiles file with bin_rel.")
            sys.exit(1)
        ROM_BASE = int(profile["config"].get("base_addr", "0x90000000"), 16)
        bin_path = resolve_workspace_path(workspace, bin_rel)
    header_skip = int(test_config.get("header_skip_bytes", "0x0"), 16)
    
    # Resolve Entry Point (raw hex string or a key from memory_map)
    ep_str = test_config["entry_point"]
    if ep_str in profile["memory_map"]:
        ep_str = profile["memory_map"][ep_str]
        
    if ep_str == "NOT_SUPPORTED":
        print(f"[-] FATAL: Test requires a feature not supported by {target_drive}.")
        sys.exit(1)
        
    GATEWAY_ADDRESS = int(ep_str, 16)

    # Initialize Emulator Engine
    # v62/v63: active GCC E7/HL tests can use one generic semantic descriptor
    # for all observed HL jump-table subcodes.  Expand it into the existing
    # internal command, transfer, and narrow DMA-status blocks so earlier
    # configs remain compatible.
    gcc_hl_present = "gcc_e7_hl" in test_config
    gcc_hl = test_config.get("gcc_e7_hl", {}) or {}
    if gcc_hl_present:
        if not isinstance(gcc_hl, dict):
            print("[-] FATAL: gcc_e7_hl must be an object when present.")
            sys.exit(1)
        cmd_cfg = dict(test_config.get("gcc_e7_hl_command", {}) or {})
        for key in ("base", "opcode", "subcode", "magic", "source_offset", "offset", "transfer_length", "length"):
            if key in gcc_hl and key not in cmd_cfg:
                cmd_cfg[key] = gcc_hl[key]
        test_config["gcc_e7_hl_command"] = cmd_cfg
        test_config["_gcc_e7_hl_command_from_generic"] = True

        transfer_keys = (
            "source_bytes", "source_byte_stream", "completion_bytes", "bytes",
            "completion_words", "words", "source_offsets", "source_binding",
            "source_map_mode", "repeat_last", "status_model", "status_read_pc",
            "status_poll_pc", "completion_read_pc", "status_initial_phase",
            "enabled",
        )
        transfer_cfg = dict(test_config.get("gcc_transfer_model", {}) or {})
        for key in transfer_keys:
            if key in gcc_hl and key not in transfer_cfg:
                transfer_cfg[key] = gcc_hl[key]
        if transfer_cfg:
            transfer_cfg.setdefault("_config_scope", "gcc_e7_hl")
            test_config["gcc_transfer_model"] = transfer_cfg
            test_config["_gcc_transfer_model_from_generic"] = True

        subcode_probe = cmd_cfg.get("subcode", gcc_hl.get("subcode", "0x02"))
        try:
            subcode_int = parse_int(subcode_probe) & 0xFF
        except Exception:
            subcode_int = 0x02
        dma_opt = str(gcc_hl.get("dma_status", gcc_hl.get("dma", "auto"))).strip().lower()
        dma_auto = subcode_int in (0x0D, 0x0F) and dma_opt not in ("0", "false", "no", "off", "disabled")
        if dma_auto and "gcc_dma_status_model" not in test_config:
            test_config["gcc_dma_status_model"] = {"_source": "gcc_e7_hl", "_auto_subcode": f"0x{subcode_int:02X}"}
            test_config["_gcc_dma_status_model_from_generic"] = True
        transfer_note = "generic" if transfer_cfg else "none"
        dma_note = "auto" if dma_auto else "none"
        print(f"[*] GCC E7/HL descriptor active: subcode=0x{subcode_int:02X}; command source=gcc_e7_hl; transfer source={transfer_note}; dma source={dma_note}")

    # v60: active GCC E7/HL subcode-2 tests can use one semantic descriptor
    # instead of separate command-staging and transfer-model blocks.  Expand it
    # into the existing internal blocks so older configs remain compatible.
    gcc_subcode2_present = "gcc_e7_hl_subcode2" in test_config
    gcc_subcode2 = test_config.get("gcc_e7_hl_subcode2", {}) or {}
    if gcc_subcode2_present:
        if not isinstance(gcc_subcode2, dict):
            print("[-] FATAL: gcc_e7_hl_subcode2 must be an object when present.")
            sys.exit(1)
        cmd_cfg = dict(test_config.get("gcc_e7_hl_command", {}) or {})
        for key in ("base", "opcode", "subcode", "magic", "source_offset", "offset", "transfer_length", "length"):
            if key in gcc_subcode2 and key not in cmd_cfg:
                cmd_cfg[key] = gcc_subcode2[key]
        test_config["gcc_e7_hl_command"] = cmd_cfg
        test_config["_gcc_e7_hl_command_from_subcode2"] = True

        transfer_keys = (
            "source_bytes", "source_byte_stream", "completion_bytes", "bytes",
            "completion_words", "words", "source_offsets", "source_binding",
            "source_map_mode", "repeat_last", "status_model", "status_read_pc",
            "status_poll_pc", "completion_read_pc", "status_initial_phase",
            "enabled",
        )
        transfer_cfg = dict(test_config.get("gcc_transfer_model", {}) or {})
        for key in transfer_keys:
            if key in gcc_subcode2 and key not in transfer_cfg:
                transfer_cfg[key] = gcc_subcode2[key]
        if transfer_cfg:
            transfer_cfg.setdefault("_config_scope", "e7_hl_subcode2")
            test_config["gcc_transfer_model"] = transfer_cfg
            test_config["_gcc_transfer_model_from_subcode2"] = True
        transfer_note = "compound" if transfer_cfg else "none"
        print(f"[*] GCC E7/HL subcode-2 descriptor active: command source=compound; transfer source={transfer_note}")

    trace_config = dict(test_config.get("trace", {}))
    # v59: allow the GCC transfer model to live at top-level because it is a
    # semantic hardware model, not trace plumbing.  Keep the older trace-local
    # form as a compatibility fallback for prior configs. v60 may have expanded
    # the top-level block from gcc_e7_hl_subcode2.
    if "gcc_transfer_model" in test_config:
        transfer_cfg = dict(test_config.get("gcc_transfer_model", {}) or {})
        transfer_cfg.setdefault("_config_scope", "top-level")
        trace_config["gcc_transfer_model"] = transfer_cfg
    elif "gcc_transfer_model" in trace_config:
        transfer_cfg = dict(trace_config.get("gcc_transfer_model", {}) or {})
        transfer_cfg.setdefault("_config_scope", "trace")
        trace_config["gcc_transfer_model"] = transfer_cfg
    if "gcc_dma_status_model" in test_config:
        dma_cfg = dict(test_config.get("gcc_dma_status_model", {}) or {})
        dma_cfg["_present"] = True
        trace_config["gcc_dma_status_model"] = dma_cfg
    elif "gcc_dma_status_model" in trace_config:
        dma_cfg = dict(trace_config.get("gcc_dma_status_model", {}) or {})
        dma_cfg["_present"] = True
        trace_config["gcc_dma_status_model"] = dma_cfg

    if "gdr_hardware_model" in test_config:
        trace_config["gdr_hardware_model"] = dict(test_config.get("gdr_hardware_model", {}) or {})
    if "gdr_packet_session_model" in test_config:
        trace_config["gdr_packet_session_model"] = dict(test_config.get("gdr_packet_session_model", {}) or {})
    if "gdr_packet_data_in_model" in test_config:
        trace_config["gdr_packet_data_in_model"] = dict(test_config.get("gdr_packet_data_in_model", {}) or {})
    if "gdr_packet_source_stream_model" in test_config:
        trace_config["gdr_packet_source_stream_model"] = dict(test_config.get("gdr_packet_source_stream_model", {}) or {})
    elif "gdr_packet_source_stream_model" not in trace_config and "packet_data_in" in test_config:
        pdi_cfg = dict(test_config.get("packet_data_in", {}) or {})
        if pdi_cfg.get("source_mode") == "stream":
            trace_config["gdr_packet_source_stream_model"] = {
                "enabled": True,
                "length": pdi_cfg.get("length", 0),
                "full_hex": pdi_cfg.get("full_hex", pdi_cfg.get("first_64_hex", "")),
                "source": "packet_data_in metadata",
                "source_prelude_len": pdi_cfg.get("source_prelude_len", 2),
            }
    if "gdr_packet_data_in_model" not in trace_config and "packet_data_in" in test_config:
        pdi_cfg = dict(test_config.get("packet_data_in", {}) or {})
        if pdi_cfg:
            trace_config["gdr_packet_data_in_model"] = {
                "enabled": True,
                "length": pdi_cfg.get("length", 0),
                "first_64_hex": pdi_cfg.get("first_64_hex", ""),
                "source": "packet_data_in metadata",
            }
    trace_config.setdefault("base_dir", os.path.dirname(os.path.abspath(test_file)) or os.getcwd())
    call_policy_preview = trace_config.get("call_policy", {}) or {}
    if call_policy_preview.get("skip_targets"):
        print("[*] Call policy skip targets:")
        for t in call_policy_preview.get("skip_targets", []):
            print(f"    {t}")
    if trace_config.get("transaction_ledger", {}).get("enabled", False):
        print("[*] Transaction ledger enabled.")
    if trace_config.get("pointer_tracking", {}).get("enabled", False):
        print("[*] Pointer tracking enabled.")
    media_cfg = test_config.get("media", {}) or trace_config.get("media", {}) or {}
    if media_cfg:
        print(f"[*] Virtual media: type={media_cfg.get('type')} present={media_cfg.get('present')} tray={media_cfg.get('tray_state')} image={media_cfg.get('iso_path', '')} sectors={media_cfg.get('sectors_2048', 'n/a')}")
    emu = MN103SEmulator(rom_base=ROM_BASE, trace_config=trace_config)

    # Optional memory aliases from JSON, e.g. map 0x80000000 SRAM alias to 0x00000000.
    for alias in test_config.get("memory_aliases", []):
        emu.mem.add_alias(parse_int(alias["from"]), parse_int(alias["to"]), parse_int(alias["size"]), alias.get("name", ""))
        print(f"[*] Memory alias: {alias.get('name', '')} 0x{parse_int(alias['from']):08X}->0x{parse_int(alias['to']):08X} size=0x{parse_int(alias['size']):X}")

    # Attach Hardware Mocks from the TEST CONFIGURATION
    if "mmio_peripherals" in test_config:
        for p_name, p_data in test_config["mmio_peripherals"].items():
            p_base = int(p_data["base_addr"], 16)
            p_size = int(p_data["size"], 16)
            dsp_mock = DSPPeripheral(p_name, p_base, p_size)
            emu.mem.attach_peripheral(dsp_mock)
    
    if not os.path.exists(bin_path):
        print(f"[-] FATAL: Firmware file missing at: {bin_path}")
        sys.exit(1)
        
    print(f"[*] Loading ROM: {bin_path} (Skipping 0x{header_skip:X} bytes)")
    size = emu.mem.load_bin(bin_path, ROM_BASE, skip_bytes=header_skip)
    print(f"[*] Loaded {size} bytes into ROM at 0x{ROM_BASE:08X}")

    # Apply semantic GCC E7/HL command staging before any legacy raw injections.
    # This replaces the active v55 raw command/header/source/length memory_injections
    # with a higher-level command descriptor while preserving the firmware-facing
    # packet layout at 0x085C..0x0867.
    gcc_cmd_present = "gcc_e7_hl_command" in test_config
    gcc_cmd = test_config.get("gcc_e7_hl_command", {}) or {}
    gcc_cmd_implicit_enabled = gcc_cmd_present and "enabled" not in gcc_cmd
    if gcc_cmd_present and bool(gcc_cmd.get("enabled", True)):
        defaulted = []
        if "base" not in gcc_cmd:
            defaulted.append("base")
        if "opcode" not in gcc_cmd:
            defaulted.append("opcode")
        if "subcode" not in gcc_cmd:
            defaulted.append("subcode")
        if "magic" not in gcc_cmd:
            defaulted.append("magic")
        if "source_offset" not in gcc_cmd and "offset" not in gcc_cmd:
            defaulted.append("source_offset")
        if "transfer_length" not in gcc_cmd and "length" not in gcc_cmd:
            defaulted.append("transfer_length")
        base = parse_int(gcc_cmd.get("base", "0x085C")) & 0xFFFFFFFF
        opcode = parse_int(gcc_cmd.get("opcode", "0xE7")) & 0xFF
        subcode = parse_int(gcc_cmd.get("subcode", "0x02")) & 0xFF
        magic = str(gcc_cmd.get("magic", "HL"))
        magic_bytes = [ord(magic[0]) if len(magic) > 0 else 0, ord(magic[1]) if len(magic) > 1 else 0]
        source_offset = parse_int(gcc_cmd.get("source_offset", gcc_cmd.get("offset", "0"))) & 0xFFFF
        transfer_length = parse_int(gcc_cmd.get("transfer_length", gcc_cmd.get("length", "0"))) & 0xFFFF
        packet = [opcode, subcode, magic_bytes[0] & 0xFF, magic_bytes[1] & 0xFF] + [0] * 8
        for i, b in enumerate(packet):
            emu.mem.write_byte(base + i, b)
        emu.mem.write_byte(base + 0x08, (source_offset >> 8) & 0xFF)
        emu.mem.write_byte(base + 0x09, source_offset & 0xFF)
        emu.mem.write_byte(base + 0x0A, (transfer_length >> 8) & 0xFF)
        emu.mem.write_byte(base + 0x0B, transfer_length & 0xFF)
        defaults_note = ",".join(defaulted) if defaulted else "none"
        enable_note = "implicit" if gcc_cmd_implicit_enabled else "explicit"
        if test_config.get("_gcc_e7_hl_command_from_generic"):
            command_source = "gcc_e7_hl"
        elif test_config.get("_gcc_e7_hl_command_from_subcode2"):
            command_source = "e7_hl_subcode2"
        else:
            command_source = "gcc_e7_hl_command"
        print(f"[*] GCC E7/HL command staged: opcode=0x{opcode:02X} subcode=0x{subcode:02X} magic={chr(magic_bytes[0] & 0xFF)}{chr(magic_bytes[1] & 0xFF)} source_offset=0x{source_offset:04X} transfer_length=0x{transfer_length:04X} base=0x{base:08X} defaults={defaults_note}; command enable={enable_note}; command source={command_source}")
    
    # Apply Memory Injections from Test Config
    if "memory_injections" in test_config:
        for inj in test_config["memory_injections"]:
            addr_str = str(inj["address"])
            if addr_str in profile["memory_map"]:
                addr = int(profile["memory_map"][addr_str], 16)
            else:
                addr = int(addr_str, 16)
                
            data = inj["data"]
            print(f"[*] Injecting: {inj.get('description', 'Data')} at 0x{addr:08X}")
            for i, b in enumerate(data):
                emu.mem.write_byte(addr + i, b)

    # Apply Initial Registers
    if "initial_registers" in test_config:
        for reg, val_str in test_config["initial_registers"].items():
            val_str = str(val_str)
            if val_str in profile["memory_map"]:
                resolved_val = int(profile["memory_map"][val_str], 16)
            else:
                resolved_val = int(val_str, 16)
                
            reg = reg.upper()
            if reg == "A0": emu.cpu.a[0] = resolved_val
            elif reg == "A1": emu.cpu.a[1] = resolved_val
            elif reg == "A2": emu.cpu.a[2] = resolved_val
            elif reg == "A3": emu.cpu.a[3] = resolved_val
            elif reg == "D0": emu.cpu.d[0] = resolved_val
            elif reg == "D1": emu.cpu.d[1] = resolved_val
            elif reg == "D2": emu.cpu.d[2] = resolved_val
            elif reg == "D3": emu.cpu.d[3] = resolved_val
            elif reg == "SP": emu.cpu.sp = resolved_val
            print(f"[*] Set {reg} to 0x{resolved_val:08X}")

    # Aim the CPU directly at the target
    emu.cpu.pc = GATEWAY_ADDRESS
    
    # Run the Engine
    MAX_CYCLES = parse_int(test_config.get("max_cycles", 5000), 5000)
    emu.run(MAX_CYCLES)

    print(f"[*] Payload Inspection (0x01C8): {hex(emu.mem.read_dword(0x01C8))}")