acdream/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs
Erik c87b15303d refactor(lifetime): own render resource prefixes
Give terrain, sky, retained UI, portal preparation, and the update/render frame pair explicit single owners. Make shader, texture, text, bindless, and GL construction prefixes checked and retryable so partial failure cannot lose or replay resource ownership.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 14:42:14 +02:00

102 lines
2.9 KiB
C#

namespace AcDream.App.Rendering;
internal interface IRetryableResourceCleanup
{
bool IsCleanupComplete { get; }
void RetryCleanup();
}
internal sealed class GlResourceConstructionException : AggregateException,
IRetryableResourceCleanup
{
private readonly IRetryableResourceCleanup _cleanup;
public GlResourceConstructionException(
string message,
IRetryableResourceCleanup cleanup,
IEnumerable<Exception> failures)
: base(message, failures)
{
_cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
}
public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
public void RetryCleanup() => _cleanup.RetryCleanup();
}
/// <summary>
/// Lifetime root for cleanup work that could not finish before a throwing GL
/// factory returned control. The original exception remains the retry owner;
/// this ledger prevents it and its exact pending names from becoming local-only.
/// </summary>
internal sealed class GlConstructionCleanupLedger : IDisposable
{
private readonly List<IRetryableResourceCleanup> _pending = [];
private bool _disposing;
public bool IsComplete => _pending.Count == 0;
public bool RetainFrom(Exception failure)
{
ArgumentNullException.ThrowIfNull(failure);
bool retained = false;
Visit(failure);
return retained;
void Visit(Exception current)
{
if (current is IRetryableResourceCleanup cleanup)
{
if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup))
_pending.Add(cleanup);
retained = true;
}
if (current is AggregateException aggregate)
{
foreach (Exception inner in aggregate.InnerExceptions)
Visit(inner);
}
else if (current.InnerException is { } inner)
{
Visit(inner);
}
}
}
public void Dispose()
{
if (_disposing || _pending.Count == 0)
return;
_disposing = true;
List<Exception>? failures = null;
try
{
for (int i = _pending.Count - 1; i >= 0; i--)
{
IRetryableResourceCleanup cleanup = _pending[i];
try
{
cleanup.RetryCleanup();
if (cleanup.IsCleanupComplete)
_pending.RemoveAt(i);
}
catch (Exception failure)
{
(failures ??= []).Add(failure);
}
}
}
finally
{
_disposing = false;
}
if (failures is not null)
throw new AggregateException(
"One or more failed GL construction transactions remain pending.",
failures);
}
}