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:
parent
3e18fc2730
commit
f2644d42c2
18 changed files with 857 additions and 34 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ public sealed class GameWindow :
|
|||
private AcDream.Content.Vfx.RetailPhysicsScriptLoader? _physicsScriptLoader;
|
||||
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
||||
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new();
|
||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue;
|
||||
// Remote-entity motion inference: tracks when each remote entity last
|
||||
// moved meaningfully. Used in TickAnimations to swap to Ready when
|
||||
// position has stalled for >StopIdleMs — retail observer pattern per
|
||||
|
|
@ -564,6 +564,11 @@ public sealed class GameWindow :
|
|||
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
var alphaScratchBudgets =
|
||||
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||
_options.ResidencyBudgets.AlphaScratchBytes);
|
||||
_retailAlphaQueue = new AcDream.App.Rendering.RetailAlphaQueue(
|
||||
alphaScratchBudgets.QueueBytes);
|
||||
WorldTime = _worldEnvironment.WorldTime;
|
||||
Weather = _worldEnvironment.Weather;
|
||||
_retainedInputCapture = new AcDream.App.Input.RetainedUiInputCaptureSlot();
|
||||
|
|
@ -1141,6 +1146,7 @@ public sealed class GameWindow :
|
|||
new ContentEffectsAudioDependencies(
|
||||
_datDir,
|
||||
_options.PreparedAssetPath,
|
||||
_options.ResidencyBudgets,
|
||||
_physicsDataCache,
|
||||
_animationDiagnostics.DumpMotionEnabled,
|
||||
SpellBook,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ internal readonly record struct ResidencyCharges(
|
|||
long LogicalBytes = 0,
|
||||
long CpuPreparedBytes = 0,
|
||||
long DecodedBytes = 0,
|
||||
long ScratchBytes = 0,
|
||||
long PinnedBytes = 0,
|
||||
long StagingBytes = 0,
|
||||
long GpuRequestedBytes = 0,
|
||||
|
|
@ -97,6 +98,7 @@ internal readonly record struct ResidencyCharges(
|
|||
LogicalBytes
|
||||
+ CpuPreparedBytes
|
||||
+ DecodedBytes
|
||||
+ ScratchBytes
|
||||
+ StagingBytes);
|
||||
|
||||
public long PhysicalGpuBytes => checked(
|
||||
|
|
@ -111,6 +113,7 @@ internal readonly record struct ResidencyCharges(
|
|||
ArgumentOutOfRangeException.ThrowIfNegative(LogicalBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(CpuPreparedBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(DecodedBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(ScratchBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(PinnedBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(StagingBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(GpuRequestedBytes);
|
||||
|
|
@ -126,6 +129,7 @@ internal readonly record struct ResidencyCharges(
|
|||
checked(left.LogicalBytes + right.LogicalBytes),
|
||||
checked(left.CpuPreparedBytes + right.CpuPreparedBytes),
|
||||
checked(left.DecodedBytes + right.DecodedBytes),
|
||||
checked(left.ScratchBytes + right.ScratchBytes),
|
||||
checked(left.PinnedBytes + right.PinnedBytes),
|
||||
checked(left.StagingBytes + right.StagingBytes),
|
||||
checked(left.GpuRequestedBytes + right.GpuRequestedBytes),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
namespace AcDream.App.Rendering.Residency;
|
||||
|
||||
/// <summary>
|
||||
/// Splits the one deferred-alpha scratch ceiling between the three physical
|
||||
/// owners. The final share receives the division remainder so the aggregate
|
||||
/// remains exact for every valid startup budget.
|
||||
/// </summary>
|
||||
internal readonly record struct AlphaScratchBudgetProfile(
|
||||
long QueueBytes,
|
||||
long DispatcherBytes,
|
||||
long ParticleBytes)
|
||||
{
|
||||
public long TotalBytes => checked(
|
||||
QueueBytes + DispatcherBytes + ParticleBytes);
|
||||
|
||||
public static AlphaScratchBudgetProfile Create(long totalBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalBytes);
|
||||
long queue = totalBytes / 4;
|
||||
long particles = totalBytes / 4;
|
||||
long dispatcher = checked(totalBytes - queue - particles);
|
||||
return new AlphaScratchBudgetProfile(queue, dispatcher, particles);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capacity retention policy for render-thread scratch storage. Growth is
|
||||
/// always immediate at the physical owner. This policy only recommends a
|
||||
/// smaller warmed capacity after several completed low-demand observations,
|
||||
/// preventing one dense alpha frame from pinning its high-water arrays for the
|
||||
/// rest of the process without introducing an elapsed-time workaround.
|
||||
/// </summary>
|
||||
internal sealed class RetainedScratchCapacityPolicy(
|
||||
long budgetBytes,
|
||||
int lowDemandSamplesBeforeShrink = 3)
|
||||
{
|
||||
private readonly long _budgetBytes =
|
||||
budgetBytes > 0
|
||||
? budgetBytes
|
||||
: throw new ArgumentOutOfRangeException(nameof(budgetBytes));
|
||||
private readonly int _lowDemandSamplesBeforeShrink =
|
||||
lowDemandSamplesBeforeShrink > 0
|
||||
? lowDemandSamplesBeforeShrink
|
||||
: throw new ArgumentOutOfRangeException(
|
||||
nameof(lowDemandSamplesBeforeShrink));
|
||||
private int _lowDemandSamples;
|
||||
|
||||
public long BudgetBytes => _budgetBytes;
|
||||
|
||||
public int ObserveAndSelectCapacity(
|
||||
int currentCapacity,
|
||||
int requiredCapacity,
|
||||
int bytesPerUnit,
|
||||
int minimumCapacity,
|
||||
int growthQuantum)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(currentCapacity);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(requiredCapacity);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytesPerUnit);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(minimumCapacity);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(growthQuantum);
|
||||
|
||||
if (requiredCapacity > currentCapacity)
|
||||
{
|
||||
_lowDemandSamples = 0;
|
||||
return RoundUp(requiredCapacity, growthQuantum);
|
||||
}
|
||||
|
||||
int budgetCapacity = checked((int)Math.Min(
|
||||
int.MaxValue,
|
||||
Math.Max(minimumCapacity, _budgetBytes / bytesPerUnit)));
|
||||
bool demandHasFallen =
|
||||
currentCapacity > budgetCapacity
|
||||
&& requiredCapacity <= budgetCapacity
|
||||
&& (requiredCapacity == 0
|
||||
|| (long)currentCapacity >= (long)requiredCapacity * 4);
|
||||
if (!demandHasFallen)
|
||||
{
|
||||
_lowDemandSamples = 0;
|
||||
return currentCapacity;
|
||||
}
|
||||
|
||||
_lowDemandSamples++;
|
||||
if (_lowDemandSamples < _lowDemandSamplesBeforeShrink)
|
||||
return currentCapacity;
|
||||
|
||||
_lowDemandSamples = 0;
|
||||
long warmedDemand = Math.Max(
|
||||
minimumCapacity,
|
||||
Math.Min(int.MaxValue, (long)requiredCapacity * 2));
|
||||
int target = RoundUp(
|
||||
checked((int)Math.Min(warmedDemand, budgetCapacity)),
|
||||
growthQuantum);
|
||||
target = Math.Max(requiredCapacity, Math.Min(target, budgetCapacity));
|
||||
return Math.Min(currentCapacity, target);
|
||||
}
|
||||
|
||||
private static int RoundUp(int value, int quantum)
|
||||
{
|
||||
if (value == 0)
|
||||
return 0;
|
||||
long rounded = ((long)value + quantum - 1) / quantum * quantum;
|
||||
return checked((int)Math.Min(int.MaxValue, rounded));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue