using System.Numerics; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; public sealed class Shader : IDisposable { private readonly GL _gl; private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal); public uint Program { get; private set; } public Shader(GL gl, string vertexPath, string fragmentPath) : this(gl, vertexPath, fragmentPath, includeCommonPreamble: false) { } /// /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md ยง3.4): when /// is true, the text of /// Shaders/common.glsl โ€” sitting alongside /// โ€” is spliced into both sources right after their leading /// #version/#extension block. GL has no #include, so this /// is plain string concatenation at load time rather than a GLSL-level /// mechanism. Every existing two-argument-path caller is unaffected: the /// convenience constructor above always passes false. /// public Shader(GL gl, string vertexPath, string fragmentPath, bool includeCommonPreamble) { _gl = gl; string vertexSource = File.ReadAllText(vertexPath); string fragmentSource = File.ReadAllText(fragmentPath); if (includeCommonPreamble) { string? directory = Path.GetDirectoryName(vertexPath); string commonPath = directory is null ? "common.glsl" : Path.Combine(directory, "common.glsl"); string commonSource = File.ReadAllText(commonPath); vertexSource = InjectPreamble(vertexSource, commonSource); fragmentSource = InjectPreamble(fragmentSource, commonSource); } Program = ShaderProgramConstruction.Build( new GlShaderProgramBuildApi(gl), vertexSource, fragmentSource); } /// /// Inserts right after the shader's leading /// #version/#extension/blank-line block. GLSL requires /// #version to be the very first statement in the source, so the /// preamble cannot simply be prepended โ€” it has to land after that block, /// before the first real declaration. /// /// Internal rather than private since slice V6d: GlGpuDevice.CreatePipeline /// loads RHI shader pairs itself and has to splice the same preamble the /// same way. Two implementations of "where does the preamble go" is exactly /// the kind of drift that shows up as one shader silently missing a binding. /// internal static string InjectPreamble(string source, string preamble) { int insertAt = 0; int lineStart = 0; while (lineStart < source.Length) { int lineEnd = source.IndexOf('\n', lineStart); if (lineEnd < 0) lineEnd = source.Length; string trimmed = source[lineStart..lineEnd].TrimStart(); bool isLeadingLine = trimmed.Length == 0 || trimmed.StartsWith("#version", StringComparison.Ordinal) || trimmed.StartsWith("#extension", StringComparison.Ordinal); if (!isLeadingLine) break; insertAt = lineEnd < source.Length ? lineEnd + 1 : lineEnd; lineStart = lineEnd + 1; } return string.Concat(source.AsSpan(0, insertAt), preamble, "\n", source.AsSpan(insertAt)); } public void Use() => _gl.UseProgram(Program); public unsafe void SetMatrix4(string name, Matrix4x4 m) { int loc = GetUniformLocation(name); _gl.UniformMatrix4(loc, 1, false, (float*)&m); } public void SetInt(string name, int value) { int loc = GetUniformLocation(name); _gl.Uniform1(loc, value); } public void SetFloat(string name, float value) { int loc = GetUniformLocation(name); _gl.Uniform1(loc, value); } public void SetVec3(string name, Vector3 v) { int loc = GetUniformLocation(name); _gl.Uniform3(loc, v.X, v.Y, v.Z); } public void SetVec2(string name, Vector2 v) { int loc = GetUniformLocation(name); _gl.Uniform2(loc, v.X, v.Y); } public void SetVec4(string name, Vector4 v) { int loc = GetUniformLocation(name); _gl.Uniform4(loc, v.X, v.Y, v.Z, v.W); } private int GetUniformLocation(string name) { ArgumentException.ThrowIfNullOrWhiteSpace(name); if (_uniformLocations.TryGetValue(name, out int location)) return location; location = _gl.GetUniformLocation(Program, name); _uniformLocations.Add(name, location); return location; } public void Dispose() { uint program = Program; if (program == 0) return; _uniformLocations.Clear(); GlResourceCommand.DeleteProgram(_gl, program, $"delete Shader program {program}"); Program = 0; } }