using AcDream.Content; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using AcDream.App.Rendering; namespace AcDream.App.Rendering.Wb; internal sealed record GlobalMeshAllocation( MeshBufferRange Vertices, MeshBufferRange Indices, IReadOnlyList BatchFirstIndices); internal readonly record struct GlobalMeshUploadPlan( long UploadBytes, long AllocationBytes, long CopyBytes, int NewBufferCount); internal readonly record struct GlobalMeshMaintenanceStep( long AllocationBytes, long CopyBytes, int NewBufferCount, bool Completed); /// /// Retains the staged GL name and its exact release cursor while an aborted /// arena migration is being unwound. The owner may only forget the migration /// after this ticket has converged. /// internal sealed class GlobalMeshMigrationAbortTicket { private readonly RetryableGpuResourceRelease _release; public GlobalMeshMigrationAbortTicket( uint buffer, long capacityBytes, RetryableGpuResourceRelease release) { if (buffer == 0) throw new ArgumentOutOfRangeException(nameof(buffer)); ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes); Buffer = buffer; CapacityBytes = capacityBytes; _release = release ?? throw new ArgumentNullException(nameof(release)); } public uint Buffer { get; } public long CapacityBytes { get; } public bool IsComplete => _release.IsComplete; public void Advance() => _release.Run(); } internal static class GlobalMeshVaoAccounting { public static void TrackAllocation() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO); public static void TrackDeallocation() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO); } internal enum GlobalMeshCapacityResult { Ready, MigrationStarted, MigrationInProgress, NeedsReclamation, } /// /// Shared modern-rendering vertex/index buffers with reclaimable ranges. /// ObjectMeshManager owns allocation lifetime and releases a mesh's ranges /// when its zero-reference LRU entry is evicted. /// public sealed class GlobalMeshBuffer : IDisposable { internal const int InitialVertexCapacity = 1024 * 1024; internal const int InitialIndexCapacity = 3 * 1024 * 1024; internal const int VertexGrowthQuantum = 256 * 1024; internal const int IndexGrowthQuantum = 1024 * 1024; internal const long MaximumVertexBufferBytes = 384L * 1024 * 1024; internal const long MaximumIndexBufferBytes = 128L * 1024 * 1024; // Worst legal dual-buffer overlap: just-under-384 MiB old vertex store + // 384 MiB destination + the 128 MiB active index store. No route can // accumulate a second staged/retired generation beyond this ceiling. internal const long MaximumPhysicalArenaBytes = 896L * 1024 * 1024; internal static readonly int MaximumVertexCapacity = checked( (int)(MaximumVertexBufferBytes / VertexPositionNormalTexture.Size)); internal const int MaximumIndexCapacity = (int)(MaximumIndexBufferBytes / sizeof(ushort)); private readonly GL _gl; private readonly GpuRetirementLedger _retirementLedger; private readonly GpuRetiredRangeAllocator _vertices; private readonly GpuRetiredRangeAllocator _indices; private BufferMigration? _migration; private GlobalMeshMigrationAbortTicket? _migrationAbort; private long _retiredCapacityBytes; private bool _disposed; private RetryableResourceReleaseLedger? _disposeResources; private enum BufferKind { Vertices, Indices, } private sealed record BufferMigration( BufferKind Kind, uint OldBuffer, uint NewBuffer, int OldCapacity, int NewCapacity, long OldCapacityBytes, long NewCapacityBytes, long CopyBytes) { public long CopiedBytes { get; set; } } public uint VAO { get; private set; } public uint VBO { get; private set; } public uint IBO { get; private set; } internal long UploadCount { get; private set; } internal long UploadedBytes { get; private set; } internal long CapacityBytes => (long)_vertices.Capacity * VertexPositionNormalTexture.Size + (long)_indices.Capacity * sizeof(ushort); internal long PhysicalCapacityBytes => checked( CapacityBytes + (_migration?.NewCapacityBytes ?? 0) + _retiredCapacityBytes); internal bool IsMigrationInProgress => _migration is not null; internal bool HasPendingReclamation => _migration is not null || _vertices.PendingReleaseCount != 0 || _indices.PendingReleaseCount != 0 || _retiredCapacityBytes != 0; internal int VertexHighWaterMark => _vertices.HighWaterMark; internal int IndexHighWaterMark => _indices.HighWaterMark; internal GlobalMeshUploadPlan PlanUpload(int vertexCount, int indexCount) { ObjectDisposedException.ThrowIf(_disposed, this); _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); if (_migration is not null) throw new InvalidOperationException("Upload planning is unavailable while a backing-buffer migration is in progress."); ArgumentOutOfRangeException.ThrowIfNegative(vertexCount); ArgumentOutOfRangeException.ThrowIfNegative(indexCount); long allocationBytes = 0; long copyBytes = 0; int newBuffers = 0; if (vertexCount > _vertices.LargestFreeRange) { int newCapacity = CalculateGrowthCapacity( _vertices.Capacity, _vertices.TrailingFreeLength, vertexCount, VertexGrowthQuantum, MaximumVertexCapacity); allocationBytes = checked(allocationBytes + (long)newCapacity * VertexPositionNormalTexture.Size); copyBytes = checked(copyBytes + (long)_vertices.HighWaterMark * VertexPositionNormalTexture.Size); newBuffers++; } if (indexCount > _indices.LargestFreeRange) { int newCapacity = CalculateGrowthCapacity( _indices.Capacity, _indices.TrailingFreeLength, indexCount, IndexGrowthQuantum, MaximumIndexCapacity); allocationBytes = checked(allocationBytes + (long)newCapacity * sizeof(ushort)); copyBytes = checked(copyBytes + (long)_indices.HighWaterMark * sizeof(ushort)); newBuffers++; } return new GlobalMeshUploadPlan( checked((long)vertexCount * VertexPositionNormalTexture.Size + (long)indexCount * sizeof(ushort)), allocationBytes, copyBytes, newBuffers); } public GlobalMeshBuffer(GL gl) : this(gl, ImmediateGpuResourceRetirementQueue.Instance) { } internal GlobalMeshBuffer(GL gl, IGpuResourceRetirementQueue retirement) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); ArgumentNullException.ThrowIfNull(retirement); _retirementLedger = new GpuRetirementLedger(retirement); _vertices = new GpuRetiredRangeAllocator(InitialVertexCapacity, retirement); // ~32 MB _indices = new GpuRetiredRangeAllocator(InitialIndexCapacity, retirement); // ~6 MB InitBuffers(); } private unsafe void InitBuffers() { uint vao = 0; uint vbo = 0; uint ibo = 0; long vertexBytes = (long)_vertices.Capacity * VertexPositionNormalTexture.Size; long indexBytes = (long)_indices.Capacity * sizeof(ushort); bool vaoTracked = false; bool vertexTracked = false; bool indexTracked = false; try { _gl.GenVertexArrays(1, out vao); _gl.GenBuffers(1, out vbo); _gl.GenBuffers(1, out ibo); if (vao == 0 || vbo == 0 || ibo == 0) throw new InvalidOperationException("OpenGL did not create the global mesh-buffer objects."); _gl.BindVertexArray(vao); _gl.BindBuffer(GLEnum.ArrayBuffer, vbo); _gl.BufferData(GLEnum.ArrayBuffer, ToNativeSize(vertexBytes), null, GLEnum.StaticDraw); ConfigureVertexAttributes(); _gl.BindBuffer(GLEnum.ElementArrayBuffer, ibo); _gl.BufferData(GLEnum.ElementArrayBuffer, ToNativeSize(indexBytes), null, GLEnum.StaticDraw); GLHelpers.ThrowOnResourceError( _gl, $"creating global mesh buffers ({vertexBytes} vertex bytes, {indexBytes} index bytes)"); GlobalMeshVaoAccounting.TrackAllocation(); vaoTracked = true; GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer); GpuMemoryTracker.TrackAllocation(vertexBytes, GpuResourceType.Buffer); vertexTracked = true; GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer); GpuMemoryTracker.TrackAllocation(indexBytes, GpuResourceType.Buffer); indexTracked = true; VAO = vao; VBO = vbo; IBO = ibo; } catch { if (ibo != 0) _gl.DeleteBuffer(ibo); if (vbo != 0) _gl.DeleteBuffer(vbo); if (vao != 0) _gl.DeleteVertexArray(vao); if (indexTracked) { GpuMemoryTracker.TrackDeallocation(indexBytes, GpuResourceType.Buffer); GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer); } if (vertexTracked) { GpuMemoryTracker.TrackDeallocation(vertexBytes, GpuResourceType.Buffer); GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer); } if (vaoTracked) GlobalMeshVaoAccounting.TrackDeallocation(); throw; } finally { _gl.BindVertexArray(0); } } private unsafe void ConfigureVertexAttributes() { int stride = VertexPositionNormalTexture.Size; _gl.EnableVertexAttribArray(0); _gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0); _gl.EnableVertexAttribArray(1); _gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float))); _gl.EnableVertexAttribArray(2); _gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float))); } internal unsafe GlobalMeshAllocation UploadMesh( VertexPositionNormalTexture[] vertices, IReadOnlyList indexBatches) { ObjectDisposedException.ThrowIf(_disposed, this); _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); if (_migration is not null) throw new InvalidOperationException("A mesh upload cannot mutate the arena while a backing-buffer migration is in progress."); ArgumentNullException.ThrowIfNull(vertices); ArgumentNullException.ThrowIfNull(indexBatches); if (vertices.Length == 0) throw new ArgumentException("A global mesh allocation requires vertices.", nameof(vertices)); int totalIndices = 0; for (int i = 0; i < indexBatches.Count; i++) { ushort[] batch = indexBatches[i] ?? throw new ArgumentException("Index batches cannot contain null.", nameof(indexBatches)); totalIndices = checked(totalIndices + batch.Length); } if (totalIndices == 0) throw new ArgumentException("A global mesh allocation requires indices.", nameof(indexBatches)); MeshBufferRange vertexRange = AllocateVertices(vertices.Length); MeshBufferRange indexRange; try { indexRange = AllocateIndices(totalIndices); } catch { _vertices.ReleaseUnsubmitted(vertexRange); throw; } var firstIndices = new int[indexBatches.Count]; try { _gl.BindBuffer(GLEnum.ArrayBuffer, VBO); fixed (VertexPositionNormalTexture* ptr = vertices) { long vertexOffsetBytes = checked((long)vertexRange.Offset * VertexPositionNormalTexture.Size); long vertexUploadBytes = checked((long)vertices.Length * VertexPositionNormalTexture.Size); _gl.BufferSubData( GLEnum.ArrayBuffer, ToNativeOffset(vertexOffsetBytes), ToNativeSize(vertexUploadBytes), ptr); } // ElementArrayBuffer binding is VAO state. Use the neutral copy // target for staging so uploads cannot mutate whichever VAO the // preceding render pass happened to leave bound. _gl.BindBuffer(GLEnum.CopyWriteBuffer, IBO); int indexOffset = indexRange.Offset; for (int i = 0; i < indexBatches.Count; i++) { ushort[] batch = indexBatches[i]; firstIndices[i] = indexOffset; if (batch.Length > 0) { fixed (ushort* ptr = batch) { long indexOffsetBytes = checked((long)indexOffset * sizeof(ushort)); long indexUploadBytes = checked((long)batch.Length * sizeof(ushort)); _gl.BufferSubData( GLEnum.CopyWriteBuffer, ToNativeOffset(indexOffsetBytes), ToNativeSize(indexUploadBytes), ptr); } indexOffset = checked(indexOffset + batch.Length); } } GLHelpers.ThrowOnResourceError( _gl, $"uploading global mesh ({vertices.Length} vertices, {totalIndices} indices)"); } catch { _indices.ReleaseUnsubmitted(indexRange); _vertices.ReleaseUnsubmitted(vertexRange); throw; } UploadCount++; UploadedBytes = checked(UploadedBytes + checked((long)vertices.Length * VertexPositionNormalTexture.Size) + checked((long)totalIndices * sizeof(ushort))); return new GlobalMeshAllocation(vertexRange, indexRange, firstIndices); } internal void Release(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _indices.ReleaseAfterGpuUse(allocation.Indices); _vertices.ReleaseAfterGpuUse(allocation.Vertices); } // Narrow seams for ObjectMeshManager's per-resource retirement ledger. // The ordinary Release method remains the convenience API; a retryable // owner uses these seams so one accepted range is never submitted twice. internal void ReleaseIndexRange(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _indices.ReleaseAfterGpuUse(allocation.Indices); } internal void ReleaseVertexRange(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _vertices.ReleaseAfterGpuUse(allocation.Vertices); } /// /// Rolls back a mesh transaction which never published render data and /// therefore can never have been referenced by a submitted draw. /// internal void Abort(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _indices.ReleaseUnsubmitted(allocation.Indices); _vertices.ReleaseUnsubmitted(allocation.Vertices); } // Failed uploads were never submitted, so these matching seams return // each range immediately while preserving independent rollback progress. internal void AbortIndexRange(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _indices.ReleaseUnsubmitted(allocation.Indices); } internal void AbortVertexRange(GlobalMeshAllocation allocation) { ArgumentNullException.ThrowIfNull(allocation); _vertices.ReleaseUnsubmitted(allocation.Vertices); } private MeshBufferRange AllocateVertices(int count) { if (_vertices.TryAllocate(count, out MeshBufferRange allocation)) return allocation; throw new InvalidOperationException( "Vertex capacity was not migrated before the staged mesh upload was admitted."); } private MeshBufferRange AllocateIndices(int count) { if (_indices.TryAllocate(count, out MeshBufferRange allocation)) return allocation; throw new InvalidOperationException( "Index capacity was not migrated before the staged mesh upload was admitted."); } internal static int CalculateGrowthCapacity( int capacity, int trailingFreeLength, int requiredContiguousLength, int growthQuantum, int maximumCapacity = int.MaxValue) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); ArgumentOutOfRangeException.ThrowIfNegative(trailingFreeLength); ArgumentOutOfRangeException.ThrowIfGreaterThan(trailingFreeLength, capacity); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requiredContiguousLength); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(growthQuantum); ArgumentOutOfRangeException.ThrowIfLessThan(maximumCapacity, capacity); long missing = Math.Max(0L, (long)requiredContiguousLength - trailingFreeLength); if (missing == 0) return capacity; long minimum = checked((long)capacity + missing); if (minimum > maximumCapacity) throw new NotSupportedException( $"A contiguous range of {requiredContiguousLength:N0} elements exceeds the supported arena capacity {maximumCapacity:N0}."); // A 3:2 geometric destination amortizes the immutable-prefix copy. // Rounding only the missing tail caused every few uploads to allocate // another buffer and recopy the full prefix (quadratic route cost). long geometric = checked((long)capacity + Math.Max((long)growthQuantum, capacity / 2L)); long target = Math.Min(maximumCapacity, Math.Max(minimum, geometric)); return RoundUpToLimit(target, growthQuantum, maximumCapacity); } internal static bool TryCalculateTrimCapacity( int capacity, int highWaterMark, int initialCapacity, int growthQuantum, out int trimmedCapacity) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); ArgumentOutOfRangeException.ThrowIfNegative(highWaterMark); ArgumentOutOfRangeException.ThrowIfGreaterThan(highWaterMark, capacity); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(growthQuantum); trimmedCapacity = capacity; if (capacity <= initialCapacity) return false; // Capacity-based hysteresis, not a settle timer: retain 100% headroom // above the live tail and shrink only when the result is no more than // one third of the current arena. A destination revisit therefore has // to more than double its live prefix before growth can resume. long withHeadroom = checked( highWaterMark + Math.Max((long)growthQuantum, highWaterMark)); int target = Math.Max( initialCapacity, RoundUpToLimit(withHeadroom, growthQuantum, int.MaxValue)); if (target > capacity / 3) return false; trimmedCapacity = target; return true; } internal GlobalMeshCapacityResult EnsureUploadCapacity( int vertexCount, int indexCount, out GlobalMeshMaintenanceStep step) { ObjectDisposedException.ThrowIf(_disposed, this); _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); ArgumentOutOfRangeException.ThrowIfNegative(vertexCount); ArgumentOutOfRangeException.ThrowIfNegative(indexCount); if (vertexCount > MaximumVertexCapacity) throw new NotSupportedException( $"Mesh requires {vertexCount:N0} vertices; the supported per-arena maximum is {MaximumVertexCapacity:N0}."); if (indexCount > MaximumIndexCapacity) throw new NotSupportedException( $"Mesh requires {indexCount:N0} indices; the supported per-arena maximum is {MaximumIndexCapacity:N0}."); step = default; if (_migration is not null) return GlobalMeshCapacityResult.MigrationInProgress; if (vertexCount <= _vertices.LargestFreeRange && indexCount <= _indices.LargestFreeRange) { return GlobalMeshCapacityResult.Ready; } BufferKind kind; int targetCapacity; long copyBytes; if (vertexCount > _vertices.LargestFreeRange) { long minimum = checked( (long)_vertices.Capacity + Math.Max(0L, (long)vertexCount - _vertices.TrailingFreeLength)); if (minimum > MaximumVertexCapacity) return GlobalMeshCapacityResult.NeedsReclamation; kind = BufferKind.Vertices; targetCapacity = CalculateGrowthCapacity( _vertices.Capacity, _vertices.TrailingFreeLength, vertexCount, VertexGrowthQuantum, MaximumVertexCapacity); copyBytes = checked((long)_vertices.HighWaterMark * VertexPositionNormalTexture.Size); } else { long minimum = checked( (long)_indices.Capacity + Math.Max(0L, (long)indexCount - _indices.TrailingFreeLength)); if (minimum > MaximumIndexCapacity) return GlobalMeshCapacityResult.NeedsReclamation; kind = BufferKind.Indices; targetCapacity = CalculateGrowthCapacity( _indices.Capacity, _indices.TrailingFreeLength, indexCount, IndexGrowthQuantum, MaximumIndexCapacity); copyBytes = checked((long)_indices.HighWaterMark * sizeof(ushort)); } long newBytes = CapacityBytesFor(kind, targetCapacity); if (newBytes > MaximumPhysicalArenaBytes - PhysicalCapacityBytes) return GlobalMeshCapacityResult.NeedsReclamation; BeginMigration(kind, targetCapacity, copyBytes); step = new GlobalMeshMaintenanceStep(newBytes, 0, 1, false); return GlobalMeshCapacityResult.MigrationStarted; } /// /// Copies at most of the immutable /// live prefix into the staged backing store. The active VAO continues to /// reference the old store until the final chunk succeeds, then one atomic /// VAO rebind publishes the destination. /// internal GlobalMeshMaintenanceStep AdvanceMigration(long maximumCopyBytes) { ObjectDisposedException.ThrowIf(_disposed, this); _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumCopyBytes); BufferMigration? migration = _migration; if (migration is null) return default; long chunk = CalculateCopyChunk( migration.CopyBytes, migration.CopiedBytes, maximumCopyBytes); try { if (chunk != 0) { _gl.BindBuffer(GLEnum.CopyReadBuffer, migration.OldBuffer); _gl.BindBuffer(GLEnum.CopyWriteBuffer, migration.NewBuffer); _gl.CopyBufferSubData( GLEnum.CopyReadBuffer, GLEnum.CopyWriteBuffer, ToNativeOffset(migration.CopiedBytes), ToNativeOffset(migration.CopiedBytes), ToNativeSize(chunk)); GLHelpers.ThrowOnResourceError( _gl, $"migrating {migration.Kind} arena bytes " + $"{migration.CopiedBytes:N0}..{migration.CopiedBytes + chunk:N0}"); migration.CopiedBytes = checked(migration.CopiedBytes + chunk); } bool complete = migration.CopiedBytes == migration.CopyBytes; if (complete) CommitMigration(migration); return new GlobalMeshMaintenanceStep(0, chunk, 0, complete); } catch (Exception migrationError) { try { AbortMigration(migration); } catch (Exception abortError) { throw new AggregateException( "Global mesh migration failed and its staged buffer could not yet be released.", migrationError, abortError); } throw; } } /// /// Starts a staged shrink of one cold arena. Copy size no longer prevents /// reclamation: services any prefix over as /// many bounded frames as necessary. /// internal bool TryTrimUnusedTail(out GlobalMeshMaintenanceStep step) { ObjectDisposedException.ThrowIf(_disposed, this); _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); step = default; if (_migration is not null) return false; bool trimVertices = TryCalculateTrimCapacity( _vertices.Capacity, _vertices.HighWaterMark, InitialVertexCapacity, VertexGrowthQuantum, out int vertexCapacity); bool trimIndices = TryCalculateTrimCapacity( _indices.Capacity, _indices.HighWaterMark, InitialIndexCapacity, IndexGrowthQuantum, out int indexCapacity); long vertexSaving = trimVertices ? (long)(_vertices.Capacity - vertexCapacity) * VertexPositionNormalTexture.Size : 0; long indexSaving = trimIndices ? (long)(_indices.Capacity - indexCapacity) * sizeof(ushort) : 0; if (vertexSaving == 0 && indexSaving == 0) return false; BufferKind kind; int capacity; long copyBytes; if (vertexSaving >= indexSaving) { kind = BufferKind.Vertices; capacity = vertexCapacity; copyBytes = checked((long)_vertices.HighWaterMark * VertexPositionNormalTexture.Size); } else { kind = BufferKind.Indices; capacity = indexCapacity; copyBytes = checked((long)_indices.HighWaterMark * sizeof(ushort)); } long newBytes = CapacityBytesFor(kind, capacity); if (newBytes > MaximumPhysicalArenaBytes - PhysicalCapacityBytes) return false; BeginMigration(kind, capacity, copyBytes); step = new GlobalMeshMaintenanceStep(newBytes, 0, 1, false); return true; } internal static long CalculateCopyChunk(long totalBytes, long copiedBytes, long maximumCopyBytes) { ArgumentOutOfRangeException.ThrowIfNegative(totalBytes); ArgumentOutOfRangeException.ThrowIfNegative(copiedBytes); ArgumentOutOfRangeException.ThrowIfGreaterThan(copiedBytes, totalBytes); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumCopyBytes); return Math.Min(totalBytes - copiedBytes, maximumCopyBytes); } private unsafe void BeginMigration(BufferKind kind, int newCapacity, long copyBytes) { if (_migration is not null || _migrationAbort is not null) throw new InvalidOperationException("Only one global mesh backing buffer may migrate at a time."); int oldCapacity = kind == BufferKind.Vertices ? _vertices.Capacity : _indices.Capacity; uint oldBuffer = kind == BufferKind.Vertices ? VBO : IBO; long oldBytes = CapacityBytesFor(kind, oldCapacity); long newBytes = CapacityBytesFor(kind, newCapacity); uint newBuffer = 0; try { _gl.GenBuffers(1, out newBuffer); if (newBuffer == 0) throw new InvalidOperationException($"OpenGL did not create a staged {kind} arena buffer."); _gl.BindBuffer(GLEnum.CopyWriteBuffer, newBuffer); _gl.BufferData(GLEnum.CopyWriteBuffer, ToNativeSize(newBytes), null, GLEnum.StaticDraw); GLHelpers.ThrowOnResourceError( _gl, $"allocating staged {kind} arena buffer ({newBytes:N0} bytes)"); } catch { if (newBuffer != 0) _gl.DeleteBuffer(newBuffer); throw; } GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer); GpuMemoryTracker.TrackAllocation(newBytes, GpuResourceType.Buffer); _migration = new BufferMigration( kind, oldBuffer, newBuffer, oldCapacity, newCapacity, oldBytes, newBytes, copyBytes); } private void CommitMigration(BufferMigration migration) { try { _gl.BindVertexArray(VAO); if (migration.Kind == BufferKind.Vertices) { _gl.BindBuffer(GLEnum.ArrayBuffer, migration.NewBuffer); ConfigureVertexAttributes(); } else { _gl.BindBuffer(GLEnum.ElementArrayBuffer, migration.NewBuffer); } GLHelpers.ThrowOnResourceError(_gl, $"publishing staged {migration.Kind} arena buffer"); } catch { _gl.BindVertexArray(VAO); if (migration.Kind == BufferKind.Vertices) { _gl.BindBuffer(GLEnum.ArrayBuffer, migration.OldBuffer); ConfigureVertexAttributes(); } else { _gl.BindBuffer(GLEnum.ElementArrayBuffer, migration.OldBuffer); } _gl.BindVertexArray(0); throw; } finally { _gl.BindVertexArray(0); } if (migration.Kind == BufferKind.Vertices) { VBO = migration.NewBuffer; if (migration.NewCapacity > migration.OldCapacity) _vertices.Grow(migration.NewCapacity); else _vertices.Shrink(migration.NewCapacity); } else { IBO = migration.NewBuffer; if (migration.NewCapacity > migration.OldCapacity) _indices.Grow(migration.NewCapacity); else _indices.Shrink(migration.NewCapacity); } _migration = null; _retiredCapacityBytes = checked(_retiredCapacityBytes + migration.OldCapacityBytes); RetryableGpuResourceRelease oldBufferRelease = TrackedGlResource.CreateRetryableBufferDeletion( _gl, migration.OldBuffer, migration.OldCapacityBytes, $"retiring replaced global {migration.Kind} arena buffer {migration.OldBuffer}"); _retirementLedger.Retire(new RetryableGpuResourceRelease( oldBufferRelease.Run, () => _retiredCapacityBytes = checked( _retiredCapacityBytes - migration.OldCapacityBytes))); } private void AbortMigration(BufferMigration migration) { if (!ReferenceEquals(_migration, migration)) return; _migrationAbort ??= new GlobalMeshMigrationAbortTicket( migration.NewBuffer, migration.NewCapacityBytes, TrackedGlResource.CreateRetryableBufferDeletion( _gl, migration.NewBuffer, migration.NewCapacityBytes, $"aborting staged global {migration.Kind} arena buffer {migration.NewBuffer}")); RetryPendingMigrationAbort(); } private void RetryPendingMigrationAbort() { GlobalMeshMigrationAbortTicket? ticket = _migrationAbort; if (ticket is null) return; try { ticket.Advance(); } finally { if (ticket.IsComplete) { BufferMigration migration = _migration ?? throw new InvalidOperationException( "A staged-buffer abort ticket outlived its migration record."); if (migration.NewBuffer != ticket.Buffer || migration.NewCapacityBytes != ticket.CapacityBytes) { throw new InvalidOperationException( "A staged-buffer abort ticket no longer matches its migration record."); } _migrationAbort = null; _migration = null; } } } private static long CapacityBytesFor(BufferKind kind, int capacity) => kind switch { BufferKind.Vertices => checked((long)capacity * VertexPositionNormalTexture.Size), BufferKind.Indices => checked((long)capacity * sizeof(ushort)), _ => throw new ArgumentOutOfRangeException(nameof(kind)), }; private static int RoundUpToLimit(long value, int quantum, int maximum) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); long remainder = value % quantum; long rounded = remainder == 0 ? value : checked(value + quantum - remainder); if (rounded > maximum) rounded = maximum; return checked((int)rounded); } private static nint ToNativeOffset(long value) { ArgumentOutOfRangeException.ThrowIfNegative(value); if (IntPtr.Size == 4 && value > int.MaxValue) throw new NotSupportedException("The requested GPU byte offset exceeds this process's native pointer range."); return checked((nint)value); } private static nuint ToNativeSize(long value) { ArgumentOutOfRangeException.ThrowIfNegative(value); if (UIntPtr.Size == 4 && value > uint.MaxValue) throw new NotSupportedException("The requested GPU byte count exceeds this process's native pointer range."); return checked((nuint)value); } public void Dispose() { if (_disposed) return; _retirementLedger.RetryPendingPublications(); RetryPendingMigrationAbort(); if (_disposeResources is null) { var releases = new List<(string Name, Action Release)>(); if (_migration is { } migration) { RetryableGpuResourceRelease release = TrackedGlResource.CreateRetryableBufferDeletion( _gl, migration.NewBuffer, migration.NewCapacityBytes, $"deleting staged global {migration.Kind} arena buffer {migration.NewBuffer}"); releases.Add(("staged-migration-buffer", release.Run)); } if (VAO != 0) { RetryableGpuResourceRelease release = TrackedGlResource.CreateRetryableVertexArrayDeletion( _gl, VAO, $"deleting global mesh vertex array {VAO}", GlobalMeshVaoAccounting.TrackDeallocation); releases.Add(("global-vao", release.Run)); } if (VBO != 0) { RetryableGpuResourceRelease release = TrackedGlResource.CreateRetryableBufferDeletion( _gl, VBO, (long)_vertices.Capacity * VertexPositionNormalTexture.Size, $"deleting global mesh vertex buffer {VBO}"); releases.Add(("global-vbo", release.Run)); } if (IBO != 0) { RetryableGpuResourceRelease release = TrackedGlResource.CreateRetryableBufferDeletion( _gl, IBO, (long)_indices.Capacity * sizeof(ushort), $"deleting global mesh index buffer {IBO}"); releases.Add(("global-ibo", release.Run)); } _disposeResources = new RetryableResourceReleaseLedger(releases); } ResourceReleaseAttempt attempt = _disposeResources.Advance(); if (!_disposeResources.IsComplete) throw attempt.ToException( "One or more global mesh-buffer resources could not be released."); _migration = null; _migrationAbort = null; VAO = VBO = IBO = 0; _disposeResources = null; _disposed = true; if (attempt.HasFailures) throw attempt.ToException( "Global mesh-buffer resources released with exceptional committed outcomes."); } }