Files
TeslaRel410/emulator/firmware-decomp/dis860.py
T
CydandClaude Fable 5 1323397a50 pvision solved (texture-value ramp) + i860/hardware reverse-engineering
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>
2026-07-14 22:29:53 -05:00

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