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
163
src/AcDream.App/World/CompositeLiveEntityResourceLifecycle.cs
Normal file
163
src/AcDream.App/World/CompositeLiveEntityResourceLifecycle.cs
Normal 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 ?? []);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue