Refactor and enhance graphics and audio systems
Updated target frameworks to .NET 10.0 across multiple projects for compatibility with the latest features. Refactored namespaces and introduced new classes for shader descriptors, FMOD integration, and DirectX 12 utilities using TerraFX. Replaced `Win32` bindings with TerraFX equivalents for DirectX 12. Added a C# wrapper for FMOD Studio API, including DSP and error handling. Enhanced entity queries, component storage, and query filters for better performance and type safety. Introduced new test projects and updated the solution structure. Added `meshoptimizer` bindings and integrated `meshoptimizer_native.dll`. Improved code readability, maintainability, and performance.
This commit is contained in:
47
Ghost.Shader/Compiler/Parser/DefinesBlock.cs
Normal file
47
Ghost.Shader/Compiler/Parser/DefinesBlock.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Ghost.Shader.Compiler;
|
||||
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class DefinesBlock : IBlockParser<List<Token>, List<string>>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.DEFINES);
|
||||
}
|
||||
|
||||
public static List<Token> Parse(TokenStreamSlice stream)
|
||||
{
|
||||
stream.Expect(TokenType.Keyword);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var defines = new List<Token>();
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.HasMore)
|
||||
{
|
||||
var defineToken = bodyStream.Expect(TokenType.Identifier);
|
||||
defines.Add(defineToken);
|
||||
bodyStream.Expect(TokenType.Semicolon);
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return defines;
|
||||
}
|
||||
|
||||
public static List<string>? SemanticAnalysis(List<Token>? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var defines = new List<string>(syntax.Count);
|
||||
foreach (var defineToken in syntax)
|
||||
{
|
||||
defines.Add(defineToken.lexeme);
|
||||
}
|
||||
|
||||
return defines;
|
||||
}
|
||||
}
|
||||
8
Ghost.Shader/Compiler/Parser/IBlockParser.cs
Normal file
8
Ghost.Shader/Compiler/Parser/IBlockParser.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal interface IBlockParser<T, U>
|
||||
{
|
||||
public static abstract bool ShouldEnter(Token token);
|
||||
public static abstract T? Parse(TokenStreamSlice ts);
|
||||
public static abstract U? SemanticAnalysis(T? syntax, List<ShaderError> errors);
|
||||
}
|
||||
61
Ghost.Shader/Compiler/Parser/IncludesBlock.cs
Normal file
61
Ghost.Shader/Compiler/Parser/IncludesBlock.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Ghost.Shader.Compiler;
|
||||
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class IncludesBlock : IBlockParser<List<Token>, List<string>>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.INCLUDES);
|
||||
}
|
||||
|
||||
public static List<Token> Parse(TokenStreamSlice stream)
|
||||
{
|
||||
stream.Expect(TokenType.Keyword);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var includes = new List<Token>();
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.HasMore)
|
||||
{
|
||||
var includeToken = bodyStream.Expect(TokenType.StringLiteral);
|
||||
includes.Add(includeToken);
|
||||
bodyStream.Expect(TokenType.Semicolon);
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return includes;
|
||||
}
|
||||
|
||||
public static List<string>? SemanticAnalysis(List<Token>? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null || syntax.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var includes = new List<string>(syntax.Count);
|
||||
foreach (var includeToken in syntax)
|
||||
{
|
||||
var path = includeToken.lexeme;
|
||||
if (File.Exists(path))
|
||||
{
|
||||
includes.Add(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Included file '{path}' not found.",
|
||||
line = includeToken.line,
|
||||
column = includeToken.column
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return includes;
|
||||
}
|
||||
}
|
||||
84
Ghost.Shader/Compiler/Parser/KeywordsBlock.cs
Normal file
84
Ghost.Shader/Compiler/Parser/KeywordsBlock.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Ghost.Shader.Compiler.Parser;
|
||||
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class KeywordsBlock : IBlockParser<List<FunctionCallDeclaration>, List<KeywordsGroup>>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.KEYWORDS);
|
||||
}
|
||||
|
||||
public static List<FunctionCallDeclaration> Parse(TokenStreamSlice stream)
|
||||
{
|
||||
stream.Expect(TokenType.Keyword);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var keywords = new List<FunctionCallDeclaration>();
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.HasMore)
|
||||
{
|
||||
var keywordToken = bodyStream.Expect(TokenType.Identifier);
|
||||
var args = ParseUtility.ParseFunctionArguments(ref bodyStream, TokenType.Identifier);
|
||||
keywords.Add(new FunctionCallDeclaration { name = keywordToken, arguments = args });
|
||||
bodyStream.Expect(TokenType.Semicolon);
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return keywords;
|
||||
}
|
||||
|
||||
public static List<KeywordsGroup>? SemanticAnalysis(List<FunctionCallDeclaration>? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var keywords = new List<KeywordsGroup>(syntax.Count);
|
||||
foreach (var keyword in syntax)
|
||||
{
|
||||
if (keyword.arguments == null || keyword.arguments.Count == 0)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Function '{keyword.name.lexeme}' must have at least one argument.",
|
||||
line = keyword.name.line,
|
||||
column = keyword.name.column
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var group = new KeywordsGroup();
|
||||
switch (keyword.name.lexeme)
|
||||
{
|
||||
case TokenLexicon.KnownFunctions.DYNAMIC:
|
||||
group.type = KeywordType.Dynamic;
|
||||
break;
|
||||
case TokenLexicon.KnownFunctions.STATIC:
|
||||
group.type = KeywordType.Static;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Unknown function name '{keyword.name.lexeme}'.",
|
||||
line = keyword.name.line,
|
||||
column = keyword.name.column
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var arg in keyword.arguments)
|
||||
{
|
||||
group.keywords ??= new List<string>(keyword.arguments.Count);
|
||||
group.keywords.Add(arg.lexeme);
|
||||
}
|
||||
|
||||
keywords.Add(group);
|
||||
}
|
||||
|
||||
return keywords;
|
||||
}
|
||||
}
|
||||
71
Ghost.Shader/Compiler/Parser/ParseUtility.cs
Normal file
71
Ghost.Shader/Compiler/Parser/ParseUtility.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal static class ParseUtility
|
||||
{
|
||||
public static List<Token> ParseFunctionArguments(ref TokenStreamSlice stream, TokenType tokenType)
|
||||
{
|
||||
var args = new List<Token>();
|
||||
stream.Expect(TokenType.LParen);
|
||||
|
||||
while (!stream.Peek().type.Equals(TokenType.RParen))
|
||||
{
|
||||
var argToken = stream.Expect(tokenType);
|
||||
args.Add(argToken);
|
||||
|
||||
if (stream.Peek().type == TokenType.Comma)
|
||||
{
|
||||
stream.Consume();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RParen);
|
||||
return args;
|
||||
}
|
||||
|
||||
public static FunctionCallDeclaration ParseFunction(ref TokenStreamSlice stream, TokenType tokenType)
|
||||
{
|
||||
var functionToken = stream.Expect(TokenType.Identifier);
|
||||
var args = ParseFunctionArguments(ref stream, tokenType);
|
||||
stream.Expect(TokenType.Semicolon);
|
||||
|
||||
return new FunctionCallDeclaration
|
||||
{
|
||||
name = functionToken,
|
||||
arguments = args
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TrySliceLine(ref TokenStreamSlice stream, out TokenStreamSlice lineStream)
|
||||
{
|
||||
var length = 0;
|
||||
if (!stream.TryPeek(out var nextToken))
|
||||
{
|
||||
lineStream = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!nextToken.Match(TokenType.Semicolon) && !nextToken.Match(TokenType.RBrace))
|
||||
{
|
||||
length++;
|
||||
|
||||
if (!stream.TryPeek(length, out nextToken))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
lineStream = stream.Slice(length);
|
||||
stream.Consume(); // Consume the semicolon
|
||||
return true;
|
||||
}
|
||||
|
||||
lineStream = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
162
Ghost.Shader/Compiler/Parser/PassBlock.cs
Normal file
162
Ghost.Shader/Compiler/Parser/PassBlock.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
// TODO: Add pass template support.
|
||||
// Pass templates let user to inject their own custom code into the generated HLSL code.
|
||||
// This is useful for adding custom lighting models, custom shadowing techniques, or other advanced effects without touching the core shader code.
|
||||
internal class PassBlock : IBlockParser<PassSyntax, PassSemantic>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.PASS);
|
||||
}
|
||||
|
||||
public static PassSyntax Parse(TokenStreamSlice stream)
|
||||
{
|
||||
var pass = new PassSyntax();
|
||||
|
||||
stream.Expect(TokenType.Keyword);
|
||||
pass.name = stream.Expect(TokenType.StringLiteral);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.TryPeek(out var nextToken))
|
||||
{
|
||||
if (DefinesBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
pass.defines = DefinesBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (IncludesBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
pass.includes = IncludesBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (KeywordsBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
pass.keywords = KeywordsBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (PipelineBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
pass.localPipeline = PipelineBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (PropertiesBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
pass.localProperties = PropertiesBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (nextToken.Match(TokenType.Identifier))
|
||||
{
|
||||
var func = ParseUtility.ParseFunction(ref bodyStream, TokenType.StringLiteral);
|
||||
|
||||
pass.functionCalls ??= new();
|
||||
pass.functionCalls.Add(func);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Unexpected token '{nextToken}' in pass body.");
|
||||
}
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return pass;
|
||||
}
|
||||
|
||||
public static PassSemantic? SemanticAnalysis(PassSyntax? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var model = new PassSemantic
|
||||
{
|
||||
name = syntax.name.lexeme,
|
||||
defines = DefinesBlock.SemanticAnalysis(syntax.defines, errors),
|
||||
includes = IncludesBlock.SemanticAnalysis(syntax.includes, errors),
|
||||
keywords = KeywordsBlock.SemanticAnalysis(syntax.keywords, errors),
|
||||
localProperties = PropertiesBlock.SemanticAnalysis(syntax.localProperties, errors),
|
||||
localPipeline = PipelineBlock.SemanticAnalysis(syntax.localPipeline, errors),
|
||||
};
|
||||
|
||||
if (model.localProperties != null
|
||||
&& model.localProperties.Any(p => p.scope == PropertyScope.Global))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Global properties cannot be declared inside a pass. Move them to the shader properties block.",
|
||||
line = syntax.name.line,
|
||||
column = syntax.name.column
|
||||
});
|
||||
}
|
||||
|
||||
if (syntax.functionCalls != null)
|
||||
{
|
||||
foreach (var func in syntax.functionCalls)
|
||||
{
|
||||
switch (func.name.lexeme)
|
||||
{
|
||||
case "vs":
|
||||
if (func.arguments?.Count != 2)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Vertex shader declaration requires exactly two arguments: (shaderPath, entryPoint).",
|
||||
line = func.name.line,
|
||||
column = func.name.column
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
model.vertexShader = new ShaderEntryPoint
|
||||
{
|
||||
shader = func.arguments[0].lexeme,
|
||||
entry = func.arguments[1].lexeme
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "ps":
|
||||
if (func.arguments?.Count != 2)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Pixel shader declaration requires exactly two arguments: (shaderPath, entryPoint).",
|
||||
line = func.name.line,
|
||||
column = func.name.column
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
model.pixelShader = new ShaderEntryPoint
|
||||
{
|
||||
shader = func.arguments[0].lexeme,
|
||||
entry = func.arguments[1].lexeme
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Unknown function '{func.name.lexeme}' in pass.",
|
||||
line = func.name.line,
|
||||
column = func.name.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.vertexShader.shader == null || model.pixelShader.shader == null)
|
||||
{
|
||||
// TODO: Inheritance from base pass.
|
||||
// TODO: Add mesh shader support.
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Pass must contain a vertex shader (vs) and a pixel shader (ps) declaration.",
|
||||
line = syntax.name.line,
|
||||
column = syntax.name.column
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
187
Ghost.Shader/Compiler/Parser/PipelineBlock.cs
Normal file
187
Ghost.Shader/Compiler/Parser/PipelineBlock.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class PipelineBlock : IBlockParser<PipelineSyntax, PipelineSemantic>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.PIPELINE);
|
||||
}
|
||||
|
||||
public static PipelineSyntax Parse(TokenStreamSlice stream)
|
||||
{
|
||||
stream.Expect(TokenType.Keyword);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var pipeline = new PipelineSyntax();
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.HasMore)
|
||||
{
|
||||
var stateToken = bodyStream.Expect(TokenType.Identifier);
|
||||
bodyStream.Expect(TokenType.Equals);
|
||||
var valueToken = bodyStream.Expect(TokenType.Identifier | TokenType.Number);
|
||||
|
||||
pipeline.values ??= new();
|
||||
pipeline.values.Add(new ValueDeclaration
|
||||
{
|
||||
name = stateToken,
|
||||
value = valueToken
|
||||
});
|
||||
|
||||
bodyStream.Expect(TokenType.Semicolon);
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
public static PipelineSemantic? SemanticAnalysis(PipelineSyntax? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var semantic = new PipelineSemantic();
|
||||
if (syntax.values != null)
|
||||
{
|
||||
foreach (var valueDecl in syntax.values)
|
||||
{
|
||||
switch (valueDecl.name.lexeme)
|
||||
{
|
||||
case TokenLexicon.KnownPipelineProperties.ZTEST:
|
||||
switch (valueDecl.value.lexeme)
|
||||
{
|
||||
case "disable":
|
||||
semantic.zTest = ZTestOptions.Disabled;
|
||||
break;
|
||||
case "less":
|
||||
semantic.zTest = ZTestOptions.Less;
|
||||
break;
|
||||
case "less_equal":
|
||||
semantic.zTest = ZTestOptions.LessEqual;
|
||||
break;
|
||||
case "equal":
|
||||
semantic.zTest = ZTestOptions.Equal;
|
||||
break;
|
||||
case "greater_equal":
|
||||
semantic.zTest = ZTestOptions.GreaterEqual;
|
||||
break;
|
||||
case "greater":
|
||||
semantic.zTest = ZTestOptions.Greater;
|
||||
break;
|
||||
case "not_equal":
|
||||
semantic.zTest = ZTestOptions.NotEqual;
|
||||
break;
|
||||
case "always":
|
||||
semantic.zTest = ZTestOptions.Always;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Invalid ZTest option: {valueDecl.value.lexeme}",
|
||||
line = valueDecl.value.line,
|
||||
column = valueDecl.value.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case TokenLexicon.KnownPipelineProperties.ZWRITE:
|
||||
switch (valueDecl.value.lexeme)
|
||||
{
|
||||
case "on":
|
||||
semantic.zWrite = ZWriteOptions.On;
|
||||
break;
|
||||
case "off":
|
||||
semantic.zWrite = ZWriteOptions.Off;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Invalid ZWrite option: {valueDecl.value.lexeme}",
|
||||
line = valueDecl.value.line,
|
||||
column = valueDecl.value.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case TokenLexicon.KnownPipelineProperties.CULL:
|
||||
switch (valueDecl.value.lexeme)
|
||||
{
|
||||
case "off":
|
||||
semantic.cull = CullOptions.Off;
|
||||
break;
|
||||
case "front":
|
||||
semantic.cull = CullOptions.Front;
|
||||
break;
|
||||
case "back":
|
||||
semantic.cull = CullOptions.Back;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Invalid Cull option: {valueDecl.value.lexeme}",
|
||||
line = valueDecl.value.line,
|
||||
column = valueDecl.value.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case TokenLexicon.KnownPipelineProperties.BLEND:
|
||||
switch (valueDecl.value.lexeme)
|
||||
{
|
||||
case "opaque":
|
||||
semantic.blend = BlendOptions.Opaque;
|
||||
break;
|
||||
case "alpha":
|
||||
semantic.blend = BlendOptions.Alpha;
|
||||
break;
|
||||
case "additive":
|
||||
semantic.blend = BlendOptions.Additive;
|
||||
break;
|
||||
case "multiply":
|
||||
semantic.blend = BlendOptions.Multiply;
|
||||
break;
|
||||
case "premultiplied":
|
||||
semantic.blend = BlendOptions.PremultipliedAlpha;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Invalid Blend option: {valueDecl.value.lexeme}",
|
||||
line = valueDecl.value.line,
|
||||
column = valueDecl.value.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case TokenLexicon.KnownPipelineProperties.COLORMASK:
|
||||
if (uint.TryParse(valueDecl.value.lexeme, out var colorMask))
|
||||
{
|
||||
semantic.colorMask = colorMask;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Invalid Color Mask value: {valueDecl.value.lexeme}",
|
||||
line = valueDecl.value.line,
|
||||
column = valueDecl.value.column
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return semantic;
|
||||
}
|
||||
}
|
||||
467
Ghost.Shader/Compiler/Parser/PropertiesBlock.cs
Normal file
467
Ghost.Shader/Compiler/Parser/PropertiesBlock.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using Ghost.Shader.Compiler.Parser;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySemantic>>
|
||||
{
|
||||
private delegate object? PropertyValueBuilder(List<Token> tokens, List<ShaderError> errors);
|
||||
|
||||
private sealed record PropTypeInfo(int ArgCount, TokenType ArgTokenType, PropertyValueBuilder? Builder);
|
||||
|
||||
private static readonly Dictionary<ShaderPropertyType, PropTypeInfo> s_propTypeInfo = new()
|
||||
{
|
||||
// Floats
|
||||
[ShaderPropertyType.Float] = new(1, TokenType.Number, (syntax, errors) => ParseFloatValue(syntax[0], errors)),
|
||||
[ShaderPropertyType.Float2] = new(2, TokenType.Number, (syntax, errors) => new float2(
|
||||
ParseFloatValue(syntax[0], errors),
|
||||
ParseFloatValue(syntax[1], errors))),
|
||||
[ShaderPropertyType.Float3] = new(3, TokenType.Number, (syntax, errors) => new float3(
|
||||
ParseFloatValue(syntax[0], errors),
|
||||
ParseFloatValue(syntax[1], errors),
|
||||
ParseFloatValue(syntax[2], errors))),
|
||||
[ShaderPropertyType.Float4] = new(4, TokenType.Number, (syntax, errors) => new float4(
|
||||
ParseFloatValue(syntax[0], errors),
|
||||
ParseFloatValue(syntax[1], errors),
|
||||
ParseFloatValue(syntax[2], errors),
|
||||
ParseFloatValue(syntax[3], errors))),
|
||||
|
||||
// Ints
|
||||
[ShaderPropertyType.Int] = new(1, TokenType.Number, (syntax, errors) => ParseIntValue(syntax[0], errors)),
|
||||
[ShaderPropertyType.Int2] = new(2, TokenType.Number, (syntax, errors) => new int2(
|
||||
ParseIntValue(syntax[0], errors),
|
||||
ParseIntValue(syntax[1], errors))),
|
||||
[ShaderPropertyType.Int3] = new(3, TokenType.Number, (syntax, errors) => new int3(
|
||||
ParseIntValue(syntax[0], errors),
|
||||
ParseIntValue(syntax[1], errors),
|
||||
ParseIntValue(syntax[2], errors))),
|
||||
[ShaderPropertyType.Int4] = new(4, TokenType.Number, (syntax, errors) => new int4(
|
||||
ParseIntValue(syntax[0], errors),
|
||||
ParseIntValue(syntax[1], errors),
|
||||
ParseIntValue(syntax[2], errors),
|
||||
ParseIntValue(syntax[3], errors))),
|
||||
|
||||
// UInts
|
||||
[ShaderPropertyType.UInt] = new(1, TokenType.Number, (syntax, errors) => ParseUIntValue(syntax[0], errors)),
|
||||
[ShaderPropertyType.UInt2] = new(2, TokenType.Number, (syntax, errors) => new uint2(
|
||||
ParseUIntValue(syntax[0], errors),
|
||||
ParseUIntValue(syntax[1], errors))),
|
||||
[ShaderPropertyType.UInt3] = new(3, TokenType.Number, (syntax, errors) => new uint3(
|
||||
ParseUIntValue(syntax[0], errors),
|
||||
ParseUIntValue(syntax[1], errors),
|
||||
ParseUIntValue(syntax[2], errors))),
|
||||
[ShaderPropertyType.UInt4] = new(4, TokenType.Number, (syntax, errors) => new uint4(
|
||||
ParseUIntValue(syntax[0], errors),
|
||||
ParseUIntValue(syntax[1], errors),
|
||||
ParseUIntValue(syntax[2], errors),
|
||||
ParseUIntValue(syntax[3], errors))),
|
||||
|
||||
// Bools (numbers 0/1)
|
||||
[ShaderPropertyType.Bool] = new(1, TokenType.Number, (syntax, errors) => ParseBoolValue(syntax[0], errors)),
|
||||
[ShaderPropertyType.Bool2] = new(2, TokenType.Number, (syntax, errors) => new bool2(
|
||||
ParseBoolValue(syntax[0], errors),
|
||||
ParseBoolValue(syntax[1], errors))),
|
||||
[ShaderPropertyType.Bool3] = new(3, TokenType.Number, (syntax, errors) => new bool3(
|
||||
ParseBoolValue(syntax[0], errors),
|
||||
ParseBoolValue(syntax[1], errors),
|
||||
ParseBoolValue(syntax[2], errors))),
|
||||
[ShaderPropertyType.Bool4] = new(4, TokenType.Number, (syntax, errors) => new bool4(
|
||||
ParseBoolValue(syntax[0], errors),
|
||||
ParseBoolValue(syntax[1], errors),
|
||||
ParseBoolValue(syntax[2], errors),
|
||||
ParseBoolValue(syntax[3], errors))),
|
||||
|
||||
// Textures (single identifier argument)
|
||||
[ShaderPropertyType.Texture2D] = new(1, TokenType.Identifier, (syntax, errors) => ParseTextureDefault(syntax[0], errors)),
|
||||
[ShaderPropertyType.Texture3D] = new(1, TokenType.Identifier, (syntax, errors) => ParseTextureDefault(syntax[0], errors)),
|
||||
[ShaderPropertyType.TextureCube] = new(1, TokenType.Identifier, (syntax, errors) => ParseTextureDefault(syntax[0], errors)),
|
||||
};
|
||||
|
||||
private static float ParseFloatValue(Token token, List<ShaderError> errors)
|
||||
{
|
||||
if (!float.TryParse(token.lexeme, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Failed to parse float value '{token.lexeme}'.",
|
||||
line = token.line,
|
||||
column = token.column
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ParseIntValue(Token token, List<ShaderError> errors)
|
||||
{
|
||||
if (!int.TryParse(token.lexeme, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Failed to parse int value '{token.lexeme}'.",
|
||||
line = token.line,
|
||||
column = token.column
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint ParseUIntValue(Token token, List<ShaderError> errors)
|
||||
{
|
||||
if (!uint.TryParse(token.lexeme, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Failed to parse uint value '{token.lexeme}'.",
|
||||
line = token.line,
|
||||
column = token.column
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ParseBoolValue(Token token, List<ShaderError> errors)
|
||||
{
|
||||
if (!bool.TryParse(token.lexeme, out var result))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Failed to parse bool value '{token.lexeme}'.",
|
||||
line = token.line,
|
||||
column = token.column
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ParseTextureDefault(Token token, List<ShaderError> errors)
|
||||
{
|
||||
if (!TokenLexicon.IsTextureDefaultValue(token.lexeme))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Texture default value '{token.lexeme}' is not valid.",
|
||||
line = token.line,
|
||||
column = token.column
|
||||
});
|
||||
}
|
||||
|
||||
return token.lexeme;
|
||||
}
|
||||
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.PROPERTIES);
|
||||
}
|
||||
|
||||
private static ShaderPropertyType FromString(string type)
|
||||
{
|
||||
return type.ToLower() switch
|
||||
{
|
||||
"float" => ShaderPropertyType.Float,
|
||||
"float2" => ShaderPropertyType.Float2,
|
||||
"float3" => ShaderPropertyType.Float3,
|
||||
"float4" => ShaderPropertyType.Float4,
|
||||
"int" => ShaderPropertyType.Int,
|
||||
"int2" => ShaderPropertyType.Int2,
|
||||
"int3" => ShaderPropertyType.Int3,
|
||||
"int4" => ShaderPropertyType.Int4,
|
||||
"uint" => ShaderPropertyType.UInt,
|
||||
"uint2" => ShaderPropertyType.UInt2,
|
||||
"uint3" => ShaderPropertyType.UInt3,
|
||||
"uint4" => ShaderPropertyType.UInt4,
|
||||
"bool" => ShaderPropertyType.Bool,
|
||||
"bool2" => ShaderPropertyType.Bool2,
|
||||
"bool3" => ShaderPropertyType.Bool3,
|
||||
"bool4" => ShaderPropertyType.Bool4,
|
||||
"texture2d" => ShaderPropertyType.Texture2D,
|
||||
"texture3d" => ShaderPropertyType.Texture3D,
|
||||
"texturecube" => ShaderPropertyType.TextureCube,
|
||||
_ => ShaderPropertyType.None,
|
||||
};
|
||||
}
|
||||
|
||||
public static PropertiesSyntax Parse(TokenStreamSlice stream)
|
||||
{
|
||||
stream.Expect(TokenType.Keyword);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var syntax = new PropertiesSyntax();
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.HasMore)
|
||||
{
|
||||
var shaderProperty = new PropertyDeclaration();
|
||||
if (bodyStream.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.GLOBAL)
|
||||
|| bodyStream.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.LOCAL))
|
||||
{
|
||||
var scopeToken = bodyStream.Consume();
|
||||
shaderProperty.scope = scopeToken;
|
||||
}
|
||||
|
||||
var typeToken = bodyStream.Expect(TokenType.Identifier);
|
||||
var nameToken = bodyStream.Expect(TokenType.Identifier);
|
||||
|
||||
shaderProperty.type = typeToken;
|
||||
shaderProperty.name = nameToken;
|
||||
|
||||
var nextToken = bodyStream.Consume();
|
||||
switch (nextToken.type)
|
||||
{
|
||||
case TokenType.Equals:
|
||||
{
|
||||
var constructorTypeToken = bodyStream.Expect(TokenType.Identifier);
|
||||
var args = ParseUtility.ParseFunctionArguments(ref bodyStream, TokenType.Identifier | TokenType.Number);
|
||||
shaderProperty.propertyConstructor = new FunctionCallDeclaration
|
||||
{
|
||||
name = constructorTypeToken,
|
||||
arguments = args
|
||||
};
|
||||
|
||||
bodyStream.Expect(TokenType.Semicolon);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Semicolon:
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Unexpected token '{nextToken.lexeme}' in property declaration.");
|
||||
}
|
||||
|
||||
syntax.properties ??= new();
|
||||
syntax.properties.Add(shaderProperty);
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return syntax;
|
||||
}
|
||||
|
||||
public static List<PropertySemantic>? SemanticAnalysis(PropertiesSyntax? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var models = new List<PropertySemantic>();
|
||||
var usedPropertyNames = new HashSet<string>();
|
||||
|
||||
if (syntax.properties != null)
|
||||
{
|
||||
foreach (var property in syntax.properties)
|
||||
{
|
||||
var model = new PropertySemantic
|
||||
{
|
||||
scope = property.scope.lexeme switch
|
||||
{
|
||||
TokenLexicon.KnownKeywords.GLOBAL => PropertyScope.Global,
|
||||
TokenLexicon.KnownKeywords.LOCAL => PropertyScope.Local,
|
||||
_ => PropertyScope.Local,
|
||||
}
|
||||
};
|
||||
|
||||
var flowControl = ValidatePropertyType(errors, property, model);
|
||||
if (!flowControl)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
flowControl = ValidatePropertyName(errors, usedPropertyNames, property, model);
|
||||
if (!flowControl)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.propertyConstructor != null)
|
||||
{
|
||||
flowControl = ValidatePropertyConstructor(errors, property, model);
|
||||
if (!flowControl)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
usedPropertyNames.Add(property.name.lexeme);
|
||||
models.Add(model);
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
private static bool ValidatePropertyType(List<ShaderError> errors, PropertyDeclaration property, PropertySemantic model)
|
||||
{
|
||||
if (!TokenLexicon.IsType(property.type.lexeme))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Shader property type '{property.type.lexeme}' is not a valid type.",
|
||||
line = property.type.line,
|
||||
column = property.type.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
model.type = FromString(property.type.lexeme);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ValidatePropertyName(List<ShaderError> errors, HashSet<string> usedPropertyNames, PropertyDeclaration property, PropertySemantic model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(property.name.lexeme))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Shader property has an empty name.",
|
||||
line = property.name.line,
|
||||
column = property.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
else if (usedPropertyNames.Contains(property.name.lexeme))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Shader property name '{property.name.lexeme}' is duplicated.",
|
||||
line = property.name.line,
|
||||
column = property.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
model.name = property.name.lexeme;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ValidatePropertyConstructor(List<ShaderError> errors, PropertyDeclaration property, PropertySemantic model)
|
||||
{
|
||||
var constructor = property.propertyConstructor;
|
||||
if (!constructor.HasValue)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Shader property constructor is null.",
|
||||
line = property.name.line,
|
||||
column = property.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var constructorValue = constructor.Value;
|
||||
if (string.IsNullOrWhiteSpace(constructorValue.name.lexeme))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Shader property constructor has an empty name.",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (constructorValue.name.lexeme != property.type.lexeme)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Shader property constructor name '{constructorValue.name.lexeme}' does not match property type '{property.type.lexeme}'.",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s_propTypeInfo.TryGetValue(model.type, out var info))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"No constructor metadata registered for property type '{model.type}'.",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count check
|
||||
if (constructorValue.arguments == null)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Shader property constructor arguments are null.",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (constructorValue.arguments.Count != info.ArgCount)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Shader property constructor for type '{property.type.lexeme}' expects {info.ArgCount} argument(s), but got {constructorValue.arguments.Count}.",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type check (uniform requirement for all args)
|
||||
var hasError = false;
|
||||
for (var i = 0; i < constructorValue.arguments.Count; i++)
|
||||
{
|
||||
var arg = constructorValue.arguments[i];
|
||||
if (!arg.Match(info.ArgTokenType))
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Shader property constructor argument {i} expects token kind '{info.ArgTokenType}', but got '{arg.type}'.",
|
||||
line = arg.line,
|
||||
column = arg.column
|
||||
});
|
||||
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build default value if we have a builder (textures currently null / TODO)
|
||||
if (info.Builder != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
model.defaultValue = info.Builder(constructorValue.arguments, errors);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Failed to construct default value for property '{property.name.lexeme}': {ex.Message}",
|
||||
line = constructorValue.name.line,
|
||||
column = constructorValue.name.column
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
116
Ghost.Shader/Compiler/Parser/ShaderBlock.cs
Normal file
116
Ghost.Shader/Compiler/Parser/ShaderBlock.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Shader.Compiler.Parser;
|
||||
|
||||
internal class ShaderBlock : IBlockParser<ShaderSyntax, ShaderSemantics>
|
||||
{
|
||||
public static bool ShouldEnter(Token token)
|
||||
{
|
||||
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.SHADER);
|
||||
}
|
||||
|
||||
public static ShaderSyntax Parse(TokenStreamSlice stream)
|
||||
{
|
||||
var shader = new ShaderSyntax();
|
||||
|
||||
stream.Expect(TokenType.Keyword);
|
||||
shader.name = stream.Expect(TokenType.StringLiteral);
|
||||
stream.Expect(TokenType.LBrace);
|
||||
|
||||
var bodyStream = stream.Slice(stream.Remaining - 1);
|
||||
while (bodyStream.TryPeek(out var nextToken))
|
||||
{
|
||||
if (PropertiesBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
shader.properties = PropertiesBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (PipelineBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
shader.pipeline = PipelineBlock.Parse(bodyStream.SliceNextBlock());
|
||||
}
|
||||
else if (PassBlock.ShouldEnter(nextToken))
|
||||
{
|
||||
shader.passes ??= new();
|
||||
shader.passes.Add(PassBlock.Parse(bodyStream.SliceNextBlock()));
|
||||
}
|
||||
else if (nextToken.Match(TokenType.Identifier))
|
||||
{
|
||||
var func = ParseUtility.ParseFunction(ref bodyStream, TokenType.StringLiteral | TokenType.Number | TokenType.Identifier);
|
||||
|
||||
shader.functionCalls ??= new();
|
||||
shader.functionCalls.Add(func);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Unexpected token '{nextToken}' in shader body.");
|
||||
}
|
||||
}
|
||||
|
||||
stream.Expect(TokenType.RBrace);
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
public static ShaderSemantics? SemanticAnalysis(ShaderSyntax? syntax, List<ShaderError> errors)
|
||||
{
|
||||
if (syntax == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var shaderModel = new ShaderSemantics
|
||||
{
|
||||
name = syntax.name.lexeme,
|
||||
properties = PropertiesBlock.SemanticAnalysis(syntax.properties, errors),
|
||||
pipeline = PipelineBlock.SemanticAnalysis(syntax.pipeline, errors)
|
||||
};
|
||||
|
||||
if (syntax.passes != null)
|
||||
{
|
||||
foreach (var passSyntax in syntax.passes)
|
||||
{
|
||||
var passModel = PassBlock.SemanticAnalysis(passSyntax, errors);
|
||||
if (passModel != null)
|
||||
{
|
||||
shaderModel.passes ??= new();
|
||||
shaderModel.passes.Add(passModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syntax.functionCalls != null)
|
||||
{
|
||||
foreach (var func in syntax.functionCalls)
|
||||
{
|
||||
switch (func.name.lexeme)
|
||||
{
|
||||
case TokenLexicon.KnownFunctions.FALLBACK:
|
||||
if (func.arguments == null || func.arguments.Count != 1)
|
||||
{
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = "Fallback declaration requires exactly one arguments: (fallback shader name).",
|
||||
line = func.name.line,
|
||||
column = func.name.column
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
shaderModel.fallback = func.arguments[0].lexeme;
|
||||
break;
|
||||
default:
|
||||
errors.Add(new ShaderError
|
||||
{
|
||||
message = $"Unknown function '{func.name.lexeme}' in shader.",
|
||||
line = func.name.line,
|
||||
column = func.name.column
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shaderModel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user