acdream/src/AcDream.App/Rendering/PortalMeshReferenceOwner.cs
Erik 749e8ceeb1 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>
2026-07-18 21:35:16 +02:00

154 lines
4.8 KiB
C#

using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering;
/// <summary>
/// Owns the synthetic portal creature's mesh references. Acquisition happens
/// only after the presentation has been published to its runtime owner, so a
/// partial backend failure can never strand an unreachable constructor-local
/// reference. Per-reference physical state makes both acquisition and teardown
/// resumable without replaying successful mutations.
/// </summary>
internal sealed class PortalMeshReferenceOwner : IDisposable
{
private sealed class ReferenceState(ulong gfxObjId)
{
public ulong GfxObjId { get; } = gfxObjId;
public bool Held { get; set; }
}
private readonly IWbMeshAdapter _meshAdapter;
private readonly ReferenceState[] _references;
private bool _desired;
private bool _disposeRequested;
private bool _disposed;
private bool _reconciling;
private bool _reconcileAgain;
public PortalMeshReferenceOwner(
IWbMeshAdapter meshAdapter,
IEnumerable<ulong> gfxObjIds)
{
_meshAdapter = meshAdapter ?? throw new ArgumentNullException(nameof(meshAdapter));
ArgumentNullException.ThrowIfNull(gfxObjIds);
_references = gfxObjIds
.Where(static id => id != 0u)
.Distinct()
.Select(static id => new ReferenceState(id))
.ToArray();
}
public bool IsDisposed => _disposed;
public void Acquire()
{
ObjectDisposedException.ThrowIf(_disposeRequested, this);
_desired = true;
try
{
Reconcile();
ObjectDisposedException.ThrowIf(_disposeRequested, this);
}
catch (Exception acquisitionFailure)
{
// Acquisition is transactional even though the owner is already
// reachable. Roll back every committed sibling; a failed rollback
// remains represented by Held=true so Dispose can resume it.
_desired = false;
try
{
Reconcile();
}
catch (Exception rollbackFailure)
{
throw new AggregateException(
"Portal mesh acquisition failed and its rollback did not fully converge.",
acquisitionFailure,
rollbackFailure);
}
System.Runtime.ExceptionServices.ExceptionDispatchInfo
.Capture(acquisitionFailure)
.Throw();
throw new InvalidOperationException("Unreachable exception dispatch path.");
}
}
public void Dispose()
{
if (_disposed)
return;
_disposeRequested = true;
_desired = false;
if (_reconciling)
{
_reconcileAgain = true;
return;
}
Reconcile();
}
private void Reconcile()
{
if (_reconciling)
{
_reconcileAgain = true;
return;
}
_reconciling = true;
List<Exception>? failures = null;
try
{
do
{
_reconcileAgain = false;
for (int i = 0; i < _references.Length; i++)
{
ReferenceState reference = _references[i];
bool targetHeld = _desired;
if (reference.Held == targetHeld)
continue;
try
{
if (targetHeld)
_meshAdapter.IncrementRefCount(reference.GfxObjId);
else
_meshAdapter.DecrementRefCount(reference.GfxObjId);
reference.Held = targetHeld;
}
catch (Exception error)
{
if (error is MeshReferenceMutationException
{
MutationCommitted: true,
})
{
reference.Held = targetHeld;
}
string operation = targetHeld ? "acquisition" : "release";
(failures ??= []).Add(new InvalidOperationException(
$"Portal mesh 0x{reference.GfxObjId:X10} reference {operation} failed.",
error));
}
}
}
while (_reconcileAgain);
}
finally
{
_reconciling = false;
if (_disposeRequested && _references.All(static reference => !reference.Held))
_disposed = true;
}
if (failures is not null)
throw new AggregateException(
"One or more portal mesh references failed to reconcile.",
failures);
}
}