acdream/tests/AcDream.App.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
..
Audio fix(audio): bound decoded-wave and AL-buffer caches (2026-07-24 audit review) 2026-07-24 11:49:57 +02:00
Combat refactor(app): complete session startup composition 2026-07-22 18:49:31 +02:00
Composition test(app): add canonical connected soak snapshots 2026-07-22 20:01:06 +02:00
Diagnostics test(app): make canonical soak comparisons workload-aware 2026-07-22 20:13:29 +02:00
Input feat(input): assess world objects on right click 2026-07-24 05:42:28 +02:00
Interaction feat(input): assess world objects on right click 2026-07-24 05:42:28 +02:00
Net refactor(net): own live session composition 2026-07-22 10:36:06 +02:00
Physics fix(runtime): restore interaction completion ownership 2026-07-23 05:51:51 +02:00
Plugins feat(D.2b): IUiRegistry plugin UI surface + buffered drain into UiHost 2026-06-14 17:46:37 +02:00
Rendering perf(render): gate EnvCellRenderer visibility-snapshot rebuilds on real input changes (2026-07-24 audit review) 2026-07-24 11:49:40 +02:00
Settings refactor(app): compose session and player startup 2026-07-22 18:28:32 +02:00
Spells fix(ui): restore modern spell formula presentation 2026-07-24 07:08:47 +02:00
Streaming refactor(app): extract focused window lifetime 2026-07-22 19:43:52 +02:00
Studio feat(studio): #156 #157 mockup desktop and font resolver 2026-06-26 12:56:05 +02:00
UI fix(ui): keep spell components in authored foreground 2026-07-24 08:08:29 +02:00
World fix(runtime): restore interaction completion ownership 2026-07-23 05:51:51 +02:00
AcDream.App.Tests.csproj fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
BoundedTestDatCollection.cs fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
FramePhaseTestDoubles.cs refactor(runtime): extract the live object frame 2026-07-22 00:42:26 +02:00
FrameProfilerReportTests.cs fix #225: stabilize render pacing and frame CPU 2026-07-19 06:29:30 +02:00
FrameStatsBufferTests.cs fix(pipeline): MP0 - profiler toggle-path hygiene + Max() seed (review follow-up) 2026-07-05 19:26:28 +02:00
RuntimeDatAccessArchitectureTests.cs fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
RuntimeDatCollectionFactoryTests.cs fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
RuntimeOptionsRetailUiTests.cs test(runtime): add deterministic world gate artifacts 2026-07-20 22:37:16 +02:00
RuntimeOptionsTests.cs refactor(render): extract world frame preparation 2026-07-22 05:54:00 +02:00