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:
@@ -59,22 +59,33 @@ class Mem:
|
|||||||
addr = u32(addr)
|
addr = u32(addr)
|
||||||
m = self._mmio(addr)
|
m = self._mmio(addr)
|
||||||
if m: return u32(m[0](addr))
|
if m: return u32(m[0](addr))
|
||||||
p = self._page(addr, False)
|
off = addr & 0xFFFF
|
||||||
if p is None:
|
if off <= 0xFFFC:
|
||||||
if (addr >> 16) not in self._warned:
|
p = self._page(addr, False)
|
||||||
self._warned.add(addr >> 16)
|
if p is None:
|
||||||
self.log(f" [mem] read unmapped {addr:#010x} -> 0")
|
if (addr >> 16) not in self._warned:
|
||||||
return 0
|
self._warned.add(addr >> 16)
|
||||||
return struct.unpack_from('<I', p, addr & 0xFFFF)[0]
|
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):
|
def w32(self, addr, val):
|
||||||
addr = u32(addr); val = u32(val)
|
addr = u32(addr); val = u32(val)
|
||||||
m = self._mmio(addr)
|
m = self._mmio(addr)
|
||||||
if m: return m[1](addr, val)
|
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):
|
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
|
if p is None: return 0
|
||||||
return struct.unpack_from('<H', p, addr & 0xFFFF)[0]
|
return struct.unpack_from('<H', p, addr & 0xFFFF)[0]
|
||||||
|
|
||||||
@@ -83,7 +94,11 @@ class Mem:
|
|||||||
return p[addr & 0xFFFF] if p else 0
|
return p[addr & 0xFFFF] if p else 0
|
||||||
|
|
||||||
def w16(self, addr, val):
|
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):
|
def w8(self, addr, val):
|
||||||
self._page(u32(addr), True)[addr & 0xFFFF] = val & 0xFF
|
self._page(u32(addr), True)[addr & 0xFFFF] = val & 0xFF
|
||||||
@@ -180,23 +195,27 @@ class I860:
|
|||||||
src1 = (w >> 11) & 0x1f
|
src1 = (w >> 11) & 0x1f
|
||||||
imm = w & 0xffff
|
imm = w & 0xffff
|
||||||
|
|
||||||
# ---- loads / stores ----
|
# ---- loads ---- i860 integer loads are IMMEDIATE-offset (both even/odd of a
|
||||||
if op in (0x04, 0x05): # ld.l off(src2),dest
|
# pair): EA = base(src2) + s16(offset). Indexing is done by pre-computing
|
||||||
off = s16(imm & 0xfffc)
|
# base+index into a register, then loading at offset 0. dest = bits20:16.
|
||||||
self.wr(dest, self.mem.r32(self.rd(src2) + off)); return
|
if op in (0x00, 0x01, 0x04, 0x05, 0x08, 0x09):
|
||||||
if op in (0x06, 0x07): # st.l dest? -> src? : st.l r,off(src2)
|
m = 0xffff if op < 0x04 else (0xfff8 if op >= 0x08 else 0xfffc)
|
||||||
off = s16(imm & 0xfffc)
|
off = s16(imm & m)
|
||||||
# st.l isrc1(dest field is src), src2 base : encoding stores rdest-field
|
ea = self.rd(src2) + off
|
||||||
self.mem.w32(self.rd(src2) + off, self.rd(dest)); return
|
if op in (0x00, 0x01): self.wr(dest, self.mem.r8(ea)) # ld.b (byte)
|
||||||
if op in (0x00, 0x01): # ld.s (16-bit) -- sign? treat unsigned
|
elif op in (0x04, 0x05): self.wr(dest, self.mem.r32(ea)) # ld.l
|
||||||
off = s16(imm & 0xfffe)
|
else: self.fwr(dest, self.mem.r32(ea)) # fld.l
|
||||||
self.wr(dest, self.mem.r16(self.rd(src2) + off)); return
|
return
|
||||||
if op in (0x08, 0x09): # fld.l off(src2),fdest
|
# ---- stores ---- i860 stores differ: source reg = src1 (bits15:11), and the
|
||||||
off = s16(imm & 0xfff8 if op == 0x09 else imm)
|
# 16-bit offset is SPLIT high=bits20:16, low=bits10:0 (the src1 field displaced it).
|
||||||
self.fwr(dest, self.mem.r32(self.rd(src2) + off)); return
|
if op in (0x03, 0x06, 0x07, 0x0a, 0x0b):
|
||||||
if op in (0x0a, 0x0b): # fst.l fdest,off(src2)
|
m = 0xffff if op == 0x03 else (0xfff8 if op >= 0x0a else 0xfffc)
|
||||||
off = s16(imm & 0xfff8 if op == 0x0b else imm)
|
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & m)
|
||||||
self.mem.w32(self.rd(src2) + off, self.frd(dest)); return
|
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
|
if op == 0x0c: # ld.c ctrl,dest
|
||||||
self.wr(dest, self.cr.get(src2, 0)); return
|
self.wr(dest, self.cr.get(src2, 0)); return
|
||||||
if op == 0x0e: # st.c src1,ctrl
|
if op == 0x0e: # st.c src1,ctrl
|
||||||
@@ -232,15 +251,18 @@ class I860:
|
|||||||
# ---- arithmetic / logic (odd opcode = 16-bit immediate) ----
|
# ---- arithmetic / logic (odd opcode = 16-bit immediate) ----
|
||||||
base = op & 0x3e
|
base = op & 0x3e
|
||||||
if base in (0x20, 0x22, 0x24, 0x26): # addu subu adds subs
|
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)
|
b = self.rd(src2)
|
||||||
a = s16(imm) if (op & 1) else self.rd(src1)
|
a = s16(imm) if (op & 1) else self.rd(src1)
|
||||||
if base == 0x20: r = a + b # addu
|
if base == 0x20: # addu
|
||||||
elif base == 0x22: r = b - a # subu
|
s = u32(a) + u32(b); self.wr(dest, s); self.set_cc(s > MASK32)
|
||||||
elif base == 0x24: r = i32(a) + i32(b) # adds
|
elif base == 0x24: # adds
|
||||||
else: r = i32(b) - i32(a) # subs
|
s = i32(a) + i32(b); self.wr(dest, s); self.set_cc(u32(a) + u32(b) > MASK32)
|
||||||
self.wr(dest, r)
|
elif base == 0x22: # subu: src1 - src2
|
||||||
# i860 sets CC on add/sub (borrow/carry); approximate signed compare use
|
self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b))
|
||||||
self.set_cc((r & MASK32) == 0 if False else (i32(b) - i32(a)) >= 0 if base in (0x22, 0x26) else True)
|
else: # subs: src1 - src2
|
||||||
|
self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b))
|
||||||
return
|
return
|
||||||
if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra
|
if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra
|
||||||
cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f)
|
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
|
elif g == 0x38: r = b | a # or/orh
|
||||||
else: r = b ^ a # xor/xorh
|
else: r = b ^ a # xor/xorh
|
||||||
self.wr(dest, r)
|
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
|
return
|
||||||
|
|
||||||
# ---- FP unit (opcode 0x12) ----
|
# ---- FP unit (opcode 0x12) ----
|
||||||
@@ -317,6 +339,14 @@ class I860:
|
|||||||
self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp))
|
self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp))
|
||||||
elif sub == 0x35: # feq: CC = src1 == src2
|
elif sub == 0x35: # feq: CC = src1 == src2
|
||||||
self.set_cc(self.rdf(src1, sp) == self.rdf(src2, sp))
|
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
|
elif sub == 0x40: # fxfr FP->int
|
||||||
self.wr(dest, self.frd(src1))
|
self.wr(dest, self.frd(src1))
|
||||||
elif sub == 0x49: # fiadd (integer add in FP unit, 32-bit)
|
elif sub == 0x49: # fiadd (integer add in FP unit, 32-bit)
|
||||||
|
|||||||
@@ -21,25 +21,25 @@ MNG = r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
|
|||||||
|
|
||||||
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
|
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
|
||||||
|
|
||||||
# Confirmed WIRE-action -> handler map (vaddr). IMPORTANT: the firmware's
|
# From VR_REMOT.C remote_velocirender(): the wire `action` (== the vr_action enum,
|
||||||
# dispatch jump table (file 0x47cb8) is indexed by an INTERNAL command code, NOT
|
# which matches vrboard's) drives a switch. init(0) is special -> velocirender_init
|
||||||
# the wire action -- velocirender_input remaps wire->internal first (e.g. wire
|
# + deferred do_init(); every other action indexes a jump table by (action-1).
|
||||||
# draw_scene=9 -> internal index 12; flush=3 -> 30; statistics=15 -> 31; while
|
INIT_FN = 0xf040e300 # velocirender_init(param_data) [action 0]
|
||||||
# create/list_add/dcs_link are identity). So we can't index that table by the
|
DO_INIT = 0xf040afb0 # do_init() -- deferred, before first real command
|
||||||
# capture's action. These are matched by function identity instead (create's
|
# vr_action -> handler vaddr. Handler addresses matched by function identity from
|
||||||
# prologue verified reads type/handle; statistics verified returns; draw_scene,
|
# the case blocks (create verified: reads [p+0]=type,[p+4]=handle; draw_scene and
|
||||||
# flush, list_add, dcs_link identified in the dispatch analysis).
|
# statistics verified). The switch jump-table indexing is a compiler idiom we
|
||||||
# TODO: reverse the wire->internal remap in velocirender_input to complete the
|
# don't need since remote_velocirender's source gives handler-per-action directly.
|
||||||
# map for the high-frequency per-frame commands (0x09 draw, 0x1d artics, 0x2a).
|
|
||||||
WIRE_HANDLERS = {
|
WIRE_HANDLERS = {
|
||||||
A.create: 0xf040c180,
|
A.create: 0xf040c180,
|
||||||
|
A.delete: 0xf040c3b8,
|
||||||
A.flush: 0xf040cfd8,
|
A.flush: 0xf040cfd8,
|
||||||
A.dcs_link: 0xf040dc70,
|
A.dcs_link: 0xf040dc70,
|
||||||
A.list_add: 0xf040d4d0,
|
A.list_add: 0xf040d4d0,
|
||||||
A.draw_scene: 0xf040e340,
|
A.draw_scene: 0xf040e340,
|
||||||
A.statistics: 0xf040e940,
|
A.statistics: 0xf040e940,
|
||||||
|
A.set_geom_verts: 0xf040a9a8,
|
||||||
A.set_texmap_texels: 0xf040a030,
|
A.set_texmap_texels: 0xf040a030,
|
||||||
# A.init handler + the 0x1d/0x2a artic handlers: need the remap (see TODO).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def extract_handlers(cpu):
|
def extract_handlers(cpu):
|
||||||
@@ -68,35 +68,46 @@ def main():
|
|||||||
cpu.load_mng(MNG)
|
cpu.load_mng(MNG)
|
||||||
cpu.map_control(); cpu.map_board()
|
cpu.map_control(); cpu.map_board()
|
||||||
handlers = extract_handlers(cpu)
|
handlers = extract_handlers(cpu)
|
||||||
print(f"handlers extracted: {len(handlers)} "
|
print(f"handlers: create={handlers.get(A.create):#x} flush={handlers.get(A.flush):#x} "
|
||||||
f"(draw_scene={handlers.get(A.draw_scene):#x} create={handlers.get(A.create):#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)
|
cmds = parse_capture(cap)
|
||||||
print(f"{os.path.basename(cap)}: {len(cmds)} wire commands")
|
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}
|
ANAME = {int(a): a.name for a in A}
|
||||||
pbuf = 0x00200000 # fresh payload buffer per command (advances)
|
pbuf = 0x00200000 # fresh payload buffer per command (advances)
|
||||||
done = {}
|
done = {}
|
||||||
|
init_pending = False; did_init = False
|
||||||
for idx, (act, pl) in enumerate(cmds):
|
for idx, (act, pl) in enumerate(cmds):
|
||||||
if act in BOOT_ACTIONS or act == A.init:
|
if act in BOOT_ACTIONS:
|
||||||
continue # skip firmware download; init handled below
|
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)
|
h = handlers.get(act)
|
||||||
if h is None:
|
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
|
continue
|
||||||
cpu.mem.load_blob(pbuf, pl or b'\0')
|
cpu.call(h, args=(ppl,), maxsteps=8_000_000)
|
||||||
r = cpu.call(h, args=(pbuf,), maxsteps=5_000_000)
|
|
||||||
pbuf += (len(pl) + 15) & ~15
|
|
||||||
done[ANAME.get(act, act)] = done.get(ANAME.get(act, act), 0) + 1
|
done[ANAME.get(act, act)] = done.get(ANAME.get(act, act), 0) + 1
|
||||||
if cpu.stopmsg:
|
if cpu.stopmsg:
|
||||||
print(f"\n[cmd {idx}] action {ANAME.get(act,act)} FAULTED after building "
|
report(idx, f"action {ANAME.get(act,act)} (built {sum(done.values())} cmds)"); break
|
||||||
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
|
|
||||||
if verbose and act == A.draw_scene:
|
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("\nreplayed:", done)
|
||||||
print(f"IGC/board writes captured: {len(cpu.board_log)}")
|
print(f"IGC/board writes captured: {len(cpu.board_log)}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user