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:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue