diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8abdc53 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,297 @@ +# GhostEngine - Agent Development Guide + +This guide provides essential information for AI coding agents working on the GhostEngine codebase. + +## Project Overview + +- **Type**: Game Engine +- **Language**: C# +- **Target Framework**: .NET 10.0 +- **Special Features**: ECS architecture, D3D12 rendering, AOT compilation, WinUI 3 editor +- **Platform**: Windows (net10.0-windows10.0.22621.0 for editor projects) + +## Build Commands + +### Build Entire Solution +```bash +dotnet build GhostEngine.slnx +``` + +### Build Specific Project +```bash +dotnet build Ghost.Entities/Ghost.Entities.csproj +dotnet build Ghost.Editor/Ghost.Editor.csproj +``` + +### Build with Configuration +```bash +dotnet build GhostEngine.slnx -c Release +dotnet build GhostEngine.slnx -c Debug +``` + +### Clean Build +```bash +dotnet clean GhostEngine.slnx +dotnet build GhostEngine.slnx +``` + +## Test Commands + +### Run All Tests (Custom Framework) +Tests use a custom test framework (not xUnit/NUnit/MSTest). Each test project is an executable. + +```bash +# Run entity tests +dotnet run --project Ghost.Entities.Test/Ghost.Entities.Test.csproj + +# Run shader tests +dotnet run --project Ghost.Shader.Test/Ghost.Shader.Test.csproj +``` + +### Run Single Test +Tests implement `ITest` interface. To run a specific test, modify the test project's `Program.cs`: + +```csharp +// In Ghost.Entities.Test/Program.cs +TestRunner.Run(); // Run specific test +TestRunner.Run(10); // Run with 10 iterations +``` + +### Visual Tests (Graphics) +Graphics tests use WinUI 3 and require running as packaged apps: + +```bash +dotnet run --project Ghost.Graphics.Test/Ghost.Graphics.Test.csproj +``` + +## Code Style Guidelines + +### Formatting (from .editorconfig) + +- **Braces**: Allman style - all opening braces on new lines +- **Line Length**: Max 400 characters (very permissive) +- **Single-line statements**: Preserved (allowed) +- **Single-line blocks**: Preserved (allowed) + +```csharp +// Correct brace style +public void Method() +{ + if (condition) + { + DoSomething(); + } +} +``` + +### Imports + +- **System directives**: NOT sorted first (dotnet_sort_system_directives_first = false) +- **No grouping**: Import directives not separated by blank lines +- **Order**: Organize by project convention, not alphabetically + +```csharp +using Ghost.Core; +using Ghost.Entities; +using Misaki.HighPerformance.Collections; +using System.Diagnostics; +using TerraFX.Interop.DirectX; +``` + +### Types and Nullability + +- **Nullable**: Enabled for all projects +- **Implicit usings**: Enabled +- **Unsafe code**: Allowed in most projects (AllowUnsafeBlocks = True) +- **Primary constructors**: NOT preferred (csharp_style_prefer_primary_constructors = false) + +### Naming Conventions + +- **Classes/Interfaces**: PascalCase (`EntityManager`, `ICommandBuffer`) +- **Methods**: PascalCase (`CreateEntity`, `GetComponent`) +- **Properties**: PascalCase (`IsSuccess`, `Value`) +- **Fields (private)**: Camel case with underscore prefix (`_entityLocations`, `_world`) +- **Fields (public/internal)**: Camel case, no prefix for struct fields (`archetypeID`, `chunkIndex`) +- **Type parameters**: Single letter or PascalCase (`T`, `TComponent`) +- **Constants**: PascalCase (no SCREAMING_SNAKE_CASE) + +```csharp +public class EntityManager +{ + private readonly World _world; // Private field + private UnsafeSlotMap _entityLocations; + + public World World => _world; // Property + + public Entity CreateEntity() { } // Method +} + +internal struct EntityLocation // Struct +{ + public int archetypeID; // Public struct field + public int chunkIndex; +} +``` + +### Error Handling + +**Use Result Types** - Railway-oriented programming pattern: + +```csharp +// Custom result types defined in Ghost.Core +public ErrorStatus DoOperation() +{ + return ErrorStatus.None; // or ErrorStatus.NotFound, etc. +} + +public Result GetValue() +{ + if (success) + return Result.Success(value); + else + return Result.Failure("Error message"); +} + +public Result GetValueWithStatus() +{ + if (success) + return value; // Implicit conversion + else + return ErrorStatus.NotFound; // Implicit conversion +} + +// Extension methods for checking results +result.ThrowIfFailed(); +var value = result.GetValueOrThrow(); +var value = result.GetValueOrDefault(defaultValue); +if (result.TryGetValue(out var value)) { } +``` + +**Error Status Values**: None, NotFound, InvalidArgument, InvalidState, InternalError, PermissionDenied, NotSupported, OutOfMemory, Timeout, Cancelled, UnknownError + +### Memory and Performance + +- **Use unsafe code** when needed for performance-critical paths +- **Span and stackalloc**: Prefer for temporary allocations +- **ref returns**: Use for zero-copy access to internal data +- **Allocator patterns**: Use `Allocator.Persistent` for long-lived allocations +- **AllocationManager**: Create stack scopes for temporary allocations + +```csharp +// Stack allocation pattern +var entities = (Span)stackalloc Entity[1]; + +// Using allocation scope +using var scope = AllocationManager.CreateStackScope(); +var batchDestroy = new UnsafeList(entities.Length, scope.AllocationHandle); + +// Ref returns for zero-copy access +public ref T GetSingleton() where T : unmanaged, IComponent +{ + var ptr = GetSingleton(ComponentTypeID.Value); + return ref *(T*)ptr; +} +``` + +### Type Safety Patterns + +**Strongly-typed identifiers**: +```csharp +Identifier componentID; +Identifier archetypeID; +Handle resourceHandle; +``` + +**Generic constraints**: +```csharp +public void Method() where T : unmanaged, IComponent +public void Method() where E : struct, Enum +``` + +### Documentation + +- **XML comments**: Required for public APIs +- **Summary tags**: Describe what, not how +- **Remarks**: Add for complex behavior, thread-safety warnings, structural changes + +```csharp +/// +/// Create an entity with specified components. +/// +/// A set of component space IDs to add to the entities. +/// The created entity. +/// +/// This method causes structural changes and is not thread-safe. +/// Use to defer changes. +/// +public Entity CreateEntity(ComponentSet set) { } +``` + +### Common Patterns + +**ECS Component Registration**: +```csharp +// Type-safe component ID +ComponentTypeID.Value + +// Component sets for archetypes +var set = new ComponentSet(ComponentTypeID.Value, ComponentTypeID.Value); +``` + +**Disposal Pattern**: +```csharp +private bool _disposed; + +~MyClass() +{ + Dispose(); +} + +public void Dispose() +{ + if (_disposed) return; + + // Cleanup code + _disposed = true; + GC.SuppressFinalize(this); +} +``` + +**Debug-only validation**: +```csharp +#if DEBUG || GHOST_EDITOR + if (!_isSuccess) + { + throw new InvalidOperationException($"Error: {_message}"); + } +#endif +``` + +## Architecture Notes + +### Entity Component System (ECS) +- Archetype-based storage (similar to Unity DOTS) +- Component data stored in chunks +- Queries use bitset signatures for fast matching +- Structural changes move entities between archetypes + +### Graphics (D3D12) +- Hardware abstraction via `ICommandBuffer` +- Resource lifetime managed via handles +- Pipeline state objects (PSO) cached in library +- Native interop via TerraFX.Interop + +### Custom Dependencies +- `Misaki.HighPerformance.*`: High-performance collections and utilities +- `TerraFX.Interop.*`: Native Windows/DirectX interop +- Custom source generators in `Ghost.Generator` + +## Important Rules + +1. **Never disable nullable warnings** - fix the root cause +2. **Use Result types** instead of throwing exceptions for expected failures +3. **Document thread-safety** in XML comments for public APIs +4. **AllowUnsafeBlocks** is enabled - use unsafe code when it improves performance +5. **Avoid collection expressions/initializers** (disabled in .editorconfig) +6. **Prefer explicit over implicit** - clarity over brevity +7. **Test changes** by running the appropriate test project executable diff --git a/Ghost.Core/Graphics/ShaderDescriptor.cs b/Ghost.Core/Graphics/ShaderDescriptor.cs index 3d16305..d24b414 100644 --- a/Ghost.Core/Graphics/ShaderDescriptor.cs +++ b/Ghost.Core/Graphics/ShaderDescriptor.cs @@ -33,19 +33,6 @@ public struct KeywordsGroup public List keywords; } -public interface IPassDescriptor -{ - public string Identifier - { - get; - } - - public string Name - { - get; - } -} - public struct PropertyDescriptor { public ShaderPropertyType type; @@ -53,30 +40,27 @@ public struct PropertyDescriptor public object? defaultValue; } -public class PassDescriptor : IPassDescriptor +public struct PassDescriptor { - public string uniqueIdentifier = string.Empty; - public string name = string.Empty; + public string identifier; + public string name; public ShaderEntryPoint taskShader; public ShaderEntryPoint meshShader; public ShaderEntryPoint pixelShader; - public List? defines; - public List? includes; - public List? keywords; + public string[] defines; + public string[] includes; + public KeywordsGroup[] keywords; public PipelineState localPipeline; - - public string Identifier => uniqueIdentifier; - public string Name => name; } public class ShaderDescriptor { public string name = string.Empty; public uint cbufferSize; - public List globalProperties = new(); - public List properties = new(); - public List passes = new(); + public PropertyDescriptor[] globalProperties = null!; + public PropertyDescriptor[] properties = null!; + public PassDescriptor[] passes = null!; } public static class ShaderDescriptorExtensions diff --git a/Ghost.DSL/Ghost.DSL.csproj b/Ghost.DSL/Ghost.DSL.csproj index 6532bee..755478c 100644 --- a/Ghost.DSL/Ghost.DSL.csproj +++ b/Ghost.DSL/Ghost.DSL.csproj @@ -6,6 +6,14 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index f7ba852..edba659 100644 --- a/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -145,8 +145,13 @@ internal static class DSLShaderCompiler }; } - private static uint CalculateCBufferSize(List properties) + private static uint CalculateCBufferSize(ReadOnlySpan properties) { + if (properties.IsEmpty) + { + return 0; + } + var currentOffset = 0u; foreach (var prop in properties) @@ -180,7 +185,7 @@ internal static class DSLShaderCompiler name = p.name, type = p.type, defaultValue = p.defaultValue - }).ToList(); + }).ToArray(); var shaderLocalProperties = semantics.properties? .Where(p => p.scope == PropertyScope.Local) @@ -189,41 +194,37 @@ internal static class DSLShaderCompiler name = p.name, type = p.type, defaultValue = p.defaultValue - }).ToList(); + }).ToArray(); - if (shaderGlobalProperties != null) - { - descriptor.globalProperties ??= new List(); - descriptor.globalProperties.AddRange(shaderGlobalProperties); - } - - if (shaderLocalProperties != null) - { - descriptor.properties ??= new List(); - descriptor.properties.AddRange(shaderLocalProperties); - descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties); - } + descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty(); + descriptor.properties = shaderLocalProperties ?? Array.Empty(); + descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties); if (semantics.passes != null) { - foreach (var pass in semantics.passes) + descriptor.passes = new PassDescriptor[semantics.passes.Count]; + for (int i = 0; i < semantics.passes.Count; i++) { + var pass = semantics.passes[i]; var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default); - var fullPass = new PassDescriptor + descriptor.passes[i] = new PassDescriptor { - uniqueIdentifier = GetPassUniqueId(semantics, pass), + identifier = GetPassUniqueId(semantics, pass), name = pass.name, taskShader = pass.taskShader, meshShader = pass.meshShader, pixelShader = pass.pixelShader, localPipeline = localPipeline, - defines = pass.defines, - keywords = pass.keywords, + defines = pass.defines?.ToArray() ?? Array.Empty(), + includes = pass.includes?.ToArray() ?? Array.Empty(), + keywords = pass.keywords?.ToArray() ?? Array.Empty() }; - - descriptor.passes.Add(fullPass); } } + else + { + descriptor.passes = Array.Empty(); + } return descriptor; } @@ -237,6 +238,11 @@ internal static class DSLShaderCompiler var lexer = new Lexer(source); var stream = new TokenStream(lexer.Tokenize()); var shaderInfo = ParseShaders(stream); + if (shaderInfo.Count == 0) + { + return Result.Failure("No shader found in the provided file."); + } + var model = SemanticAnalysis(shaderInfo[0], out var errors); if (errors.Count != 0 || model == null) @@ -263,14 +269,21 @@ internal static class DSLShaderCompiler return Result.Failure("Failed to generate pass files: " + generatedResult.Message); } - foreach (var pass in desc.passes) + foreach (ref var pass in desc.passes.AsSpan()) { - if (pass is PassDescriptor fullPass) + if (pass.includes == null) { - fullPass.includes ??= new List(); - fullPass.includes.Add(globalPropResult.Value); - fullPass.includes.Add(generatedResult.Value); + pass.includes = new string[2]; } + else + { + Array.Resize(ref pass.includes, pass.includes.Length + 2); + // Shift existing includes to make room for the two new includes at the front. + pass.includes.AsSpan(0, pass.includes.Length - 2).CopyTo(pass.includes.AsSpan(2)); + } + + pass.includes[0] = globalPropResult.Value; + pass.includes[1] = generatedResult.Value; } return desc; @@ -360,7 +373,7 @@ struct PerMaterialData return outputFilePath; } - public static Result GenerateGlobalProperties(List globalProperties, string targetDirectory) + public static Result GenerateGlobalProperties(ReadOnlySpan globalProperties, string targetDirectory) { if (!Directory.Exists(targetDirectory)) { diff --git a/Ghost.DSL/ShaderCompiler/DSLShaderSemantics.cs b/Ghost.DSL/ShaderCompiler/DSLShaderSemantics.cs index b8235ec..6fd4179 100644 --- a/Ghost.DSL/ShaderCompiler/DSLShaderSemantics.cs +++ b/Ghost.DSL/ShaderCompiler/DSLShaderSemantics.cs @@ -32,6 +32,7 @@ internal class PassSemantic public ShaderEntryPoint meshShader; public ShaderEntryPoint pixelShader; public List? defines; + public List? includes; public List? keywords; public PipelineSemantic? localPipeline; } diff --git a/Ghost.DSL/ShaderCompiler/DSLShaderSyntax.cs b/Ghost.DSL/ShaderCompiler/DSLShaderSyntax.cs index 439027c..6a2cee6 100644 --- a/Ghost.DSL/ShaderCompiler/DSLShaderSyntax.cs +++ b/Ghost.DSL/ShaderCompiler/DSLShaderSyntax.cs @@ -11,7 +11,7 @@ internal struct PropertyDeclaration public Token scope; public Token type; public Token name; - public FunctionCallDeclaration? propertyConstructor; + public List? propertyInitializer; } internal struct ValueDeclaration @@ -44,7 +44,7 @@ internal class PassSyntax public HlslDeclaration? hlsl; public List? defines; public List? includes; - public List? keywords; + public List>? keywords; public List? functionCalls; } diff --git a/Ghost.DSL/ShaderCompiler/Parser/KeywordsBlock.cs b/Ghost.DSL/ShaderCompiler/Parser/KeywordsBlock.cs index e7a7b2e..d7d1e75 100644 --- a/Ghost.DSL/ShaderCompiler/Parser/KeywordsBlock.cs +++ b/Ghost.DSL/ShaderCompiler/Parser/KeywordsBlock.cs @@ -2,26 +2,42 @@ using Ghost.Core.Graphics; namespace Ghost.DSL.ShaderCompiler.Parser; -internal class KeywordsBlock : IBlockParser, List> +internal class KeywordsBlock : IBlockParser>, List> { public static bool ShouldEnter(Token token) { return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.KEYWORDS); } - public static List Parse(TokenStreamSlice stream) + public static List> Parse(TokenStreamSlice stream) { stream.Expect(TokenType.Keyword); stream.Expect(TokenType.LBrace); - var keywords = new List(); + var keywords = new List>(); 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 }); + var keys = new List(); + while (!bodyStream.Match(TokenType.Semicolon)) + { + var expectType = TokenType.Identifier; + if (keys.Count == 0) + { + expectType |= TokenType.Keyword; + } + + var argument = bodyStream.Expect(expectType); + keys.Add(argument); + + if (bodyStream.Match(TokenType.Comma)) + { + bodyStream.Consume(); + } + } + + keywords.Add(keys); bodyStream.Expect(TokenType.Semicolon); } @@ -30,7 +46,7 @@ internal class KeywordsBlock : IBlockParser, List< return keywords; } - public static List? SemanticAnalysis(List? syntax, List errors) + public static List? SemanticAnalysis(List>? syntax, List errors) { if (syntax == null) { @@ -38,42 +54,42 @@ internal class KeywordsBlock : IBlockParser, List< } var keywords = new List(syntax.Count); - foreach (var keyword in syntax) + foreach (var keys in syntax) { - if (keyword.arguments == null || keyword.arguments.Count == 0) + if (keys.Count == 0) { - errors.Add(new DSLShaderError - { - 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) + group.space = keys[0].lexeme switch { - case TokenLexicon.KnownFunctions.LOCAL: - group.space = KeywordSpace.Local; - break; - case TokenLexicon.KnownFunctions.GLOBAL: - group.space = KeywordSpace.Global; - break; - default: + TokenLexicon.KnownFunctions.LOCAL => KeywordSpace.Local, + TokenLexicon.KnownFunctions.GLOBAL => KeywordSpace.Global, + _ => KeywordSpace.Local + }; + + for (var i = 0; i < keys.Count; i++) + { + var token = keys[i]; + if (i == 0 && token.type == TokenType.Keyword) + { + continue; + } + + if (token.type != TokenType.Identifier) + { errors.Add(new DSLShaderError { - message = $"Unknown function name '{keyword.name.lexeme}'.", - line = keyword.name.line, - column = keyword.name.column + message = $"Invalid keyword '{token.lexeme}' in keywords block.", + line = token.line, + column = token.column }); continue; - } + } - foreach (var arg in keyword.arguments) - { - group.keywords ??= new List(keyword.arguments.Count); - group.keywords.Add(arg.lexeme); + group.keywords ??= new List(keys.Count); + group.keywords.Add(token.lexeme); } keywords.Add(group); diff --git a/Ghost.DSL/ShaderCompiler/Parser/PassBlock.cs b/Ghost.DSL/ShaderCompiler/Parser/PassBlock.cs index a7c9481..0e1f1c4 100644 --- a/Ghost.DSL/ShaderCompiler/Parser/PassBlock.cs +++ b/Ghost.DSL/ShaderCompiler/Parser/PassBlock.cs @@ -68,6 +68,7 @@ internal class PassBlock : IBlockParser { name = syntax.name.lexeme, defines = DefinesBlock.SemanticAnalysis(syntax.defines, errors), + includes = IncludesBlock.SemanticAnalysis(syntax.includes, errors), keywords = KeywordsBlock.SemanticAnalysis(syntax.keywords, errors), localPipeline = PipelineBlock.SemanticAnalysis(syntax.localPipeline, errors), }; diff --git a/Ghost.DSL/ShaderCompiler/Parser/PropertiesBlock.cs b/Ghost.DSL/ShaderCompiler/Parser/PropertiesBlock.cs index 1078309..3429a85 100644 --- a/Ghost.DSL/ShaderCompiler/Parser/PropertiesBlock.cs +++ b/Ghost.DSL/ShaderCompiler/Parser/PropertiesBlock.cs @@ -218,14 +218,18 @@ internal class PropertiesBlock : IBlockParser(); + shaderProperty.propertyInitializer.Add(token); + } + } + bodyStream.Expect(TokenType.RBrace); bodyStream.Expect(TokenType.Semicolon); break; @@ -282,9 +286,9 @@ internal class PropertiesBlock : IBlockParser errors, PropertyDeclaration property, PropertySemantic model) + private static bool ValidatePropertyInitializer(List errors, PropertyDeclaration property, PropertySemantic model) { - var constructor = property.propertyConstructor; - if (!constructor.HasValue) + var initializer = property.propertyInitializer; + if (initializer == null) { errors.Add(new DSLShaderError { - message = "Shader property constructor is null.", + message = "Shader property initializer is null.", line = property.name.line, column = property.name.column }); @@ -361,63 +365,25 @@ internal class PropertiesBlock : IBlockParser tokens, ref int index, int length) + public static Token Peek(ReadOnlySpan tokens, int index, int length) { - return index + length < tokens.Length ? tokens[index + length] : throw new InvalidOperationException("No more tokens available"); + if (index + length < tokens.Length) + { + return tokens[index + length]; + } + + throw new IndexOutOfRangeException(); } - public static bool TryPeek(ReadOnlySpan tokens, ref int index, int length, out Token token) + public static bool TryPeek(ReadOnlySpan tokens, int index, int length, out Token token) { if (index + length < tokens.Length) { @@ -31,29 +36,33 @@ internal static class TokenStreamImple return false; } - public static Token Consume(ReadOnlySpan tokens, ref int index) + public static Token Consume(ReadOnlySpan tokens, ref int index, int count = 1) { - return index < tokens.Length ? tokens[index++] : throw new InvalidOperationException("No more tokens available"); + if (index + count <= tokens.Length) + { + index += count; + return tokens[index - 1]; + } + + throw new IndexOutOfRangeException(); } - public static bool Match(ReadOnlySpan tokens, ref int index, TokenType type, string? lexeme) + public static bool Match(ReadOnlySpan tokens, int index, TokenType type, string? lexeme) { - var t = Peek(tokens, ref index, 0); + var t = Peek(tokens, index, 0); if (!t.Match(type, lexeme)) { return false; } - //index++; return true; } - public static int MatchMany(ReadOnlySpan tokens, ref int index, TokenType type, string? lexeme) + public static int MatchMany(ReadOnlySpan tokens, int index, TokenType type, string? lexeme) { var count = 0; - while (TryPeek(tokens, ref index, 0, out var t) && t.Match(type, lexeme)) + while (TryPeek(tokens, index, 0, out var t) && t.Match(type, lexeme)) { - index++; count++; } @@ -62,7 +71,7 @@ internal static class TokenStreamImple public static Token Expect(ReadOnlySpan tokens, ref int index, TokenType type, string? lexeme) { - if (!TryPeek(tokens, ref index, 0, out var t)) + if (!TryPeek(tokens, index, 0, out var t)) { throw new InvalidOperationException("Expected token but reached end of stream"); } @@ -110,17 +119,17 @@ internal class TokenStream public Token Peek(int length = 0) { - return TokenStreamImple.Peek(_tokens, ref _index, length); + return TokenStreamImple.Peek(_tokens, _index, length); } public bool TryPeek(out Token token) { - return TokenStreamImple.TryPeek(_tokens, ref _index, 0, out token); + return TokenStreamImple.TryPeek(_tokens, _index, 0, out token); } public bool TryPeek(int length, out Token token) { - return TokenStreamImple.TryPeek(_tokens, ref _index, length, out token); + return TokenStreamImple.TryPeek(_tokens, _index, length, out token); } public bool TryConsume(out Token token) @@ -128,19 +137,19 @@ internal class TokenStream return TokenStreamImple.TryConsume(_tokens, ref _index, out token); } - public Token Consume() + public Token Consume(int count = 1) { - return TokenStreamImple.Consume(_tokens, ref _index); + return TokenStreamImple.Consume(_tokens, ref _index, count); } public bool Match(TokenType type, string? lexeme = null) { - return TokenStreamImple.Match(_tokens, ref _index, type, lexeme); + return TokenStreamImple.Match(_tokens, _index, type, lexeme); } public int MatchMany(TokenType type, string? lexeme = null) { - return TokenStreamImple.MatchMany(_tokens, ref _index, type, lexeme); + return TokenStreamImple.MatchMany(_tokens, _index, type, lexeme); } public Token Expect(TokenType type, string? lexeme = null) @@ -224,17 +233,17 @@ internal ref struct TokenStreamSlice public Token Peek(int length = 0) { - return TokenStreamImple.Peek(_tokens, ref _index, length); + return TokenStreamImple.Peek(_tokens, _index, length); } public bool TryPeek(out Token token) { - return TokenStreamImple.TryPeek(_tokens, ref _index, 0, out token); + return TokenStreamImple.TryPeek(_tokens, _index, 0, out token); } public bool TryPeek(int length, out Token token) { - return TokenStreamImple.TryPeek(_tokens, ref _index, length, out token); + return TokenStreamImple.TryPeek(_tokens, _index, length, out token); } public bool TryConsume(out Token token) @@ -242,19 +251,19 @@ internal ref struct TokenStreamSlice return TokenStreamImple.TryConsume(_tokens, ref _index, out token); } - public Token Consume() + public Token Consume(int count = 1) { - return TokenStreamImple.Consume(_tokens, ref _index); + return TokenStreamImple.Consume(_tokens, ref _index, count); } public bool Match(TokenType type, string? lexeme = null) { - return TokenStreamImple.Match(_tokens, ref _index, type, lexeme); + return TokenStreamImple.Match(_tokens, _index, type, lexeme); } public int MatchMany(TokenType type, string? lexeme = null) { - return TokenStreamImple.MatchMany(_tokens, ref _index, type, lexeme); + return TokenStreamImple.MatchMany(_tokens, _index, type, lexeme); } public Token Expect(TokenType type, string? lexeme = null) diff --git a/Ghost.Graphics/Contracts/IShaderCompiler.cs b/Ghost.Graphics/Contracts/IShaderCompiler.cs index 624037f..83d350c 100644 --- a/Ghost.Graphics/Contracts/IShaderCompiler.cs +++ b/Ghost.Graphics/Contracts/IShaderCompiler.cs @@ -144,6 +144,6 @@ public readonly struct ShaderReflectionData public interface IShaderCompiler : IDisposable { Result Compile(ref readonly ShaderCompilationConfig config, Allocator allocator); - Result CompilePass(IPassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64 key); + Result CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64 key); Result LoadCompiledCache(Key64 key); } diff --git a/Ghost.Graphics/Core/DxcShaderCompiler.cs b/Ghost.Graphics/Core/DxcShaderCompiler.cs index a8212cd..a69ed3a 100644 --- a/Ghost.Graphics/Core/DxcShaderCompiler.cs +++ b/Ghost.Graphics/Core/DxcShaderCompiler.cs @@ -367,26 +367,23 @@ internal sealed unsafe partial class DxcShaderCompiler : IShaderCompiler // TODO: This should be shader variant specific compile instead of pass specific. // TODO: Build final shader code in memory before compiling. - public Result CompilePass(IPassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64 key) + public Result CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64 key) { ObjectDisposedException.ThrowIf(_disposed, this); - if (descriptor is not PassDescriptor fullDescriptor) - { - return Result.Failure("FullPassDescriptor expected."); - } - - var fullDefines = fullDescriptor.defines ?? new List(); - fullDefines.AddRange(additionalConfig.defines); + var defineCountInDescriptor = descriptor.defines?.Length ?? 0; + var fullDefines = new string[defineCountInDescriptor + additionalConfig.defines.Length]; + descriptor.defines?.CopyTo(fullDefines); + additionalConfig.defines.CopyTo(fullDefines.AsSpan(defineCountInDescriptor)); ShaderCompileResult tsResult = default; - var tsEntry = fullDescriptor.taskShader; + var tsEntry = descriptor.taskShader; if (tsEntry.IsCreated) { var config = new ShaderCompilationConfig { defines = fullDefines.AsSpan(), - includes = fullDescriptor.includes.AsSpan(), + includes = descriptor.includes.AsSpan(), shaderPath = tsEntry.shader, entryPoint = tsEntry.entry, stage = ShaderStage.TaskShader, @@ -405,13 +402,13 @@ internal sealed unsafe partial class DxcShaderCompiler : IShaderCompiler } ShaderCompileResult msResult; - var msEntry = fullDescriptor.meshShader; + var msEntry = descriptor.meshShader; if (msEntry.IsCreated) { var config = new ShaderCompilationConfig { defines = fullDefines.AsSpan(), - includes = fullDescriptor.includes.AsSpan(), + includes = descriptor.includes.AsSpan(), shaderPath = msEntry.shader, entryPoint = msEntry.entry, stage = ShaderStage.MeshShader, @@ -434,13 +431,13 @@ internal sealed unsafe partial class DxcShaderCompiler : IShaderCompiler } ShaderCompileResult psResult; - var psEntry = fullDescriptor.pixelShader; + var psEntry = descriptor.pixelShader; if (psEntry.IsCreated) { var config = new ShaderCompilationConfig { defines = fullDefines.AsSpan(), - includes = fullDescriptor.includes.AsSpan(), + includes = descriptor.includes.AsSpan(), shaderPath = psEntry.shader, entryPoint = psEntry.entry, stage = ShaderStage.PixelShader, diff --git a/Ghost.Graphics/Core/RenderingContext.cs b/Ghost.Graphics/Core/RenderingContext.cs index 77109f1..0472def 100644 --- a/Ghost.Graphics/Core/RenderingContext.cs +++ b/Ghost.Graphics/Core/RenderingContext.cs @@ -116,8 +116,8 @@ public readonly unsafe ref struct RenderingContext localToWorld = localToWorld, worldBoundsMin = meshData.BoundingBox.Min, worldBoundsMax = meshData.BoundingBox.Max, - vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()).GetValueOrThrow(), - indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()).GetValueOrThrow(), + vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()), + indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()), }; var bufferHandle = meshData.ObjectDataBuffer.AsResource(); @@ -214,8 +214,8 @@ public readonly unsafe ref struct RenderingContext var data = new PushConstantsData { - objectIndex = _engine.ResourceDatabase.GetBindlessIndex(meshRef.ObjectDataBuffer.AsResource()).GetValueOrThrow(), - materialIndex = _engine.ResourceDatabase.GetBindlessIndex(materialRef._cBufferCache.GpuResource.AsResource()).GetValueOrThrow(), + objectIndex = _engine.ResourceDatabase.GetBindlessIndex(meshRef.ObjectDataBuffer.AsResource()), + materialIndex = _engine.ResourceDatabase.GetBindlessIndex(materialRef._cBufferCache.GpuResource.AsResource()), }; var pushConstantSpan = new ReadOnlySpan(&data, sizeof(PushConstantsData) / sizeof(uint)); diff --git a/Ghost.Graphics/Core/Shader.cs b/Ghost.Graphics/Core/Shader.cs index 2bd7d76..20550e0 100644 --- a/Ghost.Graphics/Core/Shader.cs +++ b/Ghost.Graphics/Core/Shader.cs @@ -102,30 +102,23 @@ public partial struct Shader : IResourceReleasable internal Shader(ShaderDescriptor descriptor) { _cbufferSize = descriptor.cbufferSize; - _shaderPasses = new UnsafeArray(descriptor.passes.Count, Allocator.Persistent); - _passIDToLocal = new UnsafeHashMap(descriptor.passes.Count, Allocator.Persistent); + _shaderPasses = new UnsafeArray(descriptor.passes.Length, Allocator.Persistent); + _passIDToLocal = new UnsafeHashMap(descriptor.passes.Length, Allocator.Persistent); _keywordIDToLocal = new UnsafeHashMap(32, Allocator.Persistent); - for (var i = 0; i < descriptor.passes.Count; i++) + for (var i = 0; i < descriptor.passes.Length; i++) { var pass = descriptor.passes[i]; - - // TODO: Handle inherited passes - if (pass is not PassDescriptor fullPass) - { - continue; - } - - var passKey = RHIUtility.CreateShaderPassKey(pass.Identifier); + var passKey = RHIUtility.CreateShaderPassKey(pass.identifier); var keywords = default(LocalKeywordSet); - if (fullPass.keywords != null && fullPass.keywords.Count > 0) + if (pass.keywords.Length > 0) { var localKeywordIndex = 0; - for (var j = 0; j < fullPass.keywords.Count; j++) + for (var j = 0; j < pass.keywords.Length; j++) { - var group = fullPass.keywords[j]; + var group = pass.keywords[j]; if (group.keywords == null) { continue; @@ -150,11 +143,11 @@ public partial struct Shader : IResourceReleasable _shaderPasses[i] = new ShaderPass { Key = passKey, - DeafaultState = fullPass.localPipeline, + DeafaultState = pass.localPipeline, KeywordIDs = keywords, }; - _passIDToLocal[GetPassID(pass.Name)] = (ushort)i; + _passIDToLocal[GetPassID(pass.name)] = (ushort)i; } } diff --git a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs index d727c33..c330f27 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs @@ -244,14 +244,14 @@ internal class D3D12ResourceDatabase : IResourceDatabase return r.Value.desc; } - public Result GetBindlessIndex(Handle handle) + public uint GetBindlessIndex(Handle handle) { ObjectDisposedException.ThrowIf(_disposed, this); ref var info = ref GetResourceRecord(handle, out var exist); if (!exist || !info.Allocated) { - return ErrorStatus.NotFound; + return ~0u; } return (uint)info.viewGroup.srv.Value; diff --git a/Ghost.Graphics/RHI/IResourceDatabase.cs b/Ghost.Graphics/RHI/IResourceDatabase.cs index 2bff4cd..922af84 100644 --- a/Ghost.Graphics/RHI/IResourceDatabase.cs +++ b/Ghost.Graphics/RHI/IResourceDatabase.cs @@ -59,8 +59,8 @@ public interface IResourceDatabase : IDisposable /// Retrieves the bindless index associated with the specified GPU resource handle. /// /// A handle to the GPU resource for which to obtain the bindless index. Must reference a valid, currently registered resource. - /// The bindless index corresponding to the specified GPU resource handle. -1 if the resource does not support bindless access or is not found. - Result GetBindlessIndex(Handle handle); + /// The bindless index corresponding to the specified GPU resource handle. ~0 if the resource does not support bindless access or is not found. + uint GetBindlessIndex(Handle handle); /// /// Retrieves the name of the GPU resource associated with the specified handle. diff --git a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs index 56816be..4f69952 100644 --- a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs +++ b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs @@ -48,22 +48,31 @@ internal class MeshRenderPass : IRenderPass "C:/Users/Misaki/Downloads/Im/yande.re 1134666 blue_archive nakamasa_ichika sugarhigh.jpg" ]; - private static IEnumerable> GetAllVariantCombination(List keywordsGroups) + private static IEnumerable> GetAllVariantCombination(KeywordsGroup[] keywordsGroups) { - if (keywordsGroups.Count == 0) + if (keywordsGroups.Length == 0) { - yield return []; + yield return ReadOnlyMemory.Empty; yield break; } var firstGroup = keywordsGroups[0]; - var remainingGroups = keywordsGroups.Skip(1).ToList(); + var remainingGroups = keywordsGroups[1..]; + + foreach (var combination in GetAllVariantCombination(remainingGroups)) + { + yield return combination; + } + foreach (var keyword in firstGroup.keywords) { foreach (var combination in GetAllVariantCombination(remainingGroups)) { - combination.Insert(0, keyword); - yield return combination; + var array = new string[combination.Length + 1]; + array[0] = keyword; + combination.Span.CopyTo(array.AsSpan(1)); + + yield return array; } } } @@ -75,15 +84,9 @@ internal class MeshRenderPass : IRenderPass _shader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor); _material = ctx.ResourceAllocator.CreateMaterial(_shader); - for (var i = 0; i < shaderDescriptor.passes.Count; i++) + for (var i = 0; i < shaderDescriptor.passes.Length; i++) { - var pass = shaderDescriptor.passes[i]; - - if (pass is not PassDescriptor fullPass) - { - continue; - } - + ref var pass = ref shaderDescriptor.passes[i]; var config = new ShaderCompilationConfig { optimizeLevel = CompilerOptimizeLevel.O3, @@ -94,25 +97,25 @@ internal class MeshRenderPass : IRenderPass // TODO: Ideally, in editor mode, we compile a single variant when it's needed during rendering. Before the compilation is done, we fallback to a special "compilation in progress" shader. // During the build process, we can precompile all the variants and store them in the cache for fast loading in runtime. // After the compilation, we should store the compiled result in the disk cache even in editor mode. This allows us to avoid recompiling the same variant, same code hash and same version) multiple times. - if (fullPass.keywords == null) + if (pass.keywords.Length == 0) { var emptyKeywords = new LocalKeywordSet(); var variantKey = RHIUtility.CreateShaderVariantKey( - RHIUtility.CreateShaderPassKey(pass.Identifier), + RHIUtility.CreateShaderPassKey(pass.identifier), in emptyKeywords); - ctx.ShaderCompiler.CompilePass(pass, in config, variantKey).GetValueOrThrow(); + ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow(); } else { ref var shaderRef = ref ctx.ResourceDatabase.GetShaderReference(_shader); - foreach (var keyGroup in GetAllVariantCombination(fullPass.keywords)) + foreach (var keyGroup in GetAllVariantCombination(pass.keywords)) { - config.defines = keyGroup.AsSpan(); + config.defines = keyGroup.Span; var keywordsSet = new LocalKeywordSet(); - foreach (var key in keyGroup) + foreach (var key in keyGroup.Span) { var localIndex = shaderRef.GetLocalKeywordIndex(Shader.GetKeywordID(key)); if (localIndex == -1) @@ -124,10 +127,10 @@ internal class MeshRenderPass : IRenderPass } var variantKey = RHIUtility.CreateShaderVariantKey( - RHIUtility.CreateShaderPassKey(pass.Identifier), + RHIUtility.CreateShaderPassKey(pass.identifier), in keywordsSet); - ctx.ShaderCompiler.CompilePass(pass, in config, variantKey).GetValueOrThrow(); + ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow(); } } } @@ -172,10 +175,10 @@ internal class MeshRenderPass : IRenderPass var matProps = new ShaderProperties_MyShader_Standard { color = new float4(1.0f, 1.0f, 1.0f, 1.0f), - texture1 = ctx.ResourceDatabase.GetBindlessIndex(_textures[0].AsResource()).GetValueOrThrow(), - texture2 = ctx.ResourceDatabase.GetBindlessIndex(_textures[1].AsResource()).GetValueOrThrow(), - texture3 = ctx.ResourceDatabase.GetBindlessIndex(_textures[2].AsResource()).GetValueOrThrow(), - texture4 = ctx.ResourceDatabase.GetBindlessIndex(_textures[3].AsResource()).GetValueOrThrow(), + texture1 = ctx.ResourceDatabase.GetBindlessIndex(_textures[0].AsResource()), + texture2 = ctx.ResourceDatabase.GetBindlessIndex(_textures[1].AsResource()), + texture3 = ctx.ResourceDatabase.GetBindlessIndex(_textures[2].AsResource()), + texture4 = ctx.ResourceDatabase.GetBindlessIndex(_textures[3].AsResource()), tex_sampler = (uint)sampler.Value, }; diff --git a/Ghost.Graphics/test.gsdef b/Ghost.Graphics/test.gsdef index 120ee4a..7de2a28 100644 --- a/Ghost.Graphics/test.gsdef +++ b/Ghost.Graphics/test.gsdef @@ -2,11 +2,11 @@ shader "MyShader/Standard" { properties { - float4 color = float4(1, 1, 1, 1); - tex2d texture1 = tex2d(black); - tex2d texture2 = tex2d(white); - tex2d texture3 = tex2d(grey); - tex2d texture4 = tex2d(normal); + float4 color = { 1, 1, 1, 1 }; + tex2d texture1 = { black }; + tex2d texture2 = { white }; + tex2d texture3 = { grey }; + tex2d texture4 = { normal }; sampler tex_sampler; } @@ -21,7 +21,21 @@ shader "MyShader/Standard" color_mask = all; } - ms("F:/csharp/GhostEngine/Ghost.Graphics/RenderPasses/ShaderCode.hlsl", "MSMain"); - ps("F:/csharp/GhostEngine/Ghost.Graphics/RenderPasses/ShaderCode.hlsl", "PSMain"); + keywords + { + local TEST_KEYWORD, TEST_KEYWORD2; + local TEST_KEYWORD3; + } + + hlsl + { + float Test() + { + return 1.0; + } + } + + mesh "F:/csharp/GhostEngine/Ghost.Graphics/RenderPasses/ShaderCode.hlsl" : "MSMain"; + pixel "F:/csharp/GhostEngine/Ghost.Graphics/RenderPasses/ShaderCode.hlsl" : "PSMain"; } }