feat(core,rendering)!: add cleanup component support, refactor render pipeline

Introduce ICleanupComponent and cleanup archetype logic in ECS. Refactor component versioning to uint. Update IResourceDatabase to use map/unmap pattern. Decouple per-frame render requests from RenderSystem via IRenderPayload. Update render pipeline and extraction system to new API.

BREAKING CHANGE: Entity destruction and render pipeline APIs have changed. IResourceDatabase.MapResource signature is updated; all callers must use map/memcpy/unmap. RenderSystem no longer manages per-frame render requests directly.
This commit is contained in:
2026-04-06 22:05:48 +09:00
parent c6bdbe0710
commit 6c96d4cf50
20 changed files with 399 additions and 200 deletions

View File

@@ -0,0 +1,374 @@
using Ghost.Core;
using Ghost.DSL.ShaderCompiler;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Graphics.Test.RenderPipeline;
public unsafe partial class TestRenderPipeline : IRenderPipeline
{
private class MeshletDebugPassData
{
public RenderList renderList;
public Handle<Material> material;
public uint globalIndex;
public uint viewIndex;
public uint instanceIndex;
}
private readonly RenderSystem _renderSystem;
private readonly RenderGraph _renderGraph;
private Identifier<Shader> _meshletShader;
private Handle<Material> _meshletMaterial;
private bool _disposed;
~TestRenderPipeline()
{
Dispose();
}
internal TestRenderPipeline(RenderSystem renderSystem)
{
_renderSystem = renderSystem;
_renderGraph = new RenderGraph(renderSystem.ResourceManager,
renderSystem.GraphicsEngine.ResourceAllocator,
renderSystem.GraphicsEngine.ResourceDatabase,
renderSystem.GraphicsEngine.PipelineLibrary,
renderSystem.GraphicsEngine.ShaderCompiler);
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/test.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
_meshletShader = renderSystem.ResourceManager.CreateGraphicsShader(shaderDescriptor);
_meshletMaterial = renderSystem.ResourceManager.CreateMaterial(_meshletShader);
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
tier = CompilerTier.Tier2
};
var pass = shaderDescriptor.passes[0];
var emptyKeywords = new LocalKeywordSet();
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in emptyKeywords);
renderSystem.GraphicsEngine.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
private static float3 IntersectFrustumPlanes(float4 p0, float4 p1, float4 p2)
{
var n0 = p0.xyz;
var n1 = p1.xyz;
var n2 = p2.xyz;
float det = math.dot(math.cross(n0, n1), n2);
return (math.cross(n2, n1) * p0.w + math.cross(n0, n2) * p1.w - math.cross(n0, n1) * p2.w) * (1.0f / det);
}
private static Frustum CreateFrustum(float nearClip, float farClip, float4x4 vp, float3 viewDir, float3 viewPos)
{
var frustum = new Frustum();
Frustum.CalculateFrustumPlanes(vp, ref frustum.planes);
// We need to recalculate the near and far planes otherwise it does not work for oblique projection matrices used for reflection.
var nearPlane = Plane.CreateFromUnitNormalAndPointInPlane(viewDir, viewPos);
nearPlane.Distance -= nearClip;
var farPlane = Plane.CreateFromUnitNormalAndPointInPlane(-viewDir, viewPos);
farPlane.Distance += farClip;
frustum.planes[4] = nearPlane;
frustum.planes[5] = farPlane;
frustum.corners[0] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[4]);
frustum.corners[1] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[4]);
frustum.corners[2] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[4]);
frustum.corners[3] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[4]);
frustum.corners[4] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[5]);
frustum.corners[5] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[5]);
frustum.corners[6] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[5]);
frustum.corners[7] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[5]);
return frustum;
}
public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload)
{
var testPayload = (TestRenderPayload)payload;
var renderSystem = testPayload.RenderSystem;
var resourceManager = renderSystem.ResourceManager;
var resourceDatabase = renderSystem.GraphicsEngine.ResourceDatabase;
var requests = testPayload.FrameRequestData[frameIndex].renderRequests;
for (var i = 0; i < requests.Count; i++)
{
ref readonly var request = ref requests[i];
// 1. Allocate and populate Instance Data buffer
var instanceCount = request.opaqueRenderList.TotalRecordCount;
if (instanceCount == 0)
{
continue; // Nothing to render
}
Handle<GPUTexture> rt;
if (request.swapChainIndex < 0)
{
rt = request.colorTarget;
}
else if (renderSystem.SwapChainManager.TryGetSwapChain(request.swapChainIndex, out var swapChain))
{
rt = swapChain.GetCurrentBackBuffer();
}
else
{
continue;
}
try
{
var rtResult = renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(rt.AsResource());
if (rtResult.IsFailure)
{
continue;
}
var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height);
var aspectScreen = (float)rtSize.x / rtSize.y;
// NOTE: We assume camera's scale is always (1, 1, 1). Otherwise fastinverse will fail and we need to use regular inverse which is more expensive.
var viewMatrix = math.fastinverse(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);
var projectionMatrix = new float4x4
(
m_00, 0, 0, 0,
0, m_11, 0, 0,
0, 0, m_22, m_23,
0, 0, 1, 0
);
//var vp = math.mul(projectionMatrix, viewMatrix);
//var viewDir = math.normalize(request.view.localToWorld.c2.xyz);
//var viewPos = request.view.localToWorld.c3.xyz;
//var frustum = CreateFrustum(request.view.nearClipPlane, request.view.farClipPlane, vp, viewDir, viewPos);
var instanceDataSize = (uint)(instanceCount * sizeof(InstanceData));
var instanceBufferDesc = new BufferDesc
{
Size = instanceDataSize,
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload, // Upload directly for simplicity in testing
};
var instanceBufferHandle = resourceManager.CreateTransientBuffer(in instanceBufferDesc, "Instance Buffer");
var instanceBufferResource = instanceBufferHandle.AsResource();
var instanceDataArray = new InstanceData[instanceCount];
var instanceIdx = 0;
foreach (var record in request.opaqueRenderList)
{
var (mesh, error) = resourceManager.GetMeshReference(record.mesh);
if (error.IsFailure)
{
continue;
}
(var mat, error) = resourceManager.GetMaterialReference(_meshletMaterial);
if (error.IsFailure)
{
continue;
}
instanceDataArray[instanceIdx++] = new InstanceData
{
localToWorld = record.localToWorld,
meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().ObjectDataBuffer.AsResource()),
materialBuffer = resourceDatabase.GetBindlessIndex(mat.Get()._cBufferCache.GpuResource.AsResource())
};
}
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(instanceBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
ctx.UploadBuffer(instanceBufferHandle, instanceDataArray.AsSpan());
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(instanceBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
// 2. Allocate and populate View Data buffer
var viewBufferDesc = new BufferDesc
{
Size = (uint)sizeof(ViewData),
Stride = (uint)sizeof(ViewData),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload,
};
var viewBufferHandle = resourceManager.CreateTransientBuffer(in viewBufferDesc, "View Buffer");
var viewBufferResource = viewBufferHandle.AsResource();
var viewData = new ViewData
{
viewMatrix = viewMatrix,
projectionMatrix = projectionMatrix,
cameraPosition = request.view.localToWorld.c3.xyz,
nearClip = request.view.nearClipPlane,
cameraDirection = viewMatrix.c2.xyz, // check if that's correct orientation
farClip = request.view.farClipPlane,
screenSize = new float4(request.view.sensorSize.x, request.view.sensorSize.y, 1.0f / request.view.sensorSize.x, 1.0f / request.view.sensorSize.y)
};
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(viewBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
ctx.UploadBuffer(viewBufferHandle, new ReadOnlySpan<ViewData>(in viewData));
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(viewBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
// 3. Allocate and populate Global Frame Data buffer
var frameDataSize = (uint)sizeof(FrameData);
var frameBufferDesc = new BufferDesc
{
Size = frameDataSize,
Stride = frameDataSize,
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload,
};
//var frameBufferHandle = resourceManager.CreateTransientBuffer(in frameBufferDesc, "Frame Buffer");
//var frameBufferResource = frameBufferHandle.AsResource();
//var frameData = new FrameData();
//ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(frameBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
//ctx.UploadBuffer(frameBufferHandle, new ReadOnlySpan<FrameData>(in frameData));
//ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(frameBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
_renderGraph.Reset();
var backBuffer = _renderGraph.ImportTexture(rt, "BackBuffer");
MeshletDebugPass(backBuffer, request.opaqueRenderList,
uint.MaxValue,
resourceDatabase.GetBindlessIndex(viewBufferResource),
resourceDatabase.GetBindlessIndex(instanceBufferResource));
var viewState = new ViewState(rtSize.x, rtSize.y, rtSize.x, rtSize.y);
_renderGraph.Compile(viewState);
_renderGraph.Execute(ctx.CommandBuffer);
}
finally
{
if (request.swapChainIndex >= 0)
{
renderSystem.SwapChainManager.ReleaseSwapChain(request.swapChainIndex);
}
}
}
}
private void MeshletDebugPass(Identifier<RGTexture> backbuffer, RenderList renderList, uint globalIndex, uint viewIndex, uint instanceBuffer)
{
using (var builder = _renderGraph.AddRasterRenderPass<MeshletDebugPassData>("Meshlet Debug Pass", out var passData))
{
var depth = builder.CreateTexture(RGTextureDesc.RelativeDepth(1.0f), "Depth Texture");
passData.renderList = renderList;
passData.globalIndex = globalIndex;
passData.viewIndex = viewIndex;
passData.instanceIndex = instanceBuffer;
passData.material = _meshletMaterial;
builder.SetColorAttachment(backbuffer, 0);
builder.SetDepthAttachment(depth);
builder.SetRenderFunc<MeshletDebugPassData>(static (data, ctx) =>
{
ctx.SetGlobalData(data.globalIndex, data.viewIndex);
ctx.SetInstanceData(data.instanceIndex);
ctx.SetActiveMaterial(data.material);
var instanceIndex = 0u;
foreach (var record in data.renderList)
{
ctx.SetInstanceIndex(instanceIndex);
var meshRefResult = ctx.ResourceManager.GetMeshReference(record.mesh);
if (meshRefResult.IsSuccess)
{
var meshletCount = (uint)meshRefResult.Value.MeshletData.meshletCount;
ctx.DispatchMesh(new uint3(meshletCount, 1, 1));
}
instanceIndex++;
}
});
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_renderSystem.ResourceManager.ReleaseMaterial(_meshletMaterial);
_renderSystem.ResourceManager.ReleaseShader(_meshletShader);
_renderGraph.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}