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:
+41
-9
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user