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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-15 13:20:35 -05:00
co-authored by Claude Opus 4.8
parent 753564aef2
commit d9cfd74070
3 changed files with 252 additions and 176 deletions
+100 -11
View File
@@ -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))
+132 -152
View File
@@ -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('<II', addr, val))
print(f"IGC stream -> {out}")
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__':
+20 -13
View File
@@ -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)