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:
Cyd
2026-07-15 11:26:10 -05:00
co-authored by Claude Opus 4.8
parent f5e9ae987d
commit a688cc1c30
3 changed files with 381 additions and 38 deletions
+48 -15
View File
@@ -52,11 +52,16 @@ def dec_fp(w):
return (name + fp_suffix(w), f"{FREG[src1]},{FREG[src2]},{FREG[dest]}")
# ---- primary decode ----
# immediate-form rule: for load/store (0x00-0x0b) and arith/logic (0x20-0x3f),
# the ODD opcode of each pair is the 16-bit immediate/const form.
MEM = {0x00: "ld.s", 0x01: "ld.s", 0x04: "ld.l", 0x05: "ld.l",
0x06: "st.l", 0x07: "st.l", 0x08: "fld.l", 0x09: "fld.l",
0x0a: "fst.l", 0x0b: "fst.l"}
# load/store encoding (ground truth: AS860 .S<->.O pairs + DNC.O):
# - integer loads (0x00/0x01 ld.b, 0x04/0x05 ld.s|ld.l): dest=bits20:16;
# ODD = 16-bit immediate offset, EVEN = register-indexed EA=src2+src1.
# op 4/5 size flag = instr bit0 (0=.s 16-bit, 1=.l 32-bit).
# - 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",
0x28: "shl", 0x2a: "shr", 0x2c: "shrd", 0x2e: "shra",
0x30: "and", 0x32: "andh", 0x34: "andnot", 0x36: "andnoth",
@@ -70,15 +75,32 @@ def decode(w, addr):
imm = w & 0xffff
# loads/stores
if op in MEM:
m = MEM[op]
off = s16(imm & 0xfffc) if 'l' in m else s16(imm)
if m.startswith('f'):
rd = FREG[dest]
if op in (0x00, 0x01, 0x04, 0x05): # integer loads
m = "ld.b" if op < 0x04 else ("ld.l" if (w & 1) else "ld.s")
if op & 1:
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:
rd = REG[dest]
if m.startswith('st') or m.startswith('fst'):
return m, f"{rd},{off:#x}({REG[src2]})"
return m, f"{off:#x}({REG[src2]}),{rd}"
ea = f"{REG[src1]}({REG[src2]}){pp}"
if op < 0x0a:
return m, f"{ea},{FREG[dest]}"
return m, f"{FREG[dest]},{ea}"
if op == 0x02:
return "ixfr", f"{REG[src1]},{FREG[dest]}"
if op == 0x0c:
@@ -92,9 +114,17 @@ def decode(w, addr):
if op == 0x12:
return dec_fp(w)
if op == 0x13:
# core escape: sub-op in low bits; calli common
if (w & 0x7ff) == 0 or True:
# core escape: sub-op in low 5 bits (1=lock, 2=calli, 4=intovr, 7=unlock)
sub = w & 0x1f
if sub == 0x02:
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):
base = "btne" if op < 0x16 else "bte"
broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)
@@ -110,6 +140,9 @@ def decode(w, addr):
if op in (0x18, 0x19):
off = s16(imm & 0xfff8)
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:
m = ARITH[op & 0x3e]
if op & 1: # immediate form
+95 -23
View File
@@ -109,7 +109,10 @@ 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)
# .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)
@@ -140,7 +143,11 @@ class I860:
# single-precision float helpers
@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
def b2f(b): return struct.unpack('<f', struct.pack('<I', u32(b)))[0]
@@ -196,28 +203,65 @@ class I860:
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)
# ---- 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)
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
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
# ---- 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)
# ---- 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 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
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 == 0x0c: # ld.c ctrl,dest
self.wr(dest, self.cr.get(src2, 0)); return
@@ -229,8 +273,17 @@ class I860:
# ---- 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 == 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
@@ -267,6 +320,25 @@ class I860:
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)
+238
View File
@@ -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()