// TODO can determine which uniform buffers are used in which stage based on binding index (see 'Create' in GpuProgramsVk.cpp)
        // if (binding.GetShaderStageFlags() != VK_SHADER_STAGE_FRAGMENT_BIT) // *only* used in fragment shader
        static uint SerializeUniforms(CompiledShader compiled, BinaryWriter writer)
        {
            int totalConstants = 0;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                totalConstants += constantBuffer.usedConstants;
            }
            writer.Write((ushort)totalConstants);

            int size = 0;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                for (int i = 0; i < constantBuffer.usedConstants; i++)
                {
                    var constant = constantBuffer.constants[i];
                    UnityToBgfx.ConvertBuiltInUniform(constant.name, out string uniformName);
                    writer.Write((byte)uniformName.Length);
                    writer.Write(Encoding.UTF8.GetBytes(uniformName));

                    if (constant.dataType != UnityHelper.ShaderParamType.kShaderParamFloat)
                    {
                        throw new NotSupportedException("Only float types are supported for uniforms.");
                    }

                    // Data type
                    var type = UnityToBgfx.GetBgfxUniformDataType(constant);
                    writer.Write((byte)type); // TODO bx::write(_writer, uint8_t(un.type | fragmentBit));

                    //  array size
                    writer.Write((byte)constant.arraySize); // Should be 0 if not an array

                    // regIndex - bgfx puts all constants into a single buffer
                    writer.Write((ushort)(size + constant.idx)); // TODO

                    // regCount
                    var regCount = constant.arraySize;
                    if (type == BgfxHelper.UniformType.Mat3)
                    {
                        regCount *= 3;
                    }
                    else if (type == BgfxHelper.UniformType.Mat4)
                    {
                        regCount *= 4;
                    }
                    writer.Write((ushort)regCount);

                    // TODO this is only used in renderer_webgpu.cpp?
                    // texComponent
                    // texDimension
//                    writer.Write((byte)0);
//                    writer.Write((byte)0);
                }

                size += constantBuffer.size;
            }

            return((uint)size);
        }
        static uint SerializeUniforms(CompiledShaderData compiled, BinaryWriter writer, Stage stage)
        {
            if (compiled.constantBuffers.Count == 0 && compiled.samplers.Count == 0)
            {
                writer.Write((ushort)0);
                return(0);
            }

            uint fragmentBit   = stage == Stage.Fragment ? BgfxHelper.BGFX_UNIFORM_FRAGMENTBIT : 0u;
            var  uniformsCount = compiled.samplers.Count;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                uniformsCount += constantBuffer.constants.Count;
            }
            writer.Write((ushort)uniformsCount);

            uint constantBufferSize = 0;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                foreach (var constant in constantBuffer.constants)
                {
                    UnityToBgfx.ConvertBuiltInUniform(constant.name, out string uniformName);
                    writer.Write((byte)uniformName.Length);
                    writer.Write(Encoding.UTF8.GetBytes(uniformName));

                    if (constant.dataType != UnityHelper.ShaderParamType.kShaderParamFloat)
                    {
                        throw new NotSupportedException("Only float types are supported for uniforms.");
                    }

                    // data type
                    var type = UnityToBgfx.GetBgfxUniformDataType(constant);
                    writer.Write((byte)((uint)type | fragmentBit));

                    //  array size
                    var arraySize = Math.Max(constant.arraySize, 1); // Unity passes a 0 if not an array
                    writer.Write((byte)arraySize);

                    // regIndex - merge all constants into a single buffer
                    writer.Write((ushort)(constantBufferSize + constant.idx));

                    // regCount
                    var regCount = (ushort)arraySize;
                    if (type == BgfxHelper.UniformType.Mat3)
                    {
                        regCount *= 3;
                    }
                    else if (type == BgfxHelper.UniformType.Mat4)
                    {
                        regCount *= 4;
                    }
                    writer.Write(regCount);

                    // Unused
                    writer.Write((byte)0); // texComponent
                    writer.Write((byte)0); // texDimension
                }

                constantBufferSize += (uint)constantBuffer.size;
            }

            foreach (var sampler in compiled.samplers)
            {
                writer.Write((byte)sampler.name.Length);
                writer.Write(Encoding.UTF8.GetBytes(sampler.name));
                writer.Write((byte)((ushort)BgfxHelper.UniformType.Sampler | BgfxHelper.BGFX_UNIFORM_SAMPLERBIT | fragmentBit));

                // array size
                writer.Write((byte)0);

                // regIndex
                writer.Write((ushort)0);

                // regCount
                writer.Write((ushort)0);

                // texComponent
                writer.Write((byte)0);

                // texDimension
                writer.Write((byte)0);
            }

            return(constantBufferSize);
        }
        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 void ProcessUniformDecl(string line, List <VarName> predefinedUniforms, List <VarName> hlslccUniforms, List <string> shadowSamplers, List <Constant> constants, List <TextureSampler> samplers)
        {
            // e.g. "uniform mat4x4 uniformName;"
            // e.g. "uniform sampler2D samplerName;"
            var split = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);

            if (split.Length < 3) // "uniform <type> <name>;"
            {
                throw new InvalidDataException($"SerializeShaderGLSL: Unable to parse uniform declaration '{line}'");
            }

            // Unity shader compiler does not give reflection data for uniforms in all cases so we generate it manually
            string name      = split[split.Length - 1].Remove(split[split.Length - 1].Length - 1, 1); // remove semicolon
            int    arraySize = 0;
            var    i         = name.IndexOf('[');                                                     // e.g. "uniformName[4];"

            if (i >= 0)
            {
                string pattern = @".*\[([0-9]+)\]"; // Capture number between brackets in a separate group
                var    match   = Regex.Match(name, pattern);
                if (match.Groups.Count != 2)
                {
                    throw new InvalidDataException("SerializeShaderGLSL: Unable to parse array declaration.");
                }
                arraySize = int.Parse(match.Groups[1].Value);

                name = name.Substring(0, i); // remove array brackets from name
            }

            bool mtxAsVectors = name.StartsWith(UnityToBgfx.k_HlslccMtxPrefix);

            if (UnityToBgfx.ConvertBuiltInUniform(name, out var bgfxName))
            {
                predefinedUniforms.Add(new VarName {
                    unityName = name, bgfxName = bgfxName
                });
            }
            else
            {
                // TODO fix and expose HLSLCC_FLAG_TRANSLATE_MATRICES flag
                if (mtxAsVectors) // e.g. "hlslcc_mtx4x4uniformName"
                {
                    bgfxName = name.Remove(0, UnityToBgfx.k_HlslccMtxPrefix.Length);
                    hlslccUniforms.Add(new VarName {
                        unityName = name, bgfxName = bgfxName
                    });
                }
                else if (name.StartsWith(UnityToBgfx.k_HlslccZcmpPrefix)) // e.g. "hlslcc_zcmpsamplerName"
                {
                    bgfxName = name.Remove(0, UnityToBgfx.k_HlslccZcmpPrefix.Length);
                    hlslccUniforms.Add(new VarName {
                        unityName = name, bgfxName = bgfxName
                    });
                    shadowSamplers.Add(bgfxName);
                }
                else
                {
                    bgfxName = name;
                }
            }

            string type = split[split.Length - 2];

            if (type.Contains("sampler"))
            {
                samplers.Add(GetTextureSampler(bgfxName, arraySize));
            }
            else
            {
                constants.Add(GetConstant(type, bgfxName, arraySize, mtxAsVectors));
            }
        }
        static uint SerializeUniforms(CompiledShaderData compiled, BinaryWriter writer, Stage stage)
        {
            if (compiled.constantBuffers.Count == 0 && compiled.samplers.Count == 0)
            {
                writer.Write((ushort)0);
                return(0);
            }

            uint fragmentBit   = stage == Stage.Fragment ? BgfxHelper.BGFX_UNIFORM_FRAGMENTBIT : 0u;
            var  uniformsCount = compiled.samplers.Count;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                uniformsCount += constantBuffer.constants.Count;
            }
            writer.Write((ushort)uniformsCount);

            uint globalConstantBufferSize = 0;

            foreach (var constantBuffer in compiled.constantBuffers)
            {
                uint constantBufferSize = 0;
                foreach (var constant in constantBuffer.constants)
                {
                    UnityToBgfx.ConvertBuiltInUniform(constant.name, out string uniformName);
                    writer.Write((byte)uniformName.Length);
                    writer.Write(Encoding.UTF8.GetBytes(uniformName));

                    if (constant.dataType != UnityHelper.ShaderParamType.kShaderParamFloat)
                    {
                        throw new NotSupportedException("Only float types are supported for uniforms.");
                    }

                    // Data type
                    uint parameterSize;
                    var  type = UnityToBgfx.GetBgfxUniformDataType(constant);
                    writer.Write((byte)((uint)type | fragmentBit));
                    if (type == BgfxHelper.UniformType.Vec4)
                    {
                        parameterSize = 16;
                    }
                    else if (type == BgfxHelper.UniformType.Mat4)
                    {
                        parameterSize = 64;
                    }
                    else
                    {
                        // bgfx.UniformType.Mat3. Size includes padding
                        parameterSize = 44;
                    }

                    // array size
                    writer.Write((byte)constant.arraySize); // Should be 0 if not an array

                    // regIndex - merge all constants into a single buffer
                    writer.Write((ushort)(globalConstantBufferSize + constant.idx));

                    // regCount
                    parameterSize = constant.arraySize > 0 ? parameterSize * (uint)constant.arraySize : parameterSize;
                    parameterSize = AlignUp(parameterSize, 16);
                    writer.Write((ushort)(parameterSize / 16));

                    // Unused
                    writer.Write((byte)0); // texComponent
                    writer.Write((byte)0); // texDimension

                    // Actual size of this constant buffer. All unused constants after the last used constant are removed
                    constantBufferSize = (uint)constant.idx + parameterSize;
                }

                globalConstantBufferSize += constantBufferSize;
            }

            foreach (var sampler in compiled.samplers)
            {
                writer.Write((byte)sampler.name.Length);
                writer.Write(Encoding.UTF8.GetBytes(sampler.name));
                writer.Write((byte)((ushort)BgfxHelper.UniformType.Sampler | BgfxHelper.BGFX_UNIFORM_SAMPLERBIT | fragmentBit));

                // array size
                writer.Write((byte)sampler.arraySize); // this will always be zero

                // regIndex
                writer.Write((ushort)sampler.register);

                // regCount
                writer.Write((ushort)1);

                // Unused
                writer.Write((byte)0); // texComponent
                writer.Write((byte)0); // texDimension
            }

            return(globalConstantBufferSize);
        }