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.App.Rendering.Wb;
using AcDream.Content;
using AcDream.Core.Meshing;
@ -181,6 +183,13 @@ public sealed unsafe class ParticleRenderer : IDisposable
private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256];
private uint[] _preparedInstanceOffsets = new uint[256];
private int _preparedAlphaCount;
private readonly RetainedScratchCapacityPolicy _alphaScratchPolicy;
internal long AlphaScratchBudgetBytes => _alphaScratchPolicy.BudgetBytes;
internal long RetainedAlphaScratchBytes => checked(
(long)_deferredAlpha.Capacity * Unsafe.SizeOf<DeferredParticleDraw>()
+ (long)_preparedAlpha.Length * Unsafe.SizeOf<DeferredParticleDraw>()
+ (long)_preparedInstanceOffsets.Length * sizeof(uint));
private sealed class AlphaDrawSource(ParticleRenderer owner) : IRetailAlphaDrawSource
{
@ -201,7 +210,8 @@ public sealed unsafe class ParticleRenderer : IDisposable
TextureCache? textures = null,
IDatReaderWriter? dats = null,
WbMeshAdapter? meshAdapter = null,
RetailAlphaQueue? alphaQueue = null)
RetailAlphaQueue? alphaQueue = null,
long? alphaScratchBudgetBytes = null)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_textures = textures;
@ -210,6 +220,11 @@ public sealed unsafe class ParticleRenderer : IDisposable
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
_alphaQueue = alphaQueue;
_alphaSource = new AlphaDrawSource(this);
long scratchBudget = alphaScratchBudgetBytes
?? AlphaScratchBudgetProfile.Create(
ResidencyBudgetOptions.Default.AlphaScratchBytes).ParticleBytes;
_alphaScratchPolicy =
new RetainedScratchCapacityPolicy(scratchBudget);
if (_meshAdapter is not null)
{
_meshReferences = new ParticleMeshReferenceTracker(
@ -732,8 +747,30 @@ public sealed unsafe class ParticleRenderer : IDisposable
private void ResetDeferredAlpha()
{
int observedCount = Math.Max(
_deferredAlpha.Count,
_preparedAlphaCount);
_deferredAlpha.Clear();
_preparedAlphaCount = 0;
int currentCapacity = Math.Max(
_deferredAlpha.Capacity,
Math.Max(
_preparedAlpha.Length,
_preparedInstanceOffsets.Length));
int bytesPerDraw = checked(
2 * Unsafe.SizeOf<DeferredParticleDraw>() + sizeof(uint));
int targetCapacity = _alphaScratchPolicy.ObserveAndSelectCapacity(
currentCapacity,
observedCount,
bytesPerDraw,
minimumCapacity: 256,
growthQuantum: 256);
if (targetCapacity >= currentCapacity)
return;
_deferredAlpha.Capacity = targetCapacity;
Array.Resize(ref _preparedAlpha, targetCapacity);
Array.Resize(ref _preparedInstanceOffsets, targetCapacity);
}
private void PrepareBillboardPipeline(Matrix4x4 viewProjection)