From 2b3bf21a746f3d5cc2b6f5fc1b6d885bddddda29 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sun, 22 Mar 2026 21:04:05 +0900 Subject: [PATCH] feat(engine): refactor resource mgmt & render pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors engine infrastructure for improved resource/service management and render pipeline extensibility. Replaces World’s resource API with a service-based API. Splits IGraphicsEngine’s RenderFrame into BeginFrame/EndFrame. Adds support for pluggable render pipelines in RenderSystem. Replaces disposed checks with Debug.Assert in performance paths. Updates RenderExtractionSystem and render loop for new APIs. Improves diagnostics and code clarity. BREAKING CHANGE: Resource API replaced with service API; render pipeline and frame lifecycle interfaces changed. --- src/Runtime/Ghost.Core/Result.cs | 10 ++ .../Systems/RenderExtractionSystem.cs | 15 +- src/Runtime/Ghost.Entities/Query.cs | 3 +- src/Runtime/Ghost.Entities/System.cs | 2 +- src/Runtime/Ghost.Entities/World.cs | 17 +- .../D3D12CommandBuffer.cs | 1 + .../Ghost.Graphics.D3D12/D3D12CommandQueue.cs | 13 +- .../D3D12GraphicsEngine.cs | 33 ++-- .../D3D12ResourceAllocator.cs | 9 +- .../D3D12ResourceDatabase.cs | 27 ++-- .../Ghost.Graphics.D3D12/D3D12SwapChain.cs | 12 +- .../Ghost.Graphics.RHI/IGraphicsEngine.cs | 15 +- .../RenderGraphModule/RenderGraphBuilder.cs | 3 + .../RenderPipeline/GhostRenderPipeline.cs | 4 +- .../RenderPipeline/IRenderPipeline.cs | 2 +- src/Runtime/Ghost.Graphics/RenderSystem.cs | 151 ++++++++++++------ 16 files changed, 198 insertions(+), 119 deletions(-) diff --git a/src/Runtime/Ghost.Core/Result.cs b/src/Runtime/Ghost.Core/Result.cs index de9ed0a..0502edf 100644 --- a/src/Runtime/Ghost.Core/Result.cs +++ b/src/Runtime/Ghost.Core/Result.cs @@ -390,6 +390,16 @@ public static class ResultExtensions return result; } + public static Result Then(this Result result, Func func) + { + if (result.IsFailure) + { + return Result.Failure(result.Message); + } + + return func(); + } + public static Result Then(this Result result, Func> func) { if (result.IsFailure) diff --git a/src/Runtime/Ghost.Engine/Systems/RenderExtractionSystem.cs b/src/Runtime/Ghost.Engine/Systems/RenderExtractionSystem.cs index 693e287..210fe08 100644 --- a/src/Runtime/Ghost.Engine/Systems/RenderExtractionSystem.cs +++ b/src/Runtime/Ghost.Engine/Systems/RenderExtractionSystem.cs @@ -1,9 +1,11 @@ using Ghost.Core; using Ghost.Engine.Components; using Ghost.Entities; +using Ghost.Graphics; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics.Geometry; @@ -11,14 +13,14 @@ namespace Ghost.Engine.Systems; public class RenderExtractionSystem : ISystem { - private IGraphicsEngine _graphicsEngine = null!; + private RenderSystem _renderSystem = null!; private Identifier _cameraQueryID; private Identifier _meshQueryID; public void Initialize(ref readonly SystemAPI systemAPI) { - _graphicsEngine = systemAPI.World.GetResource(); + _renderSystem = systemAPI.World.GetService(); var builder = new QueryBuilder(); @@ -77,13 +79,15 @@ public class RenderExtractionSystem : ISystem ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID); ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID); + var renderRequests = new UnsafeList(cameraQuery.CalculateEntityCount(), Allocator.Temp); + // TODO: We should extract the render record for each camera because different cameras may have different culling results. foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator()) { ref readonly var camRef = ref cam.Get(); ref readonly var camLtwRef = ref camLtw.Get(); - var rtResult = _graphicsEngine.ResourceDatabase.GetResourceDescription(camRef.colorTarget.AsResource()); + var rtResult = _renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(camRef.colorTarget.AsResource()); if (rtResult.IsFailure) { continue; @@ -206,7 +210,6 @@ public class RenderExtractionSystem : ISystem var viewPos = camLtwRef.matrix.c3.xyz; var frustum = CreateFrustum(camRef, vp, viewDir, viewPos); - // TODO: Send this to render pipeline. var request = new RenderRequest { colorTarget = camRef.colorTarget, @@ -236,7 +239,11 @@ public class RenderExtractionSystem : ISystem renderingLayerMask = camRef.renderingLayerMask, }, }; + + renderRequests.Add(request); } + + _renderPipeline.Render(new RenderContext(), renderRequests.AsSpan()); } public void Cleanup(ref readonly SystemAPI systemAPI) diff --git a/src/Runtime/Ghost.Entities/Query.cs b/src/Runtime/Ghost.Entities/Query.cs index ab99f0f..5d97c0e 100644 --- a/src/Runtime/Ghost.Entities/Query.cs +++ b/src/Runtime/Ghost.Entities/Query.cs @@ -445,8 +445,7 @@ public unsafe partial struct EntityQuery : IDisposable return new ChunkIterator(_matchingArchetypes.AsReadOnly(), world); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly int GetEntityCount() + public readonly int CalculateEntityCount() { var total = 0; var world = World.GetWorld(_worldID); diff --git a/src/Runtime/Ghost.Entities/System.cs b/src/Runtime/Ghost.Entities/System.cs index 2ed5a7a..e3524f8 100644 --- a/src/Runtime/Ghost.Entities/System.cs +++ b/src/Runtime/Ghost.Entities/System.cs @@ -46,7 +46,7 @@ public abstract class SystemBase : ISystem foreach (var queryID in _requiredQueries) { ref var query = ref World.ComponentManager.GetEntityQueryReference(new Identifier(queryID)); - if (query.GetEntityCount() == 0) + if (query.CalculateEntityCount() == 0) { return false; } diff --git a/src/Runtime/Ghost.Entities/World.cs b/src/Runtime/Ghost.Entities/World.cs index 290be2d..d4073b0 100644 --- a/src/Runtime/Ghost.Entities/World.cs +++ b/src/Runtime/Ghost.Entities/World.cs @@ -1,7 +1,6 @@ using Ghost.Core; using Misaki.HighPerformance.Jobs; using System.Runtime.CompilerServices; -using TerraFX.Interop.Windows; namespace Ghost.Entities; @@ -86,7 +85,7 @@ public partial class World : IDisposable, IEquatable private readonly ComponentManager _componentManager; private readonly SystemManager _systemManager; - private readonly Dictionary _globalResource; + private readonly Dictionary _services; private int _version; private bool _disposed = false; @@ -140,7 +139,7 @@ public partial class World : IDisposable, IEquatable _componentManager = new ComponentManager(this); _systemManager = new SystemManager(this); - _globalResource = new Dictionary(); + _services = new Dictionary(); if (jobScheduler != null) { @@ -195,20 +194,20 @@ public partial class World : IDisposable, IEquatable /// Registers or overwrites a global resource in the world. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetResource(T resource) + public void AddService(T resource) where T : class { - _globalResource[typeof(T)] = resource; + _services[typeof(T)] = resource; } /// /// Retrieves a global resource from the world. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T GetResource() + public T GetService() where T : class { - if (_globalResource.TryGetValue(typeof(T), out var resource)) + if (_services.TryGetValue(typeof(T), out var resource)) { return (T)resource; } @@ -220,10 +219,10 @@ public partial class World : IDisposable, IEquatable /// Checks if a global resource exists. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasResource() + public bool HasService() where T : class { - return _globalResource.ContainsKey(typeof(T)); + return _services.ContainsKey(typeof(T)); } public bool Equals(World? other) diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs index 650472a..777b4e4 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs @@ -79,6 +79,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer Dispose(); } + [Conditional("DEBUG")] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ThrowIfDisposed() { diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandQueue.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandQueue.cs index 11309eb..13b35d5 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandQueue.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandQueue.cs @@ -1,6 +1,7 @@ using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; +using System.Diagnostics; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -65,7 +66,7 @@ internal unsafe class D3D12CommandQueue : ICommandQueue public void Submit(ICommandBuffer commandBuffer) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (commandBuffer.IsEmpty) { @@ -86,7 +87,7 @@ internal unsafe class D3D12CommandQueue : ICommandQueue public void Submit(params ReadOnlySpan commandBuffers) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); Span executableIndices = stackalloc int[commandBuffers.Length]; executableIndices.Fill(-1); @@ -129,7 +130,7 @@ internal unsafe class D3D12CommandQueue : ICommandQueue public ulong Signal(ulong value) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); _fenceValue = value; ThrowIfFailed(_commandQueue.Get()->Signal((ID3D12Fence*)_fence.Get(), _fenceValue)); @@ -138,7 +139,7 @@ internal unsafe class D3D12CommandQueue : ICommandQueue public void WaitForValue(ulong value) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (_fence.Get()->GetCompletedValue() < value) { @@ -152,13 +153,13 @@ internal unsafe class D3D12CommandQueue : ICommandQueue public ulong GetCompletedValue() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); return _fence.Get()->GetCompletedValue(); } public void WaitIdle() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); var fenceValue = Signal(Interlocked.Increment(ref _fenceValue)); WaitForValue(fenceValue); diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs index d94fdf2..489063a 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs @@ -1,17 +1,19 @@ #if DEBUG -#define ENABLE_DEBUG +#define ENABLE_DEBUG_LAYER #endif using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; using System.Collections.Immutable; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace Ghost.Graphics.D3D12; public static class D3D12GraphicsEngineFactory { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IGraphicsEngine Create(GraphicsEngineDesc desc) { return new D3D12GraphicsEngine(desc); @@ -22,7 +24,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine { private readonly GraphicsEngineDesc _desc; -#if ENABLE_DEBUG +#if ENABLE_DEBUG_LAYER private readonly D3D12DebugLayer _debugLayer; #endif private readonly D3D12RenderDevice _device; @@ -46,7 +48,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine { _desc = desc; -#if ENABLE_DEBUG +#if ENABLE_DEBUG_LAYER _debugLayer = new D3D12DebugLayer(); #endif _device = new D3D12RenderDevice(); @@ -67,6 +69,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine Dispose(); } + [Conditional("DEBUG")] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ThrowIfDisposed() { @@ -118,28 +121,20 @@ internal class D3D12GraphicsEngine : IGraphicsEngine return new D3D12SwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _desc.FrameBufferCount); } - public Result RenderFrame(ICommandAllocator commandAllocator, uint cpuFenceValue, uint gpuFenceValue) + public Result BeginFrame(uint cpuFenceValue, uint gpuFenceValue) { ThrowIfDisposed(); - var r = Result.Success(); - _resourceDatabase.BeginFrame(cpuFenceValue); + return Result.Success(); + } - // TODO: We should not handle renderers in graphics engine since the purpose of graphics engine is to provide low-level graphics resource management and command buffer creation. - // We need to migrate this to IRenderPipeline instead when it's ready. - foreach (var renderer in _renderers) - { - r = renderer.Render(commandAllocator); - if (r.IsFailure) - { - break; - } - } + public Result EndFrame(uint cpuFenceValue, uint gpuFenceValue) + { + ThrowIfDisposed(); _resourceDatabase.EndFrame(gpuFenceValue); - - return r; + return Result.Success(); } public void Dispose() @@ -163,7 +158,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine _descriptorAllocator.Dispose(); _shaderCompiler.Dispose(); _device.Dispose(); -#if ENABLE_DEBUG +#if ENABLE_DEBUG_LAYER _debugLayer.Dispose(); #endif diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs index 5e964be..a212010 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs @@ -2,6 +2,7 @@ using Ghost.Core; using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; +using System.Diagnostics; using System.Runtime.CompilerServices; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -601,7 +602,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateTexture(ref readonly TextureDesc desc, string name, CreationOptions options = default) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); CheckTexture2DSize(desc.Width, desc.Height); @@ -699,7 +700,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateRenderTarget(ref readonly RenderTargetDesc desc, string name, CreationOptions options = default) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); var textureDesc = desc.ToTextureDescription(); return CreateTexture(in textureDesc, name, options); @@ -707,7 +708,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateBuffer(ref readonly BufferDesc desc, string name, CreationOptions options = default) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); CheckBufferSize(desc.Size); var resourceDesc = desc.ToD3D12ResourceDesc(); @@ -839,7 +840,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Identifier CreateSampler(ref readonly SamplerDesc desc) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (_resourceDatabase.TryGetSampler(in desc, out var id)) { diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs index f0e84f9..421a167 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs @@ -4,6 +4,7 @@ using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; +using System.Diagnostics; using System.Runtime.InteropServices; using TerraFX.Interop.DirectX; @@ -130,7 +131,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase internal unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, string? name = null) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (pResource == null) { @@ -156,7 +157,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase public unsafe Handle AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (allocation == null) { #if DEBUG @@ -186,13 +187,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public bool HasResource(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); return _resources.Contains(handle.ID, handle.Generation); } public RefResult GetResourceRecord(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) @@ -267,7 +268,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase public string? GetResourceName(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); #if DEBUG || GHOST_EDITOR if (_resourceName.TryGetValue(handle, out var name)) @@ -280,7 +281,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase public void ReleaseResource(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (_resources.TryGetElementAt(handle.ID, handle.Generation, out var record)) { @@ -299,7 +300,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase public void ReleaseResourceImmediately(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist || !info.Allocated) @@ -313,7 +314,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase public Identifier AddSampler(ref readonly SamplerDesc desc, int id) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (_samplers.ContainsKey(desc)) { @@ -328,13 +329,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier id) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); return _samplers.TryGetValue(desc, out id); } public void ReleaseSampler(Identifier id) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); // NOTE: We almost never release samplers individually, because they are cheap and can be reused. // Ideally we would release all samplers at once when disposing the ResourceDatabase. @@ -360,13 +361,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public void BeginFrame(uint currentFrameFenceValue) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); _currentFrameFenceValue = currentFrameFenceValue; } public void EndFrame(uint completedFenceValue) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); while (_releaseQueue.Count > 0) { @@ -384,7 +385,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase internal void ReleaseAllResourcesImmediately() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); foreach (ref var record in _resources) { diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12SwapChain.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12SwapChain.cs index ab21f1a..4e84cae 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12SwapChain.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12SwapChain.cs @@ -68,7 +68,9 @@ internal unsafe class D3D12SwapChain : ISwapChain SetScale(desc.ScaleX, desc.ScaleY); if (desc.Target.Type == SwapChainTargetType.Composition) + { _compositionSurface = desc.Target.CompositionSurface; + } } ~D3D12SwapChain() @@ -169,20 +171,20 @@ internal unsafe class D3D12SwapChain : ISwapChain [MethodImpl(MethodImplOptions.AggressiveInlining)] public Handle GetCurrentBackBuffer() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); return _backBuffers[_swapChain.Get()->GetCurrentBackBufferIndex()]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan> GetBackBuffers() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); return _backBuffers.AsSpan(); } public void Present(bool vsync = true) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); var presentFlags = 0u; var syncInterval = vsync ? 1u : 0u; @@ -192,7 +194,7 @@ internal unsafe class D3D12SwapChain : ISwapChain public void Resize(uint width, uint height) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed); if (Width == width && Height == height) { @@ -215,6 +217,8 @@ internal unsafe class D3D12SwapChain : ISwapChain public void SetScale(float scaleX, float scaleY) { + Debug.Assert(!_disposed); + var inverseScaleX = 1.0f / scaleX; var inverseScaleY = 1.0f / scaleY; diff --git a/src/Runtime/Ghost.Graphics.RHI/IGraphicsEngine.cs b/src/Runtime/Ghost.Graphics.RHI/IGraphicsEngine.cs index a760eaf..eeb3e61 100644 --- a/src/Runtime/Ghost.Graphics.RHI/IGraphicsEngine.cs +++ b/src/Runtime/Ghost.Graphics.RHI/IGraphicsEngine.cs @@ -78,11 +78,18 @@ public interface IGraphicsEngine : IDisposable ISwapChain CreateSwapChain(SwapChainDesc desc); /// - /// Renders the current frame. + /// Begin the current frame. /// - /// Command allocator to use for rendering /// CPU fence value for synchronization /// GPU fence value for synchronization - /// Result of the rendering operation - Result RenderFrame(ICommandAllocator commandAllocator, uint cpuFenceValue, uint gpuFenceValue); + /// Result of the begin frame operation + Result BeginFrame(uint cpuFenceValue, uint gpuFenceValue); + + /// + /// End the current frame. + /// + /// CPU fence value for synchronization + /// GPU fence value for synchronization + /// Result of the end frame operation + Result EndFrame(uint cpuFenceValue, uint gpuFenceValue); } diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs index e8b4121..9d88e78 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs @@ -1,6 +1,7 @@ using Ghost.Core; using Ghost.Graphics.RHI; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Ghost.Graphics.RenderGraphModule; @@ -175,6 +176,8 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra _disposed = false; } + [Conditional("DEBUG")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(_disposed, this); diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs index 72955fe..bbb7f4f 100644 --- a/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs +++ b/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs @@ -1,13 +1,14 @@ using Ghost.Graphics.Core; using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RHI; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace Ghost.Graphics.RenderPipeline; public sealed class GhostRenderPipelineSettings : IRenderPipelineSettings { - public static IRenderPipeline CreatePipeline(RenderSystem renderSystem) + public IRenderPipeline CreatePipeline(RenderSystem renderSystem) { return new GhostRenderPipeline(renderSystem); } @@ -24,6 +25,7 @@ public unsafe partial class GhostRenderPipeline : IRenderPipeline Dispose(); } + [Conditional("DEBUG")] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void ThrowIfDisposed() { diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs index 9836a59..b31cc9d 100644 --- a/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs +++ b/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs @@ -5,7 +5,7 @@ namespace Ghost.Graphics.RenderPipeline; public interface IRenderPipelineSettings { - static abstract IRenderPipeline CreatePipeline(RenderSystem renderSystem); + IRenderPipeline CreatePipeline(RenderSystem renderSystem); } public interface IRenderPipeline : IDisposable diff --git a/src/Runtime/Ghost.Graphics/RenderSystem.cs b/src/Runtime/Ghost.Graphics/RenderSystem.cs index 48b5298..a5fc82a 100644 --- a/src/Runtime/Ghost.Graphics/RenderSystem.cs +++ b/src/Runtime/Ghost.Graphics/RenderSystem.cs @@ -1,8 +1,10 @@ using Ghost.Core; using Ghost.Graphics.D3D12; +using Ghost.Graphics.RenderPipeline; using Ghost.Graphics.RHI; using Misaki.HighPerformance.Mathematics; using System.Collections.Concurrent; +using System.Diagnostics; namespace Ghost.Graphics; @@ -61,6 +63,7 @@ public class RenderSystem : IDisposable } private readonly RenderSystemDesc _config; + private readonly IGraphicsEngine _graphicsEngine; private readonly ResourceManager _resourceManager; @@ -69,6 +72,9 @@ public class RenderSystem : IDisposable private readonly AutoResetEvent _shutdownEvent; private readonly ConcurrentDictionary _resizeRequest; + private IRenderPipelineSettings _renderPipelineSettings; + private IRenderPipeline _renderPipeline; + private uint _frameIndex; private uint _cpuFenceValue; private uint _gpuFenceValue; @@ -85,6 +91,24 @@ public class RenderSystem : IDisposable 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; + } + + _renderPipelineSettings = value; + _renderPipeline = _renderPipelineSettings.CreatePipeline(this); + } + } + internal RenderSystem(RenderSystemDesc desc) { _config = desc; @@ -137,6 +161,9 @@ public class RenderSystem : IDisposable _shutdownEvent = new AutoResetEvent(false); _resizeRequest = new ConcurrentDictionary(); + _renderPipelineSettings = new GhostRenderPipelineSettings(); + _renderPipeline = _renderPipelineSettings.CreatePipeline(this); + _isRunning = false; _disposed = false; } @@ -148,6 +175,18 @@ public class RenderSystem : IDisposable 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) @@ -166,65 +205,74 @@ public class RenderSystem : IDisposable } // Only proceed if CPU ready event was signaled - if (waitResult == 0) + if (waitResult != 0) { - if (frameResource.FenceValue > 0) - { - _graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue); - } + continue; + } - 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(); - } - - 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 - } - - frameResource.CommandAllocator.Reset(); - - var r = _graphicsEngine.RenderFrame(frameResource.CommandAllocator, _cpuFenceValue, _gpuFenceValue); - if (r.IsFailure) - { - _isRunning = false; -#if DEBUG - System.Diagnostics.Debugger.Break(); -#endif - Logger.LogError($"RenderFrame failed: {r.Message}"); - } + _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(); + } + + 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(); - frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue); + + continue; // Skip rendering this frame since we just resized and may have invalid render targets } + + frameResource.CommandAllocator.Reset(); + + var r = _graphicsEngine.BeginFrame(_cpuFenceValue, _gpuFenceValue); + if (r.IsFailure) + { + StopRenderLoop(r); + } + + var cmd = _graphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics); + var renderCtx = new RenderContext + { + CommandBuffer = cmd, + }; + + _renderPipeline.Render(renderCtx, default); + + r = _graphicsEngine.EndFrame(_cpuFenceValue, _gpuFenceValue); + if (r.IsFailure) + { + StopRenderLoop(r); + } + + _gpuFenceValue++; + + frameResource.GpuReadyEvent.Set(); + frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue); } } internal void Start() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed, "Cannot start a disposed RenderSystem."); if (_isRunning) { @@ -237,7 +285,7 @@ public class RenderSystem : IDisposable internal void Stop() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed, "Cannot stop a disposed RenderSystem."); if (!_isRunning) { @@ -251,7 +299,7 @@ public class RenderSystem : IDisposable internal void SignalCPUReady() { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); _frameResources[eventIndex].CpuReadyEvent.Set(); @@ -260,13 +308,13 @@ public class RenderSystem : IDisposable internal void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed, "Cannot request swap chain resize on a disposed RenderSystem."); _resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize); } public bool WaitForGPUReady(int timeOut = -1) { - ObjectDisposedException.ThrowIf(_disposed, this); + Debug.Assert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut); @@ -274,6 +322,7 @@ public class RenderSystem : IDisposable public void WaitIdle() { + Debug.Assert(!_disposed, "Cannot wait idle on a disposed RenderSystem."); foreach (var frameResource in _frameResources) { if (frameResource.FenceValue > 0)