Eight byte-identical field stacks from night 6, all one player, all in an
Owens: ParticleEngine::Destroy +0x11, access=0 target=0x0, from the plain
per-frame render path. Nothing in the stack touches weapons or the Owens.
Conn Man's Surface Pro 9 (Iris Xe, 128 MB shared) is simply the only GPU in
the fleet that ever actually LOSES the D3D9 device -- his two-trigger
missile+laser bursts are what provoke the timeout, not what crashes.
What crashed is our device-loss handling, which was wrong three ways at once,
in two inline copies (the scene Present and the wait-screen Present):
1. On D3DERR_DEVICELOST it called Reset() IMMEDIATELY. Reset on a
still-lost device ALWAYS fails, and V() only logs. There was no
TestCooperativeLevel gate at all.
2. It then ran ParticleEngine::Initialize against the lost device. The
creates fail there and NULL their out-params -- proven, not assumed:
the bench repro faults at target=0x0, not at a dangling address.
3. The next lost frame called ParticleEngine::Destroy again, which
Release()d those NULLs blind. Read of vtable at 0x0. Dead.
So: lost frame 1 tears down and leaves NULLs, lost frame 2 crashes. Two
frames, every time, deterministic -- which is exactly why all 8 field stacks
are byte-identical.
Reproduced before fixing. BT_DEVICELOST_TEST=<frame>,crashrepro runs the
field sequence on the bench; on the unfixed build it died at Destroy +0x11,
access=0 target=0x0, and symbolized to the same four frames as the field
logs. Same shape, same offsets-modulo-hook. That run also proved the
out-param-nulling assumption the whole diagnosis rested on.
The fix -- one shared DPLRenderer::BTResetLostDevice() replacing both inline
copies:
- Destroy() is idempotent and null-safe, and nulls after release.
- Reset() is gated on TestCooperativeLevel() != D3DERR_DEVICELOST; while
the driver still says lost, skip the frame and retry.
- The Reset HRESULT is checked; on failure, log and retry next frame
instead of driving on.
- On success, re-create via the new CreateDeviceObjects(), NOT
Initialize(): Initialize memsets the installed-effects table, so every
reset that DID succeed silently killed all particle effects for the rest
of the mission. The quieter sibling bug, fixed by the same split.
- Initialize checks its HRESULTs and defends MAXPARTICLES<=0; the draw
paths guard the NULL buffer, and ExecuteParticles keeps draining
particles while the engine is dormant so they cannot pile up.
Verified: the crashrepro shape now logs SURVIVED and play continues; three
forced full loss/reset cycles each log "[render] device reset OK"; a plain
run is assert-free.
Found while verifying, worth its own line: VIDEO\particles.png has NEVER
existed -- not in the tree, not in BTL4.RES, not anywhere in git history.
The texture load has failed on every machine since the engine was written,
and every billboard particle ever rendered was untextured quads via
SetTexture(0, NULL). RenderParticles deliberately does NOT gate on the
texture -- that would disable all particles everywhere; untextured IS the
shipped look. Filed separately; a real particle sheet is a content task.
The field verification that counts is Conn Man flying his exact crash
loadout on this build: instead of a dead process he should see at worst a
brief hitch and "[render] device reset OK" in his log. #35 stays open until
that happens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
263 lines
22 KiB
Markdown
263 lines
22 KiB
Markdown
---
|
||
id: wintesla-port
|
||
title: "The WinTesla Windows Port — the base everything builds on"
|
||
status: established
|
||
source_sections: "PROGRESS_LOG.md §5b, §8"
|
||
related_topics: [source-completeness, rendering, build-and-run, multiplayer]
|
||
key_terms: [WinTesla, L4D3D, MUNGA, RP, DPL]
|
||
---
|
||
|
||
# The WinTesla Windows Port
|
||
|
||
The `Elsewhen RP411` archive is a **working Windows port of the MUNGA engine** (VS2005,
|
||
`WinTesla.sln`). It means the hardest infrastructure is **already solved** — the bt411 build sits
|
||
on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`.
|
||
|
||
## What WinTesla already did (do NOT rebuild)
|
||
- **Renderer bypass: DONE** — `MUNGA_L4/L4D3D.cpp` + `DXUtils` replace libDPL / the IG board with
|
||
Direct3D9. No `libdpl.lib` in the tree. (The early from-scratch D3D9 viewer was retired and removed.) [T1]
|
||
- **Device-loss: the #35 "Owens crash" — ROOT-CAUSED + FIXED 2026-07-29** [T2, bench-reproduced].
|
||
8 byte-identical field stacks (one player, Surface Pro 9 / **Iris Xe 128 MB** — the only GPU in
|
||
the fleet that ever actually loses the device): `ParticleEngine::Destroy +0x11`, `access=0
|
||
target=0x0`, from the per-frame render path. **Not a weapons bug** — the Owens two-trigger
|
||
missile+laser combo is merely what provokes the GPU timeout on that iGPU. The broken protocol
|
||
(both Present sites, inline copies): on `D3DERR_DEVICELOST` they called `Reset()` IMMEDIATELY —
|
||
which **always fails while still lost** (`V()` only logged) — then `ParticleEngine::Initialize()`
|
||
ran against the lost device, its creates failed and **NULLed the out-params** (verified: the bench
|
||
repro faulted at `target=0x0`, not a dangling address), and the NEXT lost frame's blind
|
||
`mVertBuffer->Release()` read a vtable at 0x0. FIX (`L4VIDEO.cpp` + `L4PARTICLES.cpp`): one shared
|
||
`DPLRenderer::BTResetLostDevice()` — release POOL_DEFAULT resources every lost frame (idempotent,
|
||
null-safe `Destroy`), **gate `Reset()` on `TestCooperativeLevel() != D3DERR_DEVICELOST`**, check
|
||
the Reset HRESULT, and on success re-create via **`CreateDeviceObjects()` — NOT `Initialize()`**,
|
||
whose `memset(mInstalledEffects)` silently killed every particle effect for the rest of the
|
||
mission on each reset that DID succeed (the quieter sibling bug). Draw paths guard the NULL buffer
|
||
(`ExecuteParticles` keeps draining particles while dormant). Bench: `BT_DEVICELOST_TEST` repro'd
|
||
the field crash byte-for-byte pre-fix, and post-fix the same shape logs `SURVIVED` + 3 forced
|
||
loss/reset cycles recover (`[render] device reset OK`).
|
||
⚠ **Discovery from the verify: `VIDEO\particles.png` has NEVER existed** — absent from the tree,
|
||
the RES, and all of git history — so `mParticleTexture` has been NULL on every machine since the
|
||
engine was written and **all billboard particles draw as untextured quads** (`SetTexture(0,NULL)`
|
||
= the shipped look). Deliberately NOT gated on in `RenderParticles` — that would disable all
|
||
particles everywhere. Filed on the tracker; a real texture (or the original particle sheet) is a
|
||
content task. Possibly related to the field "missile artifact looks wrong" observation.
|
||
- **Audio SOURCE BUDGET — the dropout complaint was fixed 2026-07-23; the leftover is NOT the same
|
||
bug** [T2, 2026-07-28]. **The reported bug is closed**: `a999e5c` (per-source delete instead of the
|
||
spec-atomic bulk `alDeleteSources`) stopped the pool leaking, and there have been **no field
|
||
complaints since**. Night-5 logs still show 2017 `ACQUIRE FAILED` lines in a match, all at
|
||
`live=256` — but that is **transient exhaustion, not a leak**: the census sits at 45-60 live, spikes
|
||
during firefights, and drains straight back (226→42), i.e. the engine's priority steal loop
|
||
(`AudioSourceStop`/`SuspendMaintenance` → `ReleaseChannels` → `ReleaseSourceSet` →
|
||
`alDeleteSources`) is working. A dropped voice there competes with ~256 already sounding, so it is
|
||
very likely sub-perceptual — treat the log noise as a symptom to watch, **not** as the old bug.
|
||
Mechanism if it ever does matter: `alcCreateContext(device, NULL)` passes no attribute list, so
|
||
OpenAL Soft applies its DEFAULT 256 mono sources (an OpenAL default, unrelated to the 1995 audio
|
||
hardware), while a busy match logs ~7200 explosions × 3 `DPLIndependantEffect` voices.
|
||
⚠ **Raising the cap is deliberately OPT-IN (`BT_AUDIO_SOURCES=<n>`, no rebuild needed)** — the 256
|
||
ceiling doubles as a **governor**. The steal loop only steals when the incoming source outranks a
|
||
running one, so a higher cap means far more voices mixing at once, and with EFX reverb + lowpass
|
||
chains live that is real CPU spent exactly during heavy combat. **Measure frame time before making
|
||
it default.** Granted values measured: 1024→1024, 2048→2048, 64→**240** (OpenAL Soft's own floor,
|
||
which is also why the NULL default lands on 256) — always read back what was granted, a request is
|
||
not a promise.
|
||
- **Audio: BACKEND now REAL + soundbank loaded; only game-triggering remains** [T2, updated
|
||
2026-07-15]. OpenAL replaces the HMI "SOS" engine and the renderer→device→buffer→source→play chain
|
||
IS all implemented (`L4AUDRND`/`L4AUDRES`/`L4AUDLVL`; `PatchLevelOfDetail::PlayNote` really calls
|
||
`alSourcePlay`). The "no sound, ever" root cause was DEEPER than a gate — **both audio backend DLLs
|
||
shipped in `engine/lib/` were no-op STUBS**: (a) `libsndfile-1.dll` exports 15 funcs BY ORDINAL
|
||
ONLY (no names), `sf_open()` always returns NULL; (b) `OpenAL32.dll` (72KB) imported **only
|
||
KERNEL32** — no dsound/wasapi/winmm — so it returned fake handles (`ctx=0x00000001`,
|
||
`alGenSources→0`) and never touched the hardware. So the chain ran clean and silent. FIXES (commit
|
||
5b46655): **OpenAL32.dll → real OpenAL Soft 1.25.2 Win32** (drop-in: the exe imports the 25 AL funcs
|
||
by NAME; `alSourcePlay` now reaches `AL_PLAYING`). **libsndfile DROPPED** — replaced by `LoadWavPCM()`
|
||
in `L4AUDRES` (a tiny in-tree RIFF/WAVE fmt+data reader, no external dep). Gates fixed earlier: the
|
||
`AWE_FRONT`/`AWE_REAR` env-var gate in `MakeAudioRenderer` was **relaxed to default-on** (`BT_NO_AUDIO=1`
|
||
disables). **Soundbank recovered**: 241 samples cracked from `AUDIO1/2.RES` (SF2 v1.0) by
|
||
`tools/sf2extract.py` → `content/AUDIO/*.wav` + the `allPresets[2][128]` table (`audiopresets.cpp`,
|
||
replacing the zero-init btstubs stub); all 241 load (`alErr=0`). **In-game triggering now WORKS** (commit 7b1e469): enabling audio unlocked
|
||
`L4AudioRenderer::CreateEntityAudioObjects`, which binds `AudioStateWatcher`s to entity/subsystem
|
||
state attrs BY NAME (the **attribute-pointer** databinding, same as gauges) and calls
|
||
`AddAudioWatcher` on the resolved `StateIndicator`. The reconstruction had pointed the Mech's state
|
||
attrs at the scalar read-pad `attrPad` (fine for a gauge that READs a value, fatal for a state watcher
|
||
that calls a METHOD → AV in `SChainOf::Add` on 0xCDCDCDCD). FIXED: the Mech's `AnimationState`/
|
||
`ReplicantAnimationState` (driven from `SetBodyAnimation`) and `CollisionState` (driven from
|
||
`ProcessCollision` via `collisionTemporaryState`) are now real `StateIndicator` members — **verified:
|
||
walking fires `SetupPatch`+`PlayNote`/`alSourcePlay` (footstep/gait patches 78/80/116)**. So mech
|
||
movement/animation/collision audio is live. **Subsystem STATE audio DONE** (commits 5ba541e/0e05a22/dcdd7dd):
|
||
the audio watcher TYPE per attribute was recovered via a `MakeObjectImplementation` ClassID trace
|
||
(class 28 = `AudioStateTrigger`/StateIndicator). KEY: the binary's 0x54-byte subsystem alarm
|
||
(`FUN_0041b9ec`, reconstructed as `GaugeAlarm54`) **IS a StateIndicator** — its "three sub-indicators"
|
||
@0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, `level@0x14` = currentState. The
|
||
reconstruction had modeled that tail as an opaque `_tail` (never constructed → AddAudioWatcher AV'd on
|
||
0xCDCDCDCD). FIX: `GaugeAlarm54` now has three REAL constructed sockets + `AddAudioWatcher`; `SetLevel`
|
||
fires them on change (empty sockets = no-op, so other alarms/weapons unaffected; `levelB`/`LevelCountB`
|
||
untouched; sizeof stays 0x54). Then registered each state attr → its subsystem's own alarm (own
|
||
`AttributePointers[]` chained to the parent): `Generator.GeneratorState`→stateAlarm,
|
||
`Condenser.CondenserState`→condenserAlarm, `Reservoir.ReservoirState`→reservoirAlarm,
|
||
`AmmoBin.AmmoState`→ammoAlarm (+ chained AmmoBin's bare index so `SimulationState` resolves too).
|
||
`GeneratorOn`→generatorOn. **audiostate skips 85 → 0** — every StateIndicator audio attribute across
|
||
the mech + all subsystems binds to a real indicator, and the alarms are already `SetLevel`'d by the
|
||
sims, so generator/condenser/reservoir/ammo state sounds fire on transition. **Still inert** (read 0,
|
||
silent, non-crashing — no modeled backing member in the size-locked layouts, a polish follow-up):
|
||
`ReportLeak` (Logical), `Torso.MotionState` (Enum), `Torso.SpeedOfTorsoHorizontal` (Scalar).
|
||
NOW REAL (fidelity Phase 1, commit fc7f311): `ConfigureActivePress` published at the **MechSubsystem
|
||
BASE** (binary attr id 2, descriptor @0x50de5c → member @+0x110 — the field previously misnamed
|
||
`vitalSubsystemIndex`; idle −1, driven 0/−1 by `MechWeapon::ConfigureMappablesMessageHandler`),
|
||
`AmmoBin.FireCountdownStarted` → `cookOffArmed`@0x18C, `L4MechControlsMapper.TargetRangeExponent` →
|
||
`targetRangeExponent`@0x1a4. The **bring-up guards**
|
||
[T3, self-clearing] stay: a NULL subsystem attr → inert pad (was a fatal `Fail`); an
|
||
`AudioStateWatcher` on an unconstructed indicator (chain@+0x18 null/0xCDCDCDCD) → skip. Both pass
|
||
automatically once a subsystem is built. Diagnostics: `BT_AUDIO_LOG` (chain + skips/redirects), `BT_ATTRBIND_LOG` (every
|
||
attr bind), `BT_AUDIO_TEST` (proof-of-life buffer0).
|
||
- **Audio FIDELITY pass (2026-07-16, commits fc7f311 + 691e885; the full audit ledger is
|
||
`docs/AUDIO_FIDELITY.md`, 37 findings).** Settled facts [T1 unless noted]:
|
||
- **Patch numbering = SF2 `wPreset`.** Any tooling must key presets on `wPreset`, never on phdr
|
||
record order. `AUDIO1/2.RES` are SF2 **v1.0** (16-byte shdr, NO name/rate/pitch field); pitch
|
||
metadata rides generators: 55 = SBK samplePitch (`root*100+cents`, on every zone), 58 =
|
||
overridingRootKey, 51/52 = coarse/fineTune. **EMU8000 v1 base rate = 44100 Hz** → a zone's true
|
||
data rate is baked into its WAV: `rate = round(44100·2^(((6000−rootCents)+tune)/1200))`
|
||
(`tools/sf2extract.py`); the engine's note-60 pitch model (`BTNotePitchFactor`) then reproduces
|
||
original playback at every note. SBK `initialAttenuation`(48) is **INVERTED** (127 = full
|
||
volume; 0.375 dB/step [T3 exact curve], baked into the PCM).
|
||
- **Full-zone table**: 603 zones / 241 presets in `audiopresets.cpp`; the authored MIDI note
|
||
SELECTS zones by keyRange in `SetupPatch/PlayNote` (key-splits: Loaded/Jam family low-36
|
||
clunk vs high-84 blip; Warnings01 8-way klaxon split), layers attach together,
|
||
`MAX_PRESET_SAMPLES=25` (AllExplosion). Authored loop REGIONS apply via `AL_SOFT_loop_points`;
|
||
authored `releaseVolEnv` (1.1–3.9 s on ~20 looping presets) fades on Stop
|
||
(`PRESET_serviceReleaseFades`, serviced from `AudioHead::Execute`).
|
||
- **⚠ THE LOOP FLAG IS NOT THE LOOP DECISION (Gitea #51, 2026-07-25).** `SetupPatch`
|
||
(`L4AUDLVL.cpp`) used to map `AL_LOOPING = (sample != ForceStatic)`, which armed **224 of the
|
||
603 authored samples** as OpenAL sources that never end — any whose note-off never arrives plays
|
||
**forever** (SAURON: "coolant flush sound glitched and perma"). Three facts say the sample flag
|
||
only expresses PERMISSION, and the SOURCE decides [T1]:
|
||
(1) the enum's own comments (`L4AUDLVL.h:14-19`) — `LoopAtWill` = "will play once **or loop as
|
||
desired**", `ForceStatic` = "plays only once **even if looped**" (a veto), `LoopAlways` =
|
||
always; (2) `LoopAtWill` is enum value **0**, i.e. the DEFAULT an unauthored sample receives
|
||
(`WTPresets.cpp:37`) — it cannot mean "loop forever"; (3) `LoopAlways`, the real always-loop, is
|
||
used by **zero** shipped samples. "As desired" is the source's authored **`AudioRenderType`**
|
||
(`Transient`=one-shot vs `Sustained`=held), streamed from `AUDIO*.RES` (`AUDLVL.cpp:28`) and
|
||
already consulted by the renderer (`L4AUDRND.cpp:567`). New rule: `LoopAlways`→1,
|
||
`ForceStatic`→0, `LoopAtWill`→ the source's render type. **Measured** (`BT_LOOP_AUDIT=1`,
|
||
`scratchpad/loopaudit.py`): `LoopAtWill×Transient` 1946 events flip 1→0, `LoopAtWill×Sustained`
|
||
65 events stay 1, `ForceStatic×Transient` 292 unchanged — the engine loops
|
||
(EngineAccel07_z0-z2/EngineMotor01/EnginePower01) are all Sustained and **preserved**; all 24
|
||
reclassified samples are genuine one-shots (laser charge/fire/explosion, missile loading,
|
||
coolant pressure inc/dec). **A/B proof** (`scratchpad/loopab.py`): under the old rule
|
||
`CoolantPresInc03_z2` + `CoolantPresDcr03_z2` are still playing with `loop=1` after every
|
||
release; under the new rule zero coolant sources survive, engine loop intact. `BT_LOOP_LEGACY=1`
|
||
restores the old rule for a field A/B — listen to the beam-sustain layers (`LaserA/CSustain*` are
|
||
`LoopAtWill` on a Transient source, so they now end with the sample). A stuck looping source also
|
||
holds a pool slot for the rest of the match, so this plausibly contributed to **#32**'s dropouts.
|
||
- **Distance/volume/doppler**: OpenAL distance model is `AL_NONE` — the AUTHORED
|
||
`GetDistanceVolumeScale()` curve multiplies in `CalculateSourceVolumeScale` (Dynamic3D +
|
||
Static3D); per-source `AL_GAIN = volume_scale²` (GM CC7 squared law); `alDopplerFactor(0)` —
|
||
the authored `GetDopplerCents()` (AUDIO.INI model, consumer decomp part_008.c:7466) adds into
|
||
the Dynamic3D pitch chain.
|
||
- **Audio clock**: `FUN_0044e19c` is `ApplicationManager::GetFrameRate` — the original audio
|
||
"frames" were SYSTEM CLOCK TICKS; `MakeAudioRenderer` calibrates at
|
||
`SystemClock::GetTicksPerSecond()` (fallback 1000), never the old garbage
|
||
`divisionParameters+0x10` read.
|
||
- **Published `localVelocity` smoothing is authentic** [T1]: `Mech::Execute` (FUN_004ab430) runs
|
||
the localVelocity components through `AverageOf` running-average filters
|
||
(part_012.c:15169-15179); the port's ~0.25 s exponential filter in `mech4.cpp` reconstructs it.
|
||
- **Phases 3+4 SHIPPED (2026-07-16, commits 0ca7d26..0e2401f).** Phase 3 EFX: `L4AUDEFX`
|
||
bridge — per-source lowpass (Dyn3D = highFreqCutoffScale×brightness UNGATED, Static/Direct =
|
||
brightness; AWE 100–8000 Hz → 5 kHz-ref gainhf [T3 curve]), one EAXReverb slot at
|
||
global_reverb_scale 0.3 (3D wet / cockpit Direct dry), Direct position-enum placement.
|
||
Phase 4 attributes (all [T1] from binary attribute tables, walked as 16-byte
|
||
{id,name,off+1,pad} rows): **HeatSink table @0x50e438 (ids 3..12)** confirms every heat
|
||
binding + adds ReportLeak id 12 @0x138 = coolantActive (serves all 19 leak watchers via
|
||
PoweredSubsystem⊂HeatSink); **Mech table @0x50c0xx (ids 21..56)**: IncomingLock@0x3fc /
|
||
DistanceToMissile@0x400 (real members, Missile::MoveAndCollide reports, PerformAndWatch
|
||
latches; old `maxSpeed@0x400` was a misread of the FLT_MAX far default),
|
||
CollisionSpeed@0x4B4 (=|impact velocity| at contact-arm), ReduceButton@0x340,
|
||
FootStep@0x394, RearFiring@0x410 (old `stateFlags` misread); **Torso table (ids 3..15)**:
|
||
full publish onto existing members (SpeedOfTorsoHorizontal=twistVelocity@0x1E8 abs'd,
|
||
MotionState=statusFlags@0x20C, ==2 = limit-hit frame). **F5 footStep = CONTACT LEVEL**:
|
||
(jointlocal.y <= *ANI hdr[2] threshold) per frame, states 0/1 retain (SelectSequence now
|
||
captures hdr[2]; the 150 ms pulse is retired). **F19**: the authored footstep volume =
|
||
LocalAcceleration |lin| [0,10]→ctl100 + LocalVelocity |lin| [0,0.6]→ctl101 (ALL motion
|
||
watchers extract |linearMotion|, motionValue=3); localAcceleration.linear = d(published
|
||
velocity)/dt (binary part_012.c:15186-15195); the mech2.cpp step broadcast is REMOVED.
|
||
**attrnull count = 0** — every authored audio attribute binds a real member.
|
||
- **Post-Phase-4 closures (2026-07-16):** UnstablePercentage is LIVE (the instability model
|
||
@0x3F0 byte-decoded + reconstructed — accel-sway² + gun-the-engine + turn-in-place terms,
|
||
feeds gyro swayBias + the audio alarm; commit c51d64a). The F14 per-zone static resonant
|
||
low-pass is BAKED (RBJ biquad at the AWE 100–8000 Hz curve, float pipeline + peak
|
||
normalization; 232 zones incl the LaserLoaded charge hum fc=57 Q=87; c581553). The
|
||
**footstep 10–20 s warm-up bug** is fixed (a11a697): an idle source's watcher chain executes
|
||
only at Start attempts and each hop is audio-frame-gated, so the authored fill-0 N=30
|
||
smoothers starved behind the 0.3 transient drop gate — fixes: AudioScaleOf sends EVERY poll
|
||
(the bitwise change-gate froze on our deterministic gait floats), recursive gate-free
|
||
Component::PrimeWatchers(30) on every transient Start request (AUDREND), and
|
||
localAcceleration via the binary's exact 15-sample ring buffers. LESSON: trace caps
|
||
saturate early and fake "per-stride" rates — always timestamp before inferring cadence.
|
||
- Still deferred: F21 attack velocity/time (all captured events were default 1), F22 HRTF
|
||
(user has 2 speakers; enable via alsoft.ini `hrtf=true` for headphones).
|
||
- **Energy-weapon post-beam buzz = AUTHENTIC (Gitea #5 verdict, 2026-07-20)** [T2, measured]:
|
||
the reported "buzz/electrical sustaining 1–2 s past the beam" is the AUTHORED charge loop
|
||
(`LaserACharge01-04`, looped, release 1.309 s), started by the WeaponState==3 (Loading) watcher
|
||
at beam-off, volume authored to track PercentDone (crescendo to ready), stopped on 3→2 with the
|
||
authored 1.309 s release fade. Measured live (BT_AUD_TAIL=1 + BT_FIRE_PULSE single shots):
|
||
beam 203–207 ms (authored DischargeTime 0.2 s); sustain loops cut ≤2 ms after leave-Firing
|
||
(authored release 0); charge StopNote ≤2 ms after Loading→Loaded; fades 1.311–1.326 s vs
|
||
authored 1.309 s (≤ 1 frame over); laser recharge 2.3–3.8 s, PPC 8.8–16.3 s under multi-weapon
|
||
generator load (authentic electrical model). NOT the #12 source overflow — no energy preset
|
||
exceeds 5 zones (max 3; >5 is only the death/UI families: AllExplosion 25, MechExplosion01/
|
||
Death01 16, AllButtons 14, AllCoolant/AllProjectile 9, Warnings01/AuxExplosion01/AllHits 8,
|
||
AllWarning 7). Caveat: the reporter's 2026-07-18 build predated 2edde29 (Emitter::SetDirty
|
||
DelayWatchersFlag poison retirement — PerformAndWatch skips ExecuteWatchers while set,
|
||
SIMULATE.cpp:461; cleared only by the encore path, UPDATE.cpp:215), which made emitter audio
|
||
watcher timing encore-erratic that session. Harness kept (env-gated, additive): `[aud-tail]`
|
||
logging in `L4AUDLVL.cpp` (PLAY/STOP/fade) + `emitter.cpp` (state transitions), and
|
||
`BT_FIRE_PULSE="on,off"` single-shot trigger pulsing in `mech4.cpp`.
|
||
- **Audio-dropout fix (field 2026-07-23: "cutting in and out toward the end of the match")** [T2,
|
||
code-verified vs the AL spec; awaiting live playtest confirm]: three structural defects in the
|
||
port's OpenAL source-pool lifecycle (`L4AUDRND.cpp` / `L4AUDIO.cpp` / `L4AUDHDW.h`):
|
||
(1) `SourceSet.sources[]` was **uninitialized heap garbage** until first acquisition — AL names
|
||
are small ints, so a recycled-heap slot could alias a LIVE source owned by another sound;
|
||
`alIsSource()` then skipped generation, two sounds shared one source, and whichever released
|
||
first deleted the other's source mid-play (random sound deaths worsening with heap churn).
|
||
Fix: `SourceSet` ctor zero-inits (0 is never a valid name). (2) `ReleaseSourceSet` used bulk
|
||
`alDeleteSources(count, sources)`, which is **ATOMIC on invalid names** — one empty/-1 slot
|
||
(partial set, double release) and NOTHING deleted; after the first pool exhaustion every partial
|
||
release leaked and the pool never recovered. Fix: per-slot guarded delete, slots parked at 0.
|
||
(3) Failed acquisitions stranded partial sets forever — dropped transients are never released by
|
||
anything (the dtor didn't release; maintenance only touches running sources). Fix: handback via
|
||
`ReleaseChannels()` at both failure sites (StartRequest + dormant-resume) + a dtor backstop.
|
||
Context: the port's `RequestAudioChannels` has NO channel budget (the 1995 per-card budget is
|
||
the commented-out block) — it alGenSources until OpenAL Soft's ~256 cap; on failure a transient
|
||
is silently dropped (StartRequest early-return), a sustained goes dormant — which is exactly the
|
||
"cutting in and out" presentation. Priority-steal (`RequestAudioResources`) only engages at cap.
|
||
PERMANENT diagnostics: 30 s `[audio] source census: live/acquireFails` + an always-on
|
||
`[audio] ACQUIRE FAILED` line. Baseline (2-node soak): live ≈ 21 peak under fire, 0 at mission
|
||
end; exhaustion needs 7-player scale or the leak spiral, aliasing needs neither.
|
||
- **L4 HAL on Windows + the VS build system: DONE.** [T1]
|
||
- **Red Planet game logic: COMPLETE & buildable** — `VTV/VTVMPPR/WEAPSYS/...`. RP is at/near playable. [T1]
|
||
- **Multiplayer: the DOS `NETNUB` driver is already replaced by a ~3.5k-line WinSock2 TCP
|
||
reimplementation** (`MUNGA_L4/L4NET.CPP`) + the master/replicant distributed-sim core. See
|
||
[[multiplayer]]. [T1]
|
||
|
||
## What BT still needs (the only remaining work below the game layer is NONE)
|
||
Everything below the game layer — engine, renderer, audio, HAL, build — exists and is shared. The
|
||
ONLY thing BT needs is its **game-logic source**, which is gone (see [[source-completeness]]) → it
|
||
must be **reconstructed**, with the three anchors (BT headers + the WinTesla engine to compile
|
||
against + RP's parallel code + the binary oracle). [T2]
|
||
|
||
## Build implications
|
||
- `WinTesla.sln` is **VS2005** and hard-depends on the **legacy DirectX SDK (June 2010)** (`d3dx9`,
|
||
`dinput`, `dxerr` — all removed from the modern Windows SDK). The DXSDK is installed at
|
||
`C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\`. [T2]
|
||
- The shared engine builds GREEN to `munga_engine.lib` via CMake/MSVC Win32 (see [[build-and-run]]).
|
||
Recipe gotchas (include-path shadowing, the CAMMGR/time.h fixes, the ATL shim) are in
|
||
`docs/PROGRESS_LOG.md §8`. [T2]
|
||
|
||
## Roadmap position
|
||
Old plan ("build a libDPL shim + port the engine") is **superseded** — WinTesla did it. New plan:
|
||
reuse the WinTesla engine (shared) + reconstruct the BT game library (`BT410_L4` + game logic). The
|
||
from-scratch libDPL shim is no longer needed. Phases 4-8 (BT game layer + pod bring-up) still apply.
|
||
[T2]
|
||
|
||
## Key Relationships
|
||
- Solves: the infrastructure that [[source-completeness]] would otherwise require.
|
||
- Feeds: [[build-and-run]], [[rendering]], [[multiplayer]].
|