From 676f8bb74c4689abc593668952bb1a76bf8cea9c Mon Sep 17 00:00:00 2001 From: Misaki Date: Mon, 1 Dec 2025 22:31:17 +0900 Subject: [PATCH] Add render graph proof of concept and refactor graphics Implemented a transient render graph system as a proof of concept, including resource aliasing, pass culling, and typed pass data. Added new project `Ghost.RenderGraph.Concept` targeting `.NET 10.0`. Refactored graphics-related components: - Simplified resource state transitions in `RenderingContext`. - Improved resize handling in `GraphicsTestWindow`. - Updated `D3D12GraphicsEngine` to streamline frame rendering. - Enhanced `D3D12ResourceDatabase` and `D3D12SwapChain` for better resource management. Added detailed documentation: - `ALIASING.md` explains resource aliasing techniques. - `API_DESIGN.md` outlines the render graph API design. Updated solution to include the new render graph project. --- Ghost.Editor/Ghost.Editor.csproj | 9 +- .../Windows/GraphicsTestWindow.xaml.cs | 39 +- Ghost.Graphics/Core/RenderingContext.cs | 74 +--- Ghost.Graphics/D3D12/D3D12CommandBuffer.cs | 5 + Ghost.Graphics/D3D12/D3D12CommandQueue.cs | 2 +- Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs | 18 +- Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs | 18 +- Ghost.Graphics/D3D12/D3D12SwapChain.cs | 36 +- .../D3D12/Utilities/D3D12Utility.cs | 10 +- Ghost.Graphics/RHI/Common.cs | 44 +- Ghost.Graphics/RHI/IGraphicsEngine.cs | 11 +- Ghost.Graphics/RHI/ISwapChain.cs | 8 - Ghost.Graphics/RenderSystem.cs | 32 +- Ghost.RenderGraph.Concept/ALIASING.md | 177 ++++++++ Ghost.RenderGraph.Concept/API_DESIGN.md | 189 ++++++++ .../Ghost.RenderGraph.Concept.csproj | 10 + Ghost.RenderGraph.Concept/ICommandBuffer.cs | 86 ++++ Ghost.RenderGraph.Concept/PassData.cs | 58 +++ Ghost.RenderGraph.Concept/Program.cs | 177 ++++++++ Ghost.RenderGraph.Concept/README.md | 306 +++++++++++++ Ghost.RenderGraph.Concept/RenderGraph.cs | 415 ++++++++++++++++++ .../RenderGraphBlackboard.cs | 36 ++ .../RenderGraphExtensions.cs | 41 ++ Ghost.RenderGraph.Concept/RenderGraphPass.cs | 50 +++ .../RenderGraphPassBuilder.cs | 105 +++++ .../RenderGraphResourceHandle.cs | 41 ++ .../ResourceAllocator.cs | 213 +++++++++ .../ResourceDescriptor.cs | 28 ++ Ghost.RenderGraph.Concept/ResourceLifetime.cs | 35 ++ Ghost.RenderGraph.Concept/ResourceState.cs | 21 + GhostEngine.sln | 15 + 31 files changed, 2167 insertions(+), 142 deletions(-) create mode 100644 Ghost.RenderGraph.Concept/ALIASING.md create mode 100644 Ghost.RenderGraph.Concept/API_DESIGN.md create mode 100644 Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj create mode 100644 Ghost.RenderGraph.Concept/ICommandBuffer.cs create mode 100644 Ghost.RenderGraph.Concept/PassData.cs create mode 100644 Ghost.RenderGraph.Concept/Program.cs create mode 100644 Ghost.RenderGraph.Concept/README.md create mode 100644 Ghost.RenderGraph.Concept/RenderGraph.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphExtensions.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphPass.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs create mode 100644 Ghost.RenderGraph.Concept/ResourceAllocator.cs create mode 100644 Ghost.RenderGraph.Concept/ResourceDescriptor.cs create mode 100644 Ghost.RenderGraph.Concept/ResourceLifetime.cs create mode 100644 Ghost.RenderGraph.Concept/ResourceState.cs diff --git a/Ghost.Editor/Ghost.Editor.csproj b/Ghost.Editor/Ghost.Editor.csproj index 50966f4..5f030e1 100644 --- a/Ghost.Editor/Ghost.Editor.csproj +++ b/Ghost.Editor/Ghost.Editor.csproj @@ -10,7 +10,7 @@ true preview - + @@ -99,11 +99,6 @@ MSBuild:Compile - - - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll - - MSBuild:Compile diff --git a/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs b/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs index ac8fbc6..59adb0e 100644 --- a/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs +++ b/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs @@ -1,18 +1,22 @@ using Ghost.Graphics.RHI; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.Mathematics; namespace Ghost.Graphics.Test.Windows; public sealed partial class GraphicsTestWindow : Window { - private bool _isFirstActivationHandled = false; - + private DispatcherTimer _resizeTimer; private IRenderSystem? _renderSystem; private IRenderer? _renderer; private ISwapChain? _swapChain; + private bool _isFirstActivationHandled; + private bool _isResizing; + public GraphicsTestWindow() { InitializeComponent(); @@ -20,6 +24,10 @@ public sealed partial class GraphicsTestWindow : Window Activated += GraphicsTestWindow_Activated; Closed += GraphicsTestWindow_Closed; + _resizeTimer = new DispatcherTimer(); + _resizeTimer.Interval = TimeSpan.FromMilliseconds(200); + _resizeTimer.Tick += OnResizeTimerTick; + Panel.SizeChanged += SwapChainPanel_SizeChanged; } @@ -45,11 +53,9 @@ public sealed partial class GraphicsTestWindow : Window { Width = (uint)AppWindow.Size.Width, Height = (uint)AppWindow.Size.Height, - BufferCount = 2, Format = TextureFormat.B8G8R8A8_UNorm, Target = SwapChainTarget.FromCompositionSurface(Panel) }); - _renderer.SetSwapChain(_swapChain); _renderSystem.Start(); CompositionTarget.Rendering += OnRendering; @@ -74,10 +80,31 @@ public sealed partial class GraphicsTestWindow : Window private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e) { - if (e.NewSize.Width > 8.0 && e.NewSize.Height > 8.0) + //if (e.NewSize.Width > 8.0 && e.NewSize.Height > 8.0) + //{ + // _renderer?.RequestResize(new((uint)e.NewSize.Width, (uint)e.NewSize.Height)); + //} + + _resizeTimer.Stop(); + _resizeTimer.Start(); + + _isResizing = true; + } + + private void OnResizeTimerTick(object? sender, object e) + { + _resizeTimer.Stop(); + _isResizing = false; + + var newWidth = (uint)(Panel.ActualWidth * Panel.CompositionScaleX); + var newHeight = (uint)(Panel.ActualHeight * Panel.CompositionScaleY); + + if (newWidth < 8 || newHeight < 8) { - _renderer?.RequestResize(new((uint)e.NewSize.Width, (uint)e.NewSize.Height)); + return; } + + _renderer?.RequestResize(new uint2(newWidth, newHeight)); } private void OnRendering(object? sender, object e) diff --git a/Ghost.Graphics/Core/RenderingContext.cs b/Ghost.Graphics/Core/RenderingContext.cs index 126c54b..b1d631a 100644 --- a/Ghost.Graphics/Core/RenderingContext.cs +++ b/Ghost.Graphics/Core/RenderingContext.cs @@ -64,15 +64,12 @@ public readonly unsafe ref struct RenderingContext var vertexHandle = meshData.VertexBuffer.AsResource(); var indexHandle = meshData.IndexBuffer.AsResource(); - _directCmd.ResourceBarrier(vertexHandle, ResourceState.Common, ResourceState.CopyDest); - _directCmd.ResourceBarrier(indexHandle, ResourceState.Common, ResourceState.CopyDest); + _directCmd.ResourceBarrier(vertexHandle, ResourceState.CopyDest); + _directCmd.ResourceBarrier(indexHandle, ResourceState.CopyDest); _directCmd.UploadBuffer(meshData.VertexBuffer, meshData.Vertices.AsSpan()); _directCmd.UploadBuffer(meshData.IndexBuffer, meshData.Indices.AsSpan()); - _directCmd.ResourceBarrier(vertexHandle, ResourceState.CopyDest, ResourceState.VertexAndConstantBuffer); - _directCmd.ResourceBarrier(indexHandle, ResourceState.CopyDest, ResourceState.IndexBuffer); - if (staticMesh) { meshData.ReleaseCpuResources(); @@ -101,41 +98,20 @@ public readonly unsafe ref struct RenderingContext /// Whether to mark the mesh as static. If it's true, the cpu buffer of the mesh will not be avaliable any more public void UploadMesh(Handle mesh, bool markMeshStatic) { - ref var meshData = ref ResourceDatabase.GetMeshReference(mesh); - var vertexState = ResourceDatabase.GetResourceState(meshData.VertexBuffer.AsResource()) - .GetValueOrThrow(ResultStatus.Success); - var indexState = ResourceDatabase.GetResourceState(meshData.IndexBuffer.AsResource()) - .GetValueOrThrow(ResultStatus.Success); + ref var meshRef = ref ResourceDatabase.GetMeshReference(mesh); - var needVertexTransition = vertexState != ResourceState.CopyDest; - var needIndexTransition = indexState != ResourceState.CopyDest; + _directCmd.ResourceBarrier(meshRef.VertexBuffer.AsResource(),ResourceState.CopyDest); + _directCmd.ResourceBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.CopyDest); - if (needVertexTransition) - { - _directCmd.ResourceBarrier(meshData.VertexBuffer.AsResource(), vertexState, ResourceState.CopyDest); - } + _directCmd.UploadBuffer(meshRef.VertexBuffer, meshRef.Vertices.AsSpan()); + _directCmd.UploadBuffer(meshRef.IndexBuffer, meshRef.Indices.AsSpan()); - if (needIndexTransition) - { - _directCmd.ResourceBarrier(meshData.IndexBuffer.AsResource(), indexState, ResourceState.CopyDest); - } - - _directCmd.UploadBuffer(meshData.VertexBuffer, meshData.Vertices.AsSpan()); - _directCmd.UploadBuffer(meshData.IndexBuffer, meshData.Indices.AsSpan()); - - if (needVertexTransition) - { - _directCmd.ResourceBarrier(meshData.VertexBuffer.AsResource(), ResourceState.CopyDest, vertexState); - } - - if (needIndexTransition) - { - _directCmd.ResourceBarrier(meshData.IndexBuffer.AsResource(), ResourceState.CopyDest, indexState); - } + _directCmd.ResourceBarrier(meshRef.VertexBuffer.AsResource(), ResourceState.NonPixelShaderResource); + _directCmd.ResourceBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.NonPixelShaderResource); if (markMeshStatic) { - meshData.ReleaseCpuResources(); + meshRef.ReleaseCpuResources(); } } @@ -152,21 +128,10 @@ public readonly unsafe ref struct RenderingContext }; var bufferHandle = meshData.ObjectDataBuffer.AsResource(); - var state = ResourceDatabase.GetResourceState(bufferHandle) - .GetValueOrThrow(ResultStatus.Success); - - var needTransition = state != ResourceState.CopyDest; - if (needTransition) - { - _directCmd.ResourceBarrier(bufferHandle, state, ResourceState.CopyDest); - } + _directCmd.ResourceBarrier(bufferHandle, ResourceState.CopyDest); _directCmd.UploadBuffer(meshData.ObjectDataBuffer, [data]); - - if (needTransition) - { - _directCmd.ResourceBarrier(bufferHandle, ResourceState.CopyDest, ResourceState.VertexAndConstantBuffer); - } + _directCmd.ResourceBarrier(bufferHandle, ResourceState.VertexAndConstantBuffer); } public Handle CreateTexture(ref readonly TextureDesc desc, ReadOnlySpan data, bool tempResource = false) @@ -191,15 +156,7 @@ public readonly unsafe ref struct RenderingContext desc.TextureDescription.Format.GetSurfaceInfo(desc.TextureDescription.Width, desc.TextureDescription.Height, out var rowPitch, out var slicePitch, out _); - var sateBefore = ResourceDatabase.GetResourceState(texture.AsResource()) - .GetValueOrThrow(ResultStatus.Success); - - var needTransition = sateBefore != ResourceState.CopyDest; - - if (needTransition) - { - _directCmd.ResourceBarrier(texture.AsResource(), sateBefore, ResourceState.CopyDest); - } + _directCmd.ResourceBarrier(texture.AsResource(), ResourceState.CopyDest); fixed (T* pData = data) { @@ -212,11 +169,6 @@ public readonly unsafe ref struct RenderingContext _directCmd.UploadTexture(texture, [subresourceData]); } - - if (needTransition) - { - _directCmd.ResourceBarrier(texture.AsResource(), ResourceState.CopyDest, sateBefore); - } } // TODO: Ideally we should queue the draw call to our rendering system, and render it in a full rendering pipeline. diff --git a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs b/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs index 9d519d5..c064d49 100644 --- a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs +++ b/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs @@ -291,6 +291,11 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer } ref var record = ref recordResult.Value; + if (record.state == stateAfter) + { + return; + } + var barrier = D3D12_RESOURCE_BARRIER.InitTransition(record.ResourcePtr, record.state.ToD3D12States(), stateAfter.ToD3D12States()); diff --git a/Ghost.Graphics/D3D12/D3D12CommandQueue.cs b/Ghost.Graphics/D3D12/D3D12CommandQueue.cs index b76e404..326645b 100644 --- a/Ghost.Graphics/D3D12/D3D12CommandQueue.cs +++ b/Ghost.Graphics/D3D12/D3D12CommandQueue.cs @@ -132,7 +132,7 @@ internal unsafe class D3D12CommandQueue : ICommandQueue ObjectDisposedException.ThrowIf(_disposed, this); _fenceValue = value; - _commandQueue.Get()->Signal((ID3D12Fence*)_fence.Get(), _fenceValue); + ThrowIfFailed(_commandQueue.Get()->Signal((ID3D12Fence*)_fence.Get(), _fenceValue)); return _fenceValue; } diff --git a/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs b/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs index 6b8a796..a6382a3 100644 --- a/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs +++ b/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs @@ -9,6 +9,8 @@ namespace Ghost.Graphics.D3D12; internal class D3D12GraphicsEngine : IGraphicsEngine { + private readonly IRenderSystem _renderSystem; + #if DEBUG private readonly D3D12DebugLayer _debugLayer; #endif @@ -34,6 +36,8 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public D3D12GraphicsEngine(IRenderSystem renderSystem) { + _renderSystem = renderSystem; + #if DEBUG _debugLayer = new D3D12DebugLayer(); #endif @@ -106,10 +110,10 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public ISwapChain CreateSwapChain(SwapChainDesc desc) { ThrowIfDisposed(); - return new D3D12SwapChain(_resourceDatabase, _descriptorAllocator, _device, desc); + return new D3D12SwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _renderSystem.MaxFrameLatency); } - public void BeginFrame() + public void RenderFrame() { ThrowIfDisposed(); @@ -119,21 +123,11 @@ internal class D3D12GraphicsEngine : IGraphicsEngine } _copyCommandBuffer.Begin(); - } - - public void RenderFrame() - { - ThrowIfDisposed(); foreach (var renderer in _renderers) { renderer.Render(); } - } - - public void EndFrame() - { - ThrowIfDisposed(); _copyCommandBuffer.End().ThrowIfFailed(); _resourceAllocator.ReleaseTempResources(); diff --git a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs index c89e95d..ebac75c 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs @@ -136,7 +136,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase resource = default!; } - public unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceState initialState, ResourceViewGroup viewGroup, string name = "") + public unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceState initialState, ResourceViewGroup viewGroup, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -144,14 +144,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase var handle = new Handle(id, generation); #if DEBUG || GHOST_EDITOR - pResource->SetName(name); - _resourceName[handle] = name; + if (!string.IsNullOrEmpty(name)) + { + pResource->SetName(name); + _resourceName[handle] = name; + } #endif return handle; } - public unsafe Handle AddResource(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState initialState, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name = "") + public unsafe Handle AddResource(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState initialState, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -159,8 +162,11 @@ internal class D3D12ResourceDatabase : IResourceDatabase var handle = new Handle(id, generation); #if DEBUG || GHOST_EDITOR - allocation->SetName(name); - _resourceName[handle] = name; + if (!string.IsNullOrEmpty(name)) + { + allocation->SetName(name); + _resourceName[handle] = name; + } #endif return handle; diff --git a/Ghost.Graphics/D3D12/D3D12SwapChain.cs b/Ghost.Graphics/D3D12/D3D12SwapChain.cs index 1bd4d75..50562bc 100644 --- a/Ghost.Graphics/D3D12/D3D12SwapChain.cs +++ b/Ghost.Graphics/D3D12/D3D12SwapChain.cs @@ -42,26 +42,20 @@ internal unsafe class D3D12SwapChain : ISwapChain get; private set; } - public uint BufferCount + public D3D12SwapChain(D3D12ResourceDatabase resourceDatabase, D3D12DescriptorAllocator descriptorAllocator, D3D12RenderDevice device, SwapChainDesc desc, uint bufferCount) { - get; - } - - public D3D12SwapChain(D3D12ResourceDatabase resourceDatabase, D3D12DescriptorAllocator descriptorAllocator, D3D12RenderDevice device, SwapChainDesc desc) - { - Debug.Assert(desc.BufferCount >= 2); + Debug.Assert(bufferCount >= 2); _resourceDatabase = resourceDatabase; _descriptorAllocator = descriptorAllocator; _renderDevice = device; - _backBuffers = new UnsafeArray>((int)desc.BufferCount, Allocator.Persistent); + _backBuffers = new UnsafeArray>((int)bufferCount, Allocator.Persistent); Width = desc.Width; Height = desc.Height; - BufferCount = desc.BufferCount; - CreateSwapChain(desc); + CreateSwapChain(desc, bufferCount); CreateBackBuffers(); _compositionSurface = desc.Target.CompositionSurface; @@ -72,7 +66,7 @@ internal unsafe class D3D12SwapChain : ISwapChain Dispose(); } - private void CreateSwapChain(SwapChainDesc desc) + private void CreateSwapChain(SwapChainDesc desc, uint bufferCount) { var swapChainDesc = new DXGI_SWAP_CHAIN_DESC1 { @@ -81,7 +75,7 @@ internal unsafe class D3D12SwapChain : ISwapChain Format = desc.Format.ToDXGIFormat(), SampleDesc = new DXGI_SAMPLE_DESC(1, 0), BufferUsage = DXGI_USAGE_BACK_BUFFER | DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount = desc.BufferCount, + BufferCount = bufferCount, Scaling = DXGI_SCALING_STRETCH, SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD, AlphaMode = DXGI_ALPHA_MODE_IGNORE, @@ -135,7 +129,7 @@ internal unsafe class D3D12SwapChain : ISwapChain private void CreateBackBuffers() { - for (uint i = 0; i < BufferCount; i++) + for (uint i = 0; i < _backBuffers.Count; i++) { ID3D12Resource* pBackBuffer = default; ThrowIfFailed(_swapChain.Get()->GetBuffer(i, __uuidof(pBackBuffer), (void**)&pBackBuffer)); @@ -186,12 +180,26 @@ internal unsafe class D3D12SwapChain : ISwapChain _resourceDatabase.ReleaseResource(_backBuffers[i].AsResource()); } - ThrowIfFailed(_swapChain.Get()->ResizeBuffers(BufferCount, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, (uint)DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING)); + ThrowIfFailed(_swapChain.Get()->ResizeBuffers((uint)_backBuffers.Count, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, (uint)DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING)); Width = width; Height = height; CreateBackBuffers(); + + //float inverseScale = 1.0f / scale; + + //DXGI_MATRIX_3X2_F inverseScaleMatrix = new DXGI_MATRIX_3X2_F + //{ + // _11 = inverseScale, // Scale X + // _22 = inverseScale, // Scale Y + // _12 = 0.0f, + // _21 = 0.0f, + // _31 = 0.0f, // Offset X + // _32 = 0.0f // Offset Y + //}; + + //_swapChain.Get()->SetMatrixTransform(&inverseScaleMatrix); } public void Dispose() diff --git a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs index 6db8992..7140d48 100644 --- a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs +++ b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs @@ -11,7 +11,7 @@ internal unsafe static class D3D12Utility public static void SetName(ref this T obj, ReadOnlySpan name) where T : unmanaged, ID3D12Object.Interface { - if (name.Length == 0) + if (name.IsEmpty) { return; } @@ -24,6 +24,11 @@ internal unsafe static class D3D12Utility public static void SetName(ref this D3D12MA_Allocation obj, ReadOnlySpan name) { + if (name.IsEmpty) + { + return; + } + fixed (char* pName = name) { obj.SetName(pName); @@ -84,6 +89,9 @@ internal unsafe static class D3D12Utility ResourceState.PixelShaderResource => D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, ResourceState.CopyDest => D3D12_RESOURCE_STATE_COPY_DEST, ResourceState.CopySource => D3D12_RESOURCE_STATE_COPY_SOURCE, + ResourceState.GenericRead => D3D12_RESOURCE_STATE_GENERIC_READ, + ResourceState.IndirectArgument => D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT, + ResourceState.NonPixelShaderResource => D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, _ => throw new ArgumentException($"Unknown resource state: {state}") }; } diff --git a/Ghost.Graphics/RHI/Common.cs b/Ghost.Graphics/RHI/Common.cs index c93608f..50a82fb 100644 --- a/Ghost.Graphics/RHI/Common.cs +++ b/Ghost.Graphics/RHI/Common.cs @@ -2,6 +2,7 @@ using Ghost.Core; using Ghost.Core.Graphics; using Ghost.Graphics.Core; using Ghost.Graphics.D3D12.Utilities; +using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Utilities; using System.IO.Hashing; using System.Runtime.CompilerServices; @@ -203,6 +204,43 @@ public readonly struct CBufferInfo } } +public struct RenderDesc +{ + public float4x4 ViewMatrix + { + get; set; + } + + public float4x4 ProjectionMatrix + { + get; set; + } + + public float4 CameraPosition + { + get; set; + } + + // The "Target" (Where to write pixels) + public Handle Target + { + get; set; + } + + public Handle DepthTarget + { + get; set; + } + + public RectDesc Viewport + { + get; set; + } + + //public RenderPathID RenderPath; + //public LayerMask CullingMask; +} + public struct ViewportDesc { public float X @@ -737,11 +775,6 @@ public struct SwapChainDesc { get; set; } - - public uint BufferCount - { - get; set; - } } /// @@ -819,6 +852,7 @@ public enum ResourceState CopySource = 1 << 8, GenericRead = 1 << 9, IndirectArgument = 1 << 10, + NonPixelShaderResource = 1 << 11, Present = 0, } diff --git a/Ghost.Graphics/RHI/IGraphicsEngine.cs b/Ghost.Graphics/RHI/IGraphicsEngine.cs index e50c333..4048e0f 100644 --- a/Ghost.Graphics/RHI/IGraphicsEngine.cs +++ b/Ghost.Graphics/RHI/IGraphicsEngine.cs @@ -1,3 +1,4 @@ +using Ghost.Core; using Ghost.Graphics.Contracts; namespace Ghost.Graphics.RHI; @@ -47,18 +48,8 @@ public interface IGraphicsEngine : IDisposable /// A new swap chain instance ISwapChain CreateSwapChain(SwapChainDesc desc); - /// - /// Begins a new rendering frame, preparing the graphics context for drawing operations. - /// - void BeginFrame(); - /// /// Renders the current frame. /// void RenderFrame(); - - /// - /// Completes the current rendering frame and performs any necessary finalization steps. - /// - void EndFrame(); } diff --git a/Ghost.Graphics/RHI/ISwapChain.cs b/Ghost.Graphics/RHI/ISwapChain.cs index 4a56f27..417b3d4 100644 --- a/Ghost.Graphics/RHI/ISwapChain.cs +++ b/Ghost.Graphics/RHI/ISwapChain.cs @@ -24,14 +24,6 @@ public interface ISwapChain : IDisposable get; } - /// - /// Number of back buffers - /// - public uint BufferCount - { - get; - } - /// /// Gets the current back buffer texture /// diff --git a/Ghost.Graphics/RenderSystem.cs b/Ghost.Graphics/RenderSystem.cs index 18ddaad..36b5eea 100644 --- a/Ghost.Graphics/RenderSystem.cs +++ b/Ghost.Graphics/RenderSystem.cs @@ -1,3 +1,4 @@ +using Ghost.Graphics.D3D12; using Ghost.Graphics.RHI; namespace Ghost.Graphics; @@ -22,28 +23,28 @@ public struct RenderingConfig public interface IFenceSynchronizer { - public uint CPUFenceValue + uint CPUFenceValue { get; } - public uint GPUFenceValue + uint GPUFenceValue { get; } - public uint FrameIndex + uint FrameIndex { get; } - public uint MaxFrameLatency + uint MaxFrameLatency { get; } - public bool WaitForGPUReady(int timeOut = -1); - public void SignalCPUReady(); + bool WaitForGPUReady(int timeOut = -1); + void SignalCPUReady(); } public interface IRenderSystem : IFenceSynchronizer, IDisposable @@ -68,18 +69,18 @@ public interface IRenderSystem : IFenceSynchronizer, IDisposable /// internal class RenderSystem : IRenderSystem { - private readonly struct FrameResource : IDisposable + private struct FrameResource : IDisposable { public readonly AutoResetEvent cpuReadyEvent; public readonly AutoResetEvent gpuReadyEvent; - public FrameResource() + public FrameResource(ICommandBuffer cmd) { cpuReadyEvent = new AutoResetEvent(false); gpuReadyEvent = new AutoResetEvent(true); } - public void Dispose() + public readonly void Dispose() { cpuReadyEvent.Dispose(); gpuReadyEvent.Dispose(); @@ -208,9 +209,18 @@ internal class RenderSystem : IRenderSystem // Only proceed if CPU ready event was signaled if (waitResult == 0) { - _graphicsEngine.BeginFrame(); _graphicsEngine.RenderFrame(); - _graphicsEngine.EndFrame(); +// if (result.IsFailure) +// { +// // Terminate the render loop on failure +// _isRunning = false; +//#if DEBUG +// throw new InvalidOperationException($"RenderFrame failed: {result.Message}"); +//#else +// Logger.LogError($"RenderFrame failed: {result.Message}"); +// break; +//#endif +// } _gpuFenceValue++; frameResource.gpuReadyEvent.Set(); diff --git a/Ghost.RenderGraph.Concept/ALIASING.md b/Ghost.RenderGraph.Concept/ALIASING.md new file mode 100644 index 0000000..179e43b --- /dev/null +++ b/Ghost.RenderGraph.Concept/ALIASING.md @@ -0,0 +1,177 @@ +# Resource Aliasing in Render Graph + +## Overview + +Resource aliasing is a memory optimization technique where multiple virtual resources share the same physical memory allocation. This significantly reduces memory usage for transient resources. + +## How It Works + +### 1. Lifetime Analysis +The render graph analyzes when each transient resource is first used and last used: +``` +GBuffer.Albedo: [0..1] ━━━━━━━━ +SSAO: [2..4] ━━━━━━━━━━━ +``` + +### 2. Aliasing Detection +Resources with non-overlapping lifetimes can share memory: +``` +Physical_Texture_2: + [0..1] GBuffer.Albedo ━━━━━━━━ + [2..4] SSAO ━━━━━━━━━━━ ← ALIAS! +``` + +### 3. Memory Allocation +Instead of creating 2 separate 8MB textures (16MB total), we create 1 physical allocation (8MB) that both virtual resources map to. + +## Aliasing Barriers + +In D3D12/Vulkan, when you reuse memory for a different resource, you must insert an **aliasing barrier** to inform the GPU that the memory interpretation has changed. + +### When Aliasing Barriers Are Needed + +An aliasing barrier is required when: +1. Two or more resources share the same physical memory +2. You're switching from one resource to another +3. Both resources are accessed within overlapping command buffer scopes + +In this implementation, aliasing barriers are automatically inserted when: +- A pass accesses a resource that shares a physical allocation +- A different resource was previously active on that allocation +- The active resource hasn't been explicitly destroyed + +## Example Output + +``` +[RG] ===== RESOURCE ALIASING ANALYSIS ===== +[ALLOC] 'GBuffer.Albedo' gets new allocation 'Physical_Texture_2' (size: 8.29 MB, lifetime: [0..1]) +[ALIAS] 'SSAO' aliases with 'Physical_Texture_2' (offset: 0, size: 8.29 MB, lifetime: [2..4]) +[ALIAS] 'BloomDownsample' aliases with 'Physical_Texture_1' (offset: 0, size: 8.29 MB, lifetime: [3..5]) + +[RG] Memory Statistics: + Total memory without aliasing: 80.64 MB + Total memory with aliasing: 47.46 MB + Memory saved: 33.18 MB (41.1%) + Allocations: 5 physical allocations for 8 resources +``` + +## Aliasing Algorithm + +The allocator uses a **First-Fit** strategy: + +```csharp +foreach (var resource in transientResources.OrderBy(FirstUse).ThenBy(Size)) +{ + // Try to find existing allocation + foreach (var slot in allocationSlots) + { + if (slot.LargeEnough && + slot.SameType && + !HasLifetimeOverlap(slot, resource)) + { + // REUSE! + slot.AddResource(resource); + return; + } + } + + // No compatible slot found, create new allocation + CreateNewAllocation(resource); +} +``` + +### Key Constraints + +1. **Size**: The physical allocation must be >= required size +2. **Type**: Textures can only alias with textures, buffers with buffers +3. **Lifetime**: Resources must have non-overlapping lifetimes +4. **Alignment**: Resources must satisfy GPU alignment requirements + +## Real-World Benefits + +### Deferred Rendering Pipeline + +| Resource | Size | Lifetime | Physical Alloc | +|----------|------|----------|----------------| +| GBuffer.Albedo | 8MB | [0..1] | Physical_1 | +| GBuffer.Normal | 16MB | [0..2] | Physical_2 | +| GBuffer.Depth | 8MB | [0..2] | Physical_3 | +| Lighting | 16MB | [1..3] | Physical_4 | +| SSAO | 8MB | [2..4] | **Physical_1** ✓ | +| TAA | 16MB | [3..4] | **Physical_2** ✓ | +| Bloom | 8MB | [3..5] | **Physical_3** ✓ | + +**Without aliasing**: 80MB +**With aliasing**: 48MB +**Savings**: 40% (32MB) + +### At 4K Resolution (3840x2160) + +| Format | Size (1080p) | Size (4K) | +|--------|-------------|-----------| +| RGBA8 | 8.3 MB | 33.2 MB | +| RGBA16F | 16.6 MB | 66.4 MB | +| Depth32F | 8.3 MB | 33.2 MB | + +**4K Savings**: 128MB → Modern AAA games save **hundreds of megabytes** to **gigabytes** using this technique. + +## Advanced Optimizations (Not Implemented) + +### 1. Pool-Based Allocation +Instead of individual allocations, use large memory pools and sub-allocate from them. + +### 2. Heap-Aware Aliasing +D3D12 has specific heap types (Default, Upload, Readback). Resources can only alias within the same heap type. + +### 3. Subresource Aliasing +Alias mip levels or array slices independently for more granular reuse. + +### 4. Multi-Queue Aliasing +Resources on different queues (Graphics, Compute, Copy) need special synchronization. + +## Comparison with Production Systems + +### Unreal Engine 5 RDG +```cpp +// Automatic aliasing based on lifetimes +FRDGTextureRef TextureA = GraphBuilder.CreateTexture(Desc, TEXT("A")); +FRDGTextureRef TextureB = GraphBuilder.CreateTexture(Desc, TEXT("B")); +// RDG automatically aliases if lifetimes don't overlap +``` + +### Frostbite Frame Graph +- Uses explicit aliasing groups +- Developers can hint which resources should share memory +- More control but less automatic + +### Unity HDRP Render Graph +```csharp +// Unity's approach (similar to ours) +var tempRT = renderGraph.CreateTexture(desc); +// Automatic aliasing through lifetime analysis +``` + +## Performance Impact + +**Memory**: 30-50% reduction typical +**CPU Overhead**: <1ms during compile phase +**GPU Performance**: Same or better (fewer allocations) +**Bandwidth**: Reduced due to better cache locality + +## Debugging Tips + +1. **Print Allocation Map**: See which resources share memory +2. **Visualize Lifetimes**: Graph timeline to spot aliasing opportunities +3. **Track Peak Memory**: Identify frames with poor aliasing +4. **Monitor Aliasing Barriers**: Too many can hurt performance + +## Conclusion + +Resource aliasing is a critical optimization in modern rendering. This proof of concept demonstrates: +- ✅ Automatic lifetime analysis +- ✅ First-fit allocation strategy +- ✅ Type-safe aliasing (textures vs buffers) +- ✅ Memory savings tracking +- ✅ Aliasing barrier insertion points (simulated) + +For production use, integrate with actual D3D12/Vulkan memory heaps and implement proper aliasing barriers as defined by the API specifications. diff --git a/Ghost.RenderGraph.Concept/API_DESIGN.md b/Ghost.RenderGraph.Concept/API_DESIGN.md new file mode 100644 index 0000000..5f7c3cc --- /dev/null +++ b/Ghost.RenderGraph.Concept/API_DESIGN.md @@ -0,0 +1,189 @@ +# Render Graph API Design Summary + +## Overview + +This render graph implementation uses a **production-grade API design** inspired by Unity HDRP's render graph, focusing on performance and usability. + +## Core Design Principles + +### 1. **Typed Pass Data > String Lookups** + +❌ **Anti-pattern** (slow, error-prone): +```csharp +builder.SetRenderFunc(cmd => { + cmd.SetRenderTarget("GBuffer.Albedo"); // String lookup! +}); +``` + +✅ **Best practice** (fast, type-safe): +```csharp +builder.SetRenderFunc((data, cmd) => { + cmd.SetRenderTarget(data.Albedo.Name); // Direct field access! +}); +``` + +### 2. **Blackboard for Complex Data** + +When multiple passes need the same resources (like GBuffer with albedo, normal, depth): + +```csharp +class GBufferData { + public RenderGraphTextureHandle Albedo = null!; + public RenderGraphTextureHandle Normal = null!; + public RenderGraphTextureHandle Depth = null!; +} + +// Producer pass +using (var builder = renderGraph.AddRenderPass("GBuffer", out var data)) { + data.Albedo = builder.WriteTexture(builder.CreateTexture(...)); + data.Normal = builder.WriteTexture(builder.CreateTexture(...)); + data.Depth = builder.UseDepthBuffer(builder.CreateTexture(...), true); + builder.SetRenderFunc((d, cmd) => { /* use d.Albedo, d.Normal, d.Depth */ }); +} +renderGraph.Blackboard.Add(gbufferData); + +// Consumer passes +using (var builder = renderGraph.AddRenderPass("Lighting", out var data)) { + var gbuffer = renderGraph.Blackboard.Get(); + data.Albedo = builder.ReadTexture(gbuffer.Albedo); + data.Normal = builder.ReadTexture(gbuffer.Normal); + // ... +} +``` + +### 3. **Direct Handle Passing for Simple Cases** + +When passing a single texture between two passes: + +```csharp +// Pass 1: Return handle +RenderGraphTextureHandle lightingOutput; +using (var builder = renderGraph.AddRenderPass("Lighting", out var data)) { + lightingOutput = builder.CreateTexture(...); + data.Output = builder.WriteTexture(lightingOutput); + builder.SetRenderFunc((d, cmd) => { /* ... */ }); +} + +// Pass 2: Use handle directly +using (var builder = renderGraph.AddRenderPass("TAA", out var data)) { + data.Input = builder.ReadTexture(lightingOutput); // Direct pass! + builder.SetRenderFunc((d, cmd) => { /* ... */ }); +} +``` + +## Performance Benefits + +| Aspect | Traditional String-Based | Typed Pass Data | +|--------|-------------------------|-----------------| +| **Resource Access** | Dictionary lookup | Direct field access | +| **Type Safety** | Runtime errors | Compile-time checks | +| **Refactoring** | Find & Replace | Compiler-assisted | +| **IDE Support** | Limited | Full IntelliSense | +| **Performance** | Hash lookup overhead | Zero overhead | + +## Real-World Example + +Here's how a complete deferred rendering pipeline looks: + +```csharp +// 1. GBuffer Pass - produce multiple outputs +GBufferData gbufferData; +using (var builder = renderGraph.AddRenderPass("GBuffer", out gbufferData)) { + gbufferData.Albedo = builder.WriteTexture(builder.CreateTexture(...)); + gbufferData.Normal = builder.WriteTexture(builder.CreateTexture(...)); + gbufferData.Depth = builder.UseDepthBuffer(builder.CreateTexture(...), true); + builder.SetRenderFunc((data, cmd) => { /* render geometry */ }); +} +renderGraph.Blackboard.Add(gbufferData); + +// 2. Lighting Pass - consume GBuffer, produce lighting +RenderGraphTextureHandle lightingOutput; +using (var builder = renderGraph.AddRenderPass("Lighting", out var data)) { + var gbuffer = renderGraph.Blackboard.Get(); + data.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo); + data.GBufferNormal = builder.ReadTexture(gbuffer.Normal); + data.GBufferDepth = builder.ReadTexture(gbuffer.Depth); + + lightingOutput = builder.CreateTexture(...); + data.Output = builder.WriteTexture(lightingOutput); + builder.SetRenderFunc((data, cmd) => { /* deferred lighting */ }); +} + +// 3. SSAO Pass - also consume GBuffer +RenderGraphTextureHandle ssaoOutput; +using (var builder = renderGraph.AddRenderPass("SSAO", out var data)) { + var gbuffer = renderGraph.Blackboard.Get(); + data.Depth = builder.ReadTexture(gbuffer.Depth); + data.Normal = builder.ReadTexture(gbuffer.Normal); + + ssaoOutput = builder.CreateTexture(...); + data.Output = builder.WriteTexture(ssaoOutput); + builder.SetRenderFunc((data, cmd) => { /* SSAO */ }); +} + +// 4. Post Processing - combine lighting + SSAO +using (var builder = renderGraph.AddRenderPass("Post", out var data)) { + data.Lighting = builder.ReadTexture(lightingOutput); // Direct handle + data.SSAO = builder.ReadTexture(ssaoOutput); // Direct handle + data.Output = builder.WriteTexture(backbuffer); + builder.SetRenderFunc((data, cmd) => { /* combine */ }); +} + +renderGraph.Compile(); +renderGraph.Execute(); +``` + +## When to Use What? + +| Scenario | Use | Example | +|----------|-----|---------| +| Multiple outputs used by many passes | **Blackboard** | GBuffer (albedo, normal, depth) | +| Single texture passed to next pass | **Direct Handle** | Lighting → TAA | +| Temporary working data | **Pass Data** | Intermediate blur textures | +| Persistent frame data | **Import** | Backbuffer, history textures | + +## Comparison with Other Systems + +### Unity HDRP +```csharp +// Unity's API (very similar!) +using (var builder = renderGraph.AddRenderPass("MyPass", out var passData)) +{ + passData.input = builder.ReadTexture(inputHandle); + passData.output = builder.WriteTexture(outputHandle); + builder.SetRenderFunc((data, ctx) => { /* ... */ }); +} +``` + +### Unreal Engine 5 RDG +```cpp +// Unreal's API (similar concepts, different syntax) +FRDGTextureRef Output = GraphBuilder.CreateTexture(Desc, TEXT("Output")); +AddPass(GraphBuilder, "MyPass", Parameters, + [Parameters](FRHICommandList& RHICmdList) { + // Execute + }); +``` + +### Frostbite +```cpp +// Frostbite (original frame graph paper) +FrameGraphTextureHandle output = frameGraph.create("Output", desc); +frameGraph.addPass("MyPass", + [&](FrameGraphBuilder& builder) { + builder.write(output); + }, + [=](const Resources& resources, CommandBuffer& cmd) { + // Execute + }); +``` + +## Conclusion + +This API design prioritizes: +1. **Performance**: Zero-cost abstractions with direct field access +2. **Safety**: Compile-time type checking +3. **Ergonomics**: Natural C# patterns (using blocks, typed data) +4. **Flexibility**: Blackboard for complex data, handles for simple cases + +It matches industry-standard patterns from Unity, Unreal, and Frostbite while leveraging C#'s type system for maximum safety and performance. diff --git a/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj b/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj new file mode 100644 index 0000000..ed9781c --- /dev/null +++ b/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/Ghost.RenderGraph.Concept/ICommandBuffer.cs b/Ghost.RenderGraph.Concept/ICommandBuffer.cs new file mode 100644 index 0000000..827cf4f --- /dev/null +++ b/Ghost.RenderGraph.Concept/ICommandBuffer.cs @@ -0,0 +1,86 @@ +namespace Ghost.RenderGraph.Concept; + +public interface ICommandBuffer +{ + void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState); + void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName); + void BeginRenderPass(string passName); + void EndRenderPass(); + void SetRenderTarget(string textureName); + void SetDepthStencil(string textureName); + void BindShaderResource(string resourceName, int slot); + void BindUnorderedAccess(string resourceName, int slot); + void Draw(int vertexCount); + void Dispatch(int x, int y, int z); + void ClearRenderTarget(string textureName, float r, float g, float b, float a); + void ClearDepth(string textureName, float depth); + void CopyTexture(string source, string destination); +} + +public class SimulatedCommandBuffer : ICommandBuffer +{ + public void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState) + { + Console.WriteLine($" [BARRIER] Transition '{resourceName}' from {beforeState} to {afterState}"); + } + + public void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName) + { + Console.WriteLine($" [ALIAS_BARRIER] Alias '{physicalAllocationName}': '{beforeResourceName}' -> '{afterResourceName}'"); + } + + public void BeginRenderPass(string passName) + { + Console.WriteLine($" [BEGIN] RenderPass '{passName}'"); + } + + public void EndRenderPass() + { + Console.WriteLine($" [END] RenderPass"); + } + + public void SetRenderTarget(string textureName) + { + Console.WriteLine($" [RT] Set RenderTarget: '{textureName}'"); + } + + public void SetDepthStencil(string textureName) + { + Console.WriteLine($" [DS] Set DepthStencil: '{textureName}'"); + } + + public void BindShaderResource(string resourceName, int slot) + { + Console.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}"); + } + + public void BindUnorderedAccess(string resourceName, int slot) + { + Console.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}"); + } + + public void Draw(int vertexCount) + { + Console.WriteLine($" [DRAW] Drawing {vertexCount} vertices"); + } + + public void Dispatch(int x, int y, int z) + { + Console.WriteLine($" [DISPATCH] Compute ({x}, {y}, {z})"); + } + + public void ClearRenderTarget(string textureName, float r, float g, float b, float a) + { + Console.WriteLine($" [CLEAR_RT] Clear '{textureName}' to ({r}, {g}, {b}, {a})"); + } + + public void ClearDepth(string textureName, float depth) + { + Console.WriteLine($" [CLEAR_DEPTH] Clear '{textureName}' to {depth}"); + } + + public void CopyTexture(string source, string destination) + { + Console.WriteLine($" [COPY] Copy from '{source}' to '{destination}'"); + } +} diff --git a/Ghost.RenderGraph.Concept/PassData.cs b/Ghost.RenderGraph.Concept/PassData.cs new file mode 100644 index 0000000..a080ade --- /dev/null +++ b/Ghost.RenderGraph.Concept/PassData.cs @@ -0,0 +1,58 @@ +namespace Ghost.RenderGraph.Concept; + +// Pass data structure for GBuffer outputs +public class GBufferData +{ + public RenderGraphTextureHandle Albedo = null!; + public RenderGraphTextureHandle Normal = null!; + public RenderGraphTextureHandle Depth = null!; +} + +public class LightingPassData +{ + public RenderGraphTextureHandle GBufferAlbedo = null!; + public RenderGraphTextureHandle GBufferNormal = null!; + public RenderGraphTextureHandle GBufferDepth = null!; + public RenderGraphTextureHandle OutputLighting = null!; +} + +public class SSAOPassData +{ + public RenderGraphTextureHandle GBufferDepth = null!; + public RenderGraphTextureHandle GBufferNormal = null!; + public RenderGraphTextureHandle OutputSSAO = null!; +} + +public class TAAPassData +{ + public RenderGraphTextureHandle InputLighting = null!; + public RenderGraphTextureHandle OutputTAA = null!; +} + +public class PostProcessingPassData +{ + public RenderGraphTextureHandle InputTAA = null!; + public RenderGraphTextureHandle InputSSAO = null!; + public RenderGraphTextureHandle OutputBackbuffer = null!; +} + +public class DebugPassData +{ + public RenderGraphTextureHandle DebugTexture = null!; +} + +public class ProfilerMarkerData { } + +public class BloomDownsampleData +{ + public RenderGraphTextureHandle Input = null!; + public RenderGraphTextureHandle Output = null!; +} + +public class PostProcessingPassDataV2 +{ + public RenderGraphTextureHandle InputTAA = null!; + public RenderGraphTextureHandle InputSSAO = null!; + public RenderGraphTextureHandle InputBloom = null!; + public RenderGraphTextureHandle OutputBackbuffer = null!; +} diff --git a/Ghost.RenderGraph.Concept/Program.cs b/Ghost.RenderGraph.Concept/Program.cs new file mode 100644 index 0000000..c489399 --- /dev/null +++ b/Ghost.RenderGraph.Concept/Program.cs @@ -0,0 +1,177 @@ +using Ghost.RenderGraph.Concept; + +Console.WriteLine("=================================================="); +Console.WriteLine(" Transient Render Graph - Proof of Concept"); +Console.WriteLine(" Using Typed Pass Data and Blackboard Pattern"); +Console.WriteLine("==================================================\n"); + +var renderGraph = new RenderGraph(); + +// Import external resources +var backbuffer = renderGraph.ImportTexture( + "Backbuffer", + new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer")); + +// ===== GBuffer Pass ===== +GBufferData gbufferData; +using (var builder = renderGraph.AddRenderPass("GBuffer Pass", out gbufferData)) +{ + // Create GBuffer textures + var albedo = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo")); + var normal = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal")); + var depth = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth")); + + // Store in pass data and mark as written + gbufferData.Albedo = builder.WriteTexture(albedo); + gbufferData.Normal = builder.WriteTexture(normal); + gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true); + + builder.SetRenderFunc((data, cmd) => + { + cmd.SetRenderTarget(data.Albedo.Name); + cmd.SetRenderTarget(data.Normal.Name); + cmd.SetDepthStencil(data.Depth.Name); + cmd.ClearRenderTarget(data.Albedo.Name, 0, 0, 0, 1); + cmd.ClearRenderTarget(data.Normal.Name, 0.5f, 0.5f, 1.0f, 1); + cmd.ClearDepth(data.Depth.Name, 1.0f); + cmd.Draw(36000); + }); +} + +// Store GBuffer data in blackboard for other passes +renderGraph.Blackboard.Add(gbufferData); + +// ===== Lighting Pass ===== +RenderGraphTextureHandle lightingOutput; +using (var builder = renderGraph.AddRenderPass("Lighting Pass", out var lightingData)) +{ + // Read GBuffer from blackboard + var gbuffer = renderGraph.Blackboard.Get(); + + lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo); + lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal); + lightingData.GBufferDepth = builder.ReadTexture(gbuffer.Depth); + + // Create output texture + lightingOutput = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult")); + lightingData.OutputLighting = builder.WriteTexture(lightingOutput); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.GBufferAlbedo.Name, 0); + cmd.BindShaderResource(data.GBufferNormal.Name, 1); + cmd.BindShaderResource(data.GBufferDepth.Name, 2); + cmd.SetRenderTarget(data.OutputLighting.Name); + cmd.Draw(3); + }); +} + +// ===== SSAO Pass ===== +RenderGraphTextureHandle ssaoOutput; +using (var builder = renderGraph.AddRenderPass("SSAO Pass", out var ssaoData)) +{ + var gbuffer = renderGraph.Blackboard.Get(); + + ssaoData.GBufferDepth = builder.ReadTexture(gbuffer.Depth); + ssaoData.GBufferNormal = builder.ReadTexture(gbuffer.Normal); + + // This will reuse GBuffer.Albedo's memory allocation + ssaoOutput = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO")); + ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.GBufferDepth.Name, 0); + cmd.BindShaderResource(data.GBufferNormal.Name, 1); + cmd.SetRenderTarget(data.OutputSSAO.Name); + cmd.Draw(3); + }); +} + +// ===== Bloom Downsample Pass (will alias with albedo) ===== +RenderGraphTextureHandle bloomOutput; +using (var builder = renderGraph.AddRenderPass("Bloom Downsample", out var bloomData)) +{ + bloomData.Input = builder.ReadTexture(lightingOutput); + + // Create a texture that will alias with SSAO (same size, same format) + bloomOutput = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample")); + bloomData.Output = builder.WriteTexture(bloomOutput); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.Input.Name, 0); + cmd.SetRenderTarget(data.Output.Name); + cmd.Draw(3); + }); +} + +// ===== Temporal AA Pass ===== +RenderGraphTextureHandle taaOutput; +using (var builder = renderGraph.AddRenderPass("Temporal AA", out var taaData)) +{ + taaData.InputLighting = builder.ReadTexture(lightingOutput); + + taaOutput = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result")); + taaData.OutputTAA = builder.WriteTexture(taaOutput); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.InputLighting.Name, 0); + cmd.SetRenderTarget(data.OutputTAA.Name); + cmd.Draw(3); + }); +} + +// ===== Post Processing Pass ===== +using (var builder = renderGraph.AddRenderPass("Post Processing", out var postData)) +{ + postData.InputTAA = builder.ReadTexture(taaOutput); + postData.InputSSAO = builder.ReadTexture(ssaoOutput); + postData.InputBloom = builder.ReadTexture(bloomOutput); + postData.OutputBackbuffer = builder.WriteTexture(backbuffer); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.InputTAA.Name, 0); + cmd.BindShaderResource(data.InputSSAO.Name, 1); + cmd.BindShaderResource(data.InputBloom.Name, 2); + cmd.SetRenderTarget(data.OutputBackbuffer.Name); + cmd.Draw(3); + }); +} + +// ===== GPU Profiler Marker Pass (non-cullable, textureless) ===== +using (var builder = renderGraph.AddRenderPass("GPU Profiler Begin Frame", out var profilerData)) +{ + builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling + builder.SetRenderFunc((data, cmd) => + { + Console.WriteLine(" [PROFILER] BeginEvent('Frame')"); + }); +} + +// ===== Unused Debug Pass (will be culled) ===== +using (var builder = renderGraph.AddRenderPass("Unused Debug Pass", out var debugData)) +{ + debugData.DebugTexture = builder.WriteTexture( + builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture"))); + + builder.SetRenderFunc((data, cmd) => + { + cmd.SetRenderTarget(data.DebugTexture.Name); + cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1); + cmd.Draw(100); + }); +} + +// Compile and execute the render graph +renderGraph.Compile(); +renderGraph.Execute(); + +Console.WriteLine("\nPress any key to exit..."); +Console.ReadKey(); diff --git a/Ghost.RenderGraph.Concept/README.md b/Ghost.RenderGraph.Concept/README.md new file mode 100644 index 0000000..01d8b68 --- /dev/null +++ b/Ghost.RenderGraph.Concept/README.md @@ -0,0 +1,306 @@ +# Transient Render Graph - Proof of Concept + +This is a high-level proof of concept implementation of a modern transient render graph system, inspired by: +- **Unreal Engine 5 RDG** (Render Dependency Graph) +- **Frostbite Frame Graph** +- **Unity HDRP Render Graph** + +## Key Features + +### 1. **Resource Virtualization** +Resources are declared during setup but not physically created until execution. This allows the graph to analyze the entire frame before committing to resource allocation. + +### 2. **Automatic Resource Lifetime Management** +- Resources are created only when first needed (at their `FirstUse` pass) +- Resources are destroyed immediately after their last use (at their `LastUse` pass) +- Imported resources (like backbuffer) are never destroyed by the graph + +### 3. **Automatic Barrier Insertion** +The graph automatically inserts resource state transitions based on how resources are used: +- Write operations: `RenderTarget`, `DepthWrite`, `UnorderedAccess`, `CopyDest` +- Read operations: `ShaderResource`, `DepthRead`, `CopySource` + +### 4. **Automatic Pass Dependencies** +Dependencies are automatically inferred from resource usage patterns: +- **Read-After-Write (RAW)**: Pass B reads what Pass A wrote +- **Write-After-Read (WAR)**: Pass B writes to what Pass A read +- **Write-After-Write (WAW)**: Pass B writes to what Pass A wrote + +### 5. **Pass Culling** +Passes that don't contribute to the final output are automatically culled: +- Starts from imported resources (outputs) +- Recursively marks all dependent passes as needed +- Unused passes are not executed + +## Architecture + +### Core Classes + +#### `RenderGraph` +The main orchestrator that manages the entire frame graph: +- **Setup Phase**: Declare resources and passes +- **Compile Phase**: Build dependencies, cull passes, analyze lifetimes +- **Execute Phase**: Create resources, insert barriers, execute passes, destroy resources + +#### `RenderGraphResourceHandle` +Handle representing a virtual resource: +- `RenderGraphTextureHandle`: Textures with format, size +- `RenderGraphBufferHandle`: Buffers with size + +#### `IRenderGraphBuilder` +Builder interface used during pass setup: +- `ReadTexture()` / `ReadBuffer()`: Declare read access +- `WriteTexture()` / `WriteBuffer()`: Declare write access +- `CreateTransientTexture()` / `CreateTransientBuffer()`: Create temporary resources + +#### `ICommandBuffer` +Abstraction for executing graphics commands (simulated with `Console.WriteLine`) + +### Resource Lifetime Example + +``` +GBuffer.Albedo: [0..1] Created in pass 0, destroyed after pass 1 +GBuffer.Normal: [0..2] Created in pass 0, destroyed after pass 2 +GBuffer.Depth: [0..2] Created in pass 0, destroyed after pass 2 +LightingResult: [1..3] Created in pass 1, destroyed after pass 3 +SSAO: [2..4] Created in pass 2, destroyed after pass 4 +TAA.Result: [3..4] Created in pass 3, destroyed after pass 4 +Backbuffer: [4..4] Imported (never created/destroyed) +``` + +## Usage Example + +### Pattern 1: Using Typed Pass Data with Blackboard (Recommended) + +```csharp +var renderGraph = new RenderGraph(); + +// Import external resources (e.g., backbuffer) +var backbuffer = renderGraph.ImportTexture( + "Backbuffer", + new TextureDescriptor(1920, 1080, TextureFormat.RGBA8)); + +// Define pass data structure +class GBufferData +{ + public RenderGraphTextureHandle Albedo = null!; + public RenderGraphTextureHandle Normal = null!; + public RenderGraphTextureHandle Depth = null!; +} + +// Create GBuffer pass with typed data +GBufferData gbufferData; +using (var builder = renderGraph.AddRenderPass("GBuffer Pass", out gbufferData)) +{ + // Create transient resources + var albedo = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo")); + var normal = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal")); + var depth = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth")); + + // Store in pass data and declare access + gbufferData.Albedo = builder.WriteTexture(albedo); + gbufferData.Normal = builder.WriteTexture(normal); + gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true); + + // Set render function with typed data + builder.SetRenderFunc((data, cmd) => + { + cmd.SetRenderTarget(data.Albedo.Name); + cmd.SetRenderTarget(data.Normal.Name); + cmd.SetDepthStencil(data.Depth.Name); + cmd.Draw(36000); + }); +} + +// Store in blackboard for other passes +renderGraph.Blackboard.Add(gbufferData); + +// ===== Lighting Pass ===== +class LightingPassData +{ + public RenderGraphTextureHandle GBufferAlbedo = null!; + public RenderGraphTextureHandle GBufferNormal = null!; + public RenderGraphTextureHandle OutputLighting = null!; +} + +RenderGraphTextureHandle lightingOutput; +using (var builder = renderGraph.AddRenderPass("Lighting Pass", out var lightingData)) +{ + // Read GBuffer from blackboard + var gbuffer = renderGraph.Blackboard.Get(); + + lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo); + lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal); + + // Create and return output handle + lightingOutput = builder.CreateTexture( + new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "Lighting")); + lightingData.OutputLighting = builder.WriteTexture(lightingOutput); + + builder.SetRenderFunc((data, cmd) => + { + cmd.BindShaderResource(data.GBufferAlbedo.Name, 0); + cmd.BindShaderResource(data.GBufferNormal.Name, 1); + cmd.SetRenderTarget(data.OutputLighting.Name); + cmd.Draw(3); + }); +} + +// Compile and execute +renderGraph.Compile(); +renderGraph.Execute(); +``` + +### Pattern 2: Simple Handle Passing + +For simple cases where you just need to pass one or two textures between passes, you can skip the blackboard: + +```csharp +// Create and return a handle +RenderGraphTextureHandle myTexture; +using (var builder = renderGraph.AddRenderPass("Pass 1", out var data)) +{ + myTexture = builder.CreateTexture(new TextureDescriptor(...)); + data.Output = builder.WriteTexture(myTexture); + builder.SetRenderFunc((d, cmd) => { /* ... */ }); +} + +// Use the handle in next pass +using (var builder = renderGraph.AddRenderPass("Pass 2", out var data)) +{ + data.Input = builder.ReadTexture(myTexture); + builder.SetRenderFunc((d, cmd) => { /* ... */ }); +} +``` + +## Key API Design Features + +### 1. **Typed Pass Data** +Each pass has a strongly-typed data structure that holds all its resource handles. This: +- Makes resource dependencies explicit and compile-time safe +- Avoids string-based lookups during execution +- Enables better IDE support and refactoring + +### 2. **Blackboard Pattern** +For sharing data structures between multiple passes: +- Store complex data (like GBuffer with multiple textures) in the blackboard +- Other passes retrieve it type-safely +- Useful for resources used by many passes + +### 3. **Direct Handle Passing** +For simple cases: +- Return a `RenderGraphTextureHandle` from a pass setup +- Pass it directly to the next pass +- No need for blackboard overhead + +### 4. **Using Block Pattern** +The `using` statement automatically commits the pass when the block ends: +- Builder is disposed → pass is committed to the graph +- Ensures all passes are properly registered +- Mimics Unity HDRP's render graph API + +## Benefits of Transient Resources + +1. **Memory Efficiency**: Resources only exist when needed, allowing memory reuse +2. **Automatic Synchronization**: Barriers inserted automatically based on usage +3. **Self-Documenting**: Clear declaration of what each pass reads/writes +4. **Type Safety**: Compile-time checking of pass data structures +5. **Performance**: No string lookups or dictionary access during execution +6. **Optimization Opportunities**: Graph can reorder passes (future work) +7. **Resource Aliasing**: Multiple transient resources can share memory (future work) + +## What's NOT Implemented (Intentionally) + +This is a proof of concept focusing on core graph mechanics. Some features are fully implemented, others are intentionally omitted: + +### ✅ Fully Implemented +- ✅ **Resource aliasing/memory pooling** - Automatic memory reuse for non-overlapping lifetimes +- ✅ **Typed pass data** - Zero-cost abstraction with compile-time safety +- ✅ **Blackboard pattern** - Type-safe data sharing between passes +- ✅ **Automatic barriers** - State transitions inferred from usage + +### ❌ Not Implemented +- ❌ Async compute queues +- ❌ Pass reordering optimization +- ❌ Subresource tracking (mip levels, array slices) +- ❌ Multi-queue synchronization +- ❌ GPU timeline profiling +- ❌ Resource versioning +- ❌ Graph visualization/debugging tools + +## Resource Aliasing (Implemented!) + +Transient resources with non-overlapping lifetimes automatically share the same physical memory: +``` +GBuffer.Albedo [0..1] ━━━━━━━━━━━ + ╰──> Reuse memory (34% savings!) +SSAO [2..4] ━━━━━━━━━━━━━ +``` + +Example output: +``` +[RG] Memory Statistics: + Total memory without aliasing: 72.19 MB + Total memory with aliasing: 47.46 MB + Memory saved: 24.73 MB (34.3%) + Allocations: 4 physical allocations for 7 resources +``` + +See [ALIASING.md](ALIASING.md) for detailed documentation. + +## Future Enhancements + +### Async Compute +Some passes can run on compute queue while graphics queue continues: +``` +Graphics: ━━[GBuffer]━━[Lighting]━━━━━━━━[PostFX]━━ + ║ +Compute: ╚═[SSAO]════╗ + ║ + [Wait]───────╝ +``` + +### Pass Reordering +Independent passes can be reordered for better GPU utilization or to enable more aliasing. + +## Output Example + +The demo program creates a deferred rendering pipeline and produces output like: + +``` +[RG] Building pass dependencies... + Pass 'Lighting Pass' depends on 'GBuffer Pass' + Pass 'SSAO Pass' depends on 'GBuffer Pass' + Pass 'Temporal AA' depends on 'Lighting Pass' + Pass 'Post Processing' depends on 'TAA' and 'SSAO' + +[RG] Culling unused passes... + Culled unused pass: 'Unused Debug Pass' + +[RG] Resource lifetimes: + 'GBuffer.Albedo': [0..1] (GBuffer Pass, Lighting Pass) + 'GBuffer.Normal': [0..2] (GBuffer Pass, Lighting Pass, SSAO Pass) + ... + +[PASS 0] Executing: 'GBuffer Pass' + [CREATE] Texture 'GBuffer.Albedo' (1920x1080, RGBA8) + [BARRIER] Transition 'GBuffer.Albedo' from Undefined to RenderTarget + [BEGIN] RenderPass 'GBuffer Pass' + [RT] Set RenderTarget: 'GBuffer.Albedo' + [DRAW] Drawing 36000 vertices + [END] RenderPass + +[PASS 1] Executing: 'Lighting Pass' + [BARRIER] Transition 'GBuffer.Albedo' from RenderTarget to ShaderResource + ... + [DESTROY] Resource 'GBuffer.Albedo' +``` + +## Conclusion + +This proof of concept demonstrates the core principles of modern transient render graphs. The system automatically manages resource lifetimes, inserts synchronization barriers, builds dependency DAGs, and culls unused work—all from high-level declarative pass descriptions. + +The architecture is designed to be extended with real graphics API integration (D3D12, Vulkan) while maintaining the same high-level interface. diff --git a/Ghost.RenderGraph.Concept/RenderGraph.cs b/Ghost.RenderGraph.Concept/RenderGraph.cs new file mode 100644 index 0000000..3e8304e --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraph.cs @@ -0,0 +1,415 @@ +namespace Ghost.RenderGraph.Concept; + +public class RenderGraph +{ + private int _resourceIdCounter = 0; + private int _passCounter = 0; + + private readonly List _resources = new(); + private readonly List _passes = new(); + + // Use List instead of Dictionary since resource IDs are sequential (0, 1, 2, ...) + private readonly List _resourceLifetimes = new(); + private readonly List _currentResourceStates = new(); + private readonly List _resourceToAllocationMap = new(); + + private readonly Dictionary _allocationActiveResource = new(); + private readonly RenderGraphBlackboard _blackboard = new(); + private readonly ResourceAllocator _allocator = new(); + + public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor) + { + var handle = new RenderGraphTextureHandle(_resourceIdCounter++, name, descriptor, isImported: true); + _resources.Add(handle); + _resourceLifetimes.Add(new ResourceLifetime(handle)); + _currentResourceStates.Add(ResourceState.Undefined); + _resourceToAllocationMap.Add(-1); // -1 means no allocation + Console.WriteLine($"[RG] Import Texture: '{name}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})"); + return handle; + } + + public RenderGraphBufferHandle ImportBuffer(string name, BufferDescriptor descriptor) + { + var handle = new RenderGraphBufferHandle(_resourceIdCounter++, name, descriptor, isImported: true); + _resources.Add(handle); + _resourceLifetimes.Add(new ResourceLifetime(handle)); + _currentResourceStates.Add(ResourceState.Undefined); + _resourceToAllocationMap.Add(-1); + Console.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)"); + return handle; + } + + internal RenderGraphTextureHandle CreateTransientTexture(TextureDescriptor descriptor) + { + var handle = new RenderGraphTextureHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false); + _resources.Add(handle); + _resourceLifetimes.Add(new ResourceLifetime(handle)); + _currentResourceStates.Add(ResourceState.Undefined); + _resourceToAllocationMap.Add(-1); + Console.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})"); + return handle; + } + + internal RenderGraphBufferHandle CreateTransientBuffer(BufferDescriptor descriptor) + { + var handle = new RenderGraphBufferHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false); + _resources.Add(handle); + _resourceLifetimes.Add(new ResourceLifetime(handle)); + _currentResourceStates.Add(ResourceState.Undefined); + _resourceToAllocationMap.Add(-1); + Console.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)"); + return handle; + } + + public RenderGraphBlackboard Blackboard => _blackboard; + + public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor) + { + return CreateTransientTexture(descriptor); + } + + public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor) + { + return CreateTransientBuffer(descriptor); + } + + public RenderGraphPassBuilder AddRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var builder = new RenderGraphPassBuilder(this, name, _passCounter); + passData = builder.PassData; + return builder; + } + + internal void CommitPass(RenderGraphPassBuilder builder, string name) + where TPassData : class, new() + { + if (builder.RenderFunc == null) + { + throw new InvalidOperationException($"Pass '{name}' has no render function set. Call SetRenderFunc() on the builder."); + } + + var pass = new RenderGraphPass( + name, + _passCounter++, + builder.PassData, + builder.RenderFunc, + builder.ResourceAccesses.ToList(), + builder.AllowCulling); + + _passes.Add(pass); + + foreach (var (handle, state) in pass.ResourceAccesses) + { + _resourceLifetimes[handle.Id].AddUsage(state, pass.Index); + } + + Console.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})"); + foreach (var (handle, state) in pass.ResourceAccesses) + { + Console.WriteLine($" - {state}: '{handle.Name}'"); + } + } + + public void Compile() + { + Console.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH =========="); + + BuildDependencies(); + CullUnusedPasses(); + AnalyzeResourceLifetimes(); + AllocatePhysicalResources(); + } + + private void AllocatePhysicalResources() + { + // Pass as IReadOnlyList since it's now a List + _allocator.AllocateResources(_resourceLifetimes, _passes); + + // Build mapping from virtual resource to physical allocation + foreach (var allocation in _allocator.Allocations) + { + foreach (var resource in allocation.AliasedResources) + { + _resourceToAllocationMap[resource.Id] = allocation.AllocationId; + } + } + } + + private void BuildDependencies() + { + Console.WriteLine("\n[RG] Building pass dependencies..."); + + for (int i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + + var writtenResources = pass.ResourceAccesses + .Where(access => IsWriteState(access.state)) + .Select(access => access.handle.Id) + .ToHashSet(); + + for (int j = 0; j < i; j++) + { + var previousPass = _passes[j]; + + var hasReadAfterWrite = previousPass.ResourceAccesses + .Where(access => IsWriteState(access.state)) + .Any(access => pass.ResourceAccesses.Any( + current => current.handle.Id == access.handle.Id && IsReadState(current.state))); + + var hasWriteAfterRead = pass.ResourceAccesses + .Where(access => IsWriteState(access.state)) + .Any(access => previousPass.ResourceAccesses.Any( + prev => prev.handle.Id == access.handle.Id && IsReadState(prev.state))); + + var hasWriteAfterWrite = previousPass.ResourceAccesses + .Where(access => IsWriteState(access.state)) + .Any(access => writtenResources.Contains(access.handle.Id)); + + if (hasReadAfterWrite || hasWriteAfterRead || hasWriteAfterWrite) + { + if (!pass.Dependencies.Contains(j)) + { + pass.Dependencies.Add(j); + Console.WriteLine($" Pass '{pass.Name}' depends on '{previousPass.Name}'"); + } + } + } + } + } + + private void CullUnusedPasses() + { + Console.WriteLine("\n[RG] Culling unused passes..."); + + // Mark passes that contribute to imported resources or don't allow culling + foreach (var pass in _passes) + { + foreach (var (handle, _) in pass.ResourceAccesses) + { + if (handle.IsImported) + { + pass.RefCount++; + } + } + + // Mark passes that don't allow culling (synchronization, debug, etc.) + if (!pass.AllowCulling) + { + pass.RefCount++; + Console.WriteLine($" Pass '{pass.Name}' marked as non-cullable"); + } + } + + // Propagate reference counts through dependencies + bool changed = true; + while (changed) + { + changed = false; + foreach (var pass in _passes) + { + if (pass.RefCount > 0) + { + foreach (var depIndex in pass.Dependencies) + { + var depPass = _passes[depIndex]; + if (depPass.RefCount == 0) + { + depPass.RefCount++; + changed = true; + } + } + } + } + } + + var culledPasses = _passes.Where(p => p.RefCount == 0 && p.AllowCulling).ToList(); + if (culledPasses.Count != 0) + { + foreach (var pass in culledPasses) + { + Console.WriteLine($" Culled unused pass: '{pass.Name}'"); + } + } + else + { + Console.WriteLine(" No passes culled."); + } + } + + private void AnalyzeResourceLifetimes() + { + Console.WriteLine("\n[RG] Resource lifetimes:"); + + foreach (var lifetime in _resourceLifetimes) + { + if (lifetime.FirstUse == int.MaxValue) + { + Console.WriteLine($" '{lifetime.Handle.Name}': UNUSED"); + } + else + { + var passNames = _passes + .Where(p => p.Index >= lifetime.FirstUse && p.Index <= lifetime.LastUse && p.RefCount > 0) + .Select(p => p.Name); + Console.WriteLine($" '{lifetime.Handle.Name}': [{lifetime.FirstUse}..{lifetime.LastUse}] ({string.Join(", ", passNames)})"); + } + } + } + + public void Execute() + { + Console.WriteLine("\n[RG] ========== EXECUTING RENDER GRAPH ==========\n"); + + var commandBuffer = new SimulatedCommandBuffer(); + + foreach (var pass in _passes.Where(p => p.RefCount > 0).OrderBy(p => p.Index)) + { + Console.WriteLine($"[PASS {pass.Index}] Executing: '{pass.Name}'"); + + var lifetime = _resourceLifetimes + .Where(lt => lt.FirstUse == pass.Index) + .ToList(); + + foreach (var lt in lifetime) + { + if (!lt.Handle.IsImported) + { + CreateResource(lt.Handle); + } + } + + InsertBarriers(pass, commandBuffer); + + commandBuffer.BeginRenderPass(pass.Name); + pass.Execute(commandBuffer); + commandBuffer.EndRenderPass(); + + var endLifetime = _resourceLifetimes + .Where(lt => lt.LastUse == pass.Index && !lt.Handle.IsImported) + .ToList(); + + foreach (var lt in endLifetime) + { + DestroyResource(lt.Handle); + } + + Console.WriteLine(); + } + + Console.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n"); + } + + private void CreateResource(RenderGraphResourceHandle handle) + { + var allocation = _allocator.GetAllocation(handle); + if (allocation != null) + { + if (handle is RenderGraphTextureHandle textureHandle) + { + var desc = textureHandle.Descriptor; + Console.WriteLine($" [CREATE] Texture '{handle.Name}' using '{allocation.DebugName}' " + + $"({desc.Width}x{desc.Height}, {desc.Format}, offset: {allocation.OffsetInBytes})"); + } + else if (handle is RenderGraphBufferHandle bufferHandle) + { + var desc = bufferHandle.Descriptor; + Console.WriteLine($" [CREATE] Buffer '{handle.Name}' using '{allocation.DebugName}' " + + $"({desc.SizeInBytes} bytes, offset: {allocation.OffsetInBytes})"); + } + + // Note: We do NOT set _allocationActiveResource here + // That happens in InsertBarriers when the resource is first accessed + } + else + { + if (handle is RenderGraphTextureHandle textureHandle) + { + var desc = textureHandle.Descriptor; + Console.WriteLine($" [CREATE] Texture '{handle.Name}' ({desc.Width}x{desc.Height}, {desc.Format})"); + } + else if (handle is RenderGraphBufferHandle bufferHandle) + { + var desc = bufferHandle.Descriptor; + Console.WriteLine($" [CREATE] Buffer '{handle.Name}' ({desc.SizeInBytes} bytes)"); + } + } + + _currentResourceStates[handle.Id] = ResourceState.Undefined; + } + + private void DestroyResource(RenderGraphResourceHandle handle) + { + Console.WriteLine($" [DESTROY] Resource '{handle.Name}'"); + _currentResourceStates[handle.Id] = ResourceState.Undefined; + + // Note: We intentionally DO NOT clear _allocationActiveResource here + // The allocation remains "owned" by this resource until another resource aliases it + // This allows us to track aliasing barriers correctly + } + + private void InsertBarriers(RenderGraphPass pass, ICommandBuffer commandBuffer) + { + foreach (var (handle, targetState) in pass.ResourceAccesses) + { + // Check if this resource shares a physical allocation + var allocation = _allocator.GetAllocation(handle); + if (allocation != null) + { + // Check what resource is currently active on this allocation + if (_allocationActiveResource.TryGetValue(allocation.AllocationId, out var activeResource)) + { + // If a different resource is currently active on this allocation, insert aliasing barrier + if (activeResource != null && activeResource.Id != handle.Id) + { + commandBuffer.AliasingBarrier(activeResource.Name, handle.Name, allocation.DebugName); + + // Clear state for the old resource since it's being aliased away + _currentResourceStates[activeResource.Id] = ResourceState.Undefined; + } + } + + // Update the active resource for this allocation + _allocationActiveResource[allocation.AllocationId] = handle; + } + + var currentState = _currentResourceStates[handle.Id]; + + if (currentState != targetState) + { + commandBuffer.ResourceBarrier(handle.Name, currentState, targetState); + _currentResourceStates[handle.Id] = targetState; + } + } + } + + private static bool IsWriteState(ResourceState state) + { + return state.HasFlag(ResourceState.RenderTarget) || + state.HasFlag(ResourceState.DepthWrite) || + state.HasFlag(ResourceState.UnorderedAccess) || + state.HasFlag(ResourceState.CopyDest); + } + + private static bool IsReadState(ResourceState state) + { + return state.HasFlag(ResourceState.ShaderResource) || + state.HasFlag(ResourceState.DepthRead) || + state.HasFlag(ResourceState.CopySource); + } + + public void Reset() + { + _passes.Clear(); + _resources.Clear(); + _resourceLifetimes.Clear(); + _currentResourceStates.Clear(); + _resourceToAllocationMap.Clear(); + _allocationActiveResource.Clear(); + _blackboard.Clear(); + _passCounter = 0; + _resourceIdCounter = 0; + Console.WriteLine("[RG] Render graph reset."); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs b/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs new file mode 100644 index 0000000..8e615b4 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs @@ -0,0 +1,36 @@ +namespace Ghost.RenderGraph.Concept; + +public class RenderGraphBlackboard +{ + private readonly Dictionary _data = new(); + + public void Add(T data) where T : class + { + _data[typeof(T)] = data; + } + + public T Get() where T : class + { + if (_data.TryGetValue(typeof(T), out var data)) + { + return (T)data; + } + throw new KeyNotFoundException($"Data of type {typeof(T).Name} not found in blackboard."); + } + + public bool TryGet(out T? data) where T : class + { + if (_data.TryGetValue(typeof(T), out var obj)) + { + data = (T)obj; + return true; + } + data = null; + return false; + } + + public void Clear() + { + _data.Clear(); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs b/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs new file mode 100644 index 0000000..672a635 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs @@ -0,0 +1,41 @@ +namespace Ghost.RenderGraph.Concept; + +public static class RenderGraphExtensions +{ + public static RenderGraphPassBuilder AddRenderPass( + this RenderGraph renderGraph, + string name, + out TPassData passData, + Action> setup) + where TPassData : class, new() + { + var builder = renderGraph.AddRenderPass(name, out passData); + setup(builder); + builder.Dispose(); + return builder; + } +} + +public sealed class RenderGraphPassScope : IDisposable + where TPassData : class, new() +{ + private readonly RenderGraphPassBuilder _builder; + private readonly string _passName; + + internal RenderGraphPassScope(RenderGraphPassBuilder builder, string passName) + { + _builder = builder; + _passName = passName; + } + + public RenderGraphPassBuilder Builder => _builder; + + public void Dispose() + { + // Commit the pass when the using block ends + if (_builder.RenderFunc != null) + { + _builder.Dispose(); + } + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphPass.cs b/Ghost.RenderGraph.Concept/RenderGraphPass.cs new file mode 100644 index 0000000..e964808 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphPass.cs @@ -0,0 +1,50 @@ +namespace Ghost.RenderGraph.Concept; + +internal abstract class RenderGraphPass +{ + public string Name { get; } + public int Index { get; } + public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; } + public List Dependencies { get; } = new(); + public int RefCount { get; set; } = 0; + public bool AllowCulling { get; } + + protected RenderGraphPass( + string name, + int index, + List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses, + bool allowCulling) + { + Name = name; + Index = index; + ResourceAccesses = resourceAccesses; + AllowCulling = allowCulling; + } + + public abstract void Execute(ICommandBuffer commandBuffer); +} + +internal class RenderGraphPass : RenderGraphPass + where TPassData : class +{ + public TPassData PassData { get; } + public Action RenderFunc { get; } + + public RenderGraphPass( + string name, + int index, + TPassData passData, + Action renderFunc, + List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses, + bool allowCulling) + : base(name, index, resourceAccesses, allowCulling) + { + PassData = passData; + RenderFunc = renderFunc; + } + + public override void Execute(ICommandBuffer commandBuffer) + { + RenderFunc(PassData, commandBuffer); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs b/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs new file mode 100644 index 0000000..584fa50 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs @@ -0,0 +1,105 @@ +namespace Ghost.RenderGraph.Concept; + +public interface IRenderGraphBuilder +{ + RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle); + RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle); + RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess); + RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor); + RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle); + RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle); + RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor); +} + +public class RenderGraphPassBuilder : IRenderGraphBuilder, IDisposable + where TPassData : class, new() +{ + private readonly RenderGraph _graph; + private readonly string _passName; + private readonly int _passIndex; + private readonly List<(RenderGraphResourceHandle handle, ResourceState state)> _resourceAccesses = new(); + private Action? _renderFunc; + private bool _committed; + private bool _allowCulling = true; + + public TPassData PassData { get; } + + internal RenderGraphPassBuilder(RenderGraph graph, string passName, int passIndex) + { + _graph = graph; + _passName = passName; + _passIndex = passIndex; + PassData = new TPassData(); + } + + internal IReadOnlyList<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses => _resourceAccesses; + internal Action? RenderFunc => _renderFunc; + internal bool AllowCulling => _allowCulling; + + public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle) + { + _resourceAccesses.Add((handle, ResourceState.ShaderResource)); + return handle; + } + + public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle) + { + _resourceAccesses.Add((handle, ResourceState.RenderTarget)); + return handle; + } + + public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess) + { + _resourceAccesses.Add((handle, writeAccess ? ResourceState.DepthWrite : ResourceState.DepthRead)); + return handle; + } + + public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor) + { + var handle = _graph.CreateTransientTexture(descriptor); + return handle; + } + + public RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle) + { + _resourceAccesses.Add((handle, ResourceState.ShaderResource)); + return handle; + } + + public RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle) + { + _resourceAccesses.Add((handle, ResourceState.UnorderedAccess)); + return handle; + } + + public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor) + { + var handle = _graph.CreateTransientBuffer(descriptor); + return handle; + } + + public void SetRenderFunc(Action renderFunc) + { + _renderFunc = renderFunc; + } + + /// + /// Controls whether this pass can be culled if it doesn't contribute to the final output. + /// Set to false for synchronization passes, debug markers, or async compute boundaries. + /// Default is true. + /// + public void SetAllowCulling(bool allowCulling) + { + _allowCulling = allowCulling; + } + + public void Dispose() + { + // Commit the pass when disposed (at end of using block) + if (!_committed) + { + _graph.CommitPass(this, _passName); + _committed = true; + } + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs b/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs new file mode 100644 index 0000000..fa6561b --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs @@ -0,0 +1,41 @@ +namespace Ghost.RenderGraph.Concept; + +public class RenderGraphResourceHandle +{ + internal int Id { get; } + internal ResourceType Type { get; } + internal string Name { get; } + internal bool IsImported { get; } + + internal RenderGraphResourceHandle(int id, ResourceType type, string name, bool isImported) + { + Id = id; + Type = type; + Name = name; + IsImported = isImported; + } + + public override string ToString() => Name; +} + +public sealed class RenderGraphTextureHandle : RenderGraphResourceHandle +{ + internal TextureDescriptor Descriptor { get; } + + internal RenderGraphTextureHandle(int id, string name, TextureDescriptor descriptor, bool isImported) + : base(id, ResourceType.Texture, name, isImported) + { + Descriptor = descriptor; + } +} + +public sealed class RenderGraphBufferHandle : RenderGraphResourceHandle +{ + internal BufferDescriptor Descriptor { get; } + + internal RenderGraphBufferHandle(int id, string name, BufferDescriptor descriptor, bool isImported) + : base(id, ResourceType.Buffer, name, isImported) + { + Descriptor = descriptor; + } +} diff --git a/Ghost.RenderGraph.Concept/ResourceAllocator.cs b/Ghost.RenderGraph.Concept/ResourceAllocator.cs new file mode 100644 index 0000000..062875e --- /dev/null +++ b/Ghost.RenderGraph.Concept/ResourceAllocator.cs @@ -0,0 +1,213 @@ +namespace Ghost.RenderGraph.Concept; + +/// +/// Represents a physical memory allocation that can be shared by multiple transient resources +/// +internal class PhysicalResourceAllocation +{ + public int AllocationId { get; } + public ulong SizeInBytes { get; } + public ulong OffsetInBytes { get; } + public string DebugName { get; } + public List AliasedResources { get; } = new(); + + public PhysicalResourceAllocation(int allocationId, ulong sizeInBytes, ulong offsetInBytes, string debugName) + { + AllocationId = allocationId; + SizeInBytes = sizeInBytes; + OffsetInBytes = offsetInBytes; + DebugName = debugName; + } +} + +/// +/// Manages memory allocation and aliasing for transient resources +/// +internal class ResourceAllocator +{ + private readonly List _allocations = new(); + private int _allocationIdCounter = 0; + + public IReadOnlyList Allocations => _allocations; + + /// + /// Allocate physical memory for resources, enabling aliasing where possible + /// + public void AllocateResources( + IReadOnlyList resourceLifetimes, + List passes) + { + Console.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS ====="); + + // Separate imported and transient resources + var transientResources = resourceLifetimes + .Where(lt => !lt.Handle.IsImported && lt.FirstUse != int.MaxValue) + .OrderBy(lt => lt.FirstUse) + .ThenByDescending(lt => GetResourceSize(lt.Handle)) + .ToList(); + + if (!transientResources.Any()) + { + Console.WriteLine("No transient resources to allocate."); + return; + } + + // Track which allocation slots are occupied at each pass + var allocationSlots = new List(); + + foreach (var resource in transientResources) + { + var size = GetResourceSize(resource.Handle); + var alignment = GetResourceAlignment(resource.Handle); + + // Find an existing allocation slot that: + // 1. Is large enough + // 2. Has no lifetime overlap + // 3. Matches resource type (texture/buffer) + AllocationSlot? reuseSlot = null; + foreach (var slot in allocationSlots) + { + if (CanAlias(slot, resource, size, alignment)) + { + reuseSlot = slot; + break; + } + } + + if (reuseSlot != null) + { + // Reuse existing allocation + reuseSlot.AddResource(resource); + Console.WriteLine($"[ALIAS] '{resource.Handle.Name}' aliases with '{reuseSlot.Allocation.DebugName}' " + + $"(offset: {reuseSlot.Allocation.OffsetInBytes}, size: {size} bytes, " + + $"lifetime: [{resource.FirstUse}..{resource.LastUse}])"); + } + else + { + // Create new allocation + var allocation = new PhysicalResourceAllocation( + _allocationIdCounter++, + size, + offsetInBytes: 0, // In a real implementation, this would be a heap offset + $"Physical_{resource.Handle.Type}_{_allocationIdCounter}"); + + var newSlot = new AllocationSlot(allocation, resource.Handle.Type); + newSlot.AddResource(resource); + allocationSlots.Add(newSlot); + + Console.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " + + $"(size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])"); + } + } + + _allocations.AddRange(allocationSlots.Select(s => s.Allocation)); + + // Print summary + Console.WriteLine($"\n[RG] Memory Statistics:"); + var totalWithoutAliasing = transientResources.Sum(r => (long)GetResourceSize(r.Handle)); + var totalWithAliasing = _allocations.Sum(a => (long)a.SizeInBytes); + var savedMemory = totalWithoutAliasing - totalWithAliasing; + var savingPercentage = totalWithoutAliasing > 0 ? (savedMemory * 100.0 / totalWithoutAliasing) : 0; + + Console.WriteLine($" Total memory without aliasing: {FormatBytes(totalWithoutAliasing)}"); + Console.WriteLine($" Total memory with aliasing: {FormatBytes(totalWithAliasing)}"); + Console.WriteLine($" Memory saved: {FormatBytes(savedMemory)} ({savingPercentage:F1}%)"); + Console.WriteLine($" Allocations: {_allocations.Count} physical allocations for {transientResources.Count} resources"); + } + + private bool CanAlias(AllocationSlot slot, ResourceLifetime resource, ulong requiredSize, ulong requiredAlignment) + { + // Must be same resource type + if (slot.ResourceType != resource.Handle.Type) + return false; + + // Must be large enough + if (slot.Allocation.SizeInBytes < requiredSize) + return false; + + // Check for lifetime overlap with any resource in this slot + foreach (var existingResource in slot.Resources) + { + if (LifetimesOverlap(existingResource, resource)) + return false; + } + + return true; + } + + private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b) + { + // Two resources overlap if their lifetimes intersect + return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse); + } + + private ulong GetResourceSize(RenderGraphResourceHandle handle) + { + return handle switch + { + RenderGraphTextureHandle texture => CalculateTextureSize(texture.Descriptor), + RenderGraphBufferHandle buffer => (ulong)buffer.Descriptor.SizeInBytes, + _ => 0 + }; + } + + private ulong GetResourceAlignment(RenderGraphResourceHandle handle) + { + // In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO + return handle switch + { + RenderGraphTextureHandle => 65536, // 64KB texture alignment (typical) + RenderGraphBufferHandle => 256, // 256 byte buffer alignment + _ => 256 + }; + } + + private ulong CalculateTextureSize(TextureDescriptor desc) + { + // Simplified size calculation + var bytesPerPixel = desc.Format switch + { + TextureFormat.RGBA8 => 4, + TextureFormat.RGBA16F => 8, + TextureFormat.RGBA32F => 16, + TextureFormat.Depth32F => 4, + TextureFormat.R32Uint => 4, + _ => 4 + }; + + return (ulong)(desc.Width * desc.Height * bytesPerPixel); + } + + private string FormatBytes(long bytes) + { + if (bytes < 1024) + return $"{bytes} B"; + if (bytes < 1024 * 1024) + return $"{bytes / 1024.0:F2} KB"; + return $"{bytes / (1024.0 * 1024.0):F2} MB"; + } + + public PhysicalResourceAllocation? GetAllocation(RenderGraphResourceHandle handle) + { + return _allocations.FirstOrDefault(a => a.AliasedResources.Any(r => r.Id == handle.Id)); + } + + private class AllocationSlot + { + public PhysicalResourceAllocation Allocation { get; } + public ResourceType ResourceType { get; } + public List Resources { get; } = new(); + + public AllocationSlot(PhysicalResourceAllocation allocation, ResourceType resourceType) + { + Allocation = allocation; + ResourceType = resourceType; + } + + public void AddResource(ResourceLifetime resource) + { + Resources.Add(resource); + Allocation.AliasedResources.Add(resource.Handle); + } + } +} diff --git a/Ghost.RenderGraph.Concept/ResourceDescriptor.cs b/Ghost.RenderGraph.Concept/ResourceDescriptor.cs new file mode 100644 index 0000000..4f54b90 --- /dev/null +++ b/Ghost.RenderGraph.Concept/ResourceDescriptor.cs @@ -0,0 +1,28 @@ +namespace Ghost.RenderGraph.Concept; + +public enum ResourceType +{ + Texture, + Buffer +} + +public enum TextureFormat +{ + RGBA8, + RGBA16F, + RGBA32F, + Depth32F, + R32Uint +} + +public record TextureDescriptor( + int Width, + int Height, + TextureFormat Format, + string DebugName = "Unnamed Texture" +); + +public record BufferDescriptor( + int SizeInBytes, + string DebugName = "Unnamed Buffer" +); diff --git a/Ghost.RenderGraph.Concept/ResourceLifetime.cs b/Ghost.RenderGraph.Concept/ResourceLifetime.cs new file mode 100644 index 0000000..cad392f --- /dev/null +++ b/Ghost.RenderGraph.Concept/ResourceLifetime.cs @@ -0,0 +1,35 @@ +namespace Ghost.RenderGraph.Concept; + +internal class ResourceUsage +{ + public RenderGraphResourceHandle Handle { get; } + public ResourceState State { get; } + public int PassIndex { get; } + + public ResourceUsage(RenderGraphResourceHandle handle, ResourceState state, int passIndex) + { + Handle = handle; + State = state; + PassIndex = passIndex; + } +} + +internal class ResourceLifetime +{ + public RenderGraphResourceHandle Handle { get; } + public int FirstUse { get; set; } = int.MaxValue; + public int LastUse { get; set; } = -1; + public List Usages { get; } = new(); + + public ResourceLifetime(RenderGraphResourceHandle handle) + { + Handle = handle; + } + + public void AddUsage(ResourceState state, int passIndex) + { + Usages.Add(new ResourceUsage(Handle, state, passIndex)); + FirstUse = Math.Min(FirstUse, passIndex); + LastUse = Math.Max(LastUse, passIndex); + } +} diff --git a/Ghost.RenderGraph.Concept/ResourceState.cs b/Ghost.RenderGraph.Concept/ResourceState.cs new file mode 100644 index 0000000..1ebebdd --- /dev/null +++ b/Ghost.RenderGraph.Concept/ResourceState.cs @@ -0,0 +1,21 @@ +namespace Ghost.RenderGraph.Concept; + +[Flags] +public enum ResourceState +{ + Undefined = 0, + RenderTarget = 1 << 0, + DepthWrite = 1 << 1, + DepthRead = 1 << 2, + ShaderResource = 1 << 3, + UnorderedAccess = 1 << 4, + CopySource = 1 << 5, + CopyDest = 1 << 6, + Present = 1 << 7 +} + +public enum BarrierType +{ + Transition, // Regular state transition + Aliasing // Aliasing barrier (resource is being reused) +} diff --git a/GhostEngine.sln b/GhostEngine.sln index 57745f8..d8ddecb 100644 --- a/GhostEngine.sln +++ b/GhostEngine.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Entities.Test", "Ghos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Test.Core", "Ghost.Test.Core\Ghost.Test.Core.csproj", "{2F2E39E6-C13F-49A5-A41E-FABBEEEAB890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.RenderGraph.Concept", "Ghost.RenderGraph.Concept\Ghost.RenderGraph.Concept.csproj", "{4B631380-1F77-4782-8823-8553D37DB2A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -243,6 +245,18 @@ Global {2F2E39E6-C13F-49A5-A41E-FABBEEEAB890}.Release|x64.Build.0 = Release|Any CPU {2F2E39E6-C13F-49A5-A41E-FABBEEEAB890}.Release|x86.ActiveCfg = Release|Any CPU {2F2E39E6-C13F-49A5-A41E-FABBEEEAB890}.Release|x86.Build.0 = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|ARM64.Build.0 = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|x64.Build.0 = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Debug|x86.Build.0 = Debug|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|ARM64.ActiveCfg = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|ARM64.Build.0 = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|x64.ActiveCfg = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|x64.Build.0 = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|x86.ActiveCfg = Release|Any CPU + {4B631380-1F77-4782-8823-8553D37DB2A0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -262,6 +276,7 @@ Global {C4F7EE9D-5E08-403D-885B-21B86BFD3498} = {82E20323-1AFE-4DFD-8228-945641754724} {11CFD14E-3B05-492F-A483-9A72A564086F} = {43E76E46-0E5F-4429-83C8-157689885174} {2F2E39E6-C13F-49A5-A41E-FABBEEEAB890} = {43E76E46-0E5F-4429-83C8-157689885174} + {4B631380-1F77-4782-8823-8553D37DB2A0} = {43E76E46-0E5F-4429-83C8-157689885174} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0C545827-2ED7-4597-BE3C-30E978C85B9E}