using System.Text; using System.Text.RegularExpressions; namespace AcDream.Tools.ShaderCompiler; /// /// Campaign V slice V6c: give every stage-to-stage varying an explicit /// layout(location = N), which SPIR-V requires and desktop GL does not. /// /// This is the one place the compiler edits a shader body rather than /// prepending to it, and it is limited to inserting a qualifier in front of a /// declaration it already matched exactly. Locations are keyed BY NAME, taken /// from the vertex stage's outputs and looked up by the fragment stage's inputs, /// so the two stages cannot drift apart if someone reorders a declaration in one /// of them. Assigning by ordinal would look identical today and produce silently /// swapped varyings the first time an author moved a line. /// /// Anything already carrying a layout( qualifier is left alone, as /// are interface blocks (which open a brace) and the redeclared /// gl_PerVertex block that lets a core-profile shader write /// gl_ClipDistance. /// internal static partial class GlslVaryingLocations { // GLSL allows the interpolation qualifier on either side of the direction — // both `out flat uvec2 v;` and `flat out uvec2 v;` appear in acdream's // shaders — so the pattern accepts either and neither. [GeneratedRegex( @"^(?\s*)(?(flat|noperspective|smooth|centroid)\s+)*(?in|out)\s+(?(flat|noperspective|smooth|centroid)\s+)*(?[A-Za-z_][A-Za-z0-9_]*)\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*(?\[[^\]]*\])?\s*;\s*(?//.*)?$", RegexOptions.ExplicitCapture)] private static partial Regex VaryingDeclaration(); /// /// Rewrites so every user varying carries a /// location. is shared across the two /// stages of a pair and is populated by whichever stage declares a name /// first. /// internal static string Apply(string source, string stage, Dictionary locationsByName) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(locationsByName); string[] lines = source.Replace("\r\n", "\n").Split('\n'); var output = new StringBuilder(); int nextFragmentOutput = 0; foreach (string line in lines) { if (line.Contains("layout(", StringComparison.Ordinal) || line.Contains('{')) { output.AppendLine(line); continue; } Match match = VaryingDeclaration().Match(line); if (!match.Success) { output.AppendLine(line); continue; } string direction = match.Groups["direction"].Value; string name = match.Groups["name"].Value; // Vertex inputs are attributes, not varyings: their locations are // the vertex layout the pipeline declares, and every acdream vertex // shader already states them. Leaving an unqualified one alone makes // the compiler say so rather than inventing a binding. if (stage == "vert" && direction == "in") { output.AppendLine(line); continue; } int location; if (stage == "frag" && direction == "out") { // Fragment outputs live in their own location space; acdream has // exactly one colour attachment everywhere. location = nextFragmentOutput++; } else if (locationsByName.TryGetValue(name, out int existing)) { location = existing; } else { location = locationsByName.Count == 0 ? 0 : locationsByName.Values.Max() + 1; locationsByName[name] = location; } string indent = match.Groups["indent"].Value; string rest = line[indent.Length..]; output.AppendLine($"{indent}layout(location = {location}) {rest}"); } return output.ToString(); } }