Files
TeslaRel410/emulator/firmware-decomp/PVISION-IMPLEMENTATION-GUIDE.md
T
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

194 lines
8.2 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.
# Implementing IR / "Predator" Vision in a BattleTech (VelociRender) renderer
**Audience:** the BT411 renderer + whoever's Claude is implementing it.
**Status:** reverse-engineered from the original VelociRender i860 firmware
(`VREND.MNG`) and the DPL3/VRENDER board source, and confirmed by original-team
recollection. This document is self-contained — the key code is quoted inline so
you don't need our tree.
---
## TL;DR
Predator/IR vision is **not** a grayscale squash, **not** a palette swap, and
**not** a scene-luminance heat map. It is the Division board's **texture-value
ramp mode** — a diagnostic the board provided for *checking texture maps* that
the game developers hijacked as the in-game IR effect.
Each **textured** surface is drawn as:
```
out_rgb = lerp(color0, color1, luminance(texel))
```
where `luminance(texel)` is the brightness of the sampled **texel** (the texture
map's value, *not* the lit/shaded pixel), and `(color0, color1)` are the two RGB
endpoints of the **ramp assigned to that surface's material**. There are four
ramps (gray / red / green / blue → white). Because different materials use
different ramps, the scene comes out **multicolor**.
---
## 1. How the game turns it on (host side)
The game's renderer object (`DPLRenderer`, shared by BattleTech and Red Planet)
has a toggle. From the original source (`MUNGA_L4/L4VIDEO.CPP`,
`DPLRenderer::DPLTogglePVision`):
```c
static Logical pvision_on = 0;
dpl_EXPLOSION_EFFECT_INFO sfx_info;
sfx_info.x = sfx_info.y = sfx_info.z = 0;
if ((pvision_on ^= 1) != 0) sfx_info.type = -1; // pvision ON
else sfx_info.type = -2; // pvision OFF
dpl_Effect(dpl_effect_type_explosion, NULL, &sfx_info);
```
Key point: **it sends no colour.** It fires a bogus "explosion" effect at the
origin whose `type` field is a magic flag: `-1` = ON, `-2` = OFF. The
`dpl_EXPLOSION_EFFECT_INFO` struct is `{float x,y,z; int32 type; dpl_TEXTURE*}`
— there is no colour to carry. So the palette is **entirely board-side**; the
host only flips a switch.
On the VelociRender wire this arrives as the effect action (in our decode,
action `0x1b`), payload first word = `type`. If you drive a board/emulator, the
handler is: `type >= 0` → normal explosion; `type == -1/-2` → set/clear the
pvision mode flag.
---
## 2. What the board does (firmware)
The board firmware selects a **texture display mode** per frame
(`dvpx_eoftexmode`, from the end-of-frame code `VRENDER/PXPL5SUP/EOF.C`):
| mode | meaning |
|------|---------|
| `0` | **texture RAMP** — texel value → interpolated colour ramp (this is IR vision) |
| `0x4` | 8-bit monochrome |
| `0x6` | 8-bit 3-3-2 full colour |
| `0x7` | 12-bit 4-4-4 full colour |
Normal rendering uses the full-colour modes. `pvision ON` flips the whole scene
into **mode 0 (ramp)**. The end-of-frame code literally linear-interpolates each
channel across the texel value:
```
/* now access the texture colour map table (8 32-bit entries) */
/* now linterp from r0 to r1 */ // Red across texel value
/* now linterp from g0 to g1 */ // Green
/* now linterp from b0 to b1 */ // Blue
```
The "texel value" is a 6-bit luminance of the 24-bit texel
(`VRENDER/DNC.C::luminize` — "returns a 6-bit luminance value from a 24-bit
texel"). Think of it as `value = luminance(texel)`, `value ∈ [0,1]`.
---
## 3. The ramps
A ramp is two RGB endpoints (`dpl_RAMP { float32 color0[3]; float32 color1[3]; }`).
`color0` = the colour for **dark** texels (value 0), `color1` = the colour for
**bright** texels (value 1). Four ramps are installed once at render init
(`VRENDER/VR_DRAW.C`, `setRampEntry(ramp, index, r0,g0,b0, r1,g1,b1)`):
| ramp | color0 (dark) | color1 (bright) | look |
|------|---------------|-----------------|------|
| 0 | `0.0, 0.0, 0.0` | `1.0, 1.0, 1.0` | black → white (grayscale) |
| 1 | `0.3, 0.0, 0.0` | `1.0, 1.0, 0.9` | dark red → warm white |
| 2 | `0.0, 0.5, 0.0` | `1.0, 1.0, 1.0` | green → white |
| 3 | `0.0, 0.0, 0.4` | `0.9, 0.9, 1.0` | blue → cool white |
Each **material** references one of these by an integer `ramp_entry` (03), so a
mech built from several materials shows several colour families at once — that's
the multicolor IR look, and it's why the tool was useful for *checking* which
material/ramp a surface used.
> **Provenance caveat:** those RGBs are the source defaults. The shipped 1996
> `VREND.MNG` build tweaked the exact floats (the constant `0.9` is absent from
> the binary — the bright endpoints were likely rounded toward pure white). The
> *structure* (four gradients from a dark saturated colour to ~white, indexed by
> texel value) is unchanged. Use the table above; nudge the two `0.9`s to `1.0`
> if you want to match the shipped build more closely.
---
## 4. How to implement it in your renderer
The recolour must happen **at texturing time in the scene pass**, on the sampled
texel — *not* in a post/present pass on the final pixel, because you need the
raw texel value, not the lit result.
Per textured fragment, when pvision is active:
```glsl
// u_pv : 1 when IR/predator vision is on
// u_ramp0/1 : the two RGB endpoints of THIS surface's ramp (from ramp_entry)
if (u_pv == 1) {
float value = clamp(dot(texel.rgb, vec3(0.299, 0.587, 0.114)), 0.0, 1.0);
frag_rgb = mix(u_ramp0, u_ramp1, value);
}
// else: normal shading (frag_rgb = texel * lighting, etc.)
```
Wiring notes:
1. **Ramp selection = the material's `ramp_entry` (03).** Set `u_ramp0`/`u_ramp1`
from `PV_RAMP[ramp_entry]` per draw call. If your pipeline doesn't yet track
`ramp_entry`, pick a **stable** ramp per surface (e.g. `hash(material_id) & 3`)
so different objects get different families — this reproduces the multicolor
look until you can plumb the real per-material index.
2. **Apply to all textured world geometry** — mechs, terrain, buildings, sky.
3. **Untextured surfaces:** use the shaded base colour's luminance as the value
(there's no texel), or leave them as-is; they're a minor case.
4. **HUD / MFDs are separate** and must *not* be ramped — they're a different
display path (the cockpit's mono/color panels), not the out-the-window scene.
5. **Fog is orthogonal.** The ramp mode is a texture-colour stage; it does not
touch the fog/haze stage. Do **not** disable fog for pvision unless you get
specific evidence otherwise. (We initially guessed "see through fog" and it
was wrong — the mode is purely about colour.)
6. **DAC gamma** (the board applies ~`pow(c, 1/1.25)` at scan-out) is unrelated
to pvision; keep whatever gamma you already do.
### Host trigger
If you emulate/interpret the wire, treat the effect action carrying `type == -1`
as "pvision on" and `type == -2` as "off"; `type >= 0` is a real explosion.
If you drive rendering directly from game state, hook `DPLTogglePVision`.
---
## 5. What NOT to do (things we tried that are wrong)
- ❌ Grayscale squash of the frame.
- ❌ False-colour heat/green/amber palette applied to **scene** luminance.
- ❌ Recolouring in a present/post pass using final-pixel luminance.
- ❌ Disabling fog as part of the effect.
- ❌ Expecting the game to send colours — it sends only the on/off flag.
The correct model is the four-ramp, per-material, **texel-value** mapping above.
---
## 6. How to verify against the originals
Everything here is checkable in the VelociRender materials:
- Host trigger: `MUNGA_L4/L4VIDEO.CPP` `DPLTogglePVision` (BT and RP both link
the same `DPLRenderer`).
- Ramp mechanism & modes: `DPL3/VRENDER/PXPL5SUP/EOF.C` ("lerp texture ramp",
`dvpx_eoftexmode`), `DPL3/VRENDER/DNC.C::luminize`,
`DPL3/VRENDER/DPLTYPES.H` (`dpl_RAMP`, `dpl_draw_luminance 0x04`).
- Ramp colours: `DPL3/VRENDER/VR_DRAW.C` (the four `setRampEntry` calls at
render init).
- Firmware (if you want ground truth from the shipped board image): in
`RPLIVE/VREND.MNG` / `BTLIVE/VREND.MNG` the effect handler is reached via the
wire-action jump table; it reads `msg[0]` as `type`, tests sign, and branches
`type==-1`/`type==-2`. Code loads at ~`0xf0400000`, globals low (~`0x10000`).
(Reversing that binary needs an i860 disassembler; the source above is easier.)
If your Claude has the shared dump, those paths are under `sda4/DPL3/VRENDER/`.
The Tesla preservation project (this repo) is the authoritative source for these
findings.