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]