#32: POOL the OpenAL sources instead of creating and destroying one per sound
Every 4.11.674 player log is saturated with acquisition failures -- 3031, 4657, 5245, 6275, 6571 across five machines. In Lynx's largest session the first failure lands 9.3% in and they continue to 96.8%: once it starts it never recovers for the rest of the match. CAUSE: RequestAudioChannels called alGenSources() per sound event and ReleaseSourceSet called alDeleteSources() on release -- create and destroy per sound. OpenAL sources are a scarce driver resource (OpenAL Soft caps a context at 256) and a combat burst churned straight through the ceiling. The two counters looked contradictory and were the tell: ACQUIRE FAILED always printed live=256 while the 30s census printed live=6. Same global, sampled at different moments -- sources spike to the cap during a burst and drain back between them. Churn, not a steady leak. FIX: generate sources once, up to a cap, and recycle them through a free list. Release scrubs and parks instead of deleting. Steady-state play performs no AL allocation at all. The scrub is load-bearing, not hygiene: a recycled source carries whatever the previous owner set, and the engine sets AL_LOOPING per sound (L4AUDLVL.cpp:327). Hand a looping source to a one-shot and it plays forever -- which is the "sound stuck looping" family (#51, #5). Every reusable property is reset at the single point where a source changes owner. VERIFIED (scratchpad/night8/audiopool.sh + the two-node mp_burst.sh): solo, sustained fire : 0 failures, pooled 152, reuses 21998 two nodes, 4 min : 0 failures on both, pooled 148/149, reuses ~7000 each ⚠ HONEST LIMIT: the bench does NOT reproduce the field failure -- the PRE-fix binary also scores 0 on it (peak 49 live), because solo/2-node combat is not dense enough to reach 256. So this verifies the pool works and allocates nothing in steady state; it does NOT by itself prove the field failures are gone. The five field logs remain the "before". Peak demand is set by how many audio COMPONENTS are alive (each reserves a SourceSet of up to 25 voices and holds them), not by audible sounds: measured high-water 138 solo, 149 two-node. That scales with player count, so the cap is set near the driver ceiling (240) and the pool now logs its high-water mark once per 25-source band -- so the next playtest sizes this from field data instead of a guess. Growth also self-limits: if a driver offers fewer sources than the cap, alGenSources simply fails, growth stops, and the pool recycles what it has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3a22c334ac
commit
ad9dfade88
+178
-34
@@ -1371,6 +1371,15 @@ Logical
|
|||||||
return resources_available;
|
return resources_available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// OpenAL source pool (gitea #32) -- defined below, used here.
|
||||||
|
//
|
||||||
|
extern Logical BTAudioPoolAcquire(ALuint *out);
|
||||||
|
extern void BTAudioPoolRelease(ALuint src);
|
||||||
|
extern int BTAudioPoolSize();
|
||||||
|
extern int BTAudioPoolFree();
|
||||||
|
extern long BTAudioPoolReuses();
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// RequestAudioChannels
|
// RequestAudioChannels
|
||||||
@@ -1402,6 +1411,9 @@ Logical
|
|||||||
{
|
{
|
||||||
s_censusAt = now_ms;
|
s_censusAt = now_ms;
|
||||||
DEBUG_STREAM << "[audio] source census: live=" << gBTAudioSourcesLive
|
DEBUG_STREAM << "[audio] source census: live=" << gBTAudioSourcesLive
|
||||||
|
<< " pooled=" << BTAudioPoolSize()
|
||||||
|
<< " free=" << BTAudioPoolFree()
|
||||||
|
<< " reuses=" << BTAudioPoolReuses()
|
||||||
<< " acquireFails=" << gBTAudioAcquireFails
|
<< " acquireFails=" << gBTAudioAcquireFails
|
||||||
<< std::endl << std::flush;
|
<< std::endl << std::flush;
|
||||||
}
|
}
|
||||||
@@ -1414,35 +1426,41 @@ Logical
|
|||||||
if (requested > AUDIO_SOURCESET_CAPACITY)
|
if (requested > AUDIO_SOURCESET_CAPACITY)
|
||||||
requested = AUDIO_SOURCESET_CAPACITY;
|
requested = AUDIO_SOURCESET_CAPACITY;
|
||||||
|
|
||||||
bool failed = true;
|
|
||||||
|
|
||||||
alGetError();
|
alGetError();
|
||||||
|
|
||||||
|
//
|
||||||
|
// SOURCE POOLING (gitea #32). This used to alGenSources() here and
|
||||||
|
// alDeleteSources() in ReleaseSourceSet -- i.e. CREATE and DESTROY OpenAL
|
||||||
|
// sources per sound event. Every 4.11.674 player log shows the result:
|
||||||
|
// thousands of acquisition failures each (3031-6571 across five machines),
|
||||||
|
// beginning ~10% into a match and never recovering, because a combat burst
|
||||||
|
// churns straight through OpenAL's hard per-context source limit (256).
|
||||||
|
// The 30s census reading `live=6` while the failure line read `live=256`
|
||||||
|
// was the tell: the same counter, sampled between bursts -- churn, not a
|
||||||
|
// steady leak.
|
||||||
|
//
|
||||||
|
// Sources are now generated ONCE and recycled through a free list, so
|
||||||
|
// steady-state demand costs no allocation at all and the driver cap is
|
||||||
|
// never approached.
|
||||||
|
//
|
||||||
for (int i = 0; i < requested; i++)
|
for (int i = 0; i < requested; i++)
|
||||||
{
|
{
|
||||||
if (!alIsSource(source_request->sources[i]))
|
if (alIsSource(source_request->sources[i]))
|
||||||
|
continue; // slot already holds one
|
||||||
|
ALuint src = 0;
|
||||||
|
if (!BTAudioPoolAcquire(&src))
|
||||||
{
|
{
|
||||||
alGenSources(1, source_request->sources + i);
|
++gBTAudioAcquireFails;
|
||||||
if (alIsSource(source_request->sources[i]))
|
DEBUG_STREAM << "[audio] ACQUIRE FAILED (requested=" << requested
|
||||||
++gBTAudioSourcesLive;
|
<< " live=" << gBTAudioSourcesLive
|
||||||
|
<< " pooled=" << BTAudioPoolSize()
|
||||||
|
<< " free=" << BTAudioPoolFree()
|
||||||
|
<< " fails=" << gBTAudioAcquireFails
|
||||||
|
<< ") -- the pool is exhausted; expect dropouts"
|
||||||
|
<< std::endl << std::flush;
|
||||||
|
return False;
|
||||||
}
|
}
|
||||||
}
|
source_request->sources[i] = src;
|
||||||
|
|
||||||
ALenum error = alGetError();
|
|
||||||
if (error == AL_NO_ERROR)
|
|
||||||
{
|
|
||||||
failed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed)
|
|
||||||
{
|
|
||||||
++gBTAudioAcquireFails;
|
|
||||||
DEBUG_STREAM << "[audio] ACQUIRE FAILED (requested=" << requested
|
|
||||||
<< " live=" << gBTAudioSourcesLive
|
|
||||||
<< " fails=" << gBTAudioAcquireFails
|
|
||||||
<< ") -- the pool is exhausted; expect dropouts"
|
|
||||||
<< std::endl << std::flush;
|
|
||||||
return False;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return True;
|
return True;
|
||||||
@@ -1524,6 +1542,137 @@ Logical
|
|||||||
long gBTAudioSourcesLive = 0;
|
long gBTAudioSourcesLive = 0;
|
||||||
long gBTAudioAcquireFails = 0;
|
long gBTAudioAcquireFails = 0;
|
||||||
|
|
||||||
|
//#############################################################################
|
||||||
|
// OpenAL SOURCE POOL (gitea #32)
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
// The renderer used to create an AL source per sound event and destroy it on
|
||||||
|
// release. OpenAL sources are a scarce driver resource -- OpenAL Soft caps a
|
||||||
|
// context at 256 -- and creating them is not cheap, so combat bursts ran the
|
||||||
|
// context dry. Field evidence (4.11.674, five machines): 3031-6571 acquisition
|
||||||
|
// failures per player, first failure ~10% into a match, still failing at 97%.
|
||||||
|
//
|
||||||
|
// Sources are now generated once, on demand, up to kAudioPoolCap and then
|
||||||
|
// RECYCLED: release scrubs the source and returns it to the free list instead
|
||||||
|
// of deleting it. Steady-state play performs no AL allocation at all.
|
||||||
|
//
|
||||||
|
// THE SCRUB IS LOAD-BEARING. A recycled source carries whatever the previous
|
||||||
|
// owner set on it, and the engine sets AL_LOOPING per sound (L4AUDLVL.cpp:327).
|
||||||
|
// Hand a looping source to a one-shot and it plays forever -- exactly the
|
||||||
|
// "sound stuck looping" family (#51, #5). Every reusable property is reset
|
||||||
|
// here, at the single point where sources change owner.
|
||||||
|
//
|
||||||
|
// Not locked: the audio renderer runs on the main thread (only the network RX
|
||||||
|
// socket has its own). If that ever changes, this needs a mutex.
|
||||||
|
//
|
||||||
|
static const int kAudioPoolCap = 240; // just under OpenAL Soft's 256 default
|
||||||
|
//
|
||||||
|
// PEAK DEMAND is set by how many audio COMPONENTS are alive, not by how many
|
||||||
|
// sounds are audible: each component reserves a SourceSet of up to
|
||||||
|
// AUDIO_SOURCESET_CAPACITY (25) voices and holds them until it is released.
|
||||||
|
// Measured high-water: 138 solo vs one enemy, 149 across two nodes. That grows
|
||||||
|
// with player count, so the cap is deliberately near the driver ceiling and the
|
||||||
|
// pool reports its high-water mark once, to size this from FIELD logs rather
|
||||||
|
// than from a guess. Growth also stops on its own if a driver offers fewer
|
||||||
|
// sources than the cap -- alGenSources failing simply ends growth and the pool
|
||||||
|
// recycles what it has.
|
||||||
|
|
||||||
|
static ALuint gAudioPoolFree[kAudioPoolCap];
|
||||||
|
static int gAudioPoolFreeCount = 0; // entries in gAudioPoolFree
|
||||||
|
static int gAudioPoolTotal = 0; // sources ever generated (<= cap)
|
||||||
|
static long gAudioPoolReuses = 0; // diagnostics
|
||||||
|
|
||||||
|
int BTAudioPoolSize() { return gAudioPoolTotal; }
|
||||||
|
int BTAudioPoolFree() { return gAudioPoolFreeCount; }
|
||||||
|
long BTAudioPoolReuses() { return gAudioPoolReuses; }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset a source to a known-neutral state so nothing carries across owners.
|
||||||
|
//
|
||||||
|
static void
|
||||||
|
BTAudioScrubSource(ALuint src)
|
||||||
|
{
|
||||||
|
ALint state = AL_STOPPED;
|
||||||
|
alGetSourcei(src, AL_SOURCE_STATE, &state);
|
||||||
|
if (state == AL_PLAYING || state == AL_PAUSED)
|
||||||
|
alSourceStop(src);
|
||||||
|
alSourcei(src, AL_BUFFER, 0); // detach (no queued buffers here)
|
||||||
|
alSourcei(src, AL_LOOPING, AL_FALSE); // <-- the stuck-loop guard
|
||||||
|
alSourcef(src, AL_GAIN, 1.0f);
|
||||||
|
alSourcef(src, AL_PITCH, 1.0f);
|
||||||
|
alSourcei(src, AL_SOURCE_RELATIVE, AL_FALSE);
|
||||||
|
alSource3f(src, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
||||||
|
alSource3f(src, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
||||||
|
alSourcef(src, AL_MIN_GAIN, 0.0f);
|
||||||
|
alSourcef(src, AL_MAX_GAIN, 1.0f);
|
||||||
|
alGetError(); // swallow any property complaint
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Hand out a source: recycle first, generate only when the pool has never been
|
||||||
|
// that large. False = genuinely out (peak concurrent demand exceeded the cap).
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
BTAudioPoolAcquire(ALuint *out)
|
||||||
|
{
|
||||||
|
Check_Pointer(out);
|
||||||
|
while (gAudioPoolFreeCount > 0)
|
||||||
|
{
|
||||||
|
ALuint src = gAudioPoolFree[--gAudioPoolFreeCount];
|
||||||
|
if (alIsSource(src)) // paranoia: a context reset invalidates names
|
||||||
|
{
|
||||||
|
++gAudioPoolReuses;
|
||||||
|
*out = src;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
--gAudioPoolTotal; // stale name: forget it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gAudioPoolTotal >= kAudioPoolCap)
|
||||||
|
return False;
|
||||||
|
|
||||||
|
ALuint src = 0;
|
||||||
|
alGetError();
|
||||||
|
alGenSources(1, &src);
|
||||||
|
if (alGetError() != AL_NO_ERROR || !alIsSource(src))
|
||||||
|
return False; // driver said no before our cap
|
||||||
|
|
||||||
|
++gAudioPoolTotal;
|
||||||
|
++gBTAudioSourcesLive;
|
||||||
|
{
|
||||||
|
// One line per new high-water band, so a field log shows how close a
|
||||||
|
// real match gets to the ceiling without spamming.
|
||||||
|
static int s_notified = 0;
|
||||||
|
if (gAudioPoolTotal >= s_notified + 25)
|
||||||
|
{
|
||||||
|
s_notified = gAudioPoolTotal;
|
||||||
|
DEBUG_STREAM << "[audio] source pool high-water: " << gAudioPoolTotal
|
||||||
|
<< " of " << kAudioPoolCap << std::endl << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = src;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Take a source back. Scrubbed and parked, NOT deleted.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
BTAudioPoolRelease(ALuint src)
|
||||||
|
{
|
||||||
|
if (!alIsSource(src))
|
||||||
|
return;
|
||||||
|
BTAudioScrubSource(src);
|
||||||
|
if (gAudioPoolFreeCount < kAudioPoolCap)
|
||||||
|
{
|
||||||
|
gAudioPoolFree[gAudioPoolFreeCount++] = src;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alDeleteSources(1, &src); // cannot happen (cap == array size)
|
||||||
|
--gAudioPoolTotal;
|
||||||
|
--gBTAudioSourcesLive;
|
||||||
|
}
|
||||||
|
|
||||||
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
||||||
{
|
{
|
||||||
// Audio-dropout fix: the old bulk alDeleteSources(count, sources) is
|
// Audio-dropout fix: the old bulk alDeleteSources(count, sources) is
|
||||||
@@ -1533,19 +1682,14 @@ void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
|||||||
// partial release leaked its real sources and the pool never recovered
|
// partial release leaked its real sources and the pool never recovered
|
||||||
// ("audio cutting in and out toward the end of the match"). Delete each
|
// ("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).
|
// valid source individually and park the slot at 0 (never a valid name).
|
||||||
extern long gBTAudioSourcesLive;
|
// SOURCE POOLING (#32): hand each source back to the free list instead of
|
||||||
|
// destroying it. BTAudioPoolRelease stops it, detaches its buffer and
|
||||||
|
// clears AL_LOOPING before parking it, so the next owner starts clean.
|
||||||
for (int i = 0; i < sourceSet.count; i++)
|
for (int i = 0; i < sourceSet.count; i++)
|
||||||
{
|
{
|
||||||
if (alIsSource(sourceSet.sources[i]))
|
if (sourceSet.sources[i] != 0)
|
||||||
{
|
BTAudioPoolRelease(sourceSet.sources[i]);
|
||||||
ALint state = AL_STOPPED;
|
sourceSet.sources[i] = 0; // 0 is never a valid AL name
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# #32 AUDIO SOURCE POOL verification.
|
||||||
|
#
|
||||||
|
# BEFORE: sources were alGenSources'd per sound event and alDeleteSources'd on
|
||||||
|
# release. Every 4.11.674 player log shows thousands of ACQUIRE FAILED lines
|
||||||
|
# (3031-6571 across five machines), first failure ~10% into the match, still
|
||||||
|
# failing at 97% -- combat bursts churned through OpenAL's 256-source context
|
||||||
|
# limit.
|
||||||
|
#
|
||||||
|
# AFTER: sources are generated once and recycled. This run reproduces the
|
||||||
|
# failure mode -- sustained combat, all weapon groups firing, missiles, deaths
|
||||||
|
# and explosions -- and reads the census.
|
||||||
|
#
|
||||||
|
# PASS:
|
||||||
|
# * ACQUIRE FAILED lines == 0
|
||||||
|
# * census `pooled` settles well under the 200 cap (steady-state demand)
|
||||||
|
# * census `reuses` climbs into the thousands (recycling is doing the work)
|
||||||
|
set -x
|
||||||
|
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||||
|
cd /c/git/bt411/content || exit 1
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > AUD.EGG
|
||||||
|
|
||||||
|
LOG=audiopool_${1:-post}.log
|
||||||
|
rm -f "$LOG"
|
||||||
|
|
||||||
|
BT_DMG_LOG=1 \
|
||||||
|
BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=2 \
|
||||||
|
BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=1 \
|
||||||
|
bt_launch "$LOG" AUD.EGG 0x03
|
||||||
|
|
||||||
|
sleep 200
|
||||||
|
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "=== ACQUIRE FAILED (want 0) ==="
|
||||||
|
grep -c "ACQUIRE FAILED" "$LOG"
|
||||||
|
echo "=== census progression ==="
|
||||||
|
grep -oE "\[audio\] source census:.*" "$LOG" | head -3
|
||||||
|
grep -oE "\[audio\] source census:.*" "$LOG" | tail -3
|
||||||
Reference in New Issue
Block a user