Files
GhostEngine/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraph.cs.backup
2026-02-18 00:52:18 +09:00

1171 lines
42 KiB
Plaintext

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;
/// <summary>
/// Main render graph class that manages resource allocation and pass execution.
/// </summary>
public sealed class RenderGraph : IDisposable
{
private readonly IGraphicsEngine _graphicsEngine;
private readonly RenderGraphObjectPool _objectPool;
private readonly RenderGraphResourceRegistry _resources;
private readonly List<RenderGraphPassBase> _passes;
private readonly List<RenderGraphPassBase> _compiledPasses;
private readonly List<NativeRenderPass> _nativePasses;
private readonly RenderGraphBuilder _builder;
private readonly ResourceAliasingManager _aliasingManager;
private readonly List<CompiledBarrier> _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<RenderGraphPassBase>(32);
_compiledPasses = new List<RenderGraphPassBase>(32);
_nativePasses = new List<NativeRenderPass>(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();
}
/// <summary>
/// Resets the render graph for a new frame.
/// Reuses existing allocations to minimize GC.
/// </summary>
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;
}
/// <summary>
/// Imports an external texture into the render graph.
/// </summary>
/// <param name="texture">The external texture handle.</param>
/// <returns>The identifier of the imported render graph texture. Invalid if import fails.</returns>
public Identifier<RGTexture> ImportTexture(Handle<Texture> 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<RGTexture>.Invalid;
}
var desc = r.Value;
return _resources.ImportTexture(in desc._desc.textureDescription, texture, name, clearColor, clearDepth, clearStencil, clearAtFirstUse, discardAtLastUse);
}
/// <summary>
/// Imports an external buffer into the render graph.
/// </summary>
/// <param name="buffer">The external buffer handle.</param>
/// <returns>The identifier of the imported render graph buffer. Invalid if import fails.</returns>
public Identifier<RGBuffer> ImportBuffer(Handle<GraphicsBuffer> buffer, string name)
{
var r = _graphicsEngine.ResourceDatabase.GetResourceDescription(buffer.AsResource());
if (r.IsFailure)
{
return Identifier<RGBuffer>.Invalid;
}
var desc = r.Value;
return _resources.ImportBuffer(in desc._desc.bufferDescription, buffer, name);
}
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var renderPass = _objectPool.Rent<RasterRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Raster);
passData = renderPass.passData;
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
public IComputeRenderGraphBuilder AddComputeRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var renderPass = _objectPool.Rent<ComputeRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Compute);
passData = renderPass.passData;
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
public IUnsafeRenderGraphBuilder AddUnsafeRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var renderPass = _objectPool.Rent<UnsafeRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Unsafe);
passData = renderPass.passData;
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
/// <summary>
/// Compiles the render graph by culling unused passes and determining resource lifetimes.
/// </summary>
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;
}
/// <summary>
/// Executes all compiled passes using native render passes where possible.
/// </summary>
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);
}
}
/// <summary>
/// Restores the render graph state from cached compilation results.
/// </summary>
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();
}
/// <summary>
/// Stores current compilation results in the cache.
/// </summary>
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<RGResource> 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]);
}
}
/// <summary>
/// Compiles all barriers needed for execution, storing only target states.
/// Barriers include aliasing barriers and implicit state transitions.
/// </summary>
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);
}
}
/// <summary>
/// Inserts aliasing barriers when a placed resource is reused.
/// </summary>
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<RGResource> 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<RGResource>(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);
}
}
}
}
}
}
}
/// <summary>
/// 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.
/// </summary>
private void CompileImplicitTransitions(RenderGraphPassBase pass, int passIdx)
{
// Helper to add a compiled barrier for a resource transition
void AddTransition(Identifier<RGResource> id, ResourceBarrierData targetState)
{
var resource = _resources.GetResource(id);
var barrier = new CompiledBarrier
{
PassIndex = passIdx,
Resource = id,
TargetState = targetState,
AliasingPredecessor = Identifier<RGResource>.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<RGResource> 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);
}
/// <summary>
/// Builds native render passes by merging compatible consecutive raster passes.
/// Uses conservative merging: only merge passes with identical attachments and no barriers between them.
/// </summary>
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]);
}
}
/// <summary>
/// Creates a new native render pass from a logical pass.
/// </summary>
private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex)
{
var nativePass = _objectPool.Rent<NativeRenderPass>();
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Checks if the attachment configuration of a pass matches the native pass.
/// </summary>
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;
}
/// <summary>
/// Checks if any barriers are required between two passes that would prevent merging.
/// Only barriers affecting render targets prevent merging; SRV barriers are fine.
/// </summary>
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<Identifier<RGResource>>();
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;
}
/// <summary>
/// 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).
/// </summary>
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;
}
}
}
/// <summary>
/// Executes all compiled passes using native render passes where possible.
/// </summary>
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<Texture>.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<PassRenderTargetDesc>(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<TextureFormat>(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++;
}
}
}
/// <summary>
/// Executes all barriers for a specific pass.
/// Uses pre-compiled barriers and queries before state from ResourceDatabase.
/// </summary>
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<BarrierDesc>(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();
}
}