// HLSL -> SPIR-V (dxc) -> GLSL ES 3.00 (SPIRV-Cross.NET), for Blacklight.WebGL2's MainVS/MainPS. // Not general-purpose: entry point names and vs/ps pairing are hardcoded to this one shader. // // usage: ShaderTranspiler [-I ]... using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using SPIRVCross.NET; using SPIRVCross.NET.GLSL; static byte[] CompileToSpirv(string dxcPath, string sourcePath, string targetProfile, string entryPoint, List includeDirectories, string tempDirectory) { string outputSpirvPath = Path.Combine(tempDirectory, entryPoint + ".spv"); var arguments = new List { "-T", targetProfile, "-E", entryPoint, "-spirv", sourcePath, "-Fo", outputSpirvPath }; foreach (string includeDirectory in includeDirectories) { arguments.Add("-I"); arguments.Add(includeDirectory); } var startInfo = new ProcessStartInfo(dxcPath) { RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, }; foreach (string argument in arguments) { startInfo.ArgumentList.Add(argument); } using var process = Process.Start(startInfo)!; string stderr = process.StandardError.ReadToEnd(); string stdout = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) { throw new InvalidOperationException($"dxc failed compiling {entryPoint} ({targetProfile}):\n{stdout}\n{stderr}"); } return File.ReadAllBytes(outputSpirvPath); } static string CrossCompileToGlslEs(byte[] spirv, string interfacePrefixToStrip) { using var context = new Context(); ParsedIR parsedIr = context.ParseSpirv(spirv); GLSLCrossCompiler compiler = context.CreateGLSLCompiler(parsedIr); compiler.glslOptions.version = 300; compiler.glslOptions.ES = true; // GLSL has no separate Texture2D/SamplerState objects, so combine each declared pair into one // sampler2D before compiling. compiler.BuildCombinedImageSamplers(); RenameCombinedSamplersByOriginalBinding(compiler); compiler.options.fixupClipSpace = false; compiler.options.flipVertexY = false; // The vertex and fragment stages are cross-compiled independently, and SPIRVCross.NET names each // stage's interface variables from its own direction (out_var_COLOR0 vs in_var_COLOR0) rather than // a name shared across the pipeline. GLSL links varyings by exact name when neither side gives an // explicit layout(location), so stripping the boundary-facing prefix down to the bare HLSL semantic // (COLOR0, TEXCOORD1, ...) makes the two stages agree. return compiler.Compile().Replace(interfacePrefixToStrip, ""); } static void RenameCombinedSamplersByOriginalBinding(Compiler compiler) { foreach (CombinedImageSampler combined in compiler.GetCombinedImageSamplers()) { uint slot = compiler.GetDecoration(combined.image_id, Decoration.Binding); compiler.SetName(combined.combined_id, $"uTexture{slot}"); } } if (args.Length < 3) { Console.Error.WriteLine("usage: ShaderTranspiler [-I ]..."); return 1; } string dxcPath = args[0]; string sourcePath = args[1]; string outputDirectory = args[2]; var includeDirectories = new List(); for (int i = 3; i < args.Length; ++i) { if (args[i] == "-I" && i + 1 < args.Length) { includeDirectories.Add(args[++i]); } } string tempDirectory = Path.Combine(Path.GetTempPath(), "Blacklight.WebGL2.ShaderTranspiler." + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDirectory); try { byte[] vertexSpirv = CompileToSpirv(dxcPath, sourcePath, "vs_6_0", "MainVS", includeDirectories, tempDirectory); byte[] fragmentSpirv = CompileToSpirv(dxcPath, sourcePath, "ps_6_0", "MainPS", includeDirectories, tempDirectory); string vertexGlsl = CrossCompileToGlslEs(vertexSpirv, interfacePrefixToStrip: "out_var_"); string fragmentGlsl = CrossCompileToGlslEs(fragmentSpirv, interfacePrefixToStrip: "in_var_"); Directory.CreateDirectory(outputDirectory); File.WriteAllText(Path.Combine(outputDirectory, "Main.vert.glsl"), vertexGlsl); File.WriteAllText(Path.Combine(outputDirectory, "Main.frag.glsl"), fragmentGlsl); } finally { Directory.Delete(tempDirectory, recursive: true); } return 0;