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