4-agent audit cross-checked every open/pending/stub/deferred claim in the context system against git + code. Fixed ~13 stale spots (currency only, no code change), each contradicted by its own file body + a landed commit: - 0xBD3 SubsystemMessageManager (task #7,afefaee): subsystems.md frontmatter+body line 57; decomp-reference.md 0xBD3 row + +0x434 row -- all still said "MISLABELS controlsMapper / WAVE 8 untangle" (done; mech.hpp names it messageManager). - Cylinder hit-location STEP 6 (d07ac7d): combat-damage.md frontmatter; decomp-reference.md "not reconstructed" line. - Per-mech turn-rate wiring (task #64b,a83995a): multiplayer.md + locomotion.md frontmatter (still "bring-up constant / not yet wired"). - Mech-level update records (task #1,c9f0c2a): open-questions.md "dead Wword branches mech.cpp:1511/1613" (those branches are gone; named fields now). - Collision damage + DeathShutdown (2026-07-08/12): combat-damage.md frontmatter. - Myomers un-stub (8b36440): decomp-reference.md "INERT un-stub" row. - Day/night BT_MATPRI (shipped, default-on): rendering.md frontmatter (LOD half kept). - Fire-trigger path (task #5,8ed6184): open-questions.md "still gBTWeaponTrigger bring-up" (globals retired/never-read). - HEAT entry FACTUAL fix: "heatLoad -> effectiveRange 0 = overheat cutout" was false peraab7a8a-- effectiveRange reads host-zone damage; the real cutout is the FailureHeat charge-hold. Frontmatter open_questions re-pointed at genuinely-open items. VERIFIED NOT stale (left as-is): multiplayer body-channel-projection notes (a real open [T3] residual per the coupled-motion section's ~2.9u leg-vs-body snap). Genuinely-open items (factory roster loops 2-4, subsystem-panel gate, missile cluster, console-death freeze, gyro tails, etc.) untouched. checkctx CLEAN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
214 lines
17 KiB
Markdown
214 lines
17 KiB
Markdown
---
|
||
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 authentic gray-metal
|
||
paint now. See [[asset-formats]]. [T2]
|
||
|
||
## 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 n−1000 (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 n−1000. **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]
|
||
|
||
## Key Relationships
|
||
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
|
||
- Renders on: [[pod-hardware]] (main 3D view).
|