Files
TeslaRel410/emulator/firmware-decomp/emu_main.py
T

234 lines
9.2 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=0xf042ee00, # runtime int divide (r22 = r22/r23)
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
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
self.heap = [0x08010000, 0x0c010000] # bump allocator over the DRAM banks
# ---- 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()