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>
266 lines
15 KiB
Markdown
266 lines
15 KiB
Markdown
# 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 — 0–12% 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.
|