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

@ -0,0 +1,163 @@
using AcDream.Core.World;
namespace AcDream.App.World;
/// <summary>
/// Symmetric multi-owner live-resource transaction. Each owner's acquisition
/// and release is tracked independently so a late failure cannot replay an
/// earlier script/effect teardown on the next tombstone retry.
/// </summary>
internal sealed class CompositeLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
{
internal readonly record struct Owner(
Action<WorldEntity> Register,
Action<WorldEntity> Unregister);
private sealed class OwnerState(int count)
{
public bool[] Held { get; } = new bool[count];
public bool DesiredRegistered { get; set; }
public bool Reconciling { get; set; }
}
private sealed record ReconcileFailures(
List<Exception> Registration,
List<Exception> Release);
private readonly Owner[] _owners;
private readonly Dictionary<WorldEntity, OwnerState> _states =
new(ReferenceEqualityComparer.Instance);
public CompositeLiveEntityResourceLifecycle(params Owner[] owners)
{
ArgumentNullException.ThrowIfNull(owners);
if (owners.Length == 0)
throw new ArgumentException("At least one resource owner is required.", nameof(owners));
_owners = [.. owners];
}
public void Register(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (!_states.TryGetValue(entity, out OwnerState? state))
{
state = new OwnerState(_owners.Length);
_states.Add(entity, state);
}
state.DesiredRegistered = true;
ReconcileFailures? failures = Reconcile(entity, state);
if (failures is null)
return;
if (failures.Registration.Count == 1 && failures.Release.Count == 0)
System.Runtime.ExceptionServices.ExceptionDispatchInfo
.Capture(failures.Registration[0])
.Throw();
throw new AggregateException(
"Live entity resource registration and owner rollback failed.",
failures.Registration.Concat(failures.Release));
}
public void Unregister(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (!_states.TryGetValue(entity, out OwnerState? state))
return;
state.DesiredRegistered = false;
ReconcileFailures? failures = Reconcile(entity, state);
if (failures is not null)
throw new AggregateException(
"One or more live entity resource owners failed to release.",
failures.Registration.Concat(failures.Release));
}
private ReconcileFailures? Reconcile(WorldEntity entity, OwnerState state)
{
if (state.Reconciling)
return null;
state.Reconciling = true;
List<Exception>? registrationFailures = null;
List<Exception>? releaseFailures = null;
try
{
while (true)
{
bool directionChanged = false;
if (state.DesiredRegistered)
{
for (int i = 0; i < _owners.Length; i++)
{
if (!state.DesiredRegistered)
{
directionChanged = true;
break;
}
if (state.Held[i])
continue;
// Entering Register creates the symmetric rollback
// obligation even when the callback partially commits
// and then throws.
state.Held[i] = true;
try
{
_owners[i].Register(entity);
}
catch (Exception error)
{
(registrationFailures ??= []).Add(error);
state.DesiredRegistered = false;
directionChanged = true;
break;
}
}
}
if (!state.DesiredRegistered)
{
for (int i = _owners.Length - 1; i >= 0; i--)
{
if (state.DesiredRegistered)
{
directionChanged = true;
break;
}
if (!state.Held[i])
continue;
try
{
_owners[i].Unregister(entity);
state.Held[i] = false;
}
catch (Exception error)
{
(releaseFailures ??= []).Add(error);
}
}
if (state.DesiredRegistered)
directionChanged = true;
}
if (!directionChanged)
break;
}
}
finally
{
state.Reconciling = false;
if (!state.DesiredRegistered && !state.Held.Contains(true))
_states.Remove(entity);
}
return registrationFailures is null && releaseFailures is null
? null
: new ReconcileFailures(
registrationFailures ?? [],
releaseFailures ?? []);
}
}