"""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 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): 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 # ---- loads ---- i860 integer loads are IMMEDIATE-offset (both even/odd of a # pair): EA = base(src2) + s16(offset). Indexing is done by pre-computing # base+index into a register, then loading at offset 0. dest = bits20:16. if op in (0x00, 0x01, 0x04, 0x05, 0x08, 0x09): m = 0xffff if op < 0x04 else (0xfff8 if op >= 0x08 else 0xfffc) off = s16(imm & m) ea = self.rd(src2) + off if op in (0x00, 0x01): self.wr(dest, self.mem.r8(ea)) # ld.b (byte) elif op in (0x04, 0x05): self.wr(dest, self.mem.r32(ea)) # ld.l else: self.fwr(dest, self.mem.r32(ea)) # fld.l return # ---- stores ---- i860 stores differ: source reg = src1 (bits15:11), and the # 16-bit offset is SPLIT high=bits20:16, low=bits10:0 (the src1 field displaced it). if op in (0x03, 0x06, 0x07, 0x0a, 0x0b): m = 0xffff if op == 0x03 else (0xfff8 if op >= 0x0a else 0xfffc) off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & m) ea = self.rd(src2) + off if op == 0x03: self.mem.w8(ea, self.rd(src1) & 0xff) # st.b (byte) elif op in (0x06, 0x07): self.mem.w32(ea, self.rd(src1)) # st.l else: self.mem.w32(ea, self.frd(src1)) # fst.l 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: # calli src1 self.wr(1, pc + 8); self.branch(self.rd(src1)); 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 s = u32(a) + u32(b); self.wr(dest, s); self.set_cc(s > MASK32) elif base == 0x24: # adds s = i32(a) + i32(b); self.wr(dest, s); self.set_cc(u32(a) + u32(b) > MASK32) 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 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('> 7) & 1 # source precision (1 = double) rp = (w >> 8) & 1 # result precision # NOTE: pf* pipelined ops are treated as non-pipelined (functional) for now. if sub == 0x20: # fmul self.wrf(dest, self.rdf(src1, sp) * self.rdf(src2, sp), rp) elif sub == 0x30: # fadd self.wrf(dest, self.rdf(src1, sp) + self.rdf(src2, sp), rp) elif sub == 0x31: # fsub self.wrf(dest, self.rdf(src1, sp) - self.rdf(src2, sp), rp) elif sub == 0x33: # famov (move src1, precision-convert) self.wrf(dest, self.rdf(src1, sp), 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) 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 == 0x49: # fiadd (integer add in FP unit, 32-bit) self.fwr(dest, self.frd(src1) + self.frd(src2)) elif sub == 0x4d: # fisub self.fwr(dest, self.frd(src1) - self.frd(src2)) 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('