Worked the full-render marathon by tracing faults in the real VREND.MNG firmware and matching them to the ground-truth AS860 assembly (VR_REMOT.S) and C source (VR_REMOT.C). Six interpreter bugs found and fixed: 1. subs/subu direction: computed src2-src1; must be src1-src2 with CC=(src1<src2). Corrupted every subtraction, compare and bounds-check. 2. logical CC: all i860 logicals (and/andnot/or/xor + .h) set CC=(result==0). Had wrongly limited it to the AND family, so the `xor 0x0,rN,r0; bnc` zero-test idiom spun forever. 3. STORE encoding (the big one): i860 stores encode the SOURCE register in bits 15:11 (src1), not the dest field, and SPLIT the 16-bit offset across bits 20:16 (high) + bits 10:0 (low). The old decode saved the wrong register to the wrong offset, so function prologues never stored r1 and every `bri r1` return jumped to 0. 4. ld.b/st.b: op 0x00/0x01 = ld.b, op 0x03 = st.b (byte). Proven by byte-scan loops (r5 advanced by 1) and a save/restore pair at 0xf042b418 -> b430. 5. Mem page-span: r16/r32/w16/w32 crashed on 64KB page boundaries; now fall back to byte access across the boundary. 6. fmlow.dd (FP subop 0x21): the i860 has no integer multiply, so ints are ixfr'd into FP regs, multiplied via fmlow.dd, and fxfr'd back. Implemented as a 32x32 -> 64 multiply across the destination register pair. emu_replay.py now runs the source-accurate init sequence: velocirender_init on the init(0) command, then do_init before the first real command. Result: boot idles at 0xf0400590; velocirender_init returns; do_init runs to completion (~9200 steps of real allocator / name-table / scene-root setup). Remaining blocker (documented in the tier-0 memory): create()'s switch needs register-indexed integer loads (VR_REMOT.S: `ld.l r30(r31),r31`) together with the correct .data link base (~0x1000, not 0) -- the two errors currently cancel for the immediate paths but break the indexed jump-table dispatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
"""Replay a captured VelociRender wire through the REAL firmware in emu860.
|
|
|
|
Feeds each captured (action, payload) command to the firmware's own handler
|
|
(payload pointer in r16, per the PGI calling convention), building the scene up
|
|
in the emulated i860's memory, then runs draw_scene -- capturing the IGC
|
|
coefficient stream the firmware emits (the Tier-1 handoff).
|
|
|
|
Bypasses the transputer/CCB/interrupt path (see HARDWARE-ARCHITECTURE.md): we
|
|
call the handlers directly, which is valid because they read the raw wire payload.
|
|
|
|
python emu_replay.py [capture.raw.bin] [--verbose]
|
|
"""
|
|
import sys, os, struct
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha')
|
|
import emu860
|
|
from vrboard import Assembler, A, BOOT_ACTIONS
|
|
|
|
MNG = r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
|
|
|
|
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
|
|
|
|
# From VR_REMOT.C remote_velocirender(): the wire `action` (== the vr_action enum,
|
|
# which matches vrboard's) drives a switch. init(0) is special -> velocirender_init
|
|
# + deferred do_init(); every other action indexes a jump table by (action-1).
|
|
INIT_FN = 0xf040e300 # velocirender_init(param_data) [action 0]
|
|
DO_INIT = 0xf040afb0 # do_init() -- deferred, before first real command
|
|
# vr_action -> handler vaddr. Handler addresses matched by function identity from
|
|
# the case blocks (create verified: reads [p+0]=type,[p+4]=handle; draw_scene and
|
|
# statistics verified). The switch jump-table indexing is a compiler idiom we
|
|
# don't need since remote_velocirender's source gives handler-per-action directly.
|
|
WIRE_HANDLERS = {
|
|
A.create: 0xf040c180,
|
|
A.delete: 0xf040c3b8,
|
|
A.flush: 0xf040cfd8,
|
|
A.dcs_link: 0xf040dc70,
|
|
A.list_add: 0xf040d4d0,
|
|
A.draw_scene: 0xf040e340,
|
|
A.statistics: 0xf040e940,
|
|
A.set_geom_verts: 0xf040a9a8,
|
|
A.set_texmap_texels: 0xf040a030,
|
|
}
|
|
|
|
def extract_handlers(cpu):
|
|
return dict(WIRE_HANDLERS)
|
|
|
|
def parse_capture(path):
|
|
data = open(path, 'rb').read()
|
|
start = None
|
|
for i in range(0, len(data) - 8):
|
|
w = struct.unpack_from('<I', data, i)[0]
|
|
if (w & 0xffff0000) == 0x40ff0000 and 8 <= (w & 0xffff) <= 4096:
|
|
act = struct.unpack_from('<I', data, i + 4)[0]
|
|
if act in (A.init, A.code860, A.args860, A.data860, A.bss860):
|
|
start = i; break
|
|
if start is None:
|
|
raise SystemExit("no framed region found")
|
|
asm = Assembler(); asm.feed(data[start:])
|
|
return [(m.action, m.payload) for m in asm if not m.iserver]
|
|
|
|
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
|
|
|
|
cpu = emu860.I860(trace=0)
|
|
cpu.load_mng(MNG)
|
|
cpu.map_control(); cpu.map_board()
|
|
handlers = extract_handlers(cpu)
|
|
print(f"handlers: create={handlers.get(A.create):#x} flush={handlers.get(A.flush):#x} "
|
|
f"list_add={handlers.get(A.list_add):#x} draw_scene={handlers.get(A.draw_scene):#x} "
|
|
f"statistics={handlers.get(A.statistics):#x}")
|
|
|
|
cmds = parse_capture(cap)
|
|
print(f"{os.path.basename(cap)}: {len(cmds)} wire commands")
|
|
|
|
import dis860
|
|
def report(idx, label):
|
|
print(f"\n[cmd {idx}] {label} FAULT: {cpu.stopmsg}")
|
|
for pc, w in cpu.tail[-8:]:
|
|
m, ops = dis860.decode(w, pc); print(f" {pc:#010x}: {w:08x} {m} {ops}")
|
|
|
|
ANAME = {int(a): a.name for a in A}
|
|
pbuf = 0x00200000 # fresh payload buffer per command (advances)
|
|
done = {}
|
|
init_pending = False; did_init = False
|
|
for idx, (act, pl) in enumerate(cmds):
|
|
if act in BOOT_ACTIONS:
|
|
continue # skip firmware-download frames
|
|
ppl = pbuf; cpu.mem.load_blob(ppl, pl or b'\0'); pbuf += (len(pl) + 15) & ~15
|
|
if act == A.init:
|
|
cpu.call(INIT_FN, args=(ppl,), maxsteps=8_000_000)
|
|
init_pending = True; done['init'] = done.get('init', 0) + 1
|
|
if cpu.stopmsg: report(idx, 'velocirender_init'); break
|
|
continue
|
|
if init_pending and not did_init: # deferred heavy init before 1st real cmd
|
|
cpu.call(DO_INIT, maxsteps=8_000_000); did_init = True
|
|
print(f"do_init() -> {'FAULT' if cpu.stopmsg else 'ok'} (steps={cpu.steps})")
|
|
if cpu.stopmsg: report(idx, 'do_init'); break
|
|
h = handlers.get(act)
|
|
if h is None:
|
|
done[str(ANAME.get(act, act)) + '?'] = done.get(str(ANAME.get(act, act)) + '?', 0) + 1
|
|
continue
|
|
cpu.call(h, args=(ppl,), maxsteps=8_000_000)
|
|
done[ANAME.get(act, act)] = done.get(ANAME.get(act, act), 0) + 1
|
|
if cpu.stopmsg:
|
|
report(idx, f"action {ANAME.get(act,act)} (built {sum(done.values())} cmds)"); break
|
|
if verbose and act == A.draw_scene:
|
|
print(f"[cmd {idx}] draw_scene ok IGC writes: {len(cpu.igc)}")
|
|
print("\nreplayed:", done)
|
|
print(f"IGC/board writes captured: {len(cpu.board_log)}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|