Sounds recycle their voices instead of churning through them

Recovering the soundbanks took voice demand per sound from about one zone to
about two and a half, and the audio path allocated an OpenAL source for every
sound event and destroyed it again on release. Sources are a hard
per-context resource - this driver grants 256 - so that churn doubled at
exactly the moment it got more expensive. Sources are now generated once and
recycled through a free list: measured, three sources generated across
twelve thousand acquisitions.

The BT tree reached the same conclusion the expensive way, from field logs
full of failed acquisitions: raising the source budget is not the fix,
because the ceiling also acts as a governor and more voices mixing is real
CPU during exactly the busiest moments. Recycling is the fix, and it costs
nothing.

Two older bugs were sitting underneath, both reproduced against the driver
rather than assumed:

Releasing a set leaked it. alDeleteSources is atomic - one bad name in the
array and nothing at all is deleted. ReleaseSourceSet handed it the whole
fixed-size array and then parked the slots at -1, so any partial set, and
any double release, leaked every source it held. Sources are now handed back
one at a time and slots park at 0, which is never a valid name.

A source set began life uninitialised. The constructor set only the count,
and the acquire path decided whether a slot was already filled by asking
OpenAL about uninitialised stack garbage. Garbage that happened to match a
live name meant two sounds silently sharing one source. Pooling would have
made that more likely, not less, since it keeps small names in circulation.

Recycled sources are scrubbed before parking - stopped, buffer detached,
looping, gain, pitch, relative flag, position and velocity reset, and the
EFX filter and reverb send dropped. Without that last part a dry cockpit
sound inherits the wet send of whatever 3D source held the name before it.
Verified: a deliberately dirtied source comes back clean.

Builds clean. Runs with memory and handle count flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-05 23:33:45 -05:00
co-authored by Claude Opus 5
parent d361a0b8be
commit e133d4c993
5 changed files with 268 additions and 37 deletions
+41 -9
View File
@@ -473,13 +473,40 @@ an entire sample every cycle — is worth re-checking for RP now that zone count
have gone up. RP's 129 looping zones should be measured for loop-region coverage
before F13 lands.
**A churn risk this work introduces.** Voice demand per sound has gone from ~1.1
to ~2.6 zones. `RequestAudioChannels` calls `alGenSources` per sound event and
`ReleaseSourceSet` calls `alDeleteSources` on release, so the allocation churn
roughly doubles. BT411 hit exactly this and its measured conclusion was that
raising the source budget was *not* the fix — **pooling** the sources was, and it
was a net CPU win (8.67 ms → 7.79 ms per frame). RP412 has the same churn
pattern and no pooling. Worth doing before any complaint arrives, not after.
**The churn this work introduced — now fixed (2026-08-05).** Recovering the
zones took voice demand per sound from ~1.1 to ~2.6, roughly doubling the
allocation churn: `RequestAudioChannels` called `alGenSources` per sound event
and `ReleaseSourceSet` called `alDeleteSources` on release. BT411 hit exactly
this, and its measured conclusion was that raising the source budget was *not*
the fix — pooling was, and a net CPU win besides.
Sources are now generated once and recycled through a free list
(`RPAudioPoolAcquire` / `RPAudioPoolRelease`, `L4AUDRND.cpp`), capped at 240
against the driver's 256-mono grant. Steady-state play costs no allocation:
measured 3 sources generated across 12,000 acquisitions.
Two real bugs were sitting underneath it, both verified against this driver
rather than assumed:
- **The bulk delete was atomic and leaked whole sets.**
`alDeleteSources(3, {valid, valid, 0})` returns an error and deletes
*nothing* — both live sources survive. The old `ReleaseSourceSet` passed the
whole fixed-size array and then parked slots at `-1` (`0xFFFFFFFF`), so any
partial set, or any double release, leaked its entire allocation. Release is
now per-source, and slots park at 0, which is never a valid AL name.
- **`SourceSet.sources[]` was never initialized.** The constructor set only
`count`, and `RequestAudioChannels` decided whether a slot was already filled
by asking `alIsSource` about uninitialized stack garbage. A value that
happened to match a live name meant two sources silently sharing one — a
latent hazard that pooling would have made *more* likely, since recycling
keeps small integer names in circulation.
Recycled sources are scrubbed before being parked: stopped, buffer detached,
looping/gain/pitch/relative/position/velocity reset, **and the EFX direct filter
and reverb send cleared** — without that last part a dry cockpit sound could
inherit the wet send of the 3D source that held the name before it. Verified: a
source deliberately dirtied then released comes back with looping=0, gain=1.0,
pitch=1.0, relative=0.
## 10. A recovery path, in order
@@ -487,8 +514,7 @@ pattern and no pooling. Worth doing before any complaint arrives, not after.
F11, F12 and P1 all landed; `L4AUDEFX.cpp/.h` ported and added to
`Munga_L4.vcxproj`. Builds clean on VS2022 `Release|Win32`; smoke-tested
against vRIO on COM1 with `RP412STEAM=0` — reaches gameplay and holds a
steady frame loop. **Not yet listened to on the pod**, which is the real
acceptance test: F4 in particular changes the level of everything.
steady frame loop.
2. ~~**Wire RP's banks in.**~~ **Done (2026-08-05).** Both banks are now in
`assets/RP411/AUDIO/`, hash-identical to the 1996 originals.
`tools/rp_sf2extract.py` extracts all 395 zones with tuning, layer
@@ -497,6 +523,12 @@ pattern and no pooling. Worth doing before any complaint arrives, not after.
preset slots that disappeared were all empty placeholders); every extreme
baked rate (1228 Hz 88200 Hz) accepted by libsndfile → `alBufferData`
`alSourcePlay` on the real runtime path. **Still open here: F13.**
**Confirmed by ear (2026-08-05): markedly more bass.** That is the expected
signature of the tuning fix — the deepest layers were the worst offenders, a
collision sub-thud playing at 44100 Hz where the bank says 1228 — compounded
by the 176 recovered zones, which are disproportionately the low rumble
layers sitting under collisions and explosions.
3. **Recover the `.SCP` sources** from `sda4/RPLIVE/AUDIO/` into the asset
pipeline, so authored audio becomes editable again rather than frozen in
`RPL4.RES`.