using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.D3D12; using Ghost.Graphics.RenderPipeline; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.Mathematics; using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Ghost.Graphics; internal enum GraphicsAPI { Direct3D12 } internal struct RenderSystemDesc { public GraphicsAPI GraphicsAPI { get; set; } public uint FrameBufferCount { get; set; } public IRenderPipelineSettings? InitialRenderPipelineSettings { get; set; } } /// /// Application-level render system that orchestrates multiple renderers /// and handles frame synchronization /// public class RenderSystem : IDisposable { private struct FrameResource : IDisposable { private UnsafeList _renderRequests; public required AutoResetEvent CpuReadyEvent { get; init; } public required AutoResetEvent GpuReadyEvent { get; init; } public required ICommandAllocator CommandAllocator { get; init; } public ulong FenceValue { get; set; } [UnscopedRef] public ref UnsafeList RenderRequests => ref _renderRequests; public void Dispose() { CpuReadyEvent.Dispose(); GpuReadyEvent.Dispose(); CommandAllocator.Dispose(); for (var i = 0; i < _renderRequests.Count; i++) { _renderRequests[i].Dispose(); } _renderRequests.Dispose(); } } private readonly RenderSystemDesc _config; private readonly IGraphicsEngine _graphicsEngine; private readonly ResourceManager _resourceManager; private readonly SwapChainManager _swapChainManager; private readonly FrameResource[] _frameResources; private readonly Thread _renderThread; private readonly AutoResetEvent _shutdownEvent; private readonly ConcurrentDictionary _resizeRequest; private IRenderPipelineSettings _renderPipelineSettings; private IRenderPipeline _renderPipeline; private uint _frameIndex; private ulong _cpuFenceValue; private ulong _gpuFenceValue; private bool _isRunning; private bool _disposed; internal SwapChainManager SwapChainManager => _swapChainManager; public IGraphicsEngine GraphicsEngine => _graphicsEngine; public ResourceManager ResourceManager => _resourceManager; public bool IsRunning => _isRunning; public ulong CPUFenceValue => _cpuFenceValue; public ulong GPUFenceValue => _gpuFenceValue; public uint FrameIndex => _frameIndex; public uint MaxFrameLatency => _config.FrameBufferCount; public IRenderPipelineSettings RenderPipelineSettings { get => _renderPipelineSettings; set { Debug.Assert(value != null, "RenderPipelineSettings cannot be set to null."); Debug.Assert(!_disposed, "Cannot set RenderPipelineSettings on a disposed RenderSystem."); if (value == _renderPipelineSettings) { return; } _renderPipeline?.Dispose(); _renderPipelineSettings = value; _renderPipeline = _renderPipelineSettings.CreatePipeline(this); } } internal RenderSystem(RenderSystemDesc desc) { _config = desc; var engineDesc = new GraphicsEngineDesc { FrameBufferCount = desc.FrameBufferCount }; switch (desc.GraphicsAPI) { case GraphicsAPI.Direct3D12: if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) { _graphicsEngine = D3D12GraphicsEngineFactory.Create(engineDesc); } else { // TODO: Fallback to Vulkan once it's implemented. throw new PlatformNotSupportedException("Direct3D12 requires Windows 10 version 2004 (build 19041) or later."); } break; default: throw new NotSupportedException($"The specified graphics API '{desc.GraphicsAPI}' is not supported."); } _resourceManager = new ResourceManager(_graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase); _swapChainManager = new SwapChainManager(_graphicsEngine); // Create frame resources for synchronization _frameResources = new FrameResource[desc.FrameBufferCount]; for (var i = 0; i < desc.FrameBufferCount; i++) { _frameResources[i] = new FrameResource { CpuReadyEvent = new AutoResetEvent(false), GpuReadyEvent = new AutoResetEvent(true), CommandAllocator = _graphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics), RenderRequests = new UnsafeList(2, Allocator.Persistent) }; } _renderThread = new Thread(RenderLoop) { IsBackground = true, Name = "Graphics Render Thread", Priority = ThreadPriority.Normal }; _shutdownEvent = new AutoResetEvent(false); _resizeRequest = new ConcurrentDictionary(); _renderPipelineSettings = _config.InitialRenderPipelineSettings ?? new GhostRenderPipelineSettings(); _renderPipeline = _renderPipelineSettings.CreatePipeline(this); _isRunning = false; _disposed = false; } ~RenderSystem() { Dispose(); } private void RenderLoop() { void StopRenderLoop(Result result) { Debug.Assert(result.IsFailure, "StopRenderLoop should only be called with a failure result."); _isRunning = false; _shutdownEvent.Set(); #if DEBUG Debugger.Break(); #endif Logger.LogError($"Render failed: {result.Message}"); } var waitHandles = new WaitHandle[] { null!, _shutdownEvent }; while (_isRunning) { _frameIndex = (uint)(_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) { continue; } _graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue); if (!_resizeRequest.IsEmpty) { _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(); } var keys = _resizeRequest.Keys.ToArray(); foreach (var swapChain in keys) { if (_resizeRequest.TryRemove(swapChain, out var newSize)) { swapChain.Resize(newSize.x, newSize.y); } } frameResource.GpuReadyEvent.Set(); continue; // Skip rendering this frame since we just resized and may have invalid render targets } // Begin rendering for this frame frameResource.CommandAllocator.Reset(); _resourceManager.BeginFrame(_cpuFenceValue); var r = _graphicsEngine.BeginFrame(_cpuFenceValue); if (r.IsFailure) { StopRenderLoop(r); break; } // Start recording commands // TODO: How can we support async compute and async copy? var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics); ref var renderRequests = ref frameResource.RenderRequests; try { cmd.Begin(frameResource.CommandAllocator); var renderCtx = new RenderContext { CommandBuffer = cmd }; _renderPipeline.Render(renderCtx, renderRequests.AsSpan()); _swapChainManager.TransitionToPresent(cmd); // End recording commands and submit r = cmd.End(); if (r.IsFailure) { StopRenderLoop(r); break; } _graphicsEngine.Device.GraphicsQueue.Submit(cmd); _swapChainManager.PresentAll(cmd); } finally { _graphicsEngine.ReturnPooledCommandBuffer(cmd); for (var i = 0; i < renderRequests.Count; i++) { renderRequests[i].Dispose(); } renderRequests.Clear(); } // End the frame and present _resourceManager.EndFrame(_cpuFenceValue); r = _graphicsEngine.EndFrame(_gpuFenceValue); if (r.IsFailure) { StopRenderLoop(r); break; } // Prepare for the next frame. _gpuFenceValue++; frameResource.GpuReadyEvent.Set(); frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue); } } internal void Start() { Debug.Assert(!_disposed, "Cannot start a disposed RenderSystem."); if (_isRunning) { return; } _isRunning = true; _renderThread.Start(); } internal void Stop() { Debug.Assert(!_disposed, "Cannot stop a disposed RenderSystem."); if (!_isRunning) { return; } _isRunning = false; _shutdownEvent.Set(); _renderThread.Join(); } internal void SignalCPUReady() { Debug.Assert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); _frameResources[eventIndex].CpuReadyEvent.Set(); _cpuFenceValue++; } internal void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize) { Debug.Assert(!_disposed, "Cannot request swap chain resize on a disposed RenderSystem."); _resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize); } public void AddRenderRequest(in RenderRequest request) { Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem."); var frameIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); _frameResources[frameIndex].RenderRequests.Add(request); } public bool WaitForGPUReady(int timeOut = -1) { Debug.Assert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut); } public void WaitIdle() { Debug.Assert(!_disposed, "Cannot wait idle on a disposed RenderSystem."); foreach (var frameResource in _frameResources) { if (frameResource.FenceValue > 0) { _graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue); } } } public void Dispose() { if (_disposed) { return; } Stop(); for (var i = 0; i < _frameResources.Length; i++) { ref var frameResource = ref _frameResources[i]; frameResource.Dispose(); } _renderPipeline.Dispose(); _swapChainManager.Dispose(); _resourceManager.Dispose(); _graphicsEngine.Dispose(); _shutdownEvent.Dispose(); _disposed = true; GC.SuppressFinalize(this); } }