Files
TeslaRel410/emulator/firmware-decomp/IG-SHADING-MODEL.md
T
CydandClaude Opus 4.8 bf234842cb Commit the texel-format fix + shared dpl_sampler module (was uncommitted)
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>
2026-07-20 10:07:16 -05:00

7.3 KiB
Raw Blame History

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.