These changes were made and verified earlier this session (the all-cyan bug
fix is already recorded as SOLVED in project memory) but never got committed --
render_final.py has referenced dpl_sampler as a module in its docstring since
ac4142e7, yet dpl_sampler.py itself was never added, leaving the repo unable
to run the M4b/M4c/M4d renderer scripts that all import it.
texstore.py: fix the texel word format. SVT texels are [pad,B,G,R] (ground
truth: dpl3-revive patha/vrboard.py do_texels + stage_assets.py); reading RGB
as bytes (0,1,2)=(pad,B,G) zeroed red, producing the all-cyan render. Correct
read is bytes (3,2,1).
dpl_sampler.py: the authentic IG-board texture-value model (wrap repeat/clamp,
near-black CUT keying) factored into its own tested module, distilled from the
shipped libDPL headers -- see IG-SHADING-MODEL.md (also added, referenced by
render_final.py's docstring but likewise never committed).
igc_exec.py: OP48/OP49 candidate opcodes (pixel global-X/Y seed into mem) added
to the CPU golden-model executor alongside the existing SCMEMA seed handling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
7.3 KiB
Markdown
140 lines
7.3 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.
|