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>
325 lines
10 KiB
C#
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));
|
|
}
|
|
}
|