Files
TeslaRel410/emulator/firmware-decomp/IG-SHADING-MODEL.md
T
CydandClaude Opus 4.8 66cef329d1 Implement fog: source-derived depth formula, not a guess
BTDPL.INI's fog values for this mission (testarn.egg = arena1/day/clear,
[ardayclear] leaf: fog=200.0 1250.0 0.32 0.3 0.5, clip=0.25 1300.0) are exact,
verified ground truth. The depth term took two tries: `tz` (texz) looked like
the right candidate (BT411 says fog is linear on "W") but measured live it
DECREASES with distance -- the wrong direction/shape. Re-derived from the
actual firmware source instead of guessing again: AS860/XFPROJ.SS's own
comment on the projection code ("pz=wz*invz, where pz=1 at near clip, 0 at
infinity") establishes the z-buffer value as a normalized hither/distance
quantity, giving distance = hither * 2^20 / raw_zbuf_value.

gpu_raster.py: added a 10th shader output channel carrying the raw z-buffer
float (previously only a filled/empty flag existed), and the fog blend using
this formula + BTDPL.INI's exact color. Verified offline: distance climbs
smoothly (394->1051) across a receding ceiling, landing inside the authored
200-1250 range with no further fudging; visually the horizon shows the
authored blue-purple haze, correctly absent near the camera.

IG-SHADING-MODEL.md: documented the false start (tz) and the source-grounded
fix, with the honest caveats that remain (2^20 exact scale, hither as a fixed
per-mission constant vs. per-view from the wire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:13:55 -05:00

266 lines
15 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.
# The IG-board shading model (ground truth from libDPL)
Authoritative reference for the software rasterizer, distilled from the **shipped
Division library headers** in the reconstructed game source:
- `C:\VWE\BT411\engine\MUNGA_L4\libDPL\dpl\dpltypes.h` — the public `dpl_*` enums
- `C:\VWE\BT411\engine\MUNGA_L4\libDPL\dpl\dpl.h` — the `dpl_Set*` API
- `C:\VWE\RP411\DivLoader\{VGCDivLoader.h,tagIdents.h}` — the `.biz`/BGF asset
format the same model is authored in (DivVertex, BIZ_IDENTITIES)
- `C:\VWE\BT411\context\rendering.md` / `docs\BGF_FORMAT.md` — how the modern
D3D9 port reproduces this same model (the ramp/BSL/effect-card fidelity work)
These are the host-side commands the game issued to the VelociRender board; our
firmware-decomp emulates the board that consumes them. Where our renderer differs
from this model, this file is the reference.
---
## 1. Vertex type mask — the per-geometry shading selector
`dpl_VERTEX_TYPE` (dpltypes.h:189) is a bit mask; it is exactly the `DivVertex.mask`
field in the asset format (VGCDivLoader.h:65):
| bit | token | meaning |
|------|-------------------------|--------------------------------------------|
| 0x01 | `dpl_vertex_coord` | has x,y,z (always) |
| 0x02 | `dpl_vertex_normal` | has Nx,Ny,Nz → **LIT** by the map light |
| 0x04 | `dpl_vertex_rgba` | per-vertex RGBA (effect cards) |
| 0x08 | `dpl_vertex_luminance` | per-vertex luminance `l` |
| 0x10 | `dpl_vertex_texture2D` | has u,v |
| 0x20 | `dpl_vertex_texture3D` | has u,v,w (perspective texture) |
| 0x40 | `dpl_vertex_radius` | sphere/point radius |
**The key rule (rendering.md §"IG-board shading model"):** shading is chosen PER
GEOMETRY by the vertex type:
- **No-normal geometry** (terrain / mesas / sky / buildings / mech — the WHITE
baked verts) is **UNLIT**, and colorized by the material's 2-endpoint RAMP.
- **Normal-bearing geometry** (~150 vehicle/missile files) is **LIT** by the map
light; the ramp is NOT applied (applying it = the "dusty white blobs" bug —
gate ramp on `!hasNormals`).
- Exception: cockpit-frame (`*_cop`) batches are pure-emissive constant colour.
---
## 2. Material ramp — how the board colours grayscale texels
`dpl_SetMaterialRamp` + `dpl_SetRampComponents(r0,g0,b0, r1,g1,b1)` (dpl.h:326,578):
a material carries a **2-endpoint colour ramp**. The (grayscale / bit-sliced)
texture's **luminance L∈[0,1] indexes the low→high gradient**:
```
out_rgb = rampLo + L * (rampHi - rampLo) # per channel
```
This is how the IG board coloured terrain from gray bit-slice art (rendering.md /
bgfload.cpp:341): rock `0.25,0.21,0.16 → 0.8,0.5,0.4` (warm tan); grass
`0.13,0.13,0.07 → 0.38,0.33,0.23`. Materials with no diffuse relied ENTIRELY on
the ramp.
> **Applicability to the wire-capture renderer:** the `0x1a` texmaps in the
> `netdeath` capture arrive as *already-colourised RGB* (HUD labels, emblems,
> panel/terrain art — 012% grayscale), i.e. the ramp is pre-baked on this path.
> So `render_final` must **not** re-apply a ramp (it would double-colour). The
> ramp model matters for a raw grayscale/BSL texmap path, not for these uploads.
---
## 3. Texture value model — wrap and alpha (implemented: `dpl_sampler.py`)
`dpl_SetTextureProperty(tex, prop, value)` (dpl.h:344) sets `dpl_TEX_PROP` fields
to `dpl_TEX_VALUE` tokens (dpltypes.h:103,121). The two that change rasterizer
*output*:
**WRAP** (`dpl_tex_prop_wrap`/`wrapu`/`wrapv`):
- `dpl_tex_value_repeat` — texel index wraps mod size (tiling terrain) — the board default
- `dpl_tex_value_clamp` — texel index clamps to the edge (a one-shot decal / HUD
label / emblem — tiling a "PLAYER 1" label is the wrong-wrap tell)
**ALPHA / cutout** (`dpl_tex_prop_alpha`):
There is **no stored alpha channel** — the texel word is `[pad, B, G, R]` (see §3a).
The board realises `dpl_tex_value_cut` (**PUNCH**: the cockpit/effect-card cutout
that retires the "opaque rectangle" reading — the wreck flame that read as a
"twisted drill bit", rendering.md §effect-cards) by **near-black keying** at draw
time: a texel whose `R+G+B <= ~24` is a HOLE (poly not drawn there, no z claim).
Ground truth: `dpl3-revive/patha/vrview_gl.py` ("alpha cutout (texel sum <= 24/255)").
Filtering (`point`/`bilinear`/`trilinear`/`mipmap_*`) is a quality axis; pvision
runs the board in texmode 0 = **point (nearest)**, which is what we model.
`dpl_sampler.py` implements wrap + near-black cutout (numpy-vectorized, with a
synthetic conformance self-test). Cutout is a per-MATERIAL property the wire
capture does not yet expose per drawn surface, so `render_final` defaults it OFF
(opaque) and leaves the hook for when a `SetTextureProperty` stream is decoded.
## 3a. Texel format — SVT `[pad, B, G, R]` (the ex-CYAN bug)
The `0x1a` upload's texel word is **`[pad, B, G, R]`** ("SVT xbgr") — ground truth
from the real renderer's own decode (`dpl3-revive/patha/vrboard.py:448` `do_texels`,
`stage_assets.py:43`). **Byte 0 is a PAD (0 in every texel across the capture);
the colour is bytes 1,2,3 = B,G,R, so RGB = bytes (3,2,1).** `mode` 1 is the
board's internal 8-bit storage path — the WIRE upload is always 4 bytes/texel.
`texstore.py` had been reading RGB as bytes (0,1,2) = `(pad, B, G)` — zero red —
which rendered the whole arena **CYAN**. Fixed to (3,2,1): the arena reads as its
true grayscale metal panels + coloured decals. This, not the texid binding (§5),
was the cyan.
---
## 4. Fog — the arcade model (not yet in the software renderer)
`dpl_FOG_TYPE` (dpltypes.h:238): the arcade uses **`dpl_fog_type_pixel_lin`** —
per-PIXEL linear fog on perspective W (rendering.md §fog: table fog on the
z-buffer collapses because the z-buffer is perspective-nonlinear; the board fogs
on W). Authored per map/time/weather as `fog = <near> <far> <r> <g> <b>` in the
DPL env INI (e.g. cavern/night/clear: near 90, far 1100, dark blue 0.1,0.1,0.12).
Night fog is dark BY DESIGN. Our renderer has the per-pixel Z/W planes to do this
faithfully; the near/far/colour are map-specific (not applied blind).
---
## 5. The remaining blocker — texid → texmap-handle binding
`render_final` still binds each drawn quad's 6-bit texid to an uploaded handle by
`texlist[tid % len]` — a placeholder. Investigated 2026-07-19/20: the draw's
texture-select word (op `0xf7`, ad 141/142) is **not** a handle — it is the
board's *resolved texture-descriptor reference* (per-texmap: high byte differs,
`0xf26a`/`0xf081`/`0xf1ea`/`0xcafa`…), computed at geometry-CREATE time from the
texmap (traced: the select-word value is written to scratch ~`0xbfc98` during the
`create` cmds, not the opaque texel store), and `battle_prog.pkl` is the
firmware's already-compiled program, so the game's material/texture *names* are
gone by then. Cracking it needs the deferred **board texture-RAM model**: RE the
firmware texmap→descriptor allocation and map the select word to an upload handle.
**Note (2026-07-20):** the monochromatic frame was NOT this binding — it was the
texel-format misread (§3a). With SVT decode fixed, most arena surfaces are
grayscale metal, so the frame reads correctly *despite* the placeholder binding;
the binding now only mis-tints the handful of coloured decals. Much lower priority.
**Escalation (2026-07-20, live-pod session):** with the reconstructed renderer
running live behind the actual game (not just replaying a static capture), this
binding gap became visually severe on **vehicles** ("trucks look like they were
painted with skittles" — user report). Vehicles are exactly the ~150
normal-bearing (LIT) BGF files that carry authored PAINT/decal materials
(rendering.md §"Per-pilot mech PAINT" — warning-stripe/badge/patch layers), so a
wrong-texture assignment here is far more visible than on a flat metal wall.
**No longer low priority — this is the top open item for texture correctness.**
Path forward (not yet done): live-instrument the firmware (we run the actual
i860 code via emu860c, so we can WATCH memory writes during `set_texmap_texels`
processing rather than guess from wire order) to find the real texid→handle
table the firmware builds, instead of the `tid % len(texlist)` placeholder.
---
## 6. Purpose of this pipeline — hardware ground truth for BT411 (2026-07-20)
This renderer's job is **not** to look good — it is to give the BT411
modernization-port developers a hardware-grounded, independently-verified
answer wherever the software renderer (`dpl3-revive`, the D3D9 port) has open
or hard-to-fix questions. Where this doc and BT411's `context/rendering.md`
overlap, treat agreement as cross-validation (two independent
reconstructions — firmware-level here, decomp-level there — agreeing) and
disagreement as a lead worth chasing on BOTH sides.
**Cross-reference index (BT411 `context/`):**
- `rendering.md` — the authoritative D3D9-port model: vertex-type shading
select (§1 above), material ramp (§2), fog (per-map/time/weather via
`BTDPL.INI`, per-pixel linear on W — NOT yet implemented in this renderer),
weapon beams (`ermlaser.bgf` TUBE geometry, muzzle-anchored, NOT a special
ray-trace primitive), mech skins (BSL bit-slice), per-pilot PAINT.
- `open-questions.md` — living list of everything BT411 has NOT yet resolved;
check before re-deriving something they've already nailed down (or vice
versa: this firmware pipeline may resolve one of THEIR opens).
- `combat-damage.md` — target acquisition (torso-boresight pick ray, auto-lock)
is RECONSTRUCTED and working in BT411's port; if our live session shows "no
weapon fire," suspect input/session state (fire button not reaching the
emulated RIO, or simply not fired yet this session) before suspecting a
missing render mechanism — the port's evidence is that firing itself works.
### 6a. The lit-color plane (r24/g24/b24) — REAL, mechanism confirmed, exact use UNRESOLVED
Verified live from the actual wire + firmware source (`sda4/DPL3/VRENDER/PXPL5SUP/`):
- `DIVPXMAP.H` defines `dvpx_r24/g24/b24` (addr 117/125/133, 8 bits each) as
scan-conversion-time fields; `EOF.C` (lines ~1456, 1742) copies them
straight into `dvpx_eofr/g/b` (184/192/200 — the final screen color) via a
24-bit `IGC_CPY` in the simple (unfogged) case.
- Confirmed present in the live per-draw coefficient program: `TREEclmpintoMEM`
(op 0x5a) at addr 118/126/134 (one bit off from the header's 117/125/133 —
this bytecode's own addressing, taken empirically), a 3-word instruction
whose 3rd word is a clamped float — REAL per-polygon values observed
(~49..395, not the same everywhere, roughly B > R > G — a genuine signal,
not noise).
- **What's still open:** applying this as a flat-poly color (§ current
gpu_raster.py) is a reasonable, low-risk use since flat polys had no real
data before (a placeholder gray). Applying it as a TEXTURE TINT is WRONG
(§5 caveat: textures arrive pre-colourised) — tried live 2026-07-20,
reverted same session (commit 0527022e) after the user reported "more
colors but neither correct." The exact relationship between this
BOARD-INTERNAL scan-conversion value and BT411's GAME-LEVEL ramp/lighting
model (§1-2) is NOT yet established — they may be the SAME quantity
(the board-side result of the ramp/lighting the game already computed and
is now shipping per-polygon) or a DIFFERENT one (e.g. a fog term). Next
step: cross-check a KNOWN lit vs unlit polygon (from BT411's vertex-type
rule) against its r24/g24/b24 value to see if unlit polys show the
material ramp's actual endpoint colors.
### 6b. Fog — IMPLEMENTED (2026-07-20), depth formula source-derived
BT411 (`rendering.md` §Fog) has the complete, verified model: per-map/time/
weather leaf in `content/BTDPL.INI` (`fog=<near> <far> <r> <g> <b>`), per-pixel
LINEAR fog on distance. For THIS mission (testarn.egg = arena1/day/clear) the
leaf is `[ardayclear]`: `clip=0.25 1300.0`, `fog=200.0 1250.0 0.32 0.3 0.5`
verified against the live game tree, exact match.
**The depth term was initially attempted with `tz` (texz) directly, which is
WRONG** — measured live (a vertical scan up a receding ceiling, row 0 to row
160): `tz` DECREASES with distance (324 -> 121), i.e. it behaves as an
inverse-depth quantity, not the "linear on W" BT411 describes. Re-derived from
actual firmware SOURCE instead of guessing again: `sda4/DPL3/VRENDER/AS860/
XFPROJ.SS`'s own comment on the projection code says `invz=eye_d/rz` and
`pz=wz*invz (where pz=1 at near clip, 0 at infinity)` — i.e. the z-buffer value
(`dvpx_zbuf`, 20-bit) is a NORMALIZED `hither/distance` quantity. So:
```
distance = hither * 2^20 / raw_zbuf_value
```
Implemented in `gpu_raster.py` (`FOG_HITHER/FOG_ZSCALE/FOG_NEAR/FOG_FAR/FOG_RGB`
+ the shader's new 10th output channel carrying the raw z-buffer float). Sanity
check (frame 11, netdeath capture, a receding ceiling): distance climbs
394 -> 1051 smoothly from the near edge to the horizon vanishing point,
landing squarely inside the authored 200-1250 fog range with no re-guessing
needed after the tz correction. Visually: the horizon now shows the authored
blue-purple haze (0.32,0.3,0.5), fading correctly toward the near floor.
**Not yet independently cross-validated:** the exact 2^20 scale (vs. 2^20-1 or
a slightly different fixed-point convention) and treating `hither` as a fixed
per-mission constant (0.25) rather than reading it per-view from the wire's
view-flush payload (the OTHER renderer already parses `hither +44, yon +48`
see [[dpl3-revive-integration]] memory) are reasonable, source-grounded
choices, not exact proof. Good enough to trust qualitatively; treat exact
near/far crossover distances as approximate until cross-checked against a
reference screenshot or the view-flush hither/yon.
### 6c. HUD — not rendering at all, cause NOT YET INVESTIGATED
No wire-level investigation done yet. BT411's `gauges-hud.md` /
`cockpit-view.md` should be checked first for the mechanism (a separate
draw pass? a different coordinate space/orthographic projection? a distinct
wire action type?) before searching the firmware blind.
### 6d. Weapon-fire rendering — not observed, cause NOT YET INVESTIGATED
Per BT411 `rendering.md` §"Weapon beams": beams are ordinary TUBE geometry
(`ermlaser.bgf`), muzzle-anchored every frame — i.e. on the wire this should
look like ordinary create/list_add/draw_scene traffic for a thin, elongated
object, NOT a special ray-trace primitive. Per `combat-damage.md`, the
original game's fire/target-lock mechanism is fully reconstructed and working
in BT411's port, so a live session showing no fire is more likely an input/
session-state gap (RIO fire button not reaching this DOSBox session) than a
missing render path — check that before assuming a rendering gap.
### 6e. Cockpit cage — intermittent, NOT YET CROSS-CHECKED
See `emulator/render-bridge/COCKPIT-CAGE-NOTES.md` (the OTHER renderer's
findings: PUNCH-texel windows, CAGE_TWIST_SIGN calibration, GLANCE_DCS
opt-in) and BT411's `cockpit-view.md` (the canopy stencil-cut kit, solved
there). Not yet cross-checked against what this firmware pipeline receives
on the wire for cockpit-frame geometry.