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,58 @@
#ifndef COMMON_HLSL
#define COMMON_HLSL
#undef USE_TRADITIONAL_BINDLESS // Just for testing, this should be handled by engine feature level.
#if defined(USE_TRADITIONAL_BINDLESS)
#define GLOBAL_TEXTURE2D_HEAP_SIZE 32768
#define GLOBAL_TEXTURE3D_HEAP_SIZE 32
#define GLOBAL_TEXTURECUBE_HEAP_SIZE 32
#define GLOBAL_TEXTURE2D_ARRAY_HEAP_SIZE 256
#define GLOBAL_TEXTURECUBE_ARRAY_HEAP_SIZE 32
#define GLOBAL_SAMPLER_HEAP_SIZE 32
#define GLOBAL_TEXTURE2D_HEAP GlobalTexture2DHeap
#define GLOBAL_TEXTURE3D_HEAP GlobalTexture3DHeap
#define GLOBAL_TEXTURECUBE_HEAP GlobalTextureCubeHeap
#define GLOBAL_TEXTURE2D_ARRAY_HEAP GlobalTexture2DArrayHeap
#define GLOBAL_TEXTURECUBE_ARRAY_HEAP GlobalTextureCubeArrayHeap
#define GLOBAL_SAMPLER_HEAP GlobalSamplerHeap
#else
#define GLOBAL_TEXTURE2D_HEAP ResourceDescriptorHeap
#define GLOBAL_TEXTURE3D_HEAP ResourceDescriptorHeap
#define GLOBAL_TEXTURECUBE_HEAP ResourceDescriptorHeap
#define GLOBAL_TEXTURE2D_ARRAY_HEAP ResourceDescriptorHeap
#define GLOBAL_TEXTURECUBE_ARRAY_HEAP ResourceDescriptorHeap
#define GLOBAL_SAMPLER_HEAP SamplerDescriptorHeap
#endif
#define TEXTURE2D_BINDLESS uint
#define TEXTURE3D_BINDLESS uint
#define TEXTURECUBE_BINDLESS uint
#define TEXTURE2D_ARRAY_BINDLESS uint
#define TEXTURECUBE_ARRAY_BINDLESS uint
#define SAMPLER_BINDLESS uint
#define STRUCT_BUFFER_BINDLESS uint
#define BYTE_ADDRESS_BUFFER_BINDLESS uint
#define GET_TEXTURE2D_BINDLESS(id) GLOBAL_TEXTURE2D_HEAP[id]
#define GET_TEXTURE2D_ARRAY_BINDLESS(id) GLOBAL_TEXTURE2D_ARRAY_HEAP[id]
#define GET_TEXTURE3D_BINDLESS(id) GLOBAL_TEXTURE3D_HEAP[id]
#define GET_TEXTURECUBE_BINDLESS(id) GLOBAL_TEXTURECUBE_HEAP[id]
#define GET_TEXTURECUBE_ARRAY_BINDLESS(id) GLOBAL_TEXTURECUBE_ARRAY_HEAP[id]
#define GET_SAMPLER_BINDLESS(id) GLOBAL_SAMPLER_HEAP[id]
#define SAMPLE_TEXTURE2D(tex, samp, uv) tex.Sample(samp, uv)
#define SAMPLE_TEXTURE2D_LEVEL(tex, samp, uv, level) tex.SampleLevel(samp, uv, level)
#define SAMPLE_TEXTURE2D_BINDLESS(texId, sampId, uv) GET_TEXTURE2D_BINDLESS(texId).Sample(GET_BINDLESS_SAMPLER(sampId), uv)
#define SAMPLE_TEXTURE2D_LEVEL_BINDLESS(texId, sampId, uv, level) GET_TEXTURE2D_BINDLESS(texId).SampleLevel(GET_BINDLESS_SAMPLER(sampId), uv, level)
#define SAMPLE_TEXTURE2D_ARRAY(tex, samp, uv, index) tex.Sample(samp, uv, index)
#define SAMPLE_TEXTURE2D_ARRAY_BINDLESS(texId, sampId, uv, index) GET_TEXTURE2D_ARRAY_BINDLESS(texId).Sample(GET_BINDLESS_SAMPLER(sampId), uv, index)
#endif // COMMON_HLSL

View File

@@ -0,0 +1,50 @@
#ifndef PROPERTIES_HLSL
#define PROPERTIES_HLSL
#include "F:/csharp/GhostEngine/Ghost.Shader/BuiltIn/Common.hlsl"
struct PerViewData
{
float4x4 cameraMatrix;
float4 screenSize; // xy = size, zw = 1/size
};
struct PerObjectData
{
float4x4 localToWorld;
float3 worldBoundsMin;
BYTE_ADDRESS_BUFFER_BINDLESS vertexBuffer;
float3 worldBoundsMax;
BYTE_ADDRESS_BUFFER_BINDLESS indexBuffer;
};
cbuffer GlobalConstants : register(b0)
{
GlobalData g_GlobalData;
};
cbuffer PerViewConstants : register(b1)
{
PerViewData g_PerViewData;
};
cbuffer PerObjectConstants : register(b2)
{
PerObjectData g_PerObjectData;
};
cbuffer PerMaterialConstants : register(b3)
{
PerMaterialData g_PerMaterialData;
};
#if defined(USE_TRADITIONAL_BINDLESS)
Texture2D GlobalTexture2DHeap[GLOBAL_TEXTURE2D_HEAP_SIZE] : register(t0);
Texture3D GlobalTexture3DHeap[GLOBAL_TEXTURE3D_HEAP_SIZE] : register(t0);
TextureCube GlobalTextureCubeHeap[GLOBAL_TEXTURECUBE_HEAP_SIZE] : register(t0);
Texture2DArray GlobalTexture2DArrayHeap[GLOBAL_TEXTURE2D_ARRAY_HEAP_SIZE] : register(t0);
TextureCubeArray GlobalTextureCubeArrayHeap[GLOBAL_TEXTURECUBE_ARRAY_HEAP_SIZE] : register(t0);
SamplerState GlobalSamplerHeap[GLOBAL_SAMPLER_HEAP_SIZE] : register(s0);
#endif
#endif

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

@@ -1,4 +1,4 @@
namespace Ghost.Shader;
namespace Ghost.Shader.Compiler.Parser;
internal static class ParseUtility
{
@@ -26,6 +26,19 @@ internal static class ParseUtility
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;

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

@@ -1,4 +1,4 @@
namespace Ghost.Shader;
namespace Ghost.Shader.Compiler;
internal static class TokenStreamImple
{
@@ -36,7 +36,7 @@ internal static class TokenStreamImple
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 = null)
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))
@@ -44,11 +44,23 @@ internal static class TokenStreamImple
return false;
}
index++;
//index++;
return true;
}
public static Token Expect(ReadOnlySpan<Token> tokens, ref int index, TokenType type, string? lexeme = null)
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))
{
@@ -91,6 +103,11 @@ internal class TokenStream
_tokens = tokens;
}
public TokenStream(IEnumerable<Token> tokens)
{
_tokens = tokens.ToArray();
}
public Token Peek(int length = 0)
{
return TokenStreamImple.Peek(_tokens, ref _index, length);
@@ -121,6 +138,11 @@ internal class TokenStream
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);
@@ -230,6 +252,11 @@ internal ref struct TokenStreamSlice
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);

View File

@@ -0,0 +1,304 @@
using Misaki.HighPerformance.Mathematics;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ghost.Shader.Generator;
public enum PackingRules
{
Exact,
Aligned,
}
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum)]
public class GenerateHLSLAttribute : Attribute
{
private readonly PackingRules _packingRules;
private readonly string? _outputSource;
public GenerateHLSLAttribute(PackingRules packingRules, string? outputSource)
{
_packingRules = packingRules;
_outputSource = outputSource;
}
}
internal static partial class ShaderStructGenerator
{
private struct ShaderFieldInfo
{
public string name;
public Type fieldType;
public ShaderFieldInfo(string name, Type fieldType)
{
this.name = name;
this.fieldType = fieldType;
}
public ShaderFieldInfo(FieldInfo fieldInfo)
: this(fieldInfo.Name, fieldInfo.FieldType)
{
}
}
private const int _HLSL_VECTOR_REGISTER_SIZE = 16; // 16 bytes (128 bits) for float4
private static void GenerateEnumHLSL(Type type, StringBuilder sb)
{
if (!type.IsEnum)
{
throw new InvalidOperationException($"Type {type.FullName} is not an enum.");
}
var enumName = type.Name;
//var underlyingType = Enum.GetUnderlyingType(type);
//var underlyingTypeName = underlyingType switch
//{
// Type t when t == typeof(byte) || t == typeof(short) || t == typeof(int) => "int",
// Type t when t == typeof(sbyte) || t == typeof(ushort) || t == typeof(uint) => "uint",
// _ => throw new InvalidOperationException($"Unsupported underlying type {underlyingType.FullName} for enum {enumName}."),
//};
// sb.Append(@$"
//enum {enumName} : {underlyingTypeName}
//{{");
var names = Enum.GetNames(type);
var values = Enum.GetValuesAsUnderlyingType(type);
for (var i = 0; i < names.Length; i++)
{
var name = $"{CamelCaseToUnderscoreRegex().Replace(enumName, "_$1")}_{names[i]}";
var value = values.GetValue(i);
// sb.Append(@$"
//{name} = {value},");
sb.Append(@$"
#define {name.ToUpperInvariant()} {value}"); // Use #define for capability. Enum is only support for newer HLSL versions.
}
// sb.AppendLine(@"
//};");
sb.AppendLine();
}
public static int FindNextFieldThatFits(FieldInfo[] fields, bool[] looked, int startIndex, int size, out int foundIndex)
{
if (size <= 0)
{
foundIndex = -1;
return size;
}
var bestFitIndex = -1;
var bestFitSize = 0;
for (var j = startIndex; j < fields.Length; j++)
{
if (looked[j])
{
continue;
}
var nextField = fields[j];
var nextSize = Marshal.SizeOf(nextField.FieldType);
if (nextSize <= size)
{
if (nextSize == size)
{
foundIndex = j;
return nextSize;
}
if (nextSize > bestFitSize)
{
bestFitSize = nextSize;
bestFitIndex = j;
}
}
}
if (bestFitIndex != -1)
{
foundIndex = bestFitIndex;
return bestFitSize;
}
foundIndex = -1;
return size;
}
private static void GenerateStructHLSL(Type type, PackingRules packingRules, StringBuilder sb)
{
if (!type.IsValueType || type.IsPrimitive)
{
throw new InvalidOperationException($"Type {type.FullName} is not a struct.");
}
var structName = type.Name;
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(static f => f.FieldType.IsValueType).ToArray();
var shaderFields = new ShaderFieldInfo[fields.Length];
if (packingRules == PackingRules.Aligned)
{
var sortedFields = new List<ShaderFieldInfo>(fields.Length);
var looked = new bool[fields.Length];
var paddingIndex = 0;
// Sort the fields to align them to HLSL vector registers (16 bytes)
for (var i = 0; i < fields.Length; i++)
{
if (looked[i])
{
continue;
}
var field = fields[i];
var size = Marshal.SizeOf(field.FieldType);
sortedFields.Add(new ShaderFieldInfo(field));
var registerRemaining = _HLSL_VECTOR_REGISTER_SIZE - (size % _HLSL_VECTOR_REGISTER_SIZE);
while (true)
{
var nextSize = FindNextFieldThatFits(fields, looked, i + 1, registerRemaining, out var nextIndex);
if (nextSize == 0 || nextIndex == -1)
{
break;
}
looked[i] = true;
looked[nextIndex] = true;
sortedFields.Add(new ShaderFieldInfo(fields[nextIndex]));
registerRemaining -= nextSize;
}
if (registerRemaining != 0)
{
// Add padding if necessary
var count = registerRemaining / sizeof(float);
for (var p = 0; p < count; p++)
{
sortedFields.Add(new ShaderFieldInfo($"_padding{paddingIndex++}", typeof(float)));
}
}
}
shaderFields = sortedFields.ToArray();
}
else
{
for (var i = 0; i < fields.Length; i++)
{
shaderFields[i] = new ShaderFieldInfo(fields[i]);
}
}
sb.Append(@$"
struct {structName}
{{");
foreach (var field in shaderFields)
{
var fieldType = field.fieldType;
var fieldName = field.name;
string hlslType;
switch (fieldType)
{
case Type t when t == typeof(float):
hlslType = "float";
break;
case Type t when t == typeof(double):
hlslType = "double";
break;
case Type t when t == typeof(int):
hlslType = "int";
break;
case Type t when t == typeof(uint):
hlslType = "uint";
break;
case Type t when t == typeof(bool):
hlslType = "bool";
break;
case Type t when t == typeof(Vector2):
hlslType = "float2";
break;
case Type t when t == typeof(Vector3):
hlslType = "float3";
break;
case Type t when t == typeof(Vector4):
hlslType = "float4";
break;
case Type t when t == typeof(Matrix4x4):
hlslType = "float4x4";
break;
default:
{
if (fieldType.Namespace == typeof(float2).Namespace)
{
if (fieldType.Name.StartsWith("float")
|| fieldType.Name.StartsWith("double")
|| fieldType.Name.StartsWith("int")
|| fieldType.Name.StartsWith("uint")
|| fieldType.Name.StartsWith("bool"))
{
hlslType = fieldType.Name;
break;
}
}
throw new InvalidOperationException($"Unsupported field type: {fieldType.FullName} in struct {structName}.");
}
}
sb.Append(@$"
{hlslType} {fieldName};");
}
sb.AppendLine(@"
};");
}
public static void GenerateHLSL(ReadOnlySpan<Type> types, PackingRules packingRules, string outputSource)
{
if (!Directory.Exists(Path.GetDirectoryName(outputSource)))
{
throw new DirectoryNotFoundException($"The directory for the output source '{outputSource}' does not exist.");
}
var hlslDefine = $"{Path.GetFileNameWithoutExtension(outputSource).ToUpperInvariant().Replace('.', '_')}_HLSL";
var sb = new StringBuilder();
sb.AppendLine(@$"// Auto-generated HLSL code, please do not edit this file directly.
#ifndef {hlslDefine}
#define {hlslDefine}");
foreach (var type in types)
{
if (type.IsEnum)
{
GenerateEnumHLSL(type, sb);
}
else if (type.IsValueType && !type.IsPrimitive)
{
GenerateStructHLSL(type, packingRules, sb);
}
else
{
continue;
}
}
sb.Append(@"
#endif");
var hlslCode = sb.ToString();
File.WriteAllText(outputSource, hlslCode);
}
[GeneratedRegex("(?<=[a-z])([A-Z])")]
private static partial Regex CamelCaseToUnderscoreRegex();
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -16,4 +16,8 @@
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,148 +0,0 @@
namespace Ghost.Shader;
public class Lexer
{
private readonly string _source;
private int _pos = 0;
private int _line = 0;
private int _column = 0;
public Lexer(string source) => _source = source;
public IEnumerable<Token> Tokenize()
{
while (_pos < _source.Length)
{
var c = _source[_pos];
// Skip whitespace
if (char.IsWhiteSpace(c))
{
if (c == '\n')
{
_line++;
_column = 0;
}
else
{
_column++;
}
_pos++;
continue;
}
// Punctuation
switch (c)
{
case '=':
yield return MakeToken(TokenType.Equals, "=");
break;
case ';':
yield return MakeToken(TokenType.Semicolon, ";");
break;
case ',':
yield return MakeToken(TokenType.Comma, ",");
break;
case '{':
yield return MakeToken(TokenType.LBrace, "{");
break;
case '}':
yield return MakeToken(TokenType.RBrace, "}");
break;
case '(':
yield return MakeToken(TokenType.LParen, "(");
break;
case ')':
yield return MakeToken(TokenType.RParen, ")");
break;
case '"':
yield return ReadString();
break;
default:
if (char.IsLetter(c) || c == '_')
{
yield return ReadIdentifierOrKeyword();
}
else if (char.IsDigit(c) || c == '.')
{
yield return ReadNumber();
}
else
{
_pos++; // skip unknown
}
break;
}
}
yield return new Token(TokenType.EndOfFile, "", _line, _column);
}
private Token MakeToken(TokenType type, string lexeme)
{
var token = new Token(type, lexeme, _line, _column);
_pos++;
_column++;
return token;
}
private Token ReadString()
{
var startCol = _column;
_pos++;
_column++; // skip "
var start = _pos;
while (_pos < _source.Length && _source[_pos] != '"')
{
_pos++;
_column++;
}
var text = _source.Substring(start, _pos - start);
_pos++;
_column++; // skip closing "
return new Token(TokenType.StringLiteral, text, _line, startCol);
}
private Token ReadIdentifierOrKeyword()
{
var startCol = _column;
var start = _pos;
while (_pos < _source.Length && (char.IsLetterOrDigit(_source[_pos]) || _source[_pos] == '_'))
{
_pos++;
_column++;
}
var text = _source.Substring(start, _pos - start);
// Optional: detect keywords
if (TokenLexicon.IsKeyword(text))
{
return new Token(TokenType.Keyword, text, _line, startCol);
}
return new Token(TokenType.Identifier, text, _line, startCol);
}
private Token ReadNumber()
{
var startCol = _column;
var start = _pos;
while (_pos < _source.Length && (char.IsDigit(_source[_pos]) || _source[_pos] == '.'))
{
_pos++;
_column++;
}
var num = _source.Substring(start, _pos - start);
return new Token(TokenType.Number, num, _line, startCol);
}
}

View File

@@ -1,29 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal class DefinesBlock : IBlockParser<List<string>>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.DEFINES);
}
public static List<string> Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var defines = new List<string>();
var bodyStream = stream.Slice(stream.Remaining - 1);
while (bodyStream.HasMore)
{
var defineToken = bodyStream.Expect(TokenType.Identifier);
defines.Add(defineToken.lexeme);
bodyStream.Expect(TokenType.Semicolon);
}
stream.Expect(TokenType.RBrace);
return defines;
}
}

View File

@@ -1,13 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal interface IBlockParser<T>
{
public static abstract bool ShouldEnter(Token token);
public static abstract T Parse(TokenStreamSlice ts);
}
internal interface IBlockParser<T, U> : IBlockParser<T>
{
public U SemanticAnalysis(T syntax, List<ShaderError> errors);
}

View File

@@ -1,29 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal class IncludesBlock : IBlockParser<List<string>>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.INCLUDES);
}
public static List<string> Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var includes = new List<string>();
var bodyStream = stream.Slice(stream.Remaining - 1);
while (bodyStream.HasMore)
{
var includeToken = bodyStream.Expect(TokenType.StringLiteral);
includes.Add(includeToken.lexeme);
bodyStream.Expect(TokenType.Semicolon);
}
stream.Expect(TokenType.RBrace);
return includes;
}
}

View File

@@ -1,30 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal class KeywordsBlock : IBlockParser<List<FunctionCall>>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.KEYWORDS);
}
public static List<FunctionCall> Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var keywords = new List<FunctionCall>();
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 FunctionCall { name = keywordToken, arguments = args });
bodyStream.Expect(TokenType.Semicolon);
}
stream.Expect(TokenType.RBrace);
return keywords;
}
}

View File

@@ -1,63 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal class PassBlock : IBlockParser<ShaderPassSyntax>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.PASS);
}
public static ShaderPassSyntax Parse(TokenStreamSlice stream)
{
var pass = new ShaderPassSyntax();
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.overridePipeline = PipelineBlock.Parse(bodyStream.SliceNextBlock());
}
else if (nextToken.Match(TokenType.Identifier, "vs"))
{
bodyStream.Expect(TokenType.Identifier, "vs");
var vsArgs = ParseUtility.ParseFunctionArguments(ref bodyStream, TokenType.StringLiteral);
pass.vertexShader = vsArgs[0];
pass.vertexEntry = vsArgs[1];
bodyStream.Expect(TokenType.Semicolon);
}
else if (nextToken.Match(TokenType.Identifier, "ps"))
{
bodyStream.Expect(TokenType.Identifier, "ps");
var psArgs = ParseUtility.ParseFunctionArguments(ref bodyStream, TokenType.StringLiteral);
pass.pixelShader = psArgs[0];
pass.pixelEntry = psArgs[1];
bodyStream.Expect(TokenType.Semicolon);
}
else
{
throw new Exception($"Unexpected token '{nextToken.lexeme}' in pass body.");
}
}
stream.Expect(TokenType.RBrace);
return pass;
}
}

View File

@@ -1,217 +0,0 @@
using static Ghost.Shader.TokenLexicon;
namespace Ghost.Shader.ParserBlock;
internal class PipelineBlock : IBlockParser<PipelineStateSyntax, PipelineStateModel>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.PIPELINE);
}
public static PipelineStateSyntax Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var pipeline = new PipelineStateSyntax();
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);
switch (stateToken.lexeme)
{
case KnownPipelineProperties.ZTEST:
pipeline.zTest = valueToken;
break;
case KnownPipelineProperties.ZWRITE:
pipeline.zWrite = valueToken;
break;
case KnownPipelineProperties.CULL:
pipeline.cull = valueToken;
break;
case KnownPipelineProperties.BLEND:
pipeline.blend = valueToken;
break;
case KnownPipelineProperties.COLORMASK:
pipeline.colorMask = valueToken;
break;
default:
throw new InvalidDataException($"Unknown pipeline state: {stateToken.lexeme}");
}
bodyStream.Expect(TokenType.Semicolon);
}
stream.Expect(TokenType.RBrace);
return pipeline;
}
public PipelineStateModel SemanticAnalysis(PipelineStateSyntax syntax, List<ShaderError> errors)
{
var model = new PipelineStateModel();
// ZTest
if (!syntax.zTest.Match(TokenType.None))
{
switch (syntax.zTest.lexeme)
{
case "disable":
model.zTest = ZTestOptions.Disabled;
break;
case "less":
model.zTest = ZTestOptions.Less;
break;
case "less_equal":
model.zTest = ZTestOptions.LessEqual;
break;
case "equal":
model.zTest = ZTestOptions.Equal;
break;
case "greater_equal":
model.zTest = ZTestOptions.GreaterEqual;
break;
case "greater":
model.zTest = ZTestOptions.Greater;
break;
case "not_equal":
model.zTest = ZTestOptions.NotEqual;
break;
case "always":
model.zTest = ZTestOptions.Always;
break;
default:
errors.Add(new ShaderError
{
message = $"Invalid ZTest option: {syntax.zTest.lexeme}",
line = syntax.zTest.line,
column = syntax.zTest.column
});
break;
}
}
else
{
model.zTest = ZTestOptions.LessEqual;
}
// ZWrite
if (!syntax.zWrite.Match(TokenType.None))
{
switch (syntax.zWrite.lexeme)
{
case "on":
model.zWrite = ZWriteOptions.On;
break;
case "off":
model.zWrite = ZWriteOptions.Off;
break;
default:
errors.Add(new ShaderError
{
message = $"Invalid ZWrite option: {syntax.zWrite.lexeme}",
line = syntax.zWrite.line,
column = syntax.zWrite.column
});
break;
}
}
else
{
model.zWrite = ZWriteOptions.On;
}
// Cull
if (!syntax.cull.Match(TokenType.None))
{
switch (syntax.cull.lexeme)
{
case "off":
model.cull = CullOptions.Off;
break;
case "front":
model.cull = CullOptions.Front;
break;
case "back":
model.cull = CullOptions.Back;
break;
default:
errors.Add(new ShaderError
{
message = $"Invalid Cull option: {syntax.cull.lexeme}",
line = syntax.cull.line,
column = syntax.cull.column
});
break;
}
}
else
{
model.cull = CullOptions.Back;
}
// Blend
if (!syntax.blend.Match(TokenType.None))
{
switch (syntax.blend.lexeme)
{
case "opaque":
model.blend = BlendOptions.Opaque;
break;
case "alpha":
model.blend = BlendOptions.Alpha;
break;
case "additive":
model.blend = BlendOptions.Additive;
break;
case "multiply":
model.blend = BlendOptions.Multiply;
break;
case "premultiplied":
model.blend = BlendOptions.PremultipliedAlpha;
break;
default:
errors.Add(new ShaderError
{
message = $"Invalid Blend option: {syntax.blend.lexeme}",
line = syntax.blend.line,
column = syntax.blend.column
});
break;
}
}
else
{
model.blend = BlendOptions.Opaque;
}
// Color Mask
if (!syntax.colorMask.Match(TokenType.None))
{
if (uint.TryParse(syntax.colorMask.lexeme, out var colorMask))
{
model.colorMask = colorMask;
}
else
{
errors.Add(new ShaderError
{
message = $"Invalid Color Mask value: {syntax.colorMask.lexeme}",
line = syntax.colorMask.line,
column = syntax.colorMask.column
});
}
}
else
{
model.colorMask = 0xF; // Default to RGBA
}
return model;
}
}

View File

@@ -1,337 +0,0 @@
using Misaki.HighPerformance.Mathematics;
using System.Globalization;
namespace Ghost.Shader.ParserBlock;
internal class PropertiesBlock : IBlockParser<List<PropertySyntax>, List<PropertyModel>>
{
private sealed record PropTypeInfo(int ArgCount, TokenType ArgTokenType, Func<List<Token>, object?>? Builder);
private static readonly Dictionary<ShaderPropertyType, PropTypeInfo> s_propTypeInfo = new()
{
// Floats
[ShaderPropertyType.Float] = new(1, TokenType.Number, a => float.Parse(a[0].lexeme, CultureInfo.InvariantCulture)),
[ShaderPropertyType.Float2] = new(2, TokenType.Number, a => new float2(
float.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[1].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.Float3] = new(3, TokenType.Number, a => new float3(
float.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[2].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.Float4] = new(4, TokenType.Number, a => new float4(
float.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[2].lexeme, CultureInfo.InvariantCulture),
float.Parse(a[3].lexeme, CultureInfo.InvariantCulture))),
// Ints
[ShaderPropertyType.Int] = new(1, TokenType.Number, a => int.Parse(a[0].lexeme, CultureInfo.InvariantCulture)),
[ShaderPropertyType.Int2] = new(2, TokenType.Number, a => new int2(
int.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[1].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.Int3] = new(3, TokenType.Number, a => new int3(
int.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[2].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.Int4] = new(4, TokenType.Number, a => new int4(
int.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[2].lexeme, CultureInfo.InvariantCulture),
int.Parse(a[3].lexeme, CultureInfo.InvariantCulture))),
// UInts
[ShaderPropertyType.UInt] = new(1, TokenType.Number, a => uint.Parse(a[0].lexeme, CultureInfo.InvariantCulture)),
[ShaderPropertyType.UInt2] = new(2, TokenType.Number, a => new uint2(
uint.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[1].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.UInt3] = new(3, TokenType.Number, a => new uint3(
uint.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[2].lexeme, CultureInfo.InvariantCulture))),
[ShaderPropertyType.UInt4] = new(4, TokenType.Number, a => new uint4(
uint.Parse(a[0].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[1].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[2].lexeme, CultureInfo.InvariantCulture),
uint.Parse(a[3].lexeme, CultureInfo.InvariantCulture))),
// Bools (numbers 0/1)
[ShaderPropertyType.Bool] = new(1, TokenType.Number, a => a[0].lexeme != "0"),
[ShaderPropertyType.Bool2] = new(2, TokenType.Number, a => new bool2(a[0].lexeme != "0", a[1].lexeme != "0")),
[ShaderPropertyType.Bool3] = new(3, TokenType.Number, a => new bool3(a[0].lexeme != "0", a[1].lexeme != "0", a[2].lexeme != "0")),
[ShaderPropertyType.Bool4] = new(4, TokenType.Number, a => new bool4(a[0].lexeme != "0", a[1].lexeme != "0", a[2].lexeme != "0", a[3].lexeme != "0")),
// Textures (single identifier argument currently no default object built)
[ShaderPropertyType.Texture2D] = new(1, TokenType.Identifier, null),
[ShaderPropertyType.Texture3D] = new(1, TokenType.Identifier, null),
[ShaderPropertyType.TextureCube] = new(1, TokenType.Identifier, null),
};
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 List<PropertySyntax> Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var properties = new List<PropertySyntax>();
var bodyStream = stream.Slice(stream.Remaining - 1);
while (bodyStream.HasMore)
{
var shaderProperty = new PropertySyntax();
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 FunctionCall
{
name = constructorTypeToken,
arguments = args
};
bodyStream.Expect(TokenType.Semicolon);
break;
}
case TokenType.Semicolon:
break;
default:
throw new Exception($"Unexpected token '{nextToken.lexeme}' in property declaration.");
}
properties.Add(shaderProperty);
}
stream.Expect(TokenType.RBrace);
return properties;
}
public List<PropertyModel> SemanticAnalysis(List<PropertySyntax> syntax, List<ShaderError> errors)
{
var models = new List<PropertyModel>();
var usedPropertyNames = new HashSet<string>();
foreach (var property in syntax)
{
var model = new PropertyModel();
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, PropertySyntax property, PropertyModel 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, PropertySyntax property, PropertyModel 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, PropertySyntax property, PropertyModel model)
{
var constructor = property.propertyConstructor;
if (constructor == null)
{
errors.Add(new ShaderError
{
message = "Shader property constructor is null.",
line = property.name.line,
column = property.name.column
});
return false;
}
if (string.IsNullOrWhiteSpace(constructor.name.lexeme))
{
errors.Add(new ShaderError
{
message = "Shader property constructor has an empty name.",
line = constructor.name.line,
column = constructor.name.column
});
return false;
}
if (constructor.name.lexeme != property.type.lexeme)
{
errors.Add(new ShaderError
{
message = $"Shader property constructor name '{constructor.name.lexeme}' does not match property type '{property.type.lexeme}'.",
line = constructor.name.line,
column = constructor.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 = constructor.name.line,
column = constructor.name.column
});
return false;
}
// Count check
if (constructor.arguments.Count != info.ArgCount)
{
errors.Add(new ShaderError
{
message = $"Shader property constructor for type '{property.type.lexeme}' expects {info.ArgCount} argument(s), but got {constructor.arguments.Count}.",
line = constructor.name.line,
column = constructor.name.column
});
return false;
}
// Type check (uniform requirement for all args)
var hasError = false;
for (var i = 0; i < constructor.arguments.Count; i++)
{
var arg = constructor.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(constructor.arguments);
}
catch (Exception ex)
{
errors.Add(new ShaderError
{
message = $"Failed to construct default value for property '{property.name.lexeme}': {ex.Message}",
line = constructor.name.line,
column = constructor.name.column
});
return false;
}
}
return true;
}
}

View File

@@ -1,43 +0,0 @@
namespace Ghost.Shader.ParserBlock;
internal class ShaderBlock : IBlockParser<ShaderSyntax>
{
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.Add(PassBlock.Parse(bodyStream.SliceNextBlock()));
}
else
{
throw new Exception($"Unexpected token '{nextToken.lexeme}' in shader body.");
}
}
stream.Expect(TokenType.RBrace);
return shader;
}
}

View File

@@ -1,35 +0,0 @@
namespace Ghost.Shader;
public enum ZTestOptions
{
Disabled,
Less,
LessEqual,
Equal,
GreaterEqual,
Greater,
NotEqual,
Always
}
public enum ZWriteOptions
{
Off,
On
}
public enum CullOptions
{
Off,
Front,
Back
}
public enum BlendOptions
{
Opaque,
Alpha,
Additive,
Multiply,
PremultipliedAlpha
}

View File

@@ -1,58 +0,0 @@
using Ghost.Shader.ParserBlock;
namespace Ghost.Shader;
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
{
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 ShaderModel SemanticAnalysis(ShaderSyntax syntax, out List<ShaderError> errors)
{
var shaderModel = new ShaderModel();
errors = new List<ShaderError>();
shaderModel.name = syntax.name.lexeme;
var propertiesBlock = new PropertiesBlock();
shaderModel.properties = propertiesBlock.SemanticAnalysis(syntax.properties, errors);
var pipelineBlock = new PipelineBlock();
shaderModel.pipeline = pipelineBlock.SemanticAnalysis(syntax.pipeline, errors);
return shaderModel;
}
}

View File

@@ -1,34 +0,0 @@
namespace Ghost.Shader;
public enum ShaderPropertyType
{
None,
Float, Float2, Float3, Float4,
Int, Int2, Int3, Int4,
UInt, UInt2, UInt3, UInt4,
Bool, Bool2, Bool3, Bool4,
Texture2D, Texture3D, TextureCube,
}
internal class PropertyModel
{
public ShaderPropertyType type;
public string name = string.Empty;
public object? defaultValue;
}
internal class PipelineStateModel
{
public ZTestOptions zTest;
public ZWriteOptions zWrite;
public CullOptions cull;
public BlendOptions blend;
public uint colorMask;
}
internal class ShaderModel
{
public string name = string.Empty;
public List<PropertyModel> properties = new();
public PipelineStateModel pipeline = new();
}

View File

@@ -1,44 +0,0 @@
namespace Ghost.Shader;
public class FunctionCall
{
public Token name;
public List<Token> arguments = new();
}
public class PropertySyntax
{
public Token type;
public Token name;
public FunctionCall? propertyConstructor;
}
public class PipelineStateSyntax
{
public Token zTest;
public Token zWrite;
public Token cull;
public Token blend;
public Token colorMask;
}
public class ShaderPassSyntax
{
public Token name;
public Token vertexShader;
public Token vertexEntry;
public Token pixelShader;
public Token pixelEntry;
public List<string>? defines;
public List<string>? includes;
public List<FunctionCall>? keywords;
public PipelineStateSyntax? overridePipeline;
}
public class ShaderSyntax
{
public Token name;
public List<PropertySyntax> properties = new();
public PipelineStateSyntax pipeline = new();
public List<ShaderPassSyntax> passes = new();
}

View File

@@ -1,138 +0,0 @@
namespace Ghost.Shader;
[Flags]
public enum TokenType
{
None = 0,
Identifier = 1 << 0, // variable names, types, etc.
Keyword = 1 << 1, // shader, properties, pipeline, pass, etc.
Number = 1 << 2, // numeric literals
StringLiteral = 1 << 3, // "ForwardVS.hlsl"
Equals = 1 << 4, // =
Semicolon = 1 << 5, // ;
Comma = 1 << 6, // ,
LBrace = 1 << 7, // {
RBrace = 1 << 8, // }
LParen = 1 << 9, // (
RParen = 1 << 10, // )
EndOfFile = 1 << 11 // EOF
}
public readonly struct Token
{
public readonly TokenType type;
public readonly string lexeme;
public readonly int line;
public readonly int column;
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 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}').");
}
}
}
public 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 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";
}
private static readonly HashSet<string> s_keywords = new()
{
KnownKeywords.SHADER,
KnownKeywords.PROPERTIES,
KnownKeywords.PIPELINE,
KnownKeywords.PASS,
KnownKeywords.DEFINES,
KnownKeywords.KEYWORDS,
KnownKeywords.INCLUDES,
};
private static readonly HashSet<string> s_function = new()
{
"vs",
"ps",
"ms",
"cs",
"dynamic",
"static",
};
private static readonly HashSet<string> s_types = new()
{
"float",
"float2",
"float3",
"float4",
"int",
"int2",
"int3",
"int4",
"uint",
"uint2",
"uint3",
"uint4",
"bool",
"bool2",
"bool3",
"bool4",
"texture2d",
"texture3d",
"texturecube",
};
public static bool IsKeyword(string lexeme) => s_keywords.Contains(lexeme);
public static bool IsFunction(string lexeme) => s_function.Contains(lexeme);
public static bool IsType(string lexeme) => s_types.Contains(lexeme);
}