using AcDream.App.Rendering; namespace AcDream.App.Rendering.Wb; /// /// A contiguous GPU-buffer suballocator whose released ranges become /// reusable only after the frame-retirement queue says that older draws have /// finished. This prevents BufferSubData from overwriting a range that /// an in-flight draw still reads. /// internal sealed class GpuRetiredRangeAllocator { private readonly ContiguousRangeAllocator _allocator; private readonly GpuRetirementLedger _retirementLedger; private readonly Dictionary _pendingReleases = []; private int _pendingReleaseCount; private int _pendingReleaseLength; public GpuRetiredRangeAllocator(int capacity, IGpuResourceRetirementQueue retirement) { _allocator = new ContiguousRangeAllocator(capacity); _retirementLedger = new GpuRetirementLedger( retirement ?? throw new ArgumentNullException(nameof(retirement))); } public int Capacity => _allocator.Capacity; public int Used => _allocator.Used; public int HighWaterMark => _allocator.HighWaterMark; public int LargestFreeRange => _allocator.LargestFreeRange; public int TrailingFreeLength => _allocator.TrailingFreeLength; public int PendingReleaseCount => _pendingReleaseCount; public int PendingReleaseLength => _pendingReleaseLength; public bool TryAllocate(int length, out MeshBufferRange allocation) => _allocator.TryAllocate(length, out allocation); public void Grow(int newCapacity) => _allocator.Grow(newCapacity); public void Shrink(int newCapacity) => _allocator.Shrink(newCapacity); public void ReleaseAfterGpuUse(MeshBufferRange allocation) { if (_pendingReleases.TryGetValue(allocation, out RetryableGpuResourceRelease? pending)) { // The caller retries this method when queue publication failed. // Re-publish the retained transaction instead of accounting for a // second logical release of the same physical range. try { _retirementLedger.RetryPendingPublication(pending); } catch when (pending.IsComplete) { // An immediate retirement queue may surface a wrapper failure // after the retained transaction itself fully committed. } return; } int nextPendingCount = checked(_pendingReleaseCount + 1); int nextPendingLength = checked(_pendingReleaseLength + allocation.Length); var release = new RetryableGpuResourceRelease( () => _allocator.Release(allocation), () => _pendingReleaseCount = checked(_pendingReleaseCount - 1), () => _pendingReleaseLength = checked( _pendingReleaseLength - allocation.Length), () => { if (!_pendingReleases.Remove(allocation)) { throw new InvalidOperationException( "GPU buffer range retirement lost its ownership record."); } }); _pendingReleases.Add(allocation, release); _pendingReleaseCount = nextPendingCount; _pendingReleaseLength = nextPendingLength; try { _retirementLedger.Retire(release); } catch when (release.IsComplete) { // Completion is stronger than publication acknowledgement. The // physical range and its accounting already converged. } } /// /// Releases a range that failed before it could be submitted in a draw. /// public void ReleaseUnsubmitted(MeshBufferRange allocation) { if (_pendingReleases.ContainsKey(allocation)) { throw new InvalidOperationException( "A GPU-submitted range cannot be released as unsubmitted."); } _allocator.Release(allocation); } }