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 Release(bool includeProgram) { var failures = new List(); 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 ReleaseProgramOnly() { var failures = new List(); Try( () => { if (Program != 0) { api.DeleteProgram(Program); Program = 0; VertexAttached = false; FragmentAttached = false; } }, failures); return failures; } public void RetryCleanup() { List 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 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 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(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)); } }