From a999e5c49ffdabea2724dc00bb199c8618828290 Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 23 Jul 2026 11:23:07 -0500 Subject: [PATCH] Audio-dropout fix: OpenAL source-pool lifecycle (uninit aliasing, atomic-delete leak, stranded partials) Field report: audio cutting in and out toward the end of the match. Three structural defects in the port's source-pool lifecycle: 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() skipped generation, two sounds shared one source, and whichever released first deleted the other's mid-play. SourceSet ctor now zero-inits. 2. ReleaseSourceSet's bulk alDeleteSources(count, sources) is ATOMIC on invalid names (AL spec) -- one empty/-1 slot and NOTHING deleted; after the first pool exhaustion every partial release leaked and the pool never recovered. Now per-slot guarded delete, slots parked at 0. 3. Failed acquisitions stranded partial sets forever (dropped transients are never released by anything). ReleaseChannels() handback at both failure sites (StartRequest + dormant-resume) + a dtor backstop. Verified: 2-node MP smoke (mission + lobby-loop relaunch clean, census live/acquireFails 0/0) + 100s solo combat smoke (52 audio triggers, zero AL errors). Awaiting live playtest confirmation at scale. KB: context/wintesla-port.md audio-dropout section. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/wintesla-port.md | 21 ++++++++++++++++++++ engine/MUNGA_L4/L4AUDHDW.h | 13 +++++++++++++ engine/MUNGA_L4/L4AUDIO.cpp | 14 ++++++++++++++ engine/MUNGA_L4/L4AUDRND.cpp | 37 ++++++++++++++++++++++++------------ 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/context/wintesla-port.md b/context/wintesla-port.md index 63af428..e21556d 100644 --- a/context/wintesla-port.md +++ b/context/wintesla-port.md @@ -144,6 +144,27 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`. 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 diff --git a/engine/MUNGA_L4/L4AUDHDW.h b/engine/MUNGA_L4/L4AUDHDW.h index fb2155c..7e2c334 100644 --- a/engine/MUNGA_L4/L4AUDHDW.h +++ b/engine/MUNGA_L4/L4AUDHDW.h @@ -24,6 +24,19 @@ struct SourceSet { int count; ALuint sources[AUDIO_SOURCESET_CAPACITY]; + + // Audio-dropout fix (field 2026-07-23): sources[] was UNINITIALIZED heap + // garbage until the first acquisition. OpenAL names are small integers -- + // exactly what recycled heap is full of -- so a garbage slot could alias a + // LIVE source owned by another sound; alIsSource() then skipped generation + // and two sounds shared one AL source, and whichever released first + // deleted the other's source mid-play (sounds randomly dying, worsening + // with heap churn late in a match). 0 is never a valid source name. + SourceSet() : count(0) + { + for (int i = 0; i < AUDIO_SOURCESET_CAPACITY; i++) + sources[i] = 0; + } }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MIDI types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/engine/MUNGA_L4/L4AUDIO.cpp b/engine/MUNGA_L4/L4AUDIO.cpp index 15fd021..681d7da 100644 --- a/engine/MUNGA_L4/L4AUDIO.cpp +++ b/engine/MUNGA_L4/L4AUDIO.cpp @@ -719,6 +719,20 @@ void // L4AudioSource::~L4AudioSource() { + // Audio-dropout fix: backstop -- if this source dies still holding OpenAL + // sources (the stop/suspend maintenance never ran for it), hand them back + // so entity churn can't drain the pool over a long match. + if (application != NULL && application->GetAudioRenderer() != NULL) + { + for (int i = 0; i < channelSet.count; i++) + { + if (alIsSource(channelSet.sources[i])) + { + application->GetAudioRenderer()->ReleaseSourceSet(channelSet); + break; + } + } + } } // diff --git a/engine/MUNGA_L4/L4AUDRND.cpp b/engine/MUNGA_L4/L4AUDRND.cpp index 87c589a..09abb06 100644 --- a/engine/MUNGA_L4/L4AUDRND.cpp +++ b/engine/MUNGA_L4/L4AUDRND.cpp @@ -558,6 +558,12 @@ void ); if (!resources_available) { + // Audio-dropout fix: a failed acquisition can leave a PARTIAL set + // (alGenSources succeeded for some slots before the pool ran dry). + // A dropped transient never plays and nothing else ever released it, + // so the partial sources dangled forever -- once the pool exhausted + // ONCE, every drop leaked a little more and it never recovered. + audio_source->ReleaseChannels(); if (audio_source->GetAudioRenderType() == SustainedAudioRenderType) { // @@ -1024,7 +1030,12 @@ void if (!RequestAudioResources(audio_source, source_result) ) + { + // Audio-dropout fix: hand back any partial acquisition (see + // StartRequest) -- the source stays dormant and retries later. + audio_source->ReleaseChannels(); break; + } // // Remove from dormant list and request list @@ -1445,26 +1456,28 @@ long gBTAudioAcquireFails = 0; void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet) { + // Audio-dropout fix: the old bulk alDeleteSources(count, sources) is + // ATOMIC per the AL spec -- ONE invalid name in the array (an empty slot + // of a partial set, or the old -1 sentinel on a double release) and + // NOTHING is deleted. After the first pool-exhaustion event every + // partial release leaked its real sources and the pool never recovered + // ("audio cutting in and out toward the end of the match"). Delete each + // valid source individually and park the slot at 0 (never a valid name). extern long gBTAudioSourcesLive; for (int i = 0; i < sourceSet.count; i++) { if (alIsSource(sourceSet.sources[i])) - --gBTAudioSourcesLive; - ALenum state; - alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state); - - if (state == AL_PLAYING) { - alSourceStop(sourceSet.sources[i]); + ALint state = AL_STOPPED; + alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state); + if (state == AL_PLAYING) + alSourceStop(sourceSet.sources[i]); + alDeleteSources(1, sourceSet.sources + i); + --gBTAudioSourcesLive; } + sourceSet.sources[i] = 0; } - - alDeleteSources(sourceSet.count, sourceSet.sources); - for (int i = 0; i < sourceSet.count; i++) - { - sourceSet.sources[i] = -1; - } } //~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~