Refactor shader system: arrays, keywords, property syntax

Major refactor of shader compiler and related systems:
- Switch ShaderDescriptor/PassDescriptor to arrays; remove IPassDescriptor
- Rewrite keywords block parser/semantic analysis for flexible syntax
- Change property initializers to brace syntax `{ ... }`
- Simplify TokenStream API (remove ref index params)
- Make GetBindlessIndex return uint (~0u for not found)
- Update shader compilation and variant logic for new descriptors
- Update test shader syntax to match new property/keyword formats
- Add AGENTS.md agent development guide
- Add Antlr4 dependency to Ghost.DSL
- Miscellaneous code style and error handling improvements
This commit is contained in:
2026-01-10 18:36:18 +09:00
parent 6a041f75ba
commit d71bdb3fc9
18 changed files with 548 additions and 246 deletions

297
AGENTS.md Normal file
View File

@@ -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<EntityQueryTest>(); // Run specific test
TestRunner.Run<EntityQueryTest>(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<EntityLocation> _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<T> GetValue()
{
if (success)
return Result<T>.Success(value);
else
return Result<T>.Failure("Error message");
}
public Result<T, ErrorStatus> 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<T> 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<Entity>)stackalloc Entity[1];
// Using allocation scope
using var scope = AllocationManager.CreateStackScope();
var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
// Ref returns for zero-copy access
public ref T GetSingleton<T>() where T : unmanaged, IComponent
{
var ptr = GetSingleton(ComponentTypeID<T>.Value);
return ref *(T*)ptr;
}
```
### Type Safety Patterns
**Strongly-typed identifiers**:
```csharp
Identifier<IComponent> componentID;
Identifier<Archetype> archetypeID;
Handle<T> resourceHandle;
```
**Generic constraints**:
```csharp
public void Method<T>() where T : unmanaged, IComponent
public void Method<T, E>() 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
/// <summary>
/// Create an entity with specified components.
/// </summary>
/// <param name="set">A set of component space IDs to add to the entities.</param>
/// <returns>The created entity.</returns>
/// <remarks>
/// This method causes structural changes and is not thread-safe.
/// Use <see cref="EntityCommandBuffer"/> to defer changes.
/// </remarks>
public Entity CreateEntity(ComponentSet set) { }
```
### Common Patterns
**ECS Component Registration**:
```csharp
// Type-safe component ID
ComponentTypeID<Transform>.Value
// Component sets for archetypes
var set = new ComponentSet(ComponentTypeID<Transform>.Value, ComponentTypeID<Velocity>.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

View File

@@ -33,19 +33,6 @@ public struct KeywordsGroup
public List<string> 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<string>? defines;
public List<string>? includes;
public List<KeywordsGroup>? 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<PropertyDescriptor> globalProperties = new();
public List<PropertyDescriptor> properties = new();
public List<IPassDescriptor> passes = new();
public PropertyDescriptor[] globalProperties = null!;
public PropertyDescriptor[] properties = null!;
public PassDescriptor[] passes = null!;
}
public static class ShaderDescriptorExtensions

View File

@@ -6,6 +6,14 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Antlr4.CodeGenerator" Version="4.6.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Ghost.Core/Ghost.Core.csproj" />
</ItemGroup>

View File

@@ -145,8 +145,13 @@ internal static class DSLShaderCompiler
};
}
private static uint CalculateCBufferSize(List<PropertyDescriptor> properties)
private static uint CalculateCBufferSize(ReadOnlySpan<PropertyDescriptor> 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<PropertyDescriptor>();
descriptor.globalProperties.AddRange(shaderGlobalProperties);
}
if (shaderLocalProperties != null)
{
descriptor.properties ??= new List<PropertyDescriptor>();
descriptor.properties.AddRange(shaderLocalProperties);
descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties);
}
descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty<PropertyDescriptor>();
descriptor.properties = shaderLocalProperties ?? Array.Empty<PropertyDescriptor>();
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<string>(),
includes = pass.includes?.ToArray() ?? Array.Empty<string>(),
keywords = pass.keywords?.ToArray() ?? Array.Empty<KeywordsGroup>()
};
descriptor.passes.Add(fullPass);
}
}
else
{
descriptor.passes = Array.Empty<PassDescriptor>();
}
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<string>();
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<string> GenerateGlobalProperties(List<PropertyDescriptor> globalProperties, string targetDirectory)
public static Result<string> GenerateGlobalProperties(ReadOnlySpan<PropertyDescriptor> globalProperties, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{

View File

@@ -32,6 +32,7 @@ internal class PassSemantic
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
public List<string>? defines;
public List<string>? includes;
public List<KeywordsGroup>? keywords;
public PipelineSemantic? localPipeline;
}

View File

@@ -11,7 +11,7 @@ internal struct PropertyDeclaration
public Token scope;
public Token type;
public Token name;
public FunctionCallDeclaration? propertyConstructor;
public List<Token>? propertyInitializer;
}
internal struct ValueDeclaration
@@ -44,7 +44,7 @@ internal class PassSyntax
public HlslDeclaration? hlsl;
public List<Token>? defines;
public List<Token>? includes;
public List<FunctionCallDeclaration>? keywords;
public List<List<Token>>? keywords;
public List<FunctionCallDeclaration>? functionCalls;
}

View File

@@ -2,26 +2,42 @@ using Ghost.Core.Graphics;
namespace Ghost.DSL.ShaderCompiler.Parser;
internal class KeywordsBlock : IBlockParser<List<FunctionCallDeclaration>, List<KeywordsGroup>>
internal class KeywordsBlock : IBlockParser<List<List<Token>>, List<KeywordsGroup>>
{
public static bool ShouldEnter(Token token)
{
return token.Match(TokenType.Keyword, TokenLexicon.KnownKeywords.KEYWORDS);
}
public static List<FunctionCallDeclaration> Parse(TokenStreamSlice stream)
public static List<List<Token>> Parse(TokenStreamSlice stream)
{
stream.Expect(TokenType.Keyword);
stream.Expect(TokenType.LBrace);
var keywords = new List<FunctionCallDeclaration>();
var keywords = new List<List<Token>>();
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<Token>();
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<FunctionCallDeclaration>, List<
return keywords;
}
public static List<KeywordsGroup>? SemanticAnalysis(List<FunctionCallDeclaration>? syntax, List<DSLShaderError> errors)
public static List<KeywordsGroup>? SemanticAnalysis(List<List<Token>>? syntax, List<DSLShaderError> errors)
{
if (syntax == null)
{
@@ -38,42 +54,42 @@ internal class KeywordsBlock : IBlockParser<List<FunctionCallDeclaration>, List<
}
var keywords = new List<KeywordsGroup>(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<string>(keyword.arguments.Count);
group.keywords.Add(arg.lexeme);
group.keywords ??= new List<string>(keys.Count);
group.keywords.Add(token.lexeme);
}
keywords.Add(group);

View File

@@ -68,6 +68,7 @@ internal class PassBlock : IBlockParser<PassSyntax, PassSemantic>
{
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),
};

View File

@@ -218,14 +218,18 @@ internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySem
{
case TokenType.Equals:
{
var constructorTypeToken = bodyStream.Expect(TokenType.Identifier);
var args = ParseUtility.ParseFunctionArguments(ref bodyStream, TokenType.Identifier | TokenType.Number);
shaderProperty.propertyConstructor = new FunctionCallDeclaration
bodyStream.Expect(TokenType.LBrace);
while (!bodyStream.Match(TokenType.RBrace))
{
name = constructorTypeToken,
arguments = args
};
var token = bodyStream.Consume();
if (!token.Match(TokenType.Comma))
{
shaderProperty.propertyInitializer ??= new List<Token>();
shaderProperty.propertyInitializer.Add(token);
}
}
bodyStream.Expect(TokenType.RBrace);
bodyStream.Expect(TokenType.Semicolon);
break;
@@ -282,9 +286,9 @@ internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySem
continue;
}
if (property.propertyConstructor != null)
if (property.propertyInitializer != null)
{
flowControl = ValidatePropertyConstructor(errors, property, model);
flowControl = ValidatePropertyInitializer(errors, property, model);
if (!flowControl)
{
continue;
@@ -346,14 +350,14 @@ internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySem
return true;
}
private static bool ValidatePropertyConstructor(List<DSLShaderError> errors, PropertyDeclaration property, PropertySemantic model)
private static bool ValidatePropertyInitializer(List<DSLShaderError> 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<PropertiesSyntax, List<PropertySem
return false;
}
var constructorValue = constructor.Value;
if (string.IsNullOrWhiteSpace(constructorValue.name.lexeme))
{
errors.Add(new DSLShaderError
{
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 DSLShaderError
{
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 DSLShaderError
{
message = $"No constructor metadata registered for property type '{model.type}'.",
line = constructorValue.name.line,
column = constructorValue.name.column
message = $"No initializer metadata registered for property type '{model.type}'.",
line = property.name.line,
column = property.name.column
});
return false;
}
// Count check
if (constructorValue.arguments == null)
if (initializer.Count != info.ArgCount)
{
errors.Add(new DSLShaderError
{
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 DSLShaderError
{
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
message = $"Shader property constructor for type '{property.type.lexeme}' expects {info.ArgCount} argument(s), but got {initializer.Count}.",
line = property.name.line,
column = property.name.column
});
return false;
@@ -425,9 +391,9 @@ internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySem
// Type check (uniform requirement for all args)
var hasError = false;
for (var i = 0; i < constructorValue.arguments.Count; i++)
for (var i = 0; i < initializer.Count; i++)
{
var arg = constructorValue.arguments[i];
var arg = initializer[i];
if (!arg.Match(info.ArgTokenType))
{
errors.Add(new DSLShaderError
@@ -451,15 +417,15 @@ internal class PropertiesBlock : IBlockParser<PropertiesSyntax, List<PropertySem
{
try
{
model.defaultValue = info.Builder(constructorValue.arguments, errors);
model.defaultValue = info.Builder(initializer, errors);
}
catch (Exception ex)
{
errors.Add(new DSLShaderError
{
message = $"Failed to construct default value for property '{property.name.lexeme}': {ex.Message}",
line = constructorValue.name.line,
column = constructorValue.name.column
line = property.name.line,
column = property.name.column
});
return false;

View File

@@ -2,12 +2,17 @@ namespace Ghost.DSL.ShaderCompiler;
internal static class TokenStreamImple
{
public static Token Peek(ReadOnlySpan<Token> tokens, ref int index, int length)
public static Token Peek(ReadOnlySpan<Token> 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<Token> tokens, ref int index, int length, out Token token)
public static bool TryPeek(ReadOnlySpan<Token> 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<Token> tokens, ref int index)
public static Token Consume(ReadOnlySpan<Token> 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<Token> tokens, ref int index, TokenType type, string? lexeme)
public static bool Match(ReadOnlySpan<Token> 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<Token> tokens, ref int index, TokenType type, string? lexeme)
public static int MatchMany(ReadOnlySpan<Token> 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<Token> 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)

View File

@@ -144,6 +144,6 @@ public readonly struct ShaderReflectionData
public interface IShaderCompiler : IDisposable
{
Result<ShaderCompileResult> Compile(ref readonly ShaderCompilationConfig config, Allocator allocator);
Result<GraphicsCompiledResult> CompilePass(IPassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key);
Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key);
Result<GraphicsCompiledResult, ErrorStatus> LoadCompiledCache(Key64<ShaderVariant> key);
}

View File

@@ -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<GraphicsCompiledResult> CompilePass(IPassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key)
public Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (descriptor is not PassDescriptor fullDescriptor)
{
return Result.Failure("FullPassDescriptor expected.");
}
var fullDefines = fullDescriptor.defines ?? new List<string>();
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,

View File

@@ -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<uint>(&data, sizeof(PushConstantsData) / sizeof(uint));

View File

@@ -102,30 +102,23 @@ public partial struct Shader : IResourceReleasable
internal Shader(ShaderDescriptor descriptor)
{
_cbufferSize = descriptor.cbufferSize;
_shaderPasses = new UnsafeArray<ShaderPass>(descriptor.passes.Count, Allocator.Persistent);
_passIDToLocal = new UnsafeHashMap<int, int>(descriptor.passes.Count, Allocator.Persistent);
_shaderPasses = new UnsafeArray<ShaderPass>(descriptor.passes.Length, Allocator.Persistent);
_passIDToLocal = new UnsafeHashMap<int, int>(descriptor.passes.Length, Allocator.Persistent);
_keywordIDToLocal = new UnsafeHashMap<int, int>(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;
}
}

View File

@@ -244,14 +244,14 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return r.Value.desc;
}
public Result<uint, ErrorStatus> GetBindlessIndex(Handle<GPUResource> handle)
public uint GetBindlessIndex(Handle<GPUResource> 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;

View File

@@ -59,8 +59,8 @@ public interface IResourceDatabase : IDisposable
/// Retrieves the bindless index associated with the specified GPU resource handle.
/// </summary>
/// <param name="handle">A handle to the GPU resource for which to obtain the bindless index. Must reference a valid, currently registered resource.</param>
/// <returns>The bindless index corresponding to the specified GPU resource handle. -1 if the resource does not support bindless access or is not found.</returns>
Result<uint, ErrorStatus> GetBindlessIndex(Handle<GPUResource> handle);
/// <returns>The bindless index corresponding to the specified GPU resource handle. ~0 if the resource does not support bindless access or is not found.</returns>
uint GetBindlessIndex(Handle<GPUResource> handle);
/// <summary>
/// Retrieves the name of the GPU resource associated with the specified handle.

View File

@@ -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<List<string>> GetAllVariantCombination(List<KeywordsGroup> keywordsGroups)
private static IEnumerable<ReadOnlyMemory<string>> GetAllVariantCombination(KeywordsGroup[] keywordsGroups)
{
if (keywordsGroups.Count == 0)
if (keywordsGroups.Length == 0)
{
yield return [];
yield return ReadOnlyMemory<string>.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,
};

View File

@@ -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";
}
}