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

@ -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()
{