Files
TeslaRel410/emulator/firmware-decomp/README.md
T
CydandClaude Fable 5 e28232e409 Tier 0 emulator: i860 interpreter boots VREND.MNG + runs real render code
emu860.py -- an i860 interpreter (reuses the dis860 decoder) that executes the
real Division firmware. This is the foundation of the preservation-grade
emulator (run the board's own code, vs the GL bridge which interprets the wire).

- Boots VREND.MNG cleanly: i860 init (psr/dirbase/fsr), enables the MMU, then
  idle-spins at 0xf0400590 waiting for a transputer interrupt (init complete).
  Board-config regs modelled (0xfffff720=2, 0xfffff70c=1) via map_control().
- Call harness (cpu.call): invoke firmware functions directly, bypassing the
  (un-emulated) transputer that normally drives the CCB.
- Correct memory map: .data/.bss linked LOW (DATA_BASE=0), so globals resolve.
- FP unit with double precision (register pairs): fadd/fsub/fmul/famov/frcp/
  frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub.
- Delay slots, control regs, sparse MMIO memory with trap/logging, tail-trace
  and stopat debug aids.

draw_scene now runs real code (incl. a double int->float conversion) until it
needs accumulated scene state -- next step is replaying a captured wire command
sequence through the command dispatcher (jump table @0x47cb8) so the scene
builds and draw_scene emits the IGC coefficient stream (Tier-1 handoff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:38:25 -05:00

230 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
- [x] **Word format cracked & CONFIRMED** — REG/CTRL layout, control-reg
numbers, from `CONTROL.O` (see `i860-encoding.md`).
- [x] **Primary opcode map validated**`derive860.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`.
- [x] **Decoder built & validated**`dis860.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.
- [x] **Disassembler proven on real firmware** — `dis860.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.MNG`** — `sigmatch860.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).