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

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
@ -455,4 +456,237 @@ public class PortalProjectionTests
$"non-finite NDC vert leaked from the divide: ({v.X},{v.Y})");
}
}
[Fact]
public void ProjectToClipLease_ReusesPooledWorkWithoutResultArrays()
{
var opening = new[]
{
new Vector3(-1f, -1f, -3f),
new Vector3(1f, -1f, -3f),
new Vector3(1f, 1f, -3f),
new Vector3(-1f, 1f, -3f),
};
Matrix4x4 viewProjection = ViewProj();
using (PortalProjection.ClipPolygonLease warm =
PortalProjection.ProjectToClipLease(
opening,
Matrix4x4.Identity,
viewProjection))
{
Assert.Equal(4, warm.Count);
}
long before = GC.GetAllocatedBytesForCurrentThread();
int totalVertices = 0;
for (int i = 0; i < 1_000; i++)
{
using PortalProjection.ClipPolygonLease lease =
PortalProjection.ProjectToClipLease(
opening,
Matrix4x4.Identity,
viewProjection);
totalVertices += lease.Count;
}
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(4_000, totalVertices);
// A tiered-JIT/ArrayPool bookkeeping transition can contribute a few hundred fixed bytes
// to the first measured batch on some runtimes. Keep the ceiling far below the former
// per-call result-array regression (~448 KB / 1,000 calls), while accepting that fixed
// process noise so this test measures linear hot-path allocation rather than JIT timing.
Assert.True(allocated <= 1_024, $"pooled projections allocated {allocated:N0} bytes");
}
[Fact]
public void ClipToRegion_FrameOwnedStore_ReusesExactResultArray()
{
var opening = new[]
{
new Vector3(-1f, -1f, -5f),
new Vector3(1f, -1f, -5f),
new Vector3(1f, 1f, -5f),
new Vector3(-1f, 1f, -5f),
};
Vector4[] subject = PortalProjection.ProjectToClip(
opening, Matrix4x4.Identity, ViewProj());
Vector2[] region = FullScreenCcw();
Vector2[] expected = PortalProjection.ClipToRegion(subject, region);
var store = new PortalPolygonVertexStore();
Vector2[] first = PortalProjection.ClipToRegion(
subject.AsSpan(), region, store);
Vector2[] firstSnapshot = (Vector2[])first.Clone();
int allocationHighWater = store.AllocationCount;
store.ResetUsage();
Vector2[] second = PortalProjection.ClipToRegion(
subject.AsSpan(), region, store);
Assert.Same(first, second);
Assert.Equal(allocationHighWater, store.AllocationCount);
Assert.Equal(expected, firstSnapshot);
Assert.Equal(expected, second);
long before = GC.GetAllocatedBytesForCurrentThread();
float checksum = 0f;
for (int i = 0; i < 1_000; i++)
{
store.ResetUsage();
Vector2[] reused = PortalProjection.ClipToRegion(
subject.AsSpan(), region, store);
checksum += reused[0].X;
}
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.True(float.IsFinite(checksum));
Assert.True(allocated <= 4_096,
$"frame-owned portal clipping allocated {allocated:N0} bytes");
}
[Fact]
public void ProjectToNdc_SecondRentFailure_ReturnsFirstBuffer()
{
var pool = new ThrowOnSecondRentPool<Vector4>();
Vector3[] opening =
[
new(-1f, -1f, -5f),
new(1f, -1f, -5f),
new(1f, 1f, -5f),
new(-1f, 1f, -5f),
];
Assert.Throws<InjectedRentException>(() =>
PortalProjection.ProjectToNdc(
opening,
Matrix4x4.Identity,
ViewProj(),
pool));
Assert.Equal(0, pool.OutstandingCount);
}
[Fact]
public void ProjectToClipLease_SecondRentFailure_ReturnsFirstBuffer()
{
var pool = new ThrowOnSecondRentPool<Vector4>();
Vector3[] opening =
[
new(-1f, -1f, -5f),
new(1f, -1f, -5f),
new(1f, 1f, -5f),
new(-1f, 1f, -5f),
];
try
{
using PortalProjection.ClipPolygonLease _ =
PortalProjection.ProjectToClipLease(
opening,
Matrix4x4.Identity,
ViewProj(),
pool);
Assert.Fail("The injected second-rent failure was not observed.");
}
catch (InjectedRentException)
{
// Expected: ownership of the first rent must already be reconciled.
}
Assert.Equal(0, pool.OutstandingCount);
}
[Fact]
public void ClipToRegion_SecondRentFailure_ReturnsFirstBuffer()
{
var pool = new ThrowOnSecondRentPool<Vector4>();
Vector4[] subject =
[
new(-1f, -1f, 0f, 1f),
new(1f, -1f, 0f, 1f),
new(1f, 1f, 0f, 1f),
new(-1f, 1f, 0f, 1f),
];
Assert.Throws<InjectedRentException>(() =>
PortalProjection.ClipToRegion(
subject.AsSpan(),
FullScreenCcw(),
new PortalPolygonVertexStore(),
pool));
Assert.Equal(0, pool.OutstandingCount);
}
[Fact]
public void ClipPolygonLease_AccessAfterDispose_FailsFast()
{
var pool = new ThrowOnSecondRentPool<Vector4>(throwOnRent: int.MaxValue);
Vector3[] opening =
[
new(-1f, -1f, -5f),
new(1f, -1f, -5f),
new(1f, 1f, -5f),
new(-1f, 1f, -5f),
];
PortalProjection.ClipPolygonLease lease =
PortalProjection.ProjectToClipLease(
opening,
Matrix4x4.Identity,
ViewProj(),
pool);
lease.Dispose();
Assert.Equal(0, pool.OutstandingCount);
try
{
_ = lease.Count;
Assert.Fail("A disposed pooled lease exposed its returned buffer.");
}
catch (ObjectDisposedException)
{
// Expected.
}
try
{
_ = lease.Span.Length;
Assert.Fail("A disposed pooled lease exposed its returned span.");
}
catch (ObjectDisposedException)
{
// Expected.
}
// Same-instance disposal is idempotent and must not return buffers twice.
lease.Dispose();
Assert.Equal(0, pool.OutstandingCount);
}
private sealed class InjectedRentException : Exception { }
private sealed class ThrowOnSecondRentPool<T>(int throwOnRent = 2) : ArrayPool<T>
{
private int _rentCount;
internal int OutstandingCount { get; private set; }
public override T[] Rent(int minimumLength)
{
_rentCount++;
if (_rentCount == throwOnRent)
throw new InjectedRentException();
OutstandingCount++;
return new T[Math.Max(1, minimumLength)];
}
public override void Return(T[] array, bool clearArray = false)
{
Assert.NotNull(array);
Assert.True(OutstandingCount > 0, "A pooled array was returned twice.");
OutstandingCount--;
}
}
}