- pfgt/pfle/pfeq are always pipelined: fdest <- A-pipe retire, push undefined; bit7 selects pfle (inverted CC sense) -- was misread as result precision. 316 pipelined compares in the firmware previously desynced the A-pipe. - h_igcwait now models full consumption: done-status nonzero AND write-index reset (+0x7f8=0), otherwise queue pages saturate at 0x7f0 across frames and enqueueing silently stops (observed: even the per-frame marker item stopped binning by cmd ~4300). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
745 lines
35 KiB
Python
745 lines
35 KiB
Python
"""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 <VREND.MNG> [--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('<I', p, off)[0]
|
|
return (self.r8(addr) | (self.r8(addr+1) << 8) |
|
|
(self.r8(addr+2) << 16) | (self.r8(addr+3) << 24))
|
|
|
|
def w32(self, addr, val):
|
|
addr = u32(addr); val = u32(val)
|
|
m = self._mmio(addr)
|
|
if m: return m[1](addr, val)
|
|
off = addr & 0xFFFF
|
|
if off <= 0xFFFC:
|
|
struct.pack_into('<I', self._page(addr, True), off, val)
|
|
else:
|
|
for i in range(4): self.w8(addr + i, (val >> (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('<H', p, addr & 0xFFFF)[0]
|
|
|
|
def r8(self, addr):
|
|
addr = u32(addr); p = self._page(addr, False)
|
|
return p[addr & 0xFFFF] if p else 0
|
|
|
|
def w16(self, addr, val):
|
|
addr = u32(addr)
|
|
if (addr & 0xFFFF) == 0xFFFF:
|
|
self.w8(addr, val & 0xFF); self.w8(addr + 1, (val >> 8) & 0xFF)
|
|
else:
|
|
struct.pack_into('<H', self._page(addr, True), addr & 0xFFFF, val & 0xFFFF)
|
|
|
|
def w8(self, addr, val):
|
|
self._page(u32(addr), True)[addr & 0xFFFF] = val & 0xFF
|
|
|
|
|
|
CTRL_NAMES = {0: 'fir', 1: 'psr', 2: 'dirbase', 3: 'db', 4: 'fsr', 5: 'epsr'}
|
|
|
|
|
|
class I860:
|
|
# .data link base = 0x1000, DEFINITIVE: VREND.MNG carries its original COFF
|
|
# header in the file tail (offset 12+tsize+dsize+16): .data vaddr=0x00001000,
|
|
# .bss vaddr=0x0001f940, .text vaddr=0xf0400000, entry=0xf0400000.
|
|
DATA_BASE = int(os.environ.get('EMU_DATA_BASE', '0x1000'), 0)
|
|
|
|
def __init__(self, trace=0, logf=None):
|
|
self.r = [0] * 32 # integer regs (r0 == 0)
|
|
self.f = [0] * 32 # FP regs as raw 32-bit (f0,f1 == 0)
|
|
self.cr = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0} # control regs
|
|
self.pc = 0
|
|
self.mem = Mem(self.log)
|
|
self.trace = trace
|
|
self.logf = logf or sys.stdout
|
|
self.steps = 0
|
|
self.stop = False
|
|
self.stopmsg = ''
|
|
self._branch = None # (target, delayed?)
|
|
self.tailn = 60 # ring buffer of last-N executed
|
|
self.tail = []
|
|
self.stopat = set() # halt when pc first reaches one of these
|
|
|
|
def log(self, s):
|
|
self.logf.write(s + "\n")
|
|
|
|
# ---- register helpers (r0 hardwired 0) ----
|
|
def rd(self, i): return self.r[i] if i else 0
|
|
def wr(self, i, v):
|
|
if i: self.r[i] = u32(v)
|
|
def frd(self, i): return 0 if i < 2 else self.f[i]
|
|
def fwr(self, i, v):
|
|
if i >= 2: self.f[i] = u32(v)
|
|
|
|
# single-precision float helpers
|
|
@staticmethod
|
|
def f2b(x):
|
|
try:
|
|
return struct.unpack('<I', struct.pack('<f', x))[0]
|
|
except OverflowError: # IEEE overflow -> +/-inf
|
|
return 0xff800000 if x < 0 else 0x7f800000
|
|
@staticmethod
|
|
def b2f(b): return struct.unpack('<f', struct.pack('<I', u32(b)))[0]
|
|
|
|
def set_cc(self, cond):
|
|
# psr bit 2 = CC (condition code)
|
|
if cond: self.cr[1] |= 0x4
|
|
else: self.cr[1] &= ~0x4
|
|
def cc(self): return (self.cr[1] >> 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; <delay>; LOOP: body...; bla r17,r18,LOOP; <delay>
|
|
# 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('<d', struct.pack('<II', self.frd(b), self.frd(b | 1)))[0]
|
|
return self.b2f(self.frd(reg))
|
|
|
|
def wrf(self, reg, val, dbl):
|
|
if dbl:
|
|
lo, hi = struct.unpack('<II', struct.pack('<d', val))
|
|
b = reg & ~1
|
|
self.fwr(b, lo); self.fwr(b | 1, hi)
|
|
else:
|
|
self.fwr(reg, self.f2b(val))
|
|
|
|
# ---- pipelined FP unit state (functional model, no cycle timing) ----
|
|
# Adder pipe = 3-stage; multiplier pipe = 2-stage double / 3-stage single;
|
|
# load pipe (pfld) = 3-stage; graphics pipe (pfiadd/pfisub) = 1-stage.
|
|
# KR/KI/T = the dual-operation constant/transfer registers.
|
|
# Each stage carries (value, result-precision): the value is rounded to
|
|
# SINGLE at push time when the pushing op has rp=0 (this is what makes the
|
|
# compiler's magic-constant float->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 in (0x34, 0x35): # pfgt/pfle (0x34) / pfeq (0x35)
|
|
# MAME-validated: bit7 (the "R" bit) selects pfgt (0) vs pfle (1) --
|
|
# NOT result precision. pfle CLEARS CC when src1 <= src2. These are
|
|
# always pipelined: fdest <- A-pipe retire; push undefined (0.0).
|
|
v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp)
|
|
if sub == 0x35:
|
|
self.set_cc(v1 == v2)
|
|
elif w & 0x80: # pfle
|
|
self.set_cc(not (v1 <= v2))
|
|
else: # pfgt
|
|
self.set_cc(v1 > v2)
|
|
ap, mp = self._fp_pipes()
|
|
self._retire(dest, ap[2])
|
|
self._padv(ap, 0.0, sp, 3)
|
|
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('<III', d, 0)
|
|
text = d[0x0c:0x0c + tsize]
|
|
data = d[0x0c + tsize:0x0c + tsize + dsize]
|
|
# text at `base` (high, ~0xf0400000); entry = base (the veneer).
|
|
# .data/.bss are linked LOW -- the code builds global addresses like
|
|
# 0xec10 / 0x22644 (< 0x30000). Load .data at data_base; .bss (zero-init)
|
|
# follows and autocreates on write (reads of unwritten bss return 0).
|
|
self.mem.load_blob(base, text)
|
|
self.mem.load_blob(self.DATA_BASE, data)
|
|
self.text_base, self.tsize = base, tsize
|
|
self.data_base, self.dsize = self.DATA_BASE, dsize
|
|
self.bss_base, self.bss = self.DATA_BASE + dsize, bsize
|
|
self.pc = base
|
|
# a stack somewhere sane
|
|
self.wr(2, 0x00080000) # r2 = sp (guess; low RAM)
|
|
return tsize, dsize, bsize
|
|
|
|
|
|
def main():
|
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
|
mng = args[0] if args else r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
|
|
trace = 40; steps = 2000
|
|
for i, a in enumerate(sys.argv):
|
|
if a == '--trace': trace = int(sys.argv[i + 1])
|
|
if a == '--steps': steps = int(sys.argv[i + 1])
|
|
cpu = I860(trace=trace)
|
|
t, dd, b = cpu.load_mng(mng)
|
|
cpu.map_control()
|
|
print(f"loaded {os.path.basename(mng)}: text={t:#x} data={dd:#x} bss={b:#x} "
|
|
f"entry={cpu.pc:#010x}\n--- trace ---")
|
|
while cpu.steps < steps and cpu.step():
|
|
pass
|
|
print("--- halt ---")
|
|
print(f"steps={cpu.steps} pc={cpu.pc:#010x}")
|
|
if cpu.stopmsg: print("STOP:", cpu.stopmsg)
|
|
print("regs:", " ".join(f"r{i}={cpu.r[i]:#x}" for i in range(8)))
|
|
print("ctrl/ccb accesses (last 15):")
|
|
for k, a, v in cpu.ctrl_log[-15:]:
|
|
print(f" {k} {a:#010x} = {v:#x}")
|
|
if '--tail' in sys.argv:
|
|
print("--- last executed (tail) ---")
|
|
seen = {}
|
|
for pc, w in cpu.tail:
|
|
m, ops = dis860.decode(w, pc)
|
|
print(f" {pc:#010x}: {w:08x} {m:<10} {ops}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|