feat(rendering): add GPU scene updates and optimizations

Added a new `code-executor` agent with strict TDD and performance focus. Refactored `TextureProcessor` and `TextureAssetHandler` to use `Magick.NET` for image processing. Enhanced `GPUScene` with `InstanceCounterBuffer` and improved instance management. Introduced a compute shader for GPU scene updates. Updated `GhostRenderPipeline` to handle add/remove instance buffers.

BREAKING CHANGE: Removed `x86` platform support and replaced `CachesFolderPath` with `LibraryFolderPath`. Updated project dependencies and removed unused utility classes.
This commit is contained in:
2026-04-14 17:56:23 +09:00
parent 817b32b8d9
commit d9bfa43663
28 changed files with 517 additions and 459 deletions

View File

@@ -1,16 +1,9 @@
using Ghost.Core.Graphics;
using Ghost.Entities;
using Ghost.Engine.RenderPipeline;
using Ghost.Graphics;
using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class EngineEntryAttribute : Attribute
{
}
[EngineEntry]
public sealed partial class EngineCore : IDisposable
{
private readonly JobScheduler _jobScheduler;
@@ -27,12 +20,11 @@ public sealed partial class EngineCore : IDisposable
{
FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12,
InitialRenderPipelineSettings = null! // TODO: We should allow user to specify the initial render pipeline settings.
InitialRenderPipelineSettings = new GhostRenderPipelineSettings(),
ShaderCacheDirectory = "ShaderCache",
};
_renderSystem = new RenderSystem(renderingConfig);
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
}
public void Dispose()

View File

@@ -9,6 +9,7 @@ internal unsafe class GPUScene : IDisposable
private readonly IResourceDatabase _resourceDatabase;
private Handle<GPUBuffer> _sceneBuffer;
private Handle<GPUBuffer> _instanceCounterBuffer;
private uint _instanceCount;
private uint _capacity;
@@ -16,6 +17,8 @@ internal unsafe class GPUScene : IDisposable
private bool _disposed;
public Handle<GPUBuffer> SceneBuffer => _sceneBuffer;
public Handle<GPUBuffer> InstanceCounterBuffer => _instanceCounterBuffer;
public uint InstanceCount => Volatile.Read(ref _instanceCount);
internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount)
{
@@ -30,9 +33,20 @@ internal unsafe class GPUScene : IDisposable
HeapType = HeapType.Default,
};
var counterBufferDesc = new BufferDesc
{
Size = sizeof(uint),
Stride = sizeof(uint),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
_sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer");
Logger.DebugAssert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer.");
_instanceCounterBuffer = _resourceAllocator.CreateBuffer(in counterBufferDesc, "SceneInstanceCounterBuffer");
Logger.DebugAssert(_instanceCounterBuffer.IsValid, "Failed to create GPUScene instance counter buffer.");
_capacity = initialCount;
}
@@ -55,7 +69,7 @@ internal unsafe class GPUScene : IDisposable
{
Size = newCapacity * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
Usage = BufferUsage.Raw | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
@@ -88,7 +102,7 @@ internal unsafe class GPUScene : IDisposable
{
if (index < 0 || index >= _capacity)
{
return ~0u;
return uint.MaxValue;
}
// Return the last index. We will swap the last instance data with the removed index on gpu to keep the buffer compact.
@@ -104,6 +118,7 @@ internal unsafe class GPUScene : IDisposable
}
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());
_resourceDatabase.ReleaseResource(_instanceCounterBuffer.AsResource());
_disposed = true;
GC.SuppressFinalize(this);

View File

@@ -2,8 +2,11 @@ using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.RenderPipeline;
[GenerateShaderProperty("Internal/UpdateGPUScene")]
@@ -18,8 +21,96 @@ public partial struct UpdateGPUSceneShaderProperty
internal partial class GhostRenderPipeline
{
public void UpdateGPUScene(RenderContext ctx, Handle<GPUBuffer> addBuffer, int addCount, Handle<GPUBuffer> removeBuffer, int removeCount)
private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{
if (!ghostPayload.AddRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<AddInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<AddInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
};
var addBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Add Instance Buffer");
var pAddData = (AddInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.AddRequest.TryDequeue(out var addRequest))
{
var (mesh, error) = resourceManager.GetMeshReference(addRequest.meshInstance.mesh);
if (error.IsFailure)
{
Logger.Error($"Failed to get mesh reference for mesh instance with ID {addRequest.instanceId}");
continue;
}
pAddData[i] = new AddInstanceData
{
localToWorld = addRequest.localToWorld,
instanceID = addRequest.instanceId,
meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()),
materialPalette = (uint)addRequest.meshInstance.materialPalette.Value,
renderingLayerMask = addRequest.meshInstance.renderingLayerMask,
shadowCastingMode = (uint)addRequest.meshInstance.shadowCastingMode
};
i++;
}
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, out int count)
{
if (!ghostPayload.RemoveRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<RemoveInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<RemoveInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
};
var removeBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Remove Instance Buffer");
var pRemoveData = (RemoveInstanceData*)resourceDatabase.MapResource(removeBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.RemoveRequest.TryDequeue(out var removeRequest))
{
pRemoveData[i] = new RemoveInstanceData
{
instanceID = removeRequest.instanceId,
swapWithInstanceID = removeRequest.swapWithInstanceId
};
i++;
}
resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null);
count = i;
return removeBuffer;
}
count = 0;
return default;
}
public void UpdateGPUScene(RenderContext ctx, GhostRenderPayload payload)
{
var addBuffer = CreateAddInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var addCount);
var removeBuffer = CreateRemoveInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var removeCount);
if (addCount <= 0 && removeCount <= 0)
{
Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates.");
@@ -42,7 +133,7 @@ internal partial class GhostRenderPipeline
};
// TODO: Write and load the shader. This is just a placeholder for now.
var shader = default(Handle<ComputeShader>);
var shader = Handle<ComputeShader>.Invalid;
var keywords = new LocalKeywordSet();
ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3());

View File

@@ -2,11 +2,7 @@ using Ghost.Core;
using Ghost.Graphics;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline;
@@ -15,7 +11,7 @@ internal partial class GhostRenderPipeline : IRenderPipeline
private struct AddInstanceData
{
public float4x4 localToWorld;
public uint instanceId;
public uint instanceID;
public uint meshBuffer;
public uint materialPalette;
public uint renderingLayerMask;
@@ -24,8 +20,8 @@ internal partial class GhostRenderPipeline : IRenderPipeline
private struct RemoveInstanceData
{
public uint instanceId;
public uint swapWithInstanceId;
public uint instanceID;
public uint swapWithInstanceID;
}
private readonly RenderSystem _renderSystem;
@@ -43,109 +39,18 @@ internal partial class GhostRenderPipeline : IRenderPipeline
_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, out int count)
{
if (!ghostPayload.AddRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<AddInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<AddInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
};
var addBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Add Instance Buffer");
var pAddData = (AddInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.AddRequest.TryDequeue(out var addRequest))
{
var (mesh, error) = resourceManager.GetMeshReference(addRequest.meshInstance.mesh);
if (error.IsFailure)
{
Debug.Fail($"Failed to get mesh reference for mesh instance with ID {addRequest.instanceId}");
continue;
}
pAddData[i] = new AddInstanceData
{
localToWorld = addRequest.localToWorld,
instanceId = addRequest.instanceId,
meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()),
materialPalette = (uint)addRequest.meshInstance.materialPalette.Value,
renderingLayerMask = addRequest.meshInstance.renderingLayerMask,
shadowCastingMode = (uint)addRequest.meshInstance.shadowCastingMode
};
i++;
}
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, out int count)
{
if (!ghostPayload.RemoveRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<RemoveInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<RemoveInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
};
var removeBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Remove Instance Buffer");
var pRemoveData = (RemoveInstanceData*)resourceDatabase.MapResource(removeBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.RemoveRequest.TryDequeue(out var removeRequest))
{
pRemoveData[i] = new RemoveInstanceData
{
instanceId = removeRequest.instanceId,
swapWithInstanceId = removeRequest.swapWithInstanceId
};
i++;
}
resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null);
count = i;
return removeBuffer;
}
count = 0;
return default;
}
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)
{
try
{
using var viewData = new RenderViewData(_renderSystem.SwapChainManager, resourceDatabase, in request);
using var viewData = new RenderViewData(_renderSystem.SwapChainManager, ctx.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);
UpdateGPUScene(ctx, ghostPayload);
}
catch (Exception ex)
{

View File

@@ -30,10 +30,13 @@ internal sealed class GhostRenderPayload : IRenderPayload
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
private uint _instanceCount;
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest;
public uint InstanceCount => _instanceCount;
public GhostRenderPayload(GhostRenderPipeline renderPipeline)
{
@@ -53,6 +56,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
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;
}
@@ -60,12 +64,18 @@ internal sealed class GhostRenderPayload : IRenderPayload
public void RemoveInstance(uint instanceId)
{
var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId);
if (swapWithInstanceId != ~0u)
if (swapWithInstanceId != uint.MaxValue)
{
_removeRequest.Enqueue(new RemoveInstanceRequest { instanceId = instanceId, swapWithInstanceId = swapWithInstanceId });
}
}
public void EndRecord()
{
// We capture the count here to prevent that main thread continues to add more requests for next frame while the render thread is still processing current frame's requests.
_instanceCount = _renderPipeline.GPUScene.InstanceCount;
}
public void Reset()
{
_renderRequests.Clear();

View File

@@ -0,0 +1,37 @@
compute "Internal/UpdateGPUScene"
{
includes
{
"F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl";
}
hlsl
{
[numthreads(64, 1, 1)]
void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID)
{
UpdateGPUSceneShaderProperty properties = LoadData<UpdateGPUSceneShaderProperty>(g_PushConstantData.propertiesBuffer, 0);
RWStructuredBuffer<InstanceData> gpuSceneBuffer = GET_BUFFER(properties.gpuSceneBuffer);
if (properties.addCount > 0)
{
StructuredBuffer<AddInstanceData> addBuffer = GET_BUFFER(properties.addBuffer);
AddInstanceData addData = addBuffer[dispatchThreadID.x];
gpuSceneBuffer[addData.instanceID].localToWorld = addData.localToWorld;
gpuSceneBuffer[addData.instanceID].meshBuffer = addData.meshBuffer;
gpuSceneBuffer[addData.instanceID].materialBuffer = addData.materialPalette;
}
if (properties.removeCount > 0)
{
StructuredBuffer<RemoveInstanceData> removeBuffer = GET_BUFFER(properties.removeBuffer);
RemoveInstanceData removeData = removeBuffer[dispatchThreadID.x];
gpuSceneBuffer[removeData.instanceID] = gpuSceneBuffer[removeData.swapWithInstanceID];
}
}
}
cs "hlsl_block" : "CSMain";
}

View File

@@ -7,6 +7,7 @@ using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[UpdateAfter<RemoveGPUInstanceSystem>]
[RenderPipelineSystem<GhostRenderPipelineSettings>]
internal class AddGPUInstanceSystem : SystemBase
{
@@ -48,5 +49,7 @@ internal class AddGPUInstanceSystem : SystemBase
systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index });
}
}
payload.EndRecord();
}
}