From e11a9ebb5260d7e082f62f946d2417230e13e5a3 Mon Sep 17 00:00:00 2001 From: Misaki Date: Fri, 23 Jan 2026 18:12:52 +0900 Subject: [PATCH] Refactor render graph: modular compilation & execution Major overhaul of render graph system for modularity and performance: - Split compilation and execution logic into dedicated classes (Compiler, Executor, NativePassBuilder, Barriers) - Overhauled barrier system: now uses CompiledBarrier with target state only, querying before state at execution - Resource size/alignment now queried from D3D12 device for accurate heap allocation - ResourceDesc now includes Type field and asserts correct union access - Centralized D3D12 interop logic in D3D12Utility extensions - Added RenderGraphHasher for structural graph hashing and cache invalidation - RenderGraph class simplified to orchestrate specialized components - ResourceAliasingManager now uses allocator for size queries - Compilation cache now stores compiled barriers, reducing memory usage - Improved comments, debug assertions, and removed redundant code Result: more maintainable, efficient, and robust render graph pipeline. --- .../D3D12/D3D12ResourceAllocator.cs | 117 +- Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs | 1 - .../D3D12/Utilities/D3D12Utility.cs | 98 +- Ghost.Graphics/RHI/Common.cs | 38 +- Ghost.Graphics/RHI/IResourceAllocator.cs | 15 + .../RenderGraphModule/RenderGraph.cs | 1227 +---------------- .../RenderGraphModule/RenderGraph.cs.backup | 1170 ++++++++++++++++ .../RenderGraphModule/RenderGraphAliasing.cs | 10 +- .../RenderGraphModule/RenderGraphBarriers.cs | 260 ++++ .../RenderGraphCompilationCache.cs | 17 +- .../RenderGraphModule/RenderGraphCompiler.cs | 391 ++++++ .../RenderGraphModule/RenderGraphExecutor.cs | 223 +++ .../RenderGraphModule/RenderGraphHasher.cs | 177 +++ .../RenderGraphNativePassBuilder.cs | 370 +++++ 14 files changed, 2797 insertions(+), 1317 deletions(-) create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraph.cs.backup create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphHasher.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphNativePassBuilder.cs diff --git a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs b/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs index bafbc9f..a9656c0 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs @@ -435,40 +435,6 @@ internal sealed unsafe partial class D3D12ResourceAllocator return uavDesc; } - private static D3D12_RESOURCE_FLAGS ConvertTextureUsage(TextureUsage usage) - { - var flags = D3D12_RESOURCE_FLAG_NONE; - - if (usage.HasFlag(TextureUsage.RenderTarget)) - { - flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; - } - - if (usage.HasFlag(TextureUsage.DepthStencil)) - { - flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; - } - - if (usage.HasFlag(TextureUsage.UnorderedAccess)) - { - flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; - } - - return flags; - } - - private static D3D12_RESOURCE_FLAGS ConvertBufferUsage(BufferUsage usage) - { - var flags = D3D12_RESOURCE_FLAG_NONE; - - if (usage.HasFlag(BufferUsage.Raw) || usage.HasFlag(BufferUsage.UnorderedAccess)) - { - flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; - } - - return flags; - } - private static D3D12_HEAP_TYPE ConvertMemoryType(ResourceMemoryType memoryType) { return memoryType switch @@ -559,7 +525,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, Guid* riid, void** ppv) { - var hr = S.S_OK; + HRESULT hr; if (options.AllocationType == ResourceAllocationType.Suballocation) { @@ -581,6 +547,26 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return hr; } + public ResourceSizeInfo GetSizeInfo(ResourceDesc desc) + { + D3D12_RESOURCE_DESC d3d12Desc; + if (desc.Type == ResourceType.Texture) + { + d3d12Desc = desc.TextureDescription.ToD3D12ResourceDesc(); + } + else + { + d3d12Desc = desc.BufferDescription.ToD3D12ResourceDesc(); + } + + var info = _device.NativeDevice.Get()->GetResourceAllocationInfo(0, 1, &d3d12Desc); + return new ResourceSizeInfo + { + Size = info.SizeInBytes, + Alignment = info.Alignment + }; + } + public Handle Allocate(ref readonly AllocationDesc desc, string name) { var allocDesc = new D3D12MA_ALLOCATION_DESC @@ -632,52 +618,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator ObjectDisposedException.ThrowIf(_disposed, this); CheckTexture2DSize(desc.Width, desc.Height); - - var dxgiFormat = D3D12Utility.ToDXGIFormat(desc.Format); - var maxDimension = Math.Max(desc.Width, Math.Max(desc.Height, desc.Slice)); - var mipLevels = desc.MipLevels == 0 - ? (ushort)(1 + Math.Floor(Math.Log2(maxDimension))) - : (ushort)desc.MipLevels; - - var resourceFlags = ConvertTextureUsage(desc.Usage); - var resourceDesc = desc.Dimension switch - { - TextureDimension.Texture2D => D3D12_RESOURCE_DESC.Tex2D( - dxgiFormat, - desc.Width, - desc.Height, - mipLevels: mipLevels, - flags: resourceFlags), - TextureDimension.Texture3D => D3D12_RESOURCE_DESC.Tex3D( - dxgiFormat, - desc.Width, - desc.Height, - (ushort)desc.Slice, - flags: resourceFlags), - TextureDimension.TextureCube => D3D12_RESOURCE_DESC.Tex2D( - dxgiFormat, - desc.Width, - desc.Height, - mipLevels: mipLevels, - arraySize: 6, - flags: resourceFlags), - TextureDimension.Texture2DArray => D3D12_RESOURCE_DESC.Tex2D( - dxgiFormat, - desc.Width, - desc.Height, - mipLevels: mipLevels, - arraySize: (ushort)desc.Slice, - flags: resourceFlags), - TextureDimension.TextureCubeArray => D3D12_RESOURCE_DESC.Tex2D( - dxgiFormat, - desc.Width, - desc.Height, - mipLevels: mipLevels, - arraySize: (ushort)(desc.Slice * 6), - flags: resourceFlags), - _ => throw new ArgumentException($"Unsupported texture dimension: {desc.Dimension}"), - }; - + + var resourceDesc = desc.ToD3D12ResourceDesc(); var allocationDesc = new D3D12MA_ALLOCATION_DESC { HeapType = D3D12_HEAP_TYPE_DEFAULT, @@ -715,7 +657,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv); var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray; - var srvDesc = CreateTextureSrvDesc(pResource, mipLevels, desc.Slice, isCubeMap); + var srvDesc = CreateTextureSrvDesc(pResource, resourceDesc.MipLevels, resourceDesc.DepthOrArraySize, isCubeMap); _device.NativeDevice.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle); _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv); @@ -782,14 +724,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator ObjectDisposedException.ThrowIf(_disposed, this); CheckBufferSize(desc.Size); - var alignedSize = desc.Size; - if (desc.Usage.HasFlag(BufferUsage.Constant)) - { - // D3D12 CBV size must be 256-byte aligned - alignedSize = (uint)(desc.Size + 255) & ~255u; - } - - var resourceDesc = D3D12_RESOURCE_DESC.Buffer(alignedSize, ConvertBufferUsage(desc.Usage)); + var resourceDesc = desc.ToD3D12ResourceDesc(); var isRaw = desc.Usage.HasFlag(BufferUsage.Raw); var allocationDesc = new D3D12MA_ALLOCATION_DESC @@ -839,7 +774,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC { BufferLocation = pResource->GetGPUVirtualAddress(), - SizeInBytes = (uint)alignedSize + SizeInBytes = (uint)resourceDesc.Width, }; _device.NativeDevice.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle); diff --git a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs index 45de52a..447ca0a 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs @@ -294,7 +294,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase } info.Release(_descriptorAllocator); - //System.Diagnostics.Debug.Assert(info.Release(_descriptorAllocator) == 0); #if DEBUG || GHOST_EDITOR _resourceName.Remove(handle, out var name); #endif diff --git a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs index faf539b..07eaadd 100644 --- a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs +++ b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs @@ -7,7 +7,7 @@ using static TerraFX.Aliases.DXGI_Alias; namespace Ghost.Graphics.D3D12.Utilities; -internal unsafe static class D3D12Utility +internal static unsafe class D3D12Utility { public static void SetName(ref this T obj, ReadOnlySpan name) where T : unmanaged, ID3D12Object.Interface @@ -278,6 +278,102 @@ internal unsafe static class D3D12Utility }; } + public static D3D12_RESOURCE_FLAGS ToD3D12ResourceFlag(this TextureUsage usage) + { + var flags = D3D12_RESOURCE_FLAG_NONE; + + if (usage.HasFlag(TextureUsage.RenderTarget)) + { + flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + } + + if (usage.HasFlag(TextureUsage.DepthStencil)) + { + flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + } + + if (usage.HasFlag(TextureUsage.UnorderedAccess)) + { + flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + } + + return flags; + } + + public static D3D12_RESOURCE_DESC ToD3D12ResourceDesc(this in TextureDesc desc) + { + var dxgiFormat = desc.Format.ToDXGIFormat(); + var maxDimension = Math.Max(desc.Width, Math.Max(desc.Height, desc.Slice)); + var mipLevels = desc.MipLevels == 0 + ? (ushort)(1 + Math.Floor(Math.Log2(maxDimension))) + : (ushort)desc.MipLevels; + + var resourceFlags = desc.Usage.ToD3D12ResourceFlag(); + return desc.Dimension switch + { + TextureDimension.Texture2D => D3D12_RESOURCE_DESC.Tex2D( + dxgiFormat, + desc.Width, + desc.Height, + mipLevels: mipLevels, + flags: resourceFlags), + TextureDimension.Texture3D => D3D12_RESOURCE_DESC.Tex3D( + dxgiFormat, + desc.Width, + desc.Height, + (ushort)desc.Slice, + flags: resourceFlags), + TextureDimension.TextureCube => D3D12_RESOURCE_DESC.Tex2D( + dxgiFormat, + desc.Width, + desc.Height, + mipLevels: mipLevels, + arraySize: 6, + flags: resourceFlags), + TextureDimension.Texture2DArray => D3D12_RESOURCE_DESC.Tex2D( + dxgiFormat, + desc.Width, + desc.Height, + mipLevels: mipLevels, + arraySize: (ushort)desc.Slice, + flags: resourceFlags), + TextureDimension.TextureCubeArray => D3D12_RESOURCE_DESC.Tex2D( + dxgiFormat, + desc.Width, + desc.Height, + mipLevels: mipLevels, + arraySize: (ushort)(desc.Slice * 6), + flags: resourceFlags), + _ => throw new ArgumentException($"Unsupported texture dimension: {desc.Dimension}"), + }; + } + + public static D3D12_RESOURCE_FLAGS ToD3D12ResourceFlag(this BufferUsage usage) + { + var flags = D3D12_RESOURCE_FLAG_NONE; + + if (usage.HasFlag(BufferUsage.Raw) || usage.HasFlag(BufferUsage.UnorderedAccess)) + { + flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + } + + return flags; + } + + public static D3D12_RESOURCE_DESC ToD3D12ResourceDesc(this in BufferDesc desc) + { + var alignedSize = desc.Size; + if (desc.Usage.HasFlag(BufferUsage.Constant)) + { + // D3D12 CBV size must be 256-byte aligned + alignedSize = (uint)(desc.Size + 255) & ~255u; + } + + var resourceFlags = desc.Usage.ToD3D12ResourceFlag(); + return D3D12_RESOURCE_DESC.Buffer(alignedSize, resourceFlags); + } + + public static ResourceDesc ToResourceDesc(this D3D12_RESOURCE_DESC desc) { if (desc.Dimension == D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER) diff --git a/Ghost.Graphics/RHI/Common.cs b/Ghost.Graphics/RHI/Common.cs index 000a72c..5dd3297 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 Misaki.HighPerformance.Mathematics; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -424,23 +425,45 @@ public struct ResourceDesc } internal resource_union _desc; + + public ResourceType Type + { + get; init; + } public TextureDesc TextureDescription { - readonly get => _desc.textureDescription; - set => _desc.textureDescription = value; + readonly get + { + Debug.Assert(Type == ResourceType.Texture); + return _desc.textureDescription; + } + set + { + Debug.Assert(Type == ResourceType.Texture); + _desc.textureDescription = value; + } } public BufferDesc BufferDescription { - readonly get => _desc.bufferDescription; - set => _desc.bufferDescription = value; + readonly get + { + Debug.Assert(Type == ResourceType.Buffer); + return _desc.bufferDescription; + } + set + { + Debug.Assert(Type == ResourceType.Buffer); + _desc.bufferDescription = value; + } } public static ResourceDesc Buffer(BufferDesc desc) { return new ResourceDesc { + Type = ResourceType.Buffer, BufferDescription = desc }; } @@ -449,6 +472,7 @@ public struct ResourceDesc { return new ResourceDesc { + Type = ResourceType.Texture, TextureDescription = desc }; } @@ -990,6 +1014,12 @@ public enum ResourceMemoryType Readback // GPU-to-CPU memory } +public enum ResourceType +{ + Texture, + Buffer +} + [Flags] public enum TextureUsage { diff --git a/Ghost.Graphics/RHI/IResourceAllocator.cs b/Ghost.Graphics/RHI/IResourceAllocator.cs index 1af5ada..ce4f935 100644 --- a/Ghost.Graphics/RHI/IResourceAllocator.cs +++ b/Ghost.Graphics/RHI/IResourceAllocator.cs @@ -69,8 +69,23 @@ public struct AllocationDesc } } +public readonly struct ResourceSizeInfo +{ + public ulong Size + { + get; init; + } + + public ulong Alignment + { + get; init; + } +} + public interface IResourceAllocator : IDisposable { + ResourceSizeInfo GetSizeInfo(ResourceDesc desc); + /// /// Allocates a block of memory on the GPU /// diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraph.cs b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs index 75832ec..54b810d 100644 --- a/Ghost.Graphics/RenderGraphModule/RenderGraph.cs +++ b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs @@ -1,15 +1,12 @@ using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; -using Misaki.HighPerformance.LowLevel.Buffer; -using Misaki.HighPerformance.LowLevel.Collections; -using System.Diagnostics; -using System.IO.Hashing; namespace Ghost.Graphics.RenderGraphModule; /// /// Main render graph class that manages resource allocation and pass execution. +/// Delegates complex operations to specialized components for better organization. /// public sealed class RenderGraph : IDisposable { @@ -25,15 +22,16 @@ public sealed class RenderGraph : IDisposable private readonly RenderGraphBuilder _builder; private readonly ResourceAliasingManager _aliasingManager; - private readonly Dictionary _resourceStates = new(128); - private readonly List _barriers = new(128); + private readonly List _compiledBarriers = new(128); private readonly RenderGraphCompilationCache _compilationCache = new(); private readonly RenderGraphContext _context; + private readonly RenderGraphCompiler _compiler; + private readonly RenderGraphExecutor _executor; + private readonly RenderGraphNativePassBuilder _nativePassBuilder; + private bool _compiled; - private Handle _resourceHeap; - private ViewState _currentViewState; public RenderGraphBlackboard Blackboard { @@ -52,14 +50,10 @@ public sealed class RenderGraph : IDisposable _nativePasses = new List(32); _builder = new RenderGraphBuilder(); - _aliasingManager = new ResourceAliasingManager(_objectPool); - - _resourceStates = new Dictionary(64); - _barriers = new List(128); + _aliasingManager = new ResourceAliasingManager(graphicsEngine.ResourceAllocator, _objectPool); _compilationCache = new RenderGraphCompilationCache(); - _resourceHeap = Handle.Invalid; _context = new RenderGraphContext( _graphicsEngine.ResourceDatabase, _graphicsEngine.PipelineLibrary, @@ -67,6 +61,10 @@ public sealed class RenderGraph : IDisposable _resources ); + _nativePassBuilder = new RenderGraphNativePassBuilder(_objectPool, _resources); + _compiler = new RenderGraphCompiler(_graphicsEngine, _resources, _aliasingManager, _nativePassBuilder, _compilationCache); + _executor = new RenderGraphExecutor(_graphicsEngine, _resources, _context); + Blackboard = new RenderGraphBlackboard(); } @@ -86,9 +84,8 @@ public sealed class RenderGraph : IDisposable // Reset aliasing manager _aliasingManager.Reset(); - // Clear resource states and barriers - _resourceStates.Clear(); - _barriers.Clear(); + // Clear compiled barriers + _compiledBarriers.Clear(); // Return passes to the pool and reset count for (var i = 0; i < _passes.Count; i++) @@ -187,163 +184,9 @@ public sealed class RenderGraph : IDisposable return _builder; } - - private unsafe int ComputeTextureHash(byte* pData, int offset, Identifier texture) - { - if (texture.IsInvalid) - { - return offset; - } - - var resource = _resources.GetResource(texture.AsResource()); - - // Hash imported flag - *(pData + offset) = resource.isImported ? (byte)1 : (byte)0; - offset += sizeof(byte); - - // For imported textures, hash the backing resource handle - if (resource.isImported) - { - *(int*)(pData + offset) = resource.backingResource.GetHashCode(); - offset += sizeof(int); - return offset; - } - - var desc = resource.rgTextureDesc; - - // Hash format (structural) - *(TextureFormat*)(pData + offset) = desc.format; - offset += sizeof(TextureFormat); - - // Hash size mode (structural) - *(RGTextureSizeMode*)(pData + offset) = desc.sizeMode; - offset += sizeof(RGTextureSizeMode); - - // Hash size specification based on mode - if (desc.sizeMode == RGTextureSizeMode.Absolute) - { - // Absolute mode: hash actual dimensions - *(uint*)(pData + offset) = desc.width; - offset += sizeof(uint); - *(uint*)(pData + offset) = desc.height; - offset += sizeof(uint); - } - else - { - // Relative mode: hash scale factors (NOT resolved dimensions) - *(float*)(pData + offset) = desc.scaleX; - offset += sizeof(float); - *(float*)(pData + offset) = desc.scaleY; - offset += sizeof(float); - } - - // Hash other structural properties - *(TextureDimension*)(pData + offset) = desc.dimension; - offset += sizeof(TextureDimension); - *(uint*)(pData + offset) = desc.mipLevels; - offset += sizeof(uint); - *(TextureUsage*)(pData + offset) = desc.usage; - offset += sizeof(TextureUsage); - - *(bool*)(pData + offset) = desc.clearAtFirstUse; - offset += sizeof(bool); - *(bool*)(pData + offset) = desc.discardAtLastUse; - offset += sizeof(bool); - - return offset; - } - - - private unsafe ulong ComputeGraphHash() - { - using var scope = AllocationManager.CreateStackScope(); - var bufferPool = new UnsafeList(2048, scope.AllocationHandle); - var pData = (byte*)bufferPool.GetUnsafePtr(); - var offset = 0; - - // Hash pass count - *(int*)(pData + offset) = _passes.Count; - offset += sizeof(int); - - // Hash each pass structure (excluding names) - for (var i = 0; i < _passes.Count; i++) - { - var pass = _passes[i]; - - *(RenderPassType*)(pData + offset) = pass.type; - offset += sizeof(RenderPassType); - - *(bool*)(pData + offset) = pass.allowCulling; - offset += sizeof(bool); - - *(bool*)(pData + offset) = pass.asyncCompute; - offset += sizeof(bool); - - // Hash depth attachment - offset = ComputeTextureHash(pData, offset, pass.depthAccess.id); - - pData[offset] = (byte)pass.depthAccess.accessFlags; - offset += sizeof(AccessFlags); - - *(int*)(pData + offset) = pass.maxColorIndex; - offset += sizeof(int); - for (var j = 0; j <= pass.maxColorIndex; j++) - { - offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id); - - pData[offset] = (byte)pass.colorAccess[j].accessFlags; - offset += sizeof(AccessFlags); - } - - for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) - { - var readList = pass.resourceReads[j]; - var writeList = pass.resourceWrites[j]; - var createList = pass.resourceCreates[j]; - - *(int*)(pData + offset) = readList.Count; - offset += sizeof(int); - for (var k = 0; k < readList.Count; k++) - { - *(int*)(pData + offset) = readList[k].Value; - offset += sizeof(int); - } - - *(int*)(pData + offset) = writeList.Count; - offset += sizeof(int); - for (var k = 0; k < writeList.Count; k++) - { - *(int*)(pData + offset) = writeList[k].Value; - offset += sizeof(int); - } - - *(int*)(pData + offset) = createList.Count; - offset += sizeof(int); - for (var k = 0; k < createList.Count; k++) - { - *(int*)(pData + offset) = createList[k].Value; - offset += sizeof(int); - } - - *(int*)(pData + offset) = pass.randomAccess.Count; - offset += sizeof(int); - for (var k = 0; k < pass.randomAccess.Count; k++) - { - *(int*)(pData + offset) = pass.randomAccess[k].Value; - offset += sizeof(int); - } - } - - *(int*)(pData + offset) = pass.GetRenderFuncHashCode(); - offset += sizeof(int); - } - - var span = new Span(pData, offset); - return XxHash64.HashToUInt64(span); - } - /// /// Compiles the render graph by culling unused passes and determining resource lifetimes. + /// Delegates to RenderGraphCompiler for the actual compilation work. /// public void Compile(in ViewState viewState) { @@ -352,1060 +195,36 @@ public sealed class RenderGraph : IDisposable return; } - _currentViewState = viewState; - // Resolve texture sizes before computing hash _resources.ResolveTextureSizes(in viewState); - var graphHash = ComputeGraphHash(); - if (_compilationCache.TryGetCached(graphHash, out var cached)) - { - // Check if view state changed - if (!cached.viewState.Equals(viewState)) - { - // View state changed - re-resolve sizes and recreate GPU resources - _resources.ResolveTextureSizes(in viewState); - RecreateResourcesForNewViewState(cached, viewState); - } - else - { - // Perfect cache hit - restore everything - RestoreFromCache(cached); - } - - _compiled = true; - return; - } - - _compiledPasses.Clear(); - - // Mark passes with side effects (writes to imported resources) - for (var i = 0; i < _passes.Count; i++) - { - var pass = _passes[i]; - - // Check if this pass writes to any imported resources - for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) - { - var writeList = pass.resourceWrites[j]; - for (var k = 0; k < writeList.Count; k++) - { - var writeHandle = writeList[k]; - var resource = _resources.GetResource(writeHandle); - if (resource.isImported) - { - pass.hasSideEffects = true; - break; - } - } - } - } - - // Cull passes based on dependency analysis - // Mark all passes as culled initially - for (var i = 0; i < _passes.Count; i++) - { - _passes[i].culled = _passes[i].allowCulling && !_passes[i].hasSideEffects; - } - - // Traverse backwards from passes with side effects - for (var i = _passes.Count - 1; i >= 0; i--) - { - var pass = _passes[i]; - if (!pass.culled) - { - UnculDependencies(pass); - } - } - - // Build final pass list (only non-culled passes) - for (var i = 0; i < _passes.Count; i++) - { - var pass = _passes[i]; - if (!pass.culled) - { - _compiledPasses.Add(pass); - } - } - - _aliasingManager.AssignPhysicalResources(_resources, _passes.Count); - AllocateResource(); - - GenerateAliasingBarriers(); - BuildNativeRenderPasses(); - StoreInCache(graphHash); - - _compiled = true; - } - - private void AllocateResource() - { - if (_resourceHeap.IsValid) - { - foreach (var res in _resources.Resources) - { - if (res.isImported) - { - continue; - } - - _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); - } - - _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); - } - - if (_aliasingManager.Heap.size == 0) - { - return; - } - - var allocationDesc = new AllocationDesc - { - Size = _aliasingManager.Heap.size + 1024 * 1024, - Alignment = ResourceHeap.DEFAULT_ALIGNMENT, - HeapFlags = HeapFlags.AlowBufferAndTexture, - HeapType = HeapType.Default - }; - - _resourceHeap = _graphicsEngine.ResourceAllocator.Allocate(in allocationDesc, "RenderGraphResourceHeap"); - - for (var i = 0; i < _resources.Resources.Count; i++) - { - var placedIndex = _aliasingManager.GetPlacedResourceIndex(i); - var placed = _aliasingManager.GetPlacedResource(placedIndex); - if (placed == null) - { - continue; - } - - var res = _resources.Resources[i]; - var ops = new CreationOptions - { - AllocationType = ResourceAllocationType.Suballocation, - Heap = _resourceHeap, - Offset = placed.heapOffset, - }; - - if (res.type == RenderGraphResourceType.Texture) - { - var textureDesc = res.rgTextureDesc.ToTextureDesc(res.resolvedWidth, res.resolvedHeight); - res.backingResource = _graphicsEngine.ResourceAllocator.CreateTexture(in textureDesc, res.name, ops).AsResource(); - } - else if (res.type == RenderGraphResourceType.Buffer) - { - res.backingResource = _graphicsEngine.ResourceAllocator.CreateBuffer(in res.bufferDesc, res.name, ops).AsResource(); - } - else - { - throw new NotSupportedException(); - } - - _compilationCache.UpdateBackingResource(i, res.backingResource); - } - } - - /// - /// Recreates GPU resources when view state changes but compilation cache is valid. - /// - private void RecreateResourcesForNewViewState(CachedCompilation cached, in ViewState newViewState) - { - // Restore compilation results (passes, barriers, aliasing) - RestoreFromCache(cached); - AllocateResource(); - - cached.viewState = newViewState; - } - - /// - /// Restores the render graph state from cached compilation results. - /// - private void RestoreFromCache(CachedCompilation cached) - { - // Restore compiled pass list - _compiledPasses.Clear(); - for (var i = 0; i < cached.compiledPassIndices.Count; i++) - { - var passIndex = cached.compiledPassIndices[i]; - _compiledPasses.Add(_passes[passIndex]); - } - - // Restore culling flags - for (var i = 0; i < _passes.Count && i < cached.passCulledFlags.Count; i++) - { - _passes[i].culled = cached.passCulledFlags[i]; - } - - // Restore aliasing mappings (need to update ResourceAliasingManager) - _aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources); - - // Restore barriers (deep copy to avoid shared references) - _barriers.Clear(); - for (var i = 0; i < cached.barriers.Count; i++) - { - _barriers.Add(cached.barriers[i]); - } - - // Restore resource states - _resourceStates.Clear(); - foreach (var kvp in cached.resourceStates) - { - _resourceStates[kvp.Key] = kvp.Value; - } - - for (var i = 0; i < _resources.ResourceCount; i++) - { - var res = _resources.Resources[i]; - - if (!res.isImported) - { - res.backingResource = cached.backingResources[i]; - } - } - - BuildNativeRenderPasses(); - } - - /// - /// Stores current compilation results in the cache. - /// - private void StoreInCache(ulong graphHash) - { - var cacheData = new CachedCompilation(); - - // Store view state - cacheData.viewState = _currentViewState; - - // Store compiled pass indices - for (var i = 0; i < _compiledPasses.Count; i++) - { - cacheData.compiledPassIndices.Add(_compiledPasses[i].index); - } - - // Store culling flags for all passes - for (var i = 0; i < _passes.Count; i++) - { - cacheData.passCulledFlags.Add(_passes[i].culled); - } - - // Store aliasing mappings - _aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources); - - // Store barriers - for (var i = 0; i < _barriers.Count; i++) - { - cacheData.barriers.Add(_barriers[i]); - } - - // Store resource states - foreach (var kvp in _resourceStates) - { - cacheData.resourceStates[kvp.Key] = kvp.Value; - } - - for (var i = 0; i < _resources.ResourceCount; i++) - { - var res = _resources.Resources[i]; - cacheData.backingResources.Add(res.backingResource); - } - - _compilationCache.Store(graphHash, cacheData); - } - - private void UnculProducer(Identifier resource) - { - var res = _resources.GetResource(resource); - if (res.producerPass >= 0) - { - var producer = _passes[res.producerPass]; - if (producer.culled) - { - producer.culled = false; - UnculDependencies(producer); - } - } - } - - private void UnculDependencies(RenderGraphPassBase pass) - { - // Un-cull producers of read resources - for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) - { - var readList = pass.resourceReads[i]; - for (var j = 0; j < readList.Count; j++) - { - UnculProducer(readList[j]); - } - } - - // Un-cull producers of color attachments - for (var i = 0; i < pass.maxColorIndex; i++) - { - if (pass.colorAccess[i].id.IsValid) - { - UnculProducer(pass.colorAccess[i].id.AsResource()); - } - } - - // Un-cull producer of depth attachment - if (pass.depthAccess.id.IsValid) - { - UnculProducer(pass.depthAccess.id.AsResource()); - } - - // Un-cull producers of UAV resources (if not already in reads/writes) - for (var i = 0; i < pass.randomAccess.Count; i++) - { - UnculProducer(pass.randomAccess[i]); - } - } - - /// - /// Generates aliasing barriers to synchronize resources sharing memory. - /// - private void GenerateAliasingBarriers() - { - _barriers.Clear(); - _resourceStates.Clear(); - - // Process each compiled pass in order - for (var passIdx = 0; passIdx < _compiledPasses.Count; passIdx++) - { - var pass = _compiledPasses[passIdx]; - - // Insert aliasing barriers for resources that reuse physical memory - InsertAliasingBarriers(pass, passIdx); - } - } - - /// - /// Inserts aliasing barriers when a placed resource is reused. - /// - private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx) - { - // Check all resources written by this pass (both textures and buffers) - for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++) - { - var writeList = pass.resourceWrites[resType]; - for (var i = 0; i < writeList.Count; i++) - { - var id = writeList[i]; - var resource = _resources.GetResource(id); - - // Skip imported resources - if (resource.isImported) - { - continue; - } - - // Check if this is the first use of this logical resource - if (resource.firstUsePass == pass.index) - { - // Get the placed resource - var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value); - if (placedIndex >= 0) - { - var placed = _aliasingManager.GetPlacedResource(placedIndex); - - // If this placed resource has multiple aliased resources, - // we need an aliasing barrier when switching between them - if (placed != null && placed.aliasedLogicalResources.Count > 1) - { - // Find the resource that used this placed memory most recently before this pass - Identifier resourceBefore = default; - var mostRecentLastUse = -1; - - foreach (var otherLogicalIndex in placed.aliasedLogicalResources) - { - if (otherLogicalIndex != id.Value) - { - // Get resource by global index - var otherResource = _resources.GetResourceByIndex(otherLogicalIndex); - - // Check if this resource finished before our resource starts - if (otherResource.lastUsePass < pass.index && - otherResource.lastUsePass > mostRecentLastUse) - { - mostRecentLastUse = otherResource.lastUsePass; - resourceBefore = new Identifier(otherLogicalIndex); - } - } - } - - // If we found a previous resource, insert aliasing barrier (sync update) - if (mostRecentLastUse >= 0) - { - // Aliasing Requirement: Transition to Undefined, Sync with Predecessor - var targetState = new ResourceBarrierData(BarrierLayout.Undefined, BarrierAccess.NoAccess, BarrierSync.None); - var barrier = ResourceBarrier.CreateAliasing(passIdx, id, resourceBefore, targetState); - _barriers.Add(barrier); - - // Update local tracker so subsequent transitions know it's Undefined - _resourceStates[id.Value] = targetState; - } - } - } - } - } - } - } - - private ResourceBarrierData GetBufferReadBarrierData(Identifier handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType) - { - if (resourceType == RenderGraphResourceType.Texture) - { - return new ResourceBarrierData(BarrierLayout.ShaderResource, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading); - } - - var sync = BarrierSync.PixelShading | BarrierSync.NonPixelShading; - var access = BarrierAccess.ShaderResource; + // Compute structural hash for caching + var graphHash = RenderGraphHasher.ComputeGraphHash(_passes, _resources); - var resource = _resources.GetResource(handle); - if (resource.bufferDesc.Usage.HasFlag(BufferUsage.IndirectArgument)) - { - sync = BarrierSync.ExecuteIndirect; - access = BarrierAccess.IndirectArgument; - } - - return new ResourceBarrierData(BarrierLayout.Undefined, access, sync); - } - - /// - /// Builds native render passes by merging compatible consecutive raster passes. - /// Uses conservative merging: only merge passes with identical attachments and no barriers between them. - /// - private void BuildNativeRenderPasses() - { - // Clear previous native passes - for (var i = 0; i < _nativePasses.Count; i++) - { - _objectPool.Return(_nativePasses[i]); - } - _nativePasses.Clear(); - - NativeRenderPass? currentNativePass = null; - - for (var i = 0; i < _compiledPasses.Count; i++) - { - var pass = _compiledPasses[i]; - - // Only raster passes can be merged into native render passes - // Compute passes break the current native render pass - if (pass.type != RenderPassType.Raster) - { - // Close current native pass if open - if (currentNativePass != null) - { - _nativePasses.Add(currentNativePass); - currentNativePass = null; - } - continue; // Compute/Unsafe passes execute outside native render passes - } - - - // Check if we can merge with current native pass - if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i)) - { - // Merge into existing native pass - currentNativePass.mergedPassIndices.Add(i); - currentNativePass.lastLogicalPass = i; - } - else - { - // Start new native pass - if (currentNativePass != null) - { - _nativePasses.Add(currentNativePass); - } - - currentNativePass = CreateNativePass(pass, i); - } - } - - // Add final native pass - if (currentNativePass != null) - { - _nativePasses.Add(currentNativePass); - } - - // Infer load/store operations for all native passes - for (var i = 0; i < _nativePasses.Count; i++) - { - InferLoadStoreOps(_nativePasses[i]); - } - } - - /// - /// Creates a new native render pass from a logical pass. - /// - private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex) - { - var nativePass = _objectPool.Rent(); - nativePass.Reset(); - - nativePass.index = _nativePasses.Count; - nativePass.mergedPassIndices.Add(passIndex); - nativePass.firstLogicalPass = passIndex; - nativePass.lastLogicalPass = passIndex; - nativePass.allowUAVWrites = pass.randomAccess.Count > 0; - - // Copy color attachments - nativePass.colorAttachmentCount = pass.maxColorIndex + 1; - for (var i = 0; i <= pass.maxColorIndex; i++) - { - var access = pass.colorAccess[i]; - nativePass.colorAttachments[i] = new RenderTargetInfo - { - texture = access.id, - access = access.accessFlags - }; - } - - // Copy depth attachment - if (!pass.depthAccess.id.IsInvalid) - { - nativePass.hasDepthAttachment = true; - nativePass.depthAttachment = new DepthStencilInfo - { - texture = pass.depthAccess.id, - access = pass.depthAccess.accessFlags - }; - } - - return nativePass; - } - - /// - /// Checks if a logical pass can be merged into an existing native render pass. - /// Conservative merging: only merge if attachments match and no barriers needed. - /// - private bool CanMergePasses(NativeRenderPass nativePass, RenderGraphPassBase pass, int passIndex) - { - // Don't merge if UAVs are involved (conservative) - if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites) - { - return false; - } - - // Check if attachment configuration matches - if (!AttachmentsMatch(nativePass, pass)) - { - return false; - } - - // Check if barriers are needed between last merged pass and this pass - if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex)) - { - return false; - } - - return true; - } - - /// - /// Checks if the attachment configuration of a pass matches the native pass. - /// - private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass) - { - // Check color attachment count - if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1) - { - return false; - } - - // Check each color attachment - for (var i = 0; i < nativePass.colorAttachmentCount; i++) - { - if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id) - { - return false; - } - } - - // Check depth attachment - if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid) - { - return false; - } - - if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id) - { - return false; - } - - return true; - } - - /// - /// Checks if any barriers are required between two passes that would prevent merging. - /// Only barriers affecting render targets prevent merging; SRV barriers are fine. - /// - private bool RequiresBarrierBetweenPasses(int passA, int passB) - { - var laterPass = _compiledPasses[passB]; - - // Build a set of render target resource IDs (color + depth) - var renderTargets = new HashSet>(); - for (var i = 0; i <= laterPass.maxColorIndex; i++) - { - if (!laterPass.colorAccess[i].id.IsInvalid) - { - renderTargets.Add(laterPass.colorAccess[i].id.AsResource()); - } - } - if (!laterPass.depthAccess.id.IsInvalid) - { - renderTargets.Add(laterPass.depthAccess.id.AsResource()); - } - - // Check if any barriers for passB affect render targets - for (var i = 0; i < _barriers.Count; i++) - { - if (_barriers[i].PassIndex == passB) - { - // Only prevent merge if barrier affects a render target - if (renderTargets.Contains(_barriers[i].Resource)) - { - return true; // Barrier affects render target, cannot merge - } - } - - if (_barriers[i].PassIndex > passB) - { - break; // No more barriers for this pass - } - } - - return false; - } - - /// - /// Infers optimal load/store operations for all attachments in a native render pass. - /// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs). - /// - private void InferLoadStoreOps(NativeRenderPass nativePass) - { - // Infer load/store ops for color attachments - for (var i = 0; i < nativePass.colorAttachmentCount; i++) - { - ref var attachment = ref nativePass.colorAttachments[i]; - var resource = _resources.GetResource(attachment.texture); - var flags = attachment.access; - - // ===== LOAD OP INFERENCE ===== - - // 1. First use - if (resource.firstUsePass == nativePass.firstLogicalPass) - { - // Clear at first use - if (resource.rgTextureDesc.clearAtFirstUse) - { - attachment.loadOp = AttachmentLoadOp.Clear; - attachment.clearColor = resource.rgTextureDesc.clearColor; - } - else - { - attachment.loadOp = AttachmentLoadOp.DontCare; - } - } - // 2. Discard flag: DontCare for performance - else if (flags.HasFlag(AccessFlags.Discard)) - { - attachment.loadOp = AttachmentLoadOp.DontCare; - } - // 3. Read flag: Must preserve existing contents - else if (flags.HasFlag(AccessFlags.Read)) - { - attachment.loadOp = AttachmentLoadOp.Load; - } - // 4. Continuation from previous pass - else - { - attachment.loadOp = AttachmentLoadOp.Load; - } - - // ===== STORE OP INFERENCE ===== - - // Last use: No one needs it after this native pass - if (resource.lastUsePass == nativePass.lastLogicalPass) - { - if (resource.rgTextureDesc.discardAtLastUse) - { - attachment.storeOp = AttachmentStoreOp.DontCare; - } - else - { - attachment.storeOp = AttachmentStoreOp.Store; - } - } - // Intermediate: Store for future passes - else - { - attachment.storeOp = AttachmentStoreOp.Store; - } - - } - - // Infer load/store ops for depth attachment - if (nativePass.hasDepthAttachment) - { - ref var attachment = ref nativePass.depthAttachment; - var resource = _resources.GetResource(attachment.texture); - var flags = attachment.access; - - // ===== LOAD OP INFERENCE ===== - - // 1. First Use - if (resource.firstUsePass == nativePass.firstLogicalPass) - { - // Clear at first use - if (resource.rgTextureDesc.clearAtFirstUse) - { - attachment.loadOp = AttachmentLoadOp.Clear; - attachment.clearDepth = resource.rgTextureDesc.clearDepth; - attachment.clearStencil = resource.rgTextureDesc.clearStencil; - } - else - { - attachment.loadOp = AttachmentLoadOp.DontCare; - } - } - // 2. Discard flag: DontCare for performance - else if (flags.HasFlag(AccessFlags.Discard)) - { - attachment.loadOp = AttachmentLoadOp.DontCare; - } - // 3. Read flag: Must preserve existing contents - else if (flags.HasFlag(AccessFlags.Read)) - { - attachment.loadOp = AttachmentLoadOp.Load; - } - // 4. Continuation from previous pass - else - { - attachment.loadOp = AttachmentLoadOp.Load; - } - - // ===== STORE OP INFERENCE ===== - - // Depth is commonly discarded (depth-only passes, intermediate depth) - if (resource.lastUsePass == nativePass.lastLogicalPass) - { - if (resource.rgTextureDesc.discardAtLastUse) - { - attachment.storeOp = AttachmentStoreOp.DontCare; - } - else - { - attachment.storeOp = AttachmentStoreOp.Store; - } - } - else - { - attachment.storeOp = AttachmentStoreOp.Store; - } - - } + // Delegate to compiler + _compiler.Compile(in viewState, graphHash, _passes, _compiledPasses, _nativePasses, _compiledBarriers); + _compiled = true; } /// /// Executes all compiled passes using native render passes where possible. + /// Delegates to RenderGraphExecutor for the actual execution work. /// - public unsafe void Execute(ICommandBuffer cmd) + public void Execute(ICommandBuffer cmd) { if (!_compiled) { throw new InvalidOperationException("Render graph must be compiled before execution. Call Compile(viewState) first."); } - var barrierIndex = 0; - var nativePassIndex = 0; - var logicalPassIndex = 0; - - _context.SetCommandBuffer(cmd); - - var pPassRTDescs = stackalloc PassRenderTargetDesc[8]; - var pRtFormats = stackalloc TextureFormat[8]; - - while (logicalPassIndex < _compiledPasses.Count) - { - var pass = _compiledPasses[logicalPassIndex]; - - // Check if this pass is part of a native render pass - if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count) - { - var nativePass = _nativePasses[nativePassIndex]; - - // Build barriers for ALL merged passes before beginning the native render pass - for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) - { - var mergedPassIdx = nativePass.mergedPassIndices[i]; - ExecuteBarriersForPass(cmd, mergedPassIdx, ref barrierIndex); - } - - // Begin native render pass - for (var i = 0; i < nativePass.colorAttachmentCount; i++) - { - var attachment = nativePass.colorAttachments[i]; - pPassRTDescs[i] = new PassRenderTargetDesc - { - Texture = _resources.GetResource(attachment.texture).backingResource.AsTexture(), - ClearColor = attachment.clearColor, - LoadOp = attachment.loadOp, - StoreOp = attachment.storeOp - }; - } - - var depthDesc = new PassDepthStencilDesc - { - Texture = nativePass.hasDepthAttachment - ? _resources.GetResource(nativePass.depthAttachment.texture).backingResource.AsTexture() - : Handle.Invalid, - ClearDepth = nativePass.depthAttachment.clearDepth, - ClearStencil = nativePass.depthAttachment.clearStencil, - DepthLoadOp = nativePass.hasDepthAttachment - ? nativePass.depthAttachment.loadOp - : AttachmentLoadOp.DontCare, - DepthStoreOp = nativePass.hasDepthAttachment - ? nativePass.depthAttachment.storeOp - : AttachmentStoreOp.DontCare, - StencilLoadOp = nativePass.hasDepthAttachment - ? nativePass.depthAttachment.loadOp - : AttachmentLoadOp.DontCare, - StencilStoreOp = nativePass.hasDepthAttachment - ? nativePass.depthAttachment.storeOp - : AttachmentStoreOp.DontCare - }; - - cmd.BeginRenderPass(new Span(pPassRTDescs, nativePass.colorAttachmentCount), depthDesc); - - for (var i = 0; i < nativePass.colorAttachmentCount; i++) - { - var attachment = nativePass.colorAttachments[i]; - var resource = _resources.GetResource(attachment.texture); - pRtFormats[i] = resource.rgTextureDesc.format; - } - - var depthFormat = nativePass.hasDepthAttachment - ? _resources.GetResource(nativePass.depthAttachment.texture).rgTextureDesc.format - : TextureFormat.Unknown; - _context.SetRenderTargetFormats(new ReadOnlySpan(pRtFormats, nativePass.colorAttachmentCount), depthFormat); - - // Build all merged logical passes within this native render pass - for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) - { - var mergedPassIdx = nativePass.mergedPassIndices[i]; - var mergedPass = _compiledPasses[mergedPassIdx]; - mergedPass.Execute(_context); - logicalPassIndex++; - } - - cmd.EndRenderPass(); - nativePassIndex++; - } - else - { - // Compute pass or standalone raster pass (not merged) or Unsafe pass - ExecuteBarriersForPass(cmd, logicalPassIndex, ref barrierIndex); - pass.Execute(_context); - logicalPassIndex++; - } - - } - } - - /// - /// Executes all barriers for a specific pass. - /// - private unsafe void ExecuteBarriersForPass(ICommandBuffer cmd, int passIndex, ref int barrierIndex) - { - const int MaxBatch = 64; - var barriers = stackalloc BarrierDesc[MaxBatch]; - var barrierCount = 0; - - void Flush() - { - if (barrierCount > 0) - { - cmd.ResourceBarrier(new ReadOnlySpan(barriers, barrierCount)); - barrierCount = 0; - } - } - - // 1. Process Aliasing Barriers (Explicitly scheduled) - while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex) - { - var barrierReq = _barriers[barrierIndex++]; - var resourceHandle = _resources.GetResource(barrierReq.Resource).backingResource; - - BarrierLayout layoutBefore; - BarrierAccess accessBefore; - BarrierSync syncBefore; - - if (barrierReq.AliasingPredecessor.IsValid) - { - var predHandle = _resources.GetResource(barrierReq.AliasingPredecessor).backingResource; - var predState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(predHandle).GetValueOrThrow(); - - layoutBefore = BarrierLayout.Undefined; - accessBefore = BarrierAccess.NoAccess; - syncBefore = predState.Sync; - } - else - { - var currentState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(resourceHandle).GetValueOrThrow(); - layoutBefore = currentState.Layout; - accessBefore = currentState.Access; - syncBefore = currentState.Sync; - } - - var target = barrierReq.TargetState; - var resType = _resources.GetResource(barrierReq.Resource).type; - - BarrierDesc desc; - if (resType == RenderGraphResourceType.Texture) - { - desc = BarrierDesc.Texture(resourceHandle, - syncBefore, target.Sync, - accessBefore, target.Access, - layoutBefore, target.Layout, - discard: barrierReq.Flags.HasFlag(BarrierFlags.Discard)); - } - else - { - desc = BarrierDesc.Buffer(resourceHandle, - syncBefore, target.Sync, - accessBefore, target.Access); - } - - if (barrierCount >= MaxBatch) - { - Flush(); - } - - barriers[barrierCount++] = desc; - - //_graphicsEngine.ResourceDatabase.SetResourceBarrierData(resourceHandle, target); - } - - // 2. Process Implicit Transitions (Iterate pass resources) - var pass = _compiledPasses[passIndex]; - - // Helper to check and issue transition - void IssueTransition(Identifier id, ResourceBarrierData target) - { - var resource = _resources.GetResource(id); - var handle = resource.backingResource; - var current = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(handle).GetValueOrThrow(); - - if (current.Layout != target.Layout || current.Access != target.Access || current.Sync != target.Sync) - { - BarrierDesc desc; - if (resource.type == RenderGraphResourceType.Texture) - { - desc = BarrierDesc.Texture(handle, - current.Sync, target.Sync, - current.Access, target.Access, - current.Layout, target.Layout); - } - else - { - desc = BarrierDesc.Buffer(handle, - current.Sync, target.Sync, - current.Access, target.Access); - } - - if (barrierCount >= MaxBatch) Flush(); - barriers[barrierCount++] = desc; - - //_graphicsEngine.ResourceDatabase.SetResourceBarrierData(handle, target); - } - } - - // Reads - for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) - { - var readList = pass.resourceReads[i]; - for (var j = 0; j < readList.Count; j++) - { - var handle = readList[j]; - var targetState = GetBufferReadBarrierData(handle, pass, (RenderGraphResourceType)i); - IssueTransition(handle, targetState); - } - } - - switch (pass.type) - { - case RenderPassType.Raster: - for (var i = 0; i <= pass.maxColorIndex; i++) - { - if (pass.colorAccess[i].id.IsValid) - { - var usage = pass.colorAccess[i].usage; - var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); - IssueTransition(pass.colorAccess[i].id.AsResource(), targetState); - } - } - - if (pass.depthAccess.id.IsValid) - { - var usage = pass.depthAccess.usage; - var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); - IssueTransition(pass.depthAccess.id.AsResource(), targetState); - } - - var uavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); - for (var i = 0; i < pass.randomAccess.Count; i++) - { - IssueTransition(pass.randomAccess[i], uavState); - } - break; - - case RenderPassType.Compute: - var computeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.ComputeShading); - for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) - { - var writeList = pass.resourceWrites[i]; - for (var j = 0; j < writeList.Count; j++) - { - IssueTransition(writeList[j], computeUavState); - } - } - break; - - case RenderPassType.Unsafe: - var rtState = new ResourceBarrierData(BarrierLayout.RenderTarget, BarrierAccess.RenderTarget, BarrierSync.RenderTarget); - for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) - { - var writeList = pass.resourceWrites[i]; - for (var j = 0; j < writeList.Count; j++) - { - IssueTransition(writeList[j], rtState); - } - } - - var unsafeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); - for (var i = 0; i < pass.randomAccess.Count; i++) - { - IssueTransition(pass.randomAccess[i], unsafeUavState); - } - break; - } - - Flush(); + _executor.Execute(cmd, _compiledPasses, _nativePasses, _compiledBarriers); } public void Dispose() { - foreach (var resource in _resources.Resources) - { - _graphicsEngine.ResourceDatabase.ReleaseResource(resource.backingResource); - } - - _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + _compiler.Dispose(); // We need to reset the whole graph to return resources to the pool - // FIX: Ideally we should call dispose here for each subsystem Reset(); } } diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraph.cs.backup b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs.backup new file mode 100644 index 0000000..84769df --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs.backup @@ -0,0 +1,1170 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using System.Diagnostics; +using System.IO.Hashing; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Main render graph class that manages resource allocation and pass execution. +/// +public sealed class RenderGraph : IDisposable +{ + private readonly IGraphicsEngine _graphicsEngine; + + private readonly RenderGraphObjectPool _objectPool; + private readonly RenderGraphResourceRegistry _resources; + + private readonly List _passes; + private readonly List _compiledPasses; + private readonly List _nativePasses; + + private readonly RenderGraphBuilder _builder; + private readonly ResourceAliasingManager _aliasingManager; + + private readonly List _compiledBarriers = new(128); + + private readonly RenderGraphCompilationCache _compilationCache = new(); + private readonly RenderGraphContext _context; + + private readonly RenderGraphCompiler _compiler; + private readonly RenderGraphExecutor _executor; + private readonly RenderGraphNativePassBuilder _nativePassBuilder; + + private bool _compiled; + private ViewState _currentViewState; + + public RenderGraphBlackboard Blackboard + { + get; + } + + public RenderGraph(IGraphicsEngine graphicsEngine) + { + _graphicsEngine = graphicsEngine; + + _objectPool = new RenderGraphObjectPool(); + _resources = new RenderGraphResourceRegistry(_objectPool); + + _passes = new List(32); + _compiledPasses = new List(32); + _nativePasses = new List(32); + + _builder = new RenderGraphBuilder(); + _aliasingManager = new ResourceAliasingManager(graphicsEngine.ResourceAllocator, _objectPool); + + _compilationCache = new RenderGraphCompilationCache(); + + _context = new RenderGraphContext( + _graphicsEngine.ResourceDatabase, + _graphicsEngine.PipelineLibrary, + _graphicsEngine.ShaderCompiler, + _resources + ); + + _nativePassBuilder = new RenderGraphNativePassBuilder(_objectPool, _resources); + _compiler = new RenderGraphCompiler(_graphicsEngine, _resources, _aliasingManager, _nativePassBuilder, _compilationCache); + _executor = new RenderGraphExecutor(_graphicsEngine, _resources, _context); + + Blackboard = new RenderGraphBlackboard(); + } + + + /// + /// Resets the render graph for a new frame. + /// Reuses existing allocations to minimize GC. + /// + public void Reset() + { + // Clear blackboard data + Blackboard.Clear(); + + // Reset resources but keep allocations + _resources.Reset(); + + // Reset aliasing manager + _aliasingManager.Reset(); + + // Clear compiled barriers + _compiledBarriers.Clear(); + + // Return passes to the pool and reset count + for (var i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + pass.Reset(_objectPool); + } + + _passes.Clear(); + + // Clear compiled passes list + _compiledPasses.Clear(); + + // Return native passes to pool + for (var i = 0; i < _nativePasses.Count; i++) + { + _objectPool.Return(_nativePasses[i]); + } + _nativePasses.Clear(); + + _compiled = false; + } + + /// + /// Imports an external texture into the render graph. + /// + /// The external texture handle. + /// The identifier of the imported render graph texture. Invalid if import fails. + public Identifier ImportTexture(Handle texture, string name, + Color128 clearColor = default, float clearDepth = 1.0f, byte clearStencil = 0, + bool clearAtFirstUse = true, bool discardAtLastUse = true) + { + var r = _graphicsEngine.ResourceDatabase.GetResourceDescription(texture.AsResource()); + if (r.IsFailure) + { + return Identifier.Invalid; + } + + var desc = r.Value; + return _resources.ImportTexture(in desc._desc.textureDescription, texture, name, clearColor, clearDepth, clearStencil, clearAtFirstUse, discardAtLastUse); + } + + /// + /// Imports an external buffer into the render graph. + /// + /// The external buffer handle. + /// The identifier of the imported render graph buffer. Invalid if import fails. + public Identifier ImportBuffer(Handle buffer, string name) + { + var r = _graphicsEngine.ResourceDatabase.GetResourceDescription(buffer.AsResource()); + if (r.IsFailure) + { + return Identifier.Invalid; + } + + var desc = r.Value; + return _resources.ImportBuffer(in desc._desc.bufferDescription, buffer, name); + } + + public IRasterRenderGraphBuilder AddRasterRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Raster); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + public IComputeRenderGraphBuilder AddComputeRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Compute); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + public IUnsafeRenderGraphBuilder AddUnsafeRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Unsafe); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + /// + /// Compiles the render graph by culling unused passes and determining resource lifetimes. + /// + public void Compile(in ViewState viewState) + { + if (_compiled) + { + return; + } + + _currentViewState = viewState; + + // Resolve texture sizes before computing hash + _resources.ResolveTextureSizes(in viewState); + + var graphHash = RenderGraphHasher.ComputeGraphHash(_passes, _resources); + + _compiler.Compile(in viewState, graphHash, _passes, _compiledPasses, _nativePasses, _compiledBarriers); + + _compiled = true; + } + + /// + /// Executes all compiled passes using native render passes where possible. + /// + public void Execute(ICommandBuffer cmd) + { + if (!_compiled) + { + throw new InvalidOperationException("Render graph must be compiled before execution. Call Compile(viewState) first."); + } + + _executor.Execute(cmd, _compiledPasses, _nativePasses, _compiledBarriers); + } + + public void Dispose() + { + _compiler.Dispose(); + + // We need to reset the whole graph to return resources to the pool + Reset(); + } +} + + { + Size = _aliasingManager.Heap.size + 64 * 1024, // Add 64KB padding to avoid potential overflows + Alignment = ResourceHeap.DEFAULT_ALIGNMENT, + HeapFlags = HeapFlags.AlowBufferAndTexture, + HeapType = HeapType.Default + }; + + _resourceHeap = _graphicsEngine.ResourceAllocator.Allocate(in allocationDesc, "RenderGraphResourceHeap"); + + for (var i = 0; i < _resources.Resources.Count; i++) + { + var placedIndex = _aliasingManager.GetPlacedResourceIndex(i); + var placed = _aliasingManager.GetPlacedResource(placedIndex); + if (placed == null) + { + continue; + } + + var res = _resources.Resources[i]; + var ops = new CreationOptions + { + AllocationType = ResourceAllocationType.Suballocation, + Heap = _resourceHeap, + Offset = placed.heapOffset, + }; + + if (res.type == RenderGraphResourceType.Texture) + { + var textureDesc = res.rgTextureDesc.ToTextureDesc(res.resolvedWidth, res.resolvedHeight); + res.backingResource = _graphicsEngine.ResourceAllocator.CreateTexture(in textureDesc, res.name, ops).AsResource(); + } + else if (res.type == RenderGraphResourceType.Buffer) + { + res.backingResource = _graphicsEngine.ResourceAllocator.CreateBuffer(in res.bufferDesc, res.name, ops).AsResource(); + } + else + { + throw new NotSupportedException(); + } + + _compilationCache.UpdateBackingResource(i, res.backingResource); + } + } + + /// + /// Restores the render graph state from cached compilation results. + /// + private void RestoreFromCache(CachedCompilation cached) + { + // Restore compiled pass list + _compiledPasses.Clear(); + for (var i = 0; i < cached.compiledPassIndices.Count; i++) + { + var passIndex = cached.compiledPassIndices[i]; + _compiledPasses.Add(_passes[passIndex]); + } + + // Restore culling flags + for (var i = 0; i < _passes.Count && i < cached.passCulledFlags.Count; i++) + { + _passes[i].culled = cached.passCulledFlags[i]; + } + + // Restore aliasing mappings (need to update ResourceAliasingManager) + _aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources); + + // Restore compiled barriers (deep copy to avoid shared references) + _compiledBarriers.Clear(); + for (var i = 0; i < cached.compiledBarriers.Count; i++) + { + _compiledBarriers.Add(cached.compiledBarriers[i]); + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + + if (!res.isImported) + { + res.backingResource = cached.backingResources[i]; + } + } + + BuildNativeRenderPasses(); + } + + /// + /// Stores current compilation results in the cache. + /// + private void StoreInCache(ulong graphHash) + { + var cacheData = new CachedCompilation(); + + // Store view state + cacheData.viewState = _currentViewState; + + // Store compiled pass indices + for (var i = 0; i < _compiledPasses.Count; i++) + { + cacheData.compiledPassIndices.Add(_compiledPasses[i].index); + } + + // Store culling flags for all passes + for (var i = 0; i < _passes.Count; i++) + { + cacheData.passCulledFlags.Add(_passes[i].culled); + } + + // Store aliasing mappings + _aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources); + + // Store compiled barriers + for (var i = 0; i < _compiledBarriers.Count; i++) + { + cacheData.compiledBarriers.Add(_compiledBarriers[i]); + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + cacheData.backingResources.Add(res.backingResource); + } + + _compilationCache.Store(graphHash, cacheData); + } + + private void UnculProducer(Identifier resource) + { + var res = _resources.GetResource(resource); + if (res.producerPass >= 0) + { + var producer = _passes[res.producerPass]; + if (producer.culled) + { + producer.culled = false; + UnculDependencies(producer); + } + } + } + + private void UnculDependencies(RenderGraphPassBase pass) + { + // Un-cull producers of read resources + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + UnculProducer(readList[j]); + } + } + + // Un-cull producers of color attachments + for (var i = 0; i < pass.maxColorIndex; i++) + { + if (pass.colorAccess[i].id.IsValid) + { + UnculProducer(pass.colorAccess[i].id.AsResource()); + } + } + + // Un-cull producer of depth attachment + if (pass.depthAccess.id.IsValid) + { + UnculProducer(pass.depthAccess.id.AsResource()); + } + + // Un-cull producers of UAV resources (if not already in reads/writes) + for (var i = 0; i < pass.randomAccess.Count; i++) + { + UnculProducer(pass.randomAccess[i]); + } + } + + /// + /// Compiles all barriers needed for execution, storing only target states. + /// Barriers include aliasing barriers and implicit state transitions. + /// + private void CompileBarriers() + { + _compiledBarriers.Clear(); + + // Process each compiled pass in order + for (var passIdx = 0; passIdx < _compiledPasses.Count; passIdx++) + { + var pass = _compiledPasses[passIdx]; + + // 1. Insert aliasing barriers for resources that reuse physical memory + InsertAliasingBarriers(pass, passIdx); + + // 2. Compile implicit transitions for all resources accessed by this pass + CompileImplicitTransitions(pass, passIdx); + } + } + + /// + /// Inserts aliasing barriers when a placed resource is reused. + /// + private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx) + { + // Check all resources written by this pass (both textures and buffers) + for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++) + { + var writeList = pass.resourceWrites[resType]; + for (var i = 0; i < writeList.Count; i++) + { + var id = writeList[i]; + var resource = _resources.GetResource(id); + + // Skip imported resources + if (resource.isImported) + { + continue; + } + + // Check if this is the first use of this logical resource + if (resource.firstUsePass == pass.index) + { + // Get the placed resource + var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value); + if (placedIndex >= 0) + { + var placed = _aliasingManager.GetPlacedResource(placedIndex); + + // If this placed resource has multiple aliased resources, + // we need an aliasing barrier when switching between them + if (placed != null && placed.aliasedLogicalResources.Count > 1) + { + // Find the resource that used this placed memory most recently before this pass + Identifier resourceBefore = default; + var mostRecentLastUse = -1; + + foreach (var otherLogicalIndex in placed.aliasedLogicalResources) + { + if (otherLogicalIndex != id.Value) + { + // Get resource by global index + var otherResource = _resources.GetResourceByIndex(otherLogicalIndex); + + // Check if this resource finished before our resource starts + if (otherResource.lastUsePass < pass.index && + otherResource.lastUsePass > mostRecentLastUse) + { + mostRecentLastUse = otherResource.lastUsePass; + resourceBefore = new Identifier(otherLogicalIndex); + } + } + } + + // If we found a previous resource, insert aliasing barrier + if (mostRecentLastUse >= 0) + { + // Aliasing Requirement: Transition to Undefined, Sync with Predecessor + var targetState = new ResourceBarrierData(BarrierLayout.Undefined, BarrierAccess.NoAccess, BarrierSync.None); + var barrier = new CompiledBarrier + { + PassIndex = passIdx, + Resource = id, + TargetState = targetState, + AliasingPredecessor = resourceBefore, + Flags = BarrierFlags.FirstUsage | BarrierFlags.Discard, + ResourceType = resource.type + }; + _compiledBarriers.Add(barrier); + } + } + } + } + } + } + } + + /// + /// Compiles implicit state transitions for all resources accessed by a pass. + /// Stores only the target state - the before state will be queried from ResourceDatabase at execution time. + /// + private void CompileImplicitTransitions(RenderGraphPassBase pass, int passIdx) + { + // Helper to add a compiled barrier for a resource transition + void AddTransition(Identifier id, ResourceBarrierData targetState) + { + var resource = _resources.GetResource(id); + var barrier = new CompiledBarrier + { + PassIndex = passIdx, + Resource = id, + TargetState = targetState, + AliasingPredecessor = Identifier.Invalid, + Flags = BarrierFlags.None, + ResourceType = resource.type + }; + _compiledBarriers.Add(barrier); + } + + // Compile transitions for read resources + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + var handle = readList[j]; + var targetState = GetBufferReadBarrierData(handle, pass, (RenderGraphResourceType)i); + AddTransition(handle, targetState); + } + } + + // Compile transitions based on pass type + switch (pass.type) + { + case RenderPassType.Raster: + // Color attachments + for (var i = 0; i <= pass.maxColorIndex; i++) + { + if (pass.colorAccess[i].id.IsValid) + { + var usage = pass.colorAccess[i].usage; + var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); + AddTransition(pass.colorAccess[i].id.AsResource(), targetState); + } + } + + // Depth attachment + if (pass.depthAccess.id.IsValid) + { + var usage = pass.depthAccess.usage; + var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); + AddTransition(pass.depthAccess.id.AsResource(), targetState); + } + + // UAV resources + var uavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); + for (var i = 0; i < pass.randomAccess.Count; i++) + { + AddTransition(pass.randomAccess[i], uavState); + } + break; + + case RenderPassType.Compute: + var computeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.ComputeShading); + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + AddTransition(writeList[j], computeUavState); + } + } + break; + + case RenderPassType.Unsafe: + var rtState = new ResourceBarrierData(BarrierLayout.RenderTarget, BarrierAccess.RenderTarget, BarrierSync.RenderTarget); + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + AddTransition(writeList[j], rtState); + } + } + + var unsafeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); + for (var i = 0; i < pass.randomAccess.Count; i++) + { + AddTransition(pass.randomAccess[i], unsafeUavState); + } + break; + } + } + + private ResourceBarrierData GetBufferReadBarrierData(Identifier handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType) + { + if (resourceType == RenderGraphResourceType.Texture) + { + return new ResourceBarrierData(BarrierLayout.ShaderResource, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading); + } + + var sync = BarrierSync.PixelShading | BarrierSync.NonPixelShading; + var access = BarrierAccess.ShaderResource; + + var resource = _resources.GetResource(handle); + if (resource.bufferDesc.Usage.HasFlag(BufferUsage.IndirectArgument)) + { + sync = BarrierSync.ExecuteIndirect; + access = BarrierAccess.IndirectArgument; + } + + return new ResourceBarrierData(BarrierLayout.Undefined, access, sync); + } + + /// + /// Builds native render passes by merging compatible consecutive raster passes. + /// Uses conservative merging: only merge passes with identical attachments and no barriers between them. + /// + private void BuildNativeRenderPasses() + { + // Clear previous native passes + for (var i = 0; i < _nativePasses.Count; i++) + { + _objectPool.Return(_nativePasses[i]); + } + _nativePasses.Clear(); + + NativeRenderPass? currentNativePass = null; + + for (var i = 0; i < _compiledPasses.Count; i++) + { + var pass = _compiledPasses[i]; + + // Only raster passes can be merged into native render passes + // Compute passes break the current native render pass + if (pass.type != RenderPassType.Raster) + { + // Close current native pass if open + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + currentNativePass = null; + } + continue; // Compute/Unsafe passes execute outside native render passes + } + + + // Check if we can merge with current native pass + if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i)) + { + // Merge into existing native pass + currentNativePass.mergedPassIndices.Add(i); + currentNativePass.lastLogicalPass = i; + } + else + { + // Start new native pass + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + } + + currentNativePass = CreateNativePass(pass, i); + } + } + + // Add final native pass + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + } + + // Infer load/store operations for all native passes + for (var i = 0; i < _nativePasses.Count; i++) + { + InferLoadStoreOps(_nativePasses[i]); + } + } + + /// + /// Creates a new native render pass from a logical pass. + /// + private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex) + { + var nativePass = _objectPool.Rent(); + nativePass.Reset(); + + nativePass.index = _nativePasses.Count; + nativePass.mergedPassIndices.Add(passIndex); + nativePass.firstLogicalPass = passIndex; + nativePass.lastLogicalPass = passIndex; + nativePass.allowUAVWrites = pass.randomAccess.Count > 0; + + // Copy color attachments + nativePass.colorAttachmentCount = pass.maxColorIndex + 1; + for (var i = 0; i <= pass.maxColorIndex; i++) + { + var access = pass.colorAccess[i]; + nativePass.colorAttachments[i] = new RenderTargetInfo + { + texture = access.id, + access = access.accessFlags + }; + } + + // Copy depth attachment + if (!pass.depthAccess.id.IsInvalid) + { + nativePass.hasDepthAttachment = true; + nativePass.depthAttachment = new DepthStencilInfo + { + texture = pass.depthAccess.id, + access = pass.depthAccess.accessFlags + }; + } + + return nativePass; + } + + /// + /// Checks if a logical pass can be merged into an existing native render pass. + /// Conservative merging: only merge if attachments match and no barriers needed. + /// + private bool CanMergePasses(NativeRenderPass nativePass, RenderGraphPassBase pass, int passIndex) + { + // Don't merge if UAVs are involved (conservative) + if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites) + { + return false; + } + + // Check if attachment configuration matches + if (!AttachmentsMatch(nativePass, pass)) + { + return false; + } + + // Check if barriers are needed between last merged pass and this pass + if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex)) + { + return false; + } + + return true; + } + + /// + /// Checks if the attachment configuration of a pass matches the native pass. + /// + private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass) + { + // Check color attachment count + if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1) + { + return false; + } + + // Check each color attachment + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id) + { + return false; + } + } + + // Check depth attachment + if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid) + { + return false; + } + + if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id) + { + return false; + } + + return true; + } + + /// + /// Checks if any barriers are required between two passes that would prevent merging. + /// Only barriers affecting render targets prevent merging; SRV barriers are fine. + /// + private bool RequiresBarrierBetweenPasses(int passA, int passB) + { + var laterPass = _compiledPasses[passB]; + + // Build a set of render target resource IDs (color + depth) + var renderTargets = new HashSet>(); + for (var i = 0; i <= laterPass.maxColorIndex; i++) + { + if (!laterPass.colorAccess[i].id.IsInvalid) + { + renderTargets.Add(laterPass.colorAccess[i].id.AsResource()); + } + } + if (!laterPass.depthAccess.id.IsInvalid) + { + renderTargets.Add(laterPass.depthAccess.id.AsResource()); + } + + // Check if any compiled barriers for passB affect render targets + for (var i = 0; i < _compiledBarriers.Count; i++) + { + if (_compiledBarriers[i].PassIndex == passB) + { + // Only prevent merge if barrier affects a render target + if (renderTargets.Contains(_compiledBarriers[i].Resource)) + { + return true; // Barrier affects render target, cannot merge + } + } + + if (_compiledBarriers[i].PassIndex > passB) + { + break; // No more barriers for this pass + } + } + + return false; + } + + /// + /// Infers optimal load/store operations for all attachments in a native render pass. + /// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs). + /// + private void InferLoadStoreOps(NativeRenderPass nativePass) + { + // Infer load/store ops for color attachments + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + ref var attachment = ref nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearColor = resource.rgTextureDesc.clearColor; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Last use: No one needs it after this native pass + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + // Intermediate: Store for future passes + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + + // Infer load/store ops for depth attachment + if (nativePass.hasDepthAttachment) + { + ref var attachment = ref nativePass.depthAttachment; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First Use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearDepth = resource.rgTextureDesc.clearDepth; + attachment.clearStencil = resource.rgTextureDesc.clearStencil; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Depth is commonly discarded (depth-only passes, intermediate depth) + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + } + + /// + /// Executes all compiled passes using native render passes where possible. + /// + public unsafe void Execute(ICommandBuffer cmd) + { + if (!_compiled) + { + throw new InvalidOperationException("Render graph must be compiled before execution. Call Compile(viewState) first."); + } + + var barrierIndex = 0; + var nativePassIndex = 0; + var logicalPassIndex = 0; + + _context.SetCommandBuffer(cmd); + + var pPassRTDescs = stackalloc PassRenderTargetDesc[8]; + var pRtFormats = stackalloc TextureFormat[8]; + + while (logicalPassIndex < _compiledPasses.Count) + { + var pass = _compiledPasses[logicalPassIndex]; + + // Check if this pass is part of a native render pass + if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count) + { + var nativePass = _nativePasses[nativePassIndex]; + + // Build barriers for ALL merged passes before beginning the native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + ExecuteBarriersForPass(cmd, mergedPassIdx, ref barrierIndex); + } + + // Begin native render pass + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + pPassRTDescs[i] = new PassRenderTargetDesc + { + Texture = _resources.GetResource(attachment.texture).backingResource.AsTexture(), + ClearColor = attachment.clearColor, + LoadOp = attachment.loadOp, + StoreOp = attachment.storeOp + }; + } + + var depthDesc = new PassDepthStencilDesc + { + Texture = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).backingResource.AsTexture() + : Handle.Invalid, + ClearDepth = nativePass.depthAttachment.clearDepth, + ClearStencil = nativePass.depthAttachment.clearStencil, + DepthLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + DepthStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare, + StencilLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + StencilStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare + }; + + cmd.BeginRenderPass(new Span(pPassRTDescs, nativePass.colorAttachmentCount), depthDesc); + + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + pRtFormats[i] = resource.rgTextureDesc.format; + } + + var depthFormat = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).rgTextureDesc.format + : TextureFormat.Unknown; + _context.SetRenderTargetFormats(new ReadOnlySpan(pRtFormats, nativePass.colorAttachmentCount), depthFormat); + + // Build all merged logical passes within this native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + var mergedPass = _compiledPasses[mergedPassIdx]; + mergedPass.Execute(_context); + logicalPassIndex++; + } + + cmd.EndRenderPass(); + nativePassIndex++; + } + else + { + // Compute pass or standalone raster pass (not merged) or Unsafe pass + ExecuteBarriersForPass(cmd, logicalPassIndex, ref barrierIndex); + pass.Execute(_context); + logicalPassIndex++; + } + + } + } + + /// + /// Executes all barriers for a specific pass. + /// Uses pre-compiled barriers and queries before state from ResourceDatabase. + /// + private unsafe void ExecuteBarriersForPass(ICommandBuffer cmd, int passIndex, ref int barrierIndex) + { + const int MaxBatch = 64; + var barriers = stackalloc BarrierDesc[MaxBatch]; + var barrierCount = 0; + + void Flush() + { + if (barrierCount > 0) + { + cmd.ResourceBarrier(new ReadOnlySpan(barriers, barrierCount)); + barrierCount = 0; + } + } + + // Process all pre-compiled barriers for this pass + while (barrierIndex < _compiledBarriers.Count && _compiledBarriers[barrierIndex].PassIndex == passIndex) + { + var compiledBarrier = _compiledBarriers[barrierIndex++]; + var resource = _resources.GetResource(compiledBarrier.Resource); + var resourceHandle = resource.backingResource; + + // Always query the before state from ResourceDatabase (single source of truth) + var currentState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(resourceHandle).GetValueOrThrow(); + + BarrierLayout layoutBefore; + BarrierAccess accessBefore; + BarrierSync syncBefore; + + // Handle aliasing barriers specially + if (compiledBarrier.AliasingPredecessor.IsValid) + { + var predHandle = _resources.GetResource(compiledBarrier.AliasingPredecessor).backingResource; + var predState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(predHandle).GetValueOrThrow(); + + layoutBefore = BarrierLayout.Undefined; + accessBefore = BarrierAccess.NoAccess; + syncBefore = predState.Sync; + } + else + { + layoutBefore = currentState.Layout; + accessBefore = currentState.Access; + syncBefore = currentState.Sync; + } + + var target = compiledBarrier.TargetState; + + // Skip if already in target state (optimization) + if (!compiledBarrier.AliasingPredecessor.IsValid && + layoutBefore == target.Layout && + accessBefore == target.Access && + syncBefore == target.Sync) + { + continue; + } + + // Create barrier descriptor + BarrierDesc desc; + if (compiledBarrier.ResourceType == RenderGraphResourceType.Texture) + { + desc = BarrierDesc.Texture(resourceHandle, + syncBefore, target.Sync, + accessBefore, target.Access, + layoutBefore, target.Layout, + discard: compiledBarrier.Flags.HasFlag(BarrierFlags.Discard)); + } + else + { + desc = BarrierDesc.Buffer(resourceHandle, + syncBefore, target.Sync, + accessBefore, target.Access); + } + + if (barrierCount >= MaxBatch) + { + Flush(); + } + + barriers[barrierCount++] = desc; + } + + Flush(); + } + + public void Dispose() + { + foreach (var resource in _resources.Resources) + { + _graphicsEngine.ResourceDatabase.ReleaseResource(resource.backingResource); + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + + // We need to reset the whole graph to return resources to the pool + // FIX: Ideally we should call dispose here for each subsystem + Reset(); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs index 03bae7e..26037f6 100644 --- a/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs @@ -285,6 +285,7 @@ internal sealed class PlacedResource /// internal sealed class ResourceAliasingManager { + private readonly IResourceAllocator _allocator; private readonly RenderGraphObjectPool _pool; private readonly ResourceHeap _heap; @@ -306,17 +307,20 @@ internal sealed class ResourceAliasingManager if (resource.type == RenderGraphResourceType.Texture) { var textureDesc = resource.rgTextureDesc.ToTextureDesc(resource.resolvedWidth, resource.resolvedHeight); - return AlignUp(textureDesc.GetTotalBytes(), _DEFAULT_TEXTURE_ALIGNMENT); + return _allocator.GetSizeInfo(ResourceDesc.Texture(textureDesc)).Size; } else // Buffer { - return resource.bufferDesc.Size; + //return resource.bufferDesc.Size; + return _allocator.GetSizeInfo(ResourceDesc.Buffer(resource.bufferDesc)).Size; } } - public ResourceAliasingManager(RenderGraphObjectPool pool) + public ResourceAliasingManager(IResourceAllocator allocator, RenderGraphObjectPool pool) { + _allocator = allocator; _pool = pool; + _heap = new ResourceHeap(0); _placedResources = new List(32); _logicalToPlaced = new Dictionary(64); diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs index 4e938f8..76b13e7 100644 --- a/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs @@ -71,3 +71,263 @@ internal sealed class ResourceStateTracker lastAccessPass = -1; } } + +/// +/// Represents a compiled barrier with only the target state. +/// The before state is always queried from ResourceDatabase at execution time. +/// +internal struct CompiledBarrier +{ + public int PassIndex; + public Identifier Resource; + public ResourceBarrierData TargetState; + public Identifier AliasingPredecessor; // Invalid if not aliasing + public BarrierFlags Flags; + public RenderGraphResourceType ResourceType; + + public override readonly string ToString() + { + return AliasingPredecessor.IsValid + ? $"[Pass {PassIndex}] Aliasing: {AliasingPredecessor.Value}->{Resource.Value} -> {TargetState.Layout}" + : $"[Pass {PassIndex}] Transition: {Resource.Value} -> {TargetState.Layout}"; + } +} + +/// +/// Static class containing barrier compilation logic. +/// Compiles barriers at graph compilation time, storing only target states. +/// +internal static class RenderGraphBarriers +{ + /// + /// Compiles all barriers needed for execution, storing only target states. + /// Barriers include aliasing barriers and implicit state transitions. + /// + public static void CompileBarriers( + List compiledPasses, + List compiledBarriers, + RenderGraphResourceRegistry resources, + ResourceAliasingManager aliasingManager) + { + compiledBarriers.Clear(); + + // Process each compiled pass in order + for (var passIdx = 0; passIdx < compiledPasses.Count; passIdx++) + { + var pass = compiledPasses[passIdx]; + + // 1. Insert aliasing barriers for resources that reuse physical memory + InsertAliasingBarriers(pass, passIdx, compiledBarriers, resources, aliasingManager); + + // 2. Compile implicit transitions for all resources accessed by this pass + CompileImplicitTransitions(pass, passIdx, compiledBarriers, resources); + } + } + + /// + /// Inserts aliasing barriers when a placed resource is reused. + /// + private static void InsertAliasingBarriers( + RenderGraphPassBase pass, + int passIdx, + List compiledBarriers, + RenderGraphResourceRegistry resources, + ResourceAliasingManager aliasingManager) + { + // Check all resources written by this pass (both textures and buffers) + for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++) + { + var writeList = pass.resourceWrites[resType]; + for (var i = 0; i < writeList.Count; i++) + { + var id = writeList[i]; + var resource = resources.GetResource(id); + + // Skip imported resources + if (resource.isImported) + { + continue; + } + + // Check if this is the first use of this logical resource + if (resource.firstUsePass == pass.index) + { + // Get the placed resource + var placedIndex = aliasingManager.GetPlacedResourceIndex(id.Value); + if (placedIndex >= 0) + { + var placed = aliasingManager.GetPlacedResource(placedIndex); + + // If this placed resource has multiple aliased resources, + // we need an aliasing barrier when switching between them + if (placed != null && placed.aliasedLogicalResources.Count > 1) + { + // Find the resource that used this placed memory most recently before this pass + Identifier resourceBefore = default; + var mostRecentLastUse = -1; + + foreach (var otherLogicalIndex in placed.aliasedLogicalResources) + { + if (otherLogicalIndex != id.Value) + { + // Get resource by global index + var otherResource = resources.GetResourceByIndex(otherLogicalIndex); + + // Check if this resource finished before our resource starts + if (otherResource.lastUsePass < pass.index && + otherResource.lastUsePass > mostRecentLastUse) + { + mostRecentLastUse = otherResource.lastUsePass; + resourceBefore = new Identifier(otherLogicalIndex); + } + } + } + + // If we found a previous resource, insert aliasing barrier + if (mostRecentLastUse >= 0) + { + // Aliasing Requirement: Transition to Undefined, Sync with Predecessor + var targetState = new ResourceBarrierData(BarrierLayout.Undefined, BarrierAccess.NoAccess, BarrierSync.None); + var barrier = new CompiledBarrier + { + PassIndex = passIdx, + Resource = id, + TargetState = targetState, + AliasingPredecessor = resourceBefore, + Flags = BarrierFlags.FirstUsage | BarrierFlags.Discard, + ResourceType = resource.type + }; + compiledBarriers.Add(barrier); + } + } + } + } + } + } + } + + /// + /// Compiles implicit state transitions for all resources accessed by a pass. + /// Stores only the target state - the before state will be queried from ResourceDatabase at execution time. + /// + private static void CompileImplicitTransitions( + RenderGraphPassBase pass, + int passIdx, + List compiledBarriers, + RenderGraphResourceRegistry resources) + { + // Helper to add a compiled barrier for a resource transition + void AddTransition(Identifier id, ResourceBarrierData targetState) + { + var resource = resources.GetResource(id); + var barrier = new CompiledBarrier + { + PassIndex = passIdx, + Resource = id, + TargetState = targetState, + AliasingPredecessor = Identifier.Invalid, + Flags = BarrierFlags.None, + ResourceType = resource.type + }; + compiledBarriers.Add(barrier); + } + + // Compile transitions for read resources + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + var handle = readList[j]; + var targetState = GetBufferReadBarrierData(handle, pass, (RenderGraphResourceType)i, resources); + AddTransition(handle, targetState); + } + } + + // Compile transitions based on pass type + switch (pass.type) + { + case RenderPassType.Raster: + // Color attachments + for (var i = 0; i <= pass.maxColorIndex; i++) + { + if (pass.colorAccess[i].id.IsValid) + { + var usage = pass.colorAccess[i].usage; + var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); + AddTransition(pass.colorAccess[i].id.AsResource(), targetState); + } + } + + // Depth attachment + if (pass.depthAccess.id.IsValid) + { + var usage = pass.depthAccess.usage; + var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync); + AddTransition(pass.depthAccess.id.AsResource(), targetState); + } + + // UAV resources + var uavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); + for (var i = 0; i < pass.randomAccess.Count; i++) + { + AddTransition(pass.randomAccess[i], uavState); + } + break; + + case RenderPassType.Compute: + var computeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.ComputeShading); + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + AddTransition(writeList[j], computeUavState); + } + } + break; + + case RenderPassType.Unsafe: + var rtState = new ResourceBarrierData(BarrierLayout.RenderTarget, BarrierAccess.RenderTarget, BarrierSync.RenderTarget); + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + AddTransition(writeList[j], rtState); + } + } + + var unsafeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading); + for (var i = 0; i < pass.randomAccess.Count; i++) + { + AddTransition(pass.randomAccess[i], unsafeUavState); + } + break; + } + } + + private static ResourceBarrierData GetBufferReadBarrierData( + Identifier handle, + RenderGraphPassBase pass, + RenderGraphResourceType resourceType, + RenderGraphResourceRegistry resources) + { + if (resourceType == RenderGraphResourceType.Texture) + { + return new ResourceBarrierData(BarrierLayout.ShaderResource, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading); + } + + var sync = BarrierSync.PixelShading | BarrierSync.NonPixelShading; + var access = BarrierAccess.ShaderResource; + + var resource = resources.GetResource(handle); + if (resource.bufferDesc.Usage.HasFlag(BufferUsage.IndirectArgument)) + { + sync = BarrierSync.ExecuteIndirect; + access = BarrierAccess.IndirectArgument; + } + + return new ResourceBarrierData(BarrierLayout.Undefined, access, sync); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs index e07646f..b683eaa 100644 --- a/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs @@ -23,11 +23,8 @@ internal sealed class CachedCompilation // Placed resource metadata public readonly List placedResources = new(32); - // Resource barriers - public readonly List barriers = new(128); - - // Resource state mappings (for barrier generation) - public readonly Dictionary resourceStates = new(128); + // Compiled barriers (stores only target states, queries before state from ResourceDatabase) + public readonly List compiledBarriers = new(128); // Real gpu resource public readonly List> backingResources = new(32); @@ -41,8 +38,7 @@ internal sealed class CachedCompilation passCulledFlags.Clear(); logicalToPhysical.Clear(); placedResources.Clear(); - barriers.Clear(); - resourceStates.Clear(); + compiledBarriers.Clear(); backingResources.Clear(); viewState = default; } @@ -112,12 +108,7 @@ internal sealed class RenderGraphCompilationCache } _cached.placedResources.AddRange(data.placedResources); - _cached.barriers.AddRange(data.barriers); - - foreach (var kvp in data.resourceStates) - { - _cached.resourceStates[kvp.Key] = kvp.Value; - } + _cached.compiledBarriers.AddRange(data.compiledBarriers); _cached.backingResources.AddRange(data.backingResources); } diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs new file mode 100644 index 0000000..f163836 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphCompiler.cs @@ -0,0 +1,391 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Handles compilation of the render graph including pass culling, resource allocation, +/// barrier compilation, and cache management. +/// +internal sealed class RenderGraphCompiler +{ + private readonly IGraphicsEngine _graphicsEngine; + private readonly RenderGraphResourceRegistry _resources; + private readonly ResourceAliasingManager _aliasingManager; + private readonly RenderGraphNativePassBuilder _nativePassBuilder; + private readonly RenderGraphCompilationCache _compilationCache; + + private Handle _resourceHeap; + + public RenderGraphCompiler( + IGraphicsEngine graphicsEngine, + RenderGraphResourceRegistry resources, + ResourceAliasingManager aliasingManager, + RenderGraphNativePassBuilder nativePassBuilder, + RenderGraphCompilationCache compilationCache) + { + _graphicsEngine = graphicsEngine; + _resources = resources; + _aliasingManager = aliasingManager; + _nativePassBuilder = nativePassBuilder; + _compilationCache = compilationCache; + _resourceHeap = Handle.Invalid; + } + + /// + /// Compiles the render graph by culling passes, allocating resources, and preparing barriers. + /// + public void Compile( + in ViewState viewState, + ulong graphHash, + List passes, + List compiledPasses, + List nativePasses, + List compiledBarriers) + { + // Try to restore from cache + if (_compilationCache.TryGetCached(graphHash, out var cached)) + { + // Check if view state changed + if (!cached.viewState.Equals(viewState)) + { + // View state changed - re-resolve sizes and recreate GPU resources + _resources.ResolveTextureSizes(in viewState); + RestoreFromCache(cached, compiledPasses, passes, nativePasses, compiledBarriers); + + _aliasingManager.AssignPhysicalResources(_resources, passes.Count); + AllocateResources(); + + cached.viewState = viewState; + } + else + { + // Perfect cache hit - restore everything + RestoreFromCache(cached, compiledPasses, passes, nativePasses, compiledBarriers); + } + + return; + } + + // Fresh compilation needed + compiledPasses.Clear(); + + // Mark passes with side effects (writes to imported resources) + MarkPassesWithSideEffects(passes); + + // Cull passes based on dependency analysis + CullPasses(passes); + + // Build final pass list (only non-culled passes) + for (var i = 0; i < passes.Count; i++) + { + var pass = passes[i]; + if (!pass.culled) + { + compiledPasses.Add(pass); + } + } + + _aliasingManager.AssignPhysicalResources(_resources, passes.Count); + AllocateResources(); + + CompileBarriers(compiledPasses, compiledBarriers); + _nativePassBuilder.BuildNativeRenderPasses(compiledPasses, nativePasses, compiledBarriers); + StoreInCache(graphHash, viewState, compiledPasses, passes, compiledBarriers); + } + + /// + /// Marks passes that write to imported resources as having side effects. + /// + private void MarkPassesWithSideEffects(List passes) + { + for (var i = 0; i < passes.Count; i++) + { + var pass = passes[i]; + + // Check if this pass writes to any imported resources + for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) + { + var writeList = pass.resourceWrites[j]; + for (var k = 0; k < writeList.Count; k++) + { + var writeHandle = writeList[k]; + var resource = _resources.GetResource(writeHandle); + if (resource.isImported) + { + pass.hasSideEffects = true; + break; + } + } + } + } + } + + /// + /// Culls unused passes based on dependency analysis. + /// + private void CullPasses(List passes) + { + // Mark all passes as culled initially + for (var i = 0; i < passes.Count; i++) + { + passes[i].culled = passes[i].allowCulling && !passes[i].hasSideEffects; + } + + // Traverse backwards from passes with side effects + for (var i = passes.Count - 1; i >= 0; i--) + { + var pass = passes[i]; + if (!pass.culled) + { + UnculDependencies(pass, passes); + } + } + } + + /// + /// Recursively un-culls dependencies of a pass. + /// + private void UnculDependencies(RenderGraphPassBase pass, List passes) + { + // Un-cull producers of read resources + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + UnculProducer(readList[j], passes); + } + } + + // Un-cull producers of color attachments + for (var i = 0; i < pass.maxColorIndex; i++) + { + if (pass.colorAccess[i].id.IsValid) + { + UnculProducer(pass.colorAccess[i].id.AsResource(), passes); + } + } + + // Un-cull producer of depth attachment + if (pass.depthAccess.id.IsValid) + { + UnculProducer(pass.depthAccess.id.AsResource(), passes); + } + + // Un-cull producers of UAV resources (if not already in reads/writes) + for (var i = 0; i < pass.randomAccess.Count; i++) + { + UnculProducer(pass.randomAccess[i], passes); + } + } + + /// + /// Un-culls the producer of a resource. + /// + private void UnculProducer(Identifier resource, List passes) + { + var res = _resources.GetResource(resource); + if (res.producerPass >= 0) + { + var producer = passes[res.producerPass]; + if (producer.culled) + { + producer.culled = false; + UnculDependencies(producer, passes); + } + } + } + + /// + /// Allocates GPU resources for the render graph. + /// + private void AllocateResources() + { + if (_resourceHeap.IsValid) + { + foreach (var res in _resources.Resources) + { + if (res.isImported) + { + continue; + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + } + + if (_aliasingManager.Heap.size == 0) + { + return; + } + + var allocationDesc = new AllocationDesc + { + Size = _aliasingManager.Heap.size + 64 * 1024, // Add 64KB padding to avoid potential overflows + Alignment = ResourceHeap.DEFAULT_ALIGNMENT, + HeapFlags = HeapFlags.AlowBufferAndTexture, + HeapType = HeapType.Default + }; + + _resourceHeap = _graphicsEngine.ResourceAllocator.Allocate(in allocationDesc, "RenderGraphResourceHeap"); + + for (var i = 0; i < _resources.Resources.Count; i++) + { + var placedIndex = _aliasingManager.GetPlacedResourceIndex(i); + var placed = _aliasingManager.GetPlacedResource(placedIndex); + if (placed == null) + { + continue; + } + + var res = _resources.Resources[i]; + var ops = new CreationOptions + { + AllocationType = ResourceAllocationType.Suballocation, + Heap = _resourceHeap, + Offset = placed.heapOffset, + }; + + if (res.type == RenderGraphResourceType.Texture) + { + var textureDesc = res.rgTextureDesc.ToTextureDesc(res.resolvedWidth, res.resolvedHeight); + res.backingResource = _graphicsEngine.ResourceAllocator.CreateTexture(in textureDesc, res.name, ops).AsResource(); + } + else if (res.type == RenderGraphResourceType.Buffer) + { + res.backingResource = _graphicsEngine.ResourceAllocator.CreateBuffer(in res.bufferDesc, res.name, ops).AsResource(); + } + else + { + throw new NotSupportedException(); + } + + _compilationCache.UpdateBackingResource(i, res.backingResource); + } + } + + /// + /// Compiles all barriers needed for execution. + /// Delegates to RenderGraphBarriers for the actual compilation logic. + /// + private void CompileBarriers(List compiledPasses, List compiledBarriers) + { + RenderGraphBarriers.CompileBarriers(compiledPasses, compiledBarriers, _resources, _aliasingManager); + } + + /// + /// Restores the render graph state from cached compilation results. + /// + private void RestoreFromCache( + CachedCompilation cached, + List compiledPasses, + List passes, + List nativePasses, + List compiledBarriers) + { + // Restore compiled pass list + compiledPasses.Clear(); + for (var i = 0; i < cached.compiledPassIndices.Count; i++) + { + var passIndex = cached.compiledPassIndices[i]; + compiledPasses.Add(passes[passIndex]); + } + + // Restore culling flags + for (var i = 0; i < passes.Count && i < cached.passCulledFlags.Count; i++) + { + passes[i].culled = cached.passCulledFlags[i]; + } + + // Restore aliasing mappings (need to update ResourceAliasingManager) + _aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources); + + // Restore compiled barriers (deep copy to avoid shared references) + compiledBarriers.Clear(); + for (var i = 0; i < cached.compiledBarriers.Count; i++) + { + compiledBarriers.Add(cached.compiledBarriers[i]); + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + + if (!res.isImported) + { + res.backingResource = cached.backingResources[i]; + } + } + + _nativePassBuilder.BuildNativeRenderPasses(compiledPasses, nativePasses, compiledBarriers); + } + + /// + /// Stores current compilation results in the cache. + /// + private void StoreInCache( + ulong graphHash, + in ViewState viewState, + List compiledPasses, + List passes, + List compiledBarriers) + { + var cacheData = new CachedCompilation(); + + // Store view state + cacheData.viewState = viewState; + + // Store compiled pass indices + for (var i = 0; i < compiledPasses.Count; i++) + { + cacheData.compiledPassIndices.Add(compiledPasses[i].index); + } + + // Store culling flags for all passes + for (var i = 0; i < passes.Count; i++) + { + cacheData.passCulledFlags.Add(passes[i].culled); + } + + // Store aliasing mappings + _aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources); + + // Store compiled barriers + for (var i = 0; i < compiledBarriers.Count; i++) + { + cacheData.compiledBarriers.Add(compiledBarriers[i]); + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + cacheData.backingResources.Add(res.backingResource); + } + + _compilationCache.Store(graphHash, cacheData); + } + + /// + /// Releases allocated GPU resources. + /// + public void Dispose() + { + if (_resourceHeap.IsValid) + { + foreach (var res in _resources.Resources) + { + if (!res.isImported) + { + _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); + } + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + _resourceHeap = Handle.Invalid; + } + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs new file mode 100644 index 0000000..1cb2be0 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs @@ -0,0 +1,223 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Handles execution of compiled render graphs, including barrier execution and native render passes. +/// +internal sealed class RenderGraphExecutor +{ + private readonly IGraphicsEngine _graphicsEngine; + private readonly RenderGraphResourceRegistry _resources; + private readonly RenderGraphContext _context; + + public RenderGraphExecutor( + IGraphicsEngine graphicsEngine, + RenderGraphResourceRegistry resources, + RenderGraphContext context) + { + _graphicsEngine = graphicsEngine; + _resources = resources; + _context = context; + } + + /// + /// Executes all compiled passes using native render passes where possible. + /// + public unsafe void Execute( + ICommandBuffer cmd, + List compiledPasses, + List nativePasses, + List compiledBarriers) + { + var barrierIndex = 0; + var nativePassIndex = 0; + var logicalPassIndex = 0; + + _context.SetCommandBuffer(cmd); + + var pPassRTDescs = stackalloc PassRenderTargetDesc[8]; + var pRtFormats = stackalloc TextureFormat[8]; + + while (logicalPassIndex < compiledPasses.Count) + { + var pass = compiledPasses[logicalPassIndex]; + + // Check if this pass is part of a native render pass + if (pass.type == RenderPassType.Raster && nativePassIndex < nativePasses.Count) + { + var nativePass = nativePasses[nativePassIndex]; + + // Build barriers for ALL merged passes before beginning the native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + ExecuteBarriersForPass(cmd, mergedPassIdx, ref barrierIndex, compiledBarriers); + } + + // Begin native render pass + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + pPassRTDescs[i] = new PassRenderTargetDesc + { + Texture = _resources.GetResource(attachment.texture).backingResource.AsTexture(), + ClearColor = attachment.clearColor, + LoadOp = attachment.loadOp, + StoreOp = attachment.storeOp + }; + } + + var depthDesc = new PassDepthStencilDesc + { + Texture = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).backingResource.AsTexture() + : Handle.Invalid, + ClearDepth = nativePass.depthAttachment.clearDepth, + ClearStencil = nativePass.depthAttachment.clearStencil, + DepthLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + DepthStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare, + StencilLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + StencilStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare + }; + + cmd.BeginRenderPass(new Span(pPassRTDescs, nativePass.colorAttachmentCount), depthDesc); + + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + pRtFormats[i] = resource.rgTextureDesc.format; + } + + var depthFormat = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).rgTextureDesc.format + : TextureFormat.Unknown; + _context.SetRenderTargetFormats(new ReadOnlySpan(pRtFormats, nativePass.colorAttachmentCount), depthFormat); + + // Build all merged logical passes within this native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + var mergedPass = compiledPasses[mergedPassIdx]; + mergedPass.Execute(_context); + logicalPassIndex++; + } + + cmd.EndRenderPass(); + nativePassIndex++; + } + else + { + // Compute pass or standalone raster pass (not merged) or Unsafe pass + ExecuteBarriersForPass(cmd, logicalPassIndex, ref barrierIndex, compiledBarriers); + pass.Execute(_context); + logicalPassIndex++; + } + + } + } + + /// + /// Executes all barriers for a specific pass. + /// Uses pre-compiled barriers and queries before state from ResourceDatabase. + /// + private unsafe void ExecuteBarriersForPass( + ICommandBuffer cmd, + int passIndex, + ref int barrierIndex, + List compiledBarriers) + { + const int MaxBatch = 64; + var barriers = stackalloc BarrierDesc[MaxBatch]; + var barrierCount = 0; + + void Flush() + { + if (barrierCount > 0) + { + cmd.ResourceBarrier(new ReadOnlySpan(barriers, barrierCount)); + barrierCount = 0; + } + } + + // Process all pre-compiled barriers for this pass + while (barrierIndex < compiledBarriers.Count && compiledBarriers[barrierIndex].PassIndex == passIndex) + { + var compiledBarrier = compiledBarriers[barrierIndex++]; + var resource = _resources.GetResource(compiledBarrier.Resource); + var resourceHandle = resource.backingResource; + + // Always query the before state from ResourceDatabase (single source of truth) + var currentState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(resourceHandle).GetValueOrThrow(); + + BarrierLayout layoutBefore; + BarrierAccess accessBefore; + BarrierSync syncBefore; + + // Handle aliasing barriers specially + if (compiledBarrier.AliasingPredecessor.IsValid) + { + var predHandle = _resources.GetResource(compiledBarrier.AliasingPredecessor).backingResource; + var predState = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(predHandle).GetValueOrThrow(); + + layoutBefore = BarrierLayout.Undefined; + accessBefore = BarrierAccess.NoAccess; + syncBefore = predState.Sync; + } + else + { + layoutBefore = currentState.Layout; + accessBefore = currentState.Access; + syncBefore = currentState.Sync; + } + + var target = compiledBarrier.TargetState; + + // Skip if already in target state (optimization) + if (!compiledBarrier.AliasingPredecessor.IsValid && + layoutBefore == target.Layout && + accessBefore == target.Access && + syncBefore == target.Sync) + { + continue; + } + + // Create barrier descriptor + BarrierDesc desc; + if (compiledBarrier.ResourceType == RenderGraphResourceType.Texture) + { + desc = BarrierDesc.Texture(resourceHandle, + syncBefore, target.Sync, + accessBefore, target.Access, + layoutBefore, target.Layout, + discard: compiledBarrier.Flags.HasFlag(BarrierFlags.Discard)); + } + else + { + desc = BarrierDesc.Buffer(resourceHandle, + syncBefore, target.Sync, + accessBefore, target.Access); + } + + if (barrierCount >= MaxBatch) + { + Flush(); + } + + barriers[barrierCount++] = desc; + } + + Flush(); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphHasher.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphHasher.cs new file mode 100644 index 0000000..e515794 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphHasher.cs @@ -0,0 +1,177 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using System.IO.Hashing; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Computes structural hashes of render graphs for compilation caching. +/// Hashes are based on graph topology and resource configurations, not runtime values. +/// +internal static class RenderGraphHasher +{ + /// + /// Computes a hash of the entire render graph structure. + /// Used for cache invalidation - same hash means same compilation result. + /// + public static unsafe ulong ComputeGraphHash(List passes, RenderGraphResourceRegistry resources) + { + using var scope = AllocationManager.CreateStackScope(); + var bufferPool = new UnsafeList(2048, scope.AllocationHandle); + var pData = (byte*)bufferPool.GetUnsafePtr(); + var offset = 0; + + // Hash pass count + *(int*)(pData + offset) = passes.Count; + offset += sizeof(int); + + // Hash each pass structure (excluding names) + for (var i = 0; i < passes.Count; i++) + { + var pass = passes[i]; + + *(RenderPassType*)(pData + offset) = pass.type; + offset += sizeof(RenderPassType); + + *(bool*)(pData + offset) = pass.allowCulling; + offset += sizeof(bool); + + *(bool*)(pData + offset) = pass.asyncCompute; + offset += sizeof(bool); + + // Hash depth attachment + offset = ComputeTextureHash(pData, offset, pass.depthAccess.id, resources); + + pData[offset] = (byte)pass.depthAccess.accessFlags; + offset += sizeof(AccessFlags); + + *(int*)(pData + offset) = pass.maxColorIndex; + offset += sizeof(int); + for (var j = 0; j <= pass.maxColorIndex; j++) + { + offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id, resources); + + pData[offset] = (byte)pass.colorAccess[j].accessFlags; + offset += sizeof(AccessFlags); + } + + for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) + { + var readList = pass.resourceReads[j]; + var writeList = pass.resourceWrites[j]; + var createList = pass.resourceCreates[j]; + + *(int*)(pData + offset) = readList.Count; + offset += sizeof(int); + for (var k = 0; k < readList.Count; k++) + { + *(int*)(pData + offset) = readList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = writeList.Count; + offset += sizeof(int); + for (var k = 0; k < writeList.Count; k++) + { + *(int*)(pData + offset) = writeList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = createList.Count; + offset += sizeof(int); + for (var k = 0; k < createList.Count; k++) + { + *(int*)(pData + offset) = createList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = pass.randomAccess.Count; + offset += sizeof(int); + for (var k = 0; k < pass.randomAccess.Count; k++) + { + *(int*)(pData + offset) = pass.randomAccess[k].Value; + offset += sizeof(int); + } + } + + *(int*)(pData + offset) = pass.GetRenderFuncHashCode(); + offset += sizeof(int); + } + + var span = new Span(pData, offset); + return XxHash64.HashToUInt64(span); + } + + /// + /// Computes a hash of a texture resource's structural properties. + /// For imported textures, hashes the backing handle. + /// For transient textures, hashes the descriptor (respecting size mode). + /// + private static unsafe int ComputeTextureHash(byte* pData, int offset, Identifier texture, RenderGraphResourceRegistry resources) + { + if (texture.IsInvalid) + { + return offset; + } + + var resource = resources.GetResource(texture.AsResource()); + + // Hash imported flag + *(pData + offset) = resource.isImported ? (byte)1 : (byte)0; + offset += sizeof(byte); + + // For imported textures, hash the backing resource handle + if (resource.isImported) + { + *(int*)(pData + offset) = resource.backingResource.GetHashCode(); + offset += sizeof(int); + return offset; + } + + var desc = resource.rgTextureDesc; + + // Hash format (structural) + *(TextureFormat*)(pData + offset) = desc.format; + offset += sizeof(TextureFormat); + + // Hash size mode (structural) + *(RGTextureSizeMode*)(pData + offset) = desc.sizeMode; + offset += sizeof(RGTextureSizeMode); + + // Hash size specification based on mode + if (desc.sizeMode == RGTextureSizeMode.Absolute) + { + // Absolute mode: hash actual dimensions + *(uint*)(pData + offset) = desc.width; + offset += sizeof(uint); + *(uint*)(pData + offset) = desc.height; + offset += sizeof(uint); + } + else + { + // Relative mode: hash scale factors (NOT resolved dimensions) + *(float*)(pData + offset) = desc.scaleX; + offset += sizeof(float); + *(float*)(pData + offset) = desc.scaleY; + offset += sizeof(float); + } + + // Hash other structural properties + *(TextureDimension*)(pData + offset) = desc.dimension; + offset += sizeof(TextureDimension); + *(uint*)(pData + offset) = desc.mipLevels; + offset += sizeof(uint); + *(TextureUsage*)(pData + offset) = desc.usage; + offset += sizeof(TextureUsage); + + *(bool*)(pData + offset) = desc.clearAtFirstUse; + offset += sizeof(bool); + *(bool*)(pData + offset) = desc.discardAtLastUse; + offset += sizeof(bool); + + return offset; + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphNativePassBuilder.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphNativePassBuilder.cs new file mode 100644 index 0000000..9427a49 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphNativePassBuilder.cs @@ -0,0 +1,370 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Builds native render passes by merging compatible consecutive raster passes. +/// Optimizes for tile-based deferred rendering (TBDR) GPUs by minimizing load/store operations. +/// +internal sealed class RenderGraphNativePassBuilder +{ + private readonly RenderGraphObjectPool _objectPool; + private readonly RenderGraphResourceRegistry _resources; + + public RenderGraphNativePassBuilder(RenderGraphObjectPool objectPool, RenderGraphResourceRegistry resources) + { + _objectPool = objectPool; + _resources = resources; + } + + /// + /// Builds native render passes by merging compatible consecutive raster passes. + /// Uses conservative merging: only merge passes with identical attachments and no barriers between them. + /// + public void BuildNativeRenderPasses( + List compiledPasses, + List nativePasses, + List compiledBarriers) + { + // Clear previous native passes + for (var i = 0; i < nativePasses.Count; i++) + { + _objectPool.Return(nativePasses[i]); + } + nativePasses.Clear(); + + NativeRenderPass? currentNativePass = null; + + for (var i = 0; i < compiledPasses.Count; i++) + { + var pass = compiledPasses[i]; + + // Only raster passes can be merged into native render passes + // Compute passes break the current native render pass + if (pass.type != RenderPassType.Raster) + { + // Close current native pass if open + if (currentNativePass != null) + { + nativePasses.Add(currentNativePass); + currentNativePass = null; + } + continue; // Compute/Unsafe passes execute outside native render passes + } + + + // Check if we can merge with current native pass + if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i, compiledPasses, compiledBarriers)) + { + // Merge into existing native pass + currentNativePass.mergedPassIndices.Add(i); + currentNativePass.lastLogicalPass = i; + } + else + { + // Start new native pass + if (currentNativePass != null) + { + nativePasses.Add(currentNativePass); + } + + currentNativePass = CreateNativePass(pass, i); + } + } + + // Add final native pass + if (currentNativePass != null) + { + nativePasses.Add(currentNativePass); + } + + // Infer load/store operations for all native passes + for (var i = 0; i < nativePasses.Count; i++) + { + InferLoadStoreOps(nativePasses[i]); + } + } + + /// + /// Creates a new native render pass from a logical pass. + /// + private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex) + { + var nativePass = _objectPool.Rent(); + nativePass.Reset(); + + nativePass.index = 0; // Will be set by caller + nativePass.mergedPassIndices.Add(passIndex); + nativePass.firstLogicalPass = passIndex; + nativePass.lastLogicalPass = passIndex; + nativePass.allowUAVWrites = pass.randomAccess.Count > 0; + + // Copy color attachments + nativePass.colorAttachmentCount = pass.maxColorIndex + 1; + for (var i = 0; i <= pass.maxColorIndex; i++) + { + var access = pass.colorAccess[i]; + nativePass.colorAttachments[i] = new RenderTargetInfo + { + texture = access.id, + access = access.accessFlags + }; + } + + // Copy depth attachment + if (!pass.depthAccess.id.IsInvalid) + { + nativePass.hasDepthAttachment = true; + nativePass.depthAttachment = new DepthStencilInfo + { + texture = pass.depthAccess.id, + access = pass.depthAccess.accessFlags + }; + } + + return nativePass; + } + + /// + /// Checks if a logical pass can be merged into an existing native render pass. + /// Conservative merging: only merge if attachments match and no barriers needed. + /// + private bool CanMergePasses( + NativeRenderPass nativePass, + RenderGraphPassBase pass, + int passIndex, + List compiledPasses, + List compiledBarriers) + { + // Don't merge if UAVs are involved (conservative) + if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites) + { + return false; + } + + // Check if attachment configuration matches + if (!AttachmentsMatch(nativePass, pass)) + { + return false; + } + + // Check if barriers are needed between last merged pass and this pass + if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex, compiledPasses, compiledBarriers)) + { + return false; + } + + return true; + } + + /// + /// Checks if the attachment configuration of a pass matches the native pass. + /// + private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass) + { + // Check color attachment count + if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1) + { + return false; + } + + // Check each color attachment + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id) + { + return false; + } + } + + // Check depth attachment + if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid) + { + return false; + } + + if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id) + { + return false; + } + + return true; + } + + /// + /// Checks if any barriers are required between two passes that would prevent merging. + /// Only barriers affecting render targets prevent merging; SRV barriers are fine. + /// + private bool RequiresBarrierBetweenPasses( + int passA, + int passB, + List compiledPasses, + List compiledBarriers) + { + var laterPass = compiledPasses[passB]; + + // Build a set of render target resource IDs (color + depth) + var renderTargets = new HashSet>(); + for (var i = 0; i <= laterPass.maxColorIndex; i++) + { + if (!laterPass.colorAccess[i].id.IsInvalid) + { + renderTargets.Add(laterPass.colorAccess[i].id.AsResource()); + } + } + if (!laterPass.depthAccess.id.IsInvalid) + { + renderTargets.Add(laterPass.depthAccess.id.AsResource()); + } + + // Check if any compiled barriers for passB affect render targets + for (var i = 0; i < compiledBarriers.Count; i++) + { + if (compiledBarriers[i].PassIndex == passB) + { + // Only prevent merge if barrier affects a render target + if (renderTargets.Contains(compiledBarriers[i].Resource)) + { + return true; // Barrier affects render target, cannot merge + } + } + + if (compiledBarriers[i].PassIndex > passB) + { + break; // No more barriers for this pass + } + } + + return false; + } + + /// + /// Infers optimal load/store operations for all attachments in a native render pass. + /// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs). + /// + private void InferLoadStoreOps(NativeRenderPass nativePass) + { + // Infer load/store ops for color attachments + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + ref var attachment = ref nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearColor = resource.rgTextureDesc.clearColor; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Last use: No one needs it after this native pass + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + // Intermediate: Store for future passes + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + + // Infer load/store ops for depth attachment + if (nativePass.hasDepthAttachment) + { + ref var attachment = ref nativePass.depthAttachment; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First Use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearDepth = resource.rgTextureDesc.clearDepth; + attachment.clearStencil = resource.rgTextureDesc.clearStencil; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Depth is commonly discarded (depth-only passes, intermediate depth) + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + } +}