forked from Misaki/GhostEngine
Render graph: native pass merging & heap-based aliasing
Major architecture upgrade: - Add native render pass merging (hardware pass grouping, load/store op inference) - Implement heap-based aliasing for textures & buffers (D3D12-style) - Unify resource model: buffers and textures in one registry - Extend builder API for buffer creation/usage, access flags, hints - Improve barrier/state tracking (buffer hints, indirect argument state) - Update caching, hashing, and debug output for new model - Add enums/structs: AttachmentLoadOp, StoreOp, BufferHint, etc. - D3D12 backend: support named resources, temp upload buffers, correct heap usage - Update docs, benchmarks, and project files for new features Brings render graph closer to AAA engine standards, enabling efficient memory usage, lower driver overhead, and a more flexible API.
This commit is contained in:
@@ -2,6 +2,7 @@ using Ghost.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.IO.Hashing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
@@ -21,6 +22,7 @@ public sealed class RenderGraph
|
||||
private readonly RenderGraphObjectPool _objectPool = new();
|
||||
private readonly List<RenderGraphPassBase> _passes = new(64);
|
||||
private readonly List<RenderGraphPassBase> _compiledPasses = new(64);
|
||||
private readonly List<NativeRenderPass> _nativePasses = new(32);
|
||||
private readonly RenderGraphBuilder _builder = new();
|
||||
private readonly MockCommandBuffer _commandBuffer = new();
|
||||
private readonly RenderContext _renderContext;
|
||||
@@ -68,8 +70,16 @@ public sealed class RenderGraph
|
||||
|
||||
// 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.
|
||||
@@ -78,6 +88,14 @@ public sealed class RenderGraph
|
||||
{
|
||||
return _resources.ImportTexture(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports an external buffer into the render graph.
|
||||
/// </summary>
|
||||
public Identifier<RGBuffer> ImportBuffer(BufferDescriptor descriptor)
|
||||
{
|
||||
return _resources.ImportBuffer(descriptor);
|
||||
}
|
||||
|
||||
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
|
||||
where TPassData : class, new()
|
||||
@@ -119,13 +137,13 @@ public sealed class RenderGraph
|
||||
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
|
||||
offset += sizeof(byte);
|
||||
|
||||
*(TextureFormat*)(pData + offset) = resource.descriptor.format;
|
||||
*(TextureFormat*)(pData + offset) = resource.textureDescriptor.format;
|
||||
offset += sizeof(TextureFormat);
|
||||
|
||||
*(int*)(pData + offset) = resource.descriptor.width;
|
||||
*(int*)(pData + offset) = resource.textureDescriptor.width;
|
||||
offset += sizeof(int);
|
||||
|
||||
*(int*)(pData + offset) = resource.descriptor.height;
|
||||
*(int*)(pData + offset) = resource.textureDescriptor.height;
|
||||
offset += sizeof(int);
|
||||
|
||||
return offset;
|
||||
@@ -159,11 +177,17 @@ public sealed class RenderGraph
|
||||
// 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++)
|
||||
@@ -195,27 +219,31 @@ public sealed class RenderGraph
|
||||
*(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);
|
||||
}
|
||||
|
||||
//// Hash resource descriptors
|
||||
//for (var j = 0; j < _resources.TextureResourceCount; j++)
|
||||
//{
|
||||
// var resource = _resources.GetTextureResourceByIndex(j);
|
||||
|
||||
// *(int*)(pData + offset) = resource.descriptor.width;
|
||||
// offset += sizeof(int);
|
||||
|
||||
// *(int*)(pData + offset) = resource.descriptor.height;
|
||||
// offset += sizeof(int);
|
||||
|
||||
// *(TextureFormat*)(pData + offset) = resource.descriptor.format;
|
||||
// offset += sizeof(TextureFormat);
|
||||
|
||||
// *(bool*)(pData + offset) = resource.isImported;
|
||||
// offset += sizeof(bool);
|
||||
//}
|
||||
|
||||
var span = new Span<byte>(pData, offset);
|
||||
return XxHash64.HashToUInt64(span);
|
||||
}
|
||||
@@ -316,8 +344,11 @@ public sealed class RenderGraph
|
||||
|
||||
// Step 6: Generate barriers for state transitions and aliasing
|
||||
GenerateBarriers();
|
||||
|
||||
// Step 7: Build native render passes by merging compatible passes
|
||||
BuildNativeRenderPasses();
|
||||
|
||||
// Step 7: Store in cache for future frames
|
||||
// Step 8: Store in cache for future frames
|
||||
StoreInCache(graphHash);
|
||||
|
||||
_compiled = true;
|
||||
@@ -343,7 +374,7 @@ public sealed class RenderGraph
|
||||
}
|
||||
|
||||
// Restore aliasing mappings (need to update ResourceAliasingManager)
|
||||
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.physicalResources);
|
||||
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources);
|
||||
|
||||
// Restore barriers (deep copy to avoid shared references)
|
||||
_barriers.Clear();
|
||||
@@ -380,7 +411,7 @@ public sealed class RenderGraph
|
||||
}
|
||||
|
||||
// Store aliasing mappings
|
||||
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.physicalResources);
|
||||
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources);
|
||||
|
||||
// Store barriers
|
||||
for (var i = 0; i < _barriers.Count; i++)
|
||||
@@ -476,65 +507,73 @@ public sealed class RenderGraph
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts aliasing barriers when a physical resource is reused.
|
||||
/// Inserts aliasing barriers when a placed resource is reused.
|
||||
/// </summary>
|
||||
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
|
||||
{
|
||||
// Check all resources written by this pass
|
||||
for (var i = 0; i < pass.resourceWrites.Count; i++)
|
||||
// Check all resources written by this pass (both textures and buffers)
|
||||
for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
|
||||
{
|
||||
var id = pass.resourceWrites[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)
|
||||
var writeList = pass.resourceWrites[resType];
|
||||
for (var i = 0; i < writeList.Count; i++)
|
||||
{
|
||||
// Rent the physical resource
|
||||
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
|
||||
if (physicalIndex >= 0)
|
||||
var id = writeList[i];
|
||||
var resource = _resources.GetResource(id);
|
||||
|
||||
// Skip imported resources
|
||||
if (resource.isImported)
|
||||
{
|
||||
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this physical resource has multiple aliased resources,
|
||||
// we need an aliasing barrier when switching between them
|
||||
if (physical != null && physical.aliasedLogicalResources.Count > 1)
|
||||
// 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)
|
||||
{
|
||||
// Find the resource that used this physical memory most recently before this pass
|
||||
Identifier<RGResource> resourceBefore = default;
|
||||
var mostRecentLastUse = -1;
|
||||
var placed = _aliasingManager.GetPlacedResource(placedIndex);
|
||||
|
||||
foreach (var otherLogicalIndex in physical.aliasedLogicalResources)
|
||||
// If this placed resource has multiple aliased resources,
|
||||
// we need an aliasing barrier when switching between them
|
||||
if (placed != null && placed.aliasedLogicalResources.Count > 1)
|
||||
{
|
||||
if (otherLogicalIndex != id.Value)
|
||||
// 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)
|
||||
{
|
||||
var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
|
||||
// Check if this resource finished before our resource starts
|
||||
if (otherResource.lastUsePass < pass.index &&
|
||||
otherResource.lastUsePass > mostRecentLastUse)
|
||||
if (otherLogicalIndex != id.Value)
|
||||
{
|
||||
mostRecentLastUse = otherResource.lastUsePass;
|
||||
resourceBefore = otherLogicalIndex;
|
||||
// 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);
|
||||
// If we found a previous resource, insert aliasing barrier
|
||||
if (mostRecentLastUse >= 0)
|
||||
{
|
||||
var barrier = ResourceBarrier.CreateAliasingBarrier(
|
||||
resourceBefore,
|
||||
id,
|
||||
passIdx
|
||||
);
|
||||
_barriers.Add(barrier);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($" {barrier}");
|
||||
Console.WriteLine($" {barrier}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,14 +586,15 @@ public sealed class RenderGraph
|
||||
/// </summary>
|
||||
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
|
||||
{
|
||||
// Process reads (transition to shader resource)
|
||||
// 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];
|
||||
InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx);
|
||||
var state = GetBufferReadState(handle, pass, (RenderGraphResourceType)i);
|
||||
InsertTransitionIfNeeded(handle, state, passIdx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +663,386 @@ public sealed class RenderGraph
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all compiled passes.
|
||||
/// Determines the appropriate resource state for a buffer read operation based on usage hints.
|
||||
/// </summary>
|
||||
private ResourceState GetBufferReadState(Identifier<RGResource> handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType)
|
||||
{
|
||||
// Textures always use ShaderResource state
|
||||
if (resourceType == RenderGraphResourceType.Texture)
|
||||
{
|
||||
return ResourceState.ShaderResource;
|
||||
}
|
||||
|
||||
// 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.ShaderResource;
|
||||
}
|
||||
|
||||
/// <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 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]);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine("\n=== Native Render Passes ===");
|
||||
Console.WriteLine($"Logical passes: {_compiledPasses.Count}");
|
||||
Console.WriteLine($"Native passes: {_nativePasses.Count}");
|
||||
for (var i = 0; i < _nativePasses.Count; i++)
|
||||
{
|
||||
var nativePass = _nativePasses[i];
|
||||
Console.WriteLine($"\nNative Pass {i}:");
|
||||
Console.WriteLine($" Merged passes: [{string.Join(", ", nativePass.mergedPassIndices)}]");
|
||||
Console.WriteLine($" Color attachments: {nativePass.colorAttachmentCount}");
|
||||
for (var j = 0; j < nativePass.colorAttachmentCount; j++)
|
||||
{
|
||||
Console.WriteLine($" [{j}] {nativePass.colorAttachments[j].texture}");
|
||||
}
|
||||
if (nativePass.hasDepthAttachment)
|
||||
{
|
||||
Console.WriteLine($" Depth attachment: {nativePass.depthAttachment.texture}");
|
||||
}
|
||||
}
|
||||
Console.WriteLine("============================\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <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++)
|
||||
{
|
||||
nativePass.colorAttachments[i] = new RenderTargetInfo
|
||||
{
|
||||
texture = pass.colorAccess[i].id,
|
||||
access = pass.colorAccess[i].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. WriteAll (Write | Discard): User guarantees full overwrite
|
||||
if (flags.HasFlag(AccessFlags.Discard))
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.DontCare;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] LoadOp=DontCare (WriteAll/Discard flag)");
|
||||
#endif
|
||||
}
|
||||
// 2. Read: Needs existing contents (e.g., blending)
|
||||
else if (flags.HasFlag(AccessFlags.Read))
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] LoadOp=Load (Read flag - blending)");
|
||||
#endif
|
||||
}
|
||||
// 3. First use: Could use DontCare, but user didn't specify Discard flag
|
||||
// Conservative: use Load to avoid bugs
|
||||
else if (resource.firstUsePass == nativePass.firstLogicalPass)
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] LoadOp=Load (first use, Write flag - conservative)");
|
||||
#endif
|
||||
}
|
||||
// 4. Continuation from previous pass
|
||||
else
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] LoadOp=Load (continuation from previous pass)");
|
||||
#endif
|
||||
}
|
||||
|
||||
// ===== STORE OP INFERENCE =====
|
||||
|
||||
// Last use: No one needs it after this native pass
|
||||
if (resource.lastUsePass == nativePass.lastLogicalPass)
|
||||
{
|
||||
attachment.storeOp = AttachmentStoreOp.DontCare;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] StoreOp=DontCare (last use - discard)");
|
||||
#endif
|
||||
}
|
||||
// Intermediate: Store for future passes
|
||||
else
|
||||
{
|
||||
attachment.storeOp = AttachmentStoreOp.Store;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Color[{i}] StoreOp=Store (used by later passes)");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// 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 =====
|
||||
|
||||
if (flags.HasFlag(AccessFlags.Discard))
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.DontCare;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth LoadOp=DontCare (WriteAll/Discard flag)");
|
||||
#endif
|
||||
}
|
||||
else if (flags.HasFlag(AccessFlags.Read))
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth LoadOp=Load (Read flag)");
|
||||
#endif
|
||||
}
|
||||
else if (resource.firstUsePass == nativePass.firstLogicalPass)
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth LoadOp=Load (first use, Write flag - conservative)");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
attachment.loadOp = AttachmentLoadOp.Load;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth LoadOp=Load (continuation)");
|
||||
#endif
|
||||
}
|
||||
|
||||
// ===== STORE OP INFERENCE =====
|
||||
|
||||
// Depth is commonly discarded (depth-only passes, intermediate depth)
|
||||
if (resource.lastUsePass == nativePass.lastLogicalPass)
|
||||
{
|
||||
attachment.storeOp = AttachmentStoreOp.DontCare;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth StoreOp=DontCare (last use)");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
attachment.storeOp = AttachmentStoreOp.Store;
|
||||
#if DEBUG
|
||||
Console.WriteLine($" Depth StoreOp=Store (used later)");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all compiled passes using native render passes where possible.
|
||||
/// </summary>
|
||||
public void Execute()
|
||||
{
|
||||
@@ -632,55 +1051,111 @@ public sealed class RenderGraph
|
||||
Compile();
|
||||
}
|
||||
|
||||
// Execute each non-culled pass
|
||||
var barrierIndex = 0;
|
||||
for (var i = 0; i < _compiledPasses.Count; i++)
|
||||
var nativePassIndex = 0;
|
||||
var logicalPassIndex = 0;
|
||||
|
||||
while (logicalPassIndex < _compiledPasses.Count)
|
||||
{
|
||||
var pass = _compiledPasses[i];
|
||||
|
||||
// Execute all barriers for this pass
|
||||
#if DEBUG
|
||||
bool hasBarriers = false;
|
||||
#endif
|
||||
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == i)
|
||||
var pass = _compiledPasses[logicalPassIndex];
|
||||
|
||||
// Check if this pass is part of a native render pass
|
||||
if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!hasBarriers)
|
||||
var nativePass = _nativePasses[nativePassIndex];
|
||||
|
||||
// Execute barriers for ALL merged passes before beginning the native render pass
|
||||
foreach (var mergedPassIdx in nativePass.mergedPassIndices)
|
||||
{
|
||||
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
|
||||
hasBarriers = true;
|
||||
ExecuteBarriersForPass(mergedPassIdx, ref barrierIndex);
|
||||
}
|
||||
|
||||
// Begin native render pass
|
||||
_commandBuffer.BeginRenderPass(
|
||||
nativePass.index,
|
||||
nativePass.colorAttachmentCount,
|
||||
nativePass.hasDepthAttachment
|
||||
);
|
||||
|
||||
var barrier = _barriers[barrierIndex];
|
||||
if (barrier.Type == BarrierType.Transition)
|
||||
// Execute all merged logical passes within this native render pass
|
||||
for (var i = 0; i < nativePass.mergedPassIndices.Count; i++)
|
||||
{
|
||||
_commandBuffer.ResourceBarrier(
|
||||
barrier.Resource,
|
||||
barrier.StateBefore,
|
||||
barrier.StateAfter
|
||||
);
|
||||
}
|
||||
else if (barrier.Type == BarrierType.Aliasing)
|
||||
{
|
||||
_commandBuffer.AliasBarrier(
|
||||
barrier.ResourceBefore,
|
||||
barrier.ResourceAfter
|
||||
);
|
||||
}
|
||||
var mergedPassIdx = nativePass.mergedPassIndices[i];
|
||||
var mergedPass = _compiledPasses[mergedPassIdx];
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\n--- Executing Pass {mergedPassIdx}: {mergedPass.name} (in Native Pass {nativePass.index}) ---");
|
||||
#endif
|
||||
// In a real implementation, you would execute the barrier here:
|
||||
// ExecuteBarrier(_barriers[barrierIndex]);
|
||||
|
||||
barrierIndex++;
|
||||
|
||||
mergedPass.Execute(_renderContext);
|
||||
logicalPassIndex++;
|
||||
}
|
||||
|
||||
// End native render pass
|
||||
_commandBuffer.EndRenderPass();
|
||||
|
||||
nativePassIndex++;
|
||||
}
|
||||
#if DEBUG
|
||||
if (hasBarriers)
|
||||
else
|
||||
{
|
||||
Console.WriteLine("=====================================\n");
|
||||
}
|
||||
// Compute pass or standalone raster pass (not merged)
|
||||
ExecuteBarriersForPass(logicalPassIndex, ref barrierIndex);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\n--- Executing Pass {logicalPassIndex}: {pass.name} (Standalone) ---");
|
||||
#endif
|
||||
|
||||
pass.Execute(_renderContext);
|
||||
|
||||
pass.Execute(_renderContext);
|
||||
logicalPassIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all barriers for a specific pass.
|
||||
/// </summary>
|
||||
private void ExecuteBarriersForPass(int passIndex, ref int barrierIndex)
|
||||
{
|
||||
#if DEBUG
|
||||
bool hasBarriers = false;
|
||||
#endif
|
||||
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!hasBarriers)
|
||||
{
|
||||
var pass = _compiledPasses[passIndex];
|
||||
Console.WriteLine($"\n=== Barriers before Pass {passIndex}: {pass.name} ===");
|
||||
hasBarriers = true;
|
||||
}
|
||||
|
||||
var barrier = _barriers[barrierIndex];
|
||||
if (barrier.Type == BarrierType.Transition)
|
||||
{
|
||||
_commandBuffer.ResourceBarrier(
|
||||
barrier.Resource,
|
||||
barrier.StateBefore,
|
||||
barrier.StateAfter
|
||||
);
|
||||
}
|
||||
else if (barrier.Type == BarrierType.Aliasing)
|
||||
{
|
||||
_commandBuffer.AliasBarrier(
|
||||
barrier.ResourceBefore,
|
||||
barrier.ResourceAfter
|
||||
);
|
||||
}
|
||||
#endif
|
||||
// In a real implementation, you would execute the barrier here:
|
||||
// ExecuteBarrier(_barriers[barrierIndex]);
|
||||
|
||||
barrierIndex++;
|
||||
}
|
||||
#if DEBUG
|
||||
if (hasBarriers)
|
||||
{
|
||||
Console.WriteLine("=====================================\n");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user