acdream/src/AcDream.App/Rendering/ResourceCleanupGroup.cs
Erik 6a2fe98cc4 refactor(app): compose world rendering startup
Move Region/environment, mandatory modern rendering, terrain, WB, texture, and sampler construction behind the typed Phase-4 composition boundary. Give every fallible GL constructor prefix retryable ownership so partial startup failure cannot leak or replay resource deletion while preserving the accepted render path and DAT inputs.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 16:46:05 +02:00

96 lines
2.9 KiB
C#

namespace AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
/// <summary>
/// Reverse-order, all-attempted cleanup owner used while a composite resource
/// is still under construction and after it becomes the aggregate owner.
/// </summary>
internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
{
private sealed record Entry(string Name, Action Release)
{
public bool Complete { get; set; }
}
private readonly List<Entry> _entries = [];
private bool _running;
public bool IsCleanupComplete => _entries.All(static entry => entry.Complete);
public void Add(string name, Action release)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(release);
if (_running || IsCleanupComplete && _entries.Count != 0)
throw new InvalidOperationException("The resource cleanup group is no longer accepting ownership.");
_entries.Add(new Entry(name, release));
}
public void TransferAll()
{
if (_running)
throw new InvalidOperationException(
"The resource cleanup group is currently releasing resources.");
foreach (Entry entry in _entries)
entry.Complete = true;
}
public void RetryCleanup()
{
if (_running || IsCleanupComplete)
return;
_running = true;
List<Exception>? failures = null;
try
{
for (int i = _entries.Count - 1; i >= 0; i--)
{
Entry entry = _entries[i];
if (entry.Complete)
continue;
try
{
entry.Release();
entry.Complete = true;
}
catch (Exception failure)
{
(failures ??= []).Add(new InvalidOperationException(
$"Resource cleanup operation '{entry.Name}' failed.",
failure));
}
}
}
finally
{
_running = false;
}
if (failures is not null)
throw new AggregateException("Composite resource cleanup remains incomplete.", failures);
}
public void RollbackConstructionAndThrow(
string message,
Exception constructionFailure)
{
ArgumentException.ThrowIfNullOrWhiteSpace(message);
ArgumentNullException.ThrowIfNull(constructionFailure);
try
{
RetryCleanup();
}
catch (Exception cleanupFailure)
{
throw new GlResourceConstructionException(
message,
this,
[constructionFailure, cleanupFailure]);
}
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
throw new InvalidOperationException("Unreachable construction rollback path.");
}
}