Files
CydandClaude Opus 4.8 a688cc1c30 i860 emu: ISA round 2 (ground-truth encodings) + authentic-main runner; firmware
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>
2026-07-15 11:26:10 -05:00

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