using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlRingBufferStateTests
{
[Fact]
public void FirstAllocationStartsAtZero()
{
var state = new GlRingBufferState(1024);
uint offset = state.Allocate(64, alignmentBytes: 16);
Assert.Equal(0u, offset);
Assert.Equal(64u, state.AllocatedBytes);
}
[Fact]
public void SubsequentAllocationsAlignUpToTheRequestedBoundary()
{
var state = new GlRingBufferState(1024);
state.Allocate(12, alignmentBytes: 4);
uint second = state.Allocate(64, alignmentBytes: 256);
Assert.Equal(0u, second % 256);
Assert.True(second >= 12);
}
[Fact]
public void AllocationsMergeIntoOneDirtyRange()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.Allocate(32, alignmentBytes: 4);
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(0, start);
Assert.Equal(48, length);
}
[Fact]
public void TakingTheDirtyRangeClearsItUntilTheNextWrite()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.TakeDirtyRange();
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(0, start);
Assert.Equal(0, length);
Assert.False(state.HasDirtyBytes);
}
[Fact]
public void WritesAfterAFlushExtendANewDirtyRangeWithoutRewindingTheCursor()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.TakeDirtyRange();
uint secondOffset = state.Allocate(8, alignmentBytes: 4);
Assert.Equal(16u, secondOffset);
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(16, start);
Assert.Equal(8, length);
}
///
/// The precondition for issuing the ring's writes with
/// GL_MAP_UNSYNCHRONIZED_BIT: within one frame, no flush may cover a byte an
/// earlier flush already handed to the GPU. This walks a frame's worth of
/// mixed-alignment allocations and asserts the ranges are disjoint and
/// ascending.
///
[Fact]
public void SuccessiveDirtyRangesWithinAFrameNeverOverlap()
{
var state = new GlRingBufferState(64 * 1024);
int previousEnd = 0;
foreach (int size in new[] { 100, 4, 1024, 36, 7, 512, 3 })
{
state.Allocate(size, alignmentBytes: 256);
state.Allocate(size, alignmentBytes: 4);
(int start, int length) = state.TakeDirtyRange();
Assert.True(
start >= previousEnd,
$"flush [{start}, {start + length}) reaches below the already-flushed {previousEnd} bytes");
previousEnd = start + length;
Assert.Equal(previousEnd, state.FlushedEndBytes);
}
}
[Fact]
public void AWriteBelowTheFlushedHighWaterMarkIsRefused()
{
var state = new GlRingBufferState(1024);
state.Allocate(128, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(128, state.FlushedEndBytes);
InvalidOperationException error = Assert.Throws(
() => state.MarkDirty(64, 96));
Assert.Contains("GL_MAP_UNSYNCHRONIZED_BIT", error.Message, StringComparison.Ordinal);
}
[Fact]
public void FlushedHighWaterMarkOnlyAdvancesWhenSomethingWasFlushed()
{
var state = new GlRingBufferState(1024);
state.Allocate(48, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(48, state.FlushedEndBytes);
// A draw with nothing newly written must not move the mark, or the next
// allocation's guard would compare against a number no flush produced.
state.TakeDirtyRange();
Assert.Equal(48, state.FlushedEndBytes);
}
[Fact]
public void ResetClearsTheFlushedHighWaterMarkSoTheSlotIsWritableAgain()
{
var state = new GlRingBufferState(1024);
state.Allocate(256, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(256, state.FlushedEndBytes);
// BeginFrame has waited on this slot's fence by the time Reset runs, so
// the whole slot is writable from zero again.
state.Reset();
Assert.Equal(0, state.FlushedEndBytes);
Assert.Equal(0u, state.Allocate(64, alignmentBytes: 4));
}
[Fact]
public void ResetRewindsTheCursorAndClearsDirtyState()
{
var state = new GlRingBufferState(1024);
state.Allocate(64, alignmentBytes: 4);
state.Reset();
Assert.Equal(0u, state.AllocatedBytes);
Assert.False(state.HasDirtyBytes);
Assert.Equal(0u, state.Allocate(4, alignmentBytes: 4));
}
[Fact]
public void OverCapacityRequestThrowsRatherThanTruncating()
{
var state = new GlRingBufferState(64);
Assert.Throws(() => state.Allocate(128, alignmentBytes: 4));
}
[Fact]
public void AlignmentPushingPastCapacityAlsoThrows()
{
var state = new GlRingBufferState(64);
state.Allocate(60, alignmentBytes: 4);
Assert.Throws(() => state.Allocate(8, alignmentBytes: 4));
}
[Theory]
[InlineData(0u, 256u, 0u)]
[InlineData(1u, 256u, 256u)]
[InlineData(255u, 256u, 256u)]
[InlineData(256u, 256u, 256u)]
[InlineData(10u, 1u, 10u)]
public void AlignUpMatchesStandardAlignmentArithmetic(uint value, uint alignment, uint expected)
{
Assert.Equal(expected, GlRingBufferState.AlignUp(value, alignment));
}
[Fact]
public void ZeroByteAllocationDoesNotMarkAnythingDirty()
{
var state = new GlRingBufferState(1024);
state.Allocate(0, alignmentBytes: 4);
Assert.False(state.HasDirtyBytes);
}
}