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>
230 lines
13 KiB
Markdown
230 lines
13 KiB
Markdown
# 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 (1994–95)** — 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).
|