Skip to content

fix(client): bound actor sound channels with a recycling pool (#489)#540

Merged
CoreyRDean merged 2 commits into
developfrom
fix/actor-sound-channel-pool
Jun 9, 2026
Merged

fix(client): bound actor sound channels with a recycling pool (#489)#540
CoreyRDean merged 2 commits into
developfrom
fix/actor-sound-channel-pool

Conversation

@CoreyRDean

Copy link
Copy Markdown
Collaborator

Closes #489.

Non-technical summary

After a sustained play session with several audible actors moving and fighting, the game client (bin/Client.exe) would hard-crash — no error dialog, empty log. This PR fixes that. Actor sound effects (footsteps, combat/death cries, and server-triggered positional sounds) now reuse a small fixed set of audio channels instead of allocating a fresh one every time and never giving it back. The client can now play actor audio indefinitely without exhausting the sound backend.

Technical summary

Per-step actor sounds were fire-and-forget: three sites called EmitSound(...) and discarded the channel handle it returns, so the OpenAL backend's sources accumulated until allocation failed and the process crashed (issue #489). The managed SoundZone path already avoided this by keeping its handle and ChannelPlaying()-checking it; the per-step actor paths did not.

  • New EmitActorSound(Snd, EN) in Actors3D.bb — routes every actor sound through a fixed-size ring (ActorSoundPoolSize = 24) of channel handles. Before reusing a slot it StopChannel()s whatever is still playing there (LRU reclaim), so the live actor-sound channel count is capped at the pool size regardless of session length or actor count. Mirrors the existing SoundZone reuse pattern.
  • Three call sites routed through the pool: footstep EmitSound (Client.bb), the attack/hit/death EmitSound in PlayActorSound (Actors3D.bb), and the P_Sound packet handler (ClientNet.bb) — the last is a server-triggerable sibling of the same leak, included per the repo's sibling-asymmetry doctrine.
  • A Snd = 0 guard skips emitting silence for a missing/unregistered sound id — a small improvement over the old raw EmitSound(0, ...).
  • The ClientNet.bb comment shifted downstream handler line numbers, so the auto-generated docs/protocol/index.md is regenerated (line-number shifts only — no packet semantics changed).

No breaking changes. The helper is additive; the call-site swaps are behavior-preserving except for the channel cap and the Snd=0 skip.

Acceptance criteria + test results

New non-Strict regression test src/Tests/Modules/ActorSoundPoolTest.bb (passes under test.bat ActorSoundPool) replicates the pool bookkeeping verbatim with the audio primitives stubbed, and pins:

  • Snd=0 short-circuits — returns 0, no channel consumed, cursor unchanged, nothing stopped.
  • First fill reclaims nothing — the initial poolSize emits hit empty slots; cursor wraps to 0.
  • Cursor stays in [0, poolSize) across many emits (Dim index always in bounds).
  • LRU reclaim on wrap — emit #(poolSize+1) stops the oldest channel, #(poolSize+2) the next-oldest.
  • Live channel count capped — after K emits, exactly K - poolSize channels were stopped, so at most poolSize are ever live.
  • Conditional reclaim — a slot whose sound already finished (ChannelPlaying False) is overwritten without a needless StopChannel.
  • Full compile.bat clean across all five engine targets + all seven tools (Actors3D.bb is shared by Client and GUE).
  • Both generator staleness gates (gen_packet_index.sh --check, gen_bvm_reference.sh --check) pass.

Trade-offs, deferred follow-ups, remaining gap

  • Runtime confirmation of the crash fix is walkthrough-level, not end-to-end. Agent-launched Graphics3D is blocked in this environment, so the no-crash outcome is established by the issue's own bisection + the unit-tested channel cap + static reasoning, not a live multi-actor session. The fix is the approach the issue itself recommends.
  • Re-enabling the per-step actor sounds in data/ (currently disabled as the data-side workaround) is deferred to the default-project owner — it's a content change, not an engine change, and is safe to flip once this lands.
  • Pool size (24) is a conservative constant; if a future zone needs more simultaneous distinct actor sounds it can be raised, with the same cap guarantee.

CoreyRDean and others added 2 commits June 9, 2026 13:07
Per-step actor sounds were fire-and-forget: the footstep EmitSound in the
client loop, the attack/hit/death EmitSound in PlayActorSound, and the
P_Sound packet handler all discarded the channel handle EmitSound returns
and never reclaimed it. With several actors moving and fighting, the audio
backend's sources accumulate until allocation fails and the client hard-
crashes (no RuntimeError, empty Client Log) -- issue #489. The managed
SoundZone path already avoided this by keeping its handle and
ChannelPlaying()-checking it; the per-step actor paths did not.

Add EmitActorSound() in Actors3D.bb: a fixed-size ring of channel handles
that StopChannel()s the slot it is about to reuse (LRU reclaim), capping the
live actor-sound channel count at ActorSoundPoolSize (24) regardless of
session length or actor count. Route the three unmanaged actor-positioned
EmitSound sites through it. The Snd=0 guard also skips emitting silence on a
missing/unregistered sound id, a small improvement over the old raw call.

The ClientNet.bb comment shifted downstream handler line numbers, so the
auto-generated packet index is regenerated (line-number shifts only, no
packet semantics changed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replicate EmitActorSound's bookkeeping verbatim (non-Strict, audio
primitives stubbed, pool size reduced to 4 for legible assertions) and
assert: Snd=0 short-circuits, the first fill reclaims nothing, the cursor
stays in [0,size), the oldest slot is reclaimed LRU on wrap, the live
channel count is capped at the pool size, and a finished slot isn't
needlessly re-stopped. Pins the issue #489 fix against regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CoreyRDean
CoreyRDean requested a review from a team as a code owner June 9, 2026 18:08
@CoreyRDean
CoreyRDean merged commit e37e282 into develop Jun 9, 2026
1 check passed
@CoreyRDean
CoreyRDean deleted the fix/actor-sound-channel-pool branch June 9, 2026 18:13
@rockinraymond

Copy link
Copy Markdown
Contributor

Dang this one is huge nice work!

CoreyRDean added a commit that referenced this pull request Jun 9, 2026
…sounds docstring

Keep the tool header in sync with the extended PLAN (reviewer follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rockinraymond pushed a commit to rockinraymond/rcce2-mythic that referenced this pull request Jun 9, 2026
…ase project

The default project shipped silent of footsteps and creature attack/hit/death
cries: every actor template's MSpeechIDs/FSpeechIDs Speech arrays were cleared
as a data-side workaround for RydeTec#489 (fire-and-forget EmitSound exhausted the
client's audio channels and hard-crashed it after a sustained session). That
engine bug is fixed -- actor sounds now route through the bounded recycling
channel pool EmitActorSound (RydeTec#540), which caps the live channel count -- so the
PR that landed the fix explicitly deferred re-enabling the data to the
default-project owner. This is that follow-up.

Wire the Speech arrays via the existing surgical tool (tools/projectgen/
set_actor_sounds.py), extended to also cover the Orc/Warlord mini-boss (Grukk),
which is a clone of the Orc/Raider on the same Troll mesh and so reuses the
troll vocalizations -- otherwise the boss would stay mute. Final wiring:
  - Human/Fighter (player): FootstepDry=0, FootstepWet=1
  - Rat/Critter:            Attack1=16, Hit1=17, Death=18
  - Orc/Raider, Orc/Warlord: Attack1=19, Hit1=20, Death=21, FootstepDry=1

This lights up actor audio in BOTH clients at once -- the Rust client already
reads these same arrays for combat voice (RydeTec#484/RydeTec#495/RydeTec#504) and was silent too.

Verification (data-level; live Graphics3D launch is blocked in this env):
field-level diff vs HEAD shows exactly 8 speech-array changes and 0 non-speech
changes; codec re-encode is byte-stable (2672 -> 2672 bytes); every wired sound
id resolves in Sounds.dat (the tool asserts this) and audit_media_refs is clean.
The crash that motivated disabling these is unit-tested fixed by RydeTec#540's
ActorSoundPoolTest (channel count provably capped); residual risk is acoustic
and fully reversible by re-running the tool with cleared arrays.

Update docs/sample-project-guide.md: the per-actor-sound note no longer claims
the bug is unfixed -- it now describes the wired-on state and the pooled path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client audio: unmanaged per-step EmitSound (actor footsteps/combat) exhausts OpenAL sources → hard crash after sustained play

2 participants