From d9cfd7407074fe23a8fb2988203be8239547350c Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 15 Jul 2026 13:20:35 -0500 Subject: [PATCH] i860 emu: pipelined FP unit + dual-ops, adds CC sign rule, flush op; version-matched capfw7 replay BUILDS THE SCENE - Pipelined FP model: 3-stage adder / 2-stage(double) multiplier pipes with KR/KI/T registers; pfadd/pfsub/pfmul honor the P-bit; PFAM(0x00-0x0f)/ PFSM(0x10-0x1f) dual-operations implemented from the validated mnemonic grammar (r2pt, ra1p2, m12apm, ...: M-unit/A-unit routing + K/T loading). Survives the capture build's Newton-Raphson boot math. - adds CC = result-negative (sign rule); addu stays carry. Fixes printf's pad-to-width loops and the exit-walk's empty-table branch. - op 0x0d = flush (cache-line flush, store-format split offset, bit0 auto-increment): boot cache-flush loops. - fix/ftrunc: NaN/inf/out-of-range -> IEEE indefinite instead of raising. - emu_main: per-build address maps (sda4 + capfw7); capfw7 map fully derived (main 0xf0403f80, remote_velocirender 0xf040cee0, dN_receive 0xf04024c0, velocirender_init 0xf040c5d8, do_init 0xf0409440, reply 0xf0409320, alloc core 0xf042c628, v2p 0xf042c300, locks 0xf0423360/0xf04233b8). The 860-boot preamble is skipped for capfw7 (the monitor's job -- the app build rejects those actions); the capture build dispatches 42 actions, explaining the unknown actions 29/42 in cap7. - run_to_draw: version-matched marathon from the TRUE ENTRY 0xf0400000 with the boot handshake ([0xfffff728]) satisfied -- crt0/pools/main are all the firmware's own startup, eliminating the seed-slot guesswork. STATE: authentic boot + init (reply=4) + scene build running: 152 commands (31 create, 59 flush, dcs_link, list_add, texmaps) processed and replied before the time cap. First draw_scene sits ~1400 commands ahead. Co-Authored-By: Claude Opus 4.8 --- emulator/firmware-decomp/emu860.py | 111 ++++++++- emulator/firmware-decomp/emu_main.py | 284 +++++++++++------------- emulator/firmware-decomp/run_to_draw.py | 33 +-- 3 files changed, 252 insertions(+), 176 deletions(-) diff --git a/emulator/firmware-decomp/emu860.py b/emulator/firmware-decomp/emu860.py index e078849..c11ec5c 100644 --- a/emulator/firmware-decomp/emu860.py +++ b/emulator/firmware-decomp/emu860.py @@ -263,6 +263,13 @@ class I860: for i in range(size // 4): self.mem.w32(ea + i * 4, self.frd(b0 + i)) return + if op == 0x0d: # flush #const(src2)[++] (cache-line flush) + # Store-format split offset; cache line = 32B so low 5 bits are free: + # bit0 = auto-increment (src2 <- src2+offset). No memory effect for us. + off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff) + if off & 1: + self.wr(src2, self.rd(src2) + s16(off & 0xffe0)) + return if op == 0x0c: # ld.c ctrl,dest self.wr(dest, self.cr.get(src2, 0)); return if op == 0x0e: # st.c src1,ctrl @@ -311,10 +318,11 @@ class I860: # 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 + if base == 0x20: # addu: CC = carry out 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 == 0x24: # adds: CC = result negative + s = i32(a) + i32(b); self.wr(dest, s) + self.set_cc(bool(u32(s) & 0x80000000)) elif base == 0x22: # subu: src1 - src2 self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b)) else: # subs: src1 - src2 @@ -386,17 +394,93 @@ class I860: else: self.fwr(reg, self.f2b(val)) + # ---- pipelined FP unit state (functional model, no cycle timing) ---- + # Adder pipe = 3-stage; multiplier pipe = 2-stage double / 3-stage single; + # KR/KI/T = the dual-operation constant/transfer registers. + def _fp_pipes(self): + if not hasattr(self, '_apipe'): + self._apipe = [0.0, 0.0, 0.0] + self._mpipe = [0.0, 0.0, 0.0] + self._kr = 0.0 + self._ki = 0.0 + self._t = 0.0 + return self._apipe, self._mpipe + + def _padv(self, pipe, val, depth): + out = pipe[depth - 1] + for i in range(depth - 1, 0, -1): + pipe[i] = pipe[i - 1] + pipe[0] = val + return out + + # PFAM/PFSM dual-op routing, decoded from the validated mnemonic grammar + # (AS860 corpus: r2p1, r2pt, r2ap1, r2apt, i2p1.., rat1p2, m12apm, ra1p2, + # m12tpm, m12tpa, iat1p2, ia1p2 ...): + # M-unit: r2 = KR*src2 | i2 = KI*src2 | m12 = src1*src2 | + # ra = KR*Ares | ia = KI*Ares ('t' suffix: T <- Mres) + # A-unit: p1 = Mres+src1 | pt = Mres+T | ap1 = Ares+src1 | apt = Ares+T | + # apm = Ares+Mres | tpm = T+Mres | tpa = T+Ares | 1p2 = src1+src2 + # K-load: if src1 is unused by the routing, KR (r-forms) / KI (i-forms) + # is loaded from src1. sub 0x10-0x1f = PFSM (A-unit subtracts). + # Encoded per DPC as (m1,m2, a1,a2, kload, tload) with operand tags: + # s1 s2 kr ki t am (adder-pipe result) mm (mul-pipe result) + _DUAL = { + 0x0: ('kr','s2', 's1','mm', None, False), # r2p1 + 0x1: ('kr','s2', 't', 'mm', 'kr', False), # r2pt + 0x2: ('kr','s2', 's1','am', None, False), # r2ap1 + 0x3: ('kr','s2', 't', 'am', 'kr', False), # r2apt + 0x4: ('ki','s2', 's1','mm', None, False), # i2p1 + 0x5: ('ki','s2', 't', 'mm', 'ki', False), # i2pt + 0x6: ('ki','s2', 's1','am', None, False), # i2ap1 + 0x7: ('ki','s2', 't', 'am', 'ki', False), # i2apt + 0x8: ('kr','am', 's1','s2', None, True), # rat1p2 (T <- Mres) + 0x9: ('s1','s2', 'am','mm', None, False), # m12apm + 0xa: ('kr','am', 's1','s2', None, False), # ra1p2 + 0xb: ('s1','s2', 't', 'am', None, False), # m12tpa (per grammar) + 0xc: ('ki','am', 's1','s2', None, True), # iat1p2 (T <- Mres) + 0xd: ('s1','s2', 't', 'mm', None, False), # m12tpm + 0xe: ('ki','am', 's1','s2', None, False), # ia1p2 + 0xf: ('s1','s2', 't', 'am', None, False), # m12tpa + } + 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) + pbit = (w >> 10) & 1 # P: pipelined + if sub < 0x20: # PFAM (0x00-0x0f) / PFSM (0x10-0x1f) + ap, mp = self._fp_pipes() + mdepth = 2 if sp else 3 + am_out = ap[2] # retiring adder result + mm_out = mp[mdepth - 1] # retiring multiplier result + v = {'s1': self.rdf(src1, sp), 's2': self.rdf(src2, sp), + 'kr': self._kr, 'ki': self._ki, 't': self._t, + 'am': am_out, 'mm': mm_out} + m1, m2, a1, a2, kload, tload = self._DUAL[sub & 0xf] + newm = v[m1] * v[m2] + newa = (v[a1] - v[a2]) if (sub & 0x10) else (v[a1] + v[a2]) + if kload == 'kr': self._kr = v['s1'] + elif kload == 'ki': self._ki = v['s1'] + if tload: self._t = mm_out + self._padv(ap, newa, 3) + self._padv(mp, newm, mdepth) + self.wrf(dest, am_out, rp) # fdest <- adder pipe output + return + if sub == 0x20: # fmul / pfmul + r = self.rdf(src1, sp) * self.rdf(src2, sp) + if pbit: + ap, mp = self._fp_pipes() + self.wrf(dest, self._padv(mp, r, 2 if sp else 3), rp) + else: + self.wrf(dest, r, rp) + elif sub in (0x30, 0x31): # fadd/fsub / pfadd/pfsub + a = self.rdf(src1, sp); b = self.rdf(src2, sp) + r = (a - b) if sub == 0x31 else (a + b) + if pbit: + ap, mp = self._fp_pipes() + self.wrf(dest, self._padv(ap, r, 3), rp) + else: + self.wrf(dest, r, rp) elif sub == 0x33: # famov (move src1, precision-convert) self.wrf(dest, self.rdf(src1, sp), rp) elif sub == 0x22: # frcp (reciprocal of src2) @@ -408,7 +492,12 @@ class I860: 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)) + if a != a or a in (float('inf'), float('-inf')): + iv = 0x80000000 # IEEE indefinite + elif abs(a) > 0x7fffffff: + iv = 0x80000000 + else: + 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)) diff --git a/emulator/firmware-decomp/emu_main.py b/emulator/firmware-decomp/emu_main.py index 907b606..de0a025 100644 --- a/emulator/firmware-decomp/emu_main.py +++ b/emulator/firmware-decomp/emu_main.py @@ -1,163 +1,107 @@ -"""Run VREND.MNG's own main() and feed it real wire captures through dN_receive. +"""Run the render firmware'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. +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. -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) +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] [--max-cmds N] [--verbose] +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, MNG, BOOT_ACTIONS +from emu_replay import parse_capture from vrboard import A -MAIN = 0xf0403f10 -BLA = 0xf04285f0 -DN_MYNODE = 0xf0402570 -DN_NODES = 0xf0402610 -DN_RECEIVE = 0xf0402450 -V_INIT = 0xf040e300 -DO_INIT = 0xf040afb0 +_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, + seeds=[ + (0x1000, 0), # _processorId + ], + ), +} ANAME = {int(a): a.name for a in A} class MainRunner: - def __init__(self, cap, verbose=False, max_cmds=None): + 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(MNG) + self.cpu.load_mng(self.map['fw']) 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] + 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.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) + 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 + self.heap = [0x08010000, 0x0c010000] # bump allocator over the DRAM banks # ---- hook helpers: emulate a called function returning ---- def h_ret(self, cpu, ret=None): @@ -169,6 +113,32 @@ class MainRunner: 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' @@ -176,21 +146,27 @@ class MainRunner: 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(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 % 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)}") + 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 run(self, budget=2_000_000_000): + 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 = MAIN - watch = {V_INIT: 'velocirender_init', DO_INIT: 'do_init'} + 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: @@ -203,7 +179,7 @@ class MainRunner: f"{cpu.steps:,} steps"); break continue if pc in watch: - print(f" >> {watch.pop(pc)} entered (steps={cpu.steps:,})") + print(f" >> {watch.pop(pc)} entered (steps={cpu.steps:,})", flush=True) if not cpu.step(): print(f"\nSTOP: {cpu.stopmsg}") import dis860 @@ -217,21 +193,25 @@ class MainRunner: 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, verbose=verbose, max_cmds=mc) - print(f"{os.path.basename(cap)}: {len(r.queue)} wire commands queued") + 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.igc: - out = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'igc_stream.bin') + if cpu.board_log: + out = os.path.join(_HERE, 'igc_stream.bin') with open(out, 'wb') as f: - for addr, val in cpu.igc: - f.write(struct.pack(' {out}") + for k, a, v in cpu.board_log: + f.write(struct.pack(' {out}") if __name__ == '__main__': diff --git a/emulator/firmware-decomp/run_to_draw.py b/emulator/firmware-decomp/run_to_draw.py index d0fc14f..17a1676 100644 --- a/emulator/firmware-decomp/run_to_draw.py +++ b/emulator/firmware-decomp/run_to_draw.py @@ -1,24 +1,30 @@ -"""Marathon runner: authentic main() + cap7 through the first draw_scene frames. +"""Marathon runner: version-matched (capfw7) authentic boot + cap7 replay. -Writes progress to stdout and dumps the captured IGC/board stream + a state -summary when the command budget is reached (or on fault). +Runs the capture's own firmware build from its TRUE ENTRY (0xf0400000) with the +boot handshake satisfied, so the full startup (crt0, pools, main) is the +firmware's own code; then feeds the capture's scene commands via dN_receive. + +Writes the board/IGC stream to igc_stream.bin and prints progress. + +Usage: python run_to_draw.py [capture] [max_cmds] [time_budget_s] """ import sys, os, time, struct sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import emu860, dis860, emu_main CAP = sys.argv[1] if len(sys.argv) > 1 else r'C:\VWE\TeslaRel410\dpl3-revive\patha\cap7.raw.bin' -MAXC = int(sys.argv[2]) if len(sys.argv) > 2 else 1600 -TIME_BUDGET = float(sys.argv[3]) if len(sys.argv) > 3 else 3300.0 +MAXC = int(sys.argv[2]) if len(sys.argv) > 2 else 800 +TIME_BUDGET = float(sys.argv[3]) if len(sys.argv) > 3 else 540.0 emu860.Mem.log = lambda self, *a, **k: None -r = emu_main.MainRunner(CAP, max_cmds=MAXC) +r = emu_main.MainRunner(CAP, fw='capfw7', max_cmds=MAXC) cpu = r.cpu -cpu.wr(1, cpu.RET_SENTINEL); cpu.wr(2, 0xc0000); cpu.wr(16, 0); cpu.wr(17, 0) -cpu.pc = emu_main.MAIN +cpu.ctrl[0xfffff728] = 1 # boot handshake: transputer says go +cpu.wr(2, 0x000c0000) +cpu.pc = 0xf0400000 # true entry: authentic full startup t0 = time.time(); last = t0 -result = 'time' +result = 'time-budget' while True: pc = cpu.pc if pc == cpu.RET_SENTINEL: @@ -38,12 +44,13 @@ while True: print(f" .. t={now-t0:6.0f}s steps={cpu.steps:,} cmds={r.qi} " f"igc={len(cpu.igc)} board={len(cpu.board_log)}", flush=True) if now - t0 > TIME_BUDGET: - result = 'time-budget'; break + break print(f"\nRESULT: {result}") -print(f"steps={cpu.steps:,} cmds={r.qi}/{len(r.queue)} done={dict(r.done)}") -print(f"replies={r.replies[:30]}") -print(f"IGC writes={len(cpu.igc)} board-log={len(cpu.board_log)}") +print(f"steps={cpu.steps:,} cmds={r.qi}/{len(r.queue)}") +print(f"done={dict(r.done)}") +print(f"replies (last 12): {r.replies[-12:]}") +print(f"IGC={len(cpu.igc)} board-log={len(cpu.board_log)}") if cpu.stopmsg or result.startswith('derail'): for tpc, tw in cpu.tail[-16:]: m, ops = dis860.decode(tw, tpc)