Files
CydandClaude Opus 4.8 ef4a0b67ac i860 emu: fix 6 core interpreter bugs; boot+velocirender_init+do_init now run clean
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>
2026-07-15 09:35:44 -05:00

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()