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:
2026-04-08 23:08:02 +09:00
parent 0fc449bc78
commit 68fda03aa9
54 changed files with 1414 additions and 540 deletions

View File

@@ -21,7 +21,7 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.7" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.5.8" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.10">
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -33,21 +33,14 @@ public struct KeywordsGroup
public List<string> keywords;
}
public struct PropertyDescriptor
{
public string name;
public int offset;
public int size;
public ShaderPropertyType type;
public object? defaultValue;
}
public struct PassDescriptor
{
public string identifier;
public ShaderDescriptor shader;
public ulong identifier;
public string name;
public string? hlsl;
public ShaderEntryPoint taskShader;
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
@@ -55,54 +48,23 @@ public struct PassDescriptor
public string[] includes;
public KeywordsGroup[] keywords;
public PipelineState localPipeline;
public string? hlsl;
}
public class ShaderDescriptor
{
public string name = string.Empty;
public int cbufferSize;
public PropertyDescriptor[] globalProperties = null!;
public PropertyDescriptor[] properties = null!;
public PassDescriptor[] passes = null!;
public string? hlsl;
public string propertiesCode = string.Empty;
public uint propertyBufferSize;
public PassDescriptor[] passes = Array.Empty<PassDescriptor>();
}
public static class ShaderDescriptorExtensions
public class ComputeShaderDescriptor
{
public static int GetSize(this ShaderPropertyType type)
{
return type switch
{
ShaderPropertyType.Float
or ShaderPropertyType.Int
or ShaderPropertyType.UInt
or ShaderPropertyType.Bool => 4,
ShaderPropertyType.Float2
or ShaderPropertyType.Int2
or ShaderPropertyType.UInt2
or ShaderPropertyType.Bool2 => 8,
ShaderPropertyType.Float3
or ShaderPropertyType.Int3
or ShaderPropertyType.UInt3
or ShaderPropertyType.Bool3 => 12,
ShaderPropertyType.Float4
or ShaderPropertyType.Int4
or ShaderPropertyType.UInt4
or ShaderPropertyType.Bool4 => 16,
ShaderPropertyType.Float4x4 => 64,
ShaderPropertyType.Texture2D
or ShaderPropertyType.Texture3D
or ShaderPropertyType.TextureCube
or ShaderPropertyType.Texture2DArray
or ShaderPropertyType.TextureCubeArray
or ShaderPropertyType.Sampler => 4, // Bindless resource use uint32
_ => 0,
};
}
public string name = string.Empty;
public string propertiesCode = string.Empty;
public uint propertyBufferSize;
public ShaderEntryPoint entryPoint;
public string[] defines = Array.Empty<string>();
public string[] includes = Array.Empty<string>();
public KeywordsGroup[] keywords = Array.Empty<KeywordsGroup>();
}

View File

@@ -0,0 +1,41 @@
namespace Ghost.Core.Graphics;
[AttributeUsage(AttributeTargets.Struct)]
public class GenerateShaderPropertyAttribute : Attribute
{
public GenerateShaderPropertyAttribute(string shaderName, string? name = null)
{
}
}
[AttributeUsage(AttributeTargets.Field)]
public class GenerateAsHLSLTypeAttribute : Attribute
{
public GenerateAsHLSLTypeAttribute(string hlslTypeName)
{
}
}
#if DEBUG || GHOST_EDITOR
public struct ShaderPropertyInfo
{
public string shaderName;
public string code;
public uint size;
}
public static class ShaderPropertiesRegistry
{
private static readonly Dictionary<string, ShaderPropertyInfo> s_nameToCode = new Dictionary<string, ShaderPropertyInfo>(StringComparer.Ordinal);
public static void Register(string name, string code, uint size)
{
s_nameToCode[name] = new ShaderPropertyInfo { shaderName = name, code = code, size = size };
}
public static bool TryGetCode(string name, out ShaderPropertyInfo info)
{
return s_nameToCode.TryGetValue(name, out info);
}
}
#endif

View File

@@ -186,6 +186,9 @@ public readonly struct Key64<T> : IEquatable<Key64<T>>
{
return !a.Equals(b);
}
public static implicit operator ulong(Key64<T> key) => key.Value;
public static implicit operator Key64<T>(ulong value) => new Key64<T>(value);
}
public readonly struct Key128<T> : IEquatable<Key128<T>>
@@ -239,4 +242,7 @@ public readonly struct Key128<T> : IEquatable<Key128<T>>
{
return !a.Equals(b);
}
public static implicit operator UInt128(Key128<T> key) => key.Value;
public static implicit operator Key128<T>(UInt128 value) => new Key128<T>(value);
}

View File

@@ -10,13 +10,13 @@ public static class Hash
private const ulong _PRIME4 = 0x9e3779b97f4a7c15ul; // Golden Ratio
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b)
public static ulong Combine64(ulong a, ulong b)
{
return a ^ (b * _PRIME4 + (a << 6) + (a >> 2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b, ulong c)
public static ulong Combine64(ulong a, ulong b, ulong c)
{
var h1 = a * _PRIME1;
var h2 = b * _PRIME2;
@@ -29,7 +29,7 @@ public static class Hash
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b, ulong c, ulong d)
public static ulong Combine64(ulong a, ulong b, ulong c, ulong d)
{
var h1 = a * _PRIME1;
var h2 = b * _PRIME2;

View File

@@ -39,4 +39,4 @@ public sealed partial class EngineCore : IDisposable
_renderSystem.Dispose();
_jobScheduler.Dispose();
}
}
}

View File

@@ -26,7 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
<!--<ProjectReference Include="..\Ghost.Generator\Ghost.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />-->
<ProjectReference Include="..\Ghost.Generator\Ghost.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,111 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline;
internal unsafe class GPUScene : IDisposable
{
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
private Handle<GPUBuffer> _sceneBuffer;
private uint _instanceCount;
private uint _capacity;
private uint _requiredResize;
private bool _disposed;
internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount)
{
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
var bufferDesc = new BufferDesc
{
Size = initialCount * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
_sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer");
Debug.Assert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer.");
_capacity = initialCount;
}
~GPUScene()
{
Dispose();
}
// NOTE: This is not thread safe.
public void ResizeIfNeeded(ICommandBuffer cmd)
{
if (_requiredResize == 0)
{
return;
}
var newCapacity = _capacity * 2;
newCapacity = Math.Max(newCapacity, _capacity + _requiredResize);
var newBufferDesc = new BufferDesc
{
Size = (ulong)newCapacity * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
var newBuffer = _resourceAllocator.CreateBuffer(in newBufferDesc, "SceneBuffer_Resized");
Debug.Assert(newBuffer.IsValid);
// Copy existing data to the new buffer
cmd.CopyBuffer(newBuffer, _sceneBuffer, 0, 0, (ulong)_instanceCount * (ulong)sizeof(InstanceData));
// Replace old buffer with the new one
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());
_sceneBuffer = newBuffer;
_capacity = newCapacity;
_requiredResize = 0;
}
public uint AddInstance()
{
if (Volatile.Read(ref _instanceCount) >= _capacity)
{
Interlocked.Increment(ref _requiredResize);
}
var index = Interlocked.Increment(ref _instanceCount);
return index;
}
public uint RemoveInstance(uint index)
{
if (index < 0 || index >= _capacity)
{
return ~0u;
}
// Return the last index. We will swap the last instance data with the removed index on gpu to keep the buffer compact.
var last = Interlocked.Decrement(ref _instanceCount);
return last;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,40 @@
using Ghost.Graphics;
using Ghost.Graphics.Core;
namespace Ghost.Engine.RenderPipeline;
internal class GhostRenderPipeline : IRenderPipeline
{
private readonly RenderSystem _renderSystem;
private readonly GPUScene _gpuScene;
public GPUScene GPUScene => _gpuScene;
public GhostRenderPipeline(RenderSystem renderSystem)
{
_renderSystem = renderSystem;
_gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now
}
public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload)
{
var ghostPayload = (GhostRenderPayload)payload;
var resourceManager = _renderSystem.ResourceManager;
var resourceDatabase = _renderSystem.GraphicsEngine.ResourceDatabase;
foreach (ref readonly var request in ghostPayload.RenderRequests)
{
if (!RenderPipelineUtility.GetViewAndProjectionMatrices(_renderSystem, in request, out var view, out var projection, out var screenSize))
{
continue;
}
}
}
public void Dispose()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,93 @@
using Ghost.Engine.Components;
using Ghost.Graphics;
using Ghost.Graphics.Core;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
using System.Collections.Concurrent;
namespace Ghost.Engine.RenderPipeline;
internal sealed class GhostRenderPayload : IRenderPayload
{
public struct AddInstanceRequest
{
public MeshInstance meshInstance;
public float4x4 localToWorld;
public uint instanceId;
}
public struct RemoveInstanceRequest
{
public uint instanceId;
public uint swapWithInstanceId;
}
private readonly GhostRenderPipeline _renderPipeline;
private UnsafeList<RenderRequest> _renderRequests;
// TODO: Consider using a more efficient data structure for these queues, such as a lock-free ring buffer or a custom concurrent queue implementation.
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest;
public GhostRenderPayload(GhostRenderPipeline renderPipeline)
{
_renderPipeline = renderPipeline;
_renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
_addRequest = new ConcurrentQueue<AddInstanceRequest>();
_removeRequest = new ConcurrentQueue<RemoveInstanceRequest>();
}
// NOTE: This is not thread safe.
public void AddRenderRequest(ref readonly RenderRequest renderRequest)
{
_renderRequests.Add(renderRequest);
}
public uint AddInstance(float4x4 ltw, ref readonly MeshInstance meshInstance)
{
var index = _renderPipeline.GPUScene.AddInstance();
_addRequest.Enqueue(new AddInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance });
return index;
}
public void RemoveInstance(uint instanceId)
{
var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId);
if (swapWithInstanceId != ~0u)
{
_removeRequest.Enqueue(new RemoveInstanceRequest { instanceId = instanceId, swapWithInstanceId = swapWithInstanceId });
}
}
public void Reset()
{
_renderRequests.Clear();
_addRequest.Clear();
_removeRequest.Clear();
}
public void Dispose()
{
_renderRequests.Dispose();
}
}
internal class GhostRenderPipelineSettings : IRenderPipelineSettings
{
public IRenderPipeline CreatePipeline(RenderSystem renderSystem)
{
return new GhostRenderPipeline(renderSystem);
}
public IRenderPayload CreatePayload(RenderSystem renderSystem, IRenderPipeline _renderPipeline)
{
return new GhostRenderPayload((GhostRenderPipeline)_renderPipeline);
}
}

View File

@@ -0,0 +1,52 @@
using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Engine.RenderPipeline;
using Ghost.Entities;
using Ghost.Graphics;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[RenderPipelineSystem<GhostRenderPipelineSettings>]
internal class AddGPUInstanceSystem : SystemBase
{
private RenderSystem _renderSystem = null!;
private Identifier<EntityQuery> _meshInstanceQueryID;
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
_meshInstanceQueryID = QueryBuilder.Create()
.WithAll<MeshInstance, LocalToWorld>()
.WithAbsent<GPUInstanceRef>()
.Build(systemAPI.World, true);
RequireQueryForUpdate(_meshInstanceQueryID);
}
protected override void OnUpdate(ref readonly SystemAPI systemAPI)
{
var payload = (GhostRenderPayload)_renderSystem.GetCurrentFramePayload();
ref var meshInstanceQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshInstanceQueryID);
foreach (var chunk in meshInstanceQuery.GetChunkIterator())
{
var meshInstances = chunk.GetComponentData<MeshInstance>();
var localToWorlds = chunk.GetComponentData<LocalToWorld>();
var entities = chunk.GetEntities();
for (var i = 0; i < chunk.EntityCount; i++)
{
ref readonly var meshInstance = ref meshInstances.GetElementUnsafe(i);
var localToWorld = localToWorlds.GetElementUnsafe(i);
var entity = entities.GetElementUnsafe(i);
var index = payload.AddInstance(localToWorld.matrix, in meshInstance);
systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index });
}
}
}
}

View File

@@ -0,0 +1,50 @@
using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Engine.RenderPipeline;
using Ghost.Entities;
using Ghost.Graphics;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[RenderPipelineSystem<GhostRenderPipelineSettings>]
internal class RemoveGPUInstanceSystem : SystemBase
{
private RenderSystem _renderSystem = null!;
private Identifier<EntityQuery> _gpuInstanceQueryID;
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
_gpuInstanceQueryID = QueryBuilder.Create()
.WithAll<GPUInstanceRef>()
.WithAbsent<MeshInstance>()
.Build(systemAPI.World, true);
RequireQueryForUpdate(_gpuInstanceQueryID);
}
protected override void OnUpdate(ref readonly SystemAPI systemAPI)
{
var payload = (GhostRenderPayload)_renderSystem.GetCurrentFramePayload();
ref var gpuInstanceQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_gpuInstanceQueryID);
foreach (var chunk in gpuInstanceQuery.GetChunkIterator())
{
var gpuInstanceRefs = chunk.GetComponentData<GPUInstanceRef>();
var entities = chunk.GetEntities();
for (var i = 0; i < chunk.EntityCount; i++)
{
var gpuInstance = gpuInstanceRefs.GetElementUnsafe(i);
var entity = entities.GetElementUnsafe(i);
payload.RemoveInstance(gpuInstance.gpuSceneIndex);
systemAPI.World.EntityCommandBuffer.RemoveComponent<GPUInstanceRef>(entity);
}
}
}
}

View File

@@ -1,5 +1,5 @@
using Ghost.Entities;
using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics;
namespace Ghost.Engine.Systems;
@@ -17,20 +17,20 @@ public class RenderPipelineSystemAttribute<T> : RenderPipelineSystemAttribute
public static class RenderPipelineSystemRegistry
{
private static readonly Dictionary<Type, List<Func<ISystem>>> s_renderPipelineSystems = new();
private static readonly Dictionary<nint, List<Func<ISystem>>> s_renderPipelineSystems = new();
public static void RegisterRenderPipelineSystem(Type settingsType, Func<ISystem> systemFactory)
public static void RegisterRenderPipelineSystem(nint settingsType, Func<ISystem> systemFactory)
{
if (!s_renderPipelineSystems.TryGetValue(settingsType, out var systems))
{
systems = new List<Func<ISystem>>();
systems = new List<Func<ISystem>>(4);
s_renderPipelineSystems[settingsType] = systems;
}
systems.Add(systemFactory);
}
internal static IEnumerable<Func<ISystem>> GetRenderPipelineSystems(Type settingsType)
internal static IEnumerable<Func<ISystem>> GetRenderPipelineSystems(nint settingsType)
{
if (s_renderPipelineSystems.TryGetValue(settingsType, out var systems))
{

View File

@@ -1,4 +1,5 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Entities;
@@ -24,7 +25,7 @@ public interface ISystem
public abstract class SystemBase : ISystem
{
private List<int>? _requiredQueries;
private UnsafeList<int> _requiredQueries;
public World World
{
@@ -38,13 +39,14 @@ public abstract class SystemBase : ISystem
private bool ShouldUpdate()
{
if (_requiredQueries == null || _requiredQueries.Count == 0)
if (!_requiredQueries.IsCreated || _requiredQueries.Count == 0)
{
return true;
}
foreach (var queryID in _requiredQueries)
for (var i = 0; i < _requiredQueries.Count; i++)
{
var queryID = _requiredQueries[i];
ref var query = ref World.ComponentManager.GetEntityQueryReference(new Identifier<EntityQuery>(queryID));
if (query.CalculateEntityCount() == 0)
{
@@ -57,7 +59,11 @@ public abstract class SystemBase : ISystem
protected void RequireQueryForUpdate(Identifier<EntityQuery> queryID)
{
_requiredQueries ??= new List<int>(4);
if (!_requiredQueries.IsCreated)
{
_requiredQueries = new UnsafeList<int>(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
}
_requiredQueries.Add(queryID.Value);
}
@@ -89,6 +95,7 @@ public abstract class SystemBase : ISystem
void ISystem.Cleanup(ref readonly SystemAPI systemAPI)
{
_requiredQueries.Dispose();
OnCleanup(in systemAPI);
}

View File

@@ -60,30 +60,32 @@ namespace Ghost.Generator
return;
}
var name = $"g_component_registration";
var sb = new StringBuilder();
sb.Append($@"
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static class {name}
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
public static void RegisterIComponentTypes()
{{");
foreach (var symbol in components.Distinct(SymbolEqualityComparer.Default))
{
if (symbol is null) continue;
if (symbol is null)
{
continue;
}
sb.Append($@"
global::Ghost.Entities.ComponentRegistry.GetOrRegisterComponentID<{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
sb.AppendLine($@" global::Ghost.Entities.ComponentRegistry.GetOrRegisterComponentID<{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
}
sb.Append(@"
}
}");
var typeName = $"g_component_registration";
var code = $@"// <auto-generated/>
context.AddSource($"{name}.gen.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {typeName}
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void RegisterIComponentTypes()
{{
{sb}
}}
}}";
context.AddSource($"{typeName}.gen.cs", code);
}
}
}

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

View File

@@ -0,0 +1,215 @@
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Ghost.Generator
{
[Generator]
internal class ShaderPropertiesGenerator : IIncrementalGenerator
{
private class ShaderStructInfo
{
public string ShaderName
{
set; get;
}
public string Name
{
get; set;
}
public INamedTypeSymbol TypeSymbol
{
get; set;
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var shaderProperties = context.SyntaxProvider
.ForAttributeWithMetadataName(
"Ghost.Core.Graphics.GenerateShaderPropertyAttribute",
(n, ct) => n is Microsoft.CodeAnalysis.CSharp.Syntax.StructDeclarationSyntax,
(ctx, ct) =>
{
var structSymbol = (INamedTypeSymbol)ctx.TargetSymbol;
var attributeData = ctx.Attributes.FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Ghost.Core.Graphics.GenerateShaderPropertyAttribute");
if (attributeData == null)
{
return null;
}
return new ShaderStructInfo
{
ShaderName = attributeData.ConstructorArguments[0].Value as string,
Name = attributeData.ConstructorArguments[1].Value as string ?? structSymbol.Name,
TypeSymbol = structSymbol
};
})
.Where(x => x != null)
.Collect();
context.RegisterSourceOutput(shaderProperties, GenerateHlslPropertyStruct);
}
private void GenerateHlslPropertyStruct(SourceProductionContext context, ImmutableArray<ShaderStructInfo> array)
{
if (array.IsDefaultOrEmpty)
{
return;
}
var codeBuilder = new StringBuilder();
var registerBuilder = new StringBuilder();
foreach (var info in array)
{
if (string.IsNullOrEmpty(info.ShaderName))
{
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
"GSHADER001",
"Invalid shader name",
$"Shader name for struct '{info.TypeSymbol.Name}' cannot be null or empty.",
"ShaderGeneration",
DiagnosticSeverity.Error,
isEnabledByDefault: true), info.TypeSymbol.Locations.FirstOrDefault()));
continue;
}
if (!info.TypeSymbol.IsUnmanagedType)
{
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
"GSHADER002",
"Unsupported struct type",
$"Struct '{info.TypeSymbol.Name}' is not an unmanaged type and cannot be used for HLSL generation.",
"ShaderGeneration",
DiagnosticSeverity.Error,
isEnabledByDefault: true), info.TypeSymbol.Locations.FirstOrDefault()));
continue;
}
var definedSymbol = $"__{info.Name.ToUpper()}_G_HLSL";
var fields = info.TypeSymbol.GetMembers().OfType<IFieldSymbol>();
foreach (var field in fields)
{
if (field.IsStatic || field.IsConst)
{
continue;
}
var hlslTypeAttribute = field.GetAttributes().FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Ghost.Engine.Utilities.GenerateAsHLSLTypeAttribute");
var hlslType = string.Empty;
if (hlslTypeAttribute == null)
{
switch (field.Type.SpecialType)
{
case SpecialType.System_Single:
hlslType = "float";
break;
case SpecialType.System_Int32:
hlslType = "int";
break;
case SpecialType.System_UInt32:
hlslType = "uint";
break;
default:
var typeName = field.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
switch (typeName)
{
case "global::System.Numerics.Vector2":
hlslType = "float2";
break;
case "global::System.Numerics.Vector3":
hlslType = "float3";
break;
case "global::System.Numerics.Vector4":
hlslType = "float4";
break;
case "global::System.Numerics.Matrix3x3":
hlslType = "float3x3";
break;
case "global::System.Numerics.Matrix4x4":
hlslType = "float4x4";
break;
case "global::System.Numerics.Quaternion" or "global::Misaki.HighPerformance.Mathematics.quaternion":
hlslType = "float4";
break;
case var _ when typeName.StartsWith("global::Misaki.HighPerformance.Mathematics."):
hlslType = typeName.Substring("global::Misaki.HighPerformance.Mathematics.".Length);
break;
default:
break;
}
break;
}
}
else
{
hlslType = hlslTypeAttribute.ConstructorArguments[0].Value as string;
}
if (string.IsNullOrEmpty(hlslType))
{
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(
"GSHADER003",
"Unsupported field type",
$"Field '{field.Name}' in struct '{info.TypeSymbol.Name}' has unsupported type '{field.Type.ToDisplayString()}' for HLSL generation.",
"ShaderGeneration",
DiagnosticSeverity.Error,
isEnabledByDefault: true), field.Locations.FirstOrDefault()));
continue;
}
codeBuilder.AppendLine($" {hlslType} {field.Name};");
}
var code = $@"// <auto-generated/>
namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
{{
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
{{
public const string HLSL_SOURCE = @""
# ifndef {definedSymbol}
# define {definedSymbol}
struct {info.Name}
{{
{codeBuilder}
}};
# endif // {definedSymbol}"";
}}
}}";
context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code);
codeBuilder.Clear();
var typeFullName = info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
registerBuilder.AppendLine($@" global::Ghost.Core.Graphics.ShaderPropertiesRegistry.Register(""{info.ShaderName}"", {typeFullName}.HLSL_SOURCE, (uint)sizeof({typeFullName}));");
}
var registerTypeName = "g_shaderproperty_registeration";
var registerCode = $@"// <auto-generated/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName}
{{
#if DEBUG || GHOST_EDITOR
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal unsafe static void RegisterShaderProperties()
{{
{registerBuilder}
}}
#endif
}}";
context.AddSource($"{registerTypeName}.gen.cs", registerCode);
}
}
}

View File

@@ -442,7 +442,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
null);
}
public void BeginRenderPass(ReadOnlySpan<PassRenderTargetDesc> rtDescs, PassDepthStencilDesc depthDesc, bool allowUAVWrites = false)
public void BeginRenderPass(ReadOnlySpan<PassRenderTargetDesc> rtDescs, ref readonly PassDepthStencilDesc depthDesc, bool allowUAVWrites = false)
{
AssertNotDisposed();
ThrowIfNotRecording();
@@ -786,6 +786,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
pNativeObject->DrawInstanced(vertexCount, instanceCount, startVertex, startInstance);
}
public void SetProgram(ref readonly SetProgramDesc desc)
{
// TODO
}
public void DrawIndexed(uint indexCount, uint instanceCount = 1, uint startIndex = 0, int baseVertex = 0, uint startInstance = 0)
{
AssertNotDisposed();
@@ -836,7 +841,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
throw new NotImplementedException();
}
public void DispatchGraph()
public void DispatchGraph(ref readonly DispatchGraphDesc desc)
{
throw new NotImplementedException();
}
@@ -859,7 +864,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var resource = _resourceDatabase.GetResource(argumentBuffer.AsResource());
var countResource = _resourceDatabase.GetResource(countBuffer.AsResource());
pNativeObject->ExecuteIndirect(((D3D12CommandSignature)commandSignature).NativeObject, 0,
pNativeObject->ExecuteIndirect((ID3D12CommandSignature*)commandSignature.NativePointer, 0,
resource, argumentOffset, countResource, countBufferOffset);
}

View File

@@ -86,6 +86,8 @@ internal unsafe class D3D12CommandSignature : D3D12Object<ID3D12CommandSignature
return pCommandSignature;
}
public IntPtr NativePointer => (IntPtr)NativeObject.Get();
public D3D12CommandSignature(D3D12RenderDevice device, D3D12PipelineLibrary pipelineLibrary, ref readonly CommandSignatureDesc desc, Key128<GraphicsPipeline> pipelineKey)
: base(CreateCommandSignature(device, pipelineLibrary, in desc))
{

View File

@@ -10,6 +10,7 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using TerraFX.Interop.DirectX;
namespace Ghost.Graphics.D3D12;

View File

@@ -174,7 +174,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
return D3D12Utility.D3D12_DEPTH_STENCIL_DESC_CREATE(depthEnabled, writeEnabled, cmp);
}
public Result<Key128<GraphicsPipeline>> CompilePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled)
public Result<Key128<GraphicsPipeline>> CreatePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled)
{
static Result ValidatePassReflectionData(ref readonly GraphicsCompiledResult compiled)
{

View File

@@ -96,8 +96,6 @@ internal unsafe class D3D12RenderDevice : D3D12Object<ID3D12Device14>, IRenderDe
private FeatureSupport GetFeatureSupport()
{
ThrowIfDisposed();
var support = FeatureSupport.None;
D3D12_FEATURE_DATA_D3D12_OPTIONS options = default;
@@ -149,7 +147,7 @@ internal unsafe class D3D12RenderDevice : D3D12Object<ID3D12Device14>, IRenderDe
D3D12_FEATURE_DATA_D3D12_OPTIONS21 options9 = default;
if (pNativeObject->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS21, &options9, (uint)sizeof(D3D12_FEATURE_DATA_D3D12_OPTIONS8)).SUCCEEDED)
{
if (options9.WorkGraphsTier != D3D12_WORK_GRAPHS_TIER.D3D12_WORK_GRAPHS_TIER_NOT_SUPPORTED)
if (options9.WorkGraphsTier != D3D12_WORK_GRAPHS_TIER_NOT_SUPPORTED)
{
support |= FeatureSupport.WorkGraphs;
}

View File

@@ -0,0 +1,15 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using TerraFX.Interop.DirectX;
namespace Ghost.Graphics.D3D12;
internal class D3D12WorkGraphPipeline : IWorkGraphPipeline
{
private UniquePtr<ID3D12StateObject> _stateObject;
private D3D12_PROGRAM_IDENTIFIER _programIdentifier;
private D3D12_WORK_GRAPH_MEMORY_REQUIREMENTS _memoryRequirements;
private Handle<GPUResource> _backingBuffer;
}

View File

@@ -6,7 +6,9 @@ using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Utilities;
using System.IO.Hashing;
using System.Runtime.InteropServices;
using System.Text;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@@ -88,7 +90,7 @@ internal sealed partial class DXCShaderCompiler
return argsArray;
}
private static Result<string, Error> GetFinalShaderCode(string shaderPath, ReadOnlySpan<string> includes, string? injectedCode)
private static Result<string, Error> BuildFinalShaderCode(string shaderPath, ReadOnlySpan<string> includes, string? injectedCode)
{
string shaderCode;
if (shaderPath == "hlsl_block")
@@ -110,7 +112,7 @@ internal sealed partial class DXCShaderCompiler
shaderCode = File.ReadAllText(shaderPath);
}
var sb = new System.Text.StringBuilder();
var sb = new StringBuilder();
foreach (var includePath in includes)
{
sb.AppendLine($"#include \"{includePath}\"");
@@ -118,13 +120,13 @@ internal sealed partial class DXCShaderCompiler
if (!string.IsNullOrEmpty(injectedCode))
{
sb.AppendLine($"#line 1 \"hlsl_block\"");
sb.AppendLine($"#line 0 \"injected_code\"");
sb.AppendLine(injectedCode);
}
if (!string.IsNullOrEmpty(shaderCode))
{
sb.AppendLine($"#line 1 \"{shaderPath}\"");
sb.AppendLine($"#line 0 \"{shaderPath}\"");
sb.AppendLine(shaderCode);
}
@@ -155,7 +157,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
private UniquePtr<IDxcUtils> _utils;
// NOTE: This is just a temporary cache for compiled shader code. We will implement a proper disk cache later.
// TODO: This should be shader variant specific cache instead of pass specific.
private readonly Dictionary<Key64<ShaderVariant>, GraphicsCompiledResult> _compiledResults;
private readonly Dictionary<ulong, GraphicsCompiledResult> _compiledResults;
private bool _disposed;
@@ -173,7 +175,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
_compiler.Attach(pCompiler);
_utils.Attach(pUtils);
_compiledResults = new Dictionary<Key64<ShaderVariant>, GraphicsCompiledResult>();
_compiledResults = new Dictionary<ulong, GraphicsCompiledResult>();
}
~DXCShaderCompiler()
@@ -285,7 +287,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
ThrowIfFailed(_utils.Get()->CreateDefaultIncludeHandler(includeHandler.GetAddressOf()));
var finalShaderCodeResult = GetFinalShaderCode(config.shaderPath, config.includes, config.injectedCode);
var finalShaderCodeResult = BuildFinalShaderCode(config.shaderPath, config.includes, config.injectedCode);
if (finalShaderCodeResult.IsFailure)
{
return Result.Failure(finalShaderCodeResult.Error);
@@ -364,6 +366,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
{
bytecode = bytecode,
reflectionData = reflectionData,
hashCode = XxHash64.HashToUInt64(bytecode)
};
}
finally
@@ -377,7 +380,7 @@ 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(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key)
public Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, ref readonly LocalKeywordSet keywords)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -386,6 +389,13 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
descriptor.defines?.CopyTo(fullDefines);
additionalConfig.defines.CopyTo(fullDefines.AsSpan(defineCountInDescriptor));
var injectedCodeBuilder = new StringBuilder();
injectedCodeBuilder.AppendLine(descriptor.shader.propertiesCode);
injectedCodeBuilder.AppendLine(descriptor.hlsl);
injectedCodeBuilder.AppendLine(additionalConfig.injectedCode);
var injectedCode = injectedCodeBuilder.ToString();
ShaderCompileResult tsResult = default;
var tsEntry = descriptor.taskShader;
if (tsEntry.IsCreated)
@@ -396,7 +406,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
includes = descriptor.includes.AsSpan(),
shaderPath = tsEntry.shader,
entryPoint = tsEntry.entry,
injectedCode = descriptor.hlsl + additionalConfig.injectedCode,
injectedCode = injectedCode,
stage = ShaderStage.TaskShader,
tier = additionalConfig.tier,
optimizeLevel = additionalConfig.optimizeLevel,
@@ -422,7 +432,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
includes = descriptor.includes.AsSpan(),
shaderPath = msEntry.shader,
entryPoint = msEntry.entry,
injectedCode = descriptor.hlsl + additionalConfig.injectedCode,
injectedCode = injectedCode,
stage = ShaderStage.MeshShader,
tier = additionalConfig.tier,
optimizeLevel = additionalConfig.optimizeLevel,
@@ -452,7 +462,7 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
includes = descriptor.includes.AsSpan(),
shaderPath = psEntry.shader,
entryPoint = psEntry.entry,
injectedCode = descriptor.hlsl + additionalConfig.injectedCode,
injectedCode = injectedCode,
stage = ShaderStage.PixelShader,
tier = additionalConfig.tier,
optimizeLevel = additionalConfig.optimizeLevel,
@@ -479,7 +489,10 @@ internal sealed unsafe partial class DXCShaderCompiler : IShaderCompiler
psResult = psResult,
};
_compiledResults[key] = compiled;
var passHash = RHIUtility.CreateShaderPassKey(descriptor.identifier, compiled.HashCode);
var variantHash = RHIUtility.CreateShaderVariantKey(passHash, in keywords);
_compiledResults[variantHash] = compiled;
return compiled;
}

View File

@@ -0,0 +1,198 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using TerraFX.Interop.DirectX;
using static TerraFX.Aliases.D3D12_Alias;
namespace Ghost.Graphics.D3D12.Experiment;
internal unsafe class D3D12SimpleWorkGraph : IDisposable
{
private readonly D3D12ResourceDatabase _resourceDatabase;
private readonly D3D12ResourceAllocator _resourceAllocator;
private ID3D12StateObject* _stateObject;
private ID3D12StateObjectProperties1* _stateObjectProperties;
private ID3D12WorkGraphProperties* _workGraphProperties;
// Contains Opaque ID data used by the Runtime to identify the WG
private D3D12_PROGRAM_IDENTIFIER _programIdentifier;
// Auto-allocated Backing Memory for WG Queues and Nodes state
private Handle<GPUBuffer> _backingMemory;
private D3D12_GPU_VIRTUAL_ADDRESS_RANGE _backingMemoryRange;
// First time dispatching a Work Graph requires INITIALIZE flag
private bool _isInitialized;
public D3D12SimpleWorkGraph(
D3D12RenderDevice device,
D3D12ResourceDatabase resourceDatabase,
D3D12ResourceAllocator resourceAllocator,
ID3D12RootSignature* globalRootSignature,
ReadOnlySpan<byte> bytecode,
string programName)
{
_resourceDatabase = resourceDatabase;
_resourceAllocator = resourceAllocator;
var subobjects = stackalloc D3D12_STATE_SUBOBJECT[3];
fixed (byte* pBytecode = bytecode)
fixed (char* pProgramName = programName)
{
// 1. DXIL Library Subobject
var dxilLibDesc = new D3D12_DXIL_LIBRARY_DESC
{
DXILLibrary = new D3D12_SHADER_BYTECODE
{
pShaderBytecode = pBytecode,
BytecodeLength = (nuint)bytecode.Length
},
NumExports = 0,
pExports = null
};
subobjects[0].Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY;
subobjects[0].pDesc = &dxilLibDesc;
// 2. Global Root Signature Subobject
// This is shared among all Nodes. Perfect for passing your bindless Descriptor Heaps
// and root constants (which hold your Buffer IDs and Sampler Indices).
var rootSigDesc = new D3D12_GLOBAL_ROOT_SIGNATURE
{
pGlobalRootSignature = globalRootSignature
};
subobjects[1].Type = D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE;
subobjects[1].pDesc = &rootSigDesc;
// 3. Work Graph Properties Subobject
var workGraphDesc = new D3D12_WORK_GRAPH_DESC
{
ProgramName = pProgramName,
Flags = D3D12_WORK_GRAPH_FLAG_INCLUDE_ALL_AVAILABLE_NODES
};
subobjects[2].Type = D3D12_STATE_SUBOBJECT_TYPE_WORK_GRAPH;
subobjects[2].pDesc = &workGraphDesc;
// Tie them to a state object descriptor
var stateObjectDesc = new D3D12_STATE_OBJECT_DESC
{
Type = D3D12_STATE_OBJECT_TYPE_EXECUTABLE,
NumSubobjects = 3,
pSubobjects = subobjects
};
// Build State Object!
var pDevice = device.NativeObject.Get();
ID3D12StateObject* pStateObject;
ThrowIfFailed(pDevice->CreateStateObject(&stateObjectDesc, __uuidof<ID3D12StateObject>(), (void**)&pStateObject));
_stateObject = pStateObject;
ID3D12StateObjectProperties1* pStateObjectProps;
ThrowIfFailed(pStateObject->QueryInterface(__uuidof<ID3D12StateObjectProperties1>(), (void**)&pStateObjectProps));
_stateObjectProperties = pStateObjectProps;
ID3D12WorkGraphProperties* pWorkGraphProps;
ThrowIfFailed(pStateObject->QueryInterface(__uuidof<ID3D12WorkGraphProperties>(), (void**)&pWorkGraphProps));
_workGraphProperties = pWorkGraphProps;
// Extract Program Identifier - we'll pass this via CommandList later.
_programIdentifier = pStateObjectProps->GetProgramIdentifier(pProgramName);
var workGraphIndex = pWorkGraphProps->GetWorkGraphIndex(pProgramName);
// Compute Backing Memory needed
D3D12_WORK_GRAPH_MEMORY_REQUIREMENTS memReqs;
pWorkGraphProps->GetWorkGraphMemoryRequirements(workGraphIndex, &memReqs);
// Allocate Backing Memory Buffer using your custom allocator!
var backingMemoryDesc = new BufferDesc
{
Size = memReqs.MaxSizeInBytes,
Usage = BufferUsage.UnorderedAccess, // Backing memory MUST be an Unordered Access resource.
HeapType = HeapType.Default
};
_backingMemory = _resourceAllocator.CreateBuffer(in backingMemoryDesc, $"{programName}_BackingMemory");
var backingBufferResource = _resourceDatabase.GetResource(_backingMemory.AsResource());
_backingMemoryRange = new D3D12_GPU_VIRTUAL_ADDRESS_RANGE
{
StartAddress = backingBufferResource.Get()->GetGPUVirtualAddress(),
SizeInBytes = memReqs.MaxSizeInBytes
};
}
}
// You invoke this from outside passing records via CPU array to test immediately.
public void DispatchGraph<TRecord>(D3D12CommandBuffer cmdBuffer, uint entryPointIndex, ReadOnlySpan<TRecord> records)
where TRecord : unmanaged
{
var pCmdList = cmdBuffer.NativeObject.Get();
// 1. Prepare Program descriptors
var setProgramDesc = new D3D12_SET_PROGRAM_DESC
{
Type = D3D12_PROGRAM_TYPE_WORK_GRAPH,
WorkGraph = new D3D12_SET_WORK_GRAPH_DESC
{
ProgramIdentifier = _programIdentifier,
Flags = _isInitialized ? D3D12_SET_WORK_GRAPH_FLAG_NONE : D3D12_SET_WORK_GRAPH_FLAG_INITIALIZE,
BackingMemory = _backingMemoryRange,
NodeLocalRootArgumentsTable = default // Ignored since we are pure bindless setup!
}
};
_isInitialized = true;
pCmdList->SetProgram(&setProgramDesc);
// 2. Execute! Map CPU inputs natively directly.
fixed (TRecord* pRecords = records)
{
var cpuInput = new D3D12_NODE_CPU_INPUT
{
EntrypointIndex = entryPointIndex,
NumRecords = (uint)records.Length,
pRecords = pRecords,
RecordStrideInBytes = (uint)sizeof(TRecord)
};
var dispatchDesc = new D3D12_DISPATCH_GRAPH_DESC
{
Mode = D3D12_DISPATCH_MODE_NODE_CPU_INPUT,
NodeCPUInput = cpuInput
};
pCmdList->DispatchGraph(&dispatchDesc);
}
}
public void Dispose()
{
if (_backingMemory.IsValid)
{
_resourceDatabase.ReleaseResourceImmediately(_backingMemory.AsResource());
_backingMemory = Handle<GPUBuffer>.Invalid;
}
if (_workGraphProperties != null)
{
_workGraphProperties->Release();
_workGraphProperties = null;
}
if (_stateObjectProperties != null)
{
_stateObjectProperties->Release();
_stateObjectProperties = null;
}
if (_stateObject != null)
{
_stateObject->Release();
_stateObject = null;
}
}
}

View File

@@ -0,0 +1,63 @@
#ifndef SIMPLE_WORKGRAPH_HLSL
#define SIMPLE_WORKGRAPH_HLSL
#include "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl"
#include "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Random.hlsl"
// The record types
struct InitialRecord
{
uint seed;
};
struct IncrementRecord
{
uint incrementValue;
};
// Bindless root constants
cbuffer RootConstants : register(b0)
{
uint counterBufferId;
uint threshold;
};
// Node: MainNode
// Entry point for the Work Graph. Spawns threads matching the record count.
[Shader("node")]
[NodeIsProgramEntry]
[NodeLaunch("thread")]
void MainNode(
uint dispatchThreadID : SV_DispatchThreadID,
in InitialRecord record,
[MaxRecords(1)] NodeOutput<IncrementRecord> IncrementNode) // Target the IncrementNode
{
// Generate a random number from JenkinsHash
uint randValue = JenkinsHash(record.seed ^ dispatchThreadID);
// If the random number exceeds our threshold, launch the next node
if (randValue > threshold)
{
ThreadNodeOutputRecords<IncrementRecord> outRecord = IncrementNode.GetThreadNodeOutputRecords(1);
outRecord.Get().incrementValue = 1;
outRecord.OutputComplete();
}
}
// Node: IncrementNode
// Triggered dynamically from MainNode
[Shader("node")]
[NodeLaunch("thread")]
void IncrementNode(
uint dispatchThreadID : SV_DispatchThreadID,
in IncrementRecord record)
{
// Retrieve our RWByteAddressBuffer generically through SM6.6 bindless descriptor heap
RWByteAddressBuffer counterBuffer = ResourceDescriptorHeap[counterBufferId];
// Thread-safe atomic increment of our counter across all dispatched records
uint originalValue;
counterBuffer.InterlockedAdd(0, record.incrementValue, originalValue);
}
#endif

View File

@@ -473,7 +473,6 @@ public struct PassDepthStencilDesc
}
}
public struct TextureSubresource
{
public uint MipLevel
@@ -1135,6 +1134,131 @@ public ref struct CommandSignatureDesc
}
}
public unsafe struct ProgramIdentifier
{
public void* pIdentifier;
}
public unsafe struct NodeCPUInput
{
public uint entryPointIndex;
public uint numRecords;
public void* pRecords;
public ulong recordStrideInBytes;
}
public unsafe struct MultiNodeCPUInput
{
public uint numNodeInputs;
public NodeCPUInput* pNodeInputs;
public ulong nodeInputStrideInBytes;
}
public struct DispatchGraphDesc
{
[StructLayout(LayoutKind.Explicit)]
private struct __union
{
[FieldOffset(0)]
public NodeCPUInput nodeCPUInput;
[FieldOffset(0)]
public ulong nodeGPUInput;
[FieldOffset(0)]
public MultiNodeCPUInput multiNodeCPUInput;
[FieldOffset(0)]
public ulong multiNodeGPUInput;
}
private __union _input;
public GraphDispatchMode DispatchMode
{
get; set;
}
[UnscopedRef]
public ref NodeCPUInput NodeCPUInput => ref _input.nodeCPUInput;
[UnscopedRef]
public ref ulong NodeGPUInput => ref _input.nodeGPUInput;
[UnscopedRef]
public ref MultiNodeCPUInput MultiNodeCPUInput => ref _input.multiNodeCPUInput;
[UnscopedRef]
public ref ulong MultiNodeGPUInput => ref _input.multiNodeGPUInput;
}
public struct WorkGraphMemoryRequirements
{
public ulong MinSizeInBytes
{
get; set;
}
public ulong MaxSizeInBytes
{
get; set;
}
public uint SizeGranularityInBytes
{
get; set;
}
}
public struct WorkGraphSubObjectDesc
{
public string ProgramName
{
get; set;
}
public bool IncludeAllAvailableNodes
{
get; set;
}
}
public struct SetProgramDesc
{
public ProgramIdentifier ProgramIdentifier
{
get; set;
}
public SetWorkGraphFlags Flags
{
get; set;
}
// D3D12_GPU_VIRTUAL_ADDRESS_RANGE
public ulong BackingMemoryAddress
{
get; set;
}
public ulong BackingMemorySize
{
get; set;
}
// D3D12_GPU_VIRTUAL_ADDRESS_RANGE_AND_STRIDE
public ulong NodeLocalRootArgumentsTableAddress
{
get; set;
}
public ulong NodeLocalRootArgumentsTableSizeInBytes
{
get; set;
}
public ulong NodeLocalRootArgumentsTableStrideInBytes
{
get; set;
}
}
public struct SwapChainDesc
{
public uint Width
@@ -1185,7 +1309,6 @@ public struct SwapChainTarget
get; set;
}
public static SwapChainTarget FromWindowHandle(nint hwnd)
{
return new SwapChainTarget
@@ -1213,7 +1336,6 @@ public enum SwapChainTargetType
Composition
}
public enum BarrierType
{
Global,
@@ -1503,4 +1625,19 @@ public enum IndirectArgumentType
DispatchRays,
DispatchMesh,
IncrementingConstant,
}
public enum GraphDispatchMode
{
CPUInput,
GPUInput,
MultiCPUInput,
MultiGPUInput
}
[Flags]
public enum SetWorkGraphFlags
{
None,
Initialize
}

View File

@@ -80,7 +80,7 @@ public interface ICommandBuffer : IRHIObject
/// <param name="rtDescs">Render Target descriptions</param>
/// <param name="depthDesc">Depth stencil description</param>
/// <param name="allowUAVWrites">Whether UAV writes are allowed during the render pass</param>
void BeginRenderPass(ReadOnlySpan<PassRenderTargetDesc> rtDescs, PassDepthStencilDesc depthDesc, bool allowUAVWrites = false);
void BeginRenderPass(ReadOnlySpan<PassRenderTargetDesc> rtDescs, ref readonly PassDepthStencilDesc depthDesc, bool allowUAVWrites = false);
/// <summary>
/// Ends the current render pass
@@ -136,6 +136,8 @@ public interface ICommandBuffer : IRHIObject
/// <param name="offsetIn32Bits">The Offset, in 32-bit values, from the start of the root parameter where the constants will be set.</param>
void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan<uint> constantBuffer, uint offsetIn32Bits = 0);
void SetProgram(ref readonly SetProgramDesc desc);
/// <summary>
/// Issues a non-indexed draw call.
/// </summary>
@@ -176,6 +178,8 @@ public interface ICommandBuffer : IRHIObject
/// </summary>
void DispatchRay();
void DispatchGraph(ref readonly DispatchGraphDesc desc);
/// <summary>
/// Executes a sequence of GPU commands indirectly using the specified command signature and argument buffers.
/// </summary>

View File

@@ -1,3 +1,6 @@
namespace Ghost.Graphics.RHI;
public interface ICommandSignature : IRHIObject;
public unsafe interface ICommandSignature : IRHIObject
{
IntPtr NativePointer { get; }
}

View File

@@ -6,5 +6,5 @@ public interface IPipelineLibrary : IDisposable
{
void SaveLibraryToDisk(string filePath);
bool HasPipeline(Key128<GraphicsPipeline> key);
Result<Key128<GraphicsPipeline>> CompilePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled);
Result<Key128<GraphicsPipeline>> CreatePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled);
}

View File

@@ -1,5 +1,6 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
@@ -9,6 +10,7 @@ public struct ShaderCompileResult : IDisposable
{
public UnsafeArray<byte> bytecode;
public ShaderReflectionData reflectionData;
public ulong hashCode;
public readonly bool IsCreated => bytecode.IsCreated;
@@ -20,10 +22,25 @@ public struct ShaderCompileResult : IDisposable
public struct GraphicsCompiledResult : IDisposable
{
private ulong _hashCode;
public ShaderCompileResult tsResult;
public ShaderCompileResult msResult;
public ShaderCompileResult psResult;
public Key64<GraphicsCompiledResult> HashCode
{
get
{
if (_hashCode == 0)
{
_hashCode = Hash.Combine64(tsResult.hashCode, msResult.hashCode, psResult.hashCode);
}
return _hashCode;
}
}
public void Dispose()
{
tsResult.Dispose();
@@ -144,6 +161,6 @@ public readonly struct ShaderReflectionData
public interface IShaderCompiler : IDisposable
{
Result<ShaderCompileResult> Compile(ref readonly ShaderCompilationConfig config, Allocator allocator);
Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key);
Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, ref readonly LocalKeywordSet keywords);
Result<GraphicsCompiledResult, Error> LoadCompiledCache(Key64<ShaderVariant> key);
}

View File

@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Graphics.RHI;
public interface IWorkGraphPipeline
{
}

View File

@@ -146,10 +146,9 @@ public static class RHIUtility
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Key64<ShaderPass> CreateShaderPassKey(string passID)
public static Key64<ShaderPass> CreateShaderPassKey(ulong passID, ulong compiledHash)
{
var passIdSpan = passID.AsSpan();
return new Key64<ShaderPass>(XxHash3.HashToUInt64(MemoryMarshal.AsBytes(passIdSpan)));
return Hash.Combine64(passID, compiledHash);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -157,7 +156,7 @@ public static class RHIUtility
{
var passHash = passKey.Value;
var keywordHash = keywords.GetHash64();
return new Key64<ShaderVariant>(Hash.Hash64(passHash, keywordHash));
return new Key64<ShaderVariant>(Hash.Combine64(passHash, keywordHash));
}
public static unsafe Key128<GraphicsPipeline> CreateGraphicsPipelineKey(Key64<ShaderVariant> shaderVariantKey, PipelineState pipelineState, PassPipelineHash passKey)

View File

@@ -111,17 +111,17 @@ public struct Material : IResourceReleasable
};
}
if (shader.CBufferSize != 0)
if (shader.PropertyBufferSize != 0)
{
var desc = new BufferDesc
{
Size = shader.CBufferSize,
Size = shader.PropertyBufferSize,
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
var buffer = resourceAllocator.CreateBuffer(ref desc, "MaterialCBuffer");
_cBufferCache = new CBufferCache(buffer, shader.CBufferSize);
_cBufferCache = new CBufferCache(buffer, shader.PropertyBufferSize);
}
return Error.None;

View File

@@ -14,18 +14,18 @@ namespace Ghost.Graphics.Core;
[StructLayout(LayoutKind.Sequential)]
public struct Meshlet
{
public SphereBounds boundingSphere; // 16 bytes
public SphereBounds parentBoundingSphere; // 16 bytes
public AABB boundingBox; // 24 bytes
public uint vertexOffset; // offset into meshlet vertex index array
public uint triangleOffset; // offset into packed triangle array
public uint groupIndex; // owning group
public float clusterError; // geometric error of this meshlet/cluster
public float parentError; // geometric refinement error carried into runtime LOD tests
public byte vertexCount; // max 64
public byte triangleCount; // max 124
public byte localMaterialIndex; // mesh-local material slot
public byte lodLevel; // this meshlet's LOD level
public SphereBounds boundingSphere; // 16 bytes
public SphereBounds parentBoundingSphere; // 16 bytes
public AABB boundingBox; // 24 bytes
public uint vertexOffset; // offset into meshlet vertex index array
public uint triangleOffset; // offset into packed triangle array
public uint groupIndex; // owning group
public float clusterError; // geometric error of this meshlet/cluster
public float parentError; // geometric refinement error carried into runtime LOD tests
public byte vertexCount; // max 64
public byte triangleCount; // max 124
public byte localMaterialIndex; // mesh-local material slot
public byte lodLevel; // this meshlet's LOD level
}
[StructLayout(LayoutKind.Sequential)]

View File

@@ -1,5 +1,6 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
@@ -83,19 +84,20 @@ public partial struct Shader : IResourceReleasable
// We can use a int array since the number and index of tags are fixed at compile time.
public readonly int PassCount => _shaderPasses.Count;
public readonly uint CBufferSize => _cbufferSize;
public readonly uint PropertyBufferSize => _cbufferSize;
internal Shader(ShaderDescriptor descriptor)
internal Shader(ShaderDescriptor descriptor, ref readonly GraphicsCompiledResult compiledResult)
{
_cbufferSize = (uint)descriptor.cbufferSize;
_cbufferSize = descriptor.propertyBufferSize;
_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.Length; i++)
{
var pass = descriptor.passes[i];
var passKey = RHIUtility.CreateShaderPassKey(pass.identifier);
ref readonly var pass = ref descriptor.passes[i];
var passKey = RHIUtility.CreateShaderPassKey(pass.identifier, compiledResult.HashCode);
var keywords = default(LocalKeywordSet);
if (pass.keywords.Length > 0)

View File

@@ -1,51 +0,0 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using System.Diagnostics;
namespace Ghost.Graphics;
public unsafe class GPUScene : IDisposable
{
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
private Handle<GPUBuffer> _sceneBuffer;
private bool _disposed;
internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, ulong initialCount)
{
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
var bufferDesc = new BufferDesc
{
Size = initialCount * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
_sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer");
Debug.Assert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer.");
}
~GPUScene()
{
Dispose();
}
public void Dispose()
{
if (_disposed)
{
return;
}
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,131 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics;
public interface IRenderPayload : IDisposable
{
ReadOnlySpan<RenderRequest> RenderRequests { get; }
void AddRenderRequest(ref readonly RenderRequest renderRequest);
void Reset();
}
public interface IRenderPipelineSettings
{
IRenderPipeline CreatePipeline(RenderSystem renderSystem);
IRenderPayload CreatePayload(RenderSystem renderSystem, IRenderPipeline renderPipeline);
}
public interface IRenderPipeline : IDisposable
{
void Render(RenderContext ctx, int frameIndex, IRenderPayload payload);
}
public static class RenderPipelineUtility
{
public static bool GetViewAndProjectionMatrices(RenderSystem renderSystem, ref readonly RenderRequest request, out float4x4 view, out float4x4 projection, out uint2 screenSize)
{
Handle<GPUTexture> rtHandle;
if (request.swapChainIndex < 0)
{
rtHandle = request.colorTarget;
}
else if (renderSystem.SwapChainManager.TryGetSwapChain(request.swapChainIndex, out var swapChain))
{
rtHandle = swapChain.GetCurrentBackBuffer();
}
else
{
view = default;
projection = default;
screenSize = default;
return false;
}
try
{
var rtResult = renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(rtHandle.AsResource());
if (rtResult.IsFailure)
{
view = default;
projection = default;
screenSize = default;
return false;
}
screenSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height);
var aspectScreen = (float)screenSize.x / screenSize.y;
view = math.inverse(request.view.localToWorld);
var vfov = 2.0f * math.atan(request.view.sensorSize.y / (2.0f * request.view.focalLength));
var hfov = 2.0f * math.atan(request.view.sensorSize.x / (2.0f * request.view.focalLength));
var aspectSensor = request.view.sensorSize.x / request.view.sensorSize.y;
float vfovF;
switch (request.view.gateFit)
{
case GateFit.Vertical:
vfovF = vfov;
break;
case GateFit.Horizontal:
// Adjust VFOV so that the sensor width fits the screen width
var horizontalAspectBuffer = math.tan(hfov * 0.5f);
vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen);
break;
case GateFit.Fill:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Vertical;
}
else
{
goto case GateFit.Horizontal;
}
case GateFit.Overscan:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Horizontal;
}
else
{
goto case GateFit.Vertical;
}
default:
vfovF = vfov;
break;
}
var m_11 = 1.0f / math.tan(vfovF * 0.5f);
var m_00 = m_11 / aspectScreen;
var m_22 = request.view.farClipPlane / (request.view.farClipPlane - request.view.nearClipPlane);
var m_23 = -(request.view.farClipPlane * request.view.nearClipPlane) / (request.view.farClipPlane - request.view.nearClipPlane);
projection = new float4x4
(
m_00, 0, 0, 0,
0, m_11, 0, 0,
0, 0, m_22, m_23,
0, 0, 1, 0
);
return true;
}
finally
{
if (request.swapChainIndex >= 0)
{
renderSystem.SwapChainManager.ReleaseSwapChain(request.swapChainIndex);
}
}
}
}

View File

@@ -189,7 +189,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
};
var compiled = compiledCacheResult.Value;
_pipelineLibrary.CompilePSO(in psoDes, in compiled).GetValueOrThrow();
_pipelineLibrary.CreatePSO(in psoDes, in compiled).GetValueOrThrow();
}
_activePerMaterialData = material._cBufferCache.GpuResource;

View File

@@ -150,7 +150,7 @@ internal sealed class RenderGraphExecutor
: AttachmentStoreOp.NoAccess,
};
commandBuffer.BeginRenderPass(new Span<PassRenderTargetDesc>(pPassRTDescs, nativePass.colorAttachmentCount), depthDesc);
commandBuffer.BeginRenderPass(new Span<PassRenderTargetDesc>(pPassRTDescs, nativePass.colorAttachmentCount), in depthDesc);
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{

View File

@@ -1,19 +0,0 @@
using Ghost.Graphics.Core;
namespace Ghost.Graphics.RenderPipeline;
public interface IRenderPayload : IDisposable
{
void Reset();
}
public interface IRenderPipelineSettings
{
IRenderPipeline CreatePipeline(RenderSystem renderSystem);
IRenderPayload CreatePayload(RenderSystem renderSystem);
}
public interface IRenderPipeline : IDisposable
{
void Render(RenderContext ctx, int frameIndex, IRenderPayload payload);
}

View File

@@ -1,7 +1,6 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.D3D12;
using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
using System.Collections.Concurrent;
@@ -130,7 +129,7 @@ public class RenderSystem : IDisposable
_renderPipeline = _renderPipelineSettings.CreatePipeline(this);
for (var i = 0; i < _frameResources.Length; i++)
{
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this);
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this, _renderPipeline);
}
}
}
@@ -193,7 +192,7 @@ public class RenderSystem : IDisposable
_renderPipeline = _renderPipelineSettings.CreatePipeline(this);
for (var i = 0; i < _frameResources.Length; i++)
{
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this);
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this, _renderPipeline);
}
_isRunning = false;

View File

@@ -5,6 +5,7 @@ using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Numerics;
namespace Ghost.Graphics;
@@ -141,11 +142,11 @@ public sealed partial class ResourceManager : IDisposable
/// </summary>
/// <returns>An <see cref="Identifier{Shader}"/> representing the newly created shader.</returns>
/// <param name="descriptor">The viewGroup containing the shader's properties and passes.</param>
public Identifier<Shader> CreateGraphicsShader(ShaderDescriptor descriptor)
public Identifier<Shader> CreateGraphicsShader(ShaderDescriptor descriptor, ref readonly GraphicsCompiledResult compiledResult)
{
Debug.Assert(!_disposed);
var shader = new Shader(descriptor);
var shader = new Shader(descriptor, in compiledResult);
var id = _shaders.Count;
_shaders.Add(shader);

View File

@@ -1,15 +1,5 @@
shader "MyShader/Standard"
{
properties
{
//float4 color = { 1, 1, 1, 1 };
//tex2d texture1 = { black };
//tex2d texture2 = { white };
//tex2d texture3 = { grey };
//tex2d texture4 = { normal };
//sampler tex_sampler;
}
pass "Forward"
{
pipeline