Files
TeslaRel410/emulator/firmware-decomp
CydandClaude Opus 4.8 ef4a0b67ac i860 emu: fix 6 core interpreter bugs; boot+velocirender_init+do_init now run clean
Worked the full-render marathon by tracing faults in the real VREND.MNG
firmware and matching them to the ground-truth AS860 assembly (VR_REMOT.S)
and C source (VR_REMOT.C). Six interpreter bugs found and fixed:

1. subs/subu direction: computed src2-src1; must be src1-src2 with
   CC=(src1<src2). Corrupted every subtraction, compare and bounds-check.
2. logical CC: all i860 logicals (and/andnot/or/xor + .h) set CC=(result==0).
   Had wrongly limited it to the AND family, so the `xor 0x0,rN,r0; bnc`
   zero-test idiom spun forever.
3. STORE encoding (the big one): i860 stores encode the SOURCE register in
   bits 15:11 (src1), not the dest field, and SPLIT the 16-bit offset across
   bits 20:16 (high) + bits 10:0 (low). The old decode saved the wrong
   register to the wrong offset, so function prologues never stored r1 and
   every `bri r1` return jumped to 0.
4. ld.b/st.b: op 0x00/0x01 = ld.b, op 0x03 = st.b (byte). Proven by byte-scan
   loops (r5 advanced by 1) and a save/restore pair at 0xf042b418 -> b430.
5. Mem page-span: r16/r32/w16/w32 crashed on 64KB page boundaries; now fall
   back to byte access across the boundary.
6. fmlow.dd (FP subop 0x21): the i860 has no integer multiply, so ints are
   ixfr'd into FP regs, multiplied via fmlow.dd, and fxfr'd back. Implemented
   as a 32x32 -> 64 multiply across the destination register pair.

emu_replay.py now runs the source-accurate init sequence: velocirender_init
on the init(0) command, then do_init before the first real command. Result:
boot idles at 0xf0400590; velocirender_init returns; do_init runs to
completion (~9200 steps of real allocator / name-table / scene-root setup).

Remaining blocker (documented in the tier-0 memory): create()'s switch needs
register-indexed integer loads (VR_REMOT.S: `ld.l r30(r31),r31`) together
with the correct .data link base (~0x1000, not 0) -- the two errors currently
cancel for the immediate paths but break the indexed jump-table dispatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 09:35:44 -05:00
..

VelociRender i860 firmware — decompilation reference

Purpose: make this project the authoritative answer for how the Division / VelociRender board firmware behaves (e.g. what IR/"predator" vision actually does). Started 2026-07-14.

Boot architecture (what gets loaded, and how)

The i860 render board is booted transputer-style (boot-from-link) every cold start — see sda4/DPL3/TESTVR.BAT:

test /tranny vrendmon.btl /i860 vrender.mng /link_A 3 /device 0x150 /video NTSC /n_860s 1
  • /tranny vrendmon.btl — the bootstrap monitor (~85 KB). Loaded first over the link while the i860 is held in boot-from-link.
  • /i860 vrender.mng — the actual renderer application (~380 KB; RPLIVE/VREND.MNG, BTLIVE/VREND.MNG). Produced by I8602MNG.EXE from the linked COFF. This is where the renderer logic lives — including pvision. The .BTL is only the loader.

Load is blind, every boot, no version check — the board is reset (\x00RSET strobe on port 0x160) and the host streams the image as wire actions code860/data860/bss860/args860/hspcode; a reset board can't answer a presence/version query, so the push is unconditional. dpl_VERSION's firmware_*_version fields are read after boot, for reporting only. (Same model as the sound cards.) ~465 KB/boot; contributes to boot time, worse in emulation (trapped link I/O + the bridge's fixed 0.5 s idle handshake).

What's here

  • coff860.py — validated i860 COFF (magic 0x014d) reader: file header, sections, symbol table. Names verified against the AS860/PGI source (_SemWait, _nameToAddress, _createBang, …). Usage: python coff860.py "…/VRENDER/*.O".
  • FIRMWARE-SYMBOLS.txt — full symbol/section map of the DPL3 VRENDER objects (32 modules, 633 defined symbols). The function inventory of the renderer, straight from the object files' symbol tables.

The .O objects live in the git-ignored dump (sda4/DPL3/VRENDER/*.O); they are the DPL3 SDK vintage (199495) — same era as the C/asm source we have, so they predate the pvision feature. Their value: (1) they validate the toolchain/format, (2) they name the board-side machinery, and (3) they are the signature library for naming functions in the stripped, newer VREND.MNG.

pvision — where the answer is, and the anchors we have

The game only flips a switch (L4VIDEO.CPP::DPLTogglePVision sends a special "explosion" effect at the origin, type = -1 ON / -2 OFF — no colour). So the behaviour is board-side. Operator's working model: grayscale squash + fog OFF, not a false-colour palette.

Confirmed anchors from the symbol map, both in EOF.O (end-of-frame / DAC):

  • Fog machinery lives in board globals: _eof_doFOG, _eof_FOG_rval/gval/bval, _eof_FOG_near/far, _eof_backR/G/B, _back_colour. A "fog off" in pvision = clearing _eof_doFOG.
  • Colour/DAC path: _tblcpy, _configEMCs, _send_em, and the "texture colour map table (8×32-bit)" hacked per-frame in EOF.C.
  • DNC.C::luminize exists but is a texture-MIP helper (24-bit texel → 6-bit luminance), not the screen grayscale — a near-miss, noted so nobody re-chases it.

The effect dispatch is VR_REMOT.O::_remote_velocirender → the SFX handlers (SFX.O, _PAZsfx). In our source vintage PAZsfx only knows explosion codes 0/1/2 (no 1/2), confirming pvision is newer code — i.e. only in VREND.MNG, which has no symbols.

Progress: the i860 disassembler (scope = FULL annotated, operator-chosen)

No objdump/Ghidra i860 support exists, so we build the decoder ourselves, validated against the AS860 .S.O pairs (exact ground truth).

  • Word format cracked & CONFIRMED — REG/CTRL layout, control-reg numbers, from CONTROL.O (see i860-encoding.md).
  • Primary opcode map validatedderive860.py aligns .S.O by proving every label offset against the COFF symbol table, then harvests op6→mnemonic. Clean data matches the published i860 map. Table in i860-encoding.md.
  • Decoder built & validateddis860.py; 98% base-mnemonic match on the clean pair (XFLGHTPR 220/224). Residual misses are .S/.O drift in TRISTRIP, not decode errors, plus 4 cosmetic FP graphics-op variant names.
  • Disassembler proven on real firmwaredis860.py --list VREND.MNG 0xe940 decodes _velocirender_statistics as adds 0x1,r0,r29 … bri r1 (loads 1, returns) — matching what our virtual board replies for that wire command (vrboard do_statistics → 1). Independent end-to-end confirmation.
  • [~] Name functions in VREND.MNGsigmatch860.py (reloc-invariant, tolerant). Anchors landed (velocirender_statistics, reg_dump, tri_zb_d_s_texm, dpl_init_light…) but the only objects we have are 1994 DPL3/VRENDER vs the 1996 .MNG, so C code has drifted and dense naming isn't possible from signatures alone. The stable hand-asm rasterizers/math match; navigate the rest from these anchors + the wire dispatch. - Load format (reversed): header = 3 LE size words at offset 0 (.text=0x39ec0, .data=0x1e940, .bss=0x4b40); raw .text begins at file offset 0x0c (0xf0400000 = entry veneer); .data follows; a tiny section-name stub (.text/.data/.bss) trails — no symbol table (stripped image, so signatures are required). - Signatures must be relocation-invariant: mask the disp/immediate fields of call/br/ld/st-with-address before matching .O code against the linked .text.
  • Annotate — whole-image reference; then walk the effect path (_remote_velocirender → SFX) to the type == -1 branch and read what it does to _eof_doFOG and the DAC/colour map, settling pvision.

The endgame: preservation, not interpretation

dpl3-revive (the GL bridge) interprets the wire — a modern reconstruction of what the protocol means. Valuable for a playable pod, but it is our reading of the hardware. The archival-correct path is to run the original VREND.MNG in an i860 emulator: the board's own code builds the display lists, runs the real SCANLINE.SS rasterizer, programs the DAC, and writes a framebuffer we capture — bit-exact to the hardware. Every fidelity question (pvision, point sampling, fog, gamma, filtering) then stops being "did we interpret this right?" and becomes "the board's code did this."

This disassembler is the front half of that emulator (same decode; add execution semantics). Two tiers:

  1. Functional-accurate — i860 core + FP + the board's memory-mapped peripherals (link/DMA, the _configEMCs units, DAC, video timing, reversed from the firmware + AS860 source). Yields bit-exact images. The big prize.
  2. Cycle-accurate — model the i860 dual-issue pipeline / FP latency / delayed branches. Adds timing fidelity (frame rate, the long-mission render decay). Hardest; last.

Keep the GL bridge as the fast/interactive path; the i860 emulator becomes the high-fidelity reference — and the oracle that says whether the bridge is right.

Live disassembly findings (RPLIVE/VREND.MNG)

The wire-command dispatch table is decoded and mapped (per-action case block [mov r16,r4; call <handler>; mov r8,r16; br exit]; the dispatcher indexes by action code). Confirmed against our virtual board:

action handler action handler
9 draw_scene 0xe340 16 readpixels 0xeb78
10 draw_scene_complete 0xe4c0 17 hspcode 0xec80
11 list_add 0xe6c0 18 code860 0xece0
12 list_remove 0xe780 19 data860 0xed60
13 morph 0xe810 20 bss860 0xed90
14 version 0xe888 21 args860 0x56a0
15 statistics 0xe940 (returns 1 ✓) 22 set_geom_verts 0x5710
23 set_texmap_texels 0x5738

Higher actions (the effect/psfx family, incl. pvision type -1/-2): handlers at 0xedd8, 0xee14; action-27 case is the default/error path (loads a string ptr, calls the log util 0x2c330). 0xee14 = the effect handler — prologue reads [msg+0] into r29 (the effect type), [msg+4] into r30, logs via 0x4408, then branches (btne @0xee4c → 0xee7c; calls 0xb688, 0x2c330).

NEXT (pvision, definitive): walk 0xee14's branches to the type == -1/-2

case and read what it writes — the board-side fog disable (_eof_doFOG) and any DAC/colour-map change. That is the ground-truth answer to grayscale-squash+defog vs a palette. (Anchor utils seen: 0x2c330 = most-called util/log, 0x4408 = logger, 0xb120/0xb688 = effect helpers.)

Tier 0 emulator: i860 interpreter (emu860.py) — BOOTS THE FIRMWARE

emu860.py executes the real VREND.MNG on an emulated i860 (reusing dis860's decode). First run (python emu860.py <VREND.MNG> --trace 30): 5000 instructions, zero unimplemented ops. It runs the firmware's i860 boot sequence correctly — psr/dirbase/fsr setup, a board-register write at 0x8380_7000, then it starts polling the CCB at 0xFFFFF7xx (the CM200IO.H INPUT_CCB region), dispatching on IO_BREAK(1)/IO_ACK(2). I.e. the i860 is waiting on the transputer for work — exactly the T425↔i860 model.

Validated working: integer arith/logic/shift, loads/stores, control regs, branches + delay slots, basic single-precision FP.

NEXT (Tier 0 cont.):

  1. Model the CCB handshake (0xFFFFxxxx: INPUT_CCB 0xF000/OUTPUT_CCB 0xF300, CLEAR_INT, the IO_* opcodes) so the firmware advances past the poll loop into its main command loop. Stub the transputer side: feed a captured wire in, ACK the CCB.
  2. Model the board register region (0x8380_xxxx) as it's touched.
  3. Add remaining instructions as the geometry path needs them (dual-instruction mode, the pipelined pf*/graphics FP ops).
  4. Capture the IGC coefficient stream the firmware emits (Tier 1 handoff).

Then Tier 1 = rasterize the captured coefficients; Tier 2 = the T425 + timing.

Tier 0 update: full boot mapped → idle (waiting for transputer interrupt)

With map_control() feeding the board-config regs (0xfffff720=2, 0xfffff70c=1), the firmware runs its whole low-level boot in ~73 instructions: clear the control-register block, write POST-style status codes, enable the MMU (dirbase ATE), then idle-spin at 0xf0400590 (bri r1→self). That's init-complete — a self-spin breaks only on an interrupt, so the i860 is waiting for the transputer to hand it CCB commands. The whole application runs in the interrupt handler, not the boot path.

So the next phase is the command-processing path:

  1. i860 interrupt/trap (INT → PC←0xFFFFFF00) + the MMU (ATE is now on; try identity-map first).
  2. The CCB struct + a transputer stub that writes a command and asserts the interrupt.
  3. Inject a captured wire command → the handler dispatches (jump table @0x47cb8) → runs → emits IGC coefficients (Tier-1 handoff).

emu860 debug aids added: map_control(), tail ring-buffer, stopat.

Tier 0 update: call harness + real globals + FP core (draw_scene runs real code)

Autonomous session progress on emu860.py:

  • Call harness (cpu.call(addr, args)): invoke firmware functions directly (PGI conv: args r16.., ret r16, r1=return, r2=sp), bypassing the un-emulated transputer. Validated on velocirender_statistics.
  • Data placement FIXED: .data/.bss are linked LOW (code builds global addrs 0xebc4..0x22644). DATA_BASE=0x0 (verified: globals now carry real data, e.g. [0xebc4] is ASCII, [0x10]=0xffffffff). Was the reason cold calls saw zero globals.
  • FP unit rewritten with DOUBLE precision (register pairs f[N]=low/f[N+1]=hi): fadd/fsub/fmul/famov/frcp/frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub, precision from bits 7/8. (pf* pipelined ops treated non-pipelined for now.)
  • Result: draw_scene now executes real code (299 instrs incl. a double int→float conversion helper) until it needs SCENE STATE — cold-calling it on an empty scene follows uninitialised scene pointers and derails. Confirms a frame needs the full wire sequence replayed first.

NEXT (the command-replay phase): find the top-level command handler (velocirender_input/remote_velocirender; dispatcher head ~0xf040eeb0, jump table @file 0x47cb8), feed a captured wire command sequence (create/dcs/ list_add → draw_scene) via a CCB struct at 0xffffe000, so the scene builds and draw_scene renders → capture the IGC coefficient stream (Tier-1 handoff). Boot config regs: map_control() sets 0xfffff720=2, 0xfffff70c=1. Note the transputer set up the MMU/page-tables + trap vector, so the natural interrupt boot-flow needs the transputer OR the direct-call harness (used here).