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
|
|
@ -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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue