Predator/IR vision: reverse-engineered from the original firmware and
confirmed by the build team -- it is the Division board's TEXTURE-VALUE RAMP
mode (a "check your texture maps" diagnostic the devs hijacked), NOT a
grayscale squash or a false-colour palette. Located in VREND.MNG (effect
handler @0xe6c0, wire action 0x1b, type -1 ON / -2 OFF); ramp colours from
VR_DRAW.C. Renderer reworked to match: vrview_gl now does the 4-ramp
lerp(color0,color1,luminance(texel)) in the mesh pass (grayscale+defog
removed). Live-rendered on a new night-clear arena egg; crew A/B verdict
pending.
Firmware-decomp toolchain (emulator/firmware-decomp/), all built from the
project's own artifacts and validated:
- coff860.py i860 COFF reader (symbols/sections), names match AS860 source
- derive860.py derives the i860 opcode map from matched .S<->.O pairs
- dis860.py i860 disassembler (98% on clean ground truth; proven on
VREND.MNG -- velocirender_statistics decodes correctly)
- sigmatch860.py reloc-invariant signature matcher onto the stripped image
- i860-encoding.md / FIRMWARE-SYMBOLS.txt / README.md
PVISION-IMPLEMENTATION-GUIDE.md: self-contained hand-off for the BT411 team.
HARDWARE-ARCHITECTURE.md + hardware-photos/ (15 board shots): the Division
VelociRender card is a 2-board stack driving a 3-processor pipeline --
INMOS IMS T425-J25S (comms/control, runs vrendmon.btl) + Intel i860 XP-50 (FP
geometry, runs vrender.mng) + Division PXPL IGC 5.2 ASIC with ~48x PXPL EMC
5.1 (UNC Pixel-Planes-5 SIMD array; "EMC" = the firmware's configEMCs) +
Analog Devices ADV7150 RAMDAC + NTSC. Plus the VWE Video Distribution Board
(P/N 1404: AMD MACH130 + 3x Brooktree Bt477) for the 3-VGA-head cockpit split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
220 lines
9.3 KiB
Python
220 lines
9.3 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 ----
|
|
# immediate-form rule: for load/store (0x00-0x0b) and arith/logic (0x20-0x3f),
|
|
# the ODD opcode of each pair is the 16-bit immediate/const form.
|
|
MEM = {0x00: "ld.s", 0x01: "ld.s", 0x04: "ld.l", 0x05: "ld.l",
|
|
0x06: "st.l", 0x07: "st.l", 0x08: "fld.l", 0x09: "fld.l",
|
|
0x0a: "fst.l", 0x0b: "fst.l"}
|
|
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:
|
|
m = MEM[op]
|
|
off = s16(imm & 0xfffc) if 'l' in m else s16(imm)
|
|
if m.startswith('f'):
|
|
rd = FREG[dest]
|
|
else:
|
|
rd = REG[dest]
|
|
if m.startswith('st') or m.startswith('fst'):
|
|
return m, f"{rd},{off:#x}({REG[src2]})"
|
|
return m, f"{off:#x}({REG[src2]}),{rd}"
|
|
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 bits; calli common
|
|
if (w & 0x7ff) == 0 or True:
|
|
return "calli", f"{REG[src1]}"
|
|
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 & 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>")
|