acdream/tests/AcDream.Core.Tests
Erik 1e9031e7b7 fix(audio): bound decoded-wave and AL-buffer caches (2026-07-24 audit review)
Two audit-verified caches grew monotonically for process lifetime, which is
fatal for the 30-bot long-uptime headless-fleet goal:

- DatSoundCache._waves (Core) memoized every decoded PCM WaveData forever
  in a bare ConcurrentDictionary — no LRU, no byte budget.
- OpenAlAudioEngine._bufferByWaveId (App) retained a native OpenAL buffer
  copy of the same PCM per wave id until engine disposal — a second,
  independent unbounded cache.

DatSoundCache now bounds payload residency with a 32 MiB byte-budget LRU
(decoded PCM waves run ~100-500 KB each, so this holds a comfortable
working set). Missing/unsupported-format waves are memoized separately in
an unbounded-but-cheap negative-result set (bounded by the finite Wave dat
id space) so they can never compete with or get evicted alongside payload
entries. Concurrent first-touch decodes of the same wave id are deduped
via a shared Lazy<T> so racing callers don't pay for WaveDecoder.Decode
twice. AcDream.Core cannot reference AcDream.Content (code-structure rule
2), so the LRU is a local reimplementation mirroring
BoundedDatObjectCache/DecodedTextureCache's shape rather than a shared
dependency.

OpenAlAudioEngine._bufferByWaveId now bounds native buffer residency with
a 48 MiB byte-budget LRU (AlBufferBudgetTracker), evicting least-recently-
used buffers once oversized. alDeleteBuffers fails on a buffer still
attached to a source, so eviction queries live AL per-source state
(GetSourceInteger.Buffer) rather than tracking a second, easily-stale
copy — several call sites (Play3DWave, PlayUiWave) set a source's buffer
directly. The buffer EnsureBuffer just created is explicitly protected
from its own eviction pass, since the caller hasn't attached it to a
source yet at that point. Evicted waves simply replay through
DatSoundCache -> EnsureBuffer on next use, identical to a first play.

Verified before implementing: AudioHookSink is the only GetWave caller
(single per-frame render-thread path per AnimationHookRouter's own
threading doc), and PcmBytes is read only at DatSoundCache.Admit (byte
accounting) and EnsureBuffer's first-upload branch — confirmed dead after
AL upload on the steady-state replay path, so bounding either cache
independently is correctness-safe; a cold replay after both evict simply
falls back to a full re-decode + re-upload, identical to a first play.

AlBufferBudgetTracker's eviction/budget decision is extracted as pure
logic (no AL dependency) specifically so it's unit-testable: the existing
OpenAlResourceLifetimeTests fake exposes a null AL, which short-circuits
every native buffer call before it runs, so the engine's actual AL wiring
isn't testable headless.

Tests: 9 new DatSoundCacheTests (Core.Tests) covering eviction order, byte
accounting, negative-result memoization, oversize-single-entry handling,
and concurrent-access smoke tests; 10 new AlBufferBudgetTrackerTests
(App.Tests) covering the pure LRU/budget/protection logic. Full suite:
3214/2 skip (Core.Tests), 3471/3 skip (App.Tests) plus one pre-existing,
unrelated failure (LandblockBuildOriginTests.FarLoad_..., reproduces
identically with these changes stashed out — landblock streaming, not
audio).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 76c880d35bcf30b50e3f7b4cb8635dc9ab9e3ec7)
2026-07-24 11:49:57 +02:00
..
Allegiance feat(allegiance): Phase H.2 AllegianceRequests + AllegianceTree model 2026-04-18 17:17:45 +02:00
Audio fix(audio): bound decoded-wave and AL-buffer caches (2026-07-24 audit review) 2026-07-24 11:49:57 +02:00
Chat refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Combat refactor(input): own gameplay action routing 2026-07-22 12:43:05 +02:00
Conformance refactor(pipeline): MP1a cleanup - narrow public surface, csproj parity, move residue 2026-07-05 20:52:01 +02:00
Fixtures fix(#185): landblock collision part-id uint32 overflow dropped stair steps 2026-07-08 10:07:50 +02:00
Input refactor(input): own pointer and callback lifetime 2026-07-22 11:59:33 +02:00
Items fix(ui): preserve retail item titles and spacing 2026-07-24 05:21:01 +02:00
Lighting feat(vfx): bind effects to live animated poses 2026-07-14 10:56:01 +02:00
Meshing feat(vfx): bind effects to live animated poses 2026-07-14 10:56:01 +02:00
Physics perf(physics): pool collision cell snapshots 2026-07-23 06:12:34 +02:00
Player refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Plugins fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
Rendering fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
Selection refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Social refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Spells feat(items): port retail shared cooldown overlays 2026-07-23 10:41:16 +02:00
Streaming fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
Terrain fix #108-residual (root cause): terrain drew DOUBLE-SIDED - port retail landPolysDraw eye-side gate as terrain backface cull 2026-06-12 22:05:31 +02:00
Textures fix(textures): DecodeSolidColor null-safe against null ColorValue 2026-06-14 14:36:07 +02:00
Ui feat(chat): port retail client command families 2026-07-13 14:50:15 +02:00
Vfx fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
World refactor(streaming): extract landblock physics publisher 2026-07-21 21:13:22 +02:00
AcDream.Core.Tests.csproj test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation 2026-06-02 15:20:24 +02:00
SmokeTest.cs chore: phase 1 — add Core, Abstractions, App, Tests projects 2026-04-10 09:22:33 +02:00
xunit.runner.json test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation 2026-06-02 15:20:24 +02:00