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
|
|
@ -45,47 +45,220 @@ public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[]
|
|||
/// </summary>
|
||||
public sealed class ClipFrameAssembly
|
||||
{
|
||||
public required ClipFrame Frame { get; init; }
|
||||
public ClipFrame Frame { get; private set; } = null!;
|
||||
|
||||
/// <summary>First drawable slice slot per visible cell. Compatibility map
|
||||
/// for renderer APIs that can accept only one slot at a time.</summary>
|
||||
public required Dictionary<uint, int> CellIdToSlot { get; init; }
|
||||
public Dictionary<uint, int> CellIdToSlot { get; } = new();
|
||||
|
||||
/// <summary>Slot-only cell slices, retained for older renderer APIs.</summary>
|
||||
public required Dictionary<uint, int[]> CellIdToViewSlots { get; init; }
|
||||
public Dictionary<uint, int[]> CellIdToViewSlots { get; } = new();
|
||||
|
||||
/// <summary>Full retail portal_view slices per visible cell.</summary>
|
||||
public required Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; init; }
|
||||
public Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; } = new();
|
||||
|
||||
/// <summary>Full retail outside_view slices.</summary>
|
||||
public required ClipViewSlice[] OutsideViewSlices { get; init; }
|
||||
public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>();
|
||||
|
||||
public required int OutdoorSlot { get; init; }
|
||||
public required bool OutdoorVisible { get; init; }
|
||||
public required TerrainClipMode TerrainMode { get; init; }
|
||||
public required Vector4 TerrainScissorNdcAabb { get; init; }
|
||||
public required bool HasOutsideView { get; init; }
|
||||
public required Vector4 OutsideViewNdcAabb { get; init; }
|
||||
public int OutdoorSlot { get; internal set; }
|
||||
public bool OutdoorVisible { get; internal set; }
|
||||
public TerrainClipMode TerrainMode { get; internal set; }
|
||||
public Vector4 TerrainScissorNdcAabb { get; internal set; }
|
||||
public bool HasOutsideView { get; internal set; }
|
||||
public Vector4 OutsideViewNdcAabb { get; internal set; }
|
||||
|
||||
// Probe data.
|
||||
public required int OutsidePlaneCount { get; init; }
|
||||
public required Dictionary<uint, int> PerCellPlaneCounts { get; init; }
|
||||
public required int ScissorFallbacks { get; init; }
|
||||
public int OutsidePlaneCount { get; internal set; }
|
||||
public Dictionary<uint, int> PerCellPlaneCounts { get; } = new();
|
||||
public int ScissorFallbacks { get; internal set; }
|
||||
|
||||
// The assembly is frame-scoped. An owner that passes it back to Assemble may reuse the
|
||||
// dictionaries, per-cell arrays, and construction list after the prior frame is consumed.
|
||||
// Exact-length pools keep the public array API honest: Length is always the live slice count.
|
||||
private readonly Dictionary<int, Stack<ClipViewSlice[]>> _sliceArraysByLength = new();
|
||||
private readonly Dictionary<int, Stack<int[]>> _slotArraysByLength = new();
|
||||
internal List<ClipViewSlice> SliceScratch { get; } = new();
|
||||
internal int SliceArrayAllocationCount { get; private set; }
|
||||
internal int SlotArrayAllocationCount { get; private set; }
|
||||
internal const int MaxRetainedSliceItems = 4096;
|
||||
internal const int MaxRetainedSlotItems = 8192;
|
||||
internal const int MaxRetainedArraysPerPool = 128;
|
||||
internal int RetainedSliceItems { get; private set; }
|
||||
internal int RetainedSlotItems { get; private set; }
|
||||
internal int RetainedSliceArrays { get; private set; }
|
||||
internal int RetainedSlotArrays { get; private set; }
|
||||
|
||||
internal void Reset(ClipFrame frame)
|
||||
{
|
||||
Frame = frame;
|
||||
foreach (ClipViewSlice[] slices in CellIdToViewSlices.Values)
|
||||
ReturnSlices(slices);
|
||||
foreach (int[] slots in CellIdToViewSlots.Values)
|
||||
ReturnSlots(slots);
|
||||
if (OutsideViewSlices.Length != 0)
|
||||
ReturnSlices(OutsideViewSlices);
|
||||
|
||||
CellIdToSlot.Clear();
|
||||
CellIdToViewSlots.Clear();
|
||||
CellIdToViewSlices.Clear();
|
||||
PerCellPlaneCounts.Clear();
|
||||
OutsideViewSlices = System.Array.Empty<ClipViewSlice>();
|
||||
SliceScratch.Clear();
|
||||
}
|
||||
|
||||
internal ClipViewSlice[] CopySlices(List<ClipViewSlice> source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
return System.Array.Empty<ClipViewSlice>();
|
||||
ClipViewSlice[] result = RentSlices(source.Count);
|
||||
source.CopyTo(result, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal int[] CopySlots(ClipViewSlice[] slices)
|
||||
{
|
||||
if (slices.Length == 0)
|
||||
return System.Array.Empty<int>();
|
||||
int[] result = RentSlots(slices.Length);
|
||||
for (int i = 0; i < slices.Length; i++)
|
||||
result[i] = slices[i].Slot;
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void SetOutsideViewSlices(ClipViewSlice[] slices) => OutsideViewSlices = slices;
|
||||
|
||||
private ClipViewSlice[] RentSlices(int length)
|
||||
{
|
||||
if (_sliceArraysByLength.TryGetValue(length, out Stack<ClipViewSlice[]>? pool)
|
||||
&& pool.Count != 0)
|
||||
{
|
||||
ClipViewSlice[] result = pool.Pop();
|
||||
RetainedSliceItems -= result.Length;
|
||||
RetainedSliceArrays--;
|
||||
if (pool.Count == 0)
|
||||
_sliceArraysByLength.Remove(length);
|
||||
return result;
|
||||
}
|
||||
SliceArrayAllocationCount++;
|
||||
return new ClipViewSlice[length];
|
||||
}
|
||||
|
||||
private int[] RentSlots(int length)
|
||||
{
|
||||
if (_slotArraysByLength.TryGetValue(length, out Stack<int[]>? pool)
|
||||
&& pool.Count != 0)
|
||||
{
|
||||
int[] result = pool.Pop();
|
||||
RetainedSlotItems -= result.Length;
|
||||
RetainedSlotArrays--;
|
||||
if (pool.Count == 0)
|
||||
_slotArraysByLength.Remove(length);
|
||||
return result;
|
||||
}
|
||||
SlotArrayAllocationCount++;
|
||||
return new int[length];
|
||||
}
|
||||
|
||||
private void ReturnSlices(ClipViewSlice[] array)
|
||||
{
|
||||
// ClipViewSlice contains a Vector4[] reference. Clear before either retaining or dropping
|
||||
// the array so a bounded cache cannot accidentally extend plane-payload lifetimes.
|
||||
System.Array.Clear(array);
|
||||
if (array.Length > MaxRetainedSliceItems)
|
||||
return;
|
||||
while (RetainedSliceItems + array.Length > MaxRetainedSliceItems
|
||||
|| RetainedSliceArrays >= MaxRetainedArraysPerPool)
|
||||
{
|
||||
if (!EvictOneSliceArray())
|
||||
break;
|
||||
}
|
||||
if (!_sliceArraysByLength.TryGetValue(array.Length, out Stack<ClipViewSlice[]>? pool))
|
||||
{
|
||||
pool = new Stack<ClipViewSlice[]>();
|
||||
_sliceArraysByLength.Add(array.Length, pool);
|
||||
}
|
||||
pool.Push(array);
|
||||
RetainedSliceItems += array.Length;
|
||||
RetainedSliceArrays++;
|
||||
}
|
||||
|
||||
private void ReturnSlots(int[] array)
|
||||
{
|
||||
if (array.Length > MaxRetainedSlotItems)
|
||||
return;
|
||||
while (RetainedSlotItems + array.Length > MaxRetainedSlotItems
|
||||
|| RetainedSlotArrays >= MaxRetainedArraysPerPool)
|
||||
{
|
||||
if (!EvictOneSlotArray())
|
||||
break;
|
||||
}
|
||||
if (!_slotArraysByLength.TryGetValue(array.Length, out Stack<int[]>? pool))
|
||||
{
|
||||
pool = new Stack<int[]>();
|
||||
_slotArraysByLength.Add(array.Length, pool);
|
||||
}
|
||||
pool.Push(array);
|
||||
RetainedSlotItems += array.Length;
|
||||
RetainedSlotArrays++;
|
||||
}
|
||||
|
||||
private bool EvictOneSliceArray()
|
||||
{
|
||||
int selectedLength = -1;
|
||||
foreach ((int length, Stack<ClipViewSlice[]> pool) in _sliceArraysByLength)
|
||||
{
|
||||
if (pool.Count != 0 && length > selectedLength)
|
||||
selectedLength = length;
|
||||
}
|
||||
if (selectedLength < 0)
|
||||
return false;
|
||||
Stack<ClipViewSlice[]> selected = _sliceArraysByLength[selectedLength];
|
||||
ClipViewSlice[] evicted = selected.Pop();
|
||||
RetainedSliceItems -= evicted.Length;
|
||||
RetainedSliceArrays--;
|
||||
if (selected.Count == 0)
|
||||
_sliceArraysByLength.Remove(selectedLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EvictOneSlotArray()
|
||||
{
|
||||
int selectedLength = -1;
|
||||
foreach ((int length, Stack<int[]> pool) in _slotArraysByLength)
|
||||
{
|
||||
if (pool.Count != 0 && length > selectedLength)
|
||||
selectedLength = length;
|
||||
}
|
||||
if (selectedLength < 0)
|
||||
return false;
|
||||
Stack<int[]> selected = _slotArraysByLength[selectedLength];
|
||||
int[] evicted = selected.Pop();
|
||||
RetainedSlotItems -= evicted.Length;
|
||||
RetainedSlotArrays--;
|
||||
if (selected.Count == 0)
|
||||
_slotArraysByLength.Remove(selectedLength);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClipFrameAssembler
|
||||
{
|
||||
public static ClipFrameAssembly Assemble(ClipFrame frame, PortalVisibilityFrame pvFrame)
|
||||
public static ClipFrameAssembly Assemble(
|
||||
ClipFrame frame,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly? reuseAssembly = null)
|
||||
{
|
||||
System.ArgumentNullException.ThrowIfNull(frame);
|
||||
System.ArgumentNullException.ThrowIfNull(pvFrame);
|
||||
|
||||
frame.Reset();
|
||||
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
|
||||
assembly.Reset(frame);
|
||||
|
||||
var cellIdToSlot = new Dictionary<uint, int>();
|
||||
var cellIdToViewSlots = new Dictionary<uint, int[]>();
|
||||
var cellIdToViewSlices = new Dictionary<uint, ClipViewSlice[]>();
|
||||
var perCellPlaneCounts = new Dictionary<uint, int>();
|
||||
Dictionary<uint, int> cellIdToSlot = assembly.CellIdToSlot;
|
||||
Dictionary<uint, int[]> cellIdToViewSlots = assembly.CellIdToViewSlots;
|
||||
Dictionary<uint, ClipViewSlice[]> cellIdToViewSlices = assembly.CellIdToViewSlices;
|
||||
Dictionary<uint, int> perCellPlaneCounts = assembly.PerCellPlaneCounts;
|
||||
int scissorFallbacks = 0;
|
||||
|
||||
foreach (uint cellId in pvFrame.OrderedVisibleCells)
|
||||
|
|
@ -93,12 +266,13 @@ public static class ClipFrameAssembler
|
|||
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
|
||||
continue;
|
||||
|
||||
var slices = new List<ClipViewSlice>(view.Polygons.Count);
|
||||
List<ClipViewSlice> slices = assembly.SliceScratch;
|
||||
slices.Clear();
|
||||
int maxPlaneCount = 0;
|
||||
|
||||
foreach (var poly in view.Polygons)
|
||||
{
|
||||
var cps = ClipPlaneSet.From(ViewOf(poly));
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
|
||||
|
|
@ -106,7 +280,7 @@ public static class ClipFrameAssembler
|
|||
Vector4[] planes;
|
||||
if (cps.Count > 0)
|
||||
{
|
||||
planes = ToPlaneSpan(cps);
|
||||
planes = cps.PlaneArray;
|
||||
slot = frame.AppendSlot(planes);
|
||||
if (cps.Count > maxPlaneCount)
|
||||
maxPlaneCount = cps.Count;
|
||||
|
|
@ -124,20 +298,21 @@ public static class ClipFrameAssembler
|
|||
if (slices.Count == 0)
|
||||
continue;
|
||||
|
||||
var sliceArray = slices.ToArray();
|
||||
ClipViewSlice[] sliceArray = assembly.CopySlices(slices);
|
||||
cellIdToViewSlices[cellId] = sliceArray;
|
||||
cellIdToViewSlots[cellId] = ToSlots(sliceArray);
|
||||
cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray);
|
||||
cellIdToSlot[cellId] = sliceArray[0].Slot;
|
||||
perCellPlaneCounts[cellId] = maxPlaneCount;
|
||||
}
|
||||
|
||||
var outsideSlicesList = new List<ClipViewSlice>(pvFrame.OutsideView.Polygons.Count);
|
||||
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
|
||||
outsideSlicesList.Clear();
|
||||
int outsideMaxPlaneCount = 0;
|
||||
bool outsideHasScissorFallback = false;
|
||||
|
||||
foreach (var poly in pvFrame.OutsideView.Polygons)
|
||||
{
|
||||
var cps = ClipPlaneSet.From(ViewOf(poly));
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
|
||||
|
|
@ -145,7 +320,7 @@ public static class ClipFrameAssembler
|
|||
Vector4[] planes;
|
||||
if (cps.Count > 0)
|
||||
{
|
||||
planes = ToPlaneSpan(cps);
|
||||
planes = cps.PlaneArray;
|
||||
slot = frame.AppendSlot(planes);
|
||||
if (cps.Count > outsideMaxPlaneCount)
|
||||
outsideMaxPlaneCount = cps.Count;
|
||||
|
|
@ -161,7 +336,7 @@ public static class ClipFrameAssembler
|
|||
outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
||||
}
|
||||
|
||||
var outsideViewSlices = outsideSlicesList.ToArray();
|
||||
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
|
||||
bool outdoorVisible = outsideViewSlices.Length > 0;
|
||||
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
|
||||
TerrainClipMode terrainMode = !outdoorVisible
|
||||
|
|
@ -176,49 +351,19 @@ public static class ClipFrameAssembler
|
|||
? outsideViewNdcAabb
|
||||
: Vector4.Zero;
|
||||
|
||||
return new ClipFrameAssembly
|
||||
{
|
||||
Frame = frame,
|
||||
CellIdToSlot = cellIdToSlot,
|
||||
CellIdToViewSlots = cellIdToViewSlots,
|
||||
CellIdToViewSlices = cellIdToViewSlices,
|
||||
OutsideViewSlices = outsideViewSlices,
|
||||
OutdoorSlot = outdoorSlot,
|
||||
OutdoorVisible = outdoorVisible,
|
||||
TerrainMode = terrainMode,
|
||||
TerrainScissorNdcAabb = terrainScissor,
|
||||
HasOutsideView = outdoorVisible,
|
||||
OutsideViewNdcAabb = outsideViewNdcAabb,
|
||||
OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0,
|
||||
PerCellPlaneCounts = perCellPlaneCounts,
|
||||
ScissorFallbacks = scissorFallbacks,
|
||||
};
|
||||
}
|
||||
|
||||
private static CellView ViewOf(ViewPolygon poly)
|
||||
{
|
||||
var view = new CellView();
|
||||
view.Add(poly);
|
||||
return view;
|
||||
assembly.SetOutsideViewSlices(outsideViewSlices);
|
||||
assembly.OutdoorSlot = outdoorSlot;
|
||||
assembly.OutdoorVisible = outdoorVisible;
|
||||
assembly.TerrainMode = terrainMode;
|
||||
assembly.TerrainScissorNdcAabb = terrainScissor;
|
||||
assembly.HasOutsideView = outdoorVisible;
|
||||
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
|
||||
assembly.OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0;
|
||||
assembly.ScissorFallbacks = scissorFallbacks;
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static Vector4 AabbOf(ViewPolygon poly) =>
|
||||
new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY);
|
||||
|
||||
private static int[] ToSlots(ClipViewSlice[] slices)
|
||||
{
|
||||
var slots = new int[slices.Length];
|
||||
for (int i = 0; i < slices.Length; i++)
|
||||
slots[i] = slices[i].Slot;
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static Vector4[] ToPlaneSpan(ClipPlaneSet set)
|
||||
{
|
||||
int n = set.Count;
|
||||
var planes = new Vector4[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
planes[i] = set.Planes[i];
|
||||
return planes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue