Files
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

132 lines
5.3 KiB
Python

"""Name functions in the stripped VREND.MNG by matching relocation-invariant
signatures harvested from the DPL3 VRENDER .O object files.
For each .O .text function we mask (wildcard) every 32-bit word that a COFF
relocation touches -- those hold call targets / data addresses that the linker
rewrites -- and keep the fixed opcode+register skeleton. A function that carried
over into VREND.MNG (even relocated to a new address) still matches its
skeleton. Word granularity throughout (i860 is fixed 4-byte, word-aligned)."""
import sys, struct, os, glob
def parse_coff(path):
d = open(path, 'rb').read()
magic, nscns, timdat, symptr, nsyms, opthdr, flags = struct.unpack_from('<HHIIIHH', d, 0)
secs = []
base = 20 + opthdr
for i in range(nscns):
o = base + i*40
name = d[o:o+8].split(b'\0')[0].decode('latin1')
paddr, vaddr, size, scnptr, relptr, lnnoptr, nreloc, nlnno, sflags = \
struct.unpack_from('<IIIIIIHHI', d, o+8)
secs.append(dict(name=name, vaddr=vaddr, size=size, scnptr=scnptr,
relptr=relptr, nreloc=nreloc))
# symbols
strbase = symptr + nsyms*18
def symname(o):
z, offs = struct.unpack_from('<II', d, o)
if z == 0:
e = d.index(b'\0', strbase+offs); return d[strbase+offs:e].decode('latin1')
return d[o:o+8].split(b'\0')[0].decode('latin1')
syms = []; i = 0
while i < nsyms:
o = symptr + i*18
nm = symname(o)
value, scnum, typ, sclass, naux = struct.unpack_from('<iHHBB', d, o+8)
syms.append((nm, value, scnum, sclass)); i += 1 + naux
return d, secs, syms
def text_section(secs):
for s in secs:
if s['name'] == '.text': return s
return None
def sig_for_functions(path, min_words=6):
"""yield (name, [word|None ...]) skeletons for each .text function."""
d, secs, syms = parse_coff(path)
t = text_section(secs)
if not t or t['size'] == 0: return
text = d[t['scnptr']:t['scnptr']+t['size']]
nwords = t['size'] // 4
reloc_word = set()
for r in range(t['nreloc']):
rva = struct.unpack_from('<I', d, t['relptr'] + r*10)[0]
reloc_word.add(rva // 4) # wildcard the whole word
# function boundaries = .text EXT/STAT symbols, sorted
fsyms = sorted([(v, nm) for (nm, v, sn, sc) in syms
if sn == 1 and sc in (2, 3)], key=lambda x: x[0])
for i, (v, nm) in enumerate(fsyms):
end = fsyms[i+1][0] if i+1 < len(fsyms) else t['size']
w0, w1 = v // 4, end // 4
if w1 - w0 < min_words: continue
skel = []
for w in range(w0, w1):
if w in reloc_word: skel.append(None)
else: skel.append(struct.unpack_from('<I', text, w*4)[0])
if sum(1 for x in skel if x is not None) < min_words: continue
yield nm, skel
def load_mng_text(path):
d = open(path, 'rb').read()
tsize, dsize, bsize = struct.unpack_from('<III', d, 0)
text = d[0x0c:0x0c+tsize]
return text, tsize
def build_index(text):
idx = {}
for p in range(len(text)//4):
w = struct.unpack_from('<I', text, p*4)[0]
idx.setdefault(w, []).append(p)
return idx
def match(skel, text, idx, thresh=0.75):
"""Anchor on the first concrete word, then SCORE the concrete words.
Returns (best_addr, best_frac, unique) so drifted functions still match on
their stable prologue/skeleton."""
k = next((i for i, x in enumerate(skel) if x is not None), None)
if k is None: return None
concrete = [i for i, x in enumerate(skel) if x is not None]
best = (None, 0.0); second = 0.0
for p in idx.get(skel[k], []):
start = p - k
if start < 0 or (start + len(skel))*4 > len(text): continue
good = 0
for i in concrete:
if struct.unpack_from('<I', text, (start+i)*4)[0] == skel[i]:
good += 1
frac = good / len(concrete)
if frac > best[1]:
second = best[1]; best = (start*4, frac)
elif frac > second:
second = frac
if best[0] is not None and best[1] >= thresh:
return (best[0], best[1], best[1] > second + 0.05)
return None
def main():
mng = sys.argv[1] if len(sys.argv) > 1 else r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
odir = r'C:\VWE\TeslaRel410\sda4\DPL3\VRENDER'
text, tsize = load_mng_text(mng)
idx = build_index(text)
print(f"VREND.MNG .text = {tsize:#x} bytes ({tsize//4} words); {os.path.basename(mng)}")
named = {} # addr -> (name, frac)
nsig = 0
for opath in sorted(glob.glob(os.path.join(odir, '*.O')) +
glob.glob(os.path.join(odir, 'PXPL5SUP', '*.O'))):
for nm, skel in sig_for_functions(opath):
nsig += 1
r = match(skel, text, idx)
if r is None: continue
addr, frac, uniq = r
if not uniq: nm = nm + "?"
if addr not in named or frac > named[addr][1]:
named[addr] = (nm, frac)
strong = sum(1 for (n, f) in named.values() if f >= 0.95)
print(f"signatures tried={nsig}, named={len(named)} "
f"(exact/near-exact >=0.95: {strong})")
for addr in sorted(named):
nm, frac = named[addr]
print(f" text+{addr:#08x} (mem {0x0c+addr:#08x}) {frac:0.2f} {nm}")
if __name__ == '__main__':
main()