using Ghost.Core; using Ghost.Core.Graphics; using Ghost.DSL.ShaderCompiler.Parser; using System.Text; namespace Ghost.DSL.ShaderCompiler; public struct DSLShaderError { public string message; public int line; public int column; public override readonly string ToString() { return $"Error at {line}:{column} - {message}"; } } internal static class DSLShaderCompiler { private const string _GLOBAL_PROPERTY_FILE_NAME = "GlobalData.g.hlsl"; private const string _GENERATED_FILE_HEADER = "// Auto-generated shader file. Please do not edit this file directly."; // private struct ShaderInheritance // { // public DSLShaderSemantics? parent; // public List? children; // } public static List ParseShaders(TokenStream stream) { var shaders = new List(); while (stream.TryPeek(out var nextToken)) { if (ShaderBlock.ShouldEnter(nextToken)) { var shader = ShaderBlock.Parse(stream.SliceNextBlock()); shaders.Add(shader); } else if (nextToken.Match(TokenType.EndOfFile)) { stream.Consume(); } else { throw new Exception($"Unexpected token '{nextToken.lexeme}' at top level. Expected 'shader' declaration."); } } return shaders; } public static DSLShaderSemantics? SemanticAnalysis(DSLShaderSyntax syntax, out List errors) { errors = new List(); if (string.IsNullOrWhiteSpace(syntax.name.lexeme)) { errors.Add(new DSLShaderError { message = "Shader name cannot be empty.", line = syntax.name.line, column = syntax.name.column }); return null; } var shaderModel = ShaderBlock.SemanticAnalysis(syntax, errors); return shaderModel; } private static List? TopologicalSort(ReadOnlySpan semantics) { var inDegrees = new Dictionary(); var childrenMap = new Dictionary>(); var semanticsMap = new Dictionary(); foreach (var s in semantics) { inDegrees[s.name] = 0; childrenMap[s.name] = new List(); semanticsMap[s.name] = s; } foreach (var s in semantics) { if (!string.IsNullOrEmpty(s.fallback) && semanticsMap.ContainsKey(s.fallback)) { childrenMap[s.fallback].Add(s.name); inDegrees[s.name]++; } } var queue = new Queue(); foreach (var s in semantics) { if (inDegrees[s.name] == 0) { queue.Enqueue(s); } } var sortedList = new List(); while (queue.Count > 0) { var current = queue.Dequeue(); sortedList.Add(current); foreach (var childName in childrenMap[current.name]) { inDegrees[childName]--; if (inDegrees[childName] == 0) { queue.Enqueue(semanticsMap[childName]); } } } // If there's a cycle, the graph will not be fully traversed. return sortedList.Count == semantics.Length ? sortedList : null; } private static string GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass) { return $"{shader.name}_{pass.name}"; } private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent) { if (semantic == null) { return parent; } return new PipelineState { ZTest = semantic.zTest ?? parent.ZTest, ZWrite = semantic.zWrite ?? parent.ZWrite, Cull = semantic.cull ?? parent.Cull, Blend = semantic.blend ?? parent.Blend, ColorMask = semantic.colorMask ?? parent.ColorMask }; } private static uint CalculateCBufferSize(ReadOnlySpan properties) { if (properties.IsEmpty) { return 0; } var currentOffset = 0u; foreach (var prop in properties) { var size = prop.type.GetSize(); if ((currentOffset % 16) + size > 16) { currentOffset = (currentOffset + 15u) & ~15u; } currentOffset += size; } return (currentOffset + 15u) & ~15u; } // TODO: Implement shader inheritance resolution, including property and pass merging. // Currently, we just ignore inheritance. public static ShaderDescriptor ResolveShader(DSLShaderSemantics semantics) { var descriptor = new ShaderDescriptor { name = semantics.name }; var shaderGlobalProperties = semantics.properties? .Where(p => p.scope == PropertyScope.Global) .Select(p => new PropertyDescriptor { name = p.name, type = p.type, defaultValue = p.defaultValue }).ToArray(); var shaderLocalProperties = semantics.properties? .Where(p => p.scope == PropertyScope.Local) .Select(p => new PropertyDescriptor { name = p.name, type = p.type, defaultValue = p.defaultValue }).ToArray(); descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty(); descriptor.properties = shaderLocalProperties ?? Array.Empty(); descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties); if (semantics.passes != null) { descriptor.passes = new PassDescriptor[semantics.passes.Count]; for (int i = 0; i < semantics.passes.Count; i++) { var pass = semantics.passes[i]; var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default); descriptor.passes[i] = new PassDescriptor { identifier = GetPassUniqueId(semantics, pass), name = pass.name, taskShader = pass.taskShader, meshShader = pass.meshShader, pixelShader = pass.pixelShader, localPipeline = localPipeline, defines = pass.defines?.ToArray() ?? Array.Empty(), includes = pass.includes?.ToArray() ?? Array.Empty(), keywords = pass.keywords?.ToArray() ?? Array.Empty() }; } } else { descriptor.passes = Array.Empty(); } return descriptor; } public static Result CompileShader(string shaderPath, string generatedOutputDirectory) { try { var source = File.ReadAllText(shaderPath); var lexer = new Lexer(source); var stream = new TokenStream(lexer.Tokenize()); var shaderInfo = ParseShaders(stream); if (shaderInfo.Count == 0) { return Result.Failure("No shader found in the provided file."); } var model = SemanticAnalysis(shaderInfo[0], out var errors); if (errors.Count != 0 || model == null) { var errorMessages = new StringBuilder(); foreach (var error in errors) { errorMessages.AppendLine(error.ToString()); } return Result.Failure("Failed to compile shader due to errors:\n" + errorMessages.ToString()); } var desc = ResolveShader(model); var globalPropResult = GenerateGlobalProperties(desc.globalProperties, generatedOutputDirectory); if (globalPropResult.IsFailure) { return Result.Failure("Failed to generate global properties: " + globalPropResult.Message); } var generatedResult = GenerateShaderCode(desc, generatedOutputDirectory); if (generatedResult.IsFailure) { return Result.Failure("Failed to generate pass files: " + generatedResult.Message); } foreach (ref var pass in desc.passes.AsSpan()) { if (pass.includes == null) { pass.includes = new string[2]; } else { Array.Resize(ref pass.includes, pass.includes.Length + 2); // Shift existing includes to make room for the two new includes at the front. pass.includes.AsSpan(0, pass.includes.Length - 2).CopyTo(pass.includes.AsSpan(2)); } pass.includes[0] = globalPropResult.Value; pass.includes[1] = generatedResult.Value; } return desc; } catch (Exception ex) { return Result.Failure("Failed to compile shader: " + ex.Message); } } private static string ShaderPropertyTypeToHLSLType(ShaderPropertyType type) { return type switch { ShaderPropertyType.Float => "float", ShaderPropertyType.Float2 => "float2", ShaderPropertyType.Float3 => "float3", ShaderPropertyType.Float4 => "float4", ShaderPropertyType.Int => "int", ShaderPropertyType.Int2 => "int2", ShaderPropertyType.Int3 => "int3", ShaderPropertyType.Int4 => "int4", ShaderPropertyType.UInt => "uint", ShaderPropertyType.UInt2 => "uint2", ShaderPropertyType.UInt3 => "uint3", ShaderPropertyType.UInt4 => "uint4", ShaderPropertyType.Bool => "bool", ShaderPropertyType.Bool2 => "bool2", ShaderPropertyType.Bool3 => "bool3", ShaderPropertyType.Bool4 => "bool4", // NOTE: Textures here are bindless, represented as uint (descriptor index). ShaderPropertyType.Texture2D => "TEXTURE2D", ShaderPropertyType.Texture3D => "TEXTURE3D", ShaderPropertyType.TextureCube => "TEXTURECUBE", ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY", ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY", ShaderPropertyType.Sampler => "SAMPLER", _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}") }; } public static Result GenerateShaderCode(ShaderDescriptor descriptor, string targetDirectory) { if (!Directory.Exists(targetDirectory)) { return Result.Failure("Target directory does not exist."); } var outputFileName = descriptor.name.Replace('/', '_'); var outputFilePath = Path.Combine(targetDirectory, outputFileName + ".g.hlsl"); var outputDirectory = Path.GetDirectoryName(outputFilePath); if (!Directory.Exists(outputDirectory)) { Directory.CreateDirectory(outputDirectory!); } using var fileStream = File.CreateText(outputFilePath); var fileDefine = outputFileName.Replace('/', '_').ToUpperInvariant() + "_G_HLSL"; var sb = new StringBuilder(); sb.AppendLine(_GENERATED_FILE_HEADER); sb.AppendLine(@$" #ifndef {fileDefine} #define {fileDefine} #include ""F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl"""); sb.Append(@" struct PerMaterialData {"); foreach (var prop in descriptor.properties) { sb.Append($@" {ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};"); } sb.Append(@" };"); sb.AppendLine(); sb.AppendLine(@$" #endif // {fileDefine}"); fileStream.Write(sb.ToString()); return outputFilePath; } public static Result GenerateGlobalProperties(ReadOnlySpan globalProperties, string targetDirectory) { if (!Directory.Exists(targetDirectory)) { return Result.Failure("Target directory does not exist."); } var globalFilePath = Path.Combine(targetDirectory, _GLOBAL_PROPERTY_FILE_NAME); using var globalFileStream = File.CreateText(globalFilePath); var sb = new StringBuilder(); sb.AppendLine(_GENERATED_FILE_HEADER); sb.Append(@" #ifndef GLOBALDATA_G_HLSL #define GLOBALDATA_G_HLSL #include ""F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl"" struct GlobalData {"); foreach (var prop in globalProperties) { sb.Append($@" {ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};"); } sb.AppendLine(@" }; #endif // GLOBALDATA_G_HLSL"); globalFileStream.Write(sb.ToString()); return globalFilePath; } }