diff --git a/emulator/firmware-decomp/emu860.py b/emulator/firmware-decomp/emu860.py index fcc5869..d9b2b96 100644 --- a/emulator/firmware-decomp/emu860.py +++ b/emulator/firmware-decomp/emu860.py @@ -1,708 +1,734 @@ -"""Intel i860 interpreter -- Tier 0 of the VelociRender emulator. - -Goal: execute the real firmware (VREND.MNG) so the board's own code produces the -render, rather than us reinterpreting the wire. Reuses the validated decoder -(dis860) for tracing; execution semantics are implemented here. - -Status: FOUNDATION. Core integer / load-store / branch / basic FP, delay slots, -sparse memory with MMIO traps, VREND.MNG loader. Run it to see how far the -firmware gets and which peripherals/instructions it needs next. - - python emu860.py [--trace N] [--steps N] -""" -import sys, os, struct -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -import dis860 - -PAGE = 1 << 16 -MASK32 = 0xFFFFFFFF -_INDEXED = os.environ.get('EMU_INDEXED', '0') == '1' # experiment: register-indexed even loads - -def s16(v): return v - 0x10000 if v & 0x8000 else v -def s26(v): return v - 0x4000000 if v & 0x2000000 else v -def u32(v): return v & MASK32 -def i32(v): - v &= MASK32 - return v - (1 << 32) if v & 0x80000000 else v - - -class Mem: - """Sparse page memory. RAM pages autocreate on write; reads of unmapped RAM - return 0 (logged once). MMIO ranges dispatch to callbacks.""" - def __init__(self, log): - self.pages = {} - self.mmio = [] # (lo, hi, read_cb, write_cb, name) - self.log = log - self._warned = set() - - def map_mmio(self, lo, hi, rd, wr, name): - self.mmio.append((lo, hi, rd, wr, name)) - - def _page(self, addr, create): - pn = addr >> 16 - p = self.pages.get(pn) - if p is None and create: - p = self.pages[pn] = bytearray(PAGE) - return p - - def load_blob(self, addr, data): - for i, b in enumerate(data): - p = self._page(addr + i, True) - p[(addr + i) & 0xFFFF] = b - - def _mmio(self, addr): - for lo, hi, rd, wr, name in self.mmio: - if lo <= addr < hi: - return (rd, wr, name) - return None - - def r32(self, addr): - addr = u32(addr) - m = self._mmio(addr) - if m: return u32(m[0](addr)) - off = addr & 0xFFFF - if off <= 0xFFFC: - p = self._page(addr, False) - if p is None: - if (addr >> 16) not in self._warned: - self._warned.add(addr >> 16) - self.log(f" [mem] read unmapped {addr:#010x} -> 0") - return 0 - return struct.unpack_from('> (8 * i)) & 0xFF) - - def r16(self, addr): - addr = u32(addr) - if (addr & 0xFFFF) == 0xFFFF: - return self.r8(addr) | (self.r8(addr + 1) << 8) - p = self._page(addr, False) - if p is None: return 0 - return struct.unpack_from('> 8) & 0xFF) - else: - struct.pack_into('= 2: self.f[i] = u32(v) - - # single-precision float helpers - @staticmethod - def f2b(x): - try: - return struct.unpack(' +/-inf - return 0xff800000 if x < 0 else 0x7f800000 - @staticmethod - def b2f(b): return struct.unpack('> 2) & 1 - - # ------------- one instruction ------------- - def step(self): - pc = self.pc - if pc in self.stopat: - self.stop = True; self.stopmsg = f"stopat {pc:#010x}"; return False - w = self.mem.r32(pc) - if self.tailn: - self.tail.append((pc, w)) - if len(self.tail) > self.tailn: self.tail.pop(0) - if self.trace and self.steps < self.trace: - m, ops = dis860.decode(w, pc) - self.log(f"{pc:#010x}: {w:08x} {m:<10} {ops}") - self._branch = None - self.execute(w, pc) - self.steps += 1 - if self.stop: - return False - if self._branch is not None: - target, delayed = self._branch - if delayed: - # execute one delay-slot instruction, then jump - ds = pc + 4 - w2 = self.mem.r32(ds) - if self.trace and self.steps < self.trace: - m, ops = dis860.decode(w2, ds) - self.log(f"{ds:#010x}: {w2:08x} {m:<10} {ops} ; [delay slot]") - self._branch = None - self.execute(w2, ds) - self.steps += 1 - self.pc = self._branch[0] if self._branch else target - else: - self.pc = target - else: - self.pc = pc + 4 - return not self.stop - - def branch(self, target, delayed=True): - self._branch = (u32(target), delayed) - - def execute(self, w, pc): - op = (w >> 26) & 0x3f - src2 = (w >> 21) & 0x1f - dest = (w >> 16) & 0x1f - src1 = (w >> 11) & 0x1f - imm = w & 0xffff - - # ---- integer loads: ld.b (0x00/0x01), ld.s/ld.l (0x04/0x05) ---- - # dest = bits20:16. ODD op = immediate s16 offset; EVEN op = register-indexed - # EA = src2 + src1 (ground truth: DNC.O `ld.b -1(fp),r19`=0473ffff, - # VR_REMOT.S `ld.l r30(r31),r31`, DNC.S `ld.l r0(r10),r16`). - # For op 4/5 the instr bit0 selects size: 0 = .s (16-bit), 1 = .l (32-bit) - # (DNC.O `ld.l 52(fp),r23` = 14770035, imm = 52|1). ld.b/ld.s sign-extend - # (compiled code masks with `and 0xff` right after ld.b). - if op in (0x00, 0x01, 0x04, 0x05): - if op & 1: - m = 0xffff if op == 0x01 else (0xfffc if (w & 1) else 0xfffe) - ea = self.rd(src2) + s16(imm & m) - else: - ea = self.rd(src2) + self.rd(src1) - if op < 0x04: - v = self.mem.r8(ea) - if v & 0x80: v -= 0x100 - elif w & 1: # ld.l - v = self.mem.r32(ea) - else: # ld.s - v = self.mem.r16(ea) - if v & 0x8000: v -= 0x10000 - self.wr(dest, v); return - # ---- integer stores: st.b (0x03), st.s/st.l (0x07) ---- - # source = src1 (bits15:11); the 16-bit offset is SPLIT high=bits20:16, - # low=bits10:0 (DNC.O `st.b r19,-1(fp)` = 0c7f9fff). Offset bit0 selects - # st.s (0) vs st.l (1), same flag as loads. - if op in (0x03, 0x07): - off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff) - if op == 0x03: - self.mem.w8(self.rd(src2) + s16(off), self.rd(src1) & 0xff) - elif off & 1: # st.l - self.mem.w32(self.rd(src2) + s16(off & 0xfffc), self.rd(src1)) - else: # st.s - self.mem.w16(self.rd(src2) + s16(off & 0xfffe), self.rd(src1) & 0xffff) - return - # ---- FP loads/stores: fld (0x08/0x09), fst (0x0a/0x0b) ---- - # FP reg = dest field for BOTH (load target / store source; fst does NOT use - # the split-store encoding -- `fst.l f20,0x3c88(r31)` = 2ff43c8a, imm=0x3c88|2). - # ODD op = flat s16 immediate offset; EVEN op = register-indexed (src2+src1). - # Flag bits (low bits of the offset field): bit0 = auto-increment (base <- EA), - # bit1: 1 = .l (32-bit), 0 = .d (64-bit); bit2 with bit1=0 = .q (128-bit). - # Derived from AS860 .S<->.O pairs (OPTFLOAT/TRISTRIP/ZBUF32, n>=28 each). - if op in (0x08, 0x09, 0x0a, 0x0b): - fl = w & 7 - size = 4 if (fl & 2) else (16 if (fl & 4) else 8) - if op & 1: - off = s16(imm & (0x10000 - size)) - else: - off = self.rd(src1) - ea = self.rd(src2) + off - if fl & 1: # auto-increment - self.wr(src2, ea) - b0 = dest & ~((size // 4) - 1) - if op < 0x0a: # fld - for i in range(size // 4): - self.fwr(b0 + i, self.mem.r32(ea + i * 4)) - else: # fst - for i in range(size // 4): - self.mem.w32(ea + i * 4, self.frd(b0 + i)) - return - if op in (0x18, 0x19): # pfld.y (pipelined load: 3-stage load pipe) - fl = w & 7 - size = 4 if (fl & 2) else 8 - if op & 1: - off = s16(imm & (0x10000 - size)) - else: - off = self.rd(src1) - ea = self.rd(src2) + off - if fl & 1: # auto-increment - self.wr(src2, ea) - self._fp_pipes() - entry = (self.mem.r32(ea), self.mem.r32(ea + 4) if size == 8 else None) - out = self._lpipe[2] - self._lpipe[2] = self._lpipe[1]; self._lpipe[1] = self._lpipe[0] - self._lpipe[0] = entry - lo, hi = out - if hi is None: - self.fwr(dest, lo) - else: - b0 = dest & ~1 - self.fwr(b0, lo); self.fwr(b0 | 1, hi) - return - if op == 0x0d: # flush #const(src2)[++] (cache-line flush) - # Store-format split offset; cache line = 32B so low 5 bits are free: - # bit0 = auto-increment (src2 <- src2+offset). No memory effect for us. - off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff) - if off & 1: - self.wr(src2, self.rd(src2) + s16(off & 0xffe0)) - return - if op == 0x0c: # ld.c ctrl,dest - self.wr(dest, self.cr.get(src2, 0)); return - if op == 0x0e: # st.c src1,ctrl - self.cr[src2] = self.rd(src1); return - if op == 0x02: # ixfr src1 -> fdest (int->FP reg move) - self.fwr(dest, self.rd(src1)); return - - # ---- control transfer ---- - if op == 0x10: # bri src1 (indirect, delayed) - self.branch(self.rd(src1)); return - if op == 0x13: # CORE ESCAPE: sub-op in low bits - sub = w & 0x1f - if sub == 0x02: # calli src1 - self.wr(1, pc + 8); self.branch(self.rd(src1)); return - if sub in (0x01, 0x07): # lock / unlock (bus lock for atomic RMW) - return # single-CPU emulation: no-op - if sub == 0x04: # intovr (trap on overflow) -- ignore - return - self.stop = True - self.stopmsg = f"core-escape sub {sub:#x} @ {pc:#010x} w={w:08x}" - return - if op == 0x1a: # br - self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return - if op == 0x1b: # call - self.wr(1, pc + 8); self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return - if op in (0x1c, 0x1d): # bc / bc.t (branch if CC) - tgt = pc + 4 + s26(w & 0x03ffffff) * 4 - if self.cc(): self.branch(tgt, delayed=(op == 0x1d)) - return - if op in (0x1e, 0x1f): # bnc / bnc.t (branch if !CC) - tgt = pc + 4 + s26(w & 0x03ffffff) * 4 - if not self.cc(): self.branch(tgt, delayed=(op == 0x1f)) - return - if op in (0x14, 0x15, 0x16, 0x17): # btne / bte (compare & branch) - broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff) - a = src1 if (op & 1) else self.rd(src1) # even = 5-bit const - b = self.rd(src2) - take = (a != b) if op < 0x16 else (a == b) - if take: self.branch(pc + 4 + broff * 4, delayed=False) - return - - # ---- arithmetic / logic (odd opcode = 16-bit immediate) ---- - base = op & 0x3e - if base in (0x20, 0x22, 0x24, 0x26): # addu subu adds subs - # i860: dest = SRC1 (op) SRC2, where src1 = imm (immediate form) or - # rd(src1) (register form). Subtraction is src1 - src2 (NOT reversed). - b = self.rd(src2) - a = s16(imm) if (op & 1) else self.rd(src1) - if base == 0x20: # addu: CC = carry out - s = u32(a) + u32(b); self.wr(dest, s); self.set_cc(s > MASK32) - elif base == 0x24: # adds: CC = result negative - s = i32(a) + i32(b); self.wr(dest, s) - self.set_cc(bool(u32(s) & 0x80000000)) - elif base == 0x22: # subu: src1 - src2 - self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b)) - else: # subs: src1 - src2 - self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b)) - return - if op == 0x2d: # bla isrc1,isrc2,sbroff (loop: branch on LCC + add) - # Canonical idiom (DNC.S/compiler output): adds -1,rN,r18; adds -1,r0,r17; - # bla r17,r18,LOOP; ; LOOP: body...; bla r17,r18,LOOP; - # Semantics: taken = old LCC; src2 += src1; delayed branch if taken. - # LCC rule is SIGN-dependent (i860 manual): src1 < 0 -> LCC = (signed - # sum >= 0); src1 >= 0 -> LCC = unsigned carry. The signed rule makes a - # spent countdown (src1=-1, src2=-1) yield LCC=0, so a stray follow-on - # bla falls through instead of spinning (seen at 0xf041ce68). - a = self.rd(src1); b = self.rd(src2) - taken = getattr(self, 'lcc', 0) - if i32(a) < 0: - self.lcc = 1 if i32(u32(i32(a) + i32(b))) >= 0 else 0 - else: - self.lcc = 1 if (u32(a) + u32(b)) > MASK32 else 0 - self.wr(src2, u32(a) + u32(b)) - if taken: - off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & 0xffff) - self.branch(pc + 4 + off * 4) - return - if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra - cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f) - b = self.rd(src2) - if base == 0x28: r = b << cnt # shl - elif base == 0x2a: r = b >> cnt # shr (logical) - elif base == 0x2e: r = i32(b) >> cnt # shra - else: r = b >> cnt # shrd (approx) - self.wr(dest, r); return - if base in (0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e): # and..xorh - a = imm if (op & 1) else self.rd(src1) - hi = base in (0x32, 0x36, 0x3a, 0x3e) - if hi and (op & 1): a = imm << 16 - b = self.rd(src2) - g = base & 0x3c - if g == 0x30: r = b & a # and/andh - elif g == 0x34: r = b & ~a # andnot/andnoth - elif g == 0x38: r = b | a # or/orh - else: r = b ^ a # xor/xorh - self.wr(dest, r) - self.set_cc((r & MASK32) == 0) # i860 logicals set CC = (result == 0) - return - - # ---- FP unit (opcode 0x12) ---- - if op == 0x12: - self.exec_fp(w, src1, src2, dest); return - - # ---- unhandled ---- - m, ops = dis860.decode(w, pc) - self.stop = True - self.stopmsg = f"unimplemented op {op:#04x} ({m} {ops}) @ {pc:#010x} w={w:#010x}" - - # precision-aware FP register access (doubles occupy register PAIRS: - # f[N]=low32, f[N+1]=high32, little-endian) - def rdf(self, reg, dbl): - if dbl: - b = reg & ~1 - return struct.unpack('int bit games come out exact), and the - # retire is ENCODED with the pusher's precision, not the current op's. - def _fp_pipes(self): - if not hasattr(self, '_apipe') or (self._apipe and - not isinstance(self._apipe[0], tuple)): - # fresh init, or normalize legacy float-only snapshots - old_a = getattr(self, '_apipe', [0.0] * 3) - old_m = getattr(self, '_mpipe', [0.0] * 3) - self._apipe = [(float(v), 1) for v in old_a] - self._mpipe = [(float(v), 1) for v in old_m] - self._lpipe = [(0, None)] * 3 # pfld: (lo bits, hi bits | None) - self._gpipe = [0] # graphics: raw 32-bit int - self._kr = getattr(self, '_kr', 0.0) - self._ki = getattr(self, '_ki', 0.0) - self._t = getattr(self, '_t', 0.0) - return self._apipe, self._mpipe - - def _round_rp(self, val, rp): - if rp: - return val - return self.b2f(self.f2b(val)) # round to single - - def _padv(self, pipe, val, rp, depth): - """Push (val,rp) into stage 0; return the (val,rp) leaving stage depth-1.""" - out = pipe[depth - 1] - for i in range(depth - 1, 0, -1): - pipe[i] = pipe[i - 1] - pipe[0] = (self._round_rp(val, rp), rp) - return out - - # PFAM/PFSM dual-op routing, decoded from the validated mnemonic grammar - # (AS860 corpus: r2p1, r2pt, r2ap1, r2apt, i2p1.., rat1p2, m12apm, ra1p2, - # m12tpm, m12tpa, iat1p2, ia1p2 ...): - # M-unit: r2 = KR*src2 | i2 = KI*src2 | m12 = src1*src2 | - # ra = KR*Ares | ia = KI*Ares ('t' suffix: T <- Mres) - # A-unit: p1 = Mres+src1 | pt = Mres+T | ap1 = Ares+src1 | apt = Ares+T | - # apm = Ares+Mres | tpm = T+Mres | tpa = T+Ares | 1p2 = src1+src2 - # K-load: if src1 is unused by the routing, KR (r-forms) / KI (i-forms) - # is loaded from src1. sub 0x10-0x1f = PFSM (A-unit subtracts). - # Encoded per DPC as (m1,m2, a1,a2, kload, tload) with operand tags: - # s1 s2 kr ki t am (adder-pipe result) mm (mul-pipe result) - # Dual-operation DPC routing, verbatim from the i860 spec (validated against - # MAME's i860 core src_opers[] table): (m1, m2, a1, a2, kload, tload). - # 'PP' = the FLAGM operand: A-pipe last stage for the PFAM family (P=1), - # M-pipe last stage for the PFMAM family (P=0). - _DUAL = { - 0x0: ('kr','s2', 's1','mm', False, False), # r2p1 - 0x1: ('kr','s2', 't', 'mm', True, False), # r2pt - 0x2: ('kr','s2', 's1','PP', False, True), # r2ap1 - 0x3: ('kr','s2', 't', 'PP', True, True), # r2apt - 0x4: ('ki','s2', 's1','mm', False, False), # i2p1 - 0x5: ('ki','s2', 't', 'mm', True, False), # i2pt - 0x6: ('ki','s2', 's1','PP', False, True), # i2ap1 - 0x7: ('ki','s2', 't', 'PP', True, True), # i2apt - 0x8: ('kr','PP', 's1','s2', False, True), # rat1p2 - 0x9: ('s1','s2', 'PP','mm', False, False), # m12apm - 0xa: ('kr','PP', 's1','s2', False, False), # ra1p2 - 0xb: ('s1','s2', 't', 'PP', False, True), # m12ttpa - 0xc: ('ki','PP', 's1','s2', False, True), # iat1p2 - 0xd: ('s1','s2', 't', 'mm', False, False), # m12tpm - 0xe: ('ki','PP', 's1','s2', False, False), # ia1p2 - 0xf: ('s1','s2', 't', 'PP', False, False), # m12tpa - } - - def exec_fp(self, w, src1, src2, dest): - sub = w & 0x7f - # MAME-validated bit assignments: SOURCE precision = bit8, RESULT = bit7. - sp = 1 if (w & 0x100) else 0 - rp = 1 if (w & 0x080) else 0 - pbit = (w >> 10) & 1 # pipelined (scalar ops) / PFAM-vs-PFMAM (dual ops) - if sub < 0x20: # dual ops: PF[M]AM (bit4=0) / PF[M]SM (bit4=1) - # sp = MULTIPLIER source precision; rp = ADDER source AND all results. - ap, mp = self._fp_pipes() - mdepth = 2 if sp else 3 - am_out, am_rp = ap[2] - mm_out, mm_rp = mp[mdepth - 1] - pipe_val = am_out if pbit else mm_out # FLAGM operand + fdest bypass - m1, m2, a1, a2, kload, tload = self._DUAL[sub & 0xf] - def val(tag, prec): - if tag == 's1': return self.rdf(src1, prec) - if tag == 's2': return self.rdf(src2, prec) - if tag == 'kr': return self._kr - if tag == 'ki': return self._ki - if tag == 't': return self._t - if tag == 'mm': return mm_out - return pipe_val # 'PP' - v1 = val(m1, sp); v2 = val(m2, sp) - if m2 == 's2' and dest and src2 == dest: # fdest bypass (mul op2 only) - v2 = pipe_val - newm = v1 * v2 - u1 = val(a1, rp); u2 = val(a2, rp) - if a1 == 's1' and dest and src1 == dest: u1 = pipe_val - if a2 == 's2' and dest and src2 == dest: u2 = pipe_val - newa = (u1 - u2) if (sub & 0x10) else (u1 + u2) - if tload: self._t = mm_out # T <- M-pipe last stage - if kload: # K <- fsrc1 (mul precision) - if m1 == 'ki': self._ki = self.rdf(src1, sp) - else: self._kr = self.rdf(src1, sp) - if pbit: self.wrf(dest, am_out, am_rp) # PFAM: fdest <- A retire - else: self.wrf(dest, mm_out, mm_rp) # PFMAM: fdest <- M retire - self._padv(ap, newa, rp, 3) - self._padv(mp, newm, rp, mdepth) - return - if sub in (0x20, 0x24): # fmul / pfmul (0x24 = pfmul3: 3-stage) - v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) - if pbit: - ap, mp = self._fp_pipes() - depth = 3 if sub == 0x24 else (2 if sp else 3) - out, orp = mp[depth - 1] - if dest and src1 == dest: v1 = out # fdest bypass - if dest and src2 == dest: v2 = out - self.wrf(dest, out, orp) - self._padv(mp, v1 * v2, rp, depth) - else: - self.wrf(dest, v1 * v2, rp) - elif sub in (0x30, 0x31, 0x33): # fadd/fsub/famov (+ pipelined forms) - v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) - if pbit: - ap, mp = self._fp_pipes() - out, orp = ap[2] - if dest and src1 == dest: v1 = out # fdest bypass - if dest and src2 == dest: v2 = out - if sub == 0x33: r = v1 - else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) - self.wrf(dest, out, orp) - self._padv(ap, r, rp, 3) - else: - if sub == 0x33: r = v1 - else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) - self.wrf(dest, r, rp) - elif sub == 0x22: # frcp (reciprocal of src2) - b = self.rdf(src2, sp) - self.wrf(dest, (1.0 / b) if b else 0.0, rp) - elif sub == 0x23: # frsqr (recip sqrt of src2) - import math - b = self.rdf(src2, sp) - self.wrf(dest, (1.0 / math.sqrt(b)) if b > 0 else 0.0, rp) - elif sub in (0x32, 0x3a): # fix (round) / ftrunc -> int in FP reg - a = self.rdf(src1, sp) - if a != a or a in (float('inf'), float('-inf')): - iv = 0x80000000 # IEEE indefinite - elif abs(a) > 0x7fffffff: - iv = 0x80000000 - else: - iv = int(a) if sub == 0x3a else int(a + (0.5 if a >= 0 else -0.5)) - self.fwr(dest & ~1 if rp else dest, iv & 0xFFFFFFFF) - elif sub == 0x34: # fgt: CC = src1 > src2 - self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp)) - elif sub == 0x35: # feq: CC = src1 == src2 - self.set_cc(self.rdf(src1, sp) == self.rdf(src2, sp)) - elif sub == 0x21: # fmlow.dd -- i860 FP-unit INTEGER multiply. - # The i860 has no imul: ints are ixfr'd into FP regs, fmlow.dd multiplies, - # and the low 32 bits (fdest) are fxfr/fst'd back. Operands are the low - # words (32-bit); result is the 64-bit product across the fdest pair. - a = self.frd(src1); b = self.frd(src2) - prod = a * b - b0 = dest & ~1 - self.fwr(b0, prod & 0xFFFFFFFF); self.fwr(b0 | 1, (prod >> 32) & 0xFFFFFFFF) - elif sub == 0x40: # fxfr FP->int - self.wr(dest, self.frd(src1)) - elif sub in (0x49, 0x4d): # fiadd/fisub (graphics-unit int add/sub; - # .ss = 32-bit, .dd = 64-bit across register pairs; P = 1-stage pipe) - if sp: - a = self.frd(src1 & ~1) | (self.frd((src1 & ~1) | 1) << 32) - b = self.frd(src2 & ~1) | (self.frd((src2 & ~1) | 1) << 32) - m = (1 << 64) - 1 - else: - a = self.frd(src1); b = self.frd(src2); m = MASK32 - r = ((a + b) if sub == 0x49 else (a - b)) & m - if pbit: - self._fp_pipes() - out = self._gpipe[0] - self._gpipe[0] = r - r = out - if sp: - b0 = dest & ~1 - self.fwr(b0, r & MASK32); self.fwr(b0 | 1, (r >> 32) & MASK32) - else: - self.fwr(dest, r) - elif sub == 0x5f: # fnop - pass - else: - self.stop = True - self.stopmsg = f"unimplemented FP sub {sub:#04x} @ {self.pc:#010x}" - - # ------------- board control / CCB region (0xFFFFxxxx) ------------- - def map_control(self, cfg=None): - """Model the transputer<->i860 control/config + CCB region. - cfg: {addr: value} overrides for the 0xFFFFF7xx config registers.""" - self.ctrl = {0xfffff720: 2, # must read 2 (IO_ACK) for normal boot - 0xfffff70c: 1} # board-config select (1 = CCB @0xffffe000 path) - if cfg: self.ctrl.update(cfg) - self.ctrl_log = [] - def rd(a): - v = self.ctrl.get(a, 0) - self.ctrl_log.append(('r', a, v)) - return v - def wr(a, v): - self.ctrl[a] = v - self.ctrl_log.append(('w', a, v)) - self.mem.map_mmio(0xfff00000, 0x100000000, rd, wr, 'ctrl/ccb') - - # ------------- board / IGC MMIO (logged) ------------- - def map_board(self): - """Log accesses to the board-register (0x8380_xxxx) and IGC/DMA regions, - and capture the IGC coefficient stream (Tier-1 handoff).""" - self.board_log = [] - self.igc = [] # captured (addr,val) IGC coefficient writes - def brd_rd(a): self.board_log.append(('r', a, 0)); return 0 - def brd_wr(a, v): self.board_log.append(('w', a, v)) - self.mem.map_mmio(0x83000000, 0x84000000, brd_rd, brd_wr, 'board') - - # ------------- call harness (direct handler invocation) ------------- - RET_SENTINEL = 0xbadca110 - def call(self, addr, args=(), sp=0x000c0000, maxsteps=2_000_000): - """Invoke a firmware function directly (PGI conv: args r16.., ret r16, - r1=return, r2=sp). Runs until it returns to the sentinel. Returns r16.""" - self.wr(1, self.RET_SENTINEL) - self.wr(2, sp) - for i, a in enumerate(args): - self.wr(16 + i, a) - self.pc = u32(addr) - self.stop = False; self.stopmsg = '' - n0 = self.steps - while self.steps - n0 < maxsteps: - if self.pc == self.RET_SENTINEL: - return self.rd(16) - if not self.step(): - return None # faulted (stopmsg set) - self.stop = True; self.stopmsg = f"call {addr:#x} ran {maxsteps} steps (no return)" - return None - - # ------------- loader ------------- - def load_mng(self, path, base=0xf0400000): - d = open(path, 'rb').read() - tsize, dsize, bsize = struct.unpack_from(' [--trace N] [--steps N] +""" +import sys, os, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import dis860 + +PAGE = 1 << 16 +MASK32 = 0xFFFFFFFF +_INDEXED = os.environ.get('EMU_INDEXED', '0') == '1' # experiment: register-indexed even loads + +def s16(v): return v - 0x10000 if v & 0x8000 else v +def s26(v): return v - 0x4000000 if v & 0x2000000 else v +def u32(v): return v & MASK32 +def i32(v): + v &= MASK32 + return v - (1 << 32) if v & 0x80000000 else v + + +class Mem: + """Sparse page memory. RAM pages autocreate on write; reads of unmapped RAM + return 0 (logged once). MMIO ranges dispatch to callbacks.""" + def __init__(self, log): + self.pages = {} + self.mmio = [] # (lo, hi, read_cb, write_cb, name) + self.log = log + self._warned = set() + + def map_mmio(self, lo, hi, rd, wr, name): + self.mmio.append((lo, hi, rd, wr, name)) + + def _page(self, addr, create): + pn = addr >> 16 + p = self.pages.get(pn) + if p is None and create: + p = self.pages[pn] = bytearray(PAGE) + return p + + def load_blob(self, addr, data): + for i, b in enumerate(data): + p = self._page(addr + i, True) + p[(addr + i) & 0xFFFF] = b + + def _mmio(self, addr): + for lo, hi, rd, wr, name in self.mmio: + if lo <= addr < hi: + return (rd, wr, name) + return None + + def r32(self, addr): + addr = u32(addr) + m = self._mmio(addr) + if m: return u32(m[0](addr)) + off = addr & 0xFFFF + if off <= 0xFFFC: + p = self._page(addr, False) + if p is None: + if (addr >> 16) not in self._warned: + self._warned.add(addr >> 16) + self.log(f" [mem] read unmapped {addr:#010x} -> 0") + return 0 + return struct.unpack_from('> (8 * i)) & 0xFF) + + def r16(self, addr): + addr = u32(addr) + if (addr & 0xFFFF) == 0xFFFF: + return self.r8(addr) | (self.r8(addr + 1) << 8) + p = self._page(addr, False) + if p is None: return 0 + return struct.unpack_from('> 8) & 0xFF) + else: + struct.pack_into('= 2: self.f[i] = u32(v) + + # single-precision float helpers + @staticmethod + def f2b(x): + try: + return struct.unpack(' +/-inf + return 0xff800000 if x < 0 else 0x7f800000 + @staticmethod + def b2f(b): return struct.unpack('> 2) & 1 + + # ------------- one instruction ------------- + def step(self): + pc = self.pc + if pc in self.stopat: + self.stop = True; self.stopmsg = f"stopat {pc:#010x}"; return False + w = self.mem.r32(pc) + if self.tailn: + self.tail.append((pc, w)) + if len(self.tail) > self.tailn: self.tail.pop(0) + if self.trace and self.steps < self.trace: + m, ops = dis860.decode(w, pc) + self.log(f"{pc:#010x}: {w:08x} {m:<10} {ops}") + self._branch = None + self.execute(w, pc) + self.steps += 1 + if self.stop: + return False + if self._branch is not None: + target, delayed = self._branch + if delayed: + # execute one delay-slot instruction, then jump + ds = pc + 4 + w2 = self.mem.r32(ds) + if self.trace and self.steps < self.trace: + m, ops = dis860.decode(w2, ds) + self.log(f"{ds:#010x}: {w2:08x} {m:<10} {ops} ; [delay slot]") + self._branch = None + self.execute(w2, ds) + self.steps += 1 + self.pc = self._branch[0] if self._branch else target + else: + self.pc = target + else: + self.pc = pc + 4 + return not self.stop + + def branch(self, target, delayed=True): + self._branch = (u32(target), delayed) + + def execute(self, w, pc): + op = (w >> 26) & 0x3f + src2 = (w >> 21) & 0x1f + dest = (w >> 16) & 0x1f + src1 = (w >> 11) & 0x1f + imm = w & 0xffff + + # ---- integer loads: ld.b (0x00/0x01), ld.s/ld.l (0x04/0x05) ---- + # dest = bits20:16. ODD op = immediate s16 offset; EVEN op = register-indexed + # EA = src2 + src1 (ground truth: DNC.O `ld.b -1(fp),r19`=0473ffff, + # VR_REMOT.S `ld.l r30(r31),r31`, DNC.S `ld.l r0(r10),r16`). + # For op 4/5 the instr bit0 selects size: 0 = .s (16-bit), 1 = .l (32-bit) + # (DNC.O `ld.l 52(fp),r23` = 14770035, imm = 52|1). ld.b/ld.s sign-extend + # (compiled code masks with `and 0xff` right after ld.b). + if op in (0x00, 0x01, 0x04, 0x05): + if op & 1: + m = 0xffff if op == 0x01 else (0xfffc if (w & 1) else 0xfffe) + ea = self.rd(src2) + s16(imm & m) + else: + ea = self.rd(src2) + self.rd(src1) + if op < 0x04: + v = self.mem.r8(ea) + if v & 0x80: v -= 0x100 + elif w & 1: # ld.l + v = self.mem.r32(ea) + else: # ld.s + v = self.mem.r16(ea) + if v & 0x8000: v -= 0x10000 + self.wr(dest, v); return + # ---- integer stores: st.b (0x03), st.s/st.l (0x07) ---- + # source = src1 (bits15:11); the 16-bit offset is SPLIT high=bits20:16, + # low=bits10:0 (DNC.O `st.b r19,-1(fp)` = 0c7f9fff). Offset bit0 selects + # st.s (0) vs st.l (1), same flag as loads. + if op in (0x03, 0x07): + off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff) + if op == 0x03: + self.mem.w8(self.rd(src2) + s16(off), self.rd(src1) & 0xff) + elif off & 1: # st.l + self.mem.w32(self.rd(src2) + s16(off & 0xfffc), self.rd(src1)) + else: # st.s + self.mem.w16(self.rd(src2) + s16(off & 0xfffe), self.rd(src1) & 0xffff) + return + # ---- FP loads/stores: fld (0x08/0x09), fst (0x0a/0x0b) ---- + # FP reg = dest field for BOTH (load target / store source; fst does NOT use + # the split-store encoding -- `fst.l f20,0x3c88(r31)` = 2ff43c8a, imm=0x3c88|2). + # ODD op = flat s16 immediate offset; EVEN op = register-indexed (src2+src1). + # Flag bits (low bits of the offset field): bit0 = auto-increment (base <- EA), + # bit1: 1 = .l (32-bit), 0 = .d (64-bit); bit2 with bit1=0 = .q (128-bit). + # Derived from AS860 .S<->.O pairs (OPTFLOAT/TRISTRIP/ZBUF32, n>=28 each). + if op in (0x08, 0x09, 0x0a, 0x0b): + fl = w & 7 + size = 4 if (fl & 2) else (16 if (fl & 4) else 8) + if op & 1: + off = s16(imm & (0x10000 - size)) + else: + off = self.rd(src1) + ea = self.rd(src2) + off + if fl & 1: # auto-increment + self.wr(src2, ea) + b0 = dest & ~((size // 4) - 1) + if op < 0x0a: # fld + for i in range(size // 4): + self.fwr(b0 + i, self.mem.r32(ea + i * 4)) + else: # fst + for i in range(size // 4): + self.mem.w32(ea + i * 4, self.frd(b0 + i)) + return + if op in (0x18, 0x19): # pfld.y (pipelined load: 3-stage load pipe) + fl = w & 7 + size = 4 if (fl & 2) else 8 + if op & 1: + off = s16(imm & (0x10000 - size)) + else: + off = self.rd(src1) + ea = self.rd(src2) + off + if fl & 1: # auto-increment + self.wr(src2, ea) + self._fp_pipes() + entry = (self.mem.r32(ea), self.mem.r32(ea + 4) if size == 8 else None) + out = self._lpipe[2] + self._lpipe[2] = self._lpipe[1]; self._lpipe[1] = self._lpipe[0] + self._lpipe[0] = entry + lo, hi = out + if hi is None: + self.fwr(dest, lo) + else: + b0 = dest & ~1 + self.fwr(b0, lo); self.fwr(b0 | 1, hi) + return + if op == 0x0d: # flush #const(src2)[++] (cache-line flush) + # Store-format split offset; cache line = 32B so low 5 bits are free: + # bit0 = auto-increment (src2 <- src2+offset). No memory effect for us. + off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff) + if off & 1: + self.wr(src2, self.rd(src2) + s16(off & 0xffe0)) + return + if op == 0x0c: # ld.c ctrl,dest + self.wr(dest, self.cr.get(src2, 0)); return + if op == 0x0e: # st.c src1,ctrl + self.cr[src2] = self.rd(src1); return + if op == 0x02: # ixfr src1 -> fdest (int->FP reg move) + self.fwr(dest, self.rd(src1)); return + + # ---- control transfer ---- + if op == 0x10: # bri src1 (indirect, delayed) + self.branch(self.rd(src1)); return + if op == 0x13: # CORE ESCAPE: sub-op in low bits + sub = w & 0x1f + if sub == 0x02: # calli src1 + self.wr(1, pc + 8); self.branch(self.rd(src1)); return + if sub in (0x01, 0x07): # lock / unlock (bus lock for atomic RMW) + return # single-CPU emulation: no-op + if sub == 0x04: # intovr (trap on overflow) -- ignore + return + self.stop = True + self.stopmsg = f"core-escape sub {sub:#x} @ {pc:#010x} w={w:08x}" + return + if op == 0x1a: # br + self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return + if op == 0x1b: # call + self.wr(1, pc + 8); self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return + if op in (0x1c, 0x1d): # bc / bc.t (branch if CC) + tgt = pc + 4 + s26(w & 0x03ffffff) * 4 + if self.cc(): self.branch(tgt, delayed=(op == 0x1d)) + return + if op in (0x1e, 0x1f): # bnc / bnc.t (branch if !CC) + tgt = pc + 4 + s26(w & 0x03ffffff) * 4 + if not self.cc(): self.branch(tgt, delayed=(op == 0x1f)) + return + if op in (0x14, 0x15, 0x16, 0x17): # btne / bte (compare & branch) + broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff) + a = src1 if (op & 1) else self.rd(src1) # even = 5-bit const + b = self.rd(src2) + take = (a != b) if op < 0x16 else (a == b) + if take: self.branch(pc + 4 + broff * 4, delayed=False) + return + + # ---- arithmetic / logic (odd opcode = 16-bit immediate) ---- + base = op & 0x3e + if base in (0x20, 0x22, 0x24, 0x26): # addu subu adds subs + # i860: dest = SRC1 (op) SRC2, where src1 = imm (immediate form) or + # rd(src1) (register form). Subtraction is src1 - src2 (NOT reversed). + b = self.rd(src2) + a = s16(imm) if (op & 1) else self.rd(src1) + if base == 0x20: # addu: CC = carry out + s = u32(a) + u32(b); self.wr(dest, s); self.set_cc(s > MASK32) + elif base == 0x24: # adds: CC = result negative + s = i32(a) + i32(b); self.wr(dest, s) + self.set_cc(bool(u32(s) & 0x80000000)) + elif base == 0x22: # subu: src1 - src2 + self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b)) + else: # subs: src1 - src2 + self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b)) + return + if op == 0x2d: # bla isrc1,isrc2,sbroff (loop: branch on LCC + add) + # Canonical idiom (DNC.S/compiler output): adds -1,rN,r18; adds -1,r0,r17; + # bla r17,r18,LOOP; ; LOOP: body...; bla r17,r18,LOOP; + # Semantics: taken = old LCC; src2 += src1; delayed branch if taken. + # LCC rule is SIGN-dependent (i860 manual): src1 < 0 -> LCC = (signed + # sum >= 0); src1 >= 0 -> LCC = unsigned carry. The signed rule makes a + # spent countdown (src1=-1, src2=-1) yield LCC=0, so a stray follow-on + # bla falls through instead of spinning (seen at 0xf041ce68). + a = self.rd(src1); b = self.rd(src2) + taken = getattr(self, 'lcc', 0) + if i32(a) < 0: + self.lcc = 1 if i32(u32(i32(a) + i32(b))) >= 0 else 0 + else: + self.lcc = 1 if (u32(a) + u32(b)) > MASK32 else 0 + self.wr(src2, u32(a) + u32(b)) + if taken: + off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & 0xffff) + self.branch(pc + 4 + off * 4) + return + if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra + cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f) + b = self.rd(src2) + if base == 0x28: r = b << cnt # shl + elif base == 0x2a: r = b >> cnt # shr (logical) + elif base == 0x2e: r = i32(b) >> cnt # shra + else: r = b >> cnt # shrd (approx) + self.wr(dest, r); return + if base in (0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e): # and..xorh + a = imm if (op & 1) else self.rd(src1) + hi = base in (0x32, 0x36, 0x3a, 0x3e) + if hi and (op & 1): a = imm << 16 + b = self.rd(src2) + g = base & 0x3c + if g == 0x30: r = b & a # and/andh + elif g == 0x34: r = b & ~a # andnot/andnoth + elif g == 0x38: r = b | a # or/orh + else: r = b ^ a # xor/xorh + self.wr(dest, r) + self.set_cc((r & MASK32) == 0) # i860 logicals set CC = (result == 0) + return + + # ---- FP unit (opcode 0x12) ---- + if op == 0x12: + self.exec_fp(w, src1, src2, dest); return + + # ---- unhandled ---- + m, ops = dis860.decode(w, pc) + self.stop = True + self.stopmsg = f"unimplemented op {op:#04x} ({m} {ops}) @ {pc:#010x} w={w:#010x}" + + # precision-aware FP register access (doubles occupy register PAIRS: + # f[N]=low32, f[N+1]=high32, little-endian) + def rdf(self, reg, dbl): + if dbl: + b = reg & ~1 + return struct.unpack('int bit games come out exact), and the + # retire is ENCODED with the pusher's precision, not the current op's. + def _fp_pipes(self): + if not hasattr(self, '_apipe') or (self._apipe and + not isinstance(self._apipe[0], tuple)): + pass + elif self._apipe and len(self._apipe[0]) == 2: # migrate 2-tuple pipes + self._apipe = [(v, r, None) for v, r in self._apipe] + self._mpipe = [(v, r, None) for v, r in self._mpipe] + if not hasattr(self, '_apipe') or (self._apipe and + not isinstance(self._apipe[0], tuple)): + # fresh init, or normalize legacy float-only snapshots + old_a = getattr(self, '_apipe', [0.0] * 3) + old_m = getattr(self, '_mpipe', [0.0] * 3) + self._apipe = [(float(v), 1, None) for v in old_a] + self._mpipe = [(float(v), 1, None) for v in old_m] + self._lpipe = [(0, None)] * 3 # pfld: (lo bits, hi bits | None) + self._gpipe = [0] # graphics: raw 32-bit int + self._kr = getattr(self, '_kr', 0.0) + self._ki = getattr(self, '_ki', 0.0) + self._t = getattr(self, '_t', 0.0) + return self._apipe, self._mpipe + + def _round_rp(self, val, rp): + if rp: + return val + return self.b2f(self.f2b(val)) # round to single + + def _padv(self, pipe, val, rp, depth, raw=None): + """Push a stage-entry; return the (val, rp, raw) leaving stage depth-1. + raw is a raw bit pattern (32 or 64-bit int) for non-float results + (pipelined ftrunc/fix), written back verbatim at retire.""" + out = pipe[depth - 1] + for i in range(depth - 1, 0, -1): + pipe[i] = pipe[i - 1] + pipe[0] = (self._round_rp(val, rp) if raw is None else val, rp, raw) + return out + + def _retire(self, dest, entry): + val, rp, raw = entry + if raw is None: + self.wrf(dest, val, rp) + elif rp: + b0 = dest & ~1 + self.fwr(b0, raw & MASK32); self.fwr(b0 | 1, (raw >> 32) & MASK32) + else: + self.fwr(dest, raw & MASK32) + + # PFAM/PFSM dual-op routing, decoded from the validated mnemonic grammar + # (AS860 corpus: r2p1, r2pt, r2ap1, r2apt, i2p1.., rat1p2, m12apm, ra1p2, + # m12tpm, m12tpa, iat1p2, ia1p2 ...): + # M-unit: r2 = KR*src2 | i2 = KI*src2 | m12 = src1*src2 | + # ra = KR*Ares | ia = KI*Ares ('t' suffix: T <- Mres) + # A-unit: p1 = Mres+src1 | pt = Mres+T | ap1 = Ares+src1 | apt = Ares+T | + # apm = Ares+Mres | tpm = T+Mres | tpa = T+Ares | 1p2 = src1+src2 + # K-load: if src1 is unused by the routing, KR (r-forms) / KI (i-forms) + # is loaded from src1. sub 0x10-0x1f = PFSM (A-unit subtracts). + # Encoded per DPC as (m1,m2, a1,a2, kload, tload) with operand tags: + # s1 s2 kr ki t am (adder-pipe result) mm (mul-pipe result) + # Dual-operation DPC routing, verbatim from the i860 spec (validated against + # MAME's i860 core src_opers[] table): (m1, m2, a1, a2, kload, tload). + # 'PP' = the FLAGM operand: A-pipe last stage for the PFAM family (P=1), + # M-pipe last stage for the PFMAM family (P=0). + _DUAL = { + 0x0: ('kr','s2', 's1','mm', False, False), # r2p1 + 0x1: ('kr','s2', 't', 'mm', True, False), # r2pt + 0x2: ('kr','s2', 's1','PP', False, True), # r2ap1 + 0x3: ('kr','s2', 't', 'PP', True, True), # r2apt + 0x4: ('ki','s2', 's1','mm', False, False), # i2p1 + 0x5: ('ki','s2', 't', 'mm', True, False), # i2pt + 0x6: ('ki','s2', 's1','PP', False, True), # i2ap1 + 0x7: ('ki','s2', 't', 'PP', True, True), # i2apt + 0x8: ('kr','PP', 's1','s2', False, True), # rat1p2 + 0x9: ('s1','s2', 'PP','mm', False, False), # m12apm + 0xa: ('kr','PP', 's1','s2', False, False), # ra1p2 + 0xb: ('s1','s2', 't', 'PP', False, True), # m12ttpa + 0xc: ('ki','PP', 's1','s2', False, True), # iat1p2 + 0xd: ('s1','s2', 't', 'mm', False, False), # m12tpm + 0xe: ('ki','PP', 's1','s2', False, False), # ia1p2 + 0xf: ('s1','s2', 't', 'PP', False, False), # m12tpa + } + + def exec_fp(self, w, src1, src2, dest): + sub = w & 0x7f + # MAME-validated bit assignments: SOURCE precision = bit8, RESULT = bit7. + sp = 1 if (w & 0x100) else 0 + rp = 1 if (w & 0x080) else 0 + pbit = (w >> 10) & 1 # pipelined (scalar ops) / PFAM-vs-PFMAM (dual ops) + if sub < 0x20: # dual ops: PF[M]AM (bit4=0) / PF[M]SM (bit4=1) + # sp = MULTIPLIER source precision; rp = ADDER source AND all results. + ap, mp = self._fp_pipes() + mdepth = 2 if sp else 3 + am_out, am_rp, am_raw = ap[2] + mm_out, mm_rp, mm_raw = mp[mdepth - 1] + pipe_val = am_out if pbit else mm_out # FLAGM operand + fdest bypass + m1, m2, a1, a2, kload, tload = self._DUAL[sub & 0xf] + def val(tag, prec): + if tag == 's1': return self.rdf(src1, prec) + if tag == 's2': return self.rdf(src2, prec) + if tag == 'kr': return self._kr + if tag == 'ki': return self._ki + if tag == 't': return self._t + if tag == 'mm': return mm_out + return pipe_val # 'PP' + v1 = val(m1, sp); v2 = val(m2, sp) + if m2 == 's2' and dest and src2 == dest: # fdest bypass (mul op2 only) + v2 = pipe_val + newm = v1 * v2 + u1 = val(a1, rp); u2 = val(a2, rp) + if a1 == 's1' and dest and src1 == dest: u1 = pipe_val + if a2 == 's2' and dest and src2 == dest: u2 = pipe_val + newa = (u1 - u2) if (sub & 0x10) else (u1 + u2) + if tload: self._t = mm_out # T <- M-pipe last stage + if kload: # K <- fsrc1 (mul precision) + if m1 == 'ki': self._ki = self.rdf(src1, sp) + else: self._kr = self.rdf(src1, sp) + if pbit: self._retire(dest, ap[2]) # PFAM: fdest <- A retire + else: self._retire(dest, mp[mdepth - 1]) # PFMAM: fdest <- M retire + self._padv(ap, newa, rp, 3) + self._padv(mp, newm, rp, mdepth) + return + if sub in (0x20, 0x24): # fmul / pfmul (0x24 = pfmul3: 3-stage) + v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) + if pbit: + ap, mp = self._fp_pipes() + depth = 3 if sub == 0x24 else (2 if sp else 3) + out, orp, oraw = mp[depth - 1] + if dest and src1 == dest: v1 = out # fdest bypass + if dest and src2 == dest: v2 = out + self._retire(dest, mp[depth - 1]) + self._padv(mp, v1 * v2, rp, depth) + else: + self.wrf(dest, v1 * v2, rp) + elif sub in (0x30, 0x31, 0x33): # fadd/fsub/famov (+ pipelined forms) + v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) + if pbit: + ap, mp = self._fp_pipes() + out, orp, oraw = ap[2] + if dest and src1 == dest: v1 = out # fdest bypass + if dest and src2 == dest: v2 = out + if sub == 0x33: r = v1 + else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) + self._retire(dest, ap[2]) + self._padv(ap, r, rp, 3) + else: + if sub == 0x33: r = v1 + else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) + self.wrf(dest, r, rp) + elif sub == 0x22: # frcp (reciprocal of src2) + b = self.rdf(src2, sp) + self.wrf(dest, (1.0 / b) if b else 0.0, rp) + elif sub == 0x23: # frsqr (recip sqrt of src2) + import math + b = self.rdf(src2, sp) + self.wrf(dest, (1.0 / math.sqrt(b)) if b > 0 else 0.0, rp) + elif sub in (0x32, 0x3a): # [p]fix / [p]ftrunc -> int bits + a = self.rdf(src1, sp) + if a != a or a in (float('inf'), float('-inf')) or abs(a) > 0x7fffffff: + iv = 0x80000000 # IEEE indefinite + else: + iv = int(a) if sub == 0x3a else int(a + (0.5 if a >= 0 else -0.5)) + iv &= 0xFFFFFFFFFFFFFFFF if rp else 0xFFFFFFFF + if pbit: # pipelined: through the adder pipe + ap, mp = self._fp_pipes() + self._retire(dest, ap[2]) + self._padv(ap, float(iv & 0xFFFFFFFF), rp, 3, raw=iv) + elif rp: + b0 = dest & ~1 + self.fwr(b0, iv & 0xFFFFFFFF) + self.fwr(b0 | 1, (iv >> 32) & 0xFFFFFFFF) + else: + self.fwr(dest, iv & 0xFFFFFFFF) + elif sub == 0x34: # fgt: CC = src1 > src2 + self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp)) + elif sub == 0x35: # feq: CC = src1 == src2 + self.set_cc(self.rdf(src1, sp) == self.rdf(src2, sp)) + elif sub == 0x21: # fmlow.dd -- i860 FP-unit INTEGER multiply. + # The i860 has no imul: ints are ixfr'd into FP regs, fmlow.dd multiplies, + # and the low 32 bits (fdest) are fxfr/fst'd back. Operands are the low + # words (32-bit); result is the 64-bit product across the fdest pair. + a = self.frd(src1); b = self.frd(src2) + prod = a * b + b0 = dest & ~1 + self.fwr(b0, prod & 0xFFFFFFFF); self.fwr(b0 | 1, (prod >> 32) & 0xFFFFFFFF) + elif sub == 0x40: # fxfr FP->int + self.wr(dest, self.frd(src1)) + elif sub in (0x49, 0x4d): # fiadd/fisub (graphics-unit int add/sub; + # .ss = 32-bit, .dd = 64-bit across register pairs; P = 1-stage pipe) + if sp: + a = self.frd(src1 & ~1) | (self.frd((src1 & ~1) | 1) << 32) + b = self.frd(src2 & ~1) | (self.frd((src2 & ~1) | 1) << 32) + m = (1 << 64) - 1 + else: + a = self.frd(src1); b = self.frd(src2); m = MASK32 + r = ((a + b) if sub == 0x49 else (a - b)) & m + if pbit: + self._fp_pipes() + out = self._gpipe[0] + self._gpipe[0] = r + r = out + if sp: + b0 = dest & ~1 + self.fwr(b0, r & MASK32); self.fwr(b0 | 1, (r >> 32) & MASK32) + else: + self.fwr(dest, r) + elif sub == 0x5f: # fnop + pass + else: + self.stop = True + self.stopmsg = f"unimplemented FP sub {sub:#04x} @ {self.pc:#010x}" + + # ------------- board control / CCB region (0xFFFFxxxx) ------------- + def map_control(self, cfg=None): + """Model the transputer<->i860 control/config + CCB region. + cfg: {addr: value} overrides for the 0xFFFFF7xx config registers.""" + self.ctrl = {0xfffff720: 2, # must read 2 (IO_ACK) for normal boot + 0xfffff70c: 1} # board-config select (1 = CCB @0xffffe000 path) + if cfg: self.ctrl.update(cfg) + self.ctrl_log = [] + def rd(a): + v = self.ctrl.get(a, 0) + self.ctrl_log.append(('r', a, v)) + return v + def wr(a, v): + self.ctrl[a] = v + self.ctrl_log.append(('w', a, v)) + self.mem.map_mmio(0xfff00000, 0x100000000, rd, wr, 'ctrl/ccb') + + # ------------- board / IGC MMIO (logged) ------------- + def map_board(self): + """Log accesses to the board-register (0x8380_xxxx) and IGC/DMA regions, + and capture the IGC coefficient stream (Tier-1 handoff).""" + self.board_log = [] + self.igc = [] # captured (addr,val) IGC coefficient writes + def brd_rd(a): self.board_log.append(('r', a, 0)); return 0 + def brd_wr(a, v): self.board_log.append(('w', a, v)) + self.mem.map_mmio(0x83000000, 0x84000000, brd_rd, brd_wr, 'board') + + # ------------- call harness (direct handler invocation) ------------- + RET_SENTINEL = 0xbadca110 + def call(self, addr, args=(), sp=0x000c0000, maxsteps=2_000_000): + """Invoke a firmware function directly (PGI conv: args r16.., ret r16, + r1=return, r2=sp). Runs until it returns to the sentinel. Returns r16.""" + self.wr(1, self.RET_SENTINEL) + self.wr(2, sp) + for i, a in enumerate(args): + self.wr(16 + i, a) + self.pc = u32(addr) + self.stop = False; self.stopmsg = '' + n0 = self.steps + while self.steps - n0 < maxsteps: + if self.pc == self.RET_SENTINEL: + return self.rd(16) + if not self.step(): + return None # faulted (stopmsg set) + self.stop = True; self.stopmsg = f"call {addr:#x} ran {maxsteps} steps (no return)" + return None + + # ------------- loader ------------- + def load_mng(self, path, base=0xf0400000): + d = open(path, 'rb').read() + tsize, dsize, bsize = struct.unpack_from('