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,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Residency;
using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.Rendering;
@ -91,6 +93,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private readonly IRetailSelectionLightingSource? _selectionLighting;
private readonly RetailAlphaQueue? _alphaQueue;
private readonly AlphaDrawSource _alphaSource;
private readonly RetainedScratchCapacityPolicy _alphaScratchPolicy;
private int _scratchPeakUnits;
private readonly BindlessSupport _bindless;
@ -596,7 +600,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
=> owner.DrawPreparedAlphaBatch(firstPreparedDraw, drawCount);
public void ResetAlphaSubmissions()
=> owner._deferredAlpha.Clear();
=> owner.ResetDeferredAlphaSubmissions();
}
// Per-frame scratch — reused across frames to avoid per-frame allocation.
@ -606,6 +610,25 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private readonly List<DeferredAlphaInstance> _deferredAlpha = new(128);
private TranslucencyKind[] _deferredAlphaKinds = new TranslucencyKind[128];
private Matrix4x4 _deferredAlphaViewProjection;
internal long AlphaScratchBudgetBytes => _alphaScratchPolicy.BudgetBytes;
internal long RetainedAlphaScratchBytes => checked(
(long)_instanceData.Length * sizeof(float)
+ (long)_clipSlotData.Length * sizeof(uint)
+ (long)_lightSetData.Length * sizeof(int)
+ (long)_indoorData.Length * sizeof(uint)
+ (long)_alphaData.Length * sizeof(float)
+ (long)_selectionLightingData.Length * Unsafe.SizeOf<Vector2>()
+ (long)_batchData.Length * Unsafe.SizeOf<BatchData>()
+ (long)_indirectCommands.Length
* Unsafe.SizeOf<DrawElementsIndirectCommand>()
+ (long)_drawCullModes.Length * Unsafe.SizeOf<CullMode>()
+ (long)_batchPublicScratch.Length
* Unsafe.SizeOf<BatchDataPublic>()
+ (long)_deferredAlphaKinds.Length
* Unsafe.SizeOf<TranslucencyKind>()
+ (long)_deferredAlpha.Capacity
* Unsafe.SizeOf<DeferredAlphaInstance>());
// A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list
// instead of allocating a fresh List<(WorldEntity, int)> per frame. At
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
@ -729,7 +752,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
EntityClassificationCache classificationCache,
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
IRetailSelectionRenderSink? selectionSink = null,
RetailAlphaQueue? alphaQueue = null)
RetailAlphaQueue? alphaQueue = null,
long? alphaScratchBudgetBytes = null)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentNullException.ThrowIfNull(shader);
@ -750,6 +774,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
_alphaQueue = alphaQueue;
_alphaSource = new AlphaDrawSource(this);
long scratchBudget = alphaScratchBudgetBytes
?? AlphaScratchBudgetProfile.Create(
ResidencyBudgetOptions.Default.AlphaScratchBytes).DispatcherBytes;
_alphaScratchPolicy =
new RetainedScratchCapacityPolicy(scratchBudget);
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
}
@ -766,6 +795,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
if (_groupFrame == long.MaxValue)
throw new InvalidOperationException("Instance-group frame identity was exhausted.");
ApplyScratchRetention(_scratchPeakUnits);
_scratchPeakUnits = 0;
_groupFrame++;
PruneInstanceGroupsUnusedBeforeFrame(
_groups,
@ -1965,6 +1996,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// fill via BuildIndirectArrays ──────────────────────────────────
int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count;
int totalDraws = _opaqueDraws.Count + immediateTransparentCount;
TrackScratchDemand(Math.Max(totalInstances, totalDraws));
if (_batchData.Length < totalDraws)
_batchData = new BatchData[totalDraws + 64];
if (_indirectCommands.Length < totalDraws)
@ -2493,6 +2525,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private void EnsureDeferredAlphaCapacity(int count)
{
TrackScratchDemand(count);
int neededMatrixFloats = count * 16;
if (_instanceData.Length < neededMatrixFloats)
_instanceData = new float[neededMatrixFloats + 256 * 16];
@ -2516,6 +2549,63 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_deferredAlphaKinds = new TranslucencyKind[count + 64];
}
private void ResetDeferredAlphaSubmissions()
{
_deferredAlpha.Clear();
}
private void TrackScratchDemand(int units)
{
if (units > _scratchPeakUnits)
_scratchPeakUnits = units;
}
private void ApplyScratchRetention(int observedUnits)
{
int currentCapacity = Math.Max(
_instanceData.Length / 16,
Math.Max(
_lightSetData.Length / LightManager.MaxLightsPerObject,
Math.Max(
_deferredAlpha.Capacity,
Math.Max(_batchData.Length, _indirectCommands.Length))));
int bytesPerUnit = checked(
16 * sizeof(float)
+ sizeof(uint)
+ LightManager.MaxLightsPerObject * sizeof(int)
+ sizeof(uint)
+ sizeof(float)
+ Unsafe.SizeOf<Vector2>()
+ Unsafe.SizeOf<BatchData>()
+ Unsafe.SizeOf<DrawElementsIndirectCommand>()
+ Unsafe.SizeOf<CullMode>()
+ Unsafe.SizeOf<BatchDataPublic>()
+ Unsafe.SizeOf<TranslucencyKind>()
+ Unsafe.SizeOf<DeferredAlphaInstance>());
int targetCapacity = _alphaScratchPolicy.ObserveAndSelectCapacity(
currentCapacity,
observedUnits,
bytesPerUnit,
minimumCapacity: 256,
growthQuantum: 256);
if (targetCapacity >= currentCapacity)
return;
_instanceData = new float[checked(targetCapacity * 16)];
_clipSlotData = new uint[targetCapacity];
_lightSetData = new int[
checked(targetCapacity * LightManager.MaxLightsPerObject)];
_indoorData = new uint[targetCapacity];
_alphaData = new float[targetCapacity];
_selectionLightingData = new Vector2[targetCapacity];
_batchData = new BatchData[targetCapacity];
_indirectCommands = new DrawElementsIndirectCommand[targetCapacity];
_drawCullModes = new CullMode[targetCapacity];
_batchPublicScratch = new BatchDataPublic[targetCapacity];
_deferredAlphaKinds = new TranslucencyKind[targetCapacity];
_deferredAlpha.Capacity = targetCapacity;
}
private void UploadDeferredAlphaBuffers(int count)
{
fixed (float* p = _instanceData)