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

@ -0,0 +1,89 @@
using AcDream.Core.Terrain;
namespace AcDream.App.Rendering;
/// <summary>
/// Adds GPU-frame retirement to the terrain renderer's CPU slot allocator.
/// Removing a landblock stops future draws immediately, but its VBO/EBO slot
/// is not returned for overwrite until older submitted draws have completed.
/// </summary>
internal sealed class GpuRetiredTerrainSlotAllocator
{
private readonly TerrainSlotAllocator _allocator;
private readonly GpuRetirementLedger _retirementLedger;
private readonly Dictionary<int, RetryableGpuResourceRelease> _pendingReleases = [];
public GpuRetiredTerrainSlotAllocator(
int initialCapacity,
IGpuResourceRetirementQueue retirement)
{
_allocator = new TerrainSlotAllocator(initialCapacity);
_retirementLedger = new GpuRetirementLedger(
retirement ?? throw new ArgumentNullException(nameof(retirement)));
}
public int Capacity => _allocator.Capacity;
public int LoadedCount => _allocator.LoadedCount;
internal int PendingReleaseCount => _pendingReleases.Count;
public int Allocate(out bool needsGrow) => _allocator.Allocate(out needsGrow);
public void GrowTo(int newCapacity) => _allocator.GrowTo(newCapacity);
/// <summary>
/// Returns a slot whose contents were never published to a GPU draw. No
/// retirement fence is required because no submitted command can refer to
/// it.
/// </summary>
public void ReleaseUnsubmitted(int slot)
{
if (_pendingReleases.ContainsKey(slot))
{
throw new InvalidOperationException(
"A GPU-submitted terrain slot cannot be released as unsubmitted.");
}
_allocator.Free(slot);
}
public void FreeAfterGpuUse(int slot)
{
if (_pendingReleases.TryGetValue(slot, out RetryableGpuResourceRelease? pending))
{
try
{
_retirementLedger.RetryPendingPublication(pending);
}
catch when (pending.IsComplete)
{
// Completion is stronger than publication acknowledgement.
}
return;
}
var release = new RetryableGpuResourceRelease(
() => _allocator.Free(slot),
() =>
{
if (!_pendingReleases.Remove(slot))
{
throw new InvalidOperationException(
"Terrain-slot retirement lost its ownership record.");
}
});
_pendingReleases.Add(slot, release);
try
{
_retirementLedger.Retire(release);
}
catch when (release.IsComplete)
{
// An immediate retirement queue can complete before surfacing a
// wrapper failure. The slot is already safely reusable.
}
}
public void RetryPendingPublications() =>
_retirementLedger.RetryPendingPublications();
}