using Silk.NET.OpenGL; using AcDream.App.Rendering; using System.Runtime.ExceptionServices; namespace AcDream.App.Rendering.Wb; /// /// Always-on transaction boundary and accounting for raw dynamic GL objects. /// Growth keeps the published CPU capacity unchanged until BufferData succeeds; /// OpenGL leaves the previous data store intact when allocation reports an /// error, so the caller can continue using the old capacity or unwind. /// internal static unsafe class TrackedGlResource { public static RetryableGpuResourceRelease CreateRetryableBufferDeletion( GL gl, uint buffer, long capacityBytes, string context) => CreateRetryableBufferDeletion( gl, buffer, () => capacityBytes, context); public static RetryableGpuResourceRelease CreateRetryableBufferDeletion( GL gl, uint buffer, Func capacityBytes, string context) { if (buffer == 0) throw new ArgumentOutOfRangeException(nameof(buffer)); ArgumentNullException.ThrowIfNull(capacityBytes); return new RetryableGpuResourceRelease( () => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"), () => { gl.DeleteBuffer(buffer); // Per the GL error contract, a command which generates an // error does not change object state. Keep mutation and its // validation in one retryable stage so that failed deletion // is issued again, while later accounting remains untouched. GLHelpers.ThrowOnResourceError(gl, context); }, () => { long bytes = capacityBytes(); ArgumentOutOfRangeException.ThrowIfNegative(bytes); if (bytes != 0) GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer); }, () => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer)); } public static RetryableGpuResourceRelease CreateRetryableVertexArrayDeletion( GL gl, uint vertexArray, string context, Action? trackDeallocation = null) { if (vertexArray == 0) throw new ArgumentOutOfRangeException(nameof(vertexArray)); return new RetryableGpuResourceRelease( () => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"), () => { gl.DeleteVertexArray(vertexArray); GLHelpers.ThrowOnResourceError(gl, context); }, trackDeallocation ?? (() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO))); } public static void AllocateBufferStorage( GL gl, BufferTargetARB target, uint buffer, long previousBytes, long newBytes, BufferUsageARB usage, string context) => AllocateBufferStorage( gl, (GLEnum)target, buffer, previousBytes, newBytes, (GLEnum)usage, context); public static void AllocateBufferStorage( GL gl, BufferTargetARB target, uint buffer, long previousBytes, long newBytes, BufferUsageARB usage, void* data, string context) => AllocateBufferStorage( gl, (GLEnum)target, buffer, previousBytes, newBytes, (GLEnum)usage, data, context); public static uint CreateBuffer(GL gl, string context) { return CreateTrackedName( gl, "buffer", context, gl.GenBuffer, name => GlResourceCommand.DeleteBuffer( gl, name, $"rollback buffer {name} after failed {context}"), () => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer)); } public static uint CreateVertexArray(GL gl, string context) { return CreateTrackedName( gl, "vertex-array", context, gl.GenVertexArray, name => GlResourceCommand.DeleteVertexArray( gl, name, $"rollback vertex array {name} after failed {context}"), () => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO)); } private static uint CreateTrackedName( GL gl, string resourceName, string context, Func create, Action rollback, Action publishAccounting) { uint name = GlResourceCommand.CreateName( gl, $"{resourceName} for {context}", create, rollback); var cleanup = new ResourceCleanupGroup(); cleanup.Add($"{resourceName} name {name}", () => rollback(name)); try { publishAccounting(); return name; } catch (Exception publicationFailure) { try { cleanup.RetryCleanup(); } catch (Exception cleanupFailure) { throw new GlResourceConstructionException( $"Publishing {resourceName} accounting failed and GL name {name} could not be released.", cleanup, [publicationFailure, cleanupFailure]); } ExceptionDispatchInfo.Capture(publicationFailure).Throw(); throw new InvalidOperationException("Unreachable resource-publication path."); } } public static void AllocateBufferStorage( GL gl, GLEnum target, uint buffer, long previousBytes, long newBytes, GLEnum usage, string context) => AllocateBufferStorage( gl, target, buffer, previousBytes, newBytes, usage, null, context); public static void AllocateBufferStorage( GL gl, GLEnum target, uint buffer, long previousBytes, long newBytes, GLEnum usage, void* data, string context) { ArgumentOutOfRangeException.ThrowIfNegative(previousBytes); ArgumentOutOfRangeException.ThrowIfLessThan(newBytes, 1); if (buffer == 0) throw new ArgumentOutOfRangeException(nameof(buffer)); GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); gl.BindBuffer(target, buffer); gl.BufferData(target, checked((nuint)newBytes), data, usage); GLHelpers.ThrowOnResourceError(gl, context); long delta = checked(newBytes - previousBytes); if (delta > 0) GpuMemoryTracker.TrackAllocation(delta, GpuResourceType.Buffer); else if (delta < 0) GpuMemoryTracker.TrackDeallocation(-delta, GpuResourceType.Buffer); } public static void UpdateBufferSubData( GL gl, BufferTargetARB target, uint buffer, nint byteOffset, long byteCount, void* data, string context) => UpdateBufferSubData( gl, (GLEnum)target, buffer, byteOffset, byteCount, data, context); public static void UpdateBufferSubData( GL gl, GLEnum target, uint buffer, nint byteOffset, long byteCount, void* data, string context) { ArgumentOutOfRangeException.ThrowIfNegative(byteOffset); ArgumentOutOfRangeException.ThrowIfLessThan(byteCount, 1); if (buffer == 0) throw new ArgumentOutOfRangeException(nameof(buffer)); ArgumentNullException.ThrowIfNull(data); GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); gl.BindBuffer(target, buffer); gl.BufferSubData(target, byteOffset, checked((nuint)byteCount), data); GLHelpers.ThrowOnResourceError(gl, context); } public static void DeleteBuffer(GL gl, uint buffer, long capacityBytes, string context) { if (buffer == 0) return; ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes); GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); gl.DeleteBuffer(buffer); GLHelpers.ThrowOnResourceError(gl, context); if (capacityBytes != 0) GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer); GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer); } public static void DeleteVertexArray(GL gl, uint vertexArray, string context) { if (vertexArray == 0) return; GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); gl.DeleteVertexArray(vertexArray); GLHelpers.ThrowOnResourceError(gl, context); GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO); } }