From 1853a57c12ee16f8d1d2c41cca60851a4bd89de6 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Jul 2026 16:40:17 +0200 Subject: [PATCH] feat(diagnostics): complete residency pressure ledger Complete Slice D4 by adding aggregate lifecycle occupancy and traffic facts, validating physical source reports, and including decoded audio under the typed startup budget. Exercise every domain under forced pressure and retain the real cache/fence convergence gates. --- ...odern-runtime-slice-d-unified-residency.md | 16 +++- .../ContentEffectsAudioComposition.cs | 13 ++- .../Composition/WorldRenderComposition.cs | 31 ++++++- .../Residency/AssetResidencyTypes.cs | 59 ++++++++++++- .../DelegateResidencyDomainSource.cs | 1 + .../Residency/ResidencyBudgetOptions.cs | 6 +- .../Rendering/Residency/ResidencyManager.cs | 1 + .../Rendering/Wb/ObjectMeshManager.cs | 12 ++- src/AcDream.Core/Audio/DatSoundCache.cs | 37 +++++++- .../ContentEffectsAudioCompositionTests.cs | 10 ++- .../WorldRenderCompositionTests.cs | 3 +- ...WorldLifecycleAutomationControllerTests.cs | 27 ++++++ .../Residency/ResidencyManagerTests.cs | 84 +++++++++++++++++++ .../AcDream.App.Tests/RuntimeOptionsTests.cs | 4 + .../Audio/DatSoundCacheTests.cs | 8 ++ 15 files changed, 297 insertions(+), 15 deletions(-) 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]