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>
This commit is contained in:
Cyd
2026-07-15 09:35:44 -05:00
co-authored by Claude Opus 4.8
parent 20dbd90026
commit ef4a0b67ac
2 changed files with 102 additions and 61 deletions
+65 -35
View File
@@ -59,22 +59,33 @@ class Mem:
addr = u32(addr)
m = self._mmio(addr)
if m: return u32(m[0](addr))
p = self._page(addr, False)
if p is None:
if (addr >> 16) not in self._warned:
self._warned.add(addr >> 16)
self.log(f" [mem] read unmapped {addr:#010x} -> 0")
return 0
return struct.unpack_from('<I', p, addr & 0xFFFF)[0]
off = addr & 0xFFFF
if off <= 0xFFFC:
p = self._page(addr, False)
if p is None:
if (addr >> 16) not in self._warned:
self._warned.add(addr >> 16)
self.log(f" [mem] read unmapped {addr:#010x} -> 0")
return 0
return struct.unpack_from('<I', p, off)[0]
return (self.r8(addr) | (self.r8(addr+1) << 8) |
(self.r8(addr+2) << 16) | (self.r8(addr+3) << 24))
def w32(self, addr, val):
addr = u32(addr); val = u32(val)
m = self._mmio(addr)
if m: return m[1](addr, val)
struct.pack_into('<I', self._page(addr, True), addr & 0xFFFF, val)
off = addr & 0xFFFF
if off <= 0xFFFC:
struct.pack_into('<I', self._page(addr, True), off, val)
else:
for i in range(4): self.w8(addr + i, (val >> (8 * i)) & 0xFF)
def r16(self, addr):
addr = u32(addr); p = self._page(addr, False)
addr = u32(addr)
if (addr & 0xFFFF) == 0xFFFF:
return self.r8(addr) | (self.r8(addr + 1) << 8)
p = self._page(addr, False)
if p is None: return 0
return struct.unpack_from('<H', p, addr & 0xFFFF)[0]
@@ -83,7 +94,11 @@ class Mem:
return p[addr & 0xFFFF] if p else 0
def w16(self, addr, val):
struct.pack_into('<H', self._page(u32(addr), True), addr & 0xFFFF, val & 0xFFFF)
addr = u32(addr)
if (addr & 0xFFFF) == 0xFFFF:
self.w8(addr, val & 0xFF); self.w8(addr + 1, (val >> 8) & 0xFF)
else:
struct.pack_into('<H', self._page(addr, True), addr & 0xFFFF, val & 0xFFFF)
def w8(self, addr, val):
self._page(u32(addr), True)[addr & 0xFFFF] = val & 0xFF
@@ -180,23 +195,27 @@ class I860:
src1 = (w >> 11) & 0x1f
imm = w & 0xffff
# ---- loads / stores ----
if op in (0x04, 0x05): # ld.l off(src2),dest
off = s16(imm & 0xfffc)
self.wr(dest, self.mem.r32(self.rd(src2) + off)); return
if op in (0x06, 0x07): # st.l dest? -> src? : st.l r,off(src2)
off = s16(imm & 0xfffc)
# st.l isrc1(dest field is src), src2 base : encoding stores rdest-field
self.mem.w32(self.rd(src2) + off, self.rd(dest)); return
if op in (0x00, 0x01): # ld.s (16-bit) -- sign? treat unsigned
off = s16(imm & 0xfffe)
self.wr(dest, self.mem.r16(self.rd(src2) + off)); return
if op in (0x08, 0x09): # fld.l off(src2),fdest
off = s16(imm & 0xfff8 if op == 0x09 else imm)
self.fwr(dest, self.mem.r32(self.rd(src2) + off)); return
if op in (0x0a, 0x0b): # fst.l fdest,off(src2)
off = s16(imm & 0xfff8 if op == 0x0b else imm)
self.mem.w32(self.rd(src2) + off, self.frd(dest)); return
# ---- loads ---- i860 integer loads are IMMEDIATE-offset (both even/odd of a
# pair): EA = base(src2) + s16(offset). Indexing is done by pre-computing
# base+index into a register, then loading at offset 0. dest = bits20:16.
if op in (0x00, 0x01, 0x04, 0x05, 0x08, 0x09):
m = 0xffff if op < 0x04 else (0xfff8 if op >= 0x08 else 0xfffc)
off = s16(imm & m)
ea = self.rd(src2) + off
if op in (0x00, 0x01): self.wr(dest, self.mem.r8(ea)) # ld.b (byte)
elif op in (0x04, 0x05): self.wr(dest, self.mem.r32(ea)) # ld.l
else: self.fwr(dest, self.mem.r32(ea)) # fld.l
return
# ---- stores ---- i860 stores differ: source reg = src1 (bits15:11), and the
# 16-bit offset is SPLIT high=bits20:16, low=bits10:0 (the src1 field displaced it).
if op in (0x03, 0x06, 0x07, 0x0a, 0x0b):
m = 0xffff if op == 0x03 else (0xfff8 if op >= 0x0a else 0xfffc)
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & m)
ea = self.rd(src2) + off
if op == 0x03: self.mem.w8(ea, self.rd(src1) & 0xff) # st.b (byte)
elif op in (0x06, 0x07): self.mem.w32(ea, self.rd(src1)) # st.l
else: self.mem.w32(ea, self.frd(src1)) # fst.l
return
if op == 0x0c: # ld.c ctrl,dest
self.wr(dest, self.cr.get(src2, 0)); return
if op == 0x0e: # st.c src1,ctrl
@@ -232,15 +251,18 @@ class I860:
# ---- arithmetic / logic (odd opcode = 16-bit immediate) ----
base = op & 0x3e
if base in (0x20, 0x22, 0x24, 0x26): # addu subu adds subs
# i860: dest = SRC1 (op) SRC2, where src1 = imm (immediate form) or
# 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: r = a + b # addu
elif base == 0x22: r = b - a # subu
elif base == 0x24: r = i32(a) + i32(b) # adds
else: r = i32(b) - i32(a) # subs
self.wr(dest, r)
# i860 sets CC on add/sub (borrow/carry); approximate signed compare use
self.set_cc((r & MASK32) == 0 if False else (i32(b) - i32(a)) >= 0 if base in (0x22, 0x26) else True)
if base == 0x20: # addu
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 == 0x22: # subu: src1 - src2
self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b))
else: # subs: src1 - src2
self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b))
return
if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra
cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f)
@@ -261,7 +283,7 @@ class I860:
elif g == 0x38: r = b | a # or/orh
else: r = b ^ a # xor/xorh
self.wr(dest, r)
self.set_cc((r & MASK32) == 0) # logicals set CC = (result==0)
self.set_cc((r & MASK32) == 0) # i860 logicals set CC = (result == 0)
return
# ---- FP unit (opcode 0x12) ----
@@ -317,6 +339,14 @@ class I860:
self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp))
elif sub == 0x35: # feq: CC = src1 == src2
self.set_cc(self.rdf(src1, sp) == self.rdf(src2, sp))
elif sub == 0x21: # fmlow.dd -- i860 FP-unit INTEGER multiply.
# The i860 has no imul: ints are ixfr'd into FP regs, fmlow.dd multiplies,
# and the low 32 bits (fdest) are fxfr/fst'd back. Operands are the low
# words (32-bit); result is the 64-bit product across the fdest pair.
a = self.frd(src1); b = self.frd(src2)
prod = a * b
b0 = dest & ~1
self.fwr(b0, prod & 0xFFFFFFFF); self.fwr(b0 | 1, (prod >> 32) & 0xFFFFFFFF)
elif sub == 0x40: # fxfr FP->int
self.wr(dest, self.frd(src1))
elif sub == 0x49: # fiadd (integer add in FP unit, 32-bit)
+37 -26
View File
@@ -21,25 +21,25 @@ MNG = r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
# Confirmed WIRE-action -> handler map (vaddr). IMPORTANT: the firmware's
# dispatch jump table (file 0x47cb8) is indexed by an INTERNAL command code, NOT
# the wire action -- velocirender_input remaps wire->internal first (e.g. wire
# draw_scene=9 -> internal index 12; flush=3 -> 30; statistics=15 -> 31; while
# create/list_add/dcs_link are identity). So we can't index that table by the
# capture's action. These are matched by function identity instead (create's
# prologue verified reads type/handle; statistics verified returns; draw_scene,
# flush, list_add, dcs_link identified in the dispatch analysis).
# TODO: reverse the wire->internal remap in velocirender_input to complete the
# map for the high-frequency per-frame commands (0x09 draw, 0x1d artics, 0x2a).
# 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,
# A.init handler + the 0x1d/0x2a artic handlers: need the remap (see TODO).
}
def extract_handlers(cpu):
@@ -68,35 +68,46 @@ def main():
cpu.load_mng(MNG)
cpu.map_control(); cpu.map_board()
handlers = extract_handlers(cpu)
print(f"handlers extracted: {len(handlers)} "
f"(draw_scene={handlers.get(A.draw_scene):#x} create={handlers.get(A.create):#x})")
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 or act == A.init:
continue # skip firmware download; init handled below
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[act] = done.get(act, 0) + 1
done[str(ANAME.get(act, act)) + '?'] = done.get(str(ANAME.get(act, act)) + '?', 0) + 1
continue
cpu.mem.load_blob(pbuf, pl or b'\0')
r = cpu.call(h, args=(pbuf,), maxsteps=5_000_000)
pbuf += (len(pl) + 15) & ~15
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:
print(f"\n[cmd {idx}] action {ANAME.get(act,act)} FAULTED after building "
f"{sum(v for k,v in done.items())} cmds:\n {cpu.stopmsg}")
import dis860
for pc, w in cpu.tail[-6:]:
m, ops = dis860.decode(w, pc); print(f" {pc:#010x}: {w:08x} {m} {ops}")
break
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 so far: {len(cpu.igc)}")
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)}")