diff --git a/docs/plans/2026-07-24-modern-runtime-slice-d-unified-residency.md b/docs/plans/2026-07-24-modern-runtime-slice-d-unified-residency.md index f0e73cf8..ab9eb10c 100644 --- a/docs/plans/2026-07-24-modern-runtime-slice-d-unified-residency.md +++ b/docs/plans/2026-07-24-modern-runtime-slice-d-unified-residency.md @@ -1,6 +1,6 @@ # Modern Runtime Slice D — Typed Asset Handles and Unified Residency -**Status:** active — D1 through D3 complete +**Status:** active — D1 through D4 complete **Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md` **Baseline:** Slice C closeout commit `a564c4b7` **Behavior contract:** no visual-quality or retail-behavior change @@ -173,6 +173,7 @@ contains byte/count ceilings with the current production values as defaults: - composite arrays: 128 MiB physical / 64 MiB unowned; - standalone particle arrays: 32 MiB / 256; - decoded animations: 64 MiB / 512; +- decoded audio: 32 MiB; - deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors. Environment overrides are diagnostic/startup inputs and reject zero, negative, @@ -248,6 +249,19 @@ current source, so no replacement cache was invented. drain fence release, and prove convergence. - Assert manager and physical-owner accounting return to zero at teardown. +**Complete 2026-07-24.** The lifecycle artifact now serializes validated +per-domain rows plus aggregate entry/owner, budget, allocator +capacity/usage/fragmentation, traffic, committed CPU, mapped-address-space, and +physical GPU totals. The final omitted bounded owner, decoded audio, now +receives its startup budget through `RuntimeOptions` and reports decoded bytes +and hit/miss/eviction traffic beside animations and the render caches. A +deterministic all-domain pressure matrix exceeds every reported ceiling and +proves zero-charge convergence. The focused physical-owner suites separately +exercise prepared-mesh/staging rejection, composite and standalone texture +eviction, fence-delayed release, global-arena range retirement, animation/audio +LRU pressure, and alpha-scratch convergence. Release build and the complete +8,111-test / 5-skip solution gate are green. + ### D5 — Connected gate - Run capped, uncapped, and dense connected routes against Slice C. diff --git a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs index 4a2c00ce..916454b0 100644 --- a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs +++ b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs @@ -113,7 +113,9 @@ internal interface IContentEffectsAudioCompositionFactory EntityEffectPoseRegistry poses); TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades); AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router); - DatSoundCache CreateSoundCache(IDatReaderWriter dats); + DatSoundCache CreateSoundCache( + IDatReaderWriter dats, + long maximumDecodedBytes); OpenAlAudioEngine CreateAudioEngine(); DictionaryEntitySoundTable CreateEntitySoundTables(); AudioHookSink CreateAudioSink( @@ -204,7 +206,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory AnimationHookRouter router) => new(router); - public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats); + public DatSoundCache CreateSoundCache( + IDatReaderWriter dats, + long maximumDecodedBytes) => + new(dats, maximumDecodedBytes); public OpenAlAudioEngine CreateAudioEngine() => new(); @@ -426,7 +431,9 @@ internal sealed class ContentEffectsAudioCompositionPhase : engineLease; try { - DatSoundCache cache = _factory.CreateSoundCache(dats); + DatSoundCache cache = _factory.CreateSoundCache( + dats, + _dependencies.ResidencyBudgets.AudioBytes); Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated); engineLease = audioScope.Acquire( "OpenAL audio engine", diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs index 7812e032..a0eb907b 100644 --- a/src/AcDream.App/Composition/WorldRenderComposition.cs +++ b/src/AcDream.App/Composition/WorldRenderComposition.cs @@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency; using AcDream.App.World; using AcDream.Content; using AcDream.Content.Vfx; +using AcDream.Core.Audio; using AcDream.Core.Physics; using AcDream.Core.Terrain; using AcDream.UI.Abstractions.Settings; @@ -119,7 +120,8 @@ internal interface IWorldRenderCompositionFactory WbMeshAdapter meshes, TextureCache textures, IPreparedAssetSource preparedAssets, - IAnimationLoader animations); + IAnimationLoader animations, + DatSoundCache? audio); SamplerCache CreateSamplerCache(GL gl); void Release(IDisposable resource); } @@ -299,7 +301,8 @@ internal sealed class RetailWorldRenderCompositionFactory WbMeshAdapter meshes, TextureCache textures, IPreparedAssetSource preparedAssets, - IAnimationLoader animations) + IAnimationLoader animations, + DatSoundCache? audio) { meshes.RegisterResidencySources(manager); textures.RegisterResidencySources(manager); @@ -328,9 +331,30 @@ internal sealed class RetailWorldRenderCompositionFactory DecodedBytes: diagnostics.EstimatedBytes), BudgetBytes: diagnostics.BudgetBytes, + Hits: diagnostics.Stats.Hits, + Misses: diagnostics.Stats.Misses, Evictions: diagnostics.Stats.Evictions); })); } + if (audio is not null) + { + manager.RegisterDomainSource(new DelegateResidencyDomainSource( + ResidencyDomain.Audio, + () => + { + DatSoundCacheDiagnostics diagnostics = audio.Diagnostics; + return new ResidencyDomainSnapshot( + ResidencyDomain.Audio, + EntryCount: diagnostics.CachedWaveCount, + OwnerCount: 0, + Charges: new ResidencyCharges( + DecodedBytes: diagnostics.ResidentWaveBytes), + BudgetBytes: diagnostics.BudgetBytes, + Hits: diagnostics.Hits, + Misses: diagnostics.Misses, + Evictions: diagnostics.Evictions); + })); + } } public SamplerCache CreateSamplerCache(GL gl) => new(gl); @@ -517,7 +541,8 @@ internal sealed class WorldRenderCompositionPhase meshAdapter, textureCache, content.PreparedAssets, - content.AnimationLoader); + content.AnimationLoader, + content.Audio?.SoundCache); scope.Complete(); _dependencies.Log( diff --git a/src/AcDream.App/Rendering/Residency/AssetResidencyTypes.cs b/src/AcDream.App/Rendering/Residency/AssetResidencyTypes.cs index 6db489f5..04edbeda 100644 --- a/src/AcDream.App/Rendering/Residency/AssetResidencyTypes.cs +++ b/src/AcDream.App/Rendering/Residency/AssetResidencyTypes.cs @@ -164,12 +164,41 @@ internal readonly record struct ResidencyDomainSnapshot( long CapacityBytes = 0, long UsedBytes = 0, long LargestFreeBytes = 0, + long Hits = 0, + long Misses = 0, long Evictions = 0) { public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes); public long FragmentedFreeBytes => Math.Max(0, FreeBytes - LargestFreeBytes); + + public void Validate() + { + if (!Enum.IsDefined(Domain)) + throw new ArgumentOutOfRangeException(nameof(Domain)); + ArgumentOutOfRangeException.ThrowIfNegative(EntryCount); + ArgumentOutOfRangeException.ThrowIfNegative(OwnerCount); + Charges.Validate(); + ArgumentOutOfRangeException.ThrowIfNegative(BudgetBytes); + ArgumentOutOfRangeException.ThrowIfNegative(CapacityBytes); + ArgumentOutOfRangeException.ThrowIfNegative(UsedBytes); + ArgumentOutOfRangeException.ThrowIfNegative(LargestFreeBytes); + ArgumentOutOfRangeException.ThrowIfNegative(Hits); + ArgumentOutOfRangeException.ThrowIfNegative(Misses); + ArgumentOutOfRangeException.ThrowIfNegative(Evictions); + if (UsedBytes > CapacityBytes && CapacityBytes != 0) + { + throw new InvalidOperationException( + $"{Domain} residency uses {UsedBytes} bytes from " + + $"{CapacityBytes} bytes of physical capacity."); + } + if (LargestFreeBytes > CapacityBytes) + { + throw new InvalidOperationException( + $"{Domain} largest free range exceeds physical capacity."); + } + } } internal readonly record struct ResidencySnapshot( @@ -177,7 +206,35 @@ internal readonly record struct ResidencySnapshot( ResidencyCharges TotalCharges) { public ResidencyDomainSnapshot Get(ResidencyDomain domain) => - Domains.FirstOrDefault(snapshot => snapshot.Domain == domain); + Domains?.FirstOrDefault(snapshot => snapshot.Domain == domain) + ?? default; + + public int TotalEntries => + Domains?.Sum(static snapshot => snapshot.EntryCount) ?? 0; + + public int TotalOwners => + Domains?.Sum(static snapshot => snapshot.OwnerCount) ?? 0; + + public long TotalBudgetBytes => + Domains?.Sum(static snapshot => snapshot.BudgetBytes) ?? 0; + + public long TotalCapacityBytes => + Domains?.Sum(static snapshot => snapshot.CapacityBytes) ?? 0; + + public long TotalUsedBytes => + Domains?.Sum(static snapshot => snapshot.UsedBytes) ?? 0; + + public long TotalFragmentedFreeBytes => + Domains?.Sum(static snapshot => snapshot.FragmentedFreeBytes) ?? 0; + + public long TotalHits => + Domains?.Sum(static snapshot => snapshot.Hits) ?? 0; + + public long TotalMisses => + Domains?.Sum(static snapshot => snapshot.Misses) ?? 0; + + public long TotalEvictions => + Domains?.Sum(static snapshot => snapshot.Evictions) ?? 0; } internal interface IResidencyDomainSource diff --git a/src/AcDream.App/Rendering/Residency/DelegateResidencyDomainSource.cs b/src/AcDream.App/Rendering/Residency/DelegateResidencyDomainSource.cs index 8e839e2f..8262542c 100644 --- a/src/AcDream.App/Rendering/Residency/DelegateResidencyDomainSource.cs +++ b/src/AcDream.App/Rendering/Residency/DelegateResidencyDomainSource.cs @@ -17,6 +17,7 @@ internal sealed class DelegateResidencyDomainSource( throw new InvalidOperationException( $"Residency source {Domain} returned {snapshot.Domain}."); } + snapshot.Validate(); return snapshot; } } diff --git a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs index 44a74fe6..121802d6 100644 --- a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs +++ b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs @@ -19,6 +19,7 @@ public sealed record ResidencyBudgetOptions( int StandaloneUnownedEntries, long AnimationBytes, int AnimationEntries, + long AudioBytes, long AlphaScratchBytes) { public const long MiB = 1024L * 1024L; @@ -36,6 +37,7 @@ public sealed record ResidencyBudgetOptions( StandaloneUnownedEntries: 256, AnimationBytes: 64 * MiB, AnimationEntries: 512, + AudioBytes: 32 * MiB, AlphaScratchBytes: 16 * MiB); internal static ResidencyBudgetOptions Parse( @@ -80,6 +82,9 @@ public sealed record ResidencyBudgetOptions( AnimationEntries: ParseCount( env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"), defaults.AnimationEntries), + AudioBytes: ParseMiB( + env("ACDREAM_RESIDENCY_AUDIO_MIB"), + defaults.AudioBytes), AlphaScratchBytes: ParseMiB( env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"), defaults.AlphaScratchBytes)); @@ -110,4 +115,3 @@ public sealed record ResidencyBudgetOptions( ? count : fallback; } - diff --git a/src/AcDream.App/Rendering/Residency/ResidencyManager.cs b/src/AcDream.App/Rendering/Residency/ResidencyManager.cs index 08c43a63..f870b67f 100644 --- a/src/AcDream.App/Rendering/Residency/ResidencyManager.cs +++ b/src/AcDream.App/Rendering/Residency/ResidencyManager.cs @@ -638,6 +638,7 @@ internal sealed class ResidencyManager LogicalBytes: current.LogicalBytes, CpuPreparedBytes: current.CpuPreparedBytes, DecodedBytes: current.DecodedBytes, + ScratchBytes: current.ScratchBytes, PinnedBytes: current.PinnedBytes, RetiringBytes: checked( current.GpuRequestedBytes diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index c639becd..45cd3b64 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -1316,14 +1316,20 @@ namespace AcDream.App.Rendering.Wb return bytes; } - internal ResidencyDomainSnapshot CapturePreparedMeshResidency() => - new( + internal ResidencyDomainSnapshot CapturePreparedMeshResidency() + { + CacheStats stats = _cpuMeshCache.Stats; + return new ResidencyDomainSnapshot( ResidencyDomain.PreparedMeshCpu, EntryCount: _cpuMeshCache.Count, OwnerCount: 0, Charges: new ResidencyCharges( CpuPreparedBytes: _cpuMeshCache.ResidentBytes), - BudgetBytes: _cpuMeshCache.ByteCapacity); + BudgetBytes: _cpuMeshCache.ByteCapacity, + Hits: stats.Hits, + Misses: stats.Misses, + Evictions: stats.Evictions); + } internal ResidencyDomainSnapshot CaptureStagingResidency() => new( diff --git a/src/AcDream.Core/Audio/DatSoundCache.cs b/src/AcDream.Core/Audio/DatSoundCache.cs index 188720a9..a04190e4 100644 --- a/src/AcDream.Core/Audio/DatSoundCache.cs +++ b/src/AcDream.Core/Audio/DatSoundCache.cs @@ -8,6 +8,14 @@ using AcDream.Core.Content; namespace AcDream.Core.Audio; +public readonly record struct DatSoundCacheDiagnostics( + int CachedWaveCount, + long ResidentWaveBytes, + long BudgetBytes, + long Hits, + long Misses, + long Evictions); + /// /// DatCollection-backed cache of decoded waves + SoundTable lookups. /// @@ -74,6 +82,9 @@ public sealed class DatSoundCache private readonly LinkedList _waveLru = new(); private readonly long _maxWaveBytes; private long _residentWaveBytes; + private long _hits; + private long _misses; + private long _evictions; private sealed record WaveEntry(uint WaveId, WaveData Wave, long Bytes); @@ -86,7 +97,7 @@ public sealed class DatSoundCache /// Test seam for pinning a smaller byte budget so eviction can be /// exercised deterministically without allocating tens of megabytes. /// - internal DatSoundCache(IDatObjectSource dats, long maxWaveBytes) + public DatSoundCache(IDatObjectSource dats, long maxWaveBytes) { ArgumentNullException.ThrowIfNull(dats); ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1); @@ -105,13 +116,19 @@ public sealed class DatSoundCache { if (_waveEntries.TryGetValue(waveId, out var node)) { + Interlocked.Increment(ref _hits); Touch(node); return node.Value.Wave; } } if (_negativeWaveIds.ContainsKey(waveId)) + { + Interlocked.Increment(ref _hits); return null; + } + + Interlocked.Increment(ref _misses); Lazy lazy = _inflight.GetOrAdd( waveId, @@ -171,6 +188,23 @@ public sealed class DatSoundCache /// public int CachedSoundTableCount => _tables.Count; + public DatSoundCacheDiagnostics Diagnostics + { + get + { + lock (_gate) + { + return new DatSoundCacheDiagnostics( + _waveEntries.Count, + _residentWaveBytes, + _maxWaveBytes, + Interlocked.Read(ref _hits), + Interlocked.Read(ref _misses), + Interlocked.Read(ref _evictions)); + } + } + } + private WaveData? DecodeUncached(uint waveId) { var wave = _dats.Get(waveId); @@ -225,5 +259,6 @@ public sealed class DatSoundCache _waveLru.Remove(node); _waveEntries.Remove(node.Value.WaveId); _residentWaveBytes -= node.Value.Bytes; + Interlocked.Increment(ref _evictions); } } diff --git a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs index 7fe2adf6..cf26d7f7 100644 --- a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs @@ -433,7 +433,15 @@ public sealed class ContentEffectsAudioCompositionTests EntityEffectPoseRegistry poses) => new(lighting, poses); public TranslucencyHookSink CreateTranslucencySink( TranslucencyFadeManager fades) => new(fades); - public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats); + public DatSoundCache CreateSoundCache( + IDatReaderWriter dats, + long maximumDecodedBytes) + { + Assert.Equal( + ResidencyBudgetOptions.Default.AudioBytes, + maximumDecodedBytes); + return new DatSoundCache(dats, maximumDecodedBytes); + } public OpenAlAudioEngine CreateAudioEngine() { diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index 5f7cf6a1..4fbc73a5 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -308,7 +308,8 @@ public sealed class WorldRenderCompositionTests WbMeshAdapter meshes, TextureCache textures, IPreparedAssetSource preparedAssets, - IAnimationLoader animations) + IAnimationLoader animations, + AcDream.Core.Audio.DatSoundCache? audio) { RegisteredResidency = manager; } diff --git a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs index bb55bf1d..ab600bba 100644 --- a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs +++ b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using AcDream.App.Diagnostics; using AcDream.App.Rendering; +using AcDream.App.Rendering.Residency; using AcDream.App.Streaming; using AcDream.App.UI.Testing; using SixLabors.ImageSharp; @@ -114,6 +115,20 @@ public sealed class WorldLifecycleAutomationControllerTests LoadedLandblocks = 1, WorldEntities = 42, TrackedGpuBytes = 1234, + Residency = new ResidencySnapshot( + [ + new ResidencyDomainSnapshot( + ResidencyDomain.Animations, + EntryCount: 2, + OwnerCount: 0, + Charges: new ResidencyCharges( + DecodedBytes: 1024), + BudgetBytes: 4096, + Hits: 8, + Misses: 3, + Evictions: 1), + ], + new ResidencyCharges(DecodedBytes: 1024)), }; var screenshots = new FrameScreenshotController( (_, _) => [0, 0, 0, 255], @@ -151,6 +166,18 @@ public sealed class WorldLifecycleAutomationControllerTests Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation") .GetProperty("screenshotCaptured").GetBoolean()); Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32()); + JsonElement residency = json.RootElement + .GetProperty("resources") + .GetProperty("residency"); + Assert.Equal( + 4096, + residency.GetProperty("totalBudgetBytes").GetInt64()); + Assert.Equal(8, residency.GetProperty("totalHits").GetInt64()); + Assert.Equal( + 1024, + residency.GetProperty("totalCharges") + .GetProperty("decodedBytes") + .GetInt64()); Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json"))); } finally diff --git a/tests/AcDream.App.Tests/Rendering/Residency/ResidencyManagerTests.cs b/tests/AcDream.App.Tests/Rendering/Residency/ResidencyManagerTests.cs index d6843a25..dea1cfc8 100644 --- a/tests/AcDream.App.Tests/Rendering/Residency/ResidencyManagerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Residency/ResidencyManagerTests.cs @@ -393,6 +393,90 @@ public sealed class ResidencyManagerTests 16, snapshot.Get(ResidencyDomain.StandaloneTextures) .FragmentedFreeBytes); + Assert.Equal(256, snapshot.TotalBudgetBytes); + Assert.Equal(128, snapshot.TotalCapacityBytes); + Assert.Equal(96, snapshot.TotalUsedBytes); + Assert.Equal(16, snapshot.TotalFragmentedFreeBytes); + Assert.Equal(3, snapshot.TotalEntries); + Assert.Equal(2, snapshot.TotalOwners); + } + + [Fact] + public void ForcedPressureAcrossEveryDomainConvergesToZero() + { + const long budget = 64; + var manager = new ResidencyManager(); + var current = new Dictionary< + ResidencyDomain, + ResidencyDomainSnapshot>(); + foreach (ResidencyDomain domain in Enum.GetValues()) + { + current[domain] = new ResidencyDomainSnapshot( + domain, + EntryCount: 2, + OwnerCount: 1, + Charges: new ResidencyCharges( + CpuPreparedBytes: budget * 2), + BudgetBytes: budget, + Hits: 3, + Misses: 2, + Evictions: 1); + ResidencyDomain capturedDomain = domain; + manager.RegisterDomainSource(new DelegateResidencyDomainSource( + capturedDomain, + () => current[capturedDomain])); + } + + ResidencySnapshot pressured = manager.CaptureSnapshot(); + + Assert.Equal( + Enum.GetValues().Length, + pressured.Domains.Count); + Assert.All( + pressured.Domains, + domain => Assert.True( + domain.Charges.CommittedCpuBytes > domain.BudgetBytes)); + Assert.Equal( + budget * 2 * pressured.Domains.Count, + pressured.TotalCharges.CommittedCpuBytes); + + foreach (ResidencyDomain domain in Enum.GetValues()) + { + current[domain] = new ResidencyDomainSnapshot( + domain, + EntryCount: 0, + OwnerCount: 0, + Charges: ResidencyCharges.Zero, + BudgetBytes: budget, + Hits: 3, + Misses: 2, + Evictions: 3); + } + + ResidencySnapshot drained = manager.CaptureSnapshot(); + + Assert.True(drained.TotalCharges.IsZero); + Assert.Equal(0, drained.TotalEntries); + Assert.Equal(0, drained.TotalOwners); + Assert.Equal(budget * drained.Domains.Count, drained.TotalBudgetBytes); + Assert.Equal(30, drained.TotalEvictions); + } + + [Fact] + public void DomainSourceRejectsImpossibleAllocatorFacts() + { + var source = new DelegateResidencyDomainSource( + ResidencyDomain.GlobalMeshArena, + () => new ResidencyDomainSnapshot( + ResidencyDomain.GlobalMeshArena, + EntryCount: 1, + OwnerCount: 0, + Charges: ResidencyCharges.Zero, + CapacityBytes: 64, + UsedBytes: 65)); + + Assert.Throws( + () => source.CaptureResidency()); } [Fact] diff --git a/tests/AcDream.App.Tests/RuntimeOptionsTests.cs b/tests/AcDream.App.Tests/RuntimeOptionsTests.cs index d91055da..0ea41521 100644 --- a/tests/AcDream.App.Tests/RuntimeOptionsTests.cs +++ b/tests/AcDream.App.Tests/RuntimeOptionsTests.cs @@ -31,6 +31,7 @@ public sealed class RuntimeOptionsTests ["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72", ["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48", ["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300", + ["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24", }; RuntimeOptions options = RuntimeOptions.Parse( @@ -47,6 +48,9 @@ public sealed class RuntimeOptionsTests 48 * ResidencyBudgetOptions.MiB, options.ResidencyBudgets.AnimationBytes); Assert.Equal(300, options.ResidencyBudgets.AnimationEntries); + Assert.Equal( + 24 * ResidencyBudgetOptions.MiB, + options.ResidencyBudgets.AudioBytes); } [Theory] diff --git a/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs b/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs index 7c7054db..803c49f4 100644 --- a/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs +++ b/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs @@ -110,6 +110,14 @@ public sealed class DatSoundCacheTests Assert.NotNull(b2); Assert.NotSame(b1, b2); // evicted entries decode fresh on next request Assert.Equal(2, dats.GetCallCount(2)); + + DatSoundCacheDiagnostics diagnostics = cache.Diagnostics; + Assert.Equal(2, diagnostics.CachedWaveCount); + Assert.Equal(300, diagnostics.ResidentWaveBytes); + Assert.Equal(300, diagnostics.BudgetBytes); + Assert.True(diagnostics.Hits >= 3); + Assert.True(diagnostics.Misses >= 4); + Assert.True(diagnostics.Evictions >= 2); } [Fact]