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
|
|
@ -18,9 +18,14 @@ public sealed class RetailPViewRenderer
|
|||
private readonly ClipFrame _clipFrame;
|
||||
private readonly EnvCellRenderer _envCells;
|
||||
private readonly WbDrawDispatcher _entities;
|
||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly ViewconeCuller _viewconeScratch = new();
|
||||
private readonly RetailPViewFrameResult _frameResultScratch = new();
|
||||
|
||||
private static readonly ClipViewSlice NoClipSlice =
|
||||
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
|
||||
private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice };
|
||||
|
||||
private readonly HashSet<uint> _oneCell = new(1);
|
||||
// Shell-batch scratch: all of a pass's cells collected for ONE batched
|
||||
|
|
@ -28,14 +33,20 @@ public sealed class RetailPViewRenderer
|
|||
// frames + across look-in buildings. Spec:
|
||||
// docs/superpowers/specs/2026-06-23-envcell-shell-batching-design.md
|
||||
private readonly HashSet<uint> _shellBatch = new();
|
||||
// Transparent shell cells retain IndoorDrawPlan's far-to-near order. The
|
||||
// EnvCell renderer uploads shared instance/command data once, then issues
|
||||
// range-addressed MDI calls in this exact order.
|
||||
private readonly List<uint> _orderedTransparentShellCells = new();
|
||||
|
||||
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
||||
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
||||
private readonly BuildingGroupScratch _buildingGroups = new();
|
||||
private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new();
|
||||
|
||||
// #124: per-building look-in frames under an INTERIOR root, drawn as a
|
||||
// landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
|
||||
// main frame (see DrawInside). Rebuilt each interior-root frame.
|
||||
private readonly List<PortalVisibilityFrame> _lookInFrames = new();
|
||||
private readonly Stack<PortalVisibilityFrame> _lookInFramePool = new();
|
||||
private readonly HashSet<uint> _lookInPrepareScratch = new();
|
||||
|
||||
// #131/#132: the late landscape phase's scene-particle owner survivors
|
||||
|
|
@ -56,6 +67,7 @@ public sealed class RetailPViewRenderer
|
|||
// DrawCellObjectLists, RetailPViewFrameResult.DrawableCells) reads it
|
||||
// synchronously within the same frame it was built.
|
||||
private readonly HashSet<uint> _drawableCellsScratch = new();
|
||||
private readonly RetailPViewScratchRetention _scratchRetention = new();
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||
|
|
@ -79,6 +91,8 @@ public sealed class RetailPViewRenderer
|
|||
public RetailPViewFrameResult DrawInside(RetailPViewDrawContext ctx)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
RecycleLookInFrames();
|
||||
ResetBuildingGroups();
|
||||
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
ctx.RootCell,
|
||||
|
|
@ -86,7 +100,8 @@ public sealed class RetailPViewRenderer
|
|||
ctx.CellLookup,
|
||||
ctx.ViewProjection,
|
||||
buildingMembership: null,
|
||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
|
||||
reuseFrame: _mainPortalFrameScratch);
|
||||
|
||||
// R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
|
||||
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
|
||||
|
|
@ -111,14 +126,17 @@ public sealed class RetailPViewRenderer
|
|||
// side). Outdoor roots keep the MergeNearbyBuildingFloods path above
|
||||
// (no depth clear under outdoor roots — the merged form is equivalent
|
||||
// there).
|
||||
_lookInFrames.Clear();
|
||||
if (!ctx.RootCell.IsOutdoorNode
|
||||
&& ctx.NearbyBuildingCells is not null
|
||||
&& pvFrame.OutsideView.Polygons.Count > 0)
|
||||
BuildInteriorRootLookIns(ctx, pvFrame);
|
||||
|
||||
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||
var clipAssembly = ClipFrameAssembler.Assemble(
|
||||
_clipFrame,
|
||||
pvFrame,
|
||||
_clipAssemblyScratch);
|
||||
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
||||
PrepareClipFrame(ctx.SetTerrainClipUbo, terrainUploadCount);
|
||||
|
||||
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
||||
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
||||
|
|
@ -161,13 +179,11 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
InteriorEntityPartition.Partition(_partitionResult, prepareCells, ctx.LandblockEntries);
|
||||
var partition = _partitionResult;
|
||||
var result = new RetailPViewFrameResult
|
||||
{
|
||||
PortalFrame = pvFrame,
|
||||
ClipAssembly = clipAssembly,
|
||||
DrawableCells = drawableCells,
|
||||
Partition = partition,
|
||||
};
|
||||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||
pvFrame,
|
||||
clipAssembly,
|
||||
drawableCells,
|
||||
partition);
|
||||
|
||||
ctx.EmitDiagnostics?.Invoke(result);
|
||||
|
||||
|
|
@ -183,7 +199,10 @@ public sealed class RetailPViewRenderer
|
|||
// T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
|
||||
// never clipped (Ghidra 0x0054c250). Built once per frame from the
|
||||
// assembled slices + this frame's view-projection.
|
||||
var viewcone = ViewconeCuller.Build(clipAssembly, ctx.ViewProjection);
|
||||
var viewcone = ViewconeCuller.Build(
|
||||
clipAssembly,
|
||||
ctx.ViewProjection,
|
||||
_viewconeScratch);
|
||||
|
||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
|
|
@ -224,36 +243,22 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
|
||||
// (retail's per-building ConstructView(CBldPortal)), merging each small view into the frame. The
|
||||
// grouping dict is reused across frames; inner lists are cleared each frame so a building that left
|
||||
// the near set simply contributes an empty (skipped) group.
|
||||
// grouping dict contains only this frame's keys; lists are pooled across frames.
|
||||
private void MergeNearbyBuildingFloods(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
group.Clear();
|
||||
|
||||
foreach (var cell in ctx.NearbyBuildingCells!)
|
||||
{
|
||||
// R-A2 seam fix: a cell without a BuildingId (unstamped, or outdoor-adjacent with an exit
|
||||
// portal) must STILL flood — the pre-R-A2 node flood reached it via a reverse portal, so
|
||||
// dropping it (the original `continue`) left holes at building/terrain seams. Key it by its
|
||||
// own CellId → a singleton per-entrance flood: a cell with an exit portal seeds from it, a
|
||||
// cell with none contributes nothing (same as before). BuildingId/CellId key collisions are
|
||||
// harmless — BuildFromExterior seeds each exit-portal cell in a group independently.
|
||||
uint groupKey = cell.BuildingId ?? cell.CellId;
|
||||
if (!_buildingGroups.TryGetValue(groupKey, out var group))
|
||||
{
|
||||
group = new List<LoadedCell>();
|
||||
_buildingGroups[groupKey] = group;
|
||||
}
|
||||
group.Add(cell);
|
||||
}
|
||||
RebuildBuildingGroups(ctx.NearbyBuildingCells!);
|
||||
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
{
|
||||
if (group.Count == 0)
|
||||
continue;
|
||||
var buildingFrame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection, OutdoorBuildingSeedDistance);
|
||||
group,
|
||||
ctx.ViewerEyePos,
|
||||
ctx.CellLookup,
|
||||
ctx.ViewProjection,
|
||||
OutdoorBuildingSeedDistance,
|
||||
reuseFrame: _outdoorBuildingFrameScratch);
|
||||
MergeBuildingFrame(pvFrame, buildingFrame);
|
||||
}
|
||||
}
|
||||
|
|
@ -277,15 +282,19 @@ public sealed class RetailPViewRenderer
|
|||
if (!src.CellViews.TryGetValue(cellId, out var srcView))
|
||||
continue;
|
||||
|
||||
if (target.CellViews.TryGetValue(cellId, out var existing))
|
||||
if (!target.CellViews.TryGetValue(cellId, out var existing))
|
||||
{
|
||||
foreach (var p in srcView.Polygons)
|
||||
existing.Add(p);
|
||||
continue;
|
||||
existing = target.RentCellView();
|
||||
target.CellViews[cellId] = existing;
|
||||
target.OrderedVisibleCells.Add(cellId);
|
||||
}
|
||||
|
||||
target.CellViews[cellId] = srcView;
|
||||
target.OrderedVisibleCells.Add(cellId);
|
||||
// Copy the view polygons into storage owned by the target frame.
|
||||
// Source building frames are reused immediately for the next
|
||||
// building flood, so retaining their CellView reference would
|
||||
// alias pooled scratch and mutate the merged main view.
|
||||
foreach (var p in srcView.Polygons)
|
||||
existing.Add(target.CopyPolygon(p.Vertices));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,32 +306,51 @@ public sealed class RetailPViewRenderer
|
|||
// building self-excludes via the seed eye-side test.
|
||||
private void BuildInteriorRootLookIns(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
group.Clear();
|
||||
|
||||
foreach (var cell in ctx.NearbyBuildingCells!)
|
||||
{
|
||||
uint groupKey = cell.BuildingId ?? cell.CellId;
|
||||
if (!_buildingGroups.TryGetValue(groupKey, out var group))
|
||||
{
|
||||
group = new List<LoadedCell>();
|
||||
_buildingGroups[groupKey] = group;
|
||||
}
|
||||
group.Add(cell);
|
||||
}
|
||||
RebuildBuildingGroups(ctx.NearbyBuildingCells!);
|
||||
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
{
|
||||
if (group.Count == 0)
|
||||
continue;
|
||||
PortalVisibilityFrame frameScratch = _lookInFramePool.Count != 0
|
||||
? _lookInFramePool.Pop()
|
||||
: new PortalVisibilityFrame();
|
||||
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection,
|
||||
OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons);
|
||||
OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons,
|
||||
reuseFrame: frameScratch);
|
||||
if (frame.OrderedVisibleCells.Count > 0)
|
||||
_lookInFrames.Add(frame);
|
||||
else
|
||||
ReturnLookInFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecycleLookInFrames()
|
||||
{
|
||||
for (int i = 0; i < _lookInFrames.Count; i++)
|
||||
ReturnLookInFrame(_lookInFrames[i]);
|
||||
_scratchRetention.ClearFrameBuffers(
|
||||
_lookInFrames,
|
||||
_lookInPrepareScratch,
|
||||
_drawableCellsScratch,
|
||||
_shellBatch,
|
||||
_orderedTransparentShellCells);
|
||||
}
|
||||
|
||||
private void ReturnLookInFrame(PortalVisibilityFrame frame)
|
||||
{
|
||||
frame.ResetForBuild();
|
||||
if (_lookInFramePool.Count < RetailPViewScratchRetention.MaxRetainedLookInFrames)
|
||||
_lookInFramePool.Push(frame);
|
||||
}
|
||||
|
||||
private void RebuildBuildingGroups(IReadOnlyList<LoadedCell> nearbyCells)
|
||||
=> _buildingGroups.Rebuild(nearbyCells);
|
||||
|
||||
private void ResetBuildingGroups()
|
||||
=> _buildingGroups.Reset();
|
||||
|
||||
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
||||
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
||||
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
|
||||
|
|
@ -355,17 +383,15 @@ public sealed class RetailPViewRenderer
|
|||
continue;
|
||||
foreach (var poly in view.Polygons)
|
||||
{
|
||||
var single = new CellView();
|
||||
single.Add(poly);
|
||||
var cps = ClipPlaneSet.From(single);
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
var planes = new Vector4[cps.Count];
|
||||
for (int p = 0; p < cps.Count; p++)
|
||||
planes[p] = cps.Planes[p];
|
||||
ctx.DrawLookInPortalPunch(new RetailPViewCellSliceContext(
|
||||
cellId,
|
||||
new ClipViewSlice(0, new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY), planes),
|
||||
new ClipViewSlice(
|
||||
0,
|
||||
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
|
||||
cps.PlaneArray),
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
}
|
||||
|
|
@ -454,7 +480,7 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
_clipFrame.SetTerrainClip(slice.Planes);
|
||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||
UploadTerrainClip(ctx.SetTerrainClipUbo);
|
||||
// T3 (BR-5): entities are never hard-clipped — retail viewcone-
|
||||
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
|
||||
// and draws it whole. The old per-slice entity clip routing
|
||||
|
|
@ -490,7 +516,7 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
_clipFrame.SetTerrainClip(slice.Planes);
|
||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||
UploadTerrainClip(ctx.SetTerrainClipUbo);
|
||||
_entities.ClearClipRouting();
|
||||
|
||||
_outdoorStaticScratch.Clear(); // late: dynamics survivors
|
||||
|
|
@ -595,7 +621,7 @@ public sealed class RetailPViewRenderer
|
|||
// the outside slice's slot + NDC AABB + planes (CPU side), the region-SSBO bytes decoded
|
||||
// at that slot (what mesh_modern.vert reads for routed instances), the terrain-UBO head
|
||||
// (what terrain/sky gate against), and the CellIdToSlot routing table. Fires AFTER
|
||||
// SetTerrainClip + UploadClipFrame + SetClipRouting, BEFORE DrawLandscapeSlice — so the
|
||||
// SetTerrainClip + UploadTerrainClip + SetClipRouting, BEFORE DrawLandscapeSlice — so the
|
||||
// printed bytes are exactly what this slice's draws consume.
|
||||
private void EmitClipRouteProbe(ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex)
|
||||
{
|
||||
|
|
@ -626,7 +652,7 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
sb.Append('}');
|
||||
|
||||
// Region-SSBO content decoded at the routed slot, from the packed bytes UploadClipFrame
|
||||
// Region-SSBO content decoded at the routed slot, from the packed bytes UploadRegions
|
||||
// just uploaded — slot stride 144: count uint at +0, planes[8] at +16.
|
||||
var rb = _clipFrame.RegionBytesForTest;
|
||||
int off = slice.Slot * ClipFrame.CellClipStrideBytes;
|
||||
|
|
@ -715,17 +741,17 @@ public sealed class RetailPViewRenderer
|
|||
if (_shellBatch.Count > 0)
|
||||
_envCells.Render(WbRenderPass.Opaque, _shellBatch);
|
||||
|
||||
// Transparent: far→near order matters for compositing, so keep these
|
||||
// per-cell in ShellPass order — but skip cells with no transparent
|
||||
// geometry (most are opaque-only walls/floors), removing the bulk of the
|
||||
// per-cell transparent Render calls.
|
||||
// Transparent: far-to-near order matters for compositing. The ordered
|
||||
// list retains ShellPass cell boundaries while EnvCellRenderer shares
|
||||
// one instance/command/light upload across the complete pass.
|
||||
_orderedTransparentShellCells.Clear();
|
||||
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
|
||||
{
|
||||
if (!_envCells.CellHasTransparent(entry.CellId)) continue;
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(entry.CellId);
|
||||
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||
if (_envCells.CellHasTransparent(entry.CellId))
|
||||
_orderedTransparentShellCells.Add(entry.CellId);
|
||||
}
|
||||
if (_orderedTransparentShellCells.Count > 0)
|
||||
_envCells.RenderTransparentOrdered(_orderedTransparentShellCells);
|
||||
}
|
||||
|
||||
// T1: the frame's single LAST entity pass — ALL server-spawned dynamics
|
||||
|
|
@ -994,7 +1020,7 @@ public sealed class RetailPViewRenderer
|
|||
&& slices.Length > 0)
|
||||
return slices;
|
||||
|
||||
return new[] { NoClipSlice };
|
||||
return NoClipSlices;
|
||||
}
|
||||
|
||||
private void UseIndoorMembershipOnlyRouting()
|
||||
|
|
@ -1026,19 +1052,25 @@ public sealed class RetailPViewRenderer
|
|||
animatedEntityIds: ctx.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
private void RestoreNoClip(Action<uint> setTerrainClipUbo)
|
||||
private void PrepareClipFrame(
|
||||
Action<TerrainClipBufferBinding> setTerrainClipUbo,
|
||||
int terrainUploadCount)
|
||||
{
|
||||
_clipFrame.Reset();
|
||||
UploadClipFrame(setTerrainClipUbo);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
private void UploadClipFrame(Action<uint> setTerrainClipUbo)
|
||||
{
|
||||
_clipFrame.UploadShared(_gl);
|
||||
// Allocate every terrain record before issuing the first draw. BufferData
|
||||
// therefore never replaces the arena's backing store while an earlier
|
||||
// slice can still reference it. The large region table is immutable for
|
||||
// this assembled PView frame and is uploaded exactly once.
|
||||
_clipFrame.ReserveTerrainUploads(_gl, terrainUploadCount);
|
||||
_clipFrame.UploadRegions(_gl);
|
||||
_entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
_envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
setTerrainClipUbo(_clipFrame.TerrainUbo);
|
||||
UploadTerrainClip(setTerrainClipUbo);
|
||||
}
|
||||
|
||||
private void UploadTerrainClip(Action<TerrainClipBufferBinding> setTerrainClipUbo)
|
||||
{
|
||||
TerrainClipBufferBinding binding = _clipFrame.UploadTerrainClip(_gl);
|
||||
setTerrainClipUbo(binding);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1067,6 +1099,168 @@ public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
|||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capacity policy for renderer-owned, one-frame scratch. Normal warmed frames
|
||||
/// retain their storage; a pathological visibility spike is released at the
|
||||
/// next frame boundary instead of becoming permanent process memory.
|
||||
/// </summary>
|
||||
internal sealed class RetailPViewScratchRetention
|
||||
{
|
||||
internal const int MaxRetainedLookInFrames = 32;
|
||||
internal const int MaxRetainedCellItems = 512;
|
||||
internal const int CapacityTrimIdleFrames = 120;
|
||||
|
||||
private int _lookInFramesUnderusedFrames;
|
||||
private int _lookInPrepareUnderusedFrames;
|
||||
private int _drawableCellsUnderusedFrames;
|
||||
private int _shellBatchUnderusedFrames;
|
||||
private int _orderedTransparentUnderusedFrames;
|
||||
|
||||
internal void ClearFrameBuffers(
|
||||
List<PortalVisibilityFrame> lookInFrames,
|
||||
HashSet<uint> lookInPrepare,
|
||||
HashSet<uint> drawableCells,
|
||||
HashSet<uint> shellBatch,
|
||||
List<uint> orderedTransparentShellCells)
|
||||
{
|
||||
ClearCold(
|
||||
lookInFrames,
|
||||
MaxRetainedLookInFrames,
|
||||
ref _lookInFramesUnderusedFrames);
|
||||
ClearCold(
|
||||
lookInPrepare,
|
||||
MaxRetainedCellItems,
|
||||
ref _lookInPrepareUnderusedFrames);
|
||||
ClearCold(
|
||||
drawableCells,
|
||||
MaxRetainedCellItems,
|
||||
ref _drawableCellsUnderusedFrames);
|
||||
ClearCold(shellBatch, MaxRetainedCellItems, ref _shellBatchUnderusedFrames);
|
||||
ClearCold(
|
||||
orderedTransparentShellCells,
|
||||
MaxRetainedCellItems,
|
||||
ref _orderedTransparentUnderusedFrames);
|
||||
}
|
||||
|
||||
private static void ClearCold<T>(
|
||||
List<T> values,
|
||||
int maximumRetainedCapacity,
|
||||
ref int underusedFrames)
|
||||
{
|
||||
int usedCount = values.Count;
|
||||
int capacity = values.Capacity;
|
||||
values.Clear();
|
||||
if (!ShouldTrim(
|
||||
capacity,
|
||||
usedCount,
|
||||
maximumRetainedCapacity,
|
||||
ref underusedFrames))
|
||||
return;
|
||||
|
||||
if (capacity > maximumRetainedCapacity)
|
||||
values.Capacity = 0;
|
||||
}
|
||||
|
||||
private static void ClearCold<T>(
|
||||
HashSet<T> values,
|
||||
int maximumRetainedCapacity,
|
||||
ref int underusedFrames)
|
||||
{
|
||||
int usedCount = values.Count;
|
||||
int capacity = values.EnsureCapacity(0);
|
||||
values.Clear();
|
||||
if (!ShouldTrim(
|
||||
capacity,
|
||||
usedCount,
|
||||
maximumRetainedCapacity,
|
||||
ref underusedFrames))
|
||||
return;
|
||||
|
||||
if (capacity > maximumRetainedCapacity)
|
||||
values.TrimExcess();
|
||||
}
|
||||
|
||||
private static bool ShouldTrim(
|
||||
int capacity,
|
||||
int usedCount,
|
||||
int maximumRetainedCapacity,
|
||||
ref int underusedFrames)
|
||||
{
|
||||
if (capacity <= maximumRetainedCapacity || (long)usedCount * 2L > capacity)
|
||||
{
|
||||
underusedFrames = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
underusedFrames++;
|
||||
if (underusedFrames < CapacityTrimIdleFrames)
|
||||
return false;
|
||||
|
||||
underusedFrames = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frame-scoped grouping for retail's per-building portal floods. Active keys
|
||||
/// are rebuilt in nearby-cell encounter order every frame; the value lists are
|
||||
/// retained through a bounded pool so travelling through the world cannot turn
|
||||
/// every historical building id into permanent memory or per-frame scan work.
|
||||
/// </summary>
|
||||
internal sealed class BuildingGroupScratch
|
||||
{
|
||||
internal const int MaxRetainedGroups = 256;
|
||||
internal const int MaxRetainedCellsPerGroup = 256;
|
||||
|
||||
private readonly Dictionary<uint, List<LoadedCell>> _active = new();
|
||||
private readonly Stack<List<LoadedCell>> _listPool = new();
|
||||
|
||||
internal Dictionary<uint, List<LoadedCell>>.ValueCollection Values => _active.Values;
|
||||
internal IReadOnlyDictionary<uint, List<LoadedCell>> Groups => _active;
|
||||
internal int ActiveGroupCount => _active.Count;
|
||||
internal int RetainedListCount => _listPool.Count;
|
||||
internal int MapCapacity => _active.EnsureCapacity(0);
|
||||
|
||||
internal void Rebuild(IReadOnlyList<LoadedCell> nearbyCells)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nearbyCells);
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < nearbyCells.Count; i++)
|
||||
{
|
||||
LoadedCell cell = nearbyCells[i];
|
||||
// R-A2 seam behavior: an unstamped cell still gets a singleton
|
||||
// entrance flood keyed by CellId.
|
||||
uint groupKey = cell.BuildingId ?? cell.CellId;
|
||||
if (!_active.TryGetValue(groupKey, out List<LoadedCell>? group))
|
||||
{
|
||||
group = _listPool.Count != 0
|
||||
? _listPool.Pop()
|
||||
: new List<LoadedCell>();
|
||||
_active.Add(groupKey, group);
|
||||
}
|
||||
group.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
foreach (List<LoadedCell> group in _active.Values)
|
||||
{
|
||||
group.Clear();
|
||||
if (group.Capacity <= MaxRetainedCellsPerGroup
|
||||
&& _listPool.Count < MaxRetainedGroups)
|
||||
{
|
||||
_listPool.Push(group);
|
||||
}
|
||||
}
|
||||
|
||||
_active.Clear();
|
||||
if (_active.EnsureCapacity(0) > MaxRetainedGroups)
|
||||
_active.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
||||
{
|
||||
public required LoadedCell RootCell { get; init; }
|
||||
|
|
@ -1089,7 +1283,7 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
||||
public required Action<uint> SetTerrainClipUbo { get; init; }
|
||||
public required Action<TerrainClipBufferBinding> SetTerrainClipUbo { get; init; }
|
||||
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
|
||||
|
||||
/// <summary>#131/#132: the LATE landscape phase, per slice, after the #124
|
||||
|
|
@ -1115,15 +1309,38 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
/// remains open for the final world alpha scope.</summary>
|
||||
public Action? FlushLandscapeAlpha { get; init; }
|
||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
||||
/// <summary>
|
||||
/// Synchronous observation of the borrowed current-frame result. The
|
||||
/// callback must copy anything it needs to retain beyond this invocation.
|
||||
/// </summary>
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed renderer scratch valid only until the next
|
||||
/// <see cref="RetailPViewRenderer.DrawInside"/> call. Collections and nested
|
||||
/// frame objects are deliberately reused to keep the render loop allocation
|
||||
/// free; consumers must copy any state they need to retain asynchronously.
|
||||
/// </summary>
|
||||
public sealed class RetailPViewFrameResult
|
||||
{
|
||||
public required PortalVisibilityFrame PortalFrame { get; init; }
|
||||
public required ClipFrameAssembly ClipAssembly { get; init; }
|
||||
public required HashSet<uint> DrawableCells { get; init; }
|
||||
public required InteriorEntityPartition.Result Partition { get; init; }
|
||||
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
||||
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
||||
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
||||
public InteriorEntityPartition.Result Partition { get; private set; } = null!;
|
||||
|
||||
internal RetailPViewFrameResult Reset(
|
||||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells,
|
||||
InteriorEntityPartition.Result partition)
|
||||
{
|
||||
PortalFrame = portalFrame;
|
||||
ClipAssembly = clipAssembly;
|
||||
DrawableCells = drawableCells;
|
||||
Partition = partition;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RetailPViewLandscapeSliceContext(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue