feat(render): refactor pipeline & shader system for DX12 WG
Major refactor of render pipeline and shader system: - Replaced legacy shader properties with source generator and attribute-based HLSL struct generation. - Introduced ShaderPropertiesRegistry for runtime property layout/code registration. - Added modular IRenderPipeline, IRenderPipelineSettings, and IRenderPayload interfaces. - Implemented GhostRenderPipeline and ECS-driven GPUScene management. - Added experimental DirectX 12 Work Graph support. - Refactored shader compilation, variant hashing, and caching. - Updated APIs for consistency and improved codegen for registration. These changes modernize the rendering infrastructure for advanced features like work graphs and dynamic pipelines. BREAKING CHANGE: Shader DSL, pipeline, and property APIs have changed. Existing shaders and pipeline integrations must be updated.
This commit is contained in:
@@ -17,6 +17,11 @@
|
||||
<Listener>false</Listener>
|
||||
<Visitor>true</Visitor>
|
||||
</Antlr4>
|
||||
<Antlr4 Include="Grammar\GhostComputeShaderParser.g4">
|
||||
<Visitor>true</Visitor>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<Listener>false</Listener>
|
||||
</Antlr4>
|
||||
<Antlr4 Include="Grammar\GhostShaderParser.g4">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<Listener>false</Listener>
|
||||
|
||||
68
src/Editor/Ghost.DSL/Grammar/GhostComputeShaderParser.g4
Normal file
68
src/Editor/Ghost.DSL/Grammar/GhostComputeShaderParser.g4
Normal file
@@ -0,0 +1,68 @@
|
||||
parser grammar GhostComputeParser;
|
||||
|
||||
options {
|
||||
tokenVocab = GhostComputeLexer;
|
||||
}
|
||||
|
||||
// Top-level rule
|
||||
computeFile: compute+ EOF;
|
||||
|
||||
compute:
|
||||
COMPUTE STRING_LITERAL LBRACE
|
||||
computeBody
|
||||
RBRACE;
|
||||
|
||||
computeBody:
|
||||
(definesBlock | includesBlock | keywordsBlock | hlslBlock | computeEntry)*;
|
||||
|
||||
scope:
|
||||
GLOBAL | LOCAL;
|
||||
|
||||
definesBlock:
|
||||
DEFINES LBRACE
|
||||
defineStatement*
|
||||
RBRACE;
|
||||
|
||||
defineStatement:
|
||||
IDENTIFIER SEMICOLON;
|
||||
|
||||
includesBlock:
|
||||
INCLUDES LBRACE
|
||||
includeStatement*
|
||||
RBRACE;
|
||||
|
||||
includeStatement:
|
||||
STRING_LITERAL SEMICOLON;
|
||||
|
||||
keywordsBlock:
|
||||
KEYWORDS LBRACE
|
||||
keywordStatement*
|
||||
RBRACE;
|
||||
|
||||
keywordStatement:
|
||||
scope? IDENTIFIER (COMMA IDENTIFIER)* SEMICOLON;
|
||||
|
||||
hlslBlock:
|
||||
HLSL LBRACE
|
||||
hlslBody
|
||||
RBRACE;
|
||||
|
||||
// Recursively matches content, ensuring braces are balanced.
|
||||
hlslBody:
|
||||
(
|
||||
~(LBRACE | RBRACE) // Match ANY token except open/close braces
|
||||
|
|
||||
LBRACE hlslBody RBRACE // Or match a nested block recursively
|
||||
)*;
|
||||
|
||||
computeEntry:
|
||||
IDENTIFIER STRING_LITERAL COLON STRING_LITERAL SEMICOLON;
|
||||
|
||||
functionCall:
|
||||
IDENTIFIER LPAREN functionArguments? RPAREN SEMICOLON;
|
||||
|
||||
functionArguments:
|
||||
functionArgument (COMMA functionArgument)*;
|
||||
|
||||
functionArgument:
|
||||
STRING_LITERAL | NUMBER | IDENTIFIER;
|
||||
@@ -2,7 +2,7 @@ lexer grammar GhostShaderLexer;
|
||||
|
||||
// Keywords
|
||||
SHADER: 'shader';
|
||||
PROPERTIES: 'properties';
|
||||
COMPUTE: 'compute';
|
||||
PIPELINE: 'pipeline';
|
||||
PASS: 'pass';
|
||||
DEFINES: 'defines';
|
||||
|
||||
@@ -13,23 +13,11 @@ shader:
|
||||
RBRACE;
|
||||
|
||||
shaderBody:
|
||||
(propertiesBlock | pipelineBlock | passBlock | functionCall)*;
|
||||
|
||||
// Properties block
|
||||
propertiesBlock:
|
||||
PROPERTIES LBRACE
|
||||
propertyDeclaration*
|
||||
RBRACE;
|
||||
|
||||
propertyDeclaration:
|
||||
scope? IDENTIFIER IDENTIFIER (EQUALS LBRACE propertyInitializer RBRACE)? SEMICOLON;
|
||||
(pipelineBlock | passBlock | functionCall)*;
|
||||
|
||||
scope:
|
||||
GLOBAL | LOCAL;
|
||||
|
||||
propertyInitializer:
|
||||
(NUMBER | IDENTIFIER) (COMMA (NUMBER | IDENTIFIER))*;
|
||||
|
||||
// Pipeline block
|
||||
pipelineBlock:
|
||||
PIPELINE LBRACE
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Graphics;
|
||||
using Ghost.DSL.ShaderParser;
|
||||
using System.IO.Hashing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.DSL.ShaderCompiler;
|
||||
@@ -19,12 +21,9 @@ public struct DSLShaderError
|
||||
|
||||
internal static class DSLShaderCompiler
|
||||
{
|
||||
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 static string GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass)
|
||||
private static ulong GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass)
|
||||
{
|
||||
return $"{shader.name}_{pass.name}";
|
||||
return XxHash64.HashToUInt64(MemoryMarshal.AsBytes($"{shader.name}_{pass.name}".AsSpan()));
|
||||
}
|
||||
|
||||
private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
|
||||
@@ -44,64 +43,22 @@ internal static class DSLShaderCompiler
|
||||
};
|
||||
}
|
||||
|
||||
private static int LayoutCBufferProperties(Span<PropertyDescriptor> properties)
|
||||
{
|
||||
if (properties.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var currentOffset = 0;
|
||||
|
||||
foreach (ref var prop in properties)
|
||||
{
|
||||
var size = prop.type.GetSize();
|
||||
|
||||
if ((currentOffset % 16) + size > 16)
|
||||
{
|
||||
currentOffset = (currentOffset + 15) & ~15;
|
||||
}
|
||||
|
||||
prop.offset = currentOffset;
|
||||
prop.size = size;
|
||||
|
||||
currentOffset += size;
|
||||
}
|
||||
|
||||
return (currentOffset + 15) & ~15;
|
||||
}
|
||||
|
||||
// TODO: Implement shader inheritance resolution, including property and pass merging.
|
||||
// Currently, we just ignore inheritance.
|
||||
public static ShaderDescriptor ResolveShader(DSLShaderSemantics semantics)
|
||||
public static Result<ShaderDescriptor> ResolveShader(DSLShaderSemantics semantics)
|
||||
{
|
||||
var descriptor = new ShaderDescriptor
|
||||
{
|
||||
name = semantics.name,
|
||||
hlsl = semantics.hlsl
|
||||
};
|
||||
|
||||
var shaderGlobalProperties = semantics.properties?
|
||||
.Where(p => p.scope == PropertyScope.Global)
|
||||
.Select(p => new PropertyDescriptor
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type,
|
||||
defaultValue = p.defaultValue
|
||||
}).ToArray();
|
||||
if (!ShaderPropertiesRegistry.TryGetCode(semantics.name, out var info))
|
||||
{
|
||||
info = default;
|
||||
}
|
||||
|
||||
var shaderLocalProperties = semantics.properties?
|
||||
.Where(p => p.scope == PropertyScope.Local)
|
||||
.Select(p => new PropertyDescriptor
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type,
|
||||
defaultValue = p.defaultValue
|
||||
}).ToArray();
|
||||
|
||||
descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty<PropertyDescriptor>();
|
||||
descriptor.properties = shaderLocalProperties ?? Array.Empty<PropertyDescriptor>();
|
||||
descriptor.cbufferSize = LayoutCBufferProperties(descriptor.properties);
|
||||
descriptor.propertiesCode = info.code ?? string.Empty;
|
||||
descriptor.propertyBufferSize = info.size;
|
||||
|
||||
if (semantics.passes != null)
|
||||
{
|
||||
@@ -112,6 +69,7 @@ internal static class DSLShaderCompiler
|
||||
var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default);
|
||||
descriptor.passes[i] = new PassDescriptor
|
||||
{
|
||||
shader = descriptor,
|
||||
identifier = GetPassUniqueId(semantics, pass),
|
||||
name = pass.name,
|
||||
taskShader = pass.taskShader,
|
||||
@@ -172,155 +130,17 @@ internal static class DSLShaderCompiler
|
||||
return Result.Failure("Failed to compile shader due to errors:\n" + errorMessages.ToString());
|
||||
}
|
||||
|
||||
var desc = ResolveShader(model);
|
||||
var globalPropResult = GenerateGlobalProperties(desc.globalProperties, generatedOutputDirectory);
|
||||
if (globalPropResult.IsFailure)
|
||||
var result = ResolveShader(model);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return Result.Failure("Failed to generate global properties: " + globalPropResult.Message);
|
||||
return result;
|
||||
}
|
||||
|
||||
var generatedResult = GenerateShaderCode(desc, generatedOutputDirectory);
|
||||
if (generatedResult.IsFailure)
|
||||
{
|
||||
return Result.Failure("Failed to generate pass files: " + generatedResult.Message);
|
||||
}
|
||||
|
||||
foreach (ref var pass in desc.passes.AsSpan())
|
||||
{
|
||||
if (pass.includes == null)
|
||||
{
|
||||
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;
|
||||
return result.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure("Failed to compile shader: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
ShaderPropertyType.Texture3D => "TEXTURE3D",
|
||||
ShaderPropertyType.TextureCube => "TEXTURECUBE",
|
||||
ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY",
|
||||
ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY",
|
||||
ShaderPropertyType.Sampler => "SAMPLER",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
public static Result<string> GenerateShaderCode(ShaderDescriptor descriptor, string targetDirectory)
|
||||
{
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
return Result.Failure("Target directory does not exist.");
|
||||
}
|
||||
|
||||
var outputFileName = descriptor.name.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/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl""");
|
||||
|
||||
sb.Append(@"
|
||||
struct PerMaterialData
|
||||
{");
|
||||
foreach (var prop in descriptor.properties)
|
||||
{
|
||||
sb.Append($@"
|
||||
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
|
||||
}
|
||||
sb.Append(@"
|
||||
};");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(@$"
|
||||
#endif // {fileDefine}");
|
||||
|
||||
fileStream.Write(sb.ToString());
|
||||
|
||||
return outputFilePath;
|
||||
}
|
||||
|
||||
public static Result<string> GenerateGlobalProperties(ReadOnlySpan<PropertyDescriptor> globalProperties, string targetDirectory)
|
||||
{
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
return Result.Failure("Target directory does not exist.");
|
||||
}
|
||||
|
||||
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/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl""
|
||||
|
||||
struct GlobalData
|
||||
{");
|
||||
foreach (var prop in globalProperties)
|
||||
{
|
||||
sb.Append($@"
|
||||
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
|
||||
}
|
||||
sb.AppendLine(@"
|
||||
};
|
||||
|
||||
#endif // GLOBALDATA_G_HLSL");
|
||||
globalFileStream.Write(sb.ToString());
|
||||
|
||||
return globalFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,6 @@ public enum PropertyScope
|
||||
Local,
|
||||
}
|
||||
|
||||
public class PropertySemantic
|
||||
{
|
||||
public PropertyScope scope;
|
||||
public ShaderPropertyType type;
|
||||
public string name = string.Empty;
|
||||
public object? defaultValue;
|
||||
}
|
||||
|
||||
public class PipelineSemantic
|
||||
{
|
||||
public ZTest? zTest;
|
||||
@@ -41,8 +33,6 @@ public class PassSemantic
|
||||
public class DSLShaderSemantics
|
||||
{
|
||||
public string name = string.Empty;
|
||||
public string? hlsl;
|
||||
public List<PropertySemantic>? properties;
|
||||
public PipelineSemantic? pipeline;
|
||||
public List<PassSemantic>? passes;
|
||||
}
|
||||
@@ -71,7 +71,6 @@ public class AntlrShaderCompiler
|
||||
var semantics = new DSLShaderSemantics
|
||||
{
|
||||
name = model.Name,
|
||||
properties = ConvertProperties(model.Properties, errors),
|
||||
pipeline = ConvertPipeline(model.Pipeline, errors)
|
||||
};
|
||||
|
||||
@@ -88,48 +87,6 @@ public class AntlrShaderCompiler
|
||||
return semantics;
|
||||
}
|
||||
|
||||
private static List<PropertySemantic>? ConvertProperties(PropertiesBlockModel? properties, List<DSLShaderError> errors)
|
||||
{
|
||||
if (properties == null || properties.Properties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new List<PropertySemantic>();
|
||||
var usedNames = new HashSet<string>();
|
||||
|
||||
foreach (var prop in properties.Properties)
|
||||
{
|
||||
if (usedNames.Contains(prop.Name))
|
||||
{
|
||||
errors.Add(new DSLShaderError
|
||||
{
|
||||
message = $"Duplicate property name '{prop.Name}'.",
|
||||
line = 0,
|
||||
column = 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var semantic = new PropertySemantic
|
||||
{
|
||||
name = prop.Name,
|
||||
scope = prop.Scope?.ToLower() == "global" ? PropertyScope.Global : PropertyScope.Local,
|
||||
type = ParsePropertyType(prop.Type, errors)
|
||||
};
|
||||
|
||||
if (prop.Initializer.Count > 0)
|
||||
{
|
||||
semantic.defaultValue = ParsePropertyValue(semantic.type, prop.Initializer, errors);
|
||||
}
|
||||
|
||||
usedNames.Add(prop.Name);
|
||||
result.Add(semantic);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ShaderPropertyType ParsePropertyType(string type, List<DSLShaderError> errors)
|
||||
{
|
||||
return type.ToLower() switch
|
||||
|
||||
@@ -27,11 +27,6 @@ public class ShaderVisitor : GhostShaderParserBaseVisitor<object>
|
||||
var shaderBody = context.shaderBody();
|
||||
if (shaderBody != null)
|
||||
{
|
||||
foreach (var propBlock in shaderBody.propertiesBlock())
|
||||
{
|
||||
shader.Properties = (PropertiesBlockModel)VisitPropertiesBlock(propBlock);
|
||||
}
|
||||
|
||||
foreach (var pipelineBlock in shaderBody.pipelineBlock())
|
||||
{
|
||||
shader.Pipeline = (PipelineBlockModel)VisitPipelineBlock(pipelineBlock);
|
||||
@@ -51,47 +46,6 @@ public class ShaderVisitor : GhostShaderParserBaseVisitor<object>
|
||||
return shader;
|
||||
}
|
||||
|
||||
public override object VisitPropertiesBlock([NotNull] GhostShaderParser.PropertiesBlockContext context)
|
||||
{
|
||||
var properties = new PropertiesBlockModel();
|
||||
|
||||
foreach (var propDecl in context.propertyDeclaration())
|
||||
{
|
||||
properties.Properties.Add((PropertyDeclarationModel)VisitPropertyDeclaration(propDecl));
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public override object VisitPropertyDeclaration([NotNull] GhostShaderParser.PropertyDeclarationContext context)
|
||||
{
|
||||
var property = new PropertyDeclarationModel
|
||||
{
|
||||
Type = context.IDENTIFIER(0).GetText(),
|
||||
Name = context.IDENTIFIER(1).GetText()
|
||||
};
|
||||
|
||||
if (context.scope() != null)
|
||||
{
|
||||
property.Scope = context.scope().GetText();
|
||||
}
|
||||
|
||||
if (context.propertyInitializer() != null)
|
||||
{
|
||||
var init = context.propertyInitializer();
|
||||
foreach (var number in init.NUMBER())
|
||||
{
|
||||
property.Initializer.Add(number.GetText());
|
||||
}
|
||||
foreach (var identifier in init.IDENTIFIER())
|
||||
{
|
||||
property.Initializer.Add(identifier.GetText());
|
||||
}
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
|
||||
public override object VisitPipelineBlock([NotNull] GhostShaderParser.PipelineBlockContext context)
|
||||
{
|
||||
var pipeline = new PipelineBlockModel();
|
||||
|
||||
Reference in New Issue
Block a user