i860 emu: ISA round 2 (ground-truth encodings) + authentic-main runner; firmware
processes the full wire boot incl. the downloaded PAZ/sfx module ISA fixes, all derived from the toolchain's own .S<->.O pairs (AS860.ZIP: OPTFLOAT/TRISTRIP/ZBUF32, plus DNC.O) and the firmware's linked COFF header: - DATA_BASE = 0x1000 DEFINITIVE: VREND.MNG carries its original COFF header in the file tail (.data vaddr 0x1000, .bss 0x1f940, entry 0xf0400000). - Integer loads: even opcodes are register-indexed (EA = src2 + src1); op 4/5 size flag = instr bit0 (0 = ld.s 16-bit, 1 = ld.l 32-bit); ld.b/ld.s sign-extend. - Integer stores: st.s/st.l selected by offset bit0, same split-offset rule. - FP loads/stores: FP register lives in the DEST field for both fld and fst (fst does NOT use the integer split-store encoding); flag bits: bit0 = auto-increment (base <- EA), bit1 1=.l/0=.d, bit2 = .q; .d/.q span register pairs/quads. ~450 fld.d + ~300 fst.d were previously read/written 32-bit. - bla (op 0x2d, was misdecoded as shrd): branch-on-LCC-and-add with the sign-dependent LCC rule (src1<0 -> signed sum >= 0), so spent countdown loops terminate. 335 bla instructions in the firmware. - CORE ESCAPE (op 0x13): sub-op 1 = lock, 2 = calli, 7 = unlock. Previously everything decoded as calli, so every spinlock acquire jumped to address 0 -- this was the phantom "exit stub" behind most earlier derails. - f2b: IEEE overflow -> +/-inf instead of raising. emu_main.py (new): runs the firmware's OWN main() (0xf0403f10) and feeds real wire captures through a hooked dN_receive, so init/do_init/dispatch/handlers all execute authentically. Provides the transputer-monitor environment (processor id, DRAM region descriptors in the shared control block, sbrk/ shared-block seed slots) and hooks only the link primitives (bla busy-wait, dN_mynode/dN_nodes/dN_receive/dN_send, putchar path, spinlocks, page allocator + virt->phys translator pending Tier-2 VRENDMON). KEY DISCOVERY: the capture's args860/code860/data860/bss860 preamble is the host DOWNLOADING an additional i860 module (the PAZ/sfx renderer layer, banner "i860 50MHz") which installs the runtime handler tables and system objects. Feeding it through the firmware's own handlers, the module loads and makes the first IGC board-register writes. State: 834+ wire commands processed (module download + init + create); first draw_scene sits at command 1568. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,11 +52,16 @@ def dec_fp(w):
|
|||||||
return (name + fp_suffix(w), f"{FREG[src1]},{FREG[src2]},{FREG[dest]}")
|
return (name + fp_suffix(w), f"{FREG[src1]},{FREG[src2]},{FREG[dest]}")
|
||||||
|
|
||||||
# ---- primary decode ----
|
# ---- primary decode ----
|
||||||
# immediate-form rule: for load/store (0x00-0x0b) and arith/logic (0x20-0x3f),
|
# load/store encoding (ground truth: AS860 .S<->.O pairs + DNC.O):
|
||||||
# the ODD opcode of each pair is the 16-bit immediate/const form.
|
# - integer loads (0x00/0x01 ld.b, 0x04/0x05 ld.s|ld.l): dest=bits20:16;
|
||||||
MEM = {0x00: "ld.s", 0x01: "ld.s", 0x04: "ld.l", 0x05: "ld.l",
|
# ODD = 16-bit immediate offset, EVEN = register-indexed EA=src2+src1.
|
||||||
0x06: "st.l", 0x07: "st.l", 0x08: "fld.l", 0x09: "fld.l",
|
# op 4/5 size flag = instr bit0 (0=.s 16-bit, 1=.l 32-bit).
|
||||||
0x0a: "fst.l", 0x0b: "fst.l"}
|
# - integer stores (0x03 st.b, 0x07 st.s|st.l): source reg = src1 (bits15:11),
|
||||||
|
# offset SPLIT high=bits20:16 low=bits10:0; bit0 size flag as loads.
|
||||||
|
# - FP fld (0x08/0x09) / fst (0x0a/0x0b): FP reg = dest field for BOTH;
|
||||||
|
# ODD = flat s16 imm, EVEN = indexed. Flags: bit0=auto-increment,
|
||||||
|
# bit1: 1=.l, 0=.d; bit2 (with bit1=0) = .q.
|
||||||
|
MEM = {0x00, 0x01, 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
||||||
ARITH = {0x20: "addu", 0x22: "subu", 0x24: "adds", 0x26: "subs",
|
ARITH = {0x20: "addu", 0x22: "subu", 0x24: "adds", 0x26: "subs",
|
||||||
0x28: "shl", 0x2a: "shr", 0x2c: "shrd", 0x2e: "shra",
|
0x28: "shl", 0x2a: "shr", 0x2c: "shrd", 0x2e: "shra",
|
||||||
0x30: "and", 0x32: "andh", 0x34: "andnot", 0x36: "andnoth",
|
0x30: "and", 0x32: "andh", 0x34: "andnot", 0x36: "andnoth",
|
||||||
@@ -70,15 +75,32 @@ def decode(w, addr):
|
|||||||
imm = w & 0xffff
|
imm = w & 0xffff
|
||||||
# loads/stores
|
# loads/stores
|
||||||
if op in MEM:
|
if op in MEM:
|
||||||
m = MEM[op]
|
if op in (0x00, 0x01, 0x04, 0x05): # integer loads
|
||||||
off = s16(imm & 0xfffc) if 'l' in m else s16(imm)
|
m = "ld.b" if op < 0x04 else ("ld.l" if (w & 1) else "ld.s")
|
||||||
if m.startswith('f'):
|
if op & 1:
|
||||||
rd = FREG[dest]
|
mask = 0xffff if op == 0x01 else (0xfffc if (w & 1) else 0xfffe)
|
||||||
|
return m, f"{s16(imm & mask):#x}({REG[src2]}),{REG[dest]}"
|
||||||
|
return m, f"{REG[src1]}({REG[src2]}),{REG[dest]}"
|
||||||
|
if op in (0x03, 0x07): # integer stores (split offset)
|
||||||
|
off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff)
|
||||||
|
if op == 0x03:
|
||||||
|
return "st.b", f"{REG[src1]},{s16(off):#x}({REG[src2]})"
|
||||||
|
m = "st.l" if (off & 1) else "st.s"
|
||||||
|
off &= 0xfffc if (off & 1) else 0xfffe
|
||||||
|
return m, f"{REG[src1]},{s16(off):#x}({REG[src2]})"
|
||||||
|
# FP fld/fst: FP reg in dest field; flags bit0=auto++, bit1:1=.l/0=.d, bit2=.q
|
||||||
|
fl = w & 7
|
||||||
|
sz = ".l" if (fl & 2) else (".q" if (fl & 4) else ".d")
|
||||||
|
pp = "++" if (fl & 1) else ""
|
||||||
|
m = ("fld" if op < 0x0a else "fst") + sz
|
||||||
|
if op & 1:
|
||||||
|
size = 4 if (fl & 2) else (16 if (fl & 4) else 8)
|
||||||
|
ea = f"{s16(imm & (0x10000 - size)):#x}({REG[src2]}){pp}"
|
||||||
else:
|
else:
|
||||||
rd = REG[dest]
|
ea = f"{REG[src1]}({REG[src2]}){pp}"
|
||||||
if m.startswith('st') or m.startswith('fst'):
|
if op < 0x0a:
|
||||||
return m, f"{rd},{off:#x}({REG[src2]})"
|
return m, f"{ea},{FREG[dest]}"
|
||||||
return m, f"{off:#x}({REG[src2]}),{rd}"
|
return m, f"{FREG[dest]},{ea}"
|
||||||
if op == 0x02:
|
if op == 0x02:
|
||||||
return "ixfr", f"{REG[src1]},{FREG[dest]}"
|
return "ixfr", f"{REG[src1]},{FREG[dest]}"
|
||||||
if op == 0x0c:
|
if op == 0x0c:
|
||||||
@@ -92,9 +114,17 @@ def decode(w, addr):
|
|||||||
if op == 0x12:
|
if op == 0x12:
|
||||||
return dec_fp(w)
|
return dec_fp(w)
|
||||||
if op == 0x13:
|
if op == 0x13:
|
||||||
# core escape: sub-op in low bits; calli common
|
# core escape: sub-op in low 5 bits (1=lock, 2=calli, 4=intovr, 7=unlock)
|
||||||
if (w & 0x7ff) == 0 or True:
|
sub = w & 0x1f
|
||||||
|
if sub == 0x02:
|
||||||
return "calli", f"{REG[src1]}"
|
return "calli", f"{REG[src1]}"
|
||||||
|
if sub == 0x01:
|
||||||
|
return "lock", ""
|
||||||
|
if sub == 0x07:
|
||||||
|
return "unlock", ""
|
||||||
|
if sub == 0x04:
|
||||||
|
return "intovr", ""
|
||||||
|
return "esc", f"{sub:#x}"
|
||||||
if op in (0x14, 0x15, 0x16, 0x17):
|
if op in (0x14, 0x15, 0x16, 0x17):
|
||||||
base = "btne" if op < 0x16 else "bte"
|
base = "btne" if op < 0x16 else "bte"
|
||||||
broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)
|
broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)
|
||||||
@@ -110,6 +140,9 @@ def decode(w, addr):
|
|||||||
if op in (0x18, 0x19):
|
if op in (0x18, 0x19):
|
||||||
off = s16(imm & 0xfff8)
|
off = s16(imm & 0xfff8)
|
||||||
return "pfld.d", f"{off:#x}({REG[src2]}),{FREG[dest]}"
|
return "pfld.d", f"{off:#x}({REG[src2]}),{FREG[dest]}"
|
||||||
|
if op == 0x2d: # bla isrc1,isrc2,sbroff (split offset like bte)
|
||||||
|
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & 0xffff)
|
||||||
|
return "bla", f"{REG[src1]},{REG[src2]},{addr + 4 + off*4:#010x}"
|
||||||
if (op & 0x3e) in ARITH:
|
if (op & 0x3e) in ARITH:
|
||||||
m = ARITH[op & 0x3e]
|
m = ARITH[op & 0x3e]
|
||||||
if op & 1: # immediate form
|
if op & 1: # immediate form
|
||||||
|
|||||||
@@ -109,7 +109,10 @@ CTRL_NAMES = {0: 'fir', 1: 'psr', 2: 'dirbase', 3: 'db', 4: 'fsr', 5: 'epsr'}
|
|||||||
|
|
||||||
|
|
||||||
class I860:
|
class I860:
|
||||||
DATA_BASE = int(os.environ.get('EMU_DATA_BASE', '0'), 0) # .data/.bss link base (experiment: sweep)
|
# .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):
|
def __init__(self, trace=0, logf=None):
|
||||||
self.r = [0] * 32 # integer regs (r0 == 0)
|
self.r = [0] * 32 # integer regs (r0 == 0)
|
||||||
@@ -140,7 +143,11 @@ class I860:
|
|||||||
|
|
||||||
# single-precision float helpers
|
# single-precision float helpers
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def f2b(x): return struct.unpack('<I', struct.pack('<f', x))[0]
|
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
|
@staticmethod
|
||||||
def b2f(b): return struct.unpack('<f', struct.pack('<I', u32(b)))[0]
|
def b2f(b): return struct.unpack('<f', struct.pack('<I', u32(b)))[0]
|
||||||
|
|
||||||
@@ -196,28 +203,65 @@ class I860:
|
|||||||
src1 = (w >> 11) & 0x1f
|
src1 = (w >> 11) & 0x1f
|
||||||
imm = w & 0xffff
|
imm = w & 0xffff
|
||||||
|
|
||||||
# ---- loads ---- i860 integer loads are IMMEDIATE-offset (both even/odd of a
|
# ---- integer loads: ld.b (0x00/0x01), ld.s/ld.l (0x04/0x05) ----
|
||||||
# pair): EA = base(src2) + s16(offset). Indexing is done by pre-computing
|
# dest = bits20:16. ODD op = immediate s16 offset; EVEN op = register-indexed
|
||||||
# base+index into a register, then loading at offset 0. dest = bits20:16.
|
# EA = src2 + src1 (ground truth: DNC.O `ld.b -1(fp),r19`=0473ffff,
|
||||||
if op in (0x00, 0x01, 0x04, 0x05, 0x08, 0x09):
|
# VR_REMOT.S `ld.l r30(r31),r31`, DNC.S `ld.l r0(r10),r16`).
|
||||||
if _INDEXED and not (op & 1): # EVEN opcode = register-indexed (VR_REMOT.S)
|
# For op 4/5 the instr bit0 selects size: 0 = .s (16-bit), 1 = .l (32-bit)
|
||||||
ea = self.rd(src2) + self.rd(src1)
|
# (DNC.O `ld.l 52(fp),r23` = 14770035, imm = 52|1). ld.b/ld.s sign-extend
|
||||||
else:
|
# (compiled code masks with `and 0xff` right after ld.b).
|
||||||
m = 0xffff if op < 0x04 else (0xfff8 if op >= 0x08 else 0xfffc)
|
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)
|
ea = self.rd(src2) + s16(imm & m)
|
||||||
if op in (0x00, 0x01): self.wr(dest, self.mem.r8(ea)) # ld.b (byte)
|
else:
|
||||||
elif op in (0x04, 0x05): self.wr(dest, self.mem.r32(ea)) # ld.l
|
ea = self.rd(src2) + self.rd(src1)
|
||||||
else: self.fwr(dest, self.mem.r32(ea)) # fld.l
|
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
|
return
|
||||||
# ---- stores ---- i860 stores differ: source reg = src1 (bits15:11), and the
|
# ---- FP loads/stores: fld (0x08/0x09), fst (0x0a/0x0b) ----
|
||||||
# 16-bit offset is SPLIT high=bits20:16, low=bits10:0 (the src1 field displaced it).
|
# FP reg = dest field for BOTH (load target / store source; fst does NOT use
|
||||||
if op in (0x03, 0x06, 0x07, 0x0a, 0x0b):
|
# the split-store encoding -- `fst.l f20,0x3c88(r31)` = 2ff43c8a, imm=0x3c88|2).
|
||||||
m = 0xffff if op == 0x03 else (0xfff8 if op >= 0x0a else 0xfffc)
|
# ODD op = flat s16 immediate offset; EVEN op = register-indexed (src2+src1).
|
||||||
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & m)
|
# 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
|
ea = self.rd(src2) + off
|
||||||
if op == 0x03: self.mem.w8(ea, self.rd(src1) & 0xff) # st.b (byte)
|
if fl & 1: # auto-increment
|
||||||
elif op in (0x06, 0x07): self.mem.w32(ea, self.rd(src1)) # st.l
|
self.wr(src2, ea)
|
||||||
else: self.mem.w32(ea, self.frd(src1)) # fst.l
|
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
|
return
|
||||||
if op == 0x0c: # ld.c ctrl,dest
|
if op == 0x0c: # ld.c ctrl,dest
|
||||||
self.wr(dest, self.cr.get(src2, 0)); return
|
self.wr(dest, self.cr.get(src2, 0)); return
|
||||||
@@ -229,8 +273,17 @@ class I860:
|
|||||||
# ---- control transfer ----
|
# ---- control transfer ----
|
||||||
if op == 0x10: # bri src1 (indirect, delayed)
|
if op == 0x10: # bri src1 (indirect, delayed)
|
||||||
self.branch(self.rd(src1)); return
|
self.branch(self.rd(src1)); return
|
||||||
if op == 0x13: # calli src1
|
if op == 0x13: # CORE ESCAPE: sub-op in low bits
|
||||||
self.wr(1, pc + 8); self.branch(self.rd(src1)); return
|
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
|
if op == 0x1a: # br
|
||||||
self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return
|
self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return
|
||||||
if op == 0x1b: # call
|
if op == 0x1b: # call
|
||||||
@@ -267,6 +320,25 @@ class I860:
|
|||||||
else: # subs: src1 - src2
|
else: # subs: src1 - src2
|
||||||
self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b))
|
self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b))
|
||||||
return
|
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
|
if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra
|
||||||
cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f)
|
cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f)
|
||||||
b = self.rd(src2)
|
b = self.rd(src2)
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
"""Run VREND.MNG's own main() and feed it real wire captures through dN_receive.
|
||||||
|
|
||||||
|
This replaces the per-handler harness (emu_replay) with the AUTHENTIC path: the
|
||||||
|
firmware's main() does its own startup (newBytes pools, dN_mynode/dN_nodes) and
|
||||||
|
enters remote_velocirender()'s while(1) { dN_receive(); dispatch; } loop. We only
|
||||||
|
hook the transputer-LINK primitives (they'd block on real hardware); everything
|
||||||
|
else -- init, do_init, dispatch, handlers, allocator -- is the firmware's own code.
|
||||||
|
|
||||||
|
Firmware addresses (source-matched disassembly, VREND.C + VR_REMOT.C):
|
||||||
|
main 0xf0403f10 (processor-0 path; _processorId in .data)
|
||||||
|
bla 0xf04285f0 busy-wait (also tail-called with manual r1)
|
||||||
|
dN_mynode 0xf0402570 *arg = my node id -> 2 (replying node)
|
||||||
|
dN_nodes 0xf0402610 *arg = total nodes -> 3 (1 view)
|
||||||
|
dN_receive 0xf0402450 (&client,&bytes,buf,flag) <- wire frames
|
||||||
|
remote_velocirender 0xf040eea0
|
||||||
|
velocirender_init 0xf040e300 (called by the loop on action 0)
|
||||||
|
do_init 0xf040afb0 (deferred heavy init)
|
||||||
|
|
||||||
|
Usage: python emu_main.py [capture.raw.bin] [--max-cmds N] [--verbose]
|
||||||
|
"""
|
||||||
|
import sys, os, struct
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha')
|
||||||
|
import emu860
|
||||||
|
from emu_replay import parse_capture, MNG, BOOT_ACTIONS
|
||||||
|
from vrboard import A
|
||||||
|
|
||||||
|
MAIN = 0xf0403f10
|
||||||
|
BLA = 0xf04285f0
|
||||||
|
DN_MYNODE = 0xf0402570
|
||||||
|
DN_NODES = 0xf0402610
|
||||||
|
DN_RECEIVE = 0xf0402450
|
||||||
|
V_INIT = 0xf040e300
|
||||||
|
DO_INIT = 0xf040afb0
|
||||||
|
|
||||||
|
ANAME = {int(a): a.name for a in A}
|
||||||
|
|
||||||
|
|
||||||
|
class MainRunner:
|
||||||
|
def __init__(self, cap, verbose=False, max_cmds=None):
|
||||||
|
self.cpu = emu860.I860(trace=0)
|
||||||
|
self.cpu.load_mng(MNG)
|
||||||
|
self.cpu.map_control(); self.cpu.map_board()
|
||||||
|
# Transputer-boot environment: the monitor pokes the processor id into the
|
||||||
|
# first .data word before starting the app (file ships garbage there).
|
||||||
|
# main() reads [0x1000] (_processorId) and spins the processor-1 path if != 0.
|
||||||
|
self.cpu.mem.w32(0x1000, 0)
|
||||||
|
# Monitor-provided MEMORY environment (we are the transputer/monitor).
|
||||||
|
# The page allocator (0xf04350b8) reads region descriptors from the shared
|
||||||
|
# control block: [0x23ae8]=0xfffff800 (DRAM bank 0 @0x08000000) and
|
||||||
|
# [0x23c84]=0xfffff80c (bank 1 @0x0C000000) -- set by 0xf0434e44, populated
|
||||||
|
# by the monitor. Layout: desc={+0: first extent, +8: free bytes};
|
||||||
|
# extent (in DRAM) ={+0: next extent, +8: extent size}.
|
||||||
|
RAM0, RAM1, RSIZE = 0x08000000, 0x0C000000, 0x01000000 # 16MB per bank
|
||||||
|
for desc, base in ((0xfffff800, RAM0), (0xfffff80c, RAM1)):
|
||||||
|
self.cpu.mem.w32(desc + 0, base)
|
||||||
|
self.cpu.mem.w32(desc + 4, 0)
|
||||||
|
self.cpu.mem.w32(desc + 8, RSIZE)
|
||||||
|
self.cpu.mem.w32(base + 0, 0)
|
||||||
|
self.cpu.mem.w32(base + 4, 0)
|
||||||
|
self.cpu.mem.w32(base + 8, RSIZE)
|
||||||
|
# The bss pointers to those descriptors are normally stored by the meminit
|
||||||
|
# routine 0xf0434df4 (monitor start path we don't run) -- store them ourselves.
|
||||||
|
self.cpu.mem.w32(0x23ae8, 0xfffff800)
|
||||||
|
self.cpu.mem.w32(0x23c84, 0xfffff80c)
|
||||||
|
# malloc's sbrk break pointers at [0x1ecc0]/[0x1ecc4]: file ships garbage
|
||||||
|
# (startup-written slots, like _processorId). Zero => sbrk lazy-inits itself
|
||||||
|
# through the (hooked) page allocator.
|
||||||
|
self.cpu.mem.w32(0x1ecc0, 0)
|
||||||
|
self.cpu.mem.w32(0x1ecc4, 0)
|
||||||
|
# Shared "uncached block" pointers at [0x14fc4]/[0x14fc8]/[0x14fcc]
|
||||||
|
# (transputer-visible communication blocks with embedded spinlocks at +0x18,
|
||||||
|
# used by the link/console paths). Monitor-provided; point them at fresh
|
||||||
|
# zeroed RAM so the locks start released.
|
||||||
|
self.cpu.mem.w32(0x14fc4, 0x00300000)
|
||||||
|
self.cpu.mem.w32(0x14fc8, 0x00301000)
|
||||||
|
self.cpu.mem.w32(0x14fcc, 0x00302000)
|
||||||
|
# Page-directory base used by the allocator's VM bookkeeping ([0x22590]);
|
||||||
|
# tables live at 0x76000000 on the real board. We don't translate (flat
|
||||||
|
# memory), the firmware just needs somewhere writable to keep them.
|
||||||
|
self.cpu.mem.w32(0x22590, 0x76000000)
|
||||||
|
# Feed EVERYTHING through the firmware, including the 860-boot actions:
|
||||||
|
# cap7 starts with args860/code860/data860/bss860 -- the host DOWNLOADS an
|
||||||
|
# extra i860 module (the PAZ/sfx layer) that installs the runtime handler
|
||||||
|
# tables and system objects. Skipping them leaves those tables empty.
|
||||||
|
# (hspcode is transputer-side; keep it out.)
|
||||||
|
cmds = [(a, p) for a, p in parse_capture(cap) if a != A.hspcode]
|
||||||
|
if max_cmds: cmds = cmds[:max_cmds]
|
||||||
|
self.queue = cmds
|
||||||
|
self.qi = 0
|
||||||
|
self.verbose = verbose
|
||||||
|
self.done = {}
|
||||||
|
self.stubbed = {} # discovered link primitives -> hit count
|
||||||
|
# Page allocation: the firmware's own allocator (0xf04350b8) maintains the
|
||||||
|
# board's two-level VM page tables, which the MONITOR bootstraps (pre-mapped
|
||||||
|
# DRAM PTEs at 0x76000000, phys->virt bias at [0x23ca0]) -- replicating that
|
||||||
|
# faithfully is Tier-2 (run VRENDMON on an emulated T425). Until then we hook
|
||||||
|
# the allocator core with a bump allocator over the DRAM banks; callers are:
|
||||||
|
# r16 = region selector (-1 = any, 0 = bank0, 1 = bank1), r17 = page count.
|
||||||
|
# Node addresses are opaque to the scene/render logic, so the IGC stream is
|
||||||
|
# unaffected by allocator layout.
|
||||||
|
self.heap = [0x08010000, 0x0c010000] # small offset: skip extent headers
|
||||||
|
self.hooks = {
|
||||||
|
BLA: self.h_ret,
|
||||||
|
DN_MYNODE: lambda c: self.h_writeback(c, 2),
|
||||||
|
DN_NODES: lambda c: self.h_writeback(c, 3),
|
||||||
|
DN_RECEIVE: self.h_receive,
|
||||||
|
0xf04350b8: self.h_allocpages,
|
||||||
|
# virt->phys page translator (walks the monitor's page tables, which we
|
||||||
|
# don't build): flat memory => identity. Returns the PTE for r16's page.
|
||||||
|
0xf0434d90: lambda c: self.h_ret(c, c.rd(16) & ~0xfff),
|
||||||
|
# reply path (replying = me==2): 0xf040ae90 = reply wrapper (tail-called
|
||||||
|
# from the loop with r1 = loop top), 0xf0402370 = dN_send-style link
|
||||||
|
# primitive. We log the reply value and return.
|
||||||
|
0xf040ae90: self.h_reply,
|
||||||
|
0xf0402370: self.h_ret,
|
||||||
|
# outgoing-message writer (same shared_cntl+0x18 + mutex shape as the
|
||||||
|
# send primitive; do_init calls it with message-type words like 0x30
|
||||||
|
# whose type-nibble has no local jump-table entry). Log + drop.
|
||||||
|
0xf04030c8: self.h_outmsg,
|
||||||
|
# Spinlock acquire/release (i860 lock/unlock bus primitives guarding
|
||||||
|
# shared blocks). Single-threaded emulation: no-op both, so a skipped
|
||||||
|
# release inside a hooked path can never deadlock us.
|
||||||
|
0xf042bdf0: self.h_ret,
|
||||||
|
0xf042be48: self.h_ret,
|
||||||
|
# Dispatch-tail chain (create's table sends types 2-5 to 0xf040bd0c =
|
||||||
|
# the `or 0xece8,r31,r31` inside new_node's dispatch, reusing its
|
||||||
|
# per-type init table). The `or` needs r31=0 on entry; a jump arrives
|
||||||
|
# with r31 = the entry address itself. Zero it and execute normally.
|
||||||
|
0xf040bd0c: self.h_chainfix,
|
||||||
|
}
|
||||||
|
self.replies = []
|
||||||
|
self.outmsgs = []
|
||||||
|
|
||||||
|
def h_chainfix(self, cpu):
|
||||||
|
if cpu.rd(31) == cpu.pc: # arrived via a table jump (r31 = entry addr)
|
||||||
|
idx4 = cpu.rd(30) # (type-2)*4, set by the dispatching code
|
||||||
|
tgt = cpu.mem.r32(0xece8 + idx4)
|
||||||
|
cpu.wr(31, 0)
|
||||||
|
# entry pointing back at the chain = "no per-type init" -> common tail
|
||||||
|
cpu.pc = 0xf040bd1c if tgt == 0xf040bd0c else tgt
|
||||||
|
return True
|
||||||
|
cpu.step() # normal fall-through execution of the `or`
|
||||||
|
return True
|
||||||
|
|
||||||
|
def h_outmsg(self, cpu):
|
||||||
|
self.outmsgs.append((cpu.rd(16), cpu.rd(17)))
|
||||||
|
return self.h_ret(cpu)
|
||||||
|
|
||||||
|
def h_reply(self, cpu):
|
||||||
|
self.replies.append(cpu.rd(16))
|
||||||
|
return self.h_ret(cpu)
|
||||||
|
|
||||||
|
def h_allocpages(self, cpu):
|
||||||
|
sel = cpu.rd(16)
|
||||||
|
bank = 1 if sel == 1 else 0
|
||||||
|
size = (cpu.rd(17) & 0xffff) << 12
|
||||||
|
ptr = self.heap[bank]
|
||||||
|
self.heap[bank] += size
|
||||||
|
return self.h_ret(cpu, ptr)
|
||||||
|
|
||||||
|
# ---- hook helpers: emulate a called function returning ----
|
||||||
|
def h_ret(self, cpu, ret=None):
|
||||||
|
if ret is not None: cpu.wr(16, ret)
|
||||||
|
cpu.pc = cpu.rd(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def h_writeback(self, cpu, val):
|
||||||
|
cpu.mem.w32(cpu.rd(16), val)
|
||||||
|
return self.h_ret(cpu)
|
||||||
|
|
||||||
|
def h_receive(self, cpu):
|
||||||
|
if self.qi >= len(self.queue):
|
||||||
|
return 'done'
|
||||||
|
act, pl = self.queue[self.qi]; self.qi += 1
|
||||||
|
buf = cpu.rd(18)
|
||||||
|
cpu.mem.w32(buf, act)
|
||||||
|
cpu.mem.load_blob(buf + 4, pl or b'')
|
||||||
|
cpu.mem.w32(cpu.rd(16), 0) # *client = 0
|
||||||
|
cpu.mem.w32(cpu.rd(17), 4 + len(pl)) # *receive_bytes
|
||||||
|
n = ANAME.get(act, act)
|
||||||
|
self.done[n] = self.done.get(n, 0) + 1
|
||||||
|
if self.verbose or self.qi % 2000 == 0 or self.qi <= 20:
|
||||||
|
print(f" [{self.qi:5}/{len(self.queue)}] {n:<22} len={len(pl):6} "
|
||||||
|
f"steps={cpu.steps:>12,} igc={len(cpu.igc)}")
|
||||||
|
return self.h_ret(cpu)
|
||||||
|
|
||||||
|
def run(self, budget=2_000_000_000):
|
||||||
|
cpu = self.cpu
|
||||||
|
cpu.wr(1, cpu.RET_SENTINEL); cpu.wr(2, 0x000c0000)
|
||||||
|
cpu.wr(16, 0); cpu.wr(17, 0) # argc, argv
|
||||||
|
cpu.pc = MAIN
|
||||||
|
watch = {V_INIT: 'velocirender_init', DO_INIT: 'do_init'}
|
||||||
|
while cpu.steps < budget:
|
||||||
|
pc = cpu.pc
|
||||||
|
if pc == cpu.RET_SENTINEL:
|
||||||
|
print(f"main RETURNED after {cpu.steps:,} steps"); break
|
||||||
|
h = self.hooks.get(pc)
|
||||||
|
if h:
|
||||||
|
r = h(cpu)
|
||||||
|
if r == 'done':
|
||||||
|
print(f"\ncapture exhausted after {self.qi} commands, "
|
||||||
|
f"{cpu.steps:,} steps"); break
|
||||||
|
continue
|
||||||
|
if pc in watch:
|
||||||
|
print(f" >> {watch.pop(pc)} entered (steps={cpu.steps:,})")
|
||||||
|
if not cpu.step():
|
||||||
|
print(f"\nSTOP: {cpu.stopmsg}")
|
||||||
|
import dis860
|
||||||
|
for tpc, tw in cpu.tail[-12:]:
|
||||||
|
m, ops = dis860.decode(tw, tpc)
|
||||||
|
print(f" {tpc:#010x}: {tw:08x} {m} {ops}")
|
||||||
|
break
|
||||||
|
return cpu
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||||
|
cap = args[0] if args else r'C:\VWE\TeslaRel410\dpl3-revive\patha\cap7.raw.bin'
|
||||||
|
verbose = '--verbose' in sys.argv
|
||||||
|
mc = None
|
||||||
|
if '--max-cmds' in sys.argv:
|
||||||
|
mc = int(sys.argv[sys.argv.index('--max-cmds') + 1])
|
||||||
|
r = MainRunner(cap, verbose=verbose, max_cmds=mc)
|
||||||
|
print(f"{os.path.basename(cap)}: {len(r.queue)} wire commands queued")
|
||||||
|
cpu = r.run()
|
||||||
|
print(f"\nreplayed: {r.done}")
|
||||||
|
print(f"IGC writes: {len(cpu.igc)} board-log entries: {len(cpu.board_log)}")
|
||||||
|
if cpu.igc:
|
||||||
|
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'igc_stream.bin')
|
||||||
|
with open(out, 'wb') as f:
|
||||||
|
for addr, val in cpu.igc:
|
||||||
|
f.write(struct.pack('<II', addr, val))
|
||||||
|
print(f"IGC stream -> {out}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user