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
103
src/AcDream.App/Rendering/Wb/GpuRetiredRangeAllocator.cs
Normal file
103
src/AcDream.App/Rendering/Wb/GpuRetiredRangeAllocator.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// A contiguous GPU-buffer suballocator whose released ranges become
|
||||
/// reusable only after the frame-retirement queue says that older draws have
|
||||
/// finished. This prevents <c>BufferSubData</c> from overwriting a range that
|
||||
/// an in-flight draw still reads.
|
||||
/// </summary>
|
||||
internal sealed class GpuRetiredRangeAllocator
|
||||
{
|
||||
private readonly ContiguousRangeAllocator _allocator;
|
||||
private readonly GpuRetirementLedger _retirementLedger;
|
||||
private readonly Dictionary<MeshBufferRange, RetryableGpuResourceRelease> _pendingReleases = [];
|
||||
private int _pendingReleaseCount;
|
||||
private int _pendingReleaseLength;
|
||||
|
||||
public GpuRetiredRangeAllocator(int capacity, IGpuResourceRetirementQueue retirement)
|
||||
{
|
||||
_allocator = new ContiguousRangeAllocator(capacity);
|
||||
_retirementLedger = new GpuRetirementLedger(
|
||||
retirement ?? throw new ArgumentNullException(nameof(retirement)));
|
||||
}
|
||||
|
||||
public int Capacity => _allocator.Capacity;
|
||||
public int Used => _allocator.Used;
|
||||
public int HighWaterMark => _allocator.HighWaterMark;
|
||||
public int LargestFreeRange => _allocator.LargestFreeRange;
|
||||
public int TrailingFreeLength => _allocator.TrailingFreeLength;
|
||||
public int PendingReleaseCount => _pendingReleaseCount;
|
||||
public int PendingReleaseLength => _pendingReleaseLength;
|
||||
|
||||
public bool TryAllocate(int length, out MeshBufferRange allocation) =>
|
||||
_allocator.TryAllocate(length, out allocation);
|
||||
|
||||
public void Grow(int newCapacity) => _allocator.Grow(newCapacity);
|
||||
|
||||
public void Shrink(int newCapacity) => _allocator.Shrink(newCapacity);
|
||||
|
||||
public void ReleaseAfterGpuUse(MeshBufferRange allocation)
|
||||
{
|
||||
if (_pendingReleases.TryGetValue(allocation, out RetryableGpuResourceRelease? pending))
|
||||
{
|
||||
// The caller retries this method when queue publication failed.
|
||||
// Re-publish the retained transaction instead of accounting for a
|
||||
// second logical release of the same physical range.
|
||||
try
|
||||
{
|
||||
_retirementLedger.RetryPendingPublication(pending);
|
||||
}
|
||||
catch when (pending.IsComplete)
|
||||
{
|
||||
// An immediate retirement queue may surface a wrapper failure
|
||||
// after the retained transaction itself fully committed.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int nextPendingCount = checked(_pendingReleaseCount + 1);
|
||||
int nextPendingLength = checked(_pendingReleaseLength + allocation.Length);
|
||||
var release = new RetryableGpuResourceRelease(
|
||||
() => _allocator.Release(allocation),
|
||||
() => _pendingReleaseCount = checked(_pendingReleaseCount - 1),
|
||||
() => _pendingReleaseLength = checked(
|
||||
_pendingReleaseLength - allocation.Length),
|
||||
() =>
|
||||
{
|
||||
if (!_pendingReleases.Remove(allocation))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"GPU buffer range retirement lost its ownership record.");
|
||||
}
|
||||
});
|
||||
_pendingReleases.Add(allocation, release);
|
||||
_pendingReleaseCount = nextPendingCount;
|
||||
_pendingReleaseLength = nextPendingLength;
|
||||
|
||||
try
|
||||
{
|
||||
_retirementLedger.Retire(release);
|
||||
}
|
||||
catch when (release.IsComplete)
|
||||
{
|
||||
// Completion is stronger than publication acknowledgement. The
|
||||
// physical range and its accounting already converged.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases a range that failed before it could be submitted in a draw.
|
||||
/// </summary>
|
||||
public void ReleaseUnsubmitted(MeshBufferRange allocation)
|
||||
{
|
||||
if (_pendingReleases.ContainsKey(allocation))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A GPU-submitted range cannot be released as unsubmitted.");
|
||||
}
|
||||
|
||||
_allocator.Release(allocation);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue