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

@@ -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