- 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>
251 lines
10 KiB
Python
251 lines
10 KiB
Python
"""Run the render firmware's own main() and feed it real wire captures through
|
|
dN_receive.
|
|
|
|
This is 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 hook only the transputer-LINK
|
|
primitives (they'd block on real hardware) and the page-allocation core
|
|
(whose VM page tables the monitor builds -- Tier-2 = run VRENDMON on a T425);
|
|
everything else -- init, do_init, dispatch, handlers, allocator wrappers --
|
|
is the firmware's own code.
|
|
|
|
Two builds are mapped:
|
|
sda4 -- sda4/RPLIVE/VREND.MNG (text 0x39ec0), our archive copy.
|
|
capfw7 -- capfw7.mng (text 0x31440), extracted from cap7's own wire-boot
|
|
preamble (extract_capfw.py): the EXACT build the game ran, with
|
|
an EXTENDED action set (42 actions vs the SDK enum's 24).
|
|
|
|
Usage: python emu_main.py [capture.raw.bin] [--fw sda4|capfw7]
|
|
[--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
|
|
from vrboard import A
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
MAPS = {
|
|
'sda4': dict(
|
|
fw=r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG',
|
|
main=0xf0403f10, bla=0xf04285f0,
|
|
mynode=0xf0402570, nodes=0xf0402610, receive=0xf0402450,
|
|
vinit=0xf040e300, doinit=0xf040afb0,
|
|
reply=0xf040ae90, sendprim=0xf0402370, putchar=0xf04030c8,
|
|
alloc=0xf04350b8, v2p=0xf0434d90,
|
|
lockacq=0xf042bdf0, lockrel=0xf042be48,
|
|
# create's table routes types 2-5 through the dispatch tail at `chain`
|
|
# (r31=0-on-entry semantics); new_node's init table at chain_tbl.
|
|
chain=0xf040bd0c, chain_tbl=0xece8, chain_tail=0xf040bd1c,
|
|
seeds=[
|
|
(0x1000, 0), # _processorId (monitor-written slot)
|
|
(0x1ecc0, 0), (0x1ecc4, 0), # sbrk break ptrs -> lazy-init
|
|
(0x14fc4, 0x00300000), # shared uncached blocks (locks at +0x18)
|
|
(0x14fc8, 0x00301000),
|
|
(0x14fcc, 0x00302000),
|
|
],
|
|
),
|
|
'capfw7': dict(
|
|
fw=os.path.join(_HERE, 'capfw7.mng'),
|
|
main=0xf0403f80, bla=0xf0420c00,
|
|
mynode=0xf04025e0, nodes=0xf0402680, receive=0xf04024c0,
|
|
vinit=0xf040c5d8, doinit=0xf0409440,
|
|
reply=0xf0409320, sendprim=None, putchar=None,
|
|
alloc=0xf042c628, v2p=0xf042c300,
|
|
lockacq=0xf0423360, lockrel=0xf04233b8,
|
|
chain=None, chain_tbl=None, chain_tail=None,
|
|
# intdiv hook retired 2026-07-15: the exact MAME-validated pipeline model
|
|
# (PFAM/PFMAM families, FLAGM->MPIPE, sp/rp bit swap, fdest bypass) makes
|
|
# the firmware's own Newton-Raphson divide compute correctly.
|
|
intdiv=None,
|
|
igcwait=0xf0421510, # post-flush IGC drain poll (ld.l 0(r16))
|
|
seeds=[
|
|
(0x1000, 0), # _processorId
|
|
],
|
|
),
|
|
}
|
|
|
|
ANAME = {int(a): a.name for a in A}
|
|
|
|
|
|
class MainRunner:
|
|
def __init__(self, cap, fw='capfw7', verbose=False, max_cmds=None):
|
|
self.map = MAPS[fw]
|
|
self.cpu = emu860.I860(trace=0)
|
|
self.cpu.load_mng(self.map['fw'])
|
|
self.cpu.map_control(); self.cpu.map_board()
|
|
for addr, val in self.map['seeds']:
|
|
self.cpu.mem.w32(addr, val)
|
|
# The 860-boot preamble (args860/code860/data860/bss860/hspcode) is
|
|
# consumed by the MONITOR on real hardware, not the app -- the capfw7
|
|
# app's dispatch rejects those actions (prints the message and dies).
|
|
# We are the monitor and the image is already loaded: skip them.
|
|
BOOT = {A.hspcode, A.code860, A.data860, A.bss860, A.args860}
|
|
cmds = [(a, p) for a, p in parse_capture(cap) if a not in BOOT]
|
|
if max_cmds: cmds = cmds[:max_cmds]
|
|
self.queue = cmds
|
|
self.qi = 0
|
|
self.verbose = verbose
|
|
self.done = {}
|
|
self.replies = []
|
|
self.console = []
|
|
m = self.map
|
|
self.hooks = {
|
|
m['bla']: self.h_ret,
|
|
m['mynode']: lambda c: self.h_writeback(c, 2),
|
|
m['nodes']: lambda c: self.h_writeback(c, 3),
|
|
m['receive']: self.h_receive,
|
|
m['reply']: self.h_reply,
|
|
m['alloc']: self.h_allocpages,
|
|
m['v2p']: lambda c: self.h_ret(c, c.rd(16) & ~0xfff),
|
|
m['lockacq']: self.h_ret,
|
|
m['lockrel']: self.h_ret,
|
|
}
|
|
if m['sendprim']: self.hooks[m['sendprim']] = self.h_ret
|
|
if m['putchar']: self.hooks[m['putchar']] = self.h_console
|
|
if m['chain']: self.hooks[m['chain']] = self.h_chainfix
|
|
if m.get('intdiv'): self.hooks[m['intdiv']] = self.h_intdiv
|
|
if m.get('igcwait'): self.hooks[m['igcwait']] = self.h_igcwait
|
|
self.heap = [0x08010000, 0x0c010000] # bump allocator over the DRAM banks
|
|
|
|
def h_igcwait(self, cpu):
|
|
# Post-flush drain wait: the firmware polls [page] until the IGC writes a
|
|
# NONZERO completion status after consuming it (loop at 0xf0421504: exits
|
|
# the per-page wait when [r16] != 0). We are the IGC: complete instantly,
|
|
# and RESET the page's write index (+0x7f8) so the page is reusable --
|
|
# otherwise pages fill at 0x7f0 across frames and enqueueing silently dies.
|
|
page = cpu.rd(16)
|
|
cpu.mem.w32(page, 1)
|
|
cpu.mem.w32(page + 0x7f8, 0)
|
|
cpu.step()
|
|
return True
|
|
|
|
def h_intdiv(self, cpu):
|
|
# Runtime integer divide (r22 = r22/r23, result via ftrunc+fxfr): the
|
|
# compiled Newton-Raphson relies on exact dual-op pipeline timing our
|
|
# functional FP model doesn't reproduce yet -- compute exactly instead.
|
|
a = cpu.rd(22); b = cpu.rd(23)
|
|
if a & 0x80000000: a -= 1 << 32
|
|
if b & 0x80000000: b -= 1 << 32
|
|
q = abs(a) // abs(b) if b else 0
|
|
if (a < 0) != (b < 0): q = -q
|
|
cpu.wr(22, q & 0xffffffff)
|
|
cpu.pc = cpu.rd(1)
|
|
return True
|
|
|
|
# ---- 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_reply(self, cpu):
|
|
self.replies.append(cpu.rd(16))
|
|
return self.h_ret(cpu)
|
|
|
|
def h_console(self, cpu):
|
|
self.console.append(cpu.rd(16) & 0xff)
|
|
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)
|
|
|
|
def h_chainfix(self, cpu):
|
|
if cpu.rd(31) == cpu.pc: # arrived via a table jump
|
|
idx4 = cpu.rd(30)
|
|
tgt = cpu.mem.r32(self.map['chain_tbl'] + idx4)
|
|
cpu.wr(31, 0)
|
|
cpu.pc = self.map['chain_tail'] if tgt == self.map['chain'] else tgt
|
|
return True
|
|
cpu.step()
|
|
return True
|
|
|
|
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
|
|
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 % 1000 == 0 or self.qi <= 8:
|
|
print(f" [{self.qi:5}/{len(self.queue)}] {str(n):<22} len={len(pl):6} "
|
|
f"steps={cpu.steps:>13,} igc={len(cpu.igc)} board={len(cpu.board_log)}",
|
|
flush=True)
|
|
return self.h_ret(cpu)
|
|
|
|
def start(self):
|
|
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 = self.map['main']
|
|
return cpu
|
|
|
|
def run(self, budget=2_000_000_000):
|
|
cpu = self.start()
|
|
watch = {self.map['vinit']: 'velocirender_init',
|
|
self.map['doinit']: '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:,})", flush=True)
|
|
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'
|
|
fw = 'capfw7'
|
|
if '--fw' in sys.argv:
|
|
fw = sys.argv[sys.argv.index('--fw') + 1]
|
|
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, fw=fw, verbose=verbose, max_cmds=mc)
|
|
print(f"{os.path.basename(cap)}: {len(r.queue)} wire commands queued (fw={fw})")
|
|
cpu = r.run()
|
|
print(f"\nreplayed: {r.done}")
|
|
print(f"replies: {r.replies[:20]}")
|
|
print(f"IGC writes: {len(cpu.igc)} board-log entries: {len(cpu.board_log)}")
|
|
if cpu.board_log:
|
|
out = os.path.join(_HERE, 'igc_stream.bin')
|
|
with open(out, 'wb') as f:
|
|
for k, a, v in cpu.board_log:
|
|
f.write(struct.pack('<BII', 1 if k == 'w' else 0, a, v))
|
|
print(f"board stream -> {out}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|