processes the full wire boot incl. the downloaded PAZ/sfx module ISA fixes, all derived from the toolchain's own .S<->.O pairs (AS860.ZIP: OPTFLOAT/TRISTRIP/ZBUF32, plus DNC.O) and the firmware's linked COFF header: - DATA_BASE = 0x1000 DEFINITIVE: VREND.MNG carries its original COFF header in the file tail (.data vaddr 0x1000, .bss 0x1f940, entry 0xf0400000). - Integer loads: even opcodes are register-indexed (EA = src2 + src1); op 4/5 size flag = instr bit0 (0 = ld.s 16-bit, 1 = ld.l 32-bit); ld.b/ld.s sign-extend. - Integer stores: st.s/st.l selected by offset bit0, same split-offset rule. - FP loads/stores: FP register lives in the DEST field for both fld and fst (fst does NOT use the integer split-store encoding); flag bits: bit0 = auto-increment (base <- EA), bit1 1=.l/0=.d, bit2 = .q; .d/.q span register pairs/quads. ~450 fld.d + ~300 fst.d were previously read/written 32-bit. - bla (op 0x2d, was misdecoded as shrd): branch-on-LCC-and-add with the sign-dependent LCC rule (src1<0 -> signed sum >= 0), so spent countdown loops terminate. 335 bla instructions in the firmware. - CORE ESCAPE (op 0x13): sub-op 1 = lock, 2 = calli, 7 = unlock. Previously everything decoded as calli, so every spinlock acquire jumped to address 0 -- this was the phantom "exit stub" behind most earlier derails. - f2b: IEEE overflow -> +/-inf instead of raising. emu_main.py (new): runs the firmware's OWN main() (0xf0403f10) and feeds real wire captures through a hooked dN_receive, so init/do_init/dispatch/handlers all execute authentically. Provides the transputer-monitor environment (processor id, DRAM region descriptors in the shared control block, sbrk/ shared-block seed slots) and hooks only the link primitives (bla busy-wait, dN_mynode/dN_nodes/dN_receive/dN_send, putchar path, spinlocks, page allocator + virt->phys translator pending Tier-2 VRENDMON). KEY DISCOVERY: the capture's args860/code860/data860/bss860 preamble is the host DOWNLOADING an additional i860 module (the PAZ/sfx renderer layer, banner "i860 50MHz") which installs the runtime handler tables and system objects. Feeding it through the firmware's own handlers, the module loads and makes the first IGC board-register writes. State: 834+ wire commands processed (module download + init + create); first draw_scene sits at command 1568. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
253 lines
11 KiB
Python
253 lines
11 KiB
Python
"""Intel i860 disassembler for the VelociRender firmware.
|
|
|
|
Opcode/field assignments are those VALIDATED against our own AS860 .S<->.O pairs
|
|
(see i860-encoding.md / derive860.py); unknowns decode as `.word`. This is also
|
|
the front-end for an eventual i860 *emulator* that runs VREND.MNG directly.
|
|
|
|
Run `python dis860.py --validate` to check the decoder against the matching
|
|
source/object pairs (per-instruction base-mnemonic agreement)."""
|
|
import sys, struct, os, glob
|
|
|
|
REG = [f"r{i}" for i in range(32)]
|
|
FREG = [f"f{i}" for i in range(32)]
|
|
CTRL = {0: "fir", 1: "psr", 2: "dirbase", 3: "db", 4: "fsr", 5: "epsr"}
|
|
|
|
def s16(v): return v - 0x10000 if v & 0x8000 else v
|
|
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
|
|
|
|
# ---- FP escape (op 0x12) sub-opcode[6:0] -> base name (validated where marked) ----
|
|
FP_SUB = {
|
|
0x00: "r2p1", 0x01: "r2pt", 0x02: "r2ap1", 0x04: "i2p1", 0x05: "i2pt",
|
|
0x06: "i2ap1", 0x07: "i2apt", 0x08: "rat1p2", 0x09: "m12apm",
|
|
0x0a: "ra1p2", 0x0b: "m12ttpa", 0x0c: "iat1p2", 0x0d: "m12tpm",
|
|
0x0e: "ia1p2", 0x0f: "m12tpa", 0x1c: "mimt1s2",
|
|
0x20: "fmul", 0x21: "fmlow", 0x22: "frcp", 0x23: "frsqr",
|
|
0x24: "fmul3", 0x28: "pfmam", 0x2c: "pfmsm",
|
|
0x30: "fadd", 0x31: "fsub", 0x32: "fix", 0x34: "fgt", 0x35: "feq",
|
|
0x3a: "ftrunc", 0x40: "fxfr", 0x49: "fiadd", 0x4d: "fisub",
|
|
0x50: "faddp", 0x51: "faddz", 0x57: "fzchk", 0x5a: "form",
|
|
0x5b: "fzchkl", 0x5f: "fnop",
|
|
}
|
|
# precision suffix from result/source-precision bits (bit8=R result-prec, bit7=S src-prec)
|
|
def fp_suffix(w):
|
|
s = 'd' if (w >> 7) & 1 else 's'
|
|
r = 'd' if (w >> 8) & 1 else 's'
|
|
return f".{s}{r}"
|
|
|
|
def dec_fp(w):
|
|
sub = w & 0x7f
|
|
p = (w >> 10) & 1 # P: pipelined
|
|
src2 = (w >> 21) & 0x1f; dest = (w >> 16) & 0x1f; src1 = (w >> 11) & 0x1f
|
|
base = FP_SUB.get(sub)
|
|
if base is None:
|
|
return (".fp?", f"{sub:#04x} {FREG[src1]},{FREG[src2]},{FREG[dest]}")
|
|
if base == "fxfr": # FP->int reg move
|
|
return ("fxfr", f"{FREG[src1]},{REG[dest]}")
|
|
if base == "fnop":
|
|
return ("fnop", "")
|
|
name = ("pf" if p else "f") + base[1:] if base[0] == 'f' else (("pf" if p else "") + base)
|
|
# graphics dual-ops (r2p1 etc.) keep their name; only f* get the pf/f prefix
|
|
if base[0] != 'f':
|
|
name = base
|
|
return (name + fp_suffix(w), f"{FREG[src1]},{FREG[src2]},{FREG[dest]}")
|
|
|
|
# ---- primary decode ----
|
|
# load/store encoding (ground truth: AS860 .S<->.O pairs + DNC.O):
|
|
# - integer loads (0x00/0x01 ld.b, 0x04/0x05 ld.s|ld.l): dest=bits20:16;
|
|
# ODD = 16-bit immediate offset, EVEN = register-indexed EA=src2+src1.
|
|
# op 4/5 size flag = instr bit0 (0=.s 16-bit, 1=.l 32-bit).
|
|
# - integer stores (0x03 st.b, 0x07 st.s|st.l): source reg = src1 (bits15:11),
|
|
# offset SPLIT high=bits20:16 low=bits10:0; bit0 size flag as loads.
|
|
# - FP fld (0x08/0x09) / fst (0x0a/0x0b): FP reg = dest field for BOTH;
|
|
# ODD = flat s16 imm, EVEN = indexed. Flags: bit0=auto-increment,
|
|
# bit1: 1=.l, 0=.d; bit2 (with bit1=0) = .q.
|
|
MEM = {0x00, 0x01, 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
|
ARITH = {0x20: "addu", 0x22: "subu", 0x24: "adds", 0x26: "subs",
|
|
0x28: "shl", 0x2a: "shr", 0x2c: "shrd", 0x2e: "shra",
|
|
0x30: "and", 0x32: "andh", 0x34: "andnot", 0x36: "andnoth",
|
|
0x38: "or", 0x3a: "orh", 0x3c: "xor", 0x3e: "xorh"}
|
|
CTRLB = {0x1a: "br", 0x1b: "call", 0x1c: "bc", 0x1d: "bc.t",
|
|
0x1e: "bnc", 0x1f: "bnc.t"}
|
|
|
|
def decode(w, addr):
|
|
op = (w >> 26) & 0x3f
|
|
src2 = (w >> 21) & 0x1f; dest = (w >> 16) & 0x1f; src1 = (w >> 11) & 0x1f
|
|
imm = w & 0xffff
|
|
# loads/stores
|
|
if op in MEM:
|
|
if op in (0x00, 0x01, 0x04, 0x05): # integer loads
|
|
m = "ld.b" if op < 0x04 else ("ld.l" if (w & 1) else "ld.s")
|
|
if op & 1:
|
|
mask = 0xffff if op == 0x01 else (0xfffc if (w & 1) else 0xfffe)
|
|
return m, f"{s16(imm & mask):#x}({REG[src2]}),{REG[dest]}"
|
|
return m, f"{REG[src1]}({REG[src2]}),{REG[dest]}"
|
|
if op in (0x03, 0x07): # integer stores (split offset)
|
|
off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff)
|
|
if op == 0x03:
|
|
return "st.b", f"{REG[src1]},{s16(off):#x}({REG[src2]})"
|
|
m = "st.l" if (off & 1) else "st.s"
|
|
off &= 0xfffc if (off & 1) else 0xfffe
|
|
return m, f"{REG[src1]},{s16(off):#x}({REG[src2]})"
|
|
# FP fld/fst: FP reg in dest field; flags bit0=auto++, bit1:1=.l/0=.d, bit2=.q
|
|
fl = w & 7
|
|
sz = ".l" if (fl & 2) else (".q" if (fl & 4) else ".d")
|
|
pp = "++" if (fl & 1) else ""
|
|
m = ("fld" if op < 0x0a else "fst") + sz
|
|
if op & 1:
|
|
size = 4 if (fl & 2) else (16 if (fl & 4) else 8)
|
|
ea = f"{s16(imm & (0x10000 - size)):#x}({REG[src2]}){pp}"
|
|
else:
|
|
ea = f"{REG[src1]}({REG[src2]}){pp}"
|
|
if op < 0x0a:
|
|
return m, f"{ea},{FREG[dest]}"
|
|
return m, f"{FREG[dest]},{ea}"
|
|
if op == 0x02:
|
|
return "ixfr", f"{REG[src1]},{FREG[dest]}"
|
|
if op == 0x0c:
|
|
return "ld.c", f"{CTRL.get(src2,src2)},{REG[dest]}"
|
|
if op == 0x0e:
|
|
return "st.c", f"{REG[src1]},{CTRL.get(src2,src2)}"
|
|
if op == 0x10:
|
|
return "bri", f"{REG[src1]}"
|
|
if op == 0x11:
|
|
return "trap", f"{REG[src1]},{REG[src2]},{REG[dest]}"
|
|
if op == 0x12:
|
|
return dec_fp(w)
|
|
if op == 0x13:
|
|
# core escape: sub-op in low 5 bits (1=lock, 2=calli, 4=intovr, 7=unlock)
|
|
sub = w & 0x1f
|
|
if sub == 0x02:
|
|
return "calli", f"{REG[src1]}"
|
|
if sub == 0x01:
|
|
return "lock", ""
|
|
if sub == 0x07:
|
|
return "unlock", ""
|
|
if sub == 0x04:
|
|
return "intovr", ""
|
|
return "esc", f"{sub:#x}"
|
|
if op in (0x14, 0x15, 0x16, 0x17):
|
|
base = "btne" if op < 0x16 else "bte"
|
|
broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)
|
|
tgt = addr + 4 + broff * 4
|
|
# odd opcode = 5-bit-const form (i860 rule; e.g. `btne 0x0,r19` = 0x15)
|
|
s1 = f"{src1:#x}" if op & 1 else f"{REG[src1]}"
|
|
return base, f"{s1},{REG[src2]},{tgt:#010x}"
|
|
if op in CTRLB:
|
|
tgt = addr + 4 + s26(w & 0x03ffffff) * 4
|
|
if CTRLB[op] in ("br", "call"):
|
|
return CTRLB[op], f"{tgt:#010x}"
|
|
return CTRLB[op], f"{tgt:#010x}"
|
|
if op in (0x18, 0x19):
|
|
off = s16(imm & 0xfff8)
|
|
return "pfld.d", f"{off:#x}({REG[src2]}),{FREG[dest]}"
|
|
if op == 0x2d: # bla isrc1,isrc2,sbroff (split offset like bte)
|
|
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & 0xffff)
|
|
return "bla", f"{REG[src1]},{REG[src2]},{addr + 4 + off*4:#010x}"
|
|
if (op & 0x3e) in ARITH:
|
|
m = ARITH[op & 0x3e]
|
|
if op & 1: # immediate form
|
|
v = s16(imm)
|
|
return m, f"{v:#x},{REG[src2]},{REG[dest]}"
|
|
# register form; recognise pseudo-ops (mov/nop = shl r0)
|
|
if m == "shl" and src1 == 0:
|
|
if src2 == 0 and dest == 0:
|
|
return "nop", ""
|
|
return "mov", f"{REG[src2]},{REG[dest]}"
|
|
if m == "or" and src1 == 0 and src2 == 0:
|
|
return "mov", f"r0,{REG[dest]}"
|
|
return m, f"{REG[src1]},{REG[src2]},{REG[dest]}"
|
|
return ".word", f"{w:#010x}"
|
|
|
|
# ---------------- validation ----------------
|
|
def _norm_ops(s):
|
|
"""normalize an operand string for comparison: strip spaces, lower, and
|
|
canonicalize hex/dec immediates."""
|
|
import re
|
|
s = s.lower().replace(' ', '')
|
|
def canon(m):
|
|
v = int(m.group(0), 0)
|
|
return str(v)
|
|
return re.sub(r'-?0x[0-9a-f]+|-?\b\d+\b', canon, s)
|
|
|
|
def validate():
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import derive860 as D
|
|
base = r'C:\VWE\TeslaRel410\sda4\DPL3\VRENDER\AS860'
|
|
# operand check only for ops whose operands are NOT link-time relocated
|
|
OPCHECK = ('ld.', 'st.', 'fld.', 'fst.', 'adds', 'addu', 'subs', 'subu',
|
|
'and', 'or', 'xor', 'shl', 'shr', 'shra', 'mov', 'ixfr', 'bri')
|
|
for s in sorted(glob.glob(os.path.join(base, '*.S'))):
|
|
o = s[:-2] + '.O'
|
|
if not os.path.exists(o):
|
|
continue
|
|
labels, instrs = D.parse_S(s)
|
|
text, syms = D.load_text(o)
|
|
common = [k for k in labels if k in syms]
|
|
aligned = not any(labels[k] != syms[k] for k in common)
|
|
ok = tot = 0; oper_ok = oper_tot = 0; misses = []; omiss = []
|
|
for (off, mnem, line) in instrs:
|
|
if off + 4 > len(text):
|
|
break
|
|
w = struct.unpack_from('<I', text, off)[0]
|
|
dm, dops = decode(w, off)
|
|
tot += 1
|
|
if dm.split('.')[0] == mnem.split('.')[0] or \
|
|
dm.split('.')[0].lstrip('p') == mnem.split('.')[0] or \
|
|
(mnem.startswith('pf') and dm.startswith('pf')):
|
|
ok += 1
|
|
# operand-level check where the source operands are literal
|
|
if mnem.startswith(OPCHECK) and dm == mnem:
|
|
src_ops = line[len(mnem):].strip()
|
|
if not any(c.isalpha() and c not in 'rxf' for c in
|
|
src_ops.replace('r', '').replace('f', '')
|
|
.replace('x', '')):
|
|
oper_tot += 1
|
|
if _norm_ops(src_ops) == _norm_ops(dops):
|
|
oper_ok += 1
|
|
elif len(omiss) < 8:
|
|
omiss.append(f"@{off:#x} {mnem} src[{src_ops}] "
|
|
f"dec[{dops}] w={w:#010x}")
|
|
else:
|
|
if len(misses) < 8:
|
|
misses.append(f"@{off:#x} src={mnem} dec={dm} w={w:#010x}")
|
|
tag = os.path.basename(o)
|
|
flag = "aligned" if aligned else "MISALIGNED"
|
|
print(f"[{tag:14}] {flag:10} mnemonic {ok}/{tot}="
|
|
f"{100*ok//max(tot,1)}% operands {oper_ok}/{oper_tot}="
|
|
f"{100*oper_ok//max(oper_tot,1)}%")
|
|
if aligned:
|
|
for m in misses:
|
|
print(" mn-miss ", m)
|
|
for m in omiss:
|
|
print(" op-miss ", m)
|
|
|
|
def disasm_range(text, start, count, base=0x0c):
|
|
"""Disassemble `count` words of `text` from byte offset `start`.
|
|
`base` = file offset where .text begins (VREND.MNG = 0x0c), so printed
|
|
addresses are file/memory offsets."""
|
|
out = []
|
|
for i in range(count):
|
|
off = start + i*4
|
|
if off + 4 > len(text): break
|
|
w = struct.unpack_from('<I', text, off)[0]
|
|
# decode with the PRINTED address so branch targets match the listing
|
|
m, ops = decode(w, base+off)
|
|
out.append(f" {base+off:#08x}: {w:08x} {m:<10} {ops}")
|
|
return "\n".join(out)
|
|
|
|
def load_mng(path):
|
|
d = open(path, 'rb').read()
|
|
tsize = struct.unpack_from('<I', d, 0)[0]
|
|
return d[0x0c:0x0c+tsize]
|
|
|
|
if __name__ == '__main__':
|
|
if '--validate' in sys.argv:
|
|
validate()
|
|
elif '--list' in sys.argv:
|
|
# dis860.py --list <mng> <hexoffset> <count>
|
|
a = [x for x in sys.argv if not x.startswith('--')][1:]
|
|
mng = a[0]; start = int(a[1], 0); count = int(a[2]) if len(a) > 2 else 32
|
|
print(disasm_range(load_mng(mng), start, count))
|
|
else:
|
|
print("usage: dis860.py --validate | --list <VREND.MNG> <off> <count>")
|