forked from Misaki/GhostEngine
Introduces a full-featured render graph system with pass culling, resource aliasing, and automatic barrier generation. Refactors resource and barrier APIs, improves error handling, and unifies result types. Renderer and render passes now use the new graph-based workflow. Updates shader includes, adds a blit shader, and improves HLSL parsing. Removes dynamic descriptor heaps in favor of persistent ones. Project file now includes the render graph module. Lays the foundation for advanced rendering features and improved memory efficiency.
325 lines
8.1 KiB
C#
325 lines
8.1 KiB
C#
using Ghost.Core;
|
|
using Ghost.Graphics.RHI;
|
|
using Misaki.HighPerformance.Mathematics;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace Ghost.Graphics;
|
|
|
|
public enum GraphicsAPI
|
|
{
|
|
Direct3D12
|
|
}
|
|
|
|
public struct RenderingConfig
|
|
{
|
|
public GraphicsAPI GraphicsAPI
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
public uint FrameBufferCount
|
|
{
|
|
get; set;
|
|
}
|
|
}
|
|
|
|
public interface IFenceSynchronizer
|
|
{
|
|
uint CPUFenceValue
|
|
{
|
|
get;
|
|
}
|
|
|
|
uint GPUFenceValue
|
|
{
|
|
get;
|
|
}
|
|
|
|
uint FrameIndex
|
|
{
|
|
get;
|
|
}
|
|
|
|
uint MaxFrameLatency
|
|
{
|
|
get;
|
|
}
|
|
|
|
bool WaitForGPUReady(int timeOut = -1);
|
|
void SignalCPUReady();
|
|
void WaitIdle();
|
|
}
|
|
|
|
public interface IRenderSystem : IFenceSynchronizer, IDisposable
|
|
{
|
|
IGraphicsEngine GraphicsEngine
|
|
{
|
|
get;
|
|
}
|
|
|
|
bool IsRunning
|
|
{
|
|
get;
|
|
}
|
|
|
|
void Start();
|
|
void Stop();
|
|
void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Application-level render system that orchestrates multiple renderers
|
|
/// and handles frame synchronization
|
|
/// </summary>
|
|
internal class RenderSystem : IRenderSystem
|
|
{
|
|
// TODO: Thread local command buffers.
|
|
private struct FrameResource : IDisposable
|
|
{
|
|
public required AutoResetEvent CpuReadyEvent
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public required AutoResetEvent GpuReadyEvent
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public required ICommandAllocator CommandAllocator
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public ulong FenceValue
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
public readonly void Dispose()
|
|
{
|
|
CpuReadyEvent.Dispose();
|
|
GpuReadyEvent.Dispose();
|
|
CommandAllocator.Dispose();
|
|
}
|
|
}
|
|
|
|
private readonly RenderingConfig _config;
|
|
private readonly IGraphicsEngine _graphicsEngine;
|
|
|
|
private readonly FrameResource[] _frameResources;
|
|
private readonly Thread _renderThread;
|
|
private readonly AutoResetEvent _shutdownEvent;
|
|
private readonly ConcurrentDictionary<ISwapChain, uint2> _resizeRequest;
|
|
|
|
private uint _frameIndex;
|
|
private uint _cpuFenceValue;
|
|
private uint _gpuFenceValue;
|
|
|
|
private bool _isRunning;
|
|
private bool _disposed;
|
|
|
|
public IGraphicsEngine GraphicsEngine => _graphicsEngine;
|
|
public bool IsRunning => _isRunning;
|
|
|
|
public uint CPUFenceValue => _cpuFenceValue;
|
|
public uint GPUFenceValue => _gpuFenceValue;
|
|
public uint FrameIndex => _frameIndex;
|
|
public uint MaxFrameLatency => _config.FrameBufferCount;
|
|
|
|
public RenderSystem(RenderingConfig config)
|
|
{
|
|
_config = config;
|
|
_graphicsEngine = config.GraphicsAPI switch
|
|
{
|
|
GraphicsAPI.Direct3D12 => new D3D12.D3D12GraphicsEngine(this),
|
|
_ => throw new NotSupportedException($"Graphics API {config.GraphicsAPI} is not supported.")
|
|
};
|
|
|
|
// Create frame resources for synchronization
|
|
_frameResources = new FrameResource[config.FrameBufferCount];
|
|
for (var i = 0; i < config.FrameBufferCount; i++)
|
|
{
|
|
_frameResources[i] = new FrameResource
|
|
{
|
|
CpuReadyEvent = new AutoResetEvent(false),
|
|
GpuReadyEvent = new AutoResetEvent(true),
|
|
CommandAllocator = _graphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics)
|
|
};
|
|
}
|
|
|
|
_renderThread = new Thread(RenderLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "Graphics Render Thread",
|
|
Priority = ThreadPriority.Normal
|
|
};
|
|
|
|
_shutdownEvent = new AutoResetEvent(false);
|
|
_resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>();
|
|
|
|
_isRunning = false;
|
|
_disposed = false;
|
|
}
|
|
|
|
~RenderSystem()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
if (_isRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isRunning = true;
|
|
_renderThread.Start();
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
if (!_isRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isRunning = false;
|
|
_shutdownEvent.Set();
|
|
_renderThread.Join();
|
|
}
|
|
|
|
public void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
_resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize);
|
|
}
|
|
|
|
public bool WaitForGPUReady(int timeOut = -1)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
|
|
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
|
|
}
|
|
|
|
public void SignalCPUReady()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
|
|
_frameResources[eventIndex].CpuReadyEvent.Set();
|
|
_cpuFenceValue++;
|
|
}
|
|
|
|
public void WaitIdle()
|
|
{
|
|
foreach (var frameResource in _frameResources)
|
|
{
|
|
if (frameResource.FenceValue > 0)
|
|
{
|
|
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RenderLoop()
|
|
{
|
|
var waitHandles = new WaitHandle[] { null!, _shutdownEvent };
|
|
|
|
while (_isRunning)
|
|
{
|
|
_frameIndex = _gpuFenceValue % _config.FrameBufferCount;
|
|
ref var frameResource = ref _frameResources[_frameIndex];
|
|
|
|
// Wait for either CPU ready signal or shutdown signal
|
|
waitHandles[0] = frameResource.CpuReadyEvent;
|
|
var waitResult = WaitHandle.WaitAny(waitHandles);
|
|
|
|
// If shutdown was signaled or timeout occurred, exit the loop
|
|
if (!_isRunning || waitResult == 1 || waitResult == WaitHandle.WaitTimeout)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Only proceed if CPU ready event was signaled
|
|
if (waitResult == 0)
|
|
{
|
|
if (frameResource.FenceValue > 0)
|
|
{
|
|
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
|
|
}
|
|
|
|
if (!_resizeRequest.IsEmpty)
|
|
{
|
|
//WaitIdle();
|
|
_gpuFenceValue++;
|
|
var flushFence = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue);
|
|
_graphicsEngine.Device.GraphicsQueue.WaitForValue(flushFence);
|
|
|
|
// Sync the current frame resource to this new fence to keep state consistent
|
|
frameResource.FenceValue = flushFence;
|
|
|
|
|
|
foreach (var resource in _frameResources)
|
|
{
|
|
resource.CommandAllocator.Reset();
|
|
}
|
|
|
|
foreach (var kvp in _resizeRequest)
|
|
{
|
|
var swapChain = kvp.Key;
|
|
var newSize = kvp.Value;
|
|
swapChain.Resize(newSize.x, newSize.y);
|
|
}
|
|
|
|
_resizeRequest.Clear();
|
|
}
|
|
|
|
frameResource.CommandAllocator.Reset();
|
|
|
|
var r = _graphicsEngine.RenderFrame(frameResource.CommandAllocator);
|
|
if (r.IsFailure)
|
|
{
|
|
_isRunning = false;
|
|
#if DEBUG
|
|
System.Diagnostics.Debugger.Break();
|
|
#endif
|
|
Logger.LogError($"RenderFrame failed: {r.Message}");
|
|
}
|
|
|
|
_gpuFenceValue++;
|
|
|
|
frameResource.GpuReadyEvent.Set();
|
|
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Stop();
|
|
|
|
foreach (var frameResource in _frameResources)
|
|
{
|
|
frameResource.Dispose();
|
|
}
|
|
|
|
_graphicsEngine.Dispose();
|
|
_shutdownEvent.Dispose();
|
|
|
|
_disposed = true;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|