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>
186 lines
9 KiB
C#
186 lines
9 KiB
C#
namespace AcDream.App.Rendering.Wb;
|
|
|
|
internal readonly record struct MeshUploadCost(
|
|
long SourceBytes,
|
|
long ArrayAllocationBytes,
|
|
long MipmapBytes,
|
|
int NewArrayCount,
|
|
long BufferUploadBytes = 0,
|
|
long BufferAllocationBytes = 0,
|
|
long BufferCopyBytes = 0,
|
|
int NewBufferCount = 0,
|
|
ulong AdmissionKey = 0);
|
|
|
|
internal readonly record struct MeshUploadBudgetLimits(
|
|
int MaximumObjects,
|
|
long MaximumSourceBytes,
|
|
long MaximumArrayAllocationBytes,
|
|
long MaximumMipmapBytes,
|
|
int MaximumNewArrays,
|
|
long MaximumBufferUploadBytes = long.MaxValue,
|
|
long MaximumBufferAllocationBytes = long.MaxValue,
|
|
long MaximumBufferCopyBytes = long.MaxValue,
|
|
int MaximumNewBuffers = int.MaxValue,
|
|
long MaximumSingleSourceBytes = long.MaxValue,
|
|
long MaximumSingleArrayAllocationBytes = long.MaxValue,
|
|
long MaximumSingleMipmapBytes = long.MaxValue,
|
|
int MaximumSingleNewArrays = int.MaxValue,
|
|
long MaximumSingleBufferUploadBytes = long.MaxValue);
|
|
|
|
/// <summary>
|
|
/// Pure prefix-admission policy for render-thread mesh uploads. Ordinary
|
|
/// uploads fit one frame in every independent CPU/GPU dimension. A physically
|
|
/// indivisible FIFO head may exceed a soft frame budget only when it is below
|
|
/// the explicit single-operation ceiling; it runs immediately rather than
|
|
/// accumulating fictional credits across idle frames. Global-buffer growth
|
|
/// and prefix copies are never admitted here: they are real, chunked
|
|
/// maintenance operations driven by <see cref="GlobalMeshBuffer"/>.
|
|
/// </summary>
|
|
internal sealed class MeshUploadFrameBudget
|
|
{
|
|
private readonly MeshUploadBudgetLimits _limits;
|
|
|
|
public MeshUploadFrameBudget(MeshUploadBudgetLimits limits)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumObjects, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSourceBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumArrayAllocationBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumMipmapBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumNewArrays, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumBufferUploadBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumBufferAllocationBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumBufferCopyBytes, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumNewBuffers, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSingleSourceBytes, limits.MaximumSourceBytes);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSingleArrayAllocationBytes, limits.MaximumArrayAllocationBytes);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSingleMipmapBytes, limits.MaximumMipmapBytes);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSingleNewArrays, limits.MaximumNewArrays);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(limits.MaximumSingleBufferUploadBytes, limits.MaximumBufferUploadBytes);
|
|
_limits = limits;
|
|
}
|
|
|
|
public int ObjectCount { get; private set; }
|
|
public long SourceBytes { get; private set; }
|
|
public long ArrayAllocationBytes { get; private set; }
|
|
public long MipmapBytes { get; private set; }
|
|
public int NewArrayCount { get; private set; }
|
|
public long BufferUploadBytes { get; private set; }
|
|
public long BufferAllocationBytes { get; private set; }
|
|
public long BufferCopyBytes { get; private set; }
|
|
public int NewBufferCount { get; private set; }
|
|
public void Reset()
|
|
{
|
|
ObjectCount = 0;
|
|
SourceBytes = 0;
|
|
ArrayAllocationBytes = 0;
|
|
MipmapBytes = 0;
|
|
NewArrayCount = 0;
|
|
BufferUploadBytes = 0;
|
|
BufferAllocationBytes = 0;
|
|
BufferCopyBytes = 0;
|
|
NewBufferCount = 0;
|
|
}
|
|
|
|
public bool TryAdmit(MeshUploadCost cost)
|
|
{
|
|
Validate(cost);
|
|
if (cost.BufferAllocationBytes != 0
|
|
|| cost.BufferCopyBytes != 0
|
|
|| cost.NewBufferCount != 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Global-buffer allocation/copy work must be progressed as real maintenance before upload admission.");
|
|
}
|
|
|
|
bool oversizedHead = ObjectCount == 0 && ExceedsSingleFrame(cost);
|
|
if (oversizedHead)
|
|
{
|
|
if (cost.SourceBytes > _limits.MaximumSingleSourceBytes
|
|
|| cost.ArrayAllocationBytes > _limits.MaximumSingleArrayAllocationBytes
|
|
|| cost.MipmapBytes > _limits.MaximumSingleMipmapBytes
|
|
|| cost.NewArrayCount > _limits.MaximumSingleNewArrays
|
|
|| cost.BufferUploadBytes > _limits.MaximumSingleBufferUploadBytes)
|
|
{
|
|
throw new NotSupportedException(
|
|
$"Mesh upload generation {cost.AdmissionKey} exceeds the supported single-operation ceiling.");
|
|
}
|
|
|
|
Admit(cost);
|
|
return true;
|
|
}
|
|
|
|
if (ObjectCount >= _limits.MaximumObjects
|
|
|| WouldExceed(SourceBytes, cost.SourceBytes, _limits.MaximumSourceBytes)
|
|
|| WouldExceed(ArrayAllocationBytes, cost.ArrayAllocationBytes, _limits.MaximumArrayAllocationBytes)
|
|
|| WouldExceed(MipmapBytes, cost.MipmapBytes, _limits.MaximumMipmapBytes)
|
|
|| cost.NewArrayCount > _limits.MaximumNewArrays - NewArrayCount
|
|
|| WouldExceed(BufferUploadBytes, cost.BufferUploadBytes, _limits.MaximumBufferUploadBytes)
|
|
|| WouldExceed(BufferAllocationBytes, cost.BufferAllocationBytes, _limits.MaximumBufferAllocationBytes)
|
|
|| WouldExceed(BufferCopyBytes, cost.BufferCopyBytes, _limits.MaximumBufferCopyBytes)
|
|
|| cost.NewBufferCount > _limits.MaximumNewBuffers - NewBufferCount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Admit(cost);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Records work that actually executed this frame.</summary>
|
|
public void RecordBufferMaintenance(long allocationBytes, long copyBytes, int newBufferCount)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(allocationBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(copyBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(newBufferCount);
|
|
if (WouldExceed(BufferAllocationBytes, allocationBytes, _limits.MaximumBufferAllocationBytes)
|
|
|| WouldExceed(BufferCopyBytes, copyBytes, _limits.MaximumBufferCopyBytes)
|
|
|| newBufferCount > _limits.MaximumNewBuffers - NewBufferCount)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Executed global-buffer maintenance exceeded its real per-frame bound.");
|
|
}
|
|
BufferAllocationBytes = checked(BufferAllocationBytes + allocationBytes);
|
|
BufferCopyBytes = checked(BufferCopyBytes + copyBytes);
|
|
NewBufferCount = checked(NewBufferCount + newBufferCount);
|
|
}
|
|
|
|
private bool ExceedsSingleFrame(MeshUploadCost cost) =>
|
|
cost.SourceBytes > _limits.MaximumSourceBytes
|
|
|| cost.ArrayAllocationBytes > _limits.MaximumArrayAllocationBytes
|
|
|| cost.MipmapBytes > _limits.MaximumMipmapBytes
|
|
|| cost.NewArrayCount > _limits.MaximumNewArrays
|
|
|| cost.BufferUploadBytes > _limits.MaximumBufferUploadBytes
|
|
|| cost.BufferAllocationBytes > _limits.MaximumBufferAllocationBytes
|
|
|| cost.BufferCopyBytes > _limits.MaximumBufferCopyBytes
|
|
|| cost.NewBufferCount > _limits.MaximumNewBuffers;
|
|
|
|
private void Admit(MeshUploadCost cost)
|
|
{
|
|
ObjectCount++;
|
|
SourceBytes = checked(SourceBytes + cost.SourceBytes);
|
|
ArrayAllocationBytes = checked(ArrayAllocationBytes + cost.ArrayAllocationBytes);
|
|
MipmapBytes = checked(MipmapBytes + cost.MipmapBytes);
|
|
NewArrayCount = checked(NewArrayCount + cost.NewArrayCount);
|
|
BufferUploadBytes = checked(BufferUploadBytes + cost.BufferUploadBytes);
|
|
BufferAllocationBytes = checked(BufferAllocationBytes + cost.BufferAllocationBytes);
|
|
BufferCopyBytes = checked(BufferCopyBytes + cost.BufferCopyBytes);
|
|
NewBufferCount = checked(NewBufferCount + cost.NewBufferCount);
|
|
}
|
|
|
|
// Keep the comparison overflow-free even when a bounded indivisible head
|
|
// has truthfully exceeded a soft per-frame budget.
|
|
private static bool WouldExceed(long current, long added, long maximum) =>
|
|
added > maximum - Math.Min(current, maximum);
|
|
|
|
private static void Validate(MeshUploadCost cost)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.SourceBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.ArrayAllocationBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.MipmapBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.NewArrayCount);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.BufferUploadBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.BufferAllocationBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.BufferCopyBytes);
|
|
ArgumentOutOfRangeException.ThrowIfNegative(cost.NewBufferCount);
|
|
}
|
|
}
|