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;
}