using AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering; /// /// Durable, per-emitter teardown ledger. Emitter death removes simulation /// state first, so renderer-owned resources must retain their own retryable /// obligations instead of relying on the death event being raised again. /// internal sealed class ParticleEmitterRetirementTracker { private sealed class RetirementState { public bool MeshReleased; public bool GfxInfoRemoved; public bool TextureOwnerReleased; public bool FailureReported; } private readonly Action _releaseMesh; private readonly Action _removeGfxInfo; private readonly Action _releaseTextureOwner; private readonly Action? _reportFailure; private readonly Dictionary _pending = []; private readonly HashSet _advancingHandles = []; public ParticleEmitterRetirementTracker( Action releaseMesh, Action removeGfxInfo, Action releaseTextureOwner, Action? reportFailure = null) { _releaseMesh = releaseMesh ?? throw new ArgumentNullException(nameof(releaseMesh)); _removeGfxInfo = removeGfxInfo ?? throw new ArgumentNullException(nameof(removeGfxInfo)); _releaseTextureOwner = releaseTextureOwner ?? throw new ArgumentNullException(nameof(releaseTextureOwner)); _reportFailure = reportFailure; } internal int PendingCount => _pending.Count; public void BeginRetirement(int emitterHandle) { if (_advancingHandles.Contains(emitterHandle)) return; if (!_pending.TryGetValue(emitterHandle, out RetirementState? state)) { state = new RetirementState(); _pending.Add(emitterHandle, state); } Advance(emitterHandle, state); } public void RetryPending() { if (_pending.Count == 0) return; int[] handles = [.. _pending.Keys]; for (int i = 0; i < handles.Length; i++) { if (_pending.TryGetValue(handles[i], out RetirementState? state)) Advance(handles[i], state); } } public void CompleteOrThrow() { RetryPending(); if (_pending.Count != 0) throw new InvalidOperationException( $"{_pending.Count} particle-emitter teardown obligation(s) remain incomplete."); } private void Advance(int emitterHandle, RetirementState state) { if (!_advancingHandles.Add(emitterHandle)) return; List? failures = null; try { if (!state.MeshReleased) { try { _releaseMesh(emitterHandle); state.MeshReleased = true; } catch (Exception error) { if (error is MeshReferenceMutationException { MutationCommitted: true }) state.MeshReleased = true; (failures ??= []).Add(error); } } if (!state.GfxInfoRemoved) { try { _removeGfxInfo(emitterHandle); state.GfxInfoRemoved = true; } catch (Exception error) { (failures ??= []).Add(error); } } if (!state.TextureOwnerReleased) { try { _releaseTextureOwner(emitterHandle); state.TextureOwnerReleased = true; } catch (Exception error) { (failures ??= []).Add(error); } } if (state.MeshReleased && state.GfxInfoRemoved && state.TextureOwnerReleased) _pending.Remove(emitterHandle); if (failures is not null && !state.FailureReported) { state.FailureReported = true; _reportFailure?.Invoke(new AggregateException( $"Particle emitter {emitterHandle} teardown will be retried.", failures)); } } finally { _advancingHandles.Remove(emitterHandle); } } }