Files
BT411/context/rendering.md
T
arcattackandClaude Fable 5 6f6a39b8c9 Issue #3 (b)+(c): weapon damage reaches cultural icons -> full trkdead explosion + authored burn fire
(b) The 'tiny explosion' on the ram kill is AUTHORED: crunch res 31 = 'stephit'
(one video object, effect 1008 = ddam5 damage smoke) -- a step/ram squash is
small by design.  The FULL explosion res 32 = 'trkdead' = psfx 15 dtrkboom
(fiery omni burst) + psfx 16 dtrkburn (the burning-wreck fire), reached by
WEAPON kills -- which the 1995 binary dispatches [T1]:
  - MechWeapon::SendDamageMessage @004b9728 (part_013.c:6765) gates only
    "target NOT derived from Mech@0x50bdb4 OR aimed zone set": a non-Mech
    boresight target takes the zone=-1 damage UNCONDITIONALLY;
  - Missile contact @004be078 dispatches at the struck solid's OWNER entity
    with no class test at all.
Port wiring: Mech::WorldStructurePick returns the struck solid's owning entity
(BoxedSolid::GetOwningSimulation; TERRAIN.cpp:107/246 build every static solid
with its Terrain/CulturalIcon/Door entity as owner); the mech4 pick block
designates IT instead of the gBTTerrainEntity sentinel (sentinel = fallback);
the projectile contact path grew the non-mech damage-zoned else-branch
(@004be078 mirror, direct Dispatch).  Plain terrain ignores the damage
(ENTITY.cpp:885 zone==-1 guard); an icon's handler maps -1 -> 0 and dies.
Truck armor is WeaponDamagePoints=1 (TRK.DMG): one laser = one dead truck.

(c) The burning fire is dtrkburn.pfx, authored INTO the death package (every
icon family carries 1016: trkdead/bigdead/meddead/msldead/twrdead).  There is
NO looping BurningState fire in the binary: the damage-zone effect watcher
ctor @0042a984 has exactly one call site (the Mech ctor, part_012.c:10405), so
icon ExplosionTables are inert, and CulturalIcon has no Performance.
Documented as authentic -- nothing invented.

Verified live (BT_SHOT pixel capture): 26 laser kills, [cult] TakeDamage
type=3 -> DYING res=32 -> DPLIndependantEffect 1015+1016 at the icon origin;
frames show the orange dtrkboom fireball, dtrkburn flames on the fresh wreck,
and the rubble aftermath; ram harness re-run still fires crunch res 31.
Diags: BT_FIRE_AT_ICON (designate nearest ahead icon; live-icon census in
CULTURAL.cpp), BT_FX_TEST="1015,1016".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:02:15 -05:00

321 lines
26 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 [T1, decomp]:** even if wired, the Searchlight never lights —
`ToggleLamp` (@004b860c) toggles `commandedOn` @0x1DC (part_013.c:6087) but `SearchlightSimulation`
(@004b841c) reads `requestedOn` @0x1E0 (part_013.c:5978), which the ctor zeroes and NOTHING ever writes;
no bridge 0x1DC→0x1E0 exists anywhere in the binary. So `lightState`@0x1D8 is perpetually 0. The
self-consistent sibling **ThermalSight** reads the SAME field it toggles (0x1DC) — the tell that
Searchlight's sim reads the wrong slot. The port reproduces this faithfully (searchlight.cpp:189 reads
requestedOn). **DECISION (2026-07-13): left as-is + documented** (faithful to the buggy original); the
port keeps the static lights-ON `fog=` values. To make the swap WORK is a DESIGNER-INTENT fix that
DEVIATES from the shipped binary — repair the sim to read `commandedOn` (mirror ThermalSight) + construct
`PullFogRenderable` at btl4vid.cpp `MakeMechRenderables` (the reticle-build/inside pass, == arcade
part_014.c:5173, ExecutionType Dynamic, bound to each Searchlight `lightState`) + a toggle input. 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.