static string FixupShaderSource(CompiledShaderData compiled, Stage stage, List <VarName> attributes)
        {
            // See CreateMTLLibraryFromSource in Unity
            var stream = new MemoryStream(compiled.shader.outputCode);

            using (BinaryReader br = new BinaryReader(stream))
            {
                // Remove header inserted by Unity. See ExtractMetalShaderBlobDesc in MetalShaderBlobHeader.h
                var magic = br.ReadUInt32();
                if (magic != k_MetalShaderBlobHeaderMagic)
                {
                    throw new InvalidDataException("SerializeShaderMSL: Unsupported Metal shader blob header.");
                }

                var size       = br.ReadUInt32();
                var version    = br.ReadUInt32();
                var codeOffset = br.ReadUInt32();
                if (codeOffset >= compiled.shader.outputCode.Length)
                {
                    throw new InvalidDataException("SerializeShaderMSL: Invalid shader code offset.");
                }

                long codeLength = compiled.shader.outputCode.Length - codeOffset;
                br.BaseStream.Seek(codeOffset, SeekOrigin.Begin);
                var code = br.ReadBytes((int)codeLength);
                Assert.IsTrue(br.PeekChar() == -1);

                // Patch up the vertex attribute names so bgfx can recognize them (it uses reflection data for attributes from the shader and ignores the ones we send)
                var codeString = Encoding.UTF8.GetString(code);
                if (stage == Stage.Vertex)
                {
                    using (StringReader sr = new StringReader(codeString))
                    {
                        while (sr.Peek() >= 0)
                        {
                            var line = sr.ReadLine();
                            if (Regex.IsMatch(line, @"^\s*\b(struct)\b\s+\b(Mtl_VertexIn)\b")) // starts with words 'struct Mtl_VertexIn', ignoring leading whitespace
                            {
                                while (!line.Contains("{"))
                                {
                                    if (sr.Peek() < 0)
                                    {
                                        throw new InvalidDataException("SerializeShaderMSL: Unable to parse vertex attributes.");
                                    }

                                    line = sr.ReadLine();
                                }

                                while (!line.Contains("}"))
                                {
                                    if (sr.Peek() < 0)
                                    {
                                        throw new InvalidDataException("SerializeShaderMSL: Unable to parse vertex attributes.");
                                    }

                                    // e.g. float3 POSITION0 [[ attribute(0) ]] ;
                                    line = sr.ReadLine();
                                    var split = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
                                    if (split.Length > 2)
                                    {
                                        string unityName = split[1]; // TODO precision qualifiers?
                                        UnityHelper.ShaderChannel channel = UnityHelper.ConvertHLSLccAttributeName(unityName);
                                        Assert.IsTrue(compiled.attributes.Contains(channel));
                                        BgfxHelper.Attrib bgfxAttrib = UnityToBgfx.ConvertAttribute(channel);
                                        attributes.Add(new VarName {
                                            unityName = unityName, bgfxName = BgfxHelper.GetName(bgfxAttrib)
                                        });
                                    }
                                }

                                break;
                            }
                        }
                    }
                }

                List <string> uniformDecls = new List <string>();
                using (StringReader sr = new StringReader(codeString))
                {
                    while (sr.Peek() >= 0)
                    {
                        var line = sr.ReadLine();
                        for (int i = 1; i < compiled.constantBuffers.Count; i++)
                        {
                            if (Regex.IsMatch(line, $@"^\s*\b(struct)\b\s+\b({compiled.constantBuffers[i].name}_Type)\b")) // starts with words 'struct <constant buffer name>_Type', ignoring leading whitespace
                            {
                                while (!line.Contains("{"))
                                {
                                    if (sr.Peek() < 0)
                                    {
                                        throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                    }

                                    line = sr.ReadLine();
                                }
                                line = sr.ReadLine();

                                while (!line.Contains("}"))
                                {
                                    if (sr.Peek() < 0)
                                    {
                                        throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                    }

                                    // Store uniform declaration strings so they can be added to the first uniform buffer definition
                                    // e.g. float4x4 unity_ObjectToWorld;
                                    uniformDecls.Add(line);

                                    line = sr.ReadLine();
                                }
                            }
                        }
                    }
                }

                // Patch attribute names in the fragment shader if the vertex output names were modified
                StringBuilder sb = new StringBuilder();
                foreach (var attribute in attributes)
                {
                    sb.AppendLine($"#define {attribute.unityName} {attribute.bgfxName}");
                }

                foreach (var constantBuffer in compiled.constantBuffers)
                {
                    // bgfx requires that the constant buffer name be "_mtl_u"
                    sb.AppendLine($"#define {constantBuffer.name} _mtl_u");

                    // Patch predefined uniform names
                    foreach (var uniform in constantBuffer.constants)
                    {
                        bool patchHlslccName = uniform.constantType == UnityHelper.ConstantType.kConstantTypeMatrix;
                        if (UnityToBgfx.ConvertBuiltInUniform(uniform.name, out string bgfxName))
                        {
                            // Note: uniform names in Unity reflection data don't contain the hlslcc prefix but the uniforms in the shader code do
                            string unityName = patchHlslccName ? $"hlslcc_mtx4x4{uniform.name}" : uniform.name; // TODO support mtx3x3
                            sb.AppendLine($"#define {unityName} {bgfxName}");
                        }
                        else
                        {
                            if (uniform.name != null && patchHlslccName)
                            {
                                sb.AppendLine($"#define hlslcc_mtx4x4{uniform.name} {uniform.name}");
                            }
                        }
                    }
                }

                if (compiled.constantBuffers.Count > 1 || compiled.texToSampler.Count > 0)
                {
                    string cb0Name         = compiled.constantBuffers.Count > 1 ? compiled.constantBuffers[0].name : string.Empty;
                    var    constantBuffers = compiled.constantBuffers.Count > 1 ? compiled.constantBuffers.GetRange(1, compiled.constantBuffers.Count - 1) : new List <ConstantBuffer>();
                    using (StringReader sr = new StringReader(codeString))
                        using (StringWriter sw = new StringWriter())
                        {
                            sw.Write(sb.ToString());

                            while (sr.Peek() >= 0)
                            {
                                var line = sr.ReadLine();
                                if (Regex.IsMatch(line, $@"^\s*\b(struct)\b\s+\b({cb0Name}_Type)\b")) // starts with words 'struct <cb0 name>_Type', ignoring leading whitespace
                                {
                                    // Combine all uniform buffers into a single buffer definition
                                    while (!line.Contains("{"))
                                    {
                                        sw.WriteLine(line);
                                        if (sr.Peek() < 0)
                                        {
                                            throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                        }

                                        line = sr.ReadLine();
                                    }

                                    while (!line.Contains("}"))
                                    {
                                        sw.WriteLine(line);

                                        if (sr.Peek() < 0)
                                        {
                                            throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                        }

                                        // e.g. float4x4 unity_ObjectToWorld;
                                        line = sr.ReadLine();
                                    }

                                    foreach (var uniformDecl in uniformDecls)
                                    {
                                        sw.WriteLine(uniformDecl);
                                    }

                                    sw.WriteLine("};");
                                }
                                else
                                {
                                    if (constantBuffers.Any(cb => Regex.IsMatch(line, $@"^\s*\b(struct)\b\s+\b({cb.name}_Type)\b")))
                                    {
                                        // Remove (don't write) uniform buffer definition if not cb0
                                        while (!line.Contains("{"))
                                        {
                                            if (sr.Peek() < 0)
                                            {
                                                throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                            }

                                            line = sr.ReadLine();
                                        }

                                        while (!line.Contains("}"))
                                        {
                                            if (sr.Peek() < 0)
                                            {
                                                throw new InvalidDataException("SerializeShaderMSL: Unable to parse uniform buffer definitions.");
                                            }

                                            line = sr.ReadLine();
                                        }
                                    }
                                    else if (Regex.IsMatch(line, @"^\s*(texture|depth).*(access::sample).*"))
                                    {
                                        // Patch texture register to match sampler register in main function texture parameters
                                        // This is needed because bgfx expects them to be the same and the Unity shader compiler uses automatic assignment, which for samplers is based on order of declaration and for textures is based on order of usage

                                        // Extract register from texture declaration - e.g. texture2d<float, access::sample > s_texAlbedo [[ texture(0) ]]
                                        string pattern = @"texture\s*\(\s*([0-9]+)\s*\)"; // find 'texture(<number>)' and capture <number> in a separate group
                                        var    match   = Regex.Match(line, pattern);
                                        if (match.Groups.Count != 2)
                                        {
                                            throw new InvalidDataException("SerializeShaderMSL: Unable to parse texture declaration.");
                                        }

                                        uint reg = uint.Parse(match.Groups[1].Value);
                                        if (compiled.texToSampler.ContainsKey(reg))
                                        {
                                            line = Regex.Replace(line, pattern, $"texture({compiled.texToSampler[reg]})");
                                        }

                                        sw.WriteLine(line);
                                    }
                                    else
                                    {
                                        // Remove comments
                                        if (Regex.IsMatch(line, @"^\s*//"))
                                        {
                                            continue;
                                        }

                                        // Remove (don't write) uniform buffers parameters in main function if not cb0
                                        // e.g. constant Uniforms_Type& Uniforms [[ buffer(1) ]]
                                        if (constantBuffers.Any(cb => Regex.IsMatch(line, $@"^\s*\b(constant)\b\s+\b({cb.name}_Type)\b&\s+\b({cb.name})\b"))) // starts with words 'constant <cb0 name>_Type& <cb0 name>', ignoring leading whitespace
                                        {
                                            continue;
                                        }

                                        sw.WriteLine(line);
                                    }
                                }
                            }

                            return(sw.ToString());
                        }
                }

                // No patching needed for uniforms or textures
                return(sb.Append(codeString).ToString());
            }
        }
        static CompiledShaderData FixupShader(string shaderSrc, Stage stage, bool GLES2, string name, List <UnityHelper.ShaderChannel> attributes)
        {
            List <VarName>     predefinedUniforms = new List <VarName>();
            List <VarName>     hlslccUniforms     = new List <VarName>();
            List <VarName>     attributesNames    = new List <VarName>();
            List <string>      shadowSamplers     = new List <string>();
            List <Constant>    constants          = new List <Constant>();
            CompiledShaderData shaderData         = new CompiledShaderData();

            shaderData.Init();
            if (attributes != null)
            {
                shaderData.attributes = attributes;
            }

            using (StringReader reader = new StringReader(shaderSrc))
            {
                while (reader.Peek() >= 0)
                {
                    var line = reader.ReadLine();
                    if (Regex.IsMatch(line, @"^\s*(\buniform\b|UNITY_LOCATION\s*\(\s*[0-9]\s*\)\s+\buniform\b)")) // starts with word 'uniform' or 'UNITY_LOCATION(<index>) uniform', ignoring leading whitespace
                    {
                        ProcessUniformDecl(line, predefinedUniforms, hlslccUniforms, shadowSamplers, constants, shaderData.samplers);
                    }
                    else if (Regex.IsMatch(line, @"^\s*(\bUNITY_BINDING\b\s*\(\s*[0-9]\s*\)\s+\buniform\b)")) // starts with word 'UNITY_BINDING(<index>) uniform', ignoring leading whitespace
                    {
                        UnityEngine.Debug.LogWarning($"Uniform buffer object found in GLSL shader {name}. Use 'CBUFFER_START' and 'CBUFFER_END' macros in place of raw cbuffer declarations.");
                    }
                    else if (stage == Stage.Vertex && Regex.IsMatch(line, @"^\s*\b(in|attribute)\b")) // starts with word 'in' or 'attribute', ignoring leading whitespaceS
                    {
                        // e.g. "in vec4 in_POSITION0;"
                        var    split     = line.Split(null);
                        string unityName = split[split.Length - 1].Remove(split[split.Length - 1].Length - 1, 1); // remove semicolon
                        UnityHelper.ShaderChannel channel = UnityHelper.ConvertHLSLccAttributeName(unityName);
                        Assert.IsTrue(shaderData.attributes.Contains(channel));
                        BgfxHelper.Attrib bgfxAttrib = UnityToBgfx.ConvertAttribute(channel);
                        attributesNames.Add(new VarName {
                            unityName = unityName, bgfxName = BgfxHelper.GetName(bgfxAttrib)
                        });
                    }
                }
            }

            // Disable UBO and Uniform Locations
            shaderSrc = shaderSrc.Replace("#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1", string.Empty);
            shaderSrc = shaderSrc.Replace("#define UNITY_SUPPORTS_UNIFORM_LOCATION 1", string.Empty);

            using (StringReader reader = new StringReader(shaderSrc))
                using (StringWriter writer = new StringWriter())
                {
                    // Remove everything before the '#version' line because bgfx looks for this when determining if it needs to inject the version
                    while (reader.Peek() >= 0)
                    {
                        var line = reader.ReadLine();
                        if (line.StartsWith("#version "))
                        {
                            writer.WriteLine(line);
                            break;
                        }
                    }

                    // Why doesn't HLSLcc handle this?
                    // Note: this isn't needed if matrix as vectors is enabled
                    if (GLES2)
                    {
                        writer.WriteLine("#define mat2x2 mat2");
                        writer.WriteLine("#define mat3x3 mat3");
                        writer.WriteLine("#define mat4x4 mat4");
                    }

                    // inject fixups for uniform and attribute names after the version
                    foreach (var uniform in predefinedUniforms)
                    {
                        // e.g. #define unity_ObjectToWorld u_model
                        writer.WriteLine($"#define {uniform.unityName} {uniform.bgfxName}");
                    }
                    foreach (var uniform in hlslccUniforms)
                    {
                        writer.WriteLine($"#define {uniform.unityName} {uniform.bgfxName}");
                    }

                    // Note this shouldn't modify the vertex output because the names are unique
                    foreach (var attribute in attributesNames)
                    {
                        // e.g. #define in_POSITION0 a_position
                        writer.WriteLine($"#define {attribute.unityName} {attribute.bgfxName}");
                    }

                    while (reader.Peek() >= 0)
                    {
                        var line = reader.ReadLine();

                        if (Regex.IsMatch(line, @"^\s*(\buniform\b|UNITY_LOCATION\s*\(\s*[0-9]\s*\)\s+\buniform\b)")) // starts with word 'uniform' or 'UNITY_LOCATION(<index>) uniform', ignoring leading whitespace
                        {
                            // Remove duplicate sampler created for shadow texture
                            // HLSLcc creates a shadow sampler and a regular sampler which results in issues when using the sampler names to bind them
                            if (shadowSamplers.Any(samplerName => Regex.IsMatch(line, $@"\b{samplerName}\b")))
                            {
                                continue;
                            }

                            PatchPredefinedUniformDecl(predefinedUniforms, ref line);
                        }

                        // Remove comments
                        if (Regex.IsMatch(line, @"^\s*//"))
                        {
                            continue;
                        }

                        writer.WriteLine(line);
                    }

                    string newSrc = writer.ToString();
                    shaderData.shader = new CompiledShader {
                        outputCode = Encoding.UTF8.GetBytes(newSrc)
                    };
                    shaderData.constantBuffers.Add(new ConstantBuffer {
                        constants = constants
                    });
                    return(shaderData);
                }
        }