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:
2025-10-09 05:16:28 +09:00
parent 01a850ff94
commit 682200cbf1
126 changed files with 25587 additions and 3247 deletions

View File

@@ -0,0 +1,310 @@
namespace Ghost.Shader.Compiler;
public class Lexer
{
private readonly string _source;
private int _pos = 0;
private int _line = 1; // Lines typically start at 1
private int _column = 1; // Columns typically start at 1
public Lexer(string source)
{
_source = source;
}
public IEnumerable<Token> Tokenize()
{
while (!IsAtEnd())
{
var token = ScanToken();
if (token != Token.Null)
{
yield return token;
}
}
yield return new Token(TokenType.EndOfFile, string.Empty, _line, _column);
}
#region Core Scanning Logic
private Token ScanToken()
{
var c = Consume();
// Rule 1: Skip whitespace and handle line tracking
if (char.IsWhiteSpace(c))
{
HandleWhitespace(c);
return Token.Null;
}
// Rule 2: Handle comments
if (c == '/')
{
if (HandleComments())
{
return Token.Null;
}
// If not a comment, fall through to handle '/' as an operator if needed
}
// Rule 3: Handle string literals
if (c == '"')
{
return ScanStringLiteral();
}
// Rule 4: Handle numeric literals
if (char.IsDigit(c) || (c == '.' && char.IsDigit(Peek())))
{
return ScanNumericLiteral(c);
}
// Rule 5: Handle identifiers and keywords
if (char.IsLetter(c) || c == '_')
{
return ScanIdentifierOrKeyword(c);
}
// Rule 6: Handle single-character tokens (punctuation)
var punctuationToken = ScanPunctuation(c);
if (punctuationToken != Token.Null)
{
return punctuationToken;
}
// Rule 7: Skip unknown characters (could log warning in production)
return Token.Null;
}
#endregion
#region Rule Implementations
private void HandleWhitespace(char c)
{
if (c == '\n')
{
_line++;
_column = 1;
}
else if (c == '\r')
{
// Handle Windows line endings - peek for \n
if (Peek() == '\n')
{
Consume();
}
_line++;
_column = 1;
}
else
{
_column++;
}
}
private bool HandleComments()
{
var next = Peek();
if (next == '/') // Single-line comment
{
return ScanSingleLineComment();
}
else if (next == '*') // Multi-line comment
{
return ScanMultiLineComment();
}
return false; // Not a comment
}
private bool ScanSingleLineComment()
{
// Skip the second '/'
Consume();
// Consume until end of line
while (!IsAtEnd() && Peek() != '\n' && Peek() != '\r')
{
Consume();
}
return true;
}
private bool ScanMultiLineComment()
{
// Skip the '*'
Consume();
while (!IsAtEnd())
{
var c = Consume();
if (c == '\n')
{
_line++;
_column = 1;
}
else if (c == '*' && Peek() == '/')
{
Consume(); // Consume closing '/'
return true;
}
else
{
_column++;
}
}
// Unclosed comment - could throw error in production
return true;
}
private Token ScanStringLiteral()
{
var startLine = _line;
var startColumn = _column - 1; // Account for opening quote
var start = _pos;
while (!IsAtEnd() && Peek() != '"')
{
var c = Peek();
if (c == '\n')
{
_line++;
_column = 1;
}
else if (c == '\\')
{
// Handle escape sequences
Consume(); // Skip backslash
if (!IsAtEnd())
{
Consume();
}
continue;
}
Consume();
}
if (IsAtEnd())
{
// Unterminated string - could throw error in production
var unterminatedText = _source[start.._pos];
return new Token(TokenType.StringLiteral, unterminatedText, startLine, startColumn);
}
var text = _source[start.._pos];
Consume(); // Consume closing quote
return new Token(TokenType.StringLiteral, text, startLine, startColumn);
}
private Token ScanNumericLiteral(char firstChar)
{
var startColumn = _column - 1;
var start = _pos - 1; // Include the first character
var hasDot = firstChar == '.';
while (!IsAtEnd())
{
var c = Peek();
if (char.IsDigit(c))
{
Consume();
}
else if (c == '.' && !hasDot)
{
hasDot = true;
Consume();
}
else
{
break;
}
}
var number = _source[start.._pos];
return new Token(TokenType.Number, number, _line, startColumn);
}
private Token ScanIdentifierOrKeyword(char firstChar)
{
var startColumn = _column - 1;
var start = _pos - 1; // Include the first character
while (!IsAtEnd() && (char.IsLetterOrDigit(Peek()) || Peek() == '_'))
{
Consume();
}
var text = _source[start.._pos];
var tokenType = DetermineIdentifierType(text);
return new Token(tokenType, text, _line, startColumn);
}
private Token ScanPunctuation(char c)
{
var startColumn = _column - 1;
return c switch
{
'=' => new Token(TokenType.Equals, "=", _line, startColumn),
';' => new Token(TokenType.Semicolon, ";", _line, startColumn),
',' => new Token(TokenType.Comma, ",", _line, startColumn),
'{' => new Token(TokenType.LBrace, "{", _line, startColumn),
'}' => new Token(TokenType.RBrace, "}", _line, startColumn),
'(' => new Token(TokenType.LParen, "(", _line, startColumn),
')' => new Token(TokenType.RParen, ")", _line, startColumn),
_ => Token.Null // Unknown punctuation
};
}
#endregion
#region Classification Rules
private static TokenType DetermineIdentifierType(string text)
{
// Rule: Check if it's a known keyword first
if (TokenLexicon.IsKeyword(text))
{
return TokenType.Keyword;
}
// Rule: All other identifiers are treated as identifiers
// (Could extend this to handle functions, types, etc. as separate token types)
return TokenType.Identifier;
}
#endregion
#region Helper Methods
private bool IsAtEnd() => _pos >= _source.Length;
private char Consume()
{
if (IsAtEnd())
return '\0';
var c = _source[_pos];
_pos++;
_column++;
return c;
}
private char Peek() => IsAtEnd() ? '\0' : _source[_pos];
private char PeekNext() => _pos + 1 >= _source.Length ? '\0' : _source[_pos + 1];
#endregion
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,374 @@
using Ghost.Shader.Compiler.Parser;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Shader.Compiler;
public struct ShaderError
{
public string message;
public int line;
public int column;
public readonly override string ToString()
{
return $"Error at {line}:{column} - {message}";
}
}
internal static class ShaderCompiler
{
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 ShaderSemantics? parent;
public List<ShaderInheritance>? children;
}
public static List<ShaderSyntax> ParseShaders(TokenStream stream)
{
var shaders = new List<ShaderSyntax>();
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 ShaderSemantics? SemanticAnalysis(ShaderSyntax syntax, out List<ShaderError> errors)
{
errors = new();
if (string.IsNullOrWhiteSpace(syntax.name.lexeme))
{
errors.Add(new ShaderError
{
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<ShaderSemantics>? TopologicalSort(ReadOnlySpan<ShaderSemantics> semantics)
{
var inDegrees = new Dictionary<string, int>();
var childrenMap = new Dictionary<string, List<string>>();
var semanticsMap = new Dictionary<string, ShaderSemantics>();
foreach (var s in semantics)
{
inDegrees[s.name] = 0;
childrenMap[s.name] = new List<string>();
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<ShaderSemantics>();
foreach (var s in semantics)
{
if (inDegrees[s.name] == 0)
{
queue.Enqueue(s);
}
}
var sortedList = new List<ShaderSemantics>();
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(ShaderSemantics shader, PassSemantic pass)
{
//static ulong Fnv1a64(ReadOnlySpan<char> data)
//{
// const ulong offset = 14695981039346656037;
// const ulong prime = 1099511628211;
// var hash = offset;
// foreach (var b in data)
// {
// hash ^= b;
// hash *= prime;
// }
// return hash;
//}
//return $"{Fnv1a64(shader.name)}_{pass.name}";
return $"{shader.name}_{pass.name}";
}
private static PipelineDescriptor MeragePipeline(PipelineSemantic? semantic, PipelineDescriptor parent)
{
if (semantic == null)
{
return parent;
}
return new PipelineDescriptor
{
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 List<PropertyDescriptor> MergeProperties(List<PropertySemantic>? semantics, List<PropertyDescriptor>? parent)
{
var result = new List<PropertyDescriptor>();
if (parent != null)
{
result.AddRange(parent);
}
if (semantics != null)
{
foreach (var prop in semantics)
{
if (prop.scope == PropertyScope.Local)
{
result.Add(new PropertyDescriptor
{
name = prop.name,
type = prop.type,
defaultValue = prop.defaultValue
});
}
}
}
return result.DistinctBy(p => p.name).ToList();
}
// TODO: Implement shader inheritance resolution, including property and pass merging.
// Currently, we just ignore inheritance.
public static ShaderDescriptor ResolveShader(ShaderSemantics 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
}).ToList();
var shaderLocalProperties = semantics.properties?.Where(p => p.scope == PropertyScope.Local).Select(p => new PropertyDescriptor
{
name = p.name,
type = p.type,
defaultValue = p.defaultValue
}).ToList();
if (shaderGlobalProperties != null)
{
descriptor.globalProperties.AddRange(shaderGlobalProperties);
}
if (semantics.passes != null)
{
foreach (var pass in semantics.passes)
{
var localPipeline = MeragePipeline(pass.localPipeline, PipelineDescriptor.Default);
var localProperties = MergeProperties(pass.localProperties, shaderLocalProperties); // TODO: Merge with base shader properties if inheritance is implemented.
var fullPass = new FullPassDescriptor
{
uniqueIdentifier = GetPassUniqueId(semantics, pass),
vertexShader = pass.vertexShader,
pixelShader = pass.pixelShader,
localPipeline = localPipeline,
defines = pass.defines,
includes = pass.includes,
keywords = pass.keywords,
properties = localProperties
};
descriptor.passes.Add(fullPass);
}
}
return descriptor;
}
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_BINDLESS",
ShaderPropertyType.Texture3D => "TEXTURE3D_BINDLESS",
ShaderPropertyType.TextureCube => "TEXTURECUBE_BINDLESS",
ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY_BINDLESS",
ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY_BINDLESS",
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}")
};
}
public static string CompilePass(IPassDescriptor descriptor, string targetDirectory)
{
if (descriptor is not FullPassDescriptor fullPass)
{
throw new NotSupportedException("Only full pass descriptors are supported for compilation.");
}
if (!Directory.Exists(targetDirectory))
{
throw new ArgumentException("Target directory does not exist.", nameof(targetDirectory));
}
var outputFileName = fullPass.uniqueIdentifier.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.Shader/BuiltIn/Common.hlsl""");
if (fullPass.properties != null)
{
sb.Append(@"
struct PerMaterialData
{");
foreach (var prop in fullPass.properties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.Append(@"
};");
}
sb.AppendLine();
sb.AppendLine(@$"
#endif // {fileDefine}");
fileStream.Write(sb.ToString());
return outputFilePath;
}
public static void CompileShader(ShaderDescriptor descriptor, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{
throw new ArgumentException("Target directory does not exist.", nameof(targetDirectory));
}
// Generate global property file.
if (descriptor.globalProperties.Count > 0)
{
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.Shader/BuiltIn/Common.hlsl""
struct GlobalData
{");
foreach (var prop in descriptor.globalProperties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.AppendLine(@"
};
#endif // GLOBALDATA_G_HLSL");
globalFileStream.Write(sb.ToString());
}
// Compile each pass.
foreach (var pass in descriptor.passes)
{
CompilePass(pass, targetDirectory);
}
}
}

View File

@@ -0,0 +1,47 @@
using Ghost.Core.Graphics;
namespace Ghost.Shader.Compiler;
public enum PropertyScope
{
Global,
Local,
}
internal class PropertySemantic
{
public PropertyScope scope;
public ShaderPropertyType type;
public string name = string.Empty;
public object? defaultValue;
}
internal class PipelineSemantic
{
public ZTestOptions? zTest;
public ZWriteOptions? zWrite;
public CullOptions? cull;
public BlendOptions? blend;
public uint? colorMask;
}
internal class PassSemantic
{
public string name = string.Empty;
public ShaderEntryPoint vertexShader;
public ShaderEntryPoint pixelShader;
public List<string>? defines;
public List<string>? includes;
public List<KeywordsGroup>? keywords;
public List<PropertySemantic>? localProperties;
public PipelineSemantic? localPipeline;
}
internal class ShaderSemantics
{
public string name = string.Empty;
public string fallback = string.Empty;
public List<PropertySemantic>? properties;
public PipelineSemantic? pipeline;
public List<PassSemantic>? passes;
}

View File

@@ -0,0 +1,53 @@
namespace Ghost.Shader.Compiler;
internal struct FunctionCallDeclaration
{
public Token name;
public List<Token>? arguments;
}
internal struct PropertyDeclaration
{
public Token scope;
public Token type;
public Token name;
public FunctionCallDeclaration? propertyConstructor;
}
internal struct ValueDeclaration
{
public Token name;
public Token value;
}
internal class PropertiesSyntax
{
public List<PropertyDeclaration>? properties;
public List<FunctionCallDeclaration>? functionCalls;
}
internal class PipelineSyntax
{
public List<ValueDeclaration>? values;
public List<FunctionCallDeclaration>? functionCalls;
}
internal class PassSyntax
{
public Token name;
public PipelineSyntax? localPipeline;
public PropertiesSyntax? localProperties;
public List<Token>? defines;
public List<Token>? includes;
public List<FunctionCallDeclaration>? keywords;
public List<FunctionCallDeclaration>? functionCalls;
}
internal class ShaderSyntax
{
public Token name;
public PropertiesSyntax? properties;
public PipelineSyntax? pipeline;
public List<PassSyntax>? passes;
public List<FunctionCallDeclaration>? functionCalls;
}

View File

@@ -0,0 +1,238 @@
namespace Ghost.Shader.Compiler;
[Flags]
public enum TokenType
{
None = 0,
// Literals
Identifier = 1 << 0, // variable names, function names, etc.
Keyword = 1 << 1, // shader, properties, pipeline, pass, etc.
Number = 1 << 2, // numeric literals (123, 45.67, .5)
StringLiteral = 1 << 3, // "ForwardVS.hlsl"
// Operators and Punctuation
Equals = 1 << 4, // =
Semicolon = 1 << 5, // ;
Comma = 1 << 6, // ,
// Delimiters
LBrace = 1 << 7, // {
RBrace = 1 << 8, // }
LParen = 1 << 9, // (
RParen = 1 << 10, // )
// Special
EndOfFile = 1 << 11, // EOF
// Future extensions (commented out for now)
// LBracket = 1 << 12, // [
// RBracket = 1 << 13, // ]
// Dot = 1 << 14, // .
// Colon = 1 << 15, // :
// Plus = 1 << 16, // +
// Minus = 1 << 17, // -
// Star = 1 << 18, // *
// Slash = 1 << 19, // /
}
public readonly struct Token : IEquatable<Token>
{
public readonly TokenType type;
public readonly string lexeme;
public readonly int line;
public readonly int column;
public static Token Null => new Token(TokenType.None, string.Empty, -1, -1);
public Token(TokenType type, string lexeme, int line, int column)
{
this.type = type;
this.lexeme = lexeme;
this.line = line;
this.column = column;
}
public override readonly string ToString()
{
return $"{type}('{lexeme}') at {line}:{column}";
}
public bool Equals(Token other)
{
return type == other.type && lexeme == other.lexeme && line == other.line && column == other.column;
}
public override int GetHashCode()
{
return HashCode.Combine(type, lexeme, line, column);
}
public override bool Equals(object? obj)
{
return obj is Token token && Equals(token);
}
public static bool operator ==(Token left, Token right)
{
return left.Equals(right);
}
public static bool operator !=(Token left, Token right)
{
return !(left == right);
}
}
public static class TokenExtensions
{
public static bool Match(this Token token, TokenType type, string? lexeme = null)
{
if (!type.HasFlag(token.type))
{
return false;
}
if (!string.IsNullOrEmpty(lexeme) && token.lexeme != lexeme)
{
return false;
}
return true;
}
public static void Expect(this Token token, TokenType type, string? lexeme = null)
{
if (!token.Match(type, lexeme))
{
var expected = lexeme != null ? $"{type}('{lexeme}')" : type.ToString();
throw new Exception($"Unexpected token at line {token.line}, column {token.column}. Expected {expected}, got {token.type}('{token.lexeme}').");
}
}
}
internal static class TokenLexicon
{
public static class KnownKeywords
{
public const string SHADER = "shader";
public const string PROPERTIES = "properties";
public const string PIPELINE = "pipeline";
public const string PASS = "pass";
public const string DEFINES = "defines";
public const string KEYWORDS = "keywords";
public const string INCLUDES = "includes";
public const string GLOBAL = "global";
public const string LOCAL = "local";
}
public static class KnownPipelineProperties
{
public const string ZTEST = "ztest";
public const string ZWRITE = "zwrite";
public const string CULL = "cull";
public const string BLEND = "blend";
public const string COLORMASK = "color_mask";
}
public static class KnownFunctions
{
public const string VERTEX_SHADER = "vs";
public const string PIXEL_SHADER = "ps";
public const string MESH_SHADER = "ms";
public const string COMPUTE_SHADER = "cs";
public const string DYNAMIC = "dynamic";
public const string STATIC = "static";
public const string FALLBACK = "fallback";
}
public static class KnownTypes
{
// Basic types
public const string FLOAT = "float";
public const string FLOAT2 = "float2";
public const string FLOAT3 = "float3";
public const string FLOAT4 = "float4";
public const string INT = "int";
public const string INT2 = "int2";
public const string INT3 = "int3";
public const string INT4 = "int4";
public const string UINT = "uint";
public const string UINT2 = "uint2";
public const string UINT3 = "uint3";
public const string UINT4 = "uint4";
public const string BOOL = "bool";
public const string BOOL2 = "bool2";
public const string BOOL3 = "bool3";
public const string BOOL4 = "bool4";
// Texture types
public const string TEXTURE2D = "texture2d";
public const string TEXTURE2D_ARRAY = "texture2d_array";
public const string TEXTURE3D = "texture3d";
public const string TEXTURECUBE = "texturecube";
public const string TEXTURECUBE_ARRAY = "texturecube_array";
}
public static class KnownTextureValue
{
public const string WHITE = "white";
public const string BLACK = "black";
public const string GREY = "grey";
public const string NORMAL = "normal";
public const string TRANSPARENT = "transparent";
}
private static readonly HashSet<string> s_keywords = new()
{
KnownKeywords.SHADER,
KnownKeywords.PROPERTIES,
KnownKeywords.PIPELINE,
KnownKeywords.PASS,
KnownKeywords.DEFINES,
KnownKeywords.KEYWORDS,
KnownKeywords.INCLUDES,
KnownKeywords.GLOBAL,
KnownKeywords.LOCAL,
};
private static readonly HashSet<string> s_functions = new()
{
KnownFunctions.VERTEX_SHADER,
KnownFunctions.PIXEL_SHADER,
KnownFunctions.MESH_SHADER,
KnownFunctions.COMPUTE_SHADER,
KnownFunctions.DYNAMIC,
KnownFunctions.STATIC,
};
private static readonly HashSet<string> s_types = new()
{
KnownTypes.FLOAT, KnownTypes.FLOAT2, KnownTypes.FLOAT3, KnownTypes.FLOAT4,
KnownTypes.INT, KnownTypes.INT2, KnownTypes.INT3, KnownTypes.INT4,
KnownTypes.UINT, KnownTypes.UINT2, KnownTypes.UINT3, KnownTypes.UINT4,
KnownTypes.BOOL, KnownTypes.BOOL2, KnownTypes.BOOL3, KnownTypes.BOOL4,
KnownTypes.TEXTURE2D, KnownTypes.TEXTURE2D_ARRAY, KnownTypes.TEXTURE3D,
KnownTypes.TEXTURECUBE, KnownTypes.TEXTURECUBE_ARRAY,
};
private static readonly HashSet<string> s_textureDefaultValues = new()
{
KnownTextureValue.WHITE,
KnownTextureValue.BLACK,
KnownTextureValue.GREY,
KnownTextureValue.NORMAL,
KnownTextureValue.TRANSPARENT,
};
public static bool IsKeyword(string lexeme) => s_keywords.Contains(lexeme);
public static bool IsFunction(string lexeme) => s_functions.Contains(lexeme);
public static bool IsType(string lexeme) => s_types.Contains(lexeme);
public static bool IsTextureDefaultValue(string lexeme) => s_textureDefaultValues.Contains(lexeme);
}

View File

@@ -0,0 +1,312 @@
namespace Ghost.Shader.Compiler;
internal static class TokenStreamImple
{
public static Token Peek(ReadOnlySpan<Token> tokens, ref int index, int length)
{
return index + length < tokens.Length ? tokens[index + length] : throw new InvalidOperationException("No more tokens available");
}
public static bool TryPeek(ReadOnlySpan<Token> tokens, ref int index, int length, out Token token)
{
if (index + length < tokens.Length)
{
token = tokens[index + length];
return true;
}
token = default;
return false;
}
public static bool TryConsume(ReadOnlySpan<Token> tokens, ref int index, out Token token)
{
if (index < tokens.Length)
{
token = tokens[index++];
return true;
}
token = default;
return false;
}
public static Token Consume(ReadOnlySpan<Token> tokens, ref int index)
{
return index < tokens.Length ? tokens[index++] : throw new InvalidOperationException("No more tokens available");
}
public static bool Match(ReadOnlySpan<Token> tokens, ref int index, TokenType type, string? lexeme)
{
var t = Peek(tokens, ref index, 0);
if (!t.Match(type, lexeme))
{
return false;
}
//index++;
return true;
}
public static int MatchMany(ReadOnlySpan<Token> tokens, ref int index, TokenType type, string? lexeme)
{
var count = 0;
while (TryPeek(tokens, ref index, 0, out var t) && t.Match(type, lexeme))
{
index++;
count++;
}
return count;
}
public static Token Expect(ReadOnlySpan<Token> tokens, ref int index, TokenType type, string? lexeme)
{
if (!TryPeek(tokens, ref index, 0, out var t))
{
throw new InvalidOperationException("Expected token but reached end of stream");
}
if (!t.Match(type, lexeme))
{
throw new InvalidOperationException($"Expected token {type}('{lexeme ?? "*"}') but got {t}");
}
index++;
return t;
}
}
internal class TokenStream
{
private readonly Token[] _tokens;
private int _index = 0;
public int Length => _tokens.Length;
public int Remaining => _tokens.Length - _index;
public bool HasMore => _index < _tokens.Length;
public int Position
{
get => _index;
set
{
if (value < 0 || value > _tokens.Length)
{
throw new ArgumentOutOfRangeException(nameof(value), "Position must be within the bounds of the token stream.");
}
_index = value;
}
}
public TokenStream(Token[] tokens)
{
_tokens = tokens;
}
public TokenStream(IEnumerable<Token> tokens)
{
_tokens = tokens.ToArray();
}
public Token Peek(int length = 0)
{
return TokenStreamImple.Peek(_tokens, ref _index, length);
}
public bool TryPeek(out Token token)
{
return TokenStreamImple.TryPeek(_tokens, ref _index, 0, out token);
}
public bool TryPeek(int length, out Token token)
{
return TokenStreamImple.TryPeek(_tokens, ref _index, length, out token);
}
public bool TryConsume(out Token token)
{
return TokenStreamImple.TryConsume(_tokens, ref _index, out token);
}
public Token Consume()
{
return TokenStreamImple.Consume(_tokens, ref _index);
}
public bool Match(TokenType type, string? lexeme = null)
{
return TokenStreamImple.Match(_tokens, ref _index, type, lexeme);
}
public int MatchMany(TokenType type, string? lexeme = null)
{
return TokenStreamImple.MatchMany(_tokens, ref _index, type, lexeme);
}
public Token Expect(TokenType type, string? lexeme = null)
{
return TokenStreamImple.Expect(_tokens, ref _index, type, lexeme);
}
public TokenStreamSlice Slice(int length = -1)
{
if (length <= 0)
{
length = _tokens.Length - _index;
}
var slice = _tokens.AsSpan().Slice(_index, length);
_index += length;
return new TokenStreamSlice(slice);
}
public TokenStreamSlice SliceNextBlock()
{
var length = 0;
var lBraceCount = 0;
var rBraceCount = 0;
Token nextToken;
do
{
nextToken = Peek(length);
if (length > 0 && lBraceCount > 0 && lBraceCount == rBraceCount)
{
break;
}
if (nextToken.Match(TokenType.LBrace))
{
lBraceCount++;
}
else if (nextToken.Match(TokenType.RBrace))
{
rBraceCount++;
}
length++;
}
while (_index + length < _tokens.Length);
return Slice(length);
}
}
internal ref struct TokenStreamSlice
{
private readonly ReadOnlySpan<Token> _tokens;
private int _index;
public readonly int Length => _tokens.Length;
public readonly int Remaining => _tokens.Length - _index;
public readonly bool HasMore => _index < _tokens.Length;
public int Position
{
readonly get => _index;
set
{
if (value < 0 || value > _tokens.Length)
{
throw new ArgumentOutOfRangeException(nameof(value), "Position must be within the bounds of the token stream.");
}
_index = value;
}
}
internal TokenStreamSlice(ReadOnlySpan<Token> tokens)
{
_tokens = tokens;
_index = 0;
}
public Token Peek(int length = 0)
{
return TokenStreamImple.Peek(_tokens, ref _index, length);
}
public bool TryPeek(out Token token)
{
return TokenStreamImple.TryPeek(_tokens, ref _index, 0, out token);
}
public bool TryPeek(int length, out Token token)
{
return TokenStreamImple.TryPeek(_tokens, ref _index, length, out token);
}
public bool TryConsume(out Token token)
{
return TokenStreamImple.TryConsume(_tokens, ref _index, out token);
}
public Token Consume()
{
return TokenStreamImple.Consume(_tokens, ref _index);
}
public bool Match(TokenType type, string? lexeme = null)
{
return TokenStreamImple.Match(_tokens, ref _index, type, lexeme);
}
public int MatchMany(TokenType type, string? lexeme = null)
{
return TokenStreamImple.MatchMany(_tokens, ref _index, type, lexeme);
}
public Token Expect(TokenType type, string? lexeme = null)
{
return TokenStreamImple.Expect(_tokens, ref _index, type, lexeme);
}
public TokenStreamSlice Slice(int length = -1)
{
if (length <= 0)
{
length = _tokens.Length - _index;
}
var slice = _tokens.Slice(_index, length);
_index += length;
return new TokenStreamSlice(slice);
}
public TokenStreamSlice SliceNextBlock()
{
var length = 0;
var lBraceCount = 0;
var rBraceCount = 0;
do
{
var nextToken = Peek(length);
if (length > 0 && lBraceCount > 0 && lBraceCount == rBraceCount)
{
break;
}
if (nextToken.Match(TokenType.LBrace))
{
lBraceCount++;
}
else if (nextToken.Match(TokenType.RBrace))
{
rBraceCount++;
}
length++;
}
while (_index + length < _tokens.Length);
if (lBraceCount != rBraceCount)
{
throw new InvalidOperationException("Unmatched braces in token stream.");
}
return Slice(length);
}
}