Files
BT412/context/rendering.md
T
arcattackandClaude Opus 4.8 225fa5951b Render: fog searchlight-swap verified INERT + BT_FOG_LOG diag (task #63)
Verifying the searchlight/nosearchlightfog swap (user request): it does NOT work.
The arcade swaps active fog between fog= (headlight on) and nosearchlightfog=
(off, darker/tighter) via PullFogRenderable::Execute (edge-triggered on the mech's
light attrs -> SetFogStyle searchLightOn/Off).  But PullFogRenderable is NEVER
CONSTRUCTED in the port (grep: only the .h decl + .cpp def; no `new`), and
searchLightOn/Off is called from nowhere else.  Empirical confirmation via the new
BT_FOG_LOG hook: over a 22s run SetFogStyle fired only style 0, zero style 2/3.

So fog is static at the searchlight-ON (fog=) values -- normal lights-on play, so
core per-map/time/weather fog is unaffected -- but the nosearchlightfog variant is
dead.  Corrects my earlier "the swap is wired end-to-end" claim (code paths exist,
trigger never instantiated).  KB rendering.md updated; wiring it back is a scoped
reconstruction (bind the Searchlight subsys light state to a constructed
PullFogRenderable at viewpoint setup).  No behavior change this commit -- diag only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:36:48 -05:00

205 lines
16 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; day/night material path priority (BT_MATPRI)"
---
# 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 [T1, verified 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)`). In
the port **`PullFogRenderable` is NEVER CONSTRUCTED** (grep: only its .h decl + .cpp def; no `new`), and
`searchLightOn/Off` is called from nowhere else — so the swap never fires (BT_FOG_LOG: over 22s only
`SetFogStyle(0)`, zero style 2/3). Fog is STATIC at the searchlight-ON (`fog=`) values; the
`nosearchlightfog=` variant is dead. Reconstruction gap: wiring needs the mech's headlight/searchlight
state (Searchlight subsys 3032) bound to a `PullFogRenderable` constructed at viewpoint/mech setup.
Core per-map/time/weather fog is unaffected (uses the lights-ON set, the normal play state).
- **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 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]
## Key Relationships
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
- Renders on: [[pod-hardware]] (main 3D view).