Files
BT411/context/rendering.md
T
arcattackandClaude Opus 5 f3bdb3b85a Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"
Both classes' Receiver::MessageHandlerSet were default-constructed blackholes
(input-path audit systemic cause #1): entryCount 0, no parent chain, Find()
returns NullHandler for every id, Receive drops silently.  The new unhandled-
message trace printed it on the first press:

    [btntest] PRESS 0x14 at poll 400
    [msg] UNHANDLED: Searchlight has no handler for message id 3

Each class now has a GetMessageHandlers() function-local static chained to
PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to
Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's
ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is
void(const Message*) while the decomp shape is Logical(Message&), so a cast
would be UB that merely happens to work on x86 __thiscall.  DefaultData
re-pointed off the dead empty sets.  MessageArg now reads the NAMED
ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the
binary's message+0xC (was a raw offset read -- databinding rule).

ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's**
ToggleLamp (table @0x51120C), NOT Searchlight's.  The two TUs emit parallel
shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84
Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT
pooled across them -- two copies exist, each emitted immediately before its own
class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475).
[T1: reference/decomp/section_dump.txt:69661-69711]

Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60).
RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the
EjectAmmo technique) -- and it corrected two things I had inferred wrong:
  * NO ControlsAllowLights/+0x25C novice gate.  Searchlight tests the press
    alone; that lock is ThermalSight-only.  A NOVICE pilot CAN work the lamp.
  * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit
    (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY.

Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light"
claim is RETRACTED.  It compared Searchlight's Performance (@004b841c, reads
requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different
classes -- and so invented a missing 0x1DC->0x1E0 bridge.  Every sibling toggles
the field its own Performance reads.  The searchlight DID light in the arcade,
the searchlight->fog swap was NOT inert there, and building PullFogRenderable is
FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is"
decision is void; the sim needs no repair).

Verified live, real click seam (BT_BTNTEST):
  0x14 -> [light] requested ON -> reported lightState 0 -> 1     (lamp lights)
  0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1
UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing
CreateStreamedSubsystem + Entity__SharedData::DefaultData families only).

Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp,
hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C
consumer list, context/{decomp-reference,subsystems,rendering,open-questions,
pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md.
checkctx.py CLEAN.

Still open (documented, not fixed): ThermalSight has no visible IR effect
(ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed
returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has
"LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no
identified role and measured 21 live, hinting our SubsystemResource +0x28 differs
from the binary's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:46:59 -05:00

326 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: rendering
title: "Rendering — the D3D9 pipeline, materials, sky, beams, performance"
status: established
source_sections: "PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT.md"
related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware, cockpit-view]
key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam]
open_questions:
- "Replacement-LOD for NON-additive multi-LOD models"
---
# Rendering
The port renders via the WinTesla `L4D3D`/`L4VIDEO` Direct3D9 path ([[wintesla-port]]), carrying our
BT loader (`bgfload.cpp`) + fidelity work. The goal: match the authentic pod footage. Geometry/LOD
mechanics are in [[bgf-format]]; this topic covers the D3D9 draw path + the fidelity fixes. Full
detail: `docs/PROGRESS_LOG.md §10`.
## The IG-board shading model (from libDPL DPLTYPES.H)
Shading is selected PER-GEOMETRY by vertex type: **no-normal geometry** (terrain/mesas/sky/buildings/
mech — WHITE baked verts) is UNLIT, colorized by the material's 2-endpoint **[[material-ramp]]**
(`dpl_SetMaterialRamp` — texture luminance indexes a low→high gradient); **normal-bearing geometry**
(~150 vehicle/missile files) is LIT by the map light. Applying the ramp to lit geometry = the "dusty
white blobs" bug (gate on `hasNormals`). [T2]
Exception: cockpit-frame (`*_cop`) batches are pure-emissive constant colour (0.13,0.12,0.15),
`BT_COP_FRAME` overrides — see [[cockpit-view]] (task #55; the old `BT_COP_RAMP_L` ramp-index
approach is retired). [T2]
**VERTEX-ALPHA EFFECT CARDS (2026-07-12) [T1 corpus-swept]:** exactly 14 shipped BGFs carry
per-vertex float RGBA with alpha < 1 (tags 0x0082/0x008A, RGBA at +12, values OUTSIDE [0,1] —
clamp): FLAMEBIG/FIRE5 (flames), MUZFLASH, EXDISK_A/B/C, TMST_A/B/C, BLUEBEAM/GREEBEAM/RED_BEAM/
PPC, DECLOUDS — the EFFECT-CARD family. Their authored gradient (white-hot base → dark tip fading
to alpha 0.2) IS the shading: drawn opaque with a flat batch colour they render as solid shapes
(the wreck flame read as a "twisted drill bit"). The loader now keeps the authored per-vertex
RGBA for any batch with vertex alpha < 1 (`BgfDrawBatch.vertexAlpha`) and L4D3D routes it to the
ALPHA-BLEND pass, unlit, colour = texture × gradient, alpha = vertex fade (SRCALPHA/INVSRCALPHA).
SKY objects are excluded from the routing (drawAsSky + alphaTest passes NEITHER pass filter —
DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE tag 0x2037
`" SCROLL u0 v0 du dv"`, e.g. firesmoke1 = 0.05,0.331 uv/s) is now parsed and propagated to
`L4TEXOP::doScroll` — scrolling materials (firesmoke flame cards) animate in the model path. [T2]
## The big render-fidelity fixes (dbase/desert matched to pod footage)
- **Geometry:** CONN is a flat trilist (not fan); CONN+PCONN coexist — see [[bgf-format]]. Malformed
fan-garbage was the "faceted mountains" bug. [T2]
- **RAMP colorize** (default-on `BT_RAMP`) + **day/night material priority** (default-on `BT_MATPRI` for
the ramps): the `mat\day` libs carry the warm ramps; the generic GEO libs are gray. Terrain/mesas
render warm tan; the sky's `dsky_mtl` ramp (0→0.99) gives bright clouds. `hasNormals` gate keeps
vehicles LIT (colored trucks). Ramp tint rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp
⇒ white. [T2]
- **Sky:** a `*sky.bgf` dome/plane drawn in a dedicated fullbright **PASS_SKY** (LIGHTING off, extended
far=9000 + world fog); tagged `drawAsSky` at load (the `.x` sidecar path doesn't exist). [T2]
- **The env pipeline** (`DPLReadEnvironment``DPLReadINIPage`, reads **`content/BTDPL.INI`** via
`L4DPLCFG`): resolves map+time → clip/fog/ambient + a directional sun. Bring-up stubs
(EnsureValidProjection / per-frame ambient / uninit cloud colours) had clobbered it — now they
RE-ASSERT the env values. Debug: copy BTDPL.INI → *DBG.INI with `debug=True`. [T2]
- **Fog — per map/time/WEATHER (task #63).** `BTDPL.INI` authors `fog=<near> <far> <r> <g> <b>` (+ often
`nosearchlightfog=`) on EVERY map/time/weather leaf. WEATHER is a fixed authored egg field
(`clear`/`fog`/`soup`, `mission->GetMissionWeather()`; missing = fatal in the arcade); the env pipeline
branches location→time→weather to the leaf. Shipped eggs pin `cavern/night/clear` → page `dsnitclear`
(near 90, far 1100, dark blue 0.1,0.1,0.12); `fog``dsnitfog` (500), `soup``dsnitsoup` (300). The
arcade fog model is per-PIXEL linear (`dpl_fog_type_pixel_lin`, dpl.h:265). **It rendered INVISIBLE in
the port:** `D3DRS_FOGTABLEMODE=D3DFOG_LINEAR` (table fog) reads the perspective-NONLINEAR z-buffer, so
without WFOG the authored 90..1100 range collapsed to fog-factor≈1 (no fog). FIX (L4VIDEO.cpp world
pass ~8194): auto-detect `D3DPRASTERCAPS_WFOG` → per-pixel W-fog (smooth, == the arcade); else VERTEX
fog (eye-space but per-vertex → splotchy on coarse terrain tris); FOGENABLE + mode re-asserted each
world-pass frame (the old one-time set got clobbered per-pass). **Task #20's "shipped maps define no
fog" was WRONG** — it checked `.MAP`/`.RES`, but fog lives in BTDPL.INI (the DPL env INI), as in the
arcade. Night fog is dark BY DESIGN (distance→murk, not light haze); day pages author light haze. Env:
`BT_FOGMODE=table|vertex|off`, `BT_WEATHER=clear|fog|soup`, `BT_FOG="near far r g b"` (all branches);
`[fog] resolved`/`[fog] model`/`[fogstyle]` (BT_FOG_LOG) log lines. [T1 pipeline / T0 arcade model / T2 fix]
**⚠ SEARCHLIGHT SWAP IS INERT — and it was inert in the ARCADE too (task #63 workflow, 2026-07-13).**
The arcade swaps the active fog between `fog=` (headlight ON) and `nosearchlightfog=` (OFF,
darker/tighter) via `PullFogRenderable::Execute` (L4VIDRND.cpp:4601, edge-triggered on two light attrs →
`SetFogStyle(searchLightOn/OffFogStyle)`). TWO independent reasons it never fires:
(1) **Port:** `PullFogRenderable` is NEVER CONSTRUCTED (grep: only its .h decl + .cpp def; no `new`);
`searchLightOn/Off` is called from nowhere else (BT_FOG_LOG: over 22s only `SetFogStyle(0)`, zero 2/3).
(2) ~~**Original 1995 LATENT BUG**~~ — ❌ **RETRACTED 2026-07-25 (#61). There is no 1995 bug.** The
claim rested on a **swapped table attribution**: @004b860c is **ThermalSight's** `ToggleLamp`
(table @0x51120C), not Searchlight's, so pitting its `0x1DC` write against Searchlight's
`SearchlightSimulation` (@004b841c, reads `requestedOn`@0x1E0) compared two different classes and
manufactured a missing "0x1DC→0x1E0 bridge". Searchlight's own handler is **@004b838c** (table
@0x51117C) and toggles the field its own Performance reads, `0x1E0` — exactly as self-consistently as
ThermalSight does with `0x1DC`. [T1 attribution: un-pooled `"ToggleLamp"` strings sit adjacent to
their own class names, `section_dump.txt:69661-69711`. Body: @004b838c sits in a Ghidra export gap
(#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0. All T1.]
**The searchlight DID light in the arcade, and the fog swap was NOT inert there.** Searchlight's
handler is now wired (#61) and **verified live**: pad `0x14``requestedOn 0→1``lightState 0→1`.
So the ONLY remaining blocker is (1), the un-constructed `PullFogRenderable` — and building it is
**FAITHFUL, not a designer-intent deviation** (the old "DECISION 2026-07-13: left as-is" is void;
the sim needs NO repair). Remaining work: construct `PullFogRenderable` at btl4vid.cpp
`MakeMechRenderables` (the reticle-build/inside pass, == arcade part_014.c:5173, ExecutionType
Dynamic, bound to each Searchlight `lightState`). See [[open-questions]]. Core per-map/time/weather
fog is unaffected.
- **Projection / stencil (task #55):** the BTDPL.INI `viewangle=60` is the **HORIZONTAL** FOV —
authentic 60×47 frustum at 4:3; fovY derived via `BTFovYFromHorizontal` (L4VIDEO.cpp), applied at
all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now
**`D3DFMT_D24S8`** for the cockpit punch stencil-cut (L4VIDEO.cpp:2799; sequence
L4D3D.cpp:1199-1248) — see [[cockpit-view]]. [T2]
## Backface culling (default CW, `BT_CULL`)
The bring-up `D3DCULL_NONE` drew interior/back faces (the "dark wedge" shapes = mesh insides). CW is
correct for the BGF quad triangulation `[a b c][a c d]`. [T2] Exception: cockpit `*_cop` batches
default DOUBLE-SIDED (`BT_COP_SINGLE=1` restores single-siding, diag) — see [[cockpit-view]]. [T2]
## LODs (default: shipping-engine first-LOD + ADDITIVE_LODS; `BT_ADDLOD`/`BT_LOD_SCALE`/`BT_PUNCH`)
See [[bgf-format]] for the √3 range decode, the ADDITIVE_LODS cumulative rule, PUNCH cutouts, and the
coplanar-shell submission-order depth bias. Replacement LOD selection for NON-additive multi-LOD
models is deferred (needs the board's hot-spot semantics). [T2]
## Mech skins (BSL bit-slice — the rainbow-mech fix)
`.BSL` is a bit-sliced container (6 grayscale sub-images per file); the BMF `0x18 BITSLICE` tag picks
the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" bug. Fixed:
`image.cpp decodeBSL` slice-decodes; `bgfload` threads the 0x18 channel; a cross-library RAMP registry
resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders the authentic BASE
gray-metal skin now. See [[asset-formats]]. [T2]
## Per-pilot mech PAINT (color/badge/patch substitution) — NOT WIRED (2026-07 audit)
The "missing warning stripes / coloration" report is the per-pilot paint layer, fully authored but
never engaged by the port. Mechanics (all [T1] from the BMF/BGF/vehicletable content + FUN_004d0cc0):
- Mech BGFs reference the BASE lib only (`blhskin:gen2_dz_rfoot_mtl` …). The binary rewrites lib
prefixes at load via the `vehicletable` resource `[substitute]` templates: `skin:``_%color%%serno%:`
(`blhskin:``blh_wht0:`) and `skin:lgo``<<<emblem%serno%:%badge%_%patch%>` (emblem pages →
`EMBLEM0.BMF` `lgoN_<patch>_dz_*` materials). Pilot egg values (color/badge/patch) map through the
`[color]/[badge]/[patch]` sections to the codes.
- The COLOR libs (e.g. `BLH_WHT0.BMF`) are byte-identical to the base lib EXCEPT they add tags
`0x23`(diffuse RGB) + `0x24`(ambient RGB) = the pilot color to the ~35 paintable `gen*/mech*_dz_*`
materials (base leaves them untinted). 7 colors: wht(.99,.99,.99) red(.90,.11,.10) grn(.63,.83,.52)
gry(.65,.60,.65) brn(.60,.44,.28) tan(.65,.55,.43) blk(0,0,0). All 8 sernos are byte-identical
(verified: 0 of 193 groups differ) — `%serno%` was future variety, never used. `<mech>SKIN.BMF`
(bare) == `<mech>SKIN0.BMF`.
- WARNING STRIPES are grayscale hazard-stripe art in the shared `GEN.BSL` pages (slice 1 = `gen2_tex`,
top-left), shown through the paintable-zone materials and tinted by the pilot color. `EMBLEM0.BMF`
carries `lgoN_<patchcolor>_dz_ld/rdleg` materials (8 patch tints incl. yellow .89,.73,0) over
`LGO.BSL` slices 0-5 (slice 5 = VGL winged logo).
- **WIRED 2026-07-17 (same-day fix), verified live**: MP MadCat renders crimson with hip hazard
stripes + yellow VGL leg emblems; Blackhawk white + emblems; replicants painted on both nodes.
The reconstruction: (a) `Mech::resourceNameA/B/C` are real `CString` members (the binary's exact
16-byte `CStringRepresentation` {size,len,text@+8,refs@+0xc}; ctor = part_012.c:9841-71, deep-copy
bind from the MakeMessage = `CString::operator=(const char*)` == FUN_00402a98 @10427-41, implicit
member dtors = 11388-403) with paint-name accessors; (b) `SetupMaterialSubstitutionList` reads them
(real egg names; `[paint]` log line); (c) `dpl_SetMaterialNameCallback` is REAL now
(L4VIDEO.cpp registry, cleared first in TearDown as FUN_004d11e8 does) and
`bgfload MaterialResolver::resolve()` runs every name through `dpl_ApplyMaterialNameCallback`;
no mesh-cache issue (LoadObject/LoadBgfFile don't cache — every load resolves fresh). [T2]
- DEVIATION (documented, btl4vid.cpp): on a vehicletable miss the binary Fail()ed ("Exiting"
@BTL4VID.CPP:0xbeb); the port warns + drops that placeholder (RP analogue). Real case: dev eggs
ship `color=Red` but the `[color]` table only has **Crimson** — MP_BHMC.EGG fixed to Crimson;
LAST.EGG pilot-2 still says Red (unused slot). The 7 valid egg colors:
Black/Brown/Crimson/Green/Grey/Tan/White; badges Davion/Kurita/Liao/Marik/None/Steiner/VGL;
patches Black/Blue/Green/Grey/Red/Violet/White/Yellow. [T1]
## The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded)
The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effects_to_load]`:
`specialfx3..15` = Chunks1/Chunks2/Sparks/Explosion3/MachineGun/PPCHit/LaserHit/MissileHit/
CannonHit/GroundHit/MinorHit/MajordHit/Fireball — "old-style" (version-1) descriptor sections
(`texture/type/size/velocity/v_bias/y_off/cook(3f)/varience/gravity/cool(2f)/count/repeats`),
most on `btfx:firesmoke1_scr_tex` (MissileHit size 12 ×7; Fireball size 14, gravity 1.5 rises;
Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the 1995 binary parses
them — part_008.c:1546-1590, field strings @004f25e4-004f2663 → installer FUN_00490210 packs a
0x44-byte board command via FUN_0048e2c0.] **Simulation semantics (type/cook/cool) live in the
i860 firmware** `content/VREND.MNG` (same decode workflow as the task-#55 'damageize' handler).
- **EFFECT-NUMBER ROUTING (corrects the old claim):** `effect_number < 100` = the dpl board
EFFECT TABLE = this specialfx list (1996 case-4 comments match: 3=Chunks, 12=GroundHit,
15=Fireball), **NOT** a psfx slot; `>= 1000` = psfx slot n1000 (idam/ddam PFX); 100-999 =
ExplosionScripts. The port's `<100 → psfx slot` mapping renders PPCHit(8) as ddam5 smoke and
Chunks(3)/Sparks(5) as ddamN smoke — the "hits are just a little smoke" report. The 2007
WinTesla loader SKIPS all version-1 descriptors (`if (version < 2) continue;` — it only reads
the 1998 INDIE format), which is why the layer was never seen. [T1]
- **Live hit census (mp logs):** weapon hits dispatch idam bands 1009-1013 (30×1009) + raw
8/9 (PPCHit/LaserHit, mis-rendered); **MissileHit(10)/CannonHit(11) never appeared** — the
projectile/salvo damage path likely doesn't thread the firing launcher's subsystemID into the
damage records, so messmgr never queues its explosion. [T2 census; T4 cause]
- **RECONSTRUCTION (2026-07-12, firmware-first):** the i860 sim decoded from VREND.MNG
(dispatch table at data+0xdd0c, VA base: code 0xf0400000, data 0x1000; handlers: sfx trigger
0xf040cda0→ca48, install 0xf040cdc0→cba0, step near 0xf0413698, spawn near 0xf0414afc —
string-anchored via "attempt to step a NULL sfx object"; disassembler REBUILT at
`scratchpad/i860dis.py` from binutils i860 opcodes, `scratchpad/i860_opcodes.h`).
**The heat model [T1]:** per 30Hz board frame `h *= cool_a`; `RGB_ch = h_old*(h_new*cook_ch
0.25) + 0.25` (K=0.25 @VA 0xF080 = the ember floor); kill at h ≤ 1e-4 (@0xF0A0); alpha =
cool_b per frame; `pos += vel`, gravity as accel. y_off = the particle KILL PLANE
(Chunks 1 = die at the ground) [T3 data-indicated]. Burst frame randomly spun (quadratic
small-angle rot, θ from the host-fed random table cmd 0x21). Implemented in L4VIDEO.cpp:
`BTInstallSfx`/`BTStartSfx` + sfxMode particles in the BTPfx layer; parser beside the psfx
page walk (`[sfx] page 'effects_to_load': 13 old-style specialfx installed`). Effect-number
routing fixed in BOTH consumers (ExplosionClassID + DPLIndependantEffect): <100 → specialfx,
≥1000 → psfx slot n1000. **Missile/cannon hits now bundle** [T2]: BTProjectile carries
shooter + weaponSubsys; contact routes through the SHOOTER's SubsystemMessageManager
(AddDamageMessage w/ subsystemID → mslhit/acanhit explosion at the impact), mirroring the
laser path's SendDamageMessage. Cone shape (v_bias as up-bias) + per-type draw weights are
[T3]; the full type-0/1/2 draw-geometry decode in the firmware draw fn (0xf0416160) remains
open.
## Weapon-hit forensics closeout (2026-07-12, workflow-verified) [T1]
- **The HIT PACKAGES (BTL4.RES model lists, byte-verified):** ppchit=[8], lzrhit=[9],
mghit=[7,11], canhit=[11] (no "acanhit"), mslhit=**[10, 12, 1023]** (MissileHit + GroundHit +
psfx23 SHKWAVE), gndhit=[12], minhit=[1002], majhit=[1003], explode=[6]; dam1-4hit=[1002-1005],
desthit/stephit=[1008]; cockpit "i" variants (lzrhiti etc.) = same sfx + idam 1009-1013; ALL
eight mech death lists = ["104","3","4","5","1007","15","1001"].
- **"Silver mist exactly 5×, perfectly timed" ROOT CAUSE:** SHKWAVE.PFX authors maxIssue=5,
releasePeriod=0.2, **rate=1** — the ONE shipped .PFX where rate ≠ maxIssue/releasePeriod; the
emitter trusted 'rate' → 5 huge white shells (exp 350/s) at exactly 1Hz. **Emission-window
rule hardened:** effective rate = maxIssue/releasePeriod always (BTDrawPfx emitter).
- **SpecialFX palette [T1 firmware]:** instance init @0xf04128d8: inst[0x19c]=cool_a (per
board-frame heat decay), inst[0x1a4]=cool_b (× raw dt in the step = PER-SECOND fade),
inst[0x188]=7.0s cap, heat=1.0. The heat channels are BRIGHTNESS over an inherently
FIRE-coloured texture (firesmoke through its material ramp): sfx batch draws the FIERY-ramped
sheet, psfx keeps GRAYSCALE (authored colours decide — DNBOOM orange, DDAM gray). Drawing sfx
grayscale = silver mist; scaling fade ×30 = 0.1s pops ("nothing happens on laser hits").
- **Damage-band .PFX shapes are authored GENTLE (puffy clouds):** bands 1-4 of ndam/idam/ddam
rise at 1-4 u/s with buoyant accel; ONLY band 5 + the BOOM/SHKWAVE files are explosive-omni.
idam = scaled-down WORLD venting smoke (half radius/counts of ddam), not cockpit-only.
N-set = night = same physics as D-set with fewer particles. NSRM/DSRM trails = 0.1s overbright
ORANGE directional puffs (never silver).
- **messmgr explosion DEDUP CONFIRMED [T1, decomp-read @0049b784]:** `Find` (weaponExplosions
vtable slot 0xC) gates `AddValue` (slot 8) — ONE explosion per unique weapon resource per
frame, exactly as reconstructed. (A workflow agent claimed "one per record, staggered
0.1s" — REFUTED; the stagger applies to the deduped queue.) Missile multiplicity comes from
the ORDNANCE side instead: Missile::MoveAndCollide @004bef78 detonates per round — ported as
BTSpawnRoundDetonation (mech4) + rack-tube launch spread [T3]. Alpha-strike drama = distinct
per-weapon-TYPE packages (lzrhit+ppchit) + the victim's band fire, not explosion count.
- Remaining [T3]: sfx cone shape (v_bias as up-bias), size ×0.5 + 1.25m textured floor +
hot-phase occlusion 0.45..0.9, burst scatter ×0.2; trail
density is frame-rate-dependent (2/frame @60fps = 2× pod density); .PFX 'repeats' unconsumed
(all shipped files author 1); replicant visual salvo chases the stale fire-time aim point
(no re-lead without a target handle).
## Weapon beams (the unported dpl_* layer)
The `dpl_*` beam renderable was never ported → beams are drawn by `BTPushBeam`/`BTDrawBeams`
(L4VIDEO). The real look is the `ermlaser.bgf` TUBE (native Z 0..2000, UVs tiled) with the scrolling
`bexp` grit texture (from the `.VMF` `SCROLL`). ER lasers red, PPC blue, standard yellow. The beam
origin re-anchors to the live muzzle every frame (can't run out from under your own beam). See
[[combat-damage]] for fire-rate/muzzle. [T2]
## Translocation warp (the respawn vortex) — see [[translocation-warp]]
The death/respawn "blue whirlwind" is `tsphere.bgf` (a 12-facet bicone) spun on-axis with the `bintA`
cloud through a wide lavender ramp, drawn as SKY. Full geometry/material/lifecycle/env in
[[translocation-warp]]. ⚠ **Rendering-wide fix that came out of it:** `L4D3D::SetTextureScrolling`
computed its texture-matrix offset as `scrollDelta × absolute_time`, which grew unbounded and collapsed
UV float precision → grainy/spoked scrolled textures the longer a session ran. Wrapped with `fmodf(…,
1.0f)`. This also cleans up the scrolling `bexp` beam grit and any other `SCROLL` material. [T2]
(Bug class: [[reconstruction-gotchas]] §13.)
## Performance — the 10fps→60fps fix (~50-80× render speedup)
The 10fps baseline had THREE stacked causes, all fixed (engine tree): (1) **`D3DCREATE_SOFTWARE_
VERTEXPROCESSING`** — every vertex transformed+lit on the CPU → HARDWARE first; (2) D3DX `DrawSubset`
attribute-table scan (our double-sided tris broke OptimizeInplace) → explicit `SetAttributeTable` +
direct `DrawIndexedPrimitive`; (3) no frustum culling → bounding-sphere culling per frame. ⚠ LESSON:
the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDebInfo identical) —
split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept:
`[spike]/[loadobj]/[rstat]`. [T2]
## Cultural-icon (map prop) DESTRUCTION — revived 2026-07-19 (Gitea issue #3) [T2]
Trucks/props/buildings are **CulturalIcon/Landmark** entities (`engine/MUNGA/cultural.h`, map-
streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, `ExplosionModelFile`,
`removeOnDeath`). The SIM was always complete [T0]: ram/weapon damage → `CulturalIcon::
TakeDamageMessageHandler` → zone `damageLevel ≥ 1`**Explosion entity** (Crunch variant for
collision) + `WaitingToBurn` (+`timeDelay`) → `BurningState` + `UpdateCollisionVolumes` (boxes
DELETED on `removeOnDeath`, else sunk by `destroyedYTranslation`). The mech-side crunch dispatch
(`mech4.cpp ProcessCollision``BTDispatchCollisionDamage`, ×0.001 ram economy) reaches icons
(≈19 walking-speed bumps kill the spawn-yard truck). What was BROKEN (the "no state change"
report): the 2007 port fully stubbed **`StateInstanceSwitchRenderable`** ("STUBBED: DPL RB
1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now
controls the draw component via the **`SetDrawObj` in-place drawable swap** (the mech
RemakeEntity mechanism); `[video] Object` hides at BurningState, `Rubble` shows; watcher =
`StateIndicator::AddVideoWatcher` (SetState DOES fire video watchers). ⚠ REQUIRED gotcha:
cultural objects must stay **OUT of static-mesh consolidation** (`RecurseStaticObject` — same
exclusion class as banded LODs/shadows), else the merged mesh draws the intact model forever
regardless of DrawObj. **RUBBLE PLACEMENT FIX (2026-07-19, pixel-verified via BT_SHOT):** the
rubble originally never appeared because additional cultural objects were built as
DPLStaticChildRenderable/DCSInstanceRenderable at TOP level — those read the bare matrix stack
(identity), so the rubble drew at the WORLD ORIGIN. Every cultural video object is now a
`RootRenderable` (places from `myEntity->localToWorld`). Screenshot timeline confirms: intact
MECHMOVR truck → ram death → MECHMD wreck debris scattered at the truck's spot.
**(a) SCORCH QUAD FIXED (2026-07-19, pixel-verified, awaiting human confirm):** the scorch base
(MECHMD patch 0, 4-vtx no-normal quad, `basev:bvx9_mtl` → bexp9_tex = BEXP.BSL **RGBA4444
slice 8**, ramp `cdusty`) drew as a hard dark SQUARE for TWO stacked reasons: (1) the blanket
"truecolor slice (channel ≥ 6) never ramps" gate blocked its cdusty ramp — but only 4 shipped
textures use truecolor slices, and bexp9/bdet9 are GRAYSCALE in RGB (100% / 98.8% r==g==b
texels; only bdam8 is truly coloured) — `bgfload rampableSlice()` now probes the decoded slice
(a truecolor slice ramps iff effectively gray, ≥95%; bdam8's colour stays protected); (2) the
RGBA4444 **authored alpha** (a binary 0/240 splat CUTOUT mask, 78.5% transparent) was never
alpha-tested — new `BgfDrawBatch.texAlpha` (channel ≥ 8) routes through the PUNCH alpha-test
draw states WITHOUT the black-texel keying (which would hole the near-black charred centre).
Side benefit: tree9 (GRASS tree/leaf cards) + bdet9 (trans-rail lattice) get their authored
cutouts too. Post-death shot: irregular char splat, dark brown (71,44,34) lifting to
near-terrain tan (115,100,91 vs terrain 131,119,108), no rectangle; pre-death frames
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2]
**(b)+(c) CLOSED (2026-07-19, pixel-verified, awaiting human confirm).** The icon death
EXPLOSION packages are authored per KILL TYPE [T1, BTL4.RES byte-dump]: the CRUNCH
res 31 = `stephit` (ONE video object, effect **1008** = ddam5 damage smoke) — the ram/step
squash is SMALL **BY DESIGN**; the full res 32 = `trkdead` = **1015 dtrkboom.pfx** (fiery
omni burst: vel 150, rad 3.5 exp 10, orange 2.0/0.7/0.2→1.0/0.3/0, ~1.5s) + **1016
dtrkburn.pfx** (the BURNING-WRECK FIRE: 30 rising fire-card particles over a 5s emission,
4±1s each ≈ a ~9s fire at the wreck). The whole icon family shares the pattern:
bigdead=[1017 dbigboom,1016], meddead=[1018,1016], msldead/twrdead=[1017,1016],
smldead=[1020,1022,1021]. **Item (c) verdict: the burning fire is the dtrkburn ONE-SHOT
authored INTO the death package** — there is NO looping BurningState fire: the
damage-zone effect watcher ctor @0042a984 has exactly ONE call site binary-wide (the
Mech ctor, part_012.c:10405), so icon ExplosionTables are inert, and CulturalIcon has no
Performance (AlwaysExecute is replication flush). Documented as authentic. **Item (b):
the res-32 path is reached by WEAPON kills, which the binary DOES dispatch [T1]:**
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765) gates only "target NOT
derived from Mech@0x50bdb4 OR aimed zone set" — a non-Mech target under the boresight is
damaged UNCONDITIONALLY (zone 1; the icon handler maps 1→0) — and the Missile contact
branch @004be078 dispatches a zone=1 TakeDamageMessage at the struck solid's OWNER with
no class test at all. The port never delivered this because the structure pick designated
the `gBTTerrainEntity` SENTINEL; now `Mech::WorldStructurePick` returns the struck
solid's owning entity (`BoxedSolid::GetOwningSimulation` — every static solid is built
with its Terrain/CulturalIcon/Door entity as owner, TERRAIN.cpp:107/246) and mech4.cpp
designates IT (mech.cpp `WorldStructurePick`, mech4.cpp pick block + projectile contact
else-branch). Truck armor is `WeaponDamagePoints=1` (TRK.DMG) — one laser kills it.
Verified live: 26 laser kills `[cult] TakeDamage type=3``DYING … res=32`
`DPLIndependantEffect 1015+1016` at the icon origin → BT_SHOT frames show the orange
dtrkboom fireball, dtrkburn flames on the fresh wreck, and the rubble aftermath; ram
harness re-run still fires res 31 (crunch unregressed). All 24 psfx slots (incl. 15/16)
load from the BTDPL.INI pages. Diag: `BT_FIRE_AT_ICON` (designate nearest ahead icon),
`BT_FX_TEST="1015,1016"`. LESSON: `[cultvis]` log lines verify
the STATE MACHINE, not pixels — only a screen check (BT_SHOT / human) verifies rendering.
Diags: `BT_CULT_LOG` (census/damage/death + `[cultvis]` + `[cultobj]` per-object file/type/op
flags). MP: replicants transition via `ReadUpdateRecord` → same SetState watcher path (untested
cross-pod).
## Key Relationships
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
- Renders on: [[pod-hardware]] (main 3D view).
- Prop destruction feeds: [[combat-damage]] (crunch dispatch) · uses the psfx layer.