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

@ -38,6 +38,7 @@
// pass-all" slot 0 is constructed by the consumer directly, not via From().)
// When Count > 0, Planes carries the convex gate and the scissor fields are unused.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
@ -84,6 +85,10 @@ public readonly struct ClipPlaneSet
/// A clip-space point <c>clip</c> is inside iff <c>Vector4.Dot(plane, clip) &gt;= 0</c> for every plane.</summary>
public IReadOnlyList<Vector4> Planes => _planes ?? (IReadOnlyList<Vector4>)Array.Empty<Vector4>();
// The set is immutable after construction. ClipFrameAssembler transfers this exact array into
// its frame-scoped slice instead of cloning every plane payload a second time.
internal Vector4[] PlaneArray => _planes ?? Array.Empty<Vector4>();
/// <summary>True ⇒ the convex-plane budget was exceeded; gate on <see cref="ScissorNdcAabb"/>
/// instead (draw the box). Always false when <see cref="Count"/> &gt; 0 or when the region is empty.</summary>
public bool UseScissorFallback { get; }
@ -119,34 +124,68 @@ public readonly struct ClipPlaneSet
if (region.Polygons.Count > 1)
return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
// Exactly one polygon: normalize winding to CCW, merge collinear edges, then decide.
Vector2[] verts = NormalizeAndMerge(region.Polygons[0].Vertices);
return From(region.Polygons[0]);
}
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
// meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
if (verts.Length < 3)
/// <summary>
/// Reduce one convex view polygon without wrapping it in a temporary <see cref="CellView"/>.
/// The clip-frame assembler already iterates individual retail <c>view_poly</c> slices, so
/// constructing a CellView there needlessly ran the flood's canonical-key/dedup machinery for
/// every slice on every frame. This overload is behavior-identical to the one-polygon branch
/// of <see cref="From(CellView)"/> and avoids that unrelated allocation path.
/// </summary>
public static ClipPlaneSet From(in ViewPolygon polygon)
{
if (polygon.IsEmpty)
return Empty;
// A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
// on ITS own AABB (still a superset of the polygon → over-include, safe).
if (verts.Length > MaxPlanes)
return Scissor(verts);
// Exactly one polygon: normalize winding to CCW and merge collinear edges in reusable
// scratch. The surviving vertices are consumed immediately to build the actual plane
// payload; there is no reason to allocate a second exact-sized polygon array per slice.
Vector2[] input = polygon.Vertices;
Vector2[]? rented = null;
Span<Vector2> verts = input.Length <= 32
? stackalloc Vector2[input.Length]
: (rented = ArrayPool<Vector2>.Shared.Rent(input.Length)).AsSpan(0, input.Length);
// 3..8 edges: emit one inward half-space plane per edge (CCW formula).
var planes = new Vector4[verts.Length];
for (int i = 0; i < verts.Length; i++)
try
{
Vector2 p = verts[i];
Vector2 q = verts[(i + 1) % verts.Length];
Vector2 dir = q - p;
// Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's
// interior (the "left" side of the directed edge p→q).
Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
// Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w:
// dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
int count = NormalizeAndMerge(input, verts);
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
// meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
if (count < 3)
return Empty;
ReadOnlySpan<Vector2> normalized = verts[..count];
// A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
// on ITS own AABB (still a superset of the polygon → over-include, safe).
if (count > MaxPlanes)
return Scissor(normalized);
// 3..8 edges: emit one inward half-space plane per edge (CCW formula). This array is
// the retained GPU-routing payload and therefore the one necessary allocation.
var planes = new Vector4[count];
for (int i = 0; i < count; i++)
{
Vector2 p = normalized[i];
Vector2 q = normalized[(i + 1) % count];
Vector2 dir = q - p;
// Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's
// interior (the "left" side of the directed edge p→q).
Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
// Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w:
// dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
}
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
}
finally
{
if (rented is not null)
ArrayPool<Vector2>.Shared.Return(rented);
}
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
}
private static ClipPlaneSet Scissor(float minX, float minY, float maxX, float maxY) =>
@ -171,41 +210,41 @@ public readonly struct ClipPlaneSet
/// already EnsureCcw's its output, but From() is a public entry point that must be robust to
/// either winding (e.g. a hand-built CellView), so we normalize here too.
/// </summary>
private static Vector2[] NormalizeAndMerge(Vector2[] input)
private static int NormalizeAndMerge(ReadOnlySpan<Vector2> input, Span<Vector2> points)
{
if (input is null || input.Length < 3)
return Array.Empty<Vector2>();
if (input.Length < 3)
return 0;
// 1) Drop exact/near-duplicate consecutive points first so edge directions are well-defined.
var pts = new List<Vector2>(input.Length);
foreach (var v in input)
int count = 0;
foreach (Vector2 vertex in input)
{
if (pts.Count == 0 || (v - pts[^1]).LengthSquared() > DegenerateEdgeLen * DegenerateEdgeLen)
pts.Add(v);
if (count == 0 || (vertex - points[count - 1]).LengthSquared() > DegenerateEdgeLen * DegenerateEdgeLen)
points[count++] = vertex;
}
// Wrap-around duplicate (last == first).
if (pts.Count >= 2 && (pts[^1] - pts[0]).LengthSquared() <= DegenerateEdgeLen * DegenerateEdgeLen)
pts.RemoveAt(pts.Count - 1);
if (pts.Count < 3)
return Array.Empty<Vector2>();
if (count >= 2 && (points[count - 1] - points[0]).LengthSquared() <= DegenerateEdgeLen * DegenerateEdgeLen)
count--;
if (count < 3)
return 0;
// 2) Force CCW winding (positive signed area). perp(dir)=(-y,x) is the inward normal only
// for CCW; if the caller handed us CW, reverse so the plane signs come out inside-positive.
if (SignedArea2(pts) < 0f)
pts.Reverse();
if (SignedArea2(points[..count]) < 0f)
points[..count].Reverse();
// 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same
// way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new
// collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
bool changed = true;
while (changed && pts.Count >= 3)
while (changed && count >= 3)
{
changed = false;
for (int i = 0; i < pts.Count; i++)
for (int i = 0; i < count; i++)
{
Vector2 prev = pts[(i - 1 + pts.Count) % pts.Count];
Vector2 cur = pts[i];
Vector2 next = pts[(i + 1) % pts.Count];
Vector2 prev = points[(i - 1 + count) % count];
Vector2 cur = points[i];
Vector2 next = points[(i + 1) % count];
Vector2 d0 = cur - prev;
Vector2 d1 = next - cur;
@ -213,7 +252,8 @@ public readonly struct ClipPlaneSet
float l1 = d1.Length();
if (l0 < DegenerateEdgeLen || l1 < DegenerateEdgeLen)
{
pts.RemoveAt(i);
points[(i + 1)..count].CopyTo(points[i..]);
count--;
changed = true;
break;
}
@ -224,34 +264,35 @@ public readonly struct ClipPlaneSet
float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
if (dot > 0f && MathF.Abs(cross) < CollinearSinEps)
{
pts.RemoveAt(i); // cur lies on the straight line prev→next
points[(i + 1)..count].CopyTo(points[i..]);
count--; // cur lies on the straight line prev→next
changed = true;
break;
}
}
}
if (pts.Count < 3)
return Array.Empty<Vector2>();
if (count < 3)
return 0;
// Final degeneracy gate: a polygon with negligible area is a line/point even if it still
// has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5°
// merge didn't quite collapse). Emitting its planes would yield an empty half-space
// intersection that silently gates out everything; report it honestly as nothing-visible.
if (MathF.Abs(SignedArea2(pts)) * 0.5f < MinPolygonArea)
return Array.Empty<Vector2>();
if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea)
return 0;
return pts.ToArray();
return count;
}
// Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW.
private static float SignedArea2(List<Vector2> poly)
private static float SignedArea2(ReadOnlySpan<Vector2> poly)
{
float a = 0f;
for (int i = 0; i < poly.Count; i++)
for (int i = 0; i < poly.Length; i++)
{
Vector2 p = poly[i];
Vector2 q = poly[(i + 1) % poly.Count];
Vector2 q = poly[(i + 1) % poly.Length];
a += p.X * q.Y - q.X * p.Y;
}
return a;