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>
102 lines
2.9 KiB
C#
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);
|
|
}
|
|
}
|