phase(N.5b) Task 2: TerrainSlotAllocator + tests
Pure-CPU slot allocator for the terrain modern dispatcher's global VBO/EBO. FIFO free-list + monotonic counter, mirroring WB's TerrainRenderManager pattern. Caller (TerrainModernRenderer) handles GPU buffer growth when Allocate sets needsGrow=true. 8 unit tests cover: fresh-allocator returns slot 0, sequential allocs, free+alloc reuse, FIFO ordering, needsGrow signaling on capacity overflow, GrowTo, LoadedCount tracking, and double-free detection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db0f010544
commit
ba852993e9
2 changed files with 164 additions and 0 deletions
76
src/AcDream.Core/Terrain/TerrainSlotAllocator.cs
Normal file
76
src/AcDream.Core/Terrain/TerrainSlotAllocator.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Terrain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure-CPU slot allocator for the terrain modern dispatcher's global VBO/EBO.
|
||||||
|
/// One slot = one landblock's worth of mesh data (384 verts + 384 indices).
|
||||||
|
/// Uses a FIFO free-list for slot recycling and a monotonic counter for
|
||||||
|
/// first-time growth, mirroring WorldBuilder's TerrainRenderManager pattern.
|
||||||
|
/// All bookkeeping is CPU-side; the GPU buffer growth itself is performed
|
||||||
|
/// by TerrainModernRenderer when <see cref="Allocate"/> sets needsGrow=true.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TerrainSlotAllocator
|
||||||
|
{
|
||||||
|
private readonly Queue<int> _freeSlots = new();
|
||||||
|
private readonly HashSet<int> _liveSlots = new();
|
||||||
|
private int _nextFreeSlot;
|
||||||
|
private int _capacity;
|
||||||
|
|
||||||
|
public TerrainSlotAllocator(int initialCapacity = 64)
|
||||||
|
{
|
||||||
|
if (initialCapacity <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(initialCapacity), "must be > 0");
|
||||||
|
_capacity = initialCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Current capacity in slots. Growable via <see cref="GrowTo"/>.</summary>
|
||||||
|
public int Capacity => _capacity;
|
||||||
|
|
||||||
|
/// <summary>Slots currently in use (allocated minus freed).</summary>
|
||||||
|
public int LoadedCount => _liveSlots.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocate a slot index. Reuses a freed slot via FIFO if available,
|
||||||
|
/// otherwise hands out the next monotonic index. Sets
|
||||||
|
/// <paramref name="needsGrow"/> to true when the returned slot index is
|
||||||
|
/// at or beyond current capacity — caller must <see cref="GrowTo"/>
|
||||||
|
/// before using the slot.
|
||||||
|
/// </summary>
|
||||||
|
public int Allocate(out bool needsGrow)
|
||||||
|
{
|
||||||
|
int slot;
|
||||||
|
if (_freeSlots.TryDequeue(out var freed))
|
||||||
|
{
|
||||||
|
slot = freed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
slot = _nextFreeSlot++;
|
||||||
|
}
|
||||||
|
_liveSlots.Add(slot);
|
||||||
|
needsGrow = slot >= _capacity;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a slot to the free list. Throws if the slot wasn't currently
|
||||||
|
/// allocated (catches double-free bugs).
|
||||||
|
/// </summary>
|
||||||
|
public void Free(int slot)
|
||||||
|
{
|
||||||
|
if (!_liveSlots.Remove(slot))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Slot {slot} was not allocated (double-free or unknown slot).");
|
||||||
|
_freeSlots.Enqueue(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Update capacity counter after the caller has grown the GPU buffers.</summary>
|
||||||
|
public void GrowTo(int newCapacity)
|
||||||
|
{
|
||||||
|
if (newCapacity < _capacity)
|
||||||
|
throw new ArgumentException("Capacity can only grow", nameof(newCapacity));
|
||||||
|
_capacity = newCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
using AcDream.Core.Terrain;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Terrain;
|
||||||
|
|
||||||
|
public class TerrainSlotAllocatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Allocate_FromFreshAllocator_ReturnsZero()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
Assert.Equal(0, alloc.Allocate(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Allocate_TwoTimes_ReturnsZeroThenOne()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
Assert.Equal(0, alloc.Allocate(out _));
|
||||||
|
Assert.Equal(1, alloc.Allocate(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FreeThenAllocate_ReusesFreedSlot()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
var s0 = alloc.Allocate(out _);
|
||||||
|
var s1 = alloc.Allocate(out _);
|
||||||
|
alloc.Free(s0);
|
||||||
|
Assert.Equal(s0, alloc.Allocate(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FreeOrderedFreshAllocs_ReturnsInFifoOrder()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
var s0 = alloc.Allocate(out _);
|
||||||
|
var s1 = alloc.Allocate(out _);
|
||||||
|
var s2 = alloc.Allocate(out _);
|
||||||
|
alloc.Free(s0);
|
||||||
|
alloc.Free(s2);
|
||||||
|
Assert.Equal(s0, alloc.Allocate(out _));
|
||||||
|
Assert.Equal(s2, alloc.Allocate(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Allocate_BeyondInitialCapacity_SignalsNeedsGrow()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 2);
|
||||||
|
alloc.Allocate(out var grow0);
|
||||||
|
alloc.Allocate(out var grow1);
|
||||||
|
alloc.Allocate(out var grow2);
|
||||||
|
Assert.False(grow0);
|
||||||
|
Assert.False(grow1);
|
||||||
|
Assert.True(grow2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GrowTo_DoublesCapacityCorrectly()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 4);
|
||||||
|
alloc.GrowTo(8);
|
||||||
|
Assert.Equal(8, alloc.Capacity);
|
||||||
|
alloc.GrowTo(64);
|
||||||
|
Assert.Equal(64, alloc.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadedCount_TracksAllocAndFree()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
Assert.Equal(0, alloc.LoadedCount);
|
||||||
|
var s0 = alloc.Allocate(out _);
|
||||||
|
var s1 = alloc.Allocate(out _);
|
||||||
|
Assert.Equal(2, alloc.LoadedCount);
|
||||||
|
alloc.Free(s0);
|
||||||
|
Assert.Equal(1, alloc.LoadedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Free_TwiceForSameSlot_Throws()
|
||||||
|
{
|
||||||
|
var alloc = new TerrainSlotAllocator(initialCapacity: 8);
|
||||||
|
var s0 = alloc.Allocate(out _);
|
||||||
|
alloc.Free(s0);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => alloc.Free(s0));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue