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(); } }