Files
TeslaRel410/emulator/firmware-decomp/emu860.py
T
CydandClaude Opus 4.8 f5e9ae987d i860 emu: add EMU_DATA_BASE / EMU_INDEXED experiment hooks (default off)
Env-gated hooks to continue the create()/jump-table investigation without
changing default behavior:
- EMU_DATA_BASE (default 0): overrides I860.DATA_BASE for the .data link base.
- EMU_INDEXED=1 (default off): makes EVEN integer load opcodes register-indexed
  (EA = base + index), per the ground-truth `ld.l r30(r31),r31` in VR_REMOT.S.

Sweep finding (EMU_INDEXED=1): create() RETURNS at DATA_BASE=0xff0 making real
subroutine calls, confirming register-indexed loads + a ~0x1000 data base are
correct. Residual: do_init derails to (DATA_BASE+0x30) for base >= 0xfe0 while
create needs >= ~0xfe0 -- a single flat base can't satisfy both yet, pointing
to a segment/relocation nuance (or a separate do_init jump-table derail).
Details + repro in the tier-0 memory. Default behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 09:40:33 -05:00

464 lines
20 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_BASE = int(os.environ.get('EMU_DATA_BASE', '0'), 0) # .data/.bss link base (experiment: sweep)
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): return struct.unpack('<I', struct.pack('<f', x))[0]
@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
# ---- 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):
if _INDEXED and not (op & 1): # EVEN opcode = register-indexed (VR_REMOT.S)
ea = self.rd(src2) + self.rd(src1)
else:
m = 0xffff if op < 0x04 else (0xfff8 if op >= 0x08 else 0xfffc)
ea = self.rd(src2) + s16(imm & m)
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('<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))
def exec_fp(self, w, src1, src2, dest):
sub = w & 0x7f
sp = (w >> 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('<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()