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

325 lines
10 KiB
C#

using System.Runtime.ExceptionServices;
using AcDream.App.Rendering.Wb;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
internal interface IShaderProgramBuildApi
{
uint CreateShader(ShaderType type);
void ShaderSource(uint shader, string source);
void CompileShader(uint shader);
int GetShaderCompileStatus(uint shader);
string GetShaderInfoLog(uint shader);
uint CreateProgram();
void AttachShader(uint program, uint shader);
void LinkProgram(uint program);
int GetProgramLinkStatus(uint program);
string GetProgramInfoLog(uint program);
void DetachShader(uint program, uint shader);
void DeleteShader(uint shader);
void DeleteProgram(uint program);
}
internal sealed class GlShaderProgramBuildApi(GL gl) : IShaderProgramBuildApi
{
public uint CreateShader(ShaderType type) =>
GlResourceCommand.CreateName(
gl,
$"{type} shader",
() => gl.CreateShader(type),
gl.DeleteShader);
public void ShaderSource(uint shader, string source) =>
GlResourceCommand.Execute(
gl,
$"upload shader source {shader}",
() => gl.ShaderSource(shader, source));
public void CompileShader(uint shader) =>
GlResourceCommand.Execute(
gl,
$"compile shader {shader}",
() => gl.CompileShader(shader));
public void GetShader(uint shader, ShaderParameterName name, out int value) =>
gl.GetShader(shader, name, out value);
public int GetShaderCompileStatus(uint shader)
{
return GlResourceCommand.Execute(
gl,
$"read shader {shader} compile status",
() =>
{
gl.GetShader(shader, ShaderParameterName.CompileStatus, out int value);
return value;
});
}
public string GetShaderInfoLog(uint shader) =>
GlResourceCommand.Execute(
gl,
$"read shader {shader} info log",
() => gl.GetShaderInfoLog(shader));
public uint CreateProgram() =>
GlResourceCommand.CreateName(
gl,
"shader program",
gl.CreateProgram,
gl.DeleteProgram);
public void AttachShader(uint program, uint shader) =>
GlResourceCommand.Execute(
gl,
$"attach shader {shader} to program {program}",
() => gl.AttachShader(program, shader));
public void LinkProgram(uint program) =>
GlResourceCommand.Execute(
gl,
$"link shader program {program}",
() => gl.LinkProgram(program));
public int GetProgramLinkStatus(uint program)
{
return GlResourceCommand.Execute(
gl,
$"read shader program {program} link status",
() =>
{
gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int value);
return value;
});
}
public string GetProgramInfoLog(uint program) =>
GlResourceCommand.Execute(
gl,
$"read shader program {program} info log",
() => gl.GetProgramInfoLog(program));
public void DetachShader(uint program, uint shader) =>
GlResourceCommand.Execute(
gl,
$"detach shader {shader} from program {program}",
() => gl.DetachShader(program, shader));
public void DeleteShader(uint shader) =>
GlResourceCommand.DeleteShader(gl, shader, $"delete shader {shader}");
public void DeleteProgram(uint program) =>
GlResourceCommand.DeleteProgram(gl, program, $"delete shader program {program}");
}
internal sealed class ShaderProgramCleanup(IShaderProgramBuildApi api)
: IRetryableResourceCleanup
{
public uint Vertex { get; set; }
public uint Fragment { get; set; }
public uint Program { get; set; }
public bool VertexAttached { get; set; }
public bool FragmentAttached { get; set; }
public bool IsCleanupComplete => Vertex == 0 && Fragment == 0 && Program == 0;
public uint TransferProgram()
{
if (Vertex != 0 || Fragment != 0 || Program == 0)
throw new InvalidOperationException("The linked program is not ready for ownership transfer.");
uint program = Program;
Program = 0;
return program;
}
public List<Exception> Release(bool includeProgram)
{
var failures = new List<Exception>();
Try(
() =>
{
if (Program != 0 && Vertex != 0 && VertexAttached)
{
api.DetachShader(Program, Vertex);
VertexAttached = false;
}
},
failures);
Try(
() =>
{
if (Program != 0 && Fragment != 0 && FragmentAttached)
{
api.DetachShader(Program, Fragment);
FragmentAttached = false;
}
},
failures);
Try(
() =>
{
if (Vertex != 0)
{
api.DeleteShader(Vertex);
Vertex = 0;
VertexAttached = false;
}
},
failures);
Try(
() =>
{
if (Fragment != 0)
{
api.DeleteShader(Fragment);
Fragment = 0;
FragmentAttached = false;
}
},
failures);
if (includeProgram)
{
Try(
() =>
{
if (Program != 0)
{
api.DeleteProgram(Program);
Program = 0;
VertexAttached = false;
FragmentAttached = false;
}
},
failures);
}
return failures;
}
public List<Exception> ReleaseProgramOnly()
{
var failures = new List<Exception>();
Try(
() =>
{
if (Program != 0)
{
api.DeleteProgram(Program);
Program = 0;
VertexAttached = false;
FragmentAttached = false;
}
},
failures);
return failures;
}
public void RetryCleanup()
{
List<Exception> failures = Release(includeProgram: true);
if (!IsCleanupComplete)
{
throw new AggregateException(
"Shader program cleanup remains incomplete.",
failures.Count == 0
? [new InvalidOperationException("Shader cleanup retained names without reporting an operation failure.")]
: failures);
}
}
private static void Try(Action operation, List<Exception> failures)
{
try
{
operation();
}
catch (Exception failure)
{
failures.Add(failure);
}
}
}
internal static class ShaderProgramConstruction
{
public static uint Build(
IShaderProgramBuildApi api,
string vertexSource,
string fragmentSource)
{
ArgumentNullException.ThrowIfNull(api);
ArgumentNullException.ThrowIfNull(vertexSource);
ArgumentNullException.ThrowIfNull(fragmentSource);
var cleanup = new ShaderProgramCleanup(api);
Exception? constructionFailure = null;
try
{
cleanup.Vertex = CreateShader(api, ShaderType.VertexShader);
Compile(api, cleanup.Vertex, ShaderType.VertexShader, vertexSource);
cleanup.Fragment = CreateShader(api, ShaderType.FragmentShader);
Compile(api, cleanup.Fragment, ShaderType.FragmentShader, fragmentSource);
cleanup.Program = api.CreateProgram();
if (cleanup.Program == 0)
throw new InvalidOperationException("OpenGL returned no shader program name.");
api.AttachShader(cleanup.Program, cleanup.Vertex);
cleanup.VertexAttached = true;
api.AttachShader(cleanup.Program, cleanup.Fragment);
cleanup.FragmentAttached = true;
api.LinkProgram(cleanup.Program);
if (api.GetProgramLinkStatus(cleanup.Program) == 0)
throw new InvalidOperationException(
"program link failed: " + api.GetProgramInfoLog(cleanup.Program));
}
catch (Exception failure)
{
constructionFailure = failure;
}
List<Exception> cleanupFailures = cleanup.Release(includeProgram: false);
if (constructionFailure is null && cleanupFailures.Count == 0)
return cleanup.TransferProgram();
cleanupFailures.AddRange(cleanup.ReleaseProgramOnly());
if (constructionFailure is not null && cleanupFailures.Count == 0)
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
var failures = new List<Exception>(cleanupFailures.Count + 1);
if (constructionFailure is not null)
failures.Add(constructionFailure);
failures.AddRange(cleanupFailures);
const string message =
"Shader program construction failed and one or more temporary OpenGL names could not be cleanly released.";
if (!cleanup.IsCleanupComplete)
throw new GlResourceConstructionException(message, cleanup, failures);
throw new AggregateException(message, failures);
}
private static uint CreateShader(
IShaderProgramBuildApi api,
ShaderType type)
{
uint shader = api.CreateShader(type);
if (shader == 0)
throw new InvalidOperationException($"OpenGL returned no {type} name.");
return shader;
}
private static void Compile(
IShaderProgramBuildApi api,
uint shader,
ShaderType type,
string source)
{
api.ShaderSource(shader, source);
api.CompileShader(shader);
if (api.GetShaderCompileStatus(shader) == 0)
throw new InvalidOperationException(
$"{type} compile failed: " + api.GetShaderInfoLog(shader));
}
}