acdream/src/AcDream.App/Rendering/ResourceCleanupGroup.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

213 lines
6.5 KiB
C#

namespace AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
internal interface IRetryableResourceCleanup
{
bool IsCleanupComplete { get; }
void RetryCleanup();
}
/// <summary>
/// Thrown when a resource construction failed and the partial-construction
/// rollback it triggered could not fully complete either. Backend-neutral —
/// <see cref="ResourceCleanupGroup"/> and <c>ShaderProgramConstruction</c>
/// throw it on both the (deleted, Campaign V slice V11) GL construction path
/// and the Vulkan one.
/// </summary>
internal sealed class ResourceConstructionException : AggregateException,
IRetryableResourceCleanup
{
private readonly IRetryableResourceCleanup _cleanup;
public ResourceConstructionException(
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>
/// 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 ResourceConstructionException(
message,
this,
[constructionFailure, cleanupFailure]);
}
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
throw new InvalidOperationException("Unreachable construction rollback path.");
}
}
/// <summary>
/// Lifetime root for cleanup work that could not finish before a throwing
/// composition factory returned control. The original exception remains the
/// retry owner; this ledger prevents it and its exact pending names from
/// becoming local-only.
///
/// <para>Backend-neutral, despite its former name
/// (<c>GlConstructionCleanupLedger</c>, deleted at Campaign V slice V11): it
/// walks any exception chain for <see cref="IRetryableResourceCleanup"/> —
/// which <see cref="ResourceConstructionException"/> implements on both
/// backends — and does not itself touch GL.</para>
/// </summary>
internal sealed class ResourceConstructionCleanupLedger : 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 resource construction transactions remain pending.",
failures);
}
}
}