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:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -14,9 +14,26 @@ namespace AcDream.App.Rendering;
/// <summary>Per-frame output of the portal-frame BFS.</summary>
public sealed class PortalVisibilityFrame
{
private const int MaxRetainedCellViews = 512;
internal const int MaxRetainedBuildCollectionCapacity = 512;
internal const int CapacityTrimIdleFrames = 120;
private readonly Stack<CellView> _cellViewPool = new();
private readonly PortalPolygonVertexStore _polygonVertices = new();
private int _cellViewsUnderusedFrames;
private int _crossBuildingViewsUnderusedFrames;
private int _queuedUnderusedFrames;
private int _drawListedUnderusedFrames;
private int _processedViewCountsUnderusedFrames;
private int _orderedVisibleCellsUnderusedFrames;
private int _todoUnderusedFrames;
internal PortalPolygonVertexStore PolygonVertices => _polygonVertices;
internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount;
internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount;
/// <summary>Screen region (NDC) where outdoor terrain/scenery may draw — exit portals
/// recursively clipped to their portal chain. The cellar-flap fix.</summary>
public CellView OutsideView { get; } = new();
public CellView OutsideView { get; private set; } = new();
/// <summary>Per-cell accumulated clip region, keyed by full cell id (wire-in #2).</summary>
public Dictionary<uint, CellView> CellViews { get; } = new();
@ -31,6 +48,199 @@ public sealed class PortalVisibilityFrame
/// <summary>Entry clip regions for other buildings reached through our portals, keyed by the
/// neighbour cell id that left the camera building's cell set (wire-in #3 / Step 5).</summary>
public Dictionary<uint, CellView> CrossBuildingViews { get; } = new();
// Build scratch belongs to the frame so a caller that reuses a frame also reuses the large
// hash tables and the 128-entry convergence trace. This is especially important outdoors,
// where retail runs one small ConstructView flood per nearby building every frame.
internal HashSet<uint> QueuedScratch { get; } = new();
internal HashSet<uint> DrawListedScratch { get; } = new();
internal Dictionary<uint, int> ProcessedViewCountsScratch { get; } = new();
internal uint[] PropagationChainScratch { get; } = new uint[128];
internal List<(LoadedCell Cell, float Distance)> TodoScratch { get; } = new();
// A build can recurse while an outer portal's clipped region is still live, so one mutable
// singleton is unsafe. Leases reuse Lists by recursion lifetime rather than by total portal-call
// count: ordinary sequential portals need one List, while nested AdjustCellView propagation gets
// one per active depth. Dispose returns scratch on every continue/exception path.
private readonly Stack<List<ViewPolygon>> _clipRegionPool = new();
private const int MaxRetainedClipRegionLists = 132; // recursion tripwire (128) + nested side work
private const int MaxRetainedClipRegionCapacity = 256;
internal int ClipRegionScratchCount => _clipRegionPool.Count;
internal int RetainedCellViewCount => _cellViewPool.Count;
internal int CellViewMapCapacity => CellViews.EnsureCapacity(0);
internal int CrossBuildingViewMapCapacity => CrossBuildingViews.EnsureCapacity(0);
internal int QueuedCapacity => QueuedScratch.EnsureCapacity(0);
internal int DrawListedCapacity => DrawListedScratch.EnsureCapacity(0);
internal int ProcessedViewCountCapacity => ProcessedViewCountsScratch.EnsureCapacity(0);
internal ClipRegionScratchLease RentClipRegionScratch()
{
List<ViewPolygon> region = _clipRegionPool.Count != 0
? _clipRegionPool.Pop()
: new List<ViewPolygon>(2);
region.Clear();
return new ClipRegionScratchLease(this, region);
}
private void ReturnClipRegionScratch(List<ViewPolygon> region)
{
region.Clear();
if (region.Capacity <= MaxRetainedClipRegionCapacity
&& _clipRegionPool.Count < MaxRetainedClipRegionLists)
_clipRegionPool.Push(region);
}
internal readonly ref struct ClipRegionScratchLease
{
private readonly PortalVisibilityFrame _owner;
public List<ViewPolygon> Region { get; }
internal ClipRegionScratchLease(PortalVisibilityFrame owner, List<ViewPolygon> region)
{
_owner = owner;
Region = region;
}
public void Dispose() => _owner.ReturnClipRegionScratch(Region);
}
internal void ResetForBuild()
{
int cellViewCount = CellViews.Count;
int crossBuildingViewCount = CrossBuildingViews.Count;
int queuedCount = QueuedScratch.Count;
int drawListedCount = DrawListedScratch.Count;
int processedViewCount = ProcessedViewCountsScratch.Count;
int orderedVisibleCellCount = OrderedVisibleCells.Count;
int todoCount = TodoScratch.Count;
if (OutsideView.IsRetainable)
OutsideView.Reset();
else
OutsideView = new CellView();
ReturnCellViews(CellViews);
ReturnCellViews(CrossBuildingViews);
CellViews.Clear();
OrderedVisibleCells.Clear();
CrossBuildingViews.Clear();
QueuedScratch.Clear();
DrawListedScratch.Clear();
ProcessedViewCountsScratch.Clear();
TodoScratch.Clear();
_polygonVertices.ResetUsage();
// These collections live as long as RetailPViewRenderer. A fixed
// capacity cutoff is unsafe here: a legitimate large portal flood can
// exceed it every frame, causing us to discard and rebuild the same
// warmed tables forever. Retire high-water storage only after it has
// remained at least 50% underused for a sustained idle window. Once a
// collection is right-sized for its recurring workload it stays warm.
TrimIfCold(CellViews, cellViewCount, ref _cellViewsUnderusedFrames);
TrimIfCold(
CrossBuildingViews,
crossBuildingViewCount,
ref _crossBuildingViewsUnderusedFrames);
TrimIfCold(QueuedScratch, queuedCount, ref _queuedUnderusedFrames);
TrimIfCold(DrawListedScratch, drawListedCount, ref _drawListedUnderusedFrames);
TrimIfCold(
ProcessedViewCountsScratch,
processedViewCount,
ref _processedViewCountsUnderusedFrames);
TrimIfCold(
OrderedVisibleCells,
orderedVisibleCellCount,
ref _orderedVisibleCellsUnderusedFrames);
TrimIfCold(TodoScratch, todoCount, ref _todoUnderusedFrames);
}
internal ViewPolygon CopyPolygon(ReadOnlySpan<Vector2> vertices)
{
if (vertices.Length < 3)
return default;
Vector2[] owned = _polygonVertices.Rent(vertices.Length);
vertices.CopyTo(owned);
return new ViewPolygon(owned);
}
internal CellView RentCellView(bool fullScreen = false)
{
CellView result = _cellViewPool.Count != 0
? _cellViewPool.Pop()
: new CellView();
if (fullScreen)
result.SetFullScreen();
else
result.Reset();
return result;
}
private void ReturnCellViews(Dictionary<uint, CellView> views)
{
foreach (CellView view in views.Values)
{
view.Reset();
if (view.IsRetainable && _cellViewPool.Count < MaxRetainedCellViews)
_cellViewPool.Push(view);
}
}
private static void TrimIfCold<TKey, TValue>(
Dictionary<TKey, TValue> values,
int usedCount,
ref int underusedFrames)
where TKey : notnull
{
int capacity = values.EnsureCapacity(0);
if (!ShouldTrim(capacity, usedCount, ref underusedFrames))
return;
if (capacity > MaxRetainedBuildCollectionCapacity)
values.TrimExcess();
}
private static void TrimIfCold<T>(
HashSet<T> values,
int usedCount,
ref int underusedFrames)
{
int capacity = values.EnsureCapacity(0);
if (!ShouldTrim(capacity, usedCount, ref underusedFrames))
return;
if (capacity > MaxRetainedBuildCollectionCapacity)
values.TrimExcess();
}
private static void TrimIfCold<T>(
List<T> values,
int usedCount,
ref int underusedFrames)
{
int capacity = values.Capacity;
if (!ShouldTrim(capacity, usedCount, ref underusedFrames))
return;
if (capacity > MaxRetainedBuildCollectionCapacity)
values.Capacity = 0;
}
private static bool ShouldTrim(int capacity, int usedCount, ref int underusedFrames)
{
if (capacity <= MaxRetainedBuildCollectionCapacity
|| (long)usedCount * 2L > capacity)
{
underusedFrames = 0;
return false;
}
underusedFrames++;
if (underusedFrames < CapacityTrimIdleFrames)
return false;
underusedFrames = 0;
return true;
}
}
public static class PortalVisibilityBuilder
@ -121,16 +331,18 @@ public static class PortalVisibilityBuilder
Func<uint, LoadedCell?> lookup,
Matrix4x4 viewProj,
Func<uint, bool>? buildingMembership = null,
float drawLiftZ = 0f)
float drawLiftZ = 0f,
PortalVisibilityFrame? reuseFrame = null)
{
var frame = new PortalVisibilityFrame();
var frame = reuseFrame ?? new PortalVisibilityFrame();
frame.ResetForBuild();
if (cameraCell == null) return frame;
// Interior portals never cross landblocks (same invariant as CellVisibility.GetVisibleCells);
// building-boundary crossings are handled separately via the buildingMembership escape hatch.
uint lbMask = cameraCell.CellId & 0xFFFF0000u;
frame.CellViews[cameraCell.CellId] = CellView.FullScreen();
frame.CellViews[cameraCell.CellId] = frame.RentCellView(fullScreen: true);
// Render unification (outdoor-as-cell, 2026-06-07): when the root IS the synthetic outdoor
// node, the landscape is visible FULL-SCREEN, so seed OutsideView with the full-screen NDC
@ -142,15 +354,15 @@ public static class PortalVisibilityBuilder
// ids, so an id test would misfire. An interior root never sets this flag, so the indoor
// exit-portal path (OtherCellId==0xFFFF below) still owns the doorway OutsideView region.
if (cameraCell.IsOutdoorNode)
frame.OutsideView.Add(new ViewPolygon((Vector2[])FullScreenQuad.Clone()));
frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad));
// Distance-priority work list (retail PView::cell_todo_list). Cells pop closest-first;
// each cell carries the camera→nearest-portal-vertex distance that put it on the list
// (retail keys on InitCell's per-portal min-vertex distance, decomp 432988-433004). The
// camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it
// always pops first.
var todo = new CellTodoList();
todo.Insert(cameraCell, 0f);
List<(LoadedCell Cell, float Distance)> todo = frame.TodoScratch;
InsertTodo(todo, cameraCell, 0f);
// Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the
// retail portal_view slice offset 0x44 (last-incorporated view-poly watermark) vs 0x38
@ -162,9 +374,10 @@ public static class PortalVisibilityBuilder
// the instant a cell is popped). Enqueue-once across the cell set is the hard termination
// guarantee for cyclic / hub / diamond graphs: at most N cells are ever processed. The
// camera cell is pre-marked so a portal looping back to it can never re-enqueue it.
var queued = new HashSet<uint> { cameraCell.CellId };
var drawListed = new HashSet<uint>();
var processedViewCounts = new Dictionary<uint, int>();
HashSet<uint> queued = frame.QueuedScratch;
queued.Add(cameraCell.CellId);
HashSet<uint> drawListed = frame.DrawListedScratch;
Dictionary<uint, int> processedViewCounts = frame.ProcessedViewCountsScratch;
var trace = PortalBuildTrace.Start(cameraCell, cameraPos);
// [portal-churn] apparatus (2026-06-08): when ProbePortalChurnEnabled, accumulate re-enqueue churn
@ -224,7 +437,7 @@ public static class PortalVisibilityBuilder
// reproduce the T5 firing — production-only ingredients (full lookup
// graph / real camera path) are suspected; this dump pins them on the
// next natural occurrence.
var propagationChain = new uint[RecursionTripwire];
uint[] propagationChain = frame.PropagationChainScratch;
void ProcessCellPortals(LoadedCell cell, int depth)
{
@ -251,7 +464,6 @@ public static class PortalVisibilityBuilder
}
trace?.Add($"proc cell=0x{cell.CellId:X8} processed={processedCount}->{endCount} depth={depth}");
var activeViewPolygons = currentView.Polygons.GetRange(processedCount, endCount - processedCount);
processedViewCounts[cell.CellId] = endCount;
for (int i = 0; i < cell.Portals.Count; i++)
@ -301,11 +513,18 @@ public static class PortalVisibilityBuilder
// homogeneous clip space, clip at the eye, then clip against the current
// portal_view region before the divide. Do the same here; the old early
// ProjectToNdc + 2D intersect path is too unstable for near/grazing doorways.
var clippedRegion = ClipPortalAgainstView(
using PortalVisibilityFrame.ClipRegionScratchLease clippedRegionLease =
frame.RentClipRegionScratch();
List<ViewPolygon> clippedRegion = clippedRegionLease.Region;
ClipPortalAgainstView(
frame,
poly,
cell.WorldTransform,
viewProj,
activeViewPolygons,
currentView.Polygons,
processedCount,
endCount - processedCount,
clippedRegion,
out int clipVerts);
if (dx) Console.WriteLine($"[pv-dump] EXIT-PROJ cell=0x{cell.CellId:X8} p{i} localN={poly.Length} clipN={clipVerts} local0=({poly[0].X:F2},{poly[0].Y:F2},{poly[0].Z:F2})");
if (dx) Console.WriteLine($"[pv-dump] EXIT-CLIP cell=0x{cell.CellId:X8} p{i} currentViewPolys={currentView.Polygons.Count} clipResult={clippedRegion.Count}");
@ -339,16 +558,31 @@ public static class PortalVisibilityBuilder
// lifted space or terrain stops a lift-height short of the
// drawn lintel (#130 strip). Flood semantics keep the
// unlifted clippedRegion path above.
var outsideRegion = drawLiftZ == 0f
? clippedRegion
: ClipPortalAgainstView(
int outsideCount;
if (drawLiftZ == 0f)
{
AddRegion(frame.OutsideView, clippedRegion);
outsideCount = clippedRegion.Count;
}
else
{
using PortalVisibilityFrame.ClipRegionScratchLease outsideRegionLease =
frame.RentClipRegionScratch();
List<ViewPolygon> outsideRegion = outsideRegionLease.Region;
ClipPortalAgainstView(
frame,
poly,
cell.WorldTransform * Matrix4x4.CreateTranslation(0f, 0f, drawLiftZ),
viewProj,
activeViewPolygons,
currentView.Polygons,
processedCount,
endCount - processedCount,
outsideRegion,
out _);
AddRegion(frame.OutsideView, outsideRegion);
trace?.Add($"portal cell=0x{cell.CellId:X8} p{i}->EXIT addOutside={outsideRegion.Count} clipVerts={clipVerts}");
AddRegion(frame.OutsideView, outsideRegion);
outsideCount = outsideRegion.Count;
}
trace?.Add($"portal cell=0x{cell.CellId:X8} p{i}->EXIT addOutside={outsideCount} clipVerts={clipVerts}");
continue;
}
@ -360,7 +594,7 @@ public static class PortalVisibilityBuilder
// call inside ConstructView at decomp:433692.)
if (buildingMembership != null && !buildingMembership(neighbourId))
{
var xview = GetOrCreate(frame.CrossBuildingViews, neighbourId);
var xview = GetOrCreate(frame, frame.CrossBuildingViews, neighbourId);
bool grewCross = AddRegion(xview, clippedRegion);
trace?.Add($"portal cell=0x{cell.CellId:X8} p{i}->0x{neighbourId:X8} crossBldg polys={clippedRegion.Count} grew={grewCross}");
continue;
@ -391,7 +625,8 @@ public static class PortalVisibilityBuilder
// neighbour's side; the old eye-in-opening restore was part of
// the deleted rescue.
int preReciprocalCount = clippedRegion.Count;
ApplyReciprocalClip(clippedRegion, portal.OtherPortalId, portal.Flags, neighbour, viewProj);
ApplyReciprocalClip(
frame, clippedRegion, portal.OtherPortalId, portal.Flags, neighbour, viewProj);
if (churnProbe)
churnReciprocal!.Append(System.FormattableString.Invariant(
$" recip[0x{neighbourId:X8} {preReciprocalCount}->{clippedRegion.Count}]"));
@ -402,7 +637,7 @@ public static class PortalVisibilityBuilder
}
// Union the clipped region into the neighbour's accumulated view.
var nview = GetOrCreate(frame.CellViews, neighbourId);
var nview = GetOrCreate(frame, frame.CellViews, neighbourId);
bool grew = AddRegion(nview, clippedRegion);
bool inserted = false;
bool inPlace = false;
@ -417,7 +652,7 @@ public static class PortalVisibilityBuilder
if (queued.Add(neighbourId))
{
dist = NearestPortalVertexDistance(poly, cell.WorldTransform, cameraPos);
todo.Insert(neighbour, dist);
InsertTodo(todo, neighbour, dist);
inserted = true;
}
// Growth into an already-POPPED cell → retail AdjustCellView:
@ -437,7 +672,7 @@ public static class PortalVisibilityBuilder
while (todo.Count > 0)
{
var cell = todo.PopNearest();
LoadedCell cell = PopNearest(todo);
// Single pop per cell (enqueue-once) IS the cell's closest-first
// draw position (retail appends to cell_draw_list once per pop,
@ -491,13 +726,15 @@ public static class PortalVisibilityBuilder
Func<uint, LoadedCell?> lookup,
Matrix4x4 viewProj,
float maxSeedDistance = float.PositiveInfinity,
IReadOnlyList<ViewPolygon>? seedRegion = null)
IReadOnlyList<ViewPolygon>? seedRegion = null,
PortalVisibilityFrame? reuseFrame = null)
{
var frame = new PortalVisibilityFrame();
var todo = new CellTodoList();
var queued = new HashSet<uint>();
var drawListed = new HashSet<uint>();
var processedViewCounts = new Dictionary<uint, int>();
var frame = reuseFrame ?? new PortalVisibilityFrame();
frame.ResetForBuild();
List<(LoadedCell Cell, float Distance)> todo = frame.TodoScratch;
HashSet<uint> queued = frame.QueuedScratch;
HashSet<uint> drawListed = frame.DrawListedScratch;
Dictionary<uint, int> processedViewCounts = frame.ProcessedViewCountsScratch;
foreach (var cell in candidateCells)
{
@ -534,11 +771,19 @@ public static class PortalVisibilityBuilder
if (seedDistance > maxSeedDistance)
continue;
var clippedRegion = ClipPortalAgainstView(
using PortalVisibilityFrame.ClipRegionScratchLease clippedRegionLease =
frame.RentClipRegionScratch();
List<ViewPolygon> clippedRegion = clippedRegionLease.Region;
IReadOnlyList<ViewPolygon> activeSeedRegion = seedRegion ?? FullScreenRegion;
ClipPortalAgainstView(
frame,
poly,
cell.WorldTransform,
viewProj,
seedRegion ?? FullScreenRegion,
activeSeedRegion,
0,
activeSeedRegion.Count,
clippedRegion,
out _);
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
@ -547,11 +792,11 @@ public static class PortalVisibilityBuilder
if (clippedRegion.Count == 0)
continue;
var seedView = GetOrCreate(frame.CellViews, cell.CellId);
var seedView = GetOrCreate(frame, frame.CellViews, cell.CellId);
bool grew = AddRegion(seedView, clippedRegion);
if (grew && queued.Add(cell.CellId))
todo.Insert(cell, seedDistance);
InsertTodo(todo, cell, seedDistance);
}
}
@ -560,7 +805,7 @@ public static class PortalVisibilityBuilder
// re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues
// are deleted (empty clip culls, period).
const int RecursionTripwire = 128;
var propagationChain = new uint[RecursionTripwire]; // #120 self-attribution — see Build()
uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build()
void ProcessCellPortals(LoadedCell cell, int depth)
{
@ -580,7 +825,6 @@ public static class PortalVisibilityBuilder
if (processedCount >= endCount)
return;
var activeViewPolygons = currentView.Polygons.GetRange(processedCount, endCount - processedCount);
processedViewCounts[cell.CellId] = endCount;
uint lbMask = cell.CellId & 0xFFFF0000u;
@ -602,11 +846,18 @@ public static class PortalVisibilityBuilder
&& !CameraOnInteriorSide(cell, i, cameraPos))
continue;
var clippedRegion = ClipPortalAgainstView(
using PortalVisibilityFrame.ClipRegionScratchLease clippedRegionLease =
frame.RentClipRegionScratch();
List<ViewPolygon> clippedRegion = clippedRegionLease.Region;
ClipPortalAgainstView(
frame,
poly,
cell.WorldTransform,
viewProj,
activeViewPolygons,
currentView.Polygons,
processedCount,
endCount - processedCount,
clippedRegion,
out _);
if (clippedRegion.Count == 0)
@ -617,11 +868,12 @@ public static class PortalVisibilityBuilder
if (neighbour == null)
continue;
ApplyReciprocalClip(clippedRegion, portal.OtherPortalId, portal.Flags, neighbour, viewProj);
ApplyReciprocalClip(
frame, clippedRegion, portal.OtherPortalId, portal.Flags, neighbour, viewProj);
if (clippedRegion.Count == 0)
continue;
var nview = GetOrCreate(frame.CellViews, neighbourId);
var nview = GetOrCreate(frame, frame.CellViews, neighbourId);
bool grew = AddRegion(nview, clippedRegion);
if (grew)
@ -629,7 +881,7 @@ public static class PortalVisibilityBuilder
if (queued.Add(neighbourId))
{
float dist = NearestPortalVertexDistance(poly, cell.WorldTransform, cameraPos);
todo.Insert(neighbour, dist);
InsertTodo(todo, neighbour, dist);
}
else if (drawListed.Contains(neighbourId))
{
@ -641,7 +893,7 @@ public static class PortalVisibilityBuilder
while (todo.Count > 0)
{
var cell = todo.PopNearest();
LoadedCell cell = PopNearest(todo);
if (drawListed.Add(cell.CellId))
frame.OrderedVisibleCells.Add(cell.CellId);
@ -669,8 +921,16 @@ public static class PortalVisibilityBuilder
Func<uint, LoadedCell?> lookup,
Matrix4x4 viewProj,
float maxSeedDistance = float.PositiveInfinity,
IReadOnlyList<ViewPolygon>? seedRegion = null)
=> BuildFromExterior(buildingCells, cameraPos, lookup, viewProj, maxSeedDistance, seedRegion);
IReadOnlyList<ViewPolygon>? seedRegion = null,
PortalVisibilityFrame? reuseFrame = null)
=> BuildFromExterior(
buildingCells,
cameraPos,
lookup,
viewProj,
maxSeedDistance,
seedRegion,
reuseFrame);
// The NDC [-1,1] viewport quad (CCW), reused by the flap probe's clip recompute.
private static readonly Vector2[] FullScreenQuad =
@ -679,30 +939,35 @@ public static class PortalVisibilityBuilder
private static readonly ViewPolygon[] FullScreenRegion =
{ new ViewPolygon(FullScreenQuad) };
private static List<ViewPolygon> ClipPortalAgainstView(
private static void ClipPortalAgainstView(
PortalVisibilityFrame frame,
Vector3[] localPoly,
Matrix4x4 cellToWorld,
Matrix4x4 viewProj,
IReadOnlyList<ViewPolygon> viewPolygons,
int viewStart,
int viewCount,
List<ViewPolygon> clippedRegion,
out int clipVertexCount)
{
var portalClip = PortalProjection.ProjectToClip(localPoly, cellToWorld, viewProj);
clipVertexCount = portalClip.Length;
var clippedRegion = new List<ViewPolygon>();
if (portalClip.Length < 3)
return clippedRegion;
using PortalProjection.ClipPolygonLease portalClip =
PortalProjection.ProjectToClipLease(localPoly, cellToWorld, viewProj);
clipVertexCount = portalClip.Count;
if (portalClip.Count < 3)
return;
foreach (var vp in viewPolygons)
int viewEnd = viewStart + viewCount;
for (int i = viewStart; i < viewEnd; i++)
{
ViewPolygon vp = viewPolygons[i];
if (vp.IsEmpty)
continue;
var clipped = PortalProjection.ClipToRegion(portalClip, vp.Vertices);
var clipped = PortalProjection.ClipToRegion(
portalClip.Span, vp.Vertices, frame.PolygonVertices);
if (clipped.Length >= 3)
clippedRegion.Add(new ViewPolygon(clipped));
}
return clippedRegion;
}
private const int PortalTraceEmitLimit = 160;
@ -913,6 +1178,7 @@ public static class PortalVisibilityBuilder
private const ushort PortalFlagExactMatch = 0x0001;
private static void ApplyReciprocalClip(
PortalVisibilityFrame frame,
List<ViewPolygon> clippedRegion, ushort otherPortalId, ushort portalFlags,
LoadedCell neighbour, Matrix4x4 viewProj)
{
@ -944,23 +1210,37 @@ public static class PortalVisibilityBuilder
// pins this deterministically; the glitch steps die with this change). The old path's other
// rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete:
// CanonicalKey's 1e-3-grid snap dedup (2026-06-06) absorbs re-clip drift by construction.
var reciprocalClip = PortalProjection.ProjectToClip(reciprocalPoly, neighbour.WorldTransform, viewProj);
if (reciprocalClip.Length < 3) return; // reciprocal entirely behind the eye → no constraint (over-include)
using PortalProjection.ClipPolygonLease reciprocalClip =
PortalProjection.ProjectToClipLease(
reciprocalPoly,
neighbour.WorldTransform,
viewProj);
if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include)
// Intersect the reciprocal opening into each near-side polygon; drop any that fall away.
// ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same
// region-edge homogeneous Sutherland-Hodgman the forward hop uses (polyClipFinish port).
for (int k = clippedRegion.Count - 1; k >= 0; k--)
{
var tightened = PortalProjection.ClipToRegion(reciprocalClip, clippedRegion[k].Vertices);
var tightened = PortalProjection.ClipToRegion(
reciprocalClip.Span,
clippedRegion[k].Vertices,
frame.PolygonVertices);
if (tightened.Length >= 3) clippedRegion[k] = new ViewPolygon(tightened);
else clippedRegion.RemoveAt(k);
}
}
private static CellView GetOrCreate(Dictionary<uint, CellView> map, uint key)
private static CellView GetOrCreate(
PortalVisibilityFrame frame,
Dictionary<uint, CellView> map,
uint key)
{
if (!map.TryGetValue(key, out var v)) { v = new CellView(); map[key] = v; }
if (!map.TryGetValue(key, out var v))
{
v = frame.RentCellView();
map[key] = v;
}
return v;
}
@ -998,30 +1278,22 @@ public static class PortalVisibilityBuilder
/// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST —
/// LIFO on ties, matching retail's break-on-first-not-greater + pop-from-tail.
/// </summary>
private sealed class CellTodoList
private static void InsertTodo(
List<(LoadedCell Cell, float Distance)> items,
LoadedCell cell,
float distance)
{
private readonly List<(LoadedCell Cell, float Distance)> _items = new();
int index = items.Count;
while (index > 0 && items[index - 1].Distance < distance)
index--;
items.Insert(index, (cell, distance));
}
public int Count => _items.Count;
public void Insert(LoadedCell cell, float distance)
{
// Find the slot: scan from the tail (nearest) toward the head while existing entries are
// strictly nearer than `distance`, so the newcomer lands just ABOVE every entry that is
// farther-or-equal — i.e. nearest-at-tail order, LIFO on ties (an equal-distance
// newcomer inserts at the tail and pops first).
int idx = _items.Count;
while (idx > 0 && _items[idx - 1].Distance < distance)
idx--;
_items.Insert(idx, (cell, distance));
}
public LoadedCell PopNearest()
{
int last = _items.Count - 1;
var cell = _items[last].Cell;
_items.RemoveAt(last);
return cell;
}
private static LoadedCell PopNearest(List<(LoadedCell Cell, float Distance)> items)
{
int last = items.Count - 1;
LoadedCell cell = items[last].Cell;
items.RemoveAt(last);
return cell;
}
}