Files
GhostEngine/Ghost.Graphics/RenderGraphModule/RenderGraph.cs
Misaki 92b966fe0d Render graph integration and resource management refactor
Introduces a full-featured render graph system with pass culling, resource aliasing, and automatic barrier generation. Refactors resource and barrier APIs, improves error handling, and unifies result types. Renderer and render passes now use the new graph-based workflow. Updates shader includes, adds a blit shader, and improves HLSL parsing. Removes dynamic descriptor heaps in favor of persistent ones. Project file now includes the render graph module. Lays the foundation for advanced rendering features and improved memory efficiency.
2026-01-21 18:32:03 +09:00

1387 lines
48 KiB
C#

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;
using TerraFX.Interop.Windows;
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 Dictionary<int, ResourceState> _resourceStates;
private readonly List<ResourceBarrier> _barriers;
private readonly RenderGraphCompilationCache _compilationCache;
private readonly RenderGraphContext _context;
private bool _compiled;
private Handle<GPUResource> _resourceHeap;
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(_objectPool);
_resourceStates = new Dictionary<int, ResourceState>(64);
_barriers = new List<ResourceBarrier>(128);
_compilationCache = new RenderGraphCompilationCache();
_resourceHeap = Handle<GPUResource>.Invalid;
_context = new RenderGraphContext(
_graphicsEngine.ResourceDatabase,
_graphicsEngine.PipelineLibrary,
_graphicsEngine.ShaderCompiler,
_resources
);
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.BeginFrame();
// Reset aliasing manager
_aliasingManager.BeginFrame();
// Clear resource states and barriers
_resourceStates.Clear();
_barriers.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)
{
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);
}
/// <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;
}
private unsafe int ComputeTextureHash(byte* pData, int offset, Identifier<RGTexture> 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<byte>(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);
}
// Hash buffer hints (important for correct barrier generation)
*(int*)(pData + offset) = pass.bufferHints.Count;
offset += sizeof(int);
foreach (var kvp in pass.bufferHints)
{
*(int*)(pData + offset) = kvp.Key; // Buffer resource ID
offset += sizeof(int);
*(int*)(pData + offset) = (int)kvp.Value; // BufferHint flags
offset += sizeof(int);
}
}
*(int*)(pData + offset) = pass.GetRenderFuncHashCode();
offset += sizeof(int);
}
var span = new Span<byte>(pData, offset);
return XxHash64.HashToUInt64(span);
}
/// <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 = 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();
GenerateBarriers();
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);
}
}
/// <summary>
/// Recreates GPU resources when view state changes but compilation cache is valid.
/// </summary>
private void RecreateResourcesForNewViewState(CachedCompilation cached, in ViewState newViewState)
{
// Restore compilation results (passes, barriers, aliasing)
RestoreFromCache(cached);
AllocateResource();
cached.viewState = newViewState;
}
/// <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 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];
}
}
}
/// <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 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<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>
/// Generates resource barriers for state transitions and aliasing.
/// </summary>
private void GenerateBarriers()
{
_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);
// Insert transition barriers for state changes
InsertTransitionBarriers(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)
{
var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore,
id,
passIdx
);
_barriers.Add(barrier);
}
}
}
}
}
}
}
/// <summary>
/// Inserts transition barriers when a resource changes state.
/// </summary>
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
{
// Process reads (transition to appropriate state based on resource type and hints)
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 state = GetBufferReadState(handle, pass, (RenderGraphResourceType)i);
InsertTransitionIfNeeded(handle, state, passIdx);
}
}
switch (pass.type)
{
case RenderPassType.Raster:
for (var i = 0; i < pass.maxColorIndex; i++)
{
var access = pass.colorAccess[i];
InsertTransitionIfNeeded(access.id.AsResource(), ResourceState.RenderTarget, passIdx);
}
if (pass.depthAccess.id.IsValid)
{
var depthAccess = pass.depthAccess;
InsertTransitionIfNeeded(depthAccess.id.AsResource(), ResourceState.DepthWrite, passIdx);
}
for (var i = 0; i < pass.randomAccess.Count; i++)
{
InsertTransitionIfNeeded(pass.randomAccess[i], ResourceState.UnorderedAccess, passIdx);
}
break;
case RenderPassType.Compute:
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var writeList = pass.resourceWrites[i];
for (var j = 0; j < writeList.Count; j++)
{
var id = writeList[j];
InsertTransitionIfNeeded(id, ResourceState.UnorderedAccess, passIdx);
}
}
break;
case RenderPassType.Unsafe:
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var writeList = pass.resourceWrites[i];
for (var j = 0; j < writeList.Count; j++)
{
var id = writeList[j];
InsertTransitionIfNeeded(id, ResourceState.RenderTarget, passIdx);
}
}
for (var i = 0; i < pass.randomAccess.Count; i++)
{
InsertTransitionIfNeeded(pass.randomAccess[i], ResourceState.UnorderedAccess, passIdx);
}
break;
}
}
/// <summary>
/// Inserts a transition barrier if the resource state changes.
/// </summary>
private void InsertTransitionIfNeeded(Identifier<RGResource> resource, ResourceState newState, int passIdx)
{
if (!_resourceStates.TryGetValue(resource.Value, out var currentState))
{
// First time seeing this resource, assume undefined
// currentState = ResourceState.Common;
var r = _graphicsEngine.ResourceDatabase.GetResourceState(_resources.GetResource(resource).backingResource);
currentState = r.IsSuccess ? r.Value : ResourceState.Common;
}
if (currentState != newState)
{
var barrier = ResourceBarrier.CreateTransitionBarrier(
resource,
currentState,
newState,
passIdx
);
_barriers.Add(barrier);
_resourceStates[resource.Value] = newState;
}
}
/// <summary>
/// Determines the appropriate resource state for a buffer read operation based on usage hints.
/// </summary>
private static ResourceState GetBufferReadState(Identifier<RGResource> handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType)
{
// Textures always use ShaderResource state
if (resourceType == RenderGraphResourceType.Texture)
{
return ResourceState.PixelShaderResource | ResourceState.NonPixelShaderResource;
}
// Check for buffer-specific usage hints
if (pass.bufferHints.TryGetValue(handle.Value, out var hint))
{
if (hint.HasFlag(BufferHint.IndirectArgument))
{
return ResourceState.IndirectArgument;
}
}
// Default: ByteAddressBuffer read (SRV) - matches bindless architecture
return ResourceState.PixelShaderResource | ResourceState.NonPixelShaderResource;
}
/// <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 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;
}
/// <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.
/// </summary>
private unsafe void ExecuteBarriersForPass(ICommandBuffer cmd, int passIndex, ref int barrierIndex)
{
var pBarrierDescs = stackalloc BarrierDesc[16]; // batch by 16
var count = 0;
var hasRemain = false;
Start:
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex)
{
var barrier = _barriers[barrierIndex];
var desc = new BarrierDesc();
desc.type = barrier.Type;
switch (desc.type)
{
case BarrierType.Transition:
desc.transition.resource = _resources.GetResource(barrier.Resource).backingResource;
desc.transition.stateBefore = barrier.StateBefore;
desc.transition.stateAfter = barrier.StateAfter;
break;
case BarrierType.Aliasing:
desc.aliasing.resourceBefore = _resources.GetResource(barrier.ResourceBefore).backingResource;
desc.aliasing.resourceAfter = _resources.GetResource(barrier.ResourceAfter).backingResource;
break;
case BarrierType.UAV:
desc.uav.resource = _resources.GetResource(barrier.Resource).backingResource;
break;
}
pBarrierDescs[count] = desc;
count++;
barrierIndex++;
if (count == 16)
{
hasRemain = _barriers.Count > barrierIndex && _barriers[barrierIndex].PassIndex == passIndex;
break;
}
}
if (count > 0)
{
cmd.ResourceBarrier(new ReadOnlySpan<BarrierDesc>(pBarrierDescs, count));
}
if (hasRemain)
{
count = 0;
hasRemain = false;
goto Start;
}
}
public void Dispose()
{
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap);
}
}