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>
369 lines
14 KiB
C#
369 lines
14 KiB
C#
// ClipFrameAssembler.cs
|
|
//
|
|
// Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like
|
|
// view graph: one portal_view list per visible cell plus an outside_view list.
|
|
// This assembler packs each visible polygon as an individual GPU clip slot so
|
|
// the renderer can draw the exact PView order:
|
|
//
|
|
// outside_view landscape slices
|
|
// reverse cell_draw_list exit masks
|
|
// reverse cell_draw_list EnvCell shells
|
|
// reverse cell_draw_list object lists
|
|
//
|
|
// Slot 0 is always no-clip. A slice whose polygon cannot be represented by the
|
|
// <=8 plane budget uses slot 0 and its NDC AABB; the renderer uses scissor for
|
|
// passes that need that fallback. Empty regions are omitted entirely.
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// How the landscape-through-outside_view pass should be interpreted.
|
|
/// </summary>
|
|
public enum TerrainClipMode
|
|
{
|
|
/// <summary>All outside_view slices have convex plane clips.</summary>
|
|
Planes,
|
|
|
|
/// <summary>At least one outside_view slice requires scissor fallback.</summary>
|
|
Scissor,
|
|
|
|
/// <summary>No outside_view slice is visible; skip landscape indoors.</summary>
|
|
Skip,
|
|
}
|
|
|
|
/// <summary>
|
|
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained
|
|
/// for passes that cannot write gl_ClipDistance and must use scissor.
|
|
/// </summary>
|
|
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
|
|
|
/// <summary>
|
|
/// Result of <see cref="ClipFrameAssembler.Assemble"/>: populated clip buffers
|
|
/// plus routing data consumed by the render orchestration.
|
|
/// </summary>
|
|
public sealed class ClipFrameAssembly
|
|
{
|
|
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 Dictionary<uint, int> CellIdToSlot { get; } = new();
|
|
|
|
/// <summary>Slot-only cell slices, retained for older renderer APIs.</summary>
|
|
public Dictionary<uint, int[]> CellIdToViewSlots { get; } = new();
|
|
|
|
/// <summary>Full retail portal_view slices per visible cell.</summary>
|
|
public Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; } = new();
|
|
|
|
/// <summary>Full retail outside_view slices.</summary>
|
|
public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>();
|
|
|
|
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 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,
|
|
ClipFrameAssembly? reuseAssembly = null)
|
|
{
|
|
System.ArgumentNullException.ThrowIfNull(frame);
|
|
System.ArgumentNullException.ThrowIfNull(pvFrame);
|
|
|
|
frame.Reset();
|
|
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
|
|
assembly.Reset(frame);
|
|
|
|
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)
|
|
{
|
|
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
|
|
continue;
|
|
|
|
List<ClipViewSlice> slices = assembly.SliceScratch;
|
|
slices.Clear();
|
|
int maxPlaneCount = 0;
|
|
|
|
foreach (var poly in view.Polygons)
|
|
{
|
|
var cps = ClipPlaneSet.From(poly);
|
|
if (cps.IsNothingVisible)
|
|
continue;
|
|
|
|
int slot;
|
|
Vector4[] planes;
|
|
if (cps.Count > 0)
|
|
{
|
|
planes = cps.PlaneArray;
|
|
slot = frame.AppendSlot(planes);
|
|
if (cps.Count > maxPlaneCount)
|
|
maxPlaneCount = cps.Count;
|
|
}
|
|
else
|
|
{
|
|
planes = System.Array.Empty<Vector4>();
|
|
slot = 0;
|
|
scissorFallbacks++;
|
|
}
|
|
|
|
slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
|
}
|
|
|
|
if (slices.Count == 0)
|
|
continue;
|
|
|
|
ClipViewSlice[] sliceArray = assembly.CopySlices(slices);
|
|
cellIdToViewSlices[cellId] = sliceArray;
|
|
cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray);
|
|
cellIdToSlot[cellId] = sliceArray[0].Slot;
|
|
perCellPlaneCounts[cellId] = maxPlaneCount;
|
|
}
|
|
|
|
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
|
|
outsideSlicesList.Clear();
|
|
int outsideMaxPlaneCount = 0;
|
|
bool outsideHasScissorFallback = false;
|
|
|
|
foreach (var poly in pvFrame.OutsideView.Polygons)
|
|
{
|
|
var cps = ClipPlaneSet.From(poly);
|
|
if (cps.IsNothingVisible)
|
|
continue;
|
|
|
|
int slot;
|
|
Vector4[] planes;
|
|
if (cps.Count > 0)
|
|
{
|
|
planes = cps.PlaneArray;
|
|
slot = frame.AppendSlot(planes);
|
|
if (cps.Count > outsideMaxPlaneCount)
|
|
outsideMaxPlaneCount = cps.Count;
|
|
}
|
|
else
|
|
{
|
|
planes = System.Array.Empty<Vector4>();
|
|
slot = 0;
|
|
outsideHasScissorFallback = true;
|
|
scissorFallbacks++;
|
|
}
|
|
|
|
outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
|
}
|
|
|
|
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
|
|
bool outdoorVisible = outsideViewSlices.Length > 0;
|
|
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
|
|
TerrainClipMode terrainMode = !outdoorVisible
|
|
? TerrainClipMode.Skip
|
|
: (outsideHasScissorFallback ? TerrainClipMode.Scissor : TerrainClipMode.Planes);
|
|
|
|
Vector4 outsideViewNdcAabb = outdoorVisible
|
|
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
|
|
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
|
|
: Vector4.Zero;
|
|
Vector4 terrainScissor = terrainMode == TerrainClipMode.Scissor
|
|
? outsideViewNdcAabb
|
|
: Vector4.Zero;
|
|
|
|
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);
|
|
|
|
}
|