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>
15 KiB
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 publicdpl_*enumsC:\VWE\BT411\engine\MUNGA_L4\libDPL\dpl\dpl.h— thedpl_Set*APIC:\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
0x1atexmaps in thenetdeathcapture arrive as already-colourised RGB (HUD labels, emblems, panel/terrain art — 0–12% grayscale), i.e. the ramp is pre-baked on this path. Sorender_finalmust 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 defaultdpl_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 viaBTDPL.INI, per-pixel linear on W — NOT yet implemented in this renderer), weapon beams (ermlaser.bgfTUBE 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.Hdefinesdvpx_r24/g24/b24(addr 117/125/133, 8 bits each) as scan-conversion-time fields;EOF.C(lines ~1456, 1742) copies them straight intodvpx_eofr/g/b(184/192/200 — the final screen color) via a 24-bitIGC_CPYin 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.