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

@ -1,6 +1,7 @@
using AcDream.App.Audio;
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;
@ -43,6 +44,7 @@ internal sealed record ContentEffectsAudioResult(
internal sealed record ContentEffectsAudioDependencies(
string DatDirectory,
string PreparedAssetPath,
ResidencyBudgetOptions ResidencyBudgets,
PhysicsDataCache PhysicsDataCache,
bool DumpMotionEnabled,
Spellbook SpellBook,
@ -84,7 +86,10 @@ internal interface IContentEffectsAudioCompositionFactory
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog);
int GetSpellCount(MagicCatalog catalog);
IAnimationLoader CreateAnimationLoader(IDatReaderWriter dats);
IAnimationLoader CreateAnimationLoader(
IDatReaderWriter dats,
long maximumEstimatedBytes,
int maximumEntries);
LiveEntityCollisionBuilder CreateCollisionBuilder(
PhysicsDataCache physicsData,
IDatReaderWriter dats,
@ -137,8 +142,14 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
public IAnimationLoader CreateAnimationLoader(IDatReaderWriter dats) =>
new RetailAnimationLoader(dats);
public IAnimationLoader CreateAnimationLoader(
IDatReaderWriter dats,
long maximumEstimatedBytes,
int maximumEntries) =>
new RetailAnimationLoader(
dats,
maximumEstimatedBytes,
maximumEntries);
public LiveEntityCollisionBuilder CreateCollisionBuilder(
PhysicsDataCache physicsData,
@ -302,7 +313,10 @@ internal sealed class ContentEffectsAudioCompositionPhase :
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);
IAnimationLoader animations = _factory.CreateAnimationLoader(dats);
IAnimationLoader animations = _factory.CreateAnimationLoader(
dats,
_dependencies.ResidencyBudgets.AnimationBytes,
_dependencies.ResidencyBudgets.AnimationEntries);
_publication.PublishAnimationLoader(animations);
Fault(ContentEffectsAudioCompositionPoint.AnimationLoaderPublished);

View file

@ -5,6 +5,7 @@ using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Vfx;
@ -586,6 +587,9 @@ internal sealed class LivePresentationCompositionPhase
{
LivePresentationDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
AlphaScratchBudgetProfile alphaScratchBudgets =
AlphaScratchBudgetProfile.Create(
d.Options.ResidencyBudgets.AlphaScratchBytes);
var selectionScene = new RetailSelectionScene(
new RetailSelectionGeometryCache(content.Dats, d.DatLock));
@ -601,7 +605,8 @@ internal sealed class LivePresentationCompositionPhase
d.ClassificationCache,
d.TranslucencyFades,
selectionScene,
d.RetailAlphaQueue),
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
@ -883,7 +888,8 @@ internal sealed class LivePresentationCompositionPhase
foundation.TextureCache,
content.Dats,
foundation.MeshAdapter,
d.RetailAlphaQueue),
d.RetailAlphaQueue,
alphaScratchBudgets.ParticleBytes),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated);
@ -966,6 +972,19 @@ internal sealed class LivePresentationCompositionPhase
frameDiagnostics,
bindings,
landblockLoaded);
foundation.Residency.RegisterDomainSource(
new DelegateResidencyDomainSource(
ResidencyDomain.AlphaScratch,
() => new ResidencyDomainSnapshot(
ResidencyDomain.AlphaScratch,
EntryCount: 3,
OwnerCount: 3,
Charges: new ResidencyCharges(
ScratchBytes: checked(
d.RetailAlphaQueue.RetainedScratchBytes
+ dispatcherLease.Resource.RetainedAlphaScratchBytes
+ particleLease.Resource.RetainedAlphaScratchBytes)),
BudgetBytes: alphaScratchBudgets.TotalBytes)));
_publication.PublishLivePresentation(result);
liveRuntimeLease.Transfer();

View file

@ -4,6 +4,8 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Residency;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Content.Vfx;
using AcDream.Core.Physics;
using AcDream.Core.Terrain;
using AcDream.UI.Abstractions.Settings;
using DatReaderWriter;
@ -116,7 +118,8 @@ internal interface IWorldRenderCompositionFactory
ResidencyManager manager,
WbMeshAdapter meshes,
TextureCache textures,
IPreparedAssetSource preparedAssets);
IPreparedAssetSource preparedAssets,
IAnimationLoader animations);
SamplerCache CreateSamplerCache(GL gl);
void Release(IDisposable resource);
}
@ -295,7 +298,8 @@ internal sealed class RetailWorldRenderCompositionFactory
ResidencyManager manager,
WbMeshAdapter meshes,
TextureCache textures,
IPreparedAssetSource preparedAssets)
IPreparedAssetSource preparedAssets,
IAnimationLoader animations)
{
meshes.RegisterResidencySources(manager);
textures.RegisterResidencySources(manager);
@ -308,6 +312,25 @@ internal sealed class RetailWorldRenderCompositionFactory
Charges: new ResidencyCharges(
MappedVirtualBytes:
preparedAssets.MappedVirtualBytes))));
if (animations is RetailAnimationLoader retailAnimations)
{
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
ResidencyDomain.Animations,
() =>
{
AnimationCacheDiagnostics diagnostics =
retailAnimations.Diagnostics;
return new ResidencyDomainSnapshot(
ResidencyDomain.Animations,
EntryCount: diagnostics.Count,
OwnerCount: 0,
Charges: new ResidencyCharges(
DecodedBytes:
diagnostics.EstimatedBytes),
BudgetBytes: diagnostics.BudgetBytes,
Evictions: diagnostics.Stats.Evictions);
}));
}
}
public SamplerCache CreateSamplerCache(GL gl) => new(gl);
@ -493,7 +516,8 @@ internal sealed class WorldRenderCompositionPhase
residency,
meshAdapter,
textureCache,
content.PreparedAssets);
content.PreparedAssets,
content.AnimationLoader);
scope.Complete();
_dependencies.Log(