feat(graphics): refactor pipeline keying and allocators

Major refactor of graphics pipeline keying, shader cache, and resource allocation.
Replaced most Allocator usage with AllocationHandle, modernized logger usage,
and unified pipeline state keys. Updated MeshUtility to use AllocationHandle.FreeList.
Added new shader pipeline architecture docs and improved error handling throughout.

BREAKING CHANGE: Pipeline keying and resource allocation APIs have changed.
This commit is contained in:
2026-04-13 23:07:52 +09:00
parent c66fda5332
commit 817b32b8d9
69 changed files with 1436 additions and 2095 deletions

View File

@@ -1,7 +1,6 @@
using Ghost.Core;
using Ghost.Entities;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;

View File

@@ -1,6 +1,5 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline;
@@ -16,6 +15,8 @@ internal unsafe class GPUScene : IDisposable
private uint _requiredResize;
private bool _disposed;
public Handle<GPUBuffer> SceneBuffer => _sceneBuffer;
internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount)
{
_resourceAllocator = resourceAllocator;
@@ -30,7 +31,7 @@ internal unsafe class GPUScene : IDisposable
};
_sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer");
Debug.Assert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer.");
Logger.DebugAssert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer.");
_capacity = initialCount;
}
@@ -48,22 +49,21 @@ internal unsafe class GPUScene : IDisposable
return;
}
var newCapacity = _capacity * 2;
newCapacity = Math.Max(newCapacity, _capacity + _requiredResize);
var newCapacity = Math.Max(_capacity * 2, _capacity + _requiredResize);
var newBufferDesc = new BufferDesc
{
Size = (ulong)newCapacity * (ulong)sizeof(InstanceData),
Size = 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);
Logger.DebugAssert(newBuffer.IsValid);
// Copy existing data to the new buffer
cmd.CopyBuffer(newBuffer, _sceneBuffer, 0, 0, (ulong)_instanceCount * (ulong)sizeof(InstanceData));
cmd.CopyBuffer(newBuffer, _sceneBuffer, 0, 0, _instanceCount * (ulong)sizeof(InstanceData));
// Replace old buffer with the new one
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());

View File

@@ -0,0 +1,50 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.RenderPipeline;
[GenerateShaderProperty("Internal/UpdateGPUScene")]
public partial struct UpdateGPUSceneShaderProperty
{
public uint gpuSceneBuffer;
public uint addBuffer;
public uint addCount;
public uint removeBuffer;
public uint removeCount;
}
internal partial class GhostRenderPipeline
{
public void UpdateGPUScene(RenderContext ctx, Handle<GPUBuffer> addBuffer, int addCount, Handle<GPUBuffer> removeBuffer, int removeCount)
{
if (addCount <= 0 && removeCount <= 0)
{
Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates.");
return; // No updates needed
}
// NOTE: We dispatch it here instead of in render graph is because the update does not perform every frame.
// The topology change of the graph will trigger the recompilation of the render graph, which is expensive.
// Currently the render graph does not support import invalid resources, which means we can not handle the early return in the render func.
// Furthermore, updating the GPU scene does not rely on other resources and passes, it's isolated and always run before the actual rendering.
// So it's fine to dispatch it here directly.
var property = new UpdateGPUSceneShaderProperty
{
gpuSceneBuffer = ctx.ResourceDatabase.GetBindlessIndex(_gpuScene.SceneBuffer.AsResource(), BindlessAccess.UnorderedAccess),
addBuffer = ctx.ResourceDatabase.GetBindlessIndex(addBuffer.AsResource()),
addCount = (uint)addCount,
removeBuffer = ctx.ResourceDatabase.GetBindlessIndex(removeBuffer.AsResource()),
removeCount = (uint)removeCount
};
// TODO: Write and load the shader. This is just a placeholder for now.
var shader = default(Handle<ComputeShader>);
var keywords = new LocalKeywordSet();
ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3());
}
}

View File

@@ -10,7 +10,7 @@ using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline;
internal class GhostRenderPipeline : IRenderPipeline
internal partial class GhostRenderPipeline : IRenderPipeline
{
private struct AddInstanceData
{
@@ -39,11 +39,11 @@ internal class GhostRenderPipeline : IRenderPipeline
{
_renderSystem = renderSystem;
_renderGraph = new RenderGraph(renderSystem.ResourceManager, renderSystem.GraphicsEngine);
_renderGraph = new RenderGraph(renderSystem);
_gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now
}
private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase)
private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{
if (!ghostPayload.AddRequest.IsEmpty)
{
@@ -82,13 +82,16 @@ internal class GhostRenderPipeline : IRenderPipeline
}
resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null);
count = i;
return addBuffer;
}
count = 0;
return default;
}
private static unsafe Handle<GPUBuffer> CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase)
private static unsafe Handle<GPUBuffer> CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{
if (!ghostPayload.RemoveRequest.IsEmpty)
{
@@ -116,9 +119,12 @@ internal class GhostRenderPipeline : IRenderPipeline
}
resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null);
count = i;
return removeBuffer;
}
count = 0;
return default;
}
@@ -131,15 +137,20 @@ internal class GhostRenderPipeline : IRenderPipeline
foreach (ref readonly var request in ghostPayload.RenderRequests)
{
if (!RenderPipelineUtility.GetVPMatrices(_renderSystem, in request, out var view, out var projection, out var screenSize))
try
{
continue;
using var viewData = new RenderViewData(_renderSystem.SwapChainManager, resourceDatabase, in request);
RenderPipelineUtility.GetVPMatrices(in request, viewData.ScreenSize, out var view, out var projection);
var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var addCount);
var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var removeCount);
UpdateGPUScene(ctx, addBuffer, addCount, removeBuffer, removeCount);
}
catch (Exception ex)
{
Logger.Error(ex);
}
var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase);
var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase);
}
}

View File

@@ -39,7 +39,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
{
_renderPipeline = renderPipeline;
_renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
_renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
_addRequest = new ConcurrentQueue<AddInstanceRequest>();
_removeRequest = new ConcurrentQueue<RemoveInstanceRequest>();
}

View File

@@ -15,6 +15,7 @@ public class RenderPipelineSystemAttribute<T> : RenderPipelineSystemAttribute
public override Type SettingsType => typeof(T);
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class RenderPipelineSystemRegistry
{
private static readonly Dictionary<nint, List<Func<ISystem>>> s_renderPipelineSystems = new();