feat(render): unify physical residency accounting
This commit is contained in:
parent
1866ea0c6d
commit
3e18fc2730
25 changed files with 642 additions and 48 deletions
|
|
@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
|
||||
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
|
||||
|
|
@ -143,8 +144,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
// LRU Cache for Unused objects
|
||||
private readonly LinkedList<ulong> _lruList = new();
|
||||
private readonly long _maxGpuMemory = 1024 * 1024 * 1024; // 1GB
|
||||
private readonly int _maxCachedObjects = 50; // Max number of cached objects (count-based limit)
|
||||
private readonly long _maxGpuMemory;
|
||||
private readonly int _maxCachedObjects;
|
||||
private long _currentNonArenaGpuMemory;
|
||||
|
||||
// Shared atlases grouped by (Width, Height, Format)
|
||||
|
|
@ -158,12 +159,15 @@ namespace AcDream.App.Rendering.Wb
|
|||
private const int RetainedEmptyAtlasCountLimit = 32;
|
||||
private readonly AcDream.App.Rendering.BoundedUnownedResourceCache<TextureAtlasManager>
|
||||
_safeEmptyAtlases = new(RetainedEmptyAtlasBudgetBytes, RetainedEmptyAtlasCountLimit);
|
||||
// Arrays removed from the reusable atlas families remain physical GPU
|
||||
// allocations until their frame-fenced release completes. Retaining
|
||||
// the owners here makes that overlap both retryable and observable.
|
||||
private readonly List<TextureAtlasManager> _retiringAtlases = [];
|
||||
|
||||
// CPU-side cache for prepared mesh data (to avoid re-reading/decoding from DAT)
|
||||
private readonly int _maxCpuCacheSize = 100;
|
||||
private readonly CpuMeshUploadCache _cpuMeshCache;
|
||||
|
||||
private readonly MeshUploadStagingQueue _stagedMeshData = new();
|
||||
private readonly MeshUploadStagingQueue _stagedMeshData;
|
||||
private volatile bool _arenaBackpressured;
|
||||
|
||||
/// <summary>#125: how many times a failed GL upload is re-staged before
|
||||
|
|
@ -264,8 +268,13 @@ namespace AcDream.App.Rendering.Wb
|
|||
int atlasArrays = 0;
|
||||
foreach (List<TextureAtlasManager> atlases in _globalAtlases.Values)
|
||||
atlasArrays += atlases.Count;
|
||||
long atlasBytes = CalculateAtlasBytes(_globalAtlases.Values);
|
||||
long retiringAtlasBytes = CalculateAtlasBytes(_retiringAtlases);
|
||||
long physicalBytes = CalculateTrackedGpuBytes(
|
||||
_currentNonArenaGpuMemory,
|
||||
checked(
|
||||
_currentNonArenaGpuMemory
|
||||
+ atlasBytes
|
||||
+ retiringAtlasBytes),
|
||||
GlobalBuffer?.PhysicalCapacityBytes ?? 0);
|
||||
return (_renderData.Count, atlasArrays, _lruList.Count, physicalBytes);
|
||||
}
|
||||
|
|
@ -400,15 +409,24 @@ namespace AcDream.App.Rendering.Wb
|
|||
public ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger)
|
||||
ILogger<ObjectMeshManager> logger,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
_cpuMeshCache = new CpuMeshUploadCache(_maxCpuCacheSize);
|
||||
_maxGpuMemory = budgets.ObjectMeshGpuBytes;
|
||||
_maxCachedObjects = budgets.ObjectMeshUnownedEntries;
|
||||
_cpuMeshCache = new CpuMeshUploadCache(
|
||||
budgets.PreparedMeshCpuEntries,
|
||||
budgets.PreparedMeshCpuBytes);
|
||||
_stagedMeshData = new MeshUploadStagingQueue(
|
||||
budgets.MeshStagingEntries,
|
||||
budgets.MeshStagingBytes);
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
{
|
||||
|
|
@ -506,6 +524,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
internal bool EvictOneEmptyAtlas()
|
||||
{
|
||||
RetryRetiringAtlasDisposals();
|
||||
if (!_safeEmptyAtlases.TryTakeOldestOverBudget(out TextureAtlasManager victim))
|
||||
return false;
|
||||
|
||||
|
|
@ -517,10 +536,28 @@ namespace AcDream.App.Rendering.Wb
|
|||
_globalAtlases.Remove(key);
|
||||
}
|
||||
_dirtyAtlases.Remove(victim);
|
||||
_retiringAtlases.Add(victim);
|
||||
victim.Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RetryRetiringAtlasDisposals()
|
||||
{
|
||||
for (int i = 0; i < _retiringAtlases.Count; i++)
|
||||
_retiringAtlases[i].Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
}
|
||||
|
||||
private void RemoveCompletedAtlasRetirements()
|
||||
{
|
||||
for (int i = _retiringAtlases.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_retiringAtlases[i].IsPhysicalRetirementComplete)
|
||||
_retiringAtlases.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAtlasGpuSafeEmpty(TextureAtlasManager atlas)
|
||||
{
|
||||
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
||||
|
|
@ -637,6 +674,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumCount, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBytes, 1);
|
||||
RetryRetiringAtlasDisposals();
|
||||
RetryPendingAtlasRetirements();
|
||||
// Rollbacks own unpublished resources and therefore have no LRU
|
||||
// node to wake them. Advance a bounded snapshot every render tick
|
||||
|
|
@ -649,6 +687,9 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_lruList)
|
||||
candidateBudget = _lruList.Count;
|
||||
int attemptedCandidates = 0;
|
||||
long trackedAtlasBytes = checked(
|
||||
CalculateAtlasBytes(_globalAtlases.Values)
|
||||
+ CalculateAtlasBytes(_retiringAtlases));
|
||||
|
||||
while (reclaimedCount < maximumCount
|
||||
&& attemptedCandidates < candidateBudget)
|
||||
|
|
@ -657,9 +698,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_lruList)
|
||||
{
|
||||
long physicalArenaBytes = GlobalBuffer?.PhysicalCapacityBytes ?? 0;
|
||||
long nonArenaAndAtlasBytes = checked(
|
||||
_currentNonArenaGpuMemory
|
||||
+ trackedAtlasBytes);
|
||||
if (!forceArenaReclamation
|
||||
&& IsWithinGpuCacheBudget(
|
||||
_currentNonArenaGpuMemory,
|
||||
nonArenaAndAtlasBytes,
|
||||
physicalArenaBytes,
|
||||
_maxGpuMemory)
|
||||
&& _lruList.Count <= _maxCachedObjects)
|
||||
|
|
@ -1207,6 +1251,105 @@ namespace AcDream.App.Rendering.Wb
|
|||
return _preparedAssets.Read(request, ct).Data;
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureObjectMeshResidency()
|
||||
{
|
||||
GlobalMeshBuffer? arena = GlobalBuffer;
|
||||
long nonArenaRetiring = 0;
|
||||
foreach (ObjectReleaseTicket ticket in _objectReleases.Values)
|
||||
{
|
||||
nonArenaRetiring = checked(
|
||||
nonArenaRetiring
|
||||
+ Math.Max(0, ticket.Data.NonArenaGpuBytes));
|
||||
}
|
||||
nonArenaRetiring = Math.Min(
|
||||
nonArenaRetiring,
|
||||
_currentNonArenaGpuMemory);
|
||||
|
||||
long arenaResident = arena?.ResidentCapacityBytes ?? 0;
|
||||
long arenaRequested = arena?.RequestedMigrationBytes ?? 0;
|
||||
long arenaRetiring = checked(
|
||||
(arena?.PendingRangeRetirementBytes ?? 0)
|
||||
+ (arena?.RetiredBackingBytes ?? 0));
|
||||
long atlasResident = CalculateAtlasBytes(_globalAtlases.Values);
|
||||
long atlasRetiring = CalculateAtlasBytes(_retiringAtlases);
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.ObjectMeshes,
|
||||
EntryCount: checked(
|
||||
_renderData.Count
|
||||
+ _globalAtlases.Values.Sum(static atlases => atlases.Count)
|
||||
+ _retiringAtlases.Count),
|
||||
OwnerCount: _ownership.TotalReferenceCount,
|
||||
Charges: new ResidencyCharges(
|
||||
GpuRequestedBytes: arenaRequested,
|
||||
GpuResidentBytes: checked(
|
||||
_currentNonArenaGpuMemory
|
||||
- nonArenaRetiring
|
||||
+ arenaResident
|
||||
+ atlasResident),
|
||||
RetiringBytes: checked(
|
||||
nonArenaRetiring
|
||||
+ arenaRetiring
|
||||
+ atlasRetiring)),
|
||||
BudgetBytes: _maxGpuMemory);
|
||||
}
|
||||
|
||||
internal static long CalculateAtlasBytes(
|
||||
IEnumerable<IReadOnlyCollection<TextureAtlasManager>> atlasFamilies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(atlasFamilies);
|
||||
long bytes = 0;
|
||||
foreach (IReadOnlyCollection<TextureAtlasManager> family in atlasFamilies)
|
||||
{
|
||||
foreach (TextureAtlasManager atlas in family)
|
||||
bytes = checked(bytes + atlas.AllocatedBytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static long CalculateAtlasBytes(
|
||||
IEnumerable<TextureAtlasManager> atlases)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(atlases);
|
||||
long bytes = 0;
|
||||
foreach (TextureAtlasManager atlas in atlases)
|
||||
bytes = checked(bytes + atlas.AllocatedBytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CapturePreparedMeshResidency() =>
|
||||
new(
|
||||
ResidencyDomain.PreparedMeshCpu,
|
||||
EntryCount: _cpuMeshCache.Count,
|
||||
OwnerCount: 0,
|
||||
Charges: new ResidencyCharges(
|
||||
CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
|
||||
BudgetBytes: _cpuMeshCache.ByteCapacity);
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureStagingResidency() =>
|
||||
new(
|
||||
ResidencyDomain.MeshStaging,
|
||||
EntryCount: _stagedMeshData.ClaimCount,
|
||||
OwnerCount: 0,
|
||||
Charges: new ResidencyCharges(
|
||||
StagingBytes: _stagedMeshData.ClaimedBytes),
|
||||
BudgetBytes: _stagedMeshData.MaximumBytes);
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureGlobalArenaResidency()
|
||||
{
|
||||
GlobalMeshBuffer? arena = GlobalBuffer;
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.GlobalMeshArena,
|
||||
EntryCount: arena is null ? 0 : 2,
|
||||
OwnerCount: 0,
|
||||
// Physical arena bytes are already charged once by the
|
||||
// ObjectMeshes aggregate. This row exposes allocator facts.
|
||||
Charges: ResidencyCharges.Zero,
|
||||
BudgetBytes: GlobalMeshBuffer.MaximumPhysicalArenaBytes,
|
||||
CapacityBytes: arena?.CapacityBytes ?? 0,
|
||||
UsedBytes: arena?.UsedBytes ?? 0,
|
||||
LargestFreeBytes: arena?.LargestFreeBytes ?? 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel preparation tasks for IDs that are no longer needed.
|
||||
/// </summary>
|
||||
|
|
@ -2545,6 +2688,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
// Every layer release has now committed logically. Publish any
|
||||
// callback which previously failed before queue acceptance.
|
||||
Capture(ref failures, RetryPendingAtlasRetirements);
|
||||
Capture(ref failures, RetryRetiringAtlasDisposals);
|
||||
|
||||
bool atlasFailure = false;
|
||||
foreach (List<TextureAtlasManager> atlasList in _globalAtlases.Values)
|
||||
|
|
@ -2577,6 +2721,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
_uploadRollbacks.Clear();
|
||||
_uploadRollbackQueue.Clear();
|
||||
_globalAtlases.Clear();
|
||||
_retiringAtlases.Clear();
|
||||
_dirtyAtlases.Clear();
|
||||
_safeEmptyAtlases.Clear();
|
||||
_currentNonArenaGpuMemory = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue