feat(render): per-frame render requests & thread safety

Refactor RenderSystem to store render requests per-frame within FrameResource, improving encapsulation and resource management. Update render loop and AddRenderRequest to use the new structure, ensuring proper disposal and clearing of requests to prevent memory leaks. Remove the old global renderRequests array and update Dispose logic accordingly.

Add spin lock-based thread safety to D3D12ResourceDatabase for AddResource/AddAllocation, and introduce EnterParallelRead/ExitParallelRead methods for explicit locking.

Enhance RenderExtractionSystem and Material to support transparent render lists and a MaterialRenderType property, preparing for advanced rendering features. Includes minor code cleanups and comment improvements.
This commit is contained in:
2026-03-24 16:46:30 +09:00
parent d44ec0be31
commit 92e3d33361
8 changed files with 128 additions and 70 deletions

View File

@@ -93,8 +93,9 @@ public class RenderExtractionSystem : ISystem
var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height); var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height);
var aspectScreen = (float)rtSize.x / rtSize.y; var aspectScreen = (float)rtSize.x / rtSize.y;
var renderList = new RenderList(1, 64, Allocator.Temp); var renderList = new RenderList(1, 64, Allocator.FreeList);
var shadowCasterRenderList = new RenderList(1, 64, Allocator.Temp); var transparentRenderList = new RenderList(1, 64, Allocator.FreeList);
var shadowCasterRenderList = new RenderList(1, 64, Allocator.FreeList);
// TODO: This chould be done in parallel jobs. // TODO: This chould be done in parallel jobs.
foreach (var chunk in meshQuery.GetChunkIterator()) foreach (var chunk in meshQuery.GetChunkIterator())
@@ -213,7 +214,7 @@ public class RenderExtractionSystem : ISystem
depthTarget = camRef.depthTarget, depthTarget = camRef.depthTarget,
opaqueRenderList = renderList, opaqueRenderList = renderList,
shadowCasterRenderList = shadowCasterRenderList, shadowCasterRenderList = shadowCasterRenderList,
transparentRenderList = default, transparentRenderList = default, // TODO: Classify transparent objects into a separate render list and render via oit.
renderFunc = camRef.renderFunc, renderFunc = camRef.renderFunc,
view = new RenderView view = new RenderView
{ {

View File

@@ -123,7 +123,6 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
_commandBufferReturnQueue.Enqueue(new CommandBufferReturnEntry(commandBuffer, _currentFrame)); _commandBufferReturnQueue.Enqueue(new CommandBufferReturnEntry(commandBuffer, _currentFrame));
} }
public ISwapChain CreateSwapChain(SwapChainDesc desc) public ISwapChain CreateSwapChain(SwapChainDesc desc)
{ {
ThrowIfDisposed(); ThrowIfDisposed();

View File

@@ -99,7 +99,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
private readonly D3D12DescriptorAllocator _descriptorAllocator; private readonly D3D12DescriptorAllocator _descriptorAllocator;
// TODO: Change AOS to SOA? Does it even matter since we mostly access resources by handle which is essentially random access? // TODO: Change AOS to SOA?
private UnsafeSlotMap<ResourceRecord> _resources; private UnsafeSlotMap<ResourceRecord> _resources;
private UnsafeHashMap<SamplerDesc, Identifier<Sampler>> _samplers; private UnsafeHashMap<SamplerDesc, Identifier<Sampler>> _samplers;
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
@@ -108,6 +108,8 @@ internal class D3D12ResourceDatabase : IResourceDatabase
private UnsafeQueue<ReleaseEntry> _releaseQueue; private UnsafeQueue<ReleaseEntry> _releaseQueue;
private int _writeLock;
private ulong _currentFrame; private ulong _currentFrame;
private bool _disposed; private bool _disposed;
@@ -141,23 +143,37 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid; return Handle<GPUResource>.Invalid;
} }
var id = _resources.Add(new ResourceRecord(pResource, initialBarrierData, viewGroup), out var generation); var spinner = new SpinWait();
var handle = new Handle<GPUResource>(id, generation); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var id = _resources.Add(new ResourceRecord(pResource, initialBarrierData, viewGroup), out var generation);
var handle = new Handle<GPUResource>(id, generation);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{ {
pResource->SetName(name); pResource->SetName(name);
_resourceName[handle] = name; _resourceName[handle] = name;
} }
#endif #endif
return handle; return handle;
}
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
} }
internal unsafe Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) internal unsafe Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
{ {
Debug.Assert(!_disposed); Debug.Assert(!_disposed);
if (allocation == null) if (allocation == null)
{ {
#if DEBUG #if DEBUG
@@ -166,23 +182,48 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid; return Handle<GPUResource>.Invalid;
} }
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation); var spinner = new SpinWait();
var handle = new Handle<GPUResource>(id, generation); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{
allocation->SetName(name);
var pResource = allocation->GetResource();
if (pResource != null)
{ {
pResource->SetName(name); allocation->SetName(name);
var pResource = allocation->GetResource();
if (pResource != null)
{
pResource->SetName(name);
}
_resourceName[handle] = name;
} }
_resourceName[handle] = name;
}
#endif #endif
return handle; return handle;
}
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
}
public void EnterParallelRead()
{
Debug.Assert(!_disposed);
Interlocked.Exchange(ref _writeLock, 1);
}
public void ExitParallelRead()
{
Debug.Assert(!_disposed);
Interlocked.Exchange(ref _writeLock, 0);
} }
public bool HasResource(Handle<GPUResource> handle) public bool HasResource(Handle<GPUResource> handle)

View File

@@ -47,6 +47,10 @@ public interface IResourceDatabase : IDisposable
where T : unmanaged; where T : unmanaged;
*/ */
void EnterParallelRead();
void ExitParallelRead();
/// <summary> /// <summary>
/// Checks if a resource with the specified handle exists in the database. /// Checks if a resource with the specified handle exists in the database.
/// </summary> /// </summary>

View File

@@ -65,6 +65,12 @@ public struct Material : IResourceReleasable
get; set; get; set;
} }
// For now, 0 means opaque, 1 means transparent, may be we need 2 means ui, etc. but higher values are reserved for user-defined render types.
public uint MaterialRenderType
{
get; set;
}
public Error SetShader(Identifier<Shader> shaderId, ResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator) public Error SetShader(Identifier<Shader> shaderId, ResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator)
{ {
if (!shaderId.IsValid) if (!shaderId.IsValid)

View File

@@ -235,9 +235,9 @@ public readonly unsafe ref struct RenderingContext
worldBoundsMax = meshData.BoundingBox.Max, worldBoundsMax = meshData.BoundingBox.Max,
vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()), vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()), indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()),
meshletBuffer = meshData.MeshLetBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()), meshletBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
meshletVerticesBuffer = meshData.MeshletVerticesBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()), meshletVerticesBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
meshletTrianglesBuffer = meshData.MeshletTrianglesBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()), meshletTrianglesBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()),
}; };
var bufferHandle = meshData.ObjectDataBuffer.AsResource(); var bufferHandle = meshData.ObjectDataBuffer.AsResource();

View File

@@ -1,3 +1,4 @@
#if flase
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RenderGraphModule;
@@ -47,7 +48,7 @@ public partial class GhostRenderPipeline
private readonly uint _padding1; private readonly uint _padding1;
private readonly uint _padding2; private readonly uint _padding2;
} }
#if flase
private void RenderTest(RenderGraph graph, Identifier<RGTexture> backbuffer) private void RenderTest(RenderGraph graph, Identifier<RGTexture> backbuffer)
{ {
Identifier<RGTexture> renderTarget; Identifier<RGTexture> renderTarget;
@@ -107,5 +108,5 @@ public partial class GhostRenderPipeline
}); });
} }
} }
# endif
} }
# endif

View File

@@ -8,6 +8,7 @@ using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Ghost.Graphics; namespace Ghost.Graphics;
@@ -37,6 +38,8 @@ public class RenderSystem : IDisposable
{ {
private struct FrameResource : IDisposable private struct FrameResource : IDisposable
{ {
private UnsafeList<RenderRequest> _renderRequests;
public required AutoResetEvent CpuReadyEvent public required AutoResetEvent CpuReadyEvent
{ {
get; init; get; init;
@@ -57,11 +60,21 @@ public class RenderSystem : IDisposable
get; set; get; set;
} }
public readonly void Dispose() [UnscopedRef]
public ref UnsafeList<RenderRequest> RenderRequests => ref _renderRequests;
public void Dispose()
{ {
CpuReadyEvent.Dispose(); CpuReadyEvent.Dispose();
GpuReadyEvent.Dispose(); GpuReadyEvent.Dispose();
CommandAllocator.Dispose(); CommandAllocator.Dispose();
for (var i = 0; i < _renderRequests.Count; i++)
{
_renderRequests[i].Dispose();
}
_renderRequests.Dispose();
} }
} }
@@ -74,7 +87,6 @@ public class RenderSystem : IDisposable
private readonly Thread _renderThread; private readonly Thread _renderThread;
private readonly AutoResetEvent _shutdownEvent; private readonly AutoResetEvent _shutdownEvent;
private UnsafeArray<UnsafeList<RenderRequest>> _renderRequests;
private readonly ConcurrentDictionary<ISwapChain, uint2> _resizeRequest; private readonly ConcurrentDictionary<ISwapChain, uint2> _resizeRequest;
private IRenderPipelineSettings _renderPipelineSettings; private IRenderPipelineSettings _renderPipelineSettings;
@@ -109,6 +121,7 @@ public class RenderSystem : IDisposable
return; return;
} }
_renderPipeline?.Dispose();
_renderPipelineSettings = value; _renderPipelineSettings = value;
_renderPipeline = _renderPipelineSettings.CreatePipeline(this); _renderPipeline = _renderPipelineSettings.CreatePipeline(this);
} }
@@ -152,7 +165,8 @@ public class RenderSystem : IDisposable
{ {
CpuReadyEvent = new AutoResetEvent(false), CpuReadyEvent = new AutoResetEvent(false),
GpuReadyEvent = new AutoResetEvent(true), GpuReadyEvent = new AutoResetEvent(true),
CommandAllocator = _graphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics) CommandAllocator = _graphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics),
RenderRequests = new UnsafeList<RenderRequest>(2, Allocator.Persistent)
}; };
} }
@@ -165,11 +179,6 @@ public class RenderSystem : IDisposable
_shutdownEvent = new AutoResetEvent(false); _shutdownEvent = new AutoResetEvent(false);
_resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>(); _resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>();
_renderRequests = new UnsafeArray<UnsafeList<RenderRequest>>((int)desc.FrameBufferCount, Allocator.Persistent);
for (var i = 0; i < desc.FrameBufferCount; i++)
{
_renderRequests[i] = new UnsafeList<RenderRequest>(2, Allocator.Persistent);
}
_renderPipelineSettings = new GhostRenderPipelineSettings(); _renderPipelineSettings = new GhostRenderPipelineSettings();
_renderPipeline = _renderPipelineSettings.CreatePipeline(this); _renderPipeline = _renderPipelineSettings.CreatePipeline(this);
@@ -266,28 +275,41 @@ public class RenderSystem : IDisposable
// TODO: How can we support async compute and async copy? // TODO: How can we support async compute and async copy?
var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics); var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics);
cmd.Begin(frameResource.CommandAllocator);
var renderCtx = new RenderContext try
{ {
CommandBuffer = cmd cmd.Begin(frameResource.CommandAllocator);
};
ref var renderRequests = ref _renderRequests[_frameIndex]; var renderCtx = new RenderContext
_renderPipeline.Render(renderCtx, renderRequests.AsSpan()); {
CommandBuffer = cmd
};
// End recording commands and submit ref var renderRequests = ref frameResource.RenderRequests;
r = cmd.End(); _renderPipeline.Render(renderCtx, renderRequests.AsSpan());
if (r.IsFailure)
// End recording commands and submit
r = cmd.End();
if (r.IsFailure)
{
StopRenderLoop(r);
break;
}
_graphicsEngine.Device.GraphicsQueue.Submit(cmd);
for (var i = 0; i < renderRequests.Count; i++)
{
renderRequests[i].Dispose();
}
renderRequests.Clear();
}
finally
{ {
_graphicsEngine.ReturnPooledCommandBuffer(cmd); _graphicsEngine.ReturnPooledCommandBuffer(cmd);
StopRenderLoop(r);
break;
} }
_graphicsEngine.Device.GraphicsQueue.Submit(cmd);
_graphicsEngine.ReturnPooledCommandBuffer(cmd);
// End the frame and present // End the frame and present
_resourceManager.EndFrame(_cpuFenceValue); _resourceManager.EndFrame(_cpuFenceValue);
r = _graphicsEngine.EndFrame(_gpuFenceValue); r = _graphicsEngine.EndFrame(_gpuFenceValue);
@@ -302,13 +324,6 @@ public class RenderSystem : IDisposable
// Prepare for the next frame. // Prepare for the next frame.
for (var i = 0; i < renderRequests.Count; i++)
{
renderRequests[i].Dispose();
}
renderRequests.Clear();
_gpuFenceValue++; _gpuFenceValue++;
frameResource.GpuReadyEvent.Set(); frameResource.GpuReadyEvent.Set();
@@ -363,7 +378,7 @@ public class RenderSystem : IDisposable
Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem."); Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem.");
var frameIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); var frameIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_renderRequests[frameIndex].Add(request); _frameResources[frameIndex].RenderRequests.Add(request);
} }
public bool WaitForGPUReady(int timeOut = -1) public bool WaitForGPUReady(int timeOut = -1)
@@ -395,21 +410,12 @@ public class RenderSystem : IDisposable
Stop(); Stop();
foreach (var frameResource in _frameResources) for (int i = 0; i < _frameResources.Length; i++)
{ {
ref var frameResource = ref _frameResources[i];
frameResource.Dispose(); frameResource.Dispose();
} }
foreach (ref var renderRequestList in _renderRequests)
{
foreach (ref var request in renderRequestList)
{
request.Dispose();
}
renderRequestList.Dispose();
}
_graphicsEngine.Dispose(); _graphicsEngine.Dispose();
_shutdownEvent.Dispose(); _shutdownEvent.Dispose();