fix(streaming): retire stale portal-region entities
Port retail's 25-second leave-visibility lifetime over canonical live records, retaining spatially resident and owned entities while using the conservative ACE visibility envelope for nonresident records. Route expiry through the normal generation-safe F747 teardown so animations, effects, physics, and render owners unwind symmetrically. Replace the append-only modern mesh buffer with coalescing vertex/index ranges and upload each mesh's vertices once instead of once per material. Released zero-reference meshes can now reuse GPU ranges after portal and cache churn. A connected five-region round trip returned animation ownership to baseline, recreated the starting region on revisit, and held normal FPS. Release build succeeds and all 5,927 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
2cbf34a668
commit
3971997689
13 changed files with 918 additions and 117 deletions
117
src/AcDream.App/Rendering/Wb/ContiguousRangeAllocator.cs
Normal file
117
src/AcDream.App/Rendering/Wb/ContiguousRangeAllocator.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
internal readonly record struct MeshBufferRange(int Offset, int Length)
|
||||
{
|
||||
public int End => checked(Offset + Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Best-fit allocator for one element-addressed GPU buffer. Released ranges
|
||||
/// are coalesced immediately so ObjectMeshManager eviction returns storage to
|
||||
/// the shared modern-rendering buffers instead of only dropping cache keys.
|
||||
/// </summary>
|
||||
internal sealed class ContiguousRangeAllocator
|
||||
{
|
||||
private readonly List<MeshBufferRange> _free = new();
|
||||
|
||||
public ContiguousRangeAllocator(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
|
||||
Capacity = capacity;
|
||||
_free.Add(new MeshBufferRange(0, capacity));
|
||||
}
|
||||
|
||||
public int Capacity { get; private set; }
|
||||
public int Used { get; private set; }
|
||||
public int HighWaterMark { get; private set; }
|
||||
public int Free => Capacity - Used;
|
||||
public int LargestFreeRange => _free.Count == 0 ? 0 : _free.Max(range => range.Length);
|
||||
|
||||
public bool TryAllocate(int length, out MeshBufferRange allocation)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
|
||||
|
||||
int bestIndex = -1;
|
||||
int bestLength = int.MaxValue;
|
||||
for (int i = 0; i < _free.Count; i++)
|
||||
{
|
||||
int candidateLength = _free[i].Length;
|
||||
if (candidateLength >= length && candidateLength < bestLength)
|
||||
{
|
||||
bestIndex = i;
|
||||
bestLength = candidateLength;
|
||||
if (candidateLength == length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0)
|
||||
{
|
||||
allocation = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
MeshBufferRange free = _free[bestIndex];
|
||||
allocation = new MeshBufferRange(free.Offset, length);
|
||||
if (free.Length == length)
|
||||
_free.RemoveAt(bestIndex);
|
||||
else
|
||||
_free[bestIndex] = new MeshBufferRange(free.Offset + length, free.Length - length);
|
||||
|
||||
Used = checked(Used + length);
|
||||
HighWaterMark = Math.Max(HighWaterMark, allocation.End);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Grow(int newCapacity)
|
||||
{
|
||||
if (newCapacity <= Capacity)
|
||||
throw new ArgumentOutOfRangeException(nameof(newCapacity));
|
||||
|
||||
int oldCapacity = Capacity;
|
||||
Capacity = newCapacity;
|
||||
InsertAndCoalesce(new MeshBufferRange(oldCapacity, newCapacity - oldCapacity));
|
||||
}
|
||||
|
||||
public void Release(MeshBufferRange allocation)
|
||||
{
|
||||
if (allocation.Length <= 0
|
||||
|| allocation.Offset < 0
|
||||
|| allocation.End > Capacity)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(allocation));
|
||||
}
|
||||
|
||||
InsertAndCoalesce(allocation);
|
||||
Used = checked(Used - allocation.Length);
|
||||
}
|
||||
|
||||
private void InsertAndCoalesce(MeshBufferRange released)
|
||||
{
|
||||
int index = _free.BinarySearch(
|
||||
released,
|
||||
Comparer<MeshBufferRange>.Create((left, right) => left.Offset.CompareTo(right.Offset)));
|
||||
if (index < 0)
|
||||
index = ~index;
|
||||
|
||||
if (index > 0 && _free[index - 1].End > released.Offset)
|
||||
throw new InvalidOperationException("GPU buffer range was released more than once.");
|
||||
if (index < _free.Count && released.End > _free[index].Offset)
|
||||
throw new InvalidOperationException("GPU buffer range overlaps an existing free range.");
|
||||
|
||||
int start = released.Offset;
|
||||
int end = released.End;
|
||||
if (index > 0 && _free[index - 1].End == start)
|
||||
{
|
||||
start = _free[index - 1].Offset;
|
||||
_free.RemoveAt(--index);
|
||||
}
|
||||
if (index < _free.Count && end == _free[index].Offset)
|
||||
{
|
||||
end = _free[index].End;
|
||||
_free.RemoveAt(index);
|
||||
}
|
||||
|
||||
_free.Insert(index, new MeshBufferRange(start, end - start));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue