using AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering; /// /// 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. /// 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 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? 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); } }