# 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 = ` 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= `), 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 / board-side pick-range mechanism — PARTIALLY INVESTIGATED, PAUSED 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. **User's counter-point (2026-07-21), correcting an oversimplification above:** the renderer is NOT a purely passive tap in the strict protocol sense — there IS a real query/response shape in the wire protocol (VELOCIRENDER_PROTOCOL.md `vr_sect_pixel`(4)/`vr_sect_vector`(5): synchronous, blocking, the board runs a REAL Möller–Trumbore ray-triangle test over its own live scene and replies with an instance handle — "the exact range to the pixel in the center of the target and what that pixel is a part of"). Whether the SHIPPED BT4.10/MUNGA build actually uses this path (vs. computing it host-side, as BT411's own `HudSimulation`/pick-ray reconstruction does) was the open question. **Investigated, using this pipeline's unique capability (running the REAL firmware, not a reimplementation):** - `sect_pixel`(4)/`sect_vector`(5) never appear on this capture's wire at all (0 occurrences) — this build doesn't use the 1994-enum pick-query actions. - Action `0x23` (the notes' "fire/pick" candidate) DOES appear (23 times), and checked directly against the real firmware's OWN reply mechanism (`h_reply` hook, which fires on ANY firmware-issued reply regardless of action): NO reply follows `0x23` in this build either — consistent with the old fire-and-forget finding, now confirmed against the actual firmware code rather than just the software reimplementation. - BUT the `0x23` payload itself does NOT match the documented FLYK format (`[u:f][v:f][host ptr][-1]`, screen coords) — this build's payload is `[1][incrementing event ID | 0x30000000][0]`, no screen coordinates. Wire context around it (`0x23`×2 → `list_remove`×2 → `0x1b`×2 → a 264-byte `0x1d`, matching the documented PSFX/particle-burst branch ≥160B) reads like a WEAPON-DISCHARGE NOTIFICATION with visual consequences (a projectile resolving + effect spawn), NOT a board-side "what's under the crosshair" query. So `0x23` in BT4.10/MUNGA is very likely NOT the same mechanism the 1994-era FLYK notes describe under the same action number. **Net conclusion: open.** Either this build's real pick/range mechanism lives under a still-unidentified action, or BT4.10/MUNGA moved this computation entirely host-side (matching BT411's own architecture) and `0x23` (whatever it really is) is unrelated to targeting. Paused here per the user (2026-07-21) rather than open a broader search without a specific pointer — resume by identifying what wire traffic accompanies a KNOWN weapon fire in a fresh, targeted capture (fire once, deliberately, with minimal other action noise) rather than mining an existing capture for a guessed action number. ### 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.