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.
This commit is contained in:
parent
f2644d42c2
commit
1853a57c12
15 changed files with 297 additions and 15 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Modern Runtime Slice D — Typed Asset Handles and Unified Residency
|
# 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`
|
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||||
**Baseline:** Slice C closeout commit `a564c4b7`
|
**Baseline:** Slice C closeout commit `a564c4b7`
|
||||||
**Behavior contract:** no visual-quality or retail-behavior change
|
**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;
|
- composite arrays: 128 MiB physical / 64 MiB unowned;
|
||||||
- standalone particle arrays: 32 MiB / 256;
|
- standalone particle arrays: 32 MiB / 256;
|
||||||
- decoded animations: 64 MiB / 512;
|
- decoded animations: 64 MiB / 512;
|
||||||
|
- decoded audio: 32 MiB;
|
||||||
- deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors.
|
- deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors.
|
||||||
|
|
||||||
Environment overrides are diagnostic/startup inputs and reject zero, negative,
|
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.
|
drain fence release, and prove convergence.
|
||||||
- Assert manager and physical-owner accounting return to zero at teardown.
|
- 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
|
### D5 — Connected gate
|
||||||
|
|
||||||
- Run capped, uncapped, and dense connected routes against Slice C.
|
- Run capped, uncapped, and dense connected routes against Slice C.
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
||||||
EntityEffectPoseRegistry poses);
|
EntityEffectPoseRegistry poses);
|
||||||
TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades);
|
TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades);
|
||||||
AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router);
|
AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router);
|
||||||
DatSoundCache CreateSoundCache(IDatReaderWriter dats);
|
DatSoundCache CreateSoundCache(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
long maximumDecodedBytes);
|
||||||
OpenAlAudioEngine CreateAudioEngine();
|
OpenAlAudioEngine CreateAudioEngine();
|
||||||
DictionaryEntitySoundTable CreateEntitySoundTables();
|
DictionaryEntitySoundTable CreateEntitySoundTables();
|
||||||
AudioHookSink CreateAudioSink(
|
AudioHookSink CreateAudioSink(
|
||||||
|
|
@ -204,7 +206,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
||||||
AnimationHookRouter router) =>
|
AnimationHookRouter router) =>
|
||||||
new(router);
|
new(router);
|
||||||
|
|
||||||
public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats);
|
public DatSoundCache CreateSoundCache(
|
||||||
|
IDatReaderWriter dats,
|
||||||
|
long maximumDecodedBytes) =>
|
||||||
|
new(dats, maximumDecodedBytes);
|
||||||
|
|
||||||
public OpenAlAudioEngine CreateAudioEngine() => new();
|
public OpenAlAudioEngine CreateAudioEngine() => new();
|
||||||
|
|
||||||
|
|
@ -426,7 +431,9 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
||||||
engineLease;
|
engineLease;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DatSoundCache cache = _factory.CreateSoundCache(dats);
|
DatSoundCache cache = _factory.CreateSoundCache(
|
||||||
|
dats,
|
||||||
|
_dependencies.ResidencyBudgets.AudioBytes);
|
||||||
Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated);
|
Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated);
|
||||||
engineLease = audioScope.Acquire(
|
engineLease = audioScope.Acquire(
|
||||||
"OpenAL audio engine",
|
"OpenAL audio engine",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Content.Vfx;
|
using AcDream.Content.Vfx;
|
||||||
|
using AcDream.Core.Audio;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
using AcDream.UI.Abstractions.Settings;
|
using AcDream.UI.Abstractions.Settings;
|
||||||
|
|
@ -119,7 +120,8 @@ internal interface IWorldRenderCompositionFactory
|
||||||
WbMeshAdapter meshes,
|
WbMeshAdapter meshes,
|
||||||
TextureCache textures,
|
TextureCache textures,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IAnimationLoader animations);
|
IAnimationLoader animations,
|
||||||
|
DatSoundCache? audio);
|
||||||
SamplerCache CreateSamplerCache(GL gl);
|
SamplerCache CreateSamplerCache(GL gl);
|
||||||
void Release(IDisposable resource);
|
void Release(IDisposable resource);
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +301,8 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
WbMeshAdapter meshes,
|
WbMeshAdapter meshes,
|
||||||
TextureCache textures,
|
TextureCache textures,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IAnimationLoader animations)
|
IAnimationLoader animations,
|
||||||
|
DatSoundCache? audio)
|
||||||
{
|
{
|
||||||
meshes.RegisterResidencySources(manager);
|
meshes.RegisterResidencySources(manager);
|
||||||
textures.RegisterResidencySources(manager);
|
textures.RegisterResidencySources(manager);
|
||||||
|
|
@ -328,9 +331,30 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
DecodedBytes:
|
DecodedBytes:
|
||||||
diagnostics.EstimatedBytes),
|
diagnostics.EstimatedBytes),
|
||||||
BudgetBytes: diagnostics.BudgetBytes,
|
BudgetBytes: diagnostics.BudgetBytes,
|
||||||
|
Hits: diagnostics.Stats.Hits,
|
||||||
|
Misses: diagnostics.Stats.Misses,
|
||||||
Evictions: diagnostics.Stats.Evictions);
|
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);
|
public SamplerCache CreateSamplerCache(GL gl) => new(gl);
|
||||||
|
|
@ -517,7 +541,8 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
meshAdapter,
|
meshAdapter,
|
||||||
textureCache,
|
textureCache,
|
||||||
content.PreparedAssets,
|
content.PreparedAssets,
|
||||||
content.AnimationLoader);
|
content.AnimationLoader,
|
||||||
|
content.Audio?.SoundCache);
|
||||||
|
|
||||||
scope.Complete();
|
scope.Complete();
|
||||||
_dependencies.Log(
|
_dependencies.Log(
|
||||||
|
|
|
||||||
|
|
@ -164,12 +164,41 @@ internal readonly record struct ResidencyDomainSnapshot(
|
||||||
long CapacityBytes = 0,
|
long CapacityBytes = 0,
|
||||||
long UsedBytes = 0,
|
long UsedBytes = 0,
|
||||||
long LargestFreeBytes = 0,
|
long LargestFreeBytes = 0,
|
||||||
|
long Hits = 0,
|
||||||
|
long Misses = 0,
|
||||||
long Evictions = 0)
|
long Evictions = 0)
|
||||||
{
|
{
|
||||||
public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes);
|
public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes);
|
||||||
|
|
||||||
public long FragmentedFreeBytes =>
|
public long FragmentedFreeBytes =>
|
||||||
Math.Max(0, FreeBytes - LargestFreeBytes);
|
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(
|
internal readonly record struct ResidencySnapshot(
|
||||||
|
|
@ -177,7 +206,35 @@ internal readonly record struct ResidencySnapshot(
|
||||||
ResidencyCharges TotalCharges)
|
ResidencyCharges TotalCharges)
|
||||||
{
|
{
|
||||||
public ResidencyDomainSnapshot Get(ResidencyDomain domain) =>
|
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
|
internal interface IResidencyDomainSource
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ internal sealed class DelegateResidencyDomainSource(
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Residency source {Domain} returned {snapshot.Domain}.");
|
$"Residency source {Domain} returned {snapshot.Domain}.");
|
||||||
}
|
}
|
||||||
|
snapshot.Validate();
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ public sealed record ResidencyBudgetOptions(
|
||||||
int StandaloneUnownedEntries,
|
int StandaloneUnownedEntries,
|
||||||
long AnimationBytes,
|
long AnimationBytes,
|
||||||
int AnimationEntries,
|
int AnimationEntries,
|
||||||
|
long AudioBytes,
|
||||||
long AlphaScratchBytes)
|
long AlphaScratchBytes)
|
||||||
{
|
{
|
||||||
public const long MiB = 1024L * 1024L;
|
public const long MiB = 1024L * 1024L;
|
||||||
|
|
@ -36,6 +37,7 @@ public sealed record ResidencyBudgetOptions(
|
||||||
StandaloneUnownedEntries: 256,
|
StandaloneUnownedEntries: 256,
|
||||||
AnimationBytes: 64 * MiB,
|
AnimationBytes: 64 * MiB,
|
||||||
AnimationEntries: 512,
|
AnimationEntries: 512,
|
||||||
|
AudioBytes: 32 * MiB,
|
||||||
AlphaScratchBytes: 16 * MiB);
|
AlphaScratchBytes: 16 * MiB);
|
||||||
|
|
||||||
internal static ResidencyBudgetOptions Parse(
|
internal static ResidencyBudgetOptions Parse(
|
||||||
|
|
@ -80,6 +82,9 @@ public sealed record ResidencyBudgetOptions(
|
||||||
AnimationEntries: ParseCount(
|
AnimationEntries: ParseCount(
|
||||||
env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"),
|
env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"),
|
||||||
defaults.AnimationEntries),
|
defaults.AnimationEntries),
|
||||||
|
AudioBytes: ParseMiB(
|
||||||
|
env("ACDREAM_RESIDENCY_AUDIO_MIB"),
|
||||||
|
defaults.AudioBytes),
|
||||||
AlphaScratchBytes: ParseMiB(
|
AlphaScratchBytes: ParseMiB(
|
||||||
env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"),
|
env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"),
|
||||||
defaults.AlphaScratchBytes));
|
defaults.AlphaScratchBytes));
|
||||||
|
|
@ -110,4 +115,3 @@ public sealed record ResidencyBudgetOptions(
|
||||||
? count
|
? count
|
||||||
: fallback;
|
: fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -638,6 +638,7 @@ internal sealed class ResidencyManager
|
||||||
LogicalBytes: current.LogicalBytes,
|
LogicalBytes: current.LogicalBytes,
|
||||||
CpuPreparedBytes: current.CpuPreparedBytes,
|
CpuPreparedBytes: current.CpuPreparedBytes,
|
||||||
DecodedBytes: current.DecodedBytes,
|
DecodedBytes: current.DecodedBytes,
|
||||||
|
ScratchBytes: current.ScratchBytes,
|
||||||
PinnedBytes: current.PinnedBytes,
|
PinnedBytes: current.PinnedBytes,
|
||||||
RetiringBytes: checked(
|
RetiringBytes: checked(
|
||||||
current.GpuRequestedBytes
|
current.GpuRequestedBytes
|
||||||
|
|
|
||||||
|
|
@ -1316,14 +1316,20 @@ namespace AcDream.App.Rendering.Wb
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResidencyDomainSnapshot CapturePreparedMeshResidency() =>
|
internal ResidencyDomainSnapshot CapturePreparedMeshResidency()
|
||||||
new(
|
{
|
||||||
|
CacheStats stats = _cpuMeshCache.Stats;
|
||||||
|
return new ResidencyDomainSnapshot(
|
||||||
ResidencyDomain.PreparedMeshCpu,
|
ResidencyDomain.PreparedMeshCpu,
|
||||||
EntryCount: _cpuMeshCache.Count,
|
EntryCount: _cpuMeshCache.Count,
|
||||||
OwnerCount: 0,
|
OwnerCount: 0,
|
||||||
Charges: new ResidencyCharges(
|
Charges: new ResidencyCharges(
|
||||||
CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
|
CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
|
||||||
BudgetBytes: _cpuMeshCache.ByteCapacity);
|
BudgetBytes: _cpuMeshCache.ByteCapacity,
|
||||||
|
Hits: stats.Hits,
|
||||||
|
Misses: stats.Misses,
|
||||||
|
Evictions: stats.Evictions);
|
||||||
|
}
|
||||||
|
|
||||||
internal ResidencyDomainSnapshot CaptureStagingResidency() =>
|
internal ResidencyDomainSnapshot CaptureStagingResidency() =>
|
||||||
new(
|
new(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ using AcDream.Core.Content;
|
||||||
|
|
||||||
namespace AcDream.Core.Audio;
|
namespace AcDream.Core.Audio;
|
||||||
|
|
||||||
|
public readonly record struct DatSoundCacheDiagnostics(
|
||||||
|
int CachedWaveCount,
|
||||||
|
long ResidentWaveBytes,
|
||||||
|
long BudgetBytes,
|
||||||
|
long Hits,
|
||||||
|
long Misses,
|
||||||
|
long Evictions);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DatCollection-backed cache of decoded waves + SoundTable lookups.
|
/// DatCollection-backed cache of decoded waves + SoundTable lookups.
|
||||||
///
|
///
|
||||||
|
|
@ -74,6 +82,9 @@ public sealed class DatSoundCache
|
||||||
private readonly LinkedList<WaveEntry> _waveLru = new();
|
private readonly LinkedList<WaveEntry> _waveLru = new();
|
||||||
private readonly long _maxWaveBytes;
|
private readonly long _maxWaveBytes;
|
||||||
private long _residentWaveBytes;
|
private long _residentWaveBytes;
|
||||||
|
private long _hits;
|
||||||
|
private long _misses;
|
||||||
|
private long _evictions;
|
||||||
|
|
||||||
private sealed record WaveEntry(uint WaveId, WaveData Wave, long Bytes);
|
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
|
/// Test seam for pinning a smaller byte budget so eviction can be
|
||||||
/// exercised deterministically without allocating tens of megabytes.
|
/// exercised deterministically without allocating tens of megabytes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
|
public DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(dats);
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1);
|
ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1);
|
||||||
|
|
@ -105,13 +116,19 @@ public sealed class DatSoundCache
|
||||||
{
|
{
|
||||||
if (_waveEntries.TryGetValue(waveId, out var node))
|
if (_waveEntries.TryGetValue(waveId, out var node))
|
||||||
{
|
{
|
||||||
|
Interlocked.Increment(ref _hits);
|
||||||
Touch(node);
|
Touch(node);
|
||||||
return node.Value.Wave;
|
return node.Value.Wave;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_negativeWaveIds.ContainsKey(waveId))
|
if (_negativeWaveIds.ContainsKey(waveId))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _hits);
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _misses);
|
||||||
|
|
||||||
Lazy<WaveData?> lazy = _inflight.GetOrAdd(
|
Lazy<WaveData?> lazy = _inflight.GetOrAdd(
|
||||||
waveId,
|
waveId,
|
||||||
|
|
@ -171,6 +188,23 @@ public sealed class DatSoundCache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int CachedSoundTableCount => _tables.Count;
|
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)
|
private WaveData? DecodeUncached(uint waveId)
|
||||||
{
|
{
|
||||||
var wave = _dats.Get<Wave>(waveId);
|
var wave = _dats.Get<Wave>(waveId);
|
||||||
|
|
@ -225,5 +259,6 @@ public sealed class DatSoundCache
|
||||||
_waveLru.Remove(node);
|
_waveLru.Remove(node);
|
||||||
_waveEntries.Remove(node.Value.WaveId);
|
_waveEntries.Remove(node.Value.WaveId);
|
||||||
_residentWaveBytes -= node.Value.Bytes;
|
_residentWaveBytes -= node.Value.Bytes;
|
||||||
|
Interlocked.Increment(ref _evictions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,15 @@ public sealed class ContentEffectsAudioCompositionTests
|
||||||
EntityEffectPoseRegistry poses) => new(lighting, poses);
|
EntityEffectPoseRegistry poses) => new(lighting, poses);
|
||||||
public TranslucencyHookSink CreateTranslucencySink(
|
public TranslucencyHookSink CreateTranslucencySink(
|
||||||
TranslucencyFadeManager fades) => new(fades);
|
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()
|
public OpenAlAudioEngine CreateAudioEngine()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,8 @@ public sealed class WorldRenderCompositionTests
|
||||||
WbMeshAdapter meshes,
|
WbMeshAdapter meshes,
|
||||||
TextureCache textures,
|
TextureCache textures,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IAnimationLoader animations)
|
IAnimationLoader animations,
|
||||||
|
AcDream.Core.Audio.DatSoundCache? audio)
|
||||||
{
|
{
|
||||||
RegisteredResidency = manager;
|
RegisteredResidency = manager;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AcDream.App.Diagnostics;
|
using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
|
@ -114,6 +115,20 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
LoadedLandblocks = 1,
|
LoadedLandblocks = 1,
|
||||||
WorldEntities = 42,
|
WorldEntities = 42,
|
||||||
TrackedGpuBytes = 1234,
|
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(
|
var screenshots = new FrameScreenshotController(
|
||||||
(_, _) => [0, 0, 0, 255],
|
(_, _) => [0, 0, 0, 255],
|
||||||
|
|
@ -151,6 +166,18 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
||||||
.GetProperty("screenshotCaptured").GetBoolean());
|
.GetProperty("screenshotCaptured").GetBoolean());
|
||||||
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
|
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")));
|
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,90 @@ public sealed class ResidencyManagerTests
|
||||||
16,
|
16,
|
||||||
snapshot.Get(ResidencyDomain.StandaloneTextures)
|
snapshot.Get(ResidencyDomain.StandaloneTextures)
|
||||||
.FragmentedFreeBytes);
|
.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<ResidencyDomain>())
|
||||||
|
{
|
||||||
|
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<ResidencyDomain>().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<ResidencyDomain>())
|
||||||
|
{
|
||||||
|
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<InvalidOperationException>(
|
||||||
|
() => source.CaptureResidency());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ public sealed class RuntimeOptionsTests
|
||||||
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
|
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
|
||||||
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
|
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
|
||||||
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
|
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
|
||||||
|
["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24",
|
||||||
};
|
};
|
||||||
|
|
||||||
RuntimeOptions options = RuntimeOptions.Parse(
|
RuntimeOptions options = RuntimeOptions.Parse(
|
||||||
|
|
@ -47,6 +48,9 @@ public sealed class RuntimeOptionsTests
|
||||||
48 * ResidencyBudgetOptions.MiB,
|
48 * ResidencyBudgetOptions.MiB,
|
||||||
options.ResidencyBudgets.AnimationBytes);
|
options.ResidencyBudgets.AnimationBytes);
|
||||||
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
|
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
|
||||||
|
Assert.Equal(
|
||||||
|
24 * ResidencyBudgetOptions.MiB,
|
||||||
|
options.ResidencyBudgets.AudioBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,14 @@ public sealed class DatSoundCacheTests
|
||||||
Assert.NotNull(b2);
|
Assert.NotNull(b2);
|
||||||
Assert.NotSame(b1, b2); // evicted entries decode fresh on next request
|
Assert.NotSame(b1, b2); // evicted entries decode fresh on next request
|
||||||
Assert.Equal(2, dats.GetCallCount(2));
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue