perf(render): bound animation and alpha scratch residency

Complete Slice D3 by replacing the unbounded animation dictionary with a concurrent byte/count LRU and by putting the three retail alpha scratch owners behind one typed aggregate budget. Preserve immediate growth and draw order while reclaiming one-frame density spikes after sustained under-use. Close stale bounds-cache issue evidence without inventing a cache.
This commit is contained in:
Erik 2026-07-24 16:34:28 +02:00
parent 3e18fc2730
commit f2644d42c2
18 changed files with 857 additions and 34 deletions

View file

@ -4,6 +4,7 @@ using AcDream.App.Audio;
using AcDream.App.Composition;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Spells;
using AcDream.Content;
@ -253,6 +254,7 @@ public sealed class ContentEffectsAudioCompositionTests
Dependencies = new ContentEffectsAudioDependencies(
"test-dat",
"test.pak",
ResidencyBudgetOptions.Default,
new PhysicsDataCache(),
false,
new Spellbook(),
@ -386,7 +388,19 @@ public sealed class ContentEffectsAudioCompositionTests
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) => _magic;
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) { }
public int GetSpellCount(MagicCatalog catalog) => 0;
public IAnimationLoader CreateAnimationLoader(IDatReaderWriter dats) => _animations;
public IAnimationLoader CreateAnimationLoader(
IDatReaderWriter dats,
long maximumEstimatedBytes,
int maximumEntries)
{
Assert.Equal(
ResidencyBudgetOptions.Default.AnimationBytes,
maximumEstimatedBytes);
Assert.Equal(
ResidencyBudgetOptions.Default.AnimationEntries,
maximumEntries);
return _animations;
}
public LiveEntityCollisionBuilder CreateCollisionBuilder(
PhysicsDataCache physicsData,

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Residency;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Physics;
using AcDream.Core.Terrain;
using AcDream.UI.Abstractions.Settings;
using DatReaderWriter.DBObjs;
@ -306,7 +307,8 @@ public sealed class WorldRenderCompositionTests
ResidencyManager manager,
WbMeshAdapter meshes,
TextureCache textures,
IPreparedAssetSource preparedAssets)
IPreparedAssetSource preparedAssets,
IAnimationLoader animations)
{
RegisteredResidency = manager;
}

View file

@ -7,6 +7,20 @@ public sealed class ResidencyManagerTests
private sealed class MeshAsset;
private sealed class TextureAsset;
[Fact]
public void ScratchBytesAreCommittedCpuResidence()
{
var charges = new ResidencyCharges(
LogicalBytes: 1,
CpuPreparedBytes: 2,
DecodedBytes: 3,
ScratchBytes: 4,
StagingBytes: 5);
Assert.Equal(15, charges.CommittedCpuBytes);
charges.Validate();
}
[Fact]
public void HandlesAndOwnersReserveZeroAsInvalid()
{

View file

@ -0,0 +1,86 @@
using AcDream.App.Rendering.Residency;
namespace AcDream.App.Tests.Rendering.Residency;
public sealed class RetainedScratchCapacityPolicyTests
{
[Fact]
public void AlphaBudgetProfilePartitionsTheAggregateExactly()
{
AlphaScratchBudgetProfile profile =
AlphaScratchBudgetProfile.Create(16 * 1024 * 1024);
Assert.Equal(4 * 1024 * 1024, profile.QueueBytes);
Assert.Equal(8 * 1024 * 1024, profile.DispatcherBytes);
Assert.Equal(4 * 1024 * 1024, profile.ParticleBytes);
Assert.Equal(16 * 1024 * 1024, profile.TotalBytes);
}
[Fact]
public void SpikeRetainsWarmCapacityUntilDemandFallsThenConverges()
{
var policy = new RetainedScratchCapacityPolicy(
budgetBytes: 1024,
lowDemandSamplesBeforeShrink: 3);
Assert.Equal(
4096,
policy.ObserveAndSelectCapacity(
currentCapacity: 4096,
requiredCapacity: 4096,
bytesPerUnit: 4,
minimumCapacity: 64,
growthQuantum: 64));
Assert.Equal(4096, ObserveLowDemand(policy));
Assert.Equal(4096, ObserveLowDemand(policy));
int converged = ObserveLowDemand(policy);
Assert.InRange(converged, 64, 256);
Assert.True((long)converged * 4 <= 1024);
}
[Fact]
public void SustainedLegitimateDemandIsNeverTrimmedBelowRequirement()
{
var policy = new RetainedScratchCapacityPolicy(
budgetBytes: 1024,
lowDemandSamplesBeforeShrink: 2);
for (int i = 0; i < 10; i++)
{
Assert.Equal(
4096,
policy.ObserveAndSelectCapacity(
currentCapacity: 4096,
requiredCapacity: 4096,
bytesPerUnit: 4,
minimumCapacity: 64,
growthQuantum: 64));
}
}
[Fact]
public void RequiredGrowthIsImmediateAndQuantumRounded()
{
var policy = new RetainedScratchCapacityPolicy(1024);
Assert.Equal(
320,
policy.ObserveAndSelectCapacity(
currentCapacity: 64,
requiredCapacity: 257,
bytesPerUnit: 4,
minimumCapacity: 64,
growthQuantum: 64));
}
private static int ObserveLowDemand(
RetainedScratchCapacityPolicy policy) =>
policy.ObserveAndSelectCapacity(
currentCapacity: 4096,
requiredCapacity: 32,
bytesPerUnit: 4,
minimumCapacity: 64,
growthQuantum: 64);
}

View file

@ -157,6 +157,31 @@ public sealed class RetailAlphaQueueTests
Assert.Equal(expected, source.PreparedTokens);
}
[Fact]
public void RetainedScratchConvergesAfterAOneScopeSpike()
{
const int budgetBytes = 128 * 1024;
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue(budgetBytes);
queue.BeginFrame();
for (int i = 0; i < 8_192; i++)
queue.Submit(source, i, i);
queue.EndFrame();
Assert.True(queue.RetainedScratchBytes > budgetBytes);
for (int i = 0; i < 3; i++)
{
queue.BeginFrame();
queue.EndFrame();
}
Assert.True(queue.RetainedScratchBytes <= budgetBytes);
Assert.False(queue.IsCollecting);
Assert.Equal(0, queue.PendingCount);
}
[Fact]
public void Flush_EqualDistanceOrderStartsFreshInEachFrameScope()
{

View file

@ -202,6 +202,81 @@ public sealed class RetailDatLoaderTests
Assert.Throws<InvalidDataException>(() => animations.LoadAnimation(wrongAnimationKey));
}
[Fact]
public void AnimationCache_CountPressureEvictsLruWithoutInvalidatingLiveReferences()
{
const uint firstDid = 0x03010020u;
const uint secondDid = 0x03010021u;
const uint thirdDid = 0x03010022u;
var portal = new RawDatabase();
portal.Add(firstDid, AnimationBytes(firstDid));
portal.Add(secondDid, AnimationBytes(secondDid));
portal.Add(thirdDid, AnimationBytes(thirdDid));
var loader = new RetailAnimationLoader(
portal,
maximumEstimatedBytes: long.MaxValue,
maximumEntries: 2);
DatAnimation first = Assert.IsType<DatAnimation>(
loader.LoadAnimation(firstDid));
_ = Assert.IsType<DatAnimation>(loader.LoadAnimation(secondDid));
_ = Assert.IsType<DatAnimation>(loader.LoadAnimation(thirdDid));
AnimationCacheDiagnostics pressured = loader.Diagnostics;
Assert.Equal(2, pressured.Count);
Assert.Equal(1, pressured.Stats.Evictions);
Assert.Equal(firstDid, first.Id);
DatAnimation reloaded = Assert.IsType<DatAnimation>(
loader.LoadAnimation(firstDid));
Assert.NotSame(first, reloaded);
Assert.Equal(4, portal.TotalReads);
Assert.Equal(2, loader.Diagnostics.Stats.Evictions);
}
[Fact]
public void AnimationCache_OversizeEntryIsServedButNeverRetained()
{
const uint animationDid = 0x03010023u;
var portal = new RawDatabase();
portal.Add(animationDid, AnimationBytes(animationDid));
var loader = new RetailAnimationLoader(
portal,
maximumEstimatedBytes: 1,
maximumEntries: 2);
Assert.NotNull(loader.LoadAnimation(animationDid));
Assert.NotNull(loader.LoadAnimation(animationDid));
Assert.Equal(0, loader.Diagnostics.Count);
Assert.Equal(0, loader.Diagnostics.EstimatedBytes);
Assert.Equal(2, portal.TotalReads);
}
[Fact]
public async Task AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel()
{
const uint firstDid = 0x03010024u;
const uint secondDid = 0x03010025u;
var portal = new RawDatabase { ReadDelayMilliseconds = 40 };
portal.Add(firstDid, AnimationBytes(firstDid));
portal.Add(secondDid, AnimationBytes(secondDid));
var loader = new RetailAnimationLoader(portal);
DatAnimation?[] firstPair = await Task.WhenAll(
Task.Run(() => loader.LoadAnimation(firstDid)),
Task.Run(() => loader.LoadAnimation(firstDid)));
Assert.Same(firstPair[0], firstPair[1]);
Assert.Equal(1, portal.TotalReads);
await Task.WhenAll(
Task.Run(() => loader.LoadAnimation(secondDid)),
Task.Run(() => loader.LoadAnimation(0x03010026u)));
Assert.True(portal.MaxConcurrentReads >= 2);
Assert.Equal(3, portal.TotalReads);
}
[Fact]
public async Task PhysicsScriptLoader_AllowsConcurrentFirstReads()
{
@ -224,6 +299,16 @@ public sealed class RetailDatLoaderTests
Assert.Equal(2, portal.TotalReads);
}
private static byte[] AnimationBytes(uint id)
{
byte[] bytes =
ProjectileVfxDatFixtures.OrdinaryAnimation.ToArray();
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
bytes,
id);
return bytes;
}
[Fact]
public async Task PhysicsScriptLoader_CoalescesConcurrentReadsForTheSameDid()
{