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

@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.ExceptionServices;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Residency;
namespace AcDream.App.Rendering;
@ -68,15 +70,42 @@ internal interface IWorldSceneAlphaFrame
internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
{
private const int MinimumSubmissionCapacity = 256;
private const int SubmissionGrowthQuantum = 256;
private const int MinimumSourceCapacity = 4;
private readonly List<RetailAlphaSubmission> _submissions = new(256);
private readonly List<IRetailAlphaDrawSource> _sources = new(4);
private int[] _tokenScratch = new int[256];
private int[] _sourceDrawOffsets = new int[4];
private RetailAlphaSubmission[] _sortScratch = new RetailAlphaSubmission[256];
private readonly int[] _radixOffsets = new int[256];
private readonly RetainedScratchCapacityPolicy _scratchPolicy;
private readonly long _scratchBudgetBytes;
internal RetailAlphaQueue(long? scratchBudgetBytes = null)
{
long budget = scratchBudgetBytes
?? AlphaScratchBudgetProfile.Create(
ResidencyBudgetOptions.Default.AlphaScratchBytes).QueueBytes;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(budget);
_scratchBudgetBytes = budget;
_scratchPolicy = new RetainedScratchCapacityPolicy(Math.Max(
1,
budget - (long)_radixOffsets.Length * sizeof(int)));
}
public bool IsCollecting { get; private set; }
internal int PendingCount => _submissions.Count;
internal long ScratchBudgetBytes => _scratchBudgetBytes;
internal long RetainedScratchBytes => checked(
(long)_submissions.Capacity
* Unsafe.SizeOf<RetailAlphaSubmission>()
+ (long)_sortScratch.Length
* Unsafe.SizeOf<RetailAlphaSubmission>()
+ (long)_tokenScratch.Length * sizeof(int)
+ (long)_sources.Capacity * IntPtr.Size
+ (long)_sourceDrawOffsets.Length * sizeof(int)
+ (long)_radixOffsets.Length * sizeof(int));
public void BeginFrame()
{
@ -171,6 +200,8 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
}
finally
{
int submissionCount = _submissions.Count;
int sourceCount = _sources.Count;
for (int i = 0; i < _sources.Count; i++)
{
try
@ -184,6 +215,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
}
_sources.Clear();
_submissions.Clear();
ApplyScratchRetention(submissionCount, sourceCount);
}
if (drawFailure is not null)
@ -246,9 +278,12 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
}
finally
{
int submissionCount = _submissions.Count;
int sourceCount = _sources.Count;
_sources.Clear();
_submissions.Clear();
IsCollecting = false;
ApplyScratchRetention(submissionCount, sourceCount);
}
if (failures is { Count: > 0 })
@ -339,4 +374,40 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
return i;
throw new InvalidOperationException("Retail alpha source was not registered for this frame.");
}
private void ApplyScratchRetention(
int observedSubmissionCount,
int observedSourceCount)
{
int currentCapacity = Math.Max(
_submissions.Capacity,
Math.Max(_tokenScratch.Length, _sortScratch.Length));
int bytesPerSubmission =
checked(
2 * Unsafe.SizeOf<RetailAlphaSubmission>()
+ 2 * sizeof(int)
+ IntPtr.Size);
int targetCapacity = _scratchPolicy.ObserveAndSelectCapacity(
currentCapacity,
observedSubmissionCount,
bytesPerSubmission,
MinimumSubmissionCapacity,
SubmissionGrowthQuantum);
if (targetCapacity < currentCapacity)
{
_submissions.Capacity = targetCapacity;
Array.Resize(ref _tokenScratch, targetCapacity);
Array.Resize(ref _sortScratch, targetCapacity);
int sourceTarget = Math.Max(
MinimumSourceCapacity,
observedSourceCount == 0
? MinimumSourceCapacity
: checked(observedSourceCount * 2));
sourceTarget = Math.Min(sourceTarget, _sources.Capacity);
_sources.Capacity = sourceTarget;
if (_sourceDrawOffsets.Length > sourceTarget)
Array.Resize(ref _sourceDrawOffsets, sourceTarget);
}
}
}