fix(rendering): bound portal resource lifetime

Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -39,7 +39,10 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
/// anisotropic level mid-session via <see cref="TerrainAtlas.SetAnisotropic"/>.</summary>
public TerrainAtlas Atlas => _atlas;
private readonly TerrainSlotAllocator _alloc;
private readonly GpuRetiredTerrainSlotAllocator _alloc;
private readonly GpuRetirementLedger _retirementLedger;
private RetryableResourceReleaseLedger? _disposeResources;
private bool _disposed;
// Per-slot live data (index by slot integer; null entries are unused slots).
private SlotData?[] _slots;
@ -51,14 +54,31 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
private uint _globalVao;
private uint _globalVbo;
private uint _globalEbo;
private long _globalVboCapacityBytes;
private long _globalEboCapacityBytes;
private uint _indirectBuffer;
private int _indirectCapacity;
private sealed class DynamicIndirectBuffer
{
public uint Buffer;
public int Capacity;
}
private readonly List<DynamicIndirectBuffer>[] _indirectBuffersByFrame =
[[], [], []];
private int _dynamicFrameSlot;
private int _dynamicBufferCursor;
private bool _dynamicFrameStarted;
internal int DynamicIndirectBufferCount =>
_indirectBuffersByFrame.Sum(frameBuffers => frameBuffers.Count);
// Phase U.3: terrain clip UBO (binding=2, terrain_modern.vert TerrainClip).
// The shared one is created + uploaded by the GameWindow-level ClipFrame and
// handed in via SetClipUbo. When 0, we bind a lazily-created no-clip fallback
// (count 0 = ungated) so the shader never reads an unbound UBO at binding=2.
private uint _sharedClipUbo;
private TerrainClipBufferBinding _sharedClipBinding;
private uint _fallbackClipUbo;
// Cached uvec2-handle uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
@ -91,12 +111,31 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
Shader shader,
TerrainAtlas atlas,
int initialSlotCapacity = 64)
: this(
gl,
bindless,
shader,
atlas,
ImmediateGpuResourceRetirementQueue.Instance,
initialSlotCapacity)
{
}
internal TerrainModernRenderer(
GL gl,
BindlessSupport bindless,
Shader shader,
TerrainAtlas atlas,
IGpuResourceRetirementQueue resourceRetirement,
int initialSlotCapacity = 64)
{
_gl = gl;
_bindless = bindless;
_shader = shader;
_atlas = atlas;
_alloc = new TerrainSlotAllocator(initialSlotCapacity);
ArgumentNullException.ThrowIfNull(resourceRetirement);
_retirementLedger = new GpuRetirementLedger(resourceRetirement);
_alloc = new GpuRetiredTerrainSlotAllocator(initialSlotCapacity, resourceRetirement);
_slots = new SlotData?[initialSlotCapacity];
_uTerrainHandleLoc = _gl.GetUniformLocation(_shader.Program, "uTerrainHandle");
@ -105,22 +144,105 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
if (_uTexTilingLoc < 0)
throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform.");
_globalVao = _gl.GenVertexArray();
_globalVbo = _gl.GenBuffer();
_globalEbo = _gl.GenBuffer();
AllocateGpuBuffers(initialSlotCapacity);
ConfigureVao();
try
{
_globalVao = TrackedGlResource.CreateVertexArray(
_gl,
"creating terrain global VAO");
_globalVbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global vertex buffer");
_globalEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global index buffer");
AllocateGpuBuffers(initialSlotCapacity);
ConfigureVao(_globalVao, _globalVbo, _globalEbo);
}
catch
{
TrackedGlResource.DeleteVertexArray(
_gl,
_globalVao,
"rolling back terrain global VAO");
TrackedGlResource.DeleteBuffer(
_gl,
_globalVbo,
_globalVboCapacityBytes,
"rolling back terrain global vertex buffer");
TrackedGlResource.DeleteBuffer(
_gl,
_globalEbo,
_globalEboCapacityBytes,
"rolling back terrain global index buffer");
_globalVao = 0;
_globalVbo = 0;
_globalEbo = 0;
_globalVboCapacityBytes = 0;
_globalEboCapacityBytes = 0;
throw;
}
_indirectBuffer = _gl.GenBuffer();
}
/// <summary>
/// Phase U.3: hand the renderer the SHARED terrain-clip UBO (binding=2)
/// created by <see cref="ClipFrame.UploadShared"/>. The renderer binds it to
/// binding=2 before its draw. Pass 0 to fall back to the internal no-clip UBO
/// (count 0 = ungated terrain).
/// Resets the indirect-command submission cursor for a GPU-fenced frame
/// slot. A retail outside view may draw terrain more than once in a frame;
/// each draw receives storage that cannot overwrite an earlier command.
/// </summary>
public void SetClipUbo(uint sharedClipUbo) => _sharedClipUbo = sharedClipUbo;
public void BeginFrame(int frameSlot)
{
if ((uint)frameSlot >= (uint)_indirectBuffersByFrame.Length)
throw new ArgumentOutOfRangeException(nameof(frameSlot));
_retirementLedger.RetryPendingPublications();
_dynamicFrameSlot = frameSlot;
_dynamicBufferCursor = 0;
_dynamicFrameStarted = true;
}
private void ActivateNextIndirectBuffer()
{
if (!_dynamicFrameStarted)
throw new InvalidOperationException("BeginFrame must be called before drawing terrain.");
List<DynamicIndirectBuffer> frameBuffers = _indirectBuffersByFrame[_dynamicFrameSlot];
if (_dynamicBufferCursor == frameBuffers.Count)
{
uint buffer = TrackedGlResource.CreateBuffer(
_gl,
$"creating terrain indirect buffer for frame slot {_dynamicFrameSlot}");
try
{
frameBuffers.Add(new DynamicIndirectBuffer { Buffer = buffer });
}
catch
{
TrackedGlResource.DeleteBuffer(
_gl,
buffer,
0,
"rolling back terrain indirect buffer");
throw;
}
}
DynamicIndirectBuffer active = frameBuffers[_dynamicBufferCursor++];
_indirectBuffer = active.Buffer;
_indirectCapacity = active.Capacity;
}
private void PersistIndirectCapacity()
{
_indirectBuffersByFrame[_dynamicFrameSlot][_dynamicBufferCursor - 1].Capacity =
_indirectCapacity;
}
/// <summary>
/// Hand the renderer the current aligned terrain-clip range (binding=2).
/// Each outside-view slice occupies a distinct range in the current
/// GPU-fenced frame's UBO arena.
/// </summary>
public void SetClipUbo(TerrainClipBufferBinding sharedClipBinding) =>
_sharedClipBinding = sharedClipBinding;
/// <summary>
/// Two-tier streaming entry point. Accepts a prebuilt mesh from
@ -146,15 +268,21 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
$"Expected {IndicesPerLandblock} indices, got {meshData.Indices.Length}",
nameof(meshData));
if (_idToSlot.ContainsKey(landblockId))
RemoveLandblock(landblockId);
// A prior replacement may have committed the logical slot switch
// before queue publication failed. Retry those retained physical-slot
// transactions before allocating more terrain storage.
_alloc.RetryPendingPublications();
bool replacing = _idToSlot.TryGetValue(landblockId, out int replacedSlot);
int slot = _alloc.Allocate(out var needsGrow);
if (needsGrow)
bool published = false;
try
{
int newCap = Math.Max(_alloc.Capacity * 2, slot + 1);
EnsureCapacity(newCap);
}
if (needsGrow)
{
int newCap = Math.Max(_alloc.Capacity * 2, slot + 1);
EnsureCapacity(newCap);
}
// Bake worldOrigin into vertex positions; capture min/max Z for AABB.
var bakedVerts = new TerrainVertex[VertsPerLandblock];
@ -179,41 +307,66 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
nint vboByteOffset = (nint)(slot * VertsPerLandblock * VertexSize);
nint eboByteOffset = (nint)(slot * IndicesPerLandblock * IndexSize);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo);
fixed (TerrainVertex* p = bakedVerts)
{
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, vboByteOffset,
(nuint)(VertsPerLandblock * VertexSize), p);
}
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
fixed (TerrainVertex* p = bakedVerts)
{
TrackedGlResource.UpdateBufferSubData(
_gl,
BufferTargetARB.ArrayBuffer,
_globalVbo,
vboByteOffset,
VertsPerLandblock * VertexSize,
p,
$"uploading terrain vertices for 0x{landblockId:X8}");
}
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo);
fixed (uint* p = bakedIndices)
{
_gl.BufferSubData(BufferTargetARB.ElementArrayBuffer, eboByteOffset,
(nuint)(IndicesPerLandblock * IndexSize), p);
}
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
fixed (uint* p = bakedIndices)
{
TrackedGlResource.UpdateBufferSubData(
_gl,
BufferTargetARB.ElementArrayBuffer,
_globalEbo,
eboByteOffset,
IndicesPerLandblock * IndexSize,
p,
$"uploading terrain indices for 0x{landblockId:X8}");
}
_slots[slot] = new SlotData
_slots[slot] = new SlotData
{
LandblockId = landblockId,
WorldOrigin = worldOrigin,
FirstIndex = (uint)(slot * IndicesPerLandblock),
IndexCount = IndicesPerLandblock,
AabbMin = new Vector3(worldOrigin.X, worldOrigin.Y, zMin),
AabbMax = new Vector3(worldOrigin.X + LandblockSize, worldOrigin.Y + LandblockSize, zMax),
};
_idToSlot[landblockId] = slot;
published = true;
if (replacing)
{
_slots[replacedSlot] = null;
_alloc.FreeAfterGpuUse(replacedSlot);
}
}
finally
{
LandblockId = landblockId,
WorldOrigin = worldOrigin,
FirstIndex = (uint)(slot * IndicesPerLandblock),
IndexCount = IndicesPerLandblock,
AabbMin = new Vector3(worldOrigin.X, worldOrigin.Y, zMin),
AabbMax = new Vector3(worldOrigin.X + LandblockSize, worldOrigin.Y + LandblockSize, zMax),
};
_idToSlot[landblockId] = slot;
if (!published)
_alloc.ReleaseUnsubmitted(slot);
}
}
public void RemoveLandblock(uint landblockId)
{
// Removal clears the logical lookup before retirement publication. A
// retry therefore has to advance retained publications even when the
// landblock is no longer present in the map.
_alloc.RetryPendingPublications();
if (!_idToSlot.TryGetValue(landblockId, out var slot))
return;
_idToSlot.Remove(landblockId);
_slots[slot] = null;
_alloc.Free(slot);
_alloc.FreeAfterGpuUse(slot);
// No GPU clear: the per-frame DEIC array won't reference this slot.
}
@ -252,6 +405,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
ndcClipAabb);
}
if (_visibleSlots.Count == 0) return;
ActivateNextIndirectBuffer();
// Build DEIC array.
if (_deicScratch.Length < _visibleSlots.Count)
@ -272,23 +426,31 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
// Grow indirect buffer if needed.
if (_visibleSlots.Count > _indirectCapacity)
{
_indirectCapacity = Math.Max(64, _visibleSlots.Count * 2);
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _indirectBuffer);
_gl.BufferData(GLEnum.DrawIndirectBuffer,
(nuint)(_indirectCapacity * sizeof(DrawElementsIndirectCommand)),
null, GLEnum.DynamicDraw);
}
else
{
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _indirectBuffer);
int grownCapacity = Math.Max(64, _visibleSlots.Count * 2);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.DrawIndirectBuffer,
_indirectBuffer,
checked((long)_indirectCapacity * sizeof(DrawElementsIndirectCommand)),
checked((long)grownCapacity * sizeof(DrawElementsIndirectCommand)),
GLEnum.DynamicDraw,
"growing terrain indirect command buffer");
_indirectCapacity = grownCapacity;
}
// Upload DEIC array.
fixed (DrawElementsIndirectCommand* p = _deicScratch)
{
_gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0,
(nuint)(_visibleSlots.Count * sizeof(DrawElementsIndirectCommand)), p);
TrackedGlResource.UpdateBufferSubData(
_gl,
GLEnum.DrawIndirectBuffer,
_indirectBuffer,
0,
checked((long)_visibleSlots.Count * sizeof(DrawElementsIndirectCommand)),
p,
"uploading terrain indirect commands");
}
PersistIndirectCapacity();
// Bind shader + uniforms + atlas handles.
// Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj;
@ -352,11 +514,96 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
public void Dispose()
{
_gl.DeleteVertexArray(_globalVao);
_gl.DeleteBuffer(_globalVbo);
_gl.DeleteBuffer(_globalEbo);
_gl.DeleteBuffer(_indirectBuffer);
if (_fallbackClipUbo != 0) { _gl.DeleteBuffer(_fallbackClipUbo); _fallbackClipUbo = 0; } // Phase U.3
if (_disposed)
return;
_retirementLedger.RetryPendingPublications();
if (_disposeResources is null)
{
var releases = new List<(string Name, Action Release)>();
if (_globalVao != 0)
{
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
_globalVao,
"deleting terrain global VAO");
releases.Add(("global-vao", release.Run));
}
if (_globalVbo != 0)
{
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
_globalVbo,
_globalVboCapacityBytes,
"deleting terrain global vertex buffer");
releases.Add(("global-vbo", release.Run));
}
if (_globalEbo != 0)
{
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
_globalEbo,
_globalEboCapacityBytes,
"deleting terrain global index buffer");
releases.Add(("global-ebo", release.Run));
}
for (int frame = 0; frame < _indirectBuffersByFrame.Length; frame++)
{
List<DynamicIndirectBuffer> frameBuffers = _indirectBuffersByFrame[frame];
for (int index = 0; index < frameBuffers.Count; index++)
{
DynamicIndirectBuffer buffer = frameBuffers[index];
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
buffer.Buffer,
checked((long)buffer.Capacity * sizeof(DrawElementsIndirectCommand)),
"deleting terrain indirect command buffer");
releases.Add(($"indirect-{frame}-{index}", release.Run));
}
}
if (_fallbackClipUbo != 0)
{
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
_fallbackClipUbo,
ClipFrame.TerrainUboBytes,
"deleting terrain fallback clip UBO");
releases.Add(("fallback-clip-ubo", release.Run));
}
_disposeResources = new RetryableResourceReleaseLedger(releases);
}
ResourceReleaseAttempt attempt = _disposeResources.Advance();
if (!_disposeResources.IsComplete)
{
throw attempt.ToException(
"One or more terrain GPU resources could not be released.");
}
_globalVao = 0;
_globalVbo = 0;
_globalEbo = 0;
_globalVboCapacityBytes = 0;
_globalEboCapacityBytes = 0;
foreach (List<DynamicIndirectBuffer> frameBuffers in _indirectBuffersByFrame)
frameBuffers.Clear();
_indirectBuffer = 0;
_indirectCapacity = 0;
_dynamicFrameStarted = false;
_fallbackClipUbo = 0;
_disposeResources = null;
_disposed = true;
if (attempt.HasFailures)
{
throw attempt.ToException(
"Terrain GPU resources released with exceptional committed outcomes.");
}
}
// ----------------------------------------------------------------
@ -393,28 +640,48 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
/// <summary>
/// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared
/// <see cref="ClipFrame"/> UBO (<see cref="SetClipUbo"/>); otherwise lazily
/// <see cref="ClipFrame"/> UBO range (<see cref="SetClipUbo"/>); otherwise lazily
/// creates + binds a no-clip fallback (count 0 = ungated) so the shader never
/// reads an unbound UBO. The fallback is std140-sized to
/// <see cref="ClipFrame.TerrainUboBytes"/> and zero-filled (count 0).
/// </summary>
private void BindClipUboBinding2()
{
if (_sharedClipUbo != 0)
if (_sharedClipBinding.IsValid)
{
_gl.BindBufferBase(BufferTargetARB.UniformBuffer,
ClipFrame.TerrainClipUboBinding, _sharedClipUbo);
_sharedClipBinding.Bind(_gl);
return;
}
if (_fallbackClipUbo == 0)
{
_fallbackClipUbo = _gl.GenBuffer();
var zero = stackalloc byte[ClipFrame.TerrainUboBytes];
for (int i = 0; i < ClipFrame.TerrainUboBytes; i++) zero[i] = 0;
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _fallbackClipUbo);
_gl.BufferData(BufferTargetARB.UniformBuffer,
(nuint)ClipFrame.TerrainUboBytes, zero, BufferUsageARB.DynamicDraw);
uint fallback = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain fallback clip UBO");
try
{
TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.UniformBuffer,
fallback,
0,
ClipFrame.TerrainUboBytes,
BufferUsageARB.DynamicDraw,
zero,
"allocating terrain fallback clip UBO");
_fallbackClipUbo = fallback;
}
catch
{
TrackedGlResource.DeleteBuffer(
_gl,
fallback,
0,
"rolling back terrain fallback clip UBO");
throw;
}
}
_gl.BindBufferBase(BufferTargetARB.UniformBuffer,
ClipFrame.TerrainClipUboBinding, _fallbackClipUbo);
@ -422,23 +689,35 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
private void AllocateGpuBuffers(int capacitySlots)
{
nuint vboBytes = (nuint)(capacitySlots * VertsPerLandblock * VertexSize);
nuint eboBytes = (nuint)(capacitySlots * IndicesPerLandblock * IndexSize);
long vboBytes = checked((long)capacitySlots * VertsPerLandblock * VertexSize);
long eboBytes = checked((long)capacitySlots * IndicesPerLandblock * IndexSize);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo);
_gl.BufferData(BufferTargetARB.ArrayBuffer, vboBytes, null, BufferUsageARB.DynamicDraw);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.ArrayBuffer,
_globalVbo,
_globalVboCapacityBytes,
vboBytes,
BufferUsageARB.DynamicDraw,
"allocating terrain global vertex storage");
_globalVboCapacityBytes = vboBytes;
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo);
_gl.BufferData(BufferTargetARB.ElementArrayBuffer, eboBytes, null, BufferUsageARB.DynamicDraw);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.ElementArrayBuffer,
_globalEbo,
_globalEboCapacityBytes,
eboBytes,
BufferUsageARB.DynamicDraw,
"allocating terrain global index storage");
_globalEboCapacityBytes = eboBytes;
}
private void ConfigureVao()
private void ConfigureVao(uint vao, uint vbo, uint ebo)
{
_gl.BindVertexArray(_globalVao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _globalVbo);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _globalEbo);
_gl.BindVertexArray(vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
uint stride = (uint)VertexSize;
@ -460,6 +739,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
_gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
_gl.BindVertexArray(0);
GLHelpers.ThrowOnResourceError(_gl, "configuring terrain VAO");
}
internal static void CollectVisibleCells(
@ -588,43 +868,129 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
private void EnsureCapacity(int newCapacity)
{
if (newCapacity <= _alloc.Capacity) return;
if (newCapacity <= _alloc.Capacity)
return;
// Allocate new VBO + EBO at new size; copy old contents; swap; recreate VAO.
uint newVbo = _gl.GenBuffer();
uint newEbo = _gl.GenBuffer();
var grownSlots = new SlotData?[newCapacity];
Array.Copy(_slots, grownSlots, _slots.Length);
nuint newVboBytes = (nuint)(newCapacity * VertsPerLandblock * VertexSize);
nuint newEboBytes = (nuint)(newCapacity * IndicesPerLandblock * IndexSize);
nuint oldVboBytes = (nuint)(_alloc.Capacity * VertsPerLandblock * VertexSize);
nuint oldEboBytes = (nuint)(_alloc.Capacity * IndicesPerLandblock * IndexSize);
long newVboBytes = checked((long)newCapacity * VertsPerLandblock * VertexSize);
long newEboBytes = checked((long)newCapacity * IndicesPerLandblock * IndexSize);
uint newVbo = 0;
uint newEbo = 0;
uint newVao = 0;
long allocatedNewVboBytes = 0;
long allocatedNewEboBytes = 0;
bool published = false;
try
{
newVbo = TrackedGlResource.CreateBuffer(
_gl,
"creating grown terrain vertex buffer");
TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.ArrayBuffer,
newVbo,
0,
newVboBytes,
BufferUsageARB.DynamicDraw,
"allocating grown terrain vertex buffer");
allocatedNewVboBytes = newVboBytes;
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, newVbo);
_gl.BufferData(BufferTargetARB.ArrayBuffer, newVboBytes, null, BufferUsageARB.DynamicDraw);
_gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalVbo);
_gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newVbo);
_gl.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer,
0, 0, oldVboBytes);
_gl.DeleteBuffer(_globalVbo);
_globalVbo = newVbo;
newEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating grown terrain index buffer");
TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.ElementArrayBuffer,
newEbo,
0,
newEboBytes,
BufferUsageARB.DynamicDraw,
"allocating grown terrain index buffer");
allocatedNewEboBytes = newEboBytes;
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, newEbo);
_gl.BufferData(BufferTargetARB.ElementArrayBuffer, newEboBytes, null, BufferUsageARB.DynamicDraw);
_gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalEbo);
_gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newEbo);
_gl.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer,
0, 0, oldEboBytes);
_gl.DeleteBuffer(_globalEbo);
_globalEbo = newEbo;
GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers (precondition)");
_gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalVbo);
_gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newVbo);
_gl.CopyBufferSubData(
CopyBufferSubDataTarget.CopyReadBuffer,
CopyBufferSubDataTarget.CopyWriteBuffer,
0,
0,
checked((nuint)_globalVboCapacityBytes));
_gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalEbo);
_gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newEbo);
_gl.CopyBufferSubData(
CopyBufferSubDataTarget.CopyReadBuffer,
CopyBufferSubDataTarget.CopyWriteBuffer,
0,
0,
checked((nuint)_globalEboCapacityBytes));
GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers");
// Recreate VAO with new buffer bindings.
_gl.DeleteVertexArray(_globalVao);
_globalVao = _gl.GenVertexArray();
ConfigureVao();
newVao = TrackedGlResource.CreateVertexArray(
_gl,
"creating grown terrain VAO");
ConfigureVao(newVao, newVbo, newEbo);
// Grow slot tracking array.
Array.Resize(ref _slots, newCapacity);
_alloc.GrowTo(newCapacity);
uint oldVao = _globalVao;
uint oldVbo = _globalVbo;
uint oldEbo = _globalEbo;
long oldVboBytes = _globalVboCapacityBytes;
long oldEboBytes = _globalEboCapacityBytes;
_globalVao = newVao;
_globalVbo = newVbo;
_globalEbo = newEbo;
_globalVboCapacityBytes = newVboBytes;
_globalEboCapacityBytes = newEboBytes;
_slots = grownSlots;
_alloc.GrowTo(newCapacity);
published = true;
// Older submitted draws captured the former VAO/buffer bindings.
// Retire the complete old set only after the replacement is valid.
RetryableGpuResourceRelease oldVaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
oldVao,
"retiring terrain VAO after growth");
RetryableGpuResourceRelease oldVboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
oldVbo,
oldVboBytes,
"retiring terrain vertex buffer after growth");
RetryableGpuResourceRelease oldEboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
oldEbo,
oldEboBytes,
"retiring terrain index buffer after growth");
_retirementLedger.RetireMany(
[oldVaoRelease, oldVboRelease, oldEboRelease]);
}
finally
{
if (!published)
{
TrackedGlResource.DeleteVertexArray(
_gl,
newVao,
"rolling back grown terrain VAO");
TrackedGlResource.DeleteBuffer(
_gl,
newVbo,
allocatedNewVboBytes,
"rolling back grown terrain vertex buffer");
TrackedGlResource.DeleteBuffer(
_gl,
newEbo,
allocatedNewEboBytes,
"rolling back grown terrain index buffer");
}
}
}
private sealed class SlotData