acdream/src/AcDream.App/Rendering/GlResourceCommand.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

152 lines
4.9 KiB
C#

using System.Runtime.ExceptionServices;
using AcDream.App.Rendering.Wb;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
/// <summary>
/// Always-on commit boundary for GL resource commands. OpenGL reports ordinary
/// command failures through its error flag, so an ownership state machine may
/// advance only after the post-command check succeeds.
/// </summary>
internal static class GlResourceCommand
{
public static void Execute(GL gl, string context, Action command)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentException.ThrowIfNullOrWhiteSpace(context);
ArgumentNullException.ThrowIfNull(command);
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
command();
GLHelpers.ThrowOnResourceError(gl, context);
}
public static T Execute<T>(GL gl, string context, Func<T> command)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentException.ThrowIfNullOrWhiteSpace(context);
ArgumentNullException.ThrowIfNull(command);
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
T result = command();
GLHelpers.ThrowOnResourceError(gl, context);
return result;
}
public static uint CreateName(
GL gl,
string resourceName,
Func<uint> create,
Action<uint> delete)
{
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
ArgumentNullException.ThrowIfNull(create);
ArgumentNullException.ThrowIfNull(delete);
return CreateNameCore(
resourceName,
() => GLHelpers.ThrowOnResourceError(
gl,
$"create {resourceName} (precondition)"),
create,
() => GLHelpers.ThrowOnResourceError(gl, $"create {resourceName}"),
ownedName => Execute(
gl,
$"rollback {resourceName} name {ownedName}",
() => delete(ownedName)));
}
internal static uint CreateNameCore(
string resourceName,
Action precondition,
Func<uint> create,
Action postcondition,
Action<uint> deleteChecked)
{
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
ArgumentNullException.ThrowIfNull(precondition);
ArgumentNullException.ThrowIfNull(create);
ArgumentNullException.ThrowIfNull(postcondition);
ArgumentNullException.ThrowIfNull(deleteChecked);
uint name = 0;
Exception? creationFailure = null;
try
{
precondition();
name = create();
if (name == 0)
throw new InvalidOperationException($"OpenGL returned no {resourceName} name.");
postcondition();
return name;
}
catch (Exception failure)
{
creationFailure = failure;
}
if (name != 0)
{
var cleanup = new SingleNameCleanup(name, deleteChecked);
try
{
cleanup.RetryCleanup();
}
catch (Exception cleanupFailure)
{
throw new GlResourceConstructionException(
$"Creating {resourceName} failed and its returned GL name could not be released.",
cleanup,
[creationFailure, cleanupFailure]);
}
}
ExceptionDispatchInfo.Capture(creationFailure).Throw();
throw new InvalidOperationException("Unreachable GL resource-creation path.");
}
public static uint CreateTexture(GL gl, string context) =>
CreateName(gl, context, gl.GenTexture, gl.DeleteTexture);
public static void DeleteTexture(GL gl, uint texture, string context) =>
Execute(gl, context, () => gl.DeleteTexture(texture));
public static void DeleteShader(GL gl, uint shader, string context) =>
Execute(gl, context, () => gl.DeleteShader(shader));
public static void DeleteProgram(GL gl, uint program, string context) =>
Execute(gl, context, () => gl.DeleteProgram(program));
public static void DeleteBuffer(GL gl, uint buffer, string context) =>
Execute(gl, context, () => gl.DeleteBuffer(buffer));
public static void DeleteVertexArray(GL gl, uint vertexArray, string context) =>
Execute(gl, context, () => gl.DeleteVertexArray(vertexArray));
private sealed class SingleNameCleanup(uint name, Action<uint> delete)
: IRetryableResourceCleanup
{
private uint _name = name;
private bool _running;
public bool IsCleanupComplete => _name == 0;
public void RetryCleanup()
{
if (_running || _name == 0)
return;
_running = true;
try
{
delete(_name);
_name = 0;
}
finally
{
_running = false;
}
}
}
}