namespace AcDream.App.Rendering.Gpu.Gl; /// /// Pure bookkeeping for one flight slot's upload ring: an allocation cursor /// that only grows across a frame, plus a separate "dirty" watermark that /// tracks the byte range written since the last flush. /// /// This is deliberately GL-free so it can be unit tested without a live /// context. owns one instance per flight slot and /// pairs it with a managed staging byte[] and a real GL buffer; the /// staging array receives every write, /// and immediately before each Draw/DrawIndexed/ /// MultiDrawIndexedIndirect the device uploads exactly the dirty range /// via one glBufferSubData call and calls /// to reset the watermark. The allocation cursor itself only resets at /// (once per BeginFrame) — writes made after a /// flush simply extend the dirty range again, to be picked up by the next /// flush. This is what makes "flush before every draw, not at bind time" /// correct: a renderer that writes after binding still gets uploaded before /// its draw call runs. /// internal sealed class GlRingBufferState { private readonly int _capacityBytes; private uint _cursor; private int _dirtyStart = -1; private int _dirtyEnd = -1; public GlRingBufferState(int capacityBytes) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacityBytes); _capacityBytes = capacityBytes; } public int CapacityBytes => _capacityBytes; /// Bytes handed out since the last . public uint AllocatedBytes => _cursor; /// /// Reserves bytes aligned to /// , returning the aligned offset. /// Throws — rather than truncating — when the request would exceed the /// slot's capacity, because a silently shortened allocation would corrupt /// the frame invisibly. /// public uint Allocate(int byteCount, uint alignmentBytes) { ArgumentOutOfRangeException.ThrowIfNegative(byteCount); uint aligned = AlignUp(_cursor, alignmentBytes); long end = (long)aligned + byteCount; if (end > _capacityBytes) { throw new InvalidOperationException( $"Ring allocation of {byteCount} bytes at aligned offset {aligned} " + $"needs {end} bytes; the ring slot is {_capacityBytes} bytes. " + "Increase the per-slot ring capacity (GlGpuDevice's ringCapacityBytesPerSlot)."); } _cursor = (uint)end; if (byteCount > 0) MarkDirty((int)aligned, (int)end); return aligned; } /// Starts a new frame: rewinds the allocation cursor. Dirty state is untouched — a flush always runs before this is called. public void Reset() { _cursor = 0; _dirtyStart = -1; _dirtyEnd = -1; } /// /// Returns the byte range written since the last flush (start, length), /// or (0, 0) when nothing is dirty, and clears the watermark. /// public (int Start, int Length) TakeDirtyRange() { if (_dirtyStart < 0) return (0, 0); (int start, int length) = (_dirtyStart, _dirtyEnd - _dirtyStart); _dirtyStart = -1; _dirtyEnd = -1; return (start, length); } public bool HasDirtyBytes => _dirtyStart >= 0; private void MarkDirty(int start, int end) { _dirtyStart = _dirtyStart < 0 ? start : Math.Min(_dirtyStart, start); _dirtyEnd = Math.Max(_dirtyEnd, end); } internal static uint AlignUp(uint value, uint alignmentBytes) => alignmentBytes <= 1 ? value : (value + alignmentBytes - 1) / alignmentBytes * alignmentBytes; }