Refactor render graph: modular compilation & execution

Major overhaul of render graph system for modularity and performance:
- Split compilation and execution logic into dedicated classes (Compiler, Executor, NativePassBuilder, Barriers)
- Overhauled barrier system: now uses CompiledBarrier with target state only, querying before state at execution
- Resource size/alignment now queried from D3D12 device for accurate heap allocation
- ResourceDesc now includes Type field and asserts correct union access
- Centralized D3D12 interop logic in D3D12Utility extensions
- Added RenderGraphHasher for structural graph hashing and cache invalidation
- RenderGraph class simplified to orchestrate specialized components
- ResourceAliasingManager now uses allocator for size queries
- Compilation cache now stores compiled barriers, reducing memory usage
- Improved comments, debug assertions, and removed redundant code

Result: more maintainable, efficient, and robust render graph pipeline.
This commit is contained in:
2026-01-23 18:12:52 +09:00
parent 4173ff2432
commit e11a9ebb52
14 changed files with 2797 additions and 1317 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -285,6 +285,7 @@ internal sealed class PlacedResource
/// </summary>
internal sealed class ResourceAliasingManager
{
private readonly IResourceAllocator _allocator;
private readonly RenderGraphObjectPool _pool;
private readonly ResourceHeap _heap;
@@ -306,17 +307,20 @@ internal sealed class ResourceAliasingManager
if (resource.type == RenderGraphResourceType.Texture)
{
var textureDesc = resource.rgTextureDesc.ToTextureDesc(resource.resolvedWidth, resource.resolvedHeight);
return AlignUp(textureDesc.GetTotalBytes(), _DEFAULT_TEXTURE_ALIGNMENT);
return _allocator.GetSizeInfo(ResourceDesc.Texture(textureDesc)).Size;
}
else // Buffer
{
return resource.bufferDesc.Size;
//return resource.bufferDesc.Size;
return _allocator.GetSizeInfo(ResourceDesc.Buffer(resource.bufferDesc)).Size;
}
}
public ResourceAliasingManager(RenderGraphObjectPool pool)
public ResourceAliasingManager(IResourceAllocator allocator, RenderGraphObjectPool pool)
{
_allocator = allocator;
_pool = pool;
_heap = new ResourceHeap(0);
_placedResources = new List<PlacedResource>(32);
_logicalToPlaced = new Dictionary<int, int>(64);

View File

@@ -71,3 +71,263 @@ internal sealed class ResourceStateTracker
lastAccessPass = -1;
}
}
/// <summary>
/// Represents a compiled barrier with only the target state.
/// The before state is always queried from ResourceDatabase at execution time.
/// </summary>
internal struct CompiledBarrier
{
public int PassIndex;
public Identifier<RGResource> Resource;
public ResourceBarrierData TargetState;
public Identifier<RGResource> AliasingPredecessor; // Invalid if not aliasing
public BarrierFlags Flags;
public RenderGraphResourceType ResourceType;
public override readonly string ToString()
{
return AliasingPredecessor.IsValid
? $"[Pass {PassIndex}] Aliasing: {AliasingPredecessor.Value}->{Resource.Value} -> {TargetState.Layout}"
: $"[Pass {PassIndex}] Transition: {Resource.Value} -> {TargetState.Layout}";
}
}
/// <summary>
/// Static class containing barrier compilation logic.
/// Compiles barriers at graph compilation time, storing only target states.
/// </summary>
internal static class RenderGraphBarriers
{
/// <summary>
/// Compiles all barriers needed for execution, storing only target states.
/// Barriers include aliasing barriers and implicit state transitions.
/// </summary>
public static void CompileBarriers(
List<RenderGraphPassBase> compiledPasses,
List<CompiledBarrier> compiledBarriers,
RenderGraphResourceRegistry resources,
ResourceAliasingManager aliasingManager)
{
compiledBarriers.Clear();
// Process each compiled pass in order
for (var passIdx = 0; passIdx < compiledPasses.Count; passIdx++)
{
var pass = compiledPasses[passIdx];
// 1. Insert aliasing barriers for resources that reuse physical memory
InsertAliasingBarriers(pass, passIdx, compiledBarriers, resources, aliasingManager);
// 2. Compile implicit transitions for all resources accessed by this pass
CompileImplicitTransitions(pass, passIdx, compiledBarriers, resources);
}
}
/// <summary>
/// Inserts aliasing barriers when a placed resource is reused.
/// </summary>
private static void InsertAliasingBarriers(
RenderGraphPassBase pass,
int passIdx,
List<CompiledBarrier> compiledBarriers,
RenderGraphResourceRegistry resources,
ResourceAliasingManager aliasingManager)
{
// Check all resources written by this pass (both textures and buffers)
for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
{
var writeList = pass.resourceWrites[resType];
for (var i = 0; i < writeList.Count; i++)
{
var id = writeList[i];
var resource = resources.GetResource(id);
// Skip imported resources
if (resource.isImported)
{
continue;
}
// Check if this is the first use of this logical resource
if (resource.firstUsePass == pass.index)
{
// Get the placed resource
var placedIndex = aliasingManager.GetPlacedResourceIndex(id.Value);
if (placedIndex >= 0)
{
var placed = aliasingManager.GetPlacedResource(placedIndex);
// If this placed resource has multiple aliased resources,
// we need an aliasing barrier when switching between them
if (placed != null && placed.aliasedLogicalResources.Count > 1)
{
// Find the resource that used this placed memory most recently before this pass
Identifier<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 static void CompileImplicitTransitions(
RenderGraphPassBase pass,
int passIdx,
List<CompiledBarrier> compiledBarriers,
RenderGraphResourceRegistry resources)
{
// 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, resources);
AddTransition(handle, targetState);
}
}
// Compile transitions based on pass type
switch (pass.type)
{
case RenderPassType.Raster:
// Color attachments
for (var i = 0; i <= pass.maxColorIndex; i++)
{
if (pass.colorAccess[i].id.IsValid)
{
var usage = pass.colorAccess[i].usage;
var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync);
AddTransition(pass.colorAccess[i].id.AsResource(), targetState);
}
}
// Depth attachment
if (pass.depthAccess.id.IsValid)
{
var usage = pass.depthAccess.usage;
var targetState = new ResourceBarrierData(usage.Layout, usage.Access, usage.Sync);
AddTransition(pass.depthAccess.id.AsResource(), targetState);
}
// UAV resources
var uavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading);
for (var i = 0; i < pass.randomAccess.Count; i++)
{
AddTransition(pass.randomAccess[i], uavState);
}
break;
case RenderPassType.Compute:
var computeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.ComputeShading);
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var writeList = pass.resourceWrites[i];
for (var j = 0; j < writeList.Count; j++)
{
AddTransition(writeList[j], computeUavState);
}
}
break;
case RenderPassType.Unsafe:
var rtState = new ResourceBarrierData(BarrierLayout.RenderTarget, BarrierAccess.RenderTarget, BarrierSync.RenderTarget);
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var writeList = pass.resourceWrites[i];
for (var j = 0; j < writeList.Count; j++)
{
AddTransition(writeList[j], rtState);
}
}
var unsafeUavState = new ResourceBarrierData(BarrierLayout.UnorderedAccess, BarrierAccess.UnorderedAccess, BarrierSync.AllShading);
for (var i = 0; i < pass.randomAccess.Count; i++)
{
AddTransition(pass.randomAccess[i], unsafeUavState);
}
break;
}
}
private static ResourceBarrierData GetBufferReadBarrierData(
Identifier<RGResource> handle,
RenderGraphPassBase pass,
RenderGraphResourceType resourceType,
RenderGraphResourceRegistry resources)
{
if (resourceType == RenderGraphResourceType.Texture)
{
return new ResourceBarrierData(BarrierLayout.ShaderResource, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading);
}
var sync = BarrierSync.PixelShading | BarrierSync.NonPixelShading;
var access = BarrierAccess.ShaderResource;
var resource = resources.GetResource(handle);
if (resource.bufferDesc.Usage.HasFlag(BufferUsage.IndirectArgument))
{
sync = BarrierSync.ExecuteIndirect;
access = BarrierAccess.IndirectArgument;
}
return new ResourceBarrierData(BarrierLayout.Undefined, access, sync);
}
}

View File

@@ -23,11 +23,8 @@ internal sealed class CachedCompilation
// Placed resource metadata
public readonly List<PlacedResourceData> placedResources = new(32);
// Resource barriers
public readonly List<ResourceBarrier> barriers = new(128);
// Resource state mappings (for barrier generation)
public readonly Dictionary<int, ResourceBarrierData> resourceStates = new(128);
// Compiled barriers (stores only target states, queries before state from ResourceDatabase)
public readonly List<CompiledBarrier> compiledBarriers = new(128);
// Real gpu resource
public readonly List<Handle<GPUResource>> backingResources = new(32);
@@ -41,8 +38,7 @@ internal sealed class CachedCompilation
passCulledFlags.Clear();
logicalToPhysical.Clear();
placedResources.Clear();
barriers.Clear();
resourceStates.Clear();
compiledBarriers.Clear();
backingResources.Clear();
viewState = default;
}
@@ -112,12 +108,7 @@ internal sealed class RenderGraphCompilationCache
}
_cached.placedResources.AddRange(data.placedResources);
_cached.barriers.AddRange(data.barriers);
foreach (var kvp in data.resourceStates)
{
_cached.resourceStates[kvp.Key] = kvp.Value;
}
_cached.compiledBarriers.AddRange(data.compiledBarriers);
_cached.backingResources.AddRange(data.backingResources);
}

View File

@@ -0,0 +1,391 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Handles compilation of the render graph including pass culling, resource allocation,
/// barrier compilation, and cache management.
/// </summary>
internal sealed class RenderGraphCompiler
{
private readonly IGraphicsEngine _graphicsEngine;
private readonly RenderGraphResourceRegistry _resources;
private readonly ResourceAliasingManager _aliasingManager;
private readonly RenderGraphNativePassBuilder _nativePassBuilder;
private readonly RenderGraphCompilationCache _compilationCache;
private Handle<GPUResource> _resourceHeap;
public RenderGraphCompiler(
IGraphicsEngine graphicsEngine,
RenderGraphResourceRegistry resources,
ResourceAliasingManager aliasingManager,
RenderGraphNativePassBuilder nativePassBuilder,
RenderGraphCompilationCache compilationCache)
{
_graphicsEngine = graphicsEngine;
_resources = resources;
_aliasingManager = aliasingManager;
_nativePassBuilder = nativePassBuilder;
_compilationCache = compilationCache;
_resourceHeap = Handle<GPUResource>.Invalid;
}
/// <summary>
/// Compiles the render graph by culling passes, allocating resources, and preparing barriers.
/// </summary>
public void Compile(
in ViewState viewState,
ulong graphHash,
List<RenderGraphPassBase> passes,
List<RenderGraphPassBase> compiledPasses,
List<NativeRenderPass> nativePasses,
List<CompiledBarrier> compiledBarriers)
{
// Try to restore from cache
if (_compilationCache.TryGetCached(graphHash, out var cached))
{
// Check if view state changed
if (!cached.viewState.Equals(viewState))
{
// View state changed - re-resolve sizes and recreate GPU resources
_resources.ResolveTextureSizes(in viewState);
RestoreFromCache(cached, compiledPasses, passes, nativePasses, compiledBarriers);
_aliasingManager.AssignPhysicalResources(_resources, passes.Count);
AllocateResources();
cached.viewState = viewState;
}
else
{
// Perfect cache hit - restore everything
RestoreFromCache(cached, compiledPasses, passes, nativePasses, compiledBarriers);
}
return;
}
// Fresh compilation needed
compiledPasses.Clear();
// Mark passes with side effects (writes to imported resources)
MarkPassesWithSideEffects(passes);
// Cull passes based on dependency analysis
CullPasses(passes);
// Build final pass list (only non-culled passes)
for (var i = 0; i < passes.Count; i++)
{
var pass = passes[i];
if (!pass.culled)
{
compiledPasses.Add(pass);
}
}
_aliasingManager.AssignPhysicalResources(_resources, passes.Count);
AllocateResources();
CompileBarriers(compiledPasses, compiledBarriers);
_nativePassBuilder.BuildNativeRenderPasses(compiledPasses, nativePasses, compiledBarriers);
StoreInCache(graphHash, viewState, compiledPasses, passes, compiledBarriers);
}
/// <summary>
/// Marks passes that write to imported resources as having side effects.
/// </summary>
private void MarkPassesWithSideEffects(List<RenderGraphPassBase> passes)
{
for (var i = 0; i < passes.Count; i++)
{
var pass = passes[i];
// Check if this pass writes to any imported resources
for (var j = 0; j < (int)RenderGraphResourceType.Count; j++)
{
var writeList = pass.resourceWrites[j];
for (var k = 0; k < writeList.Count; k++)
{
var writeHandle = writeList[k];
var resource = _resources.GetResource(writeHandle);
if (resource.isImported)
{
pass.hasSideEffects = true;
break;
}
}
}
}
}
/// <summary>
/// Culls unused passes based on dependency analysis.
/// </summary>
private void CullPasses(List<RenderGraphPassBase> passes)
{
// Mark all passes as culled initially
for (var i = 0; i < passes.Count; i++)
{
passes[i].culled = passes[i].allowCulling && !passes[i].hasSideEffects;
}
// Traverse backwards from passes with side effects
for (var i = passes.Count - 1; i >= 0; i--)
{
var pass = passes[i];
if (!pass.culled)
{
UnculDependencies(pass, passes);
}
}
}
/// <summary>
/// Recursively un-culls dependencies of a pass.
/// </summary>
private void UnculDependencies(RenderGraphPassBase pass, List<RenderGraphPassBase> passes)
{
// Un-cull producers of read resources
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var readList = pass.resourceReads[i];
for (var j = 0; j < readList.Count; j++)
{
UnculProducer(readList[j], passes);
}
}
// Un-cull producers of color attachments
for (var i = 0; i < pass.maxColorIndex; i++)
{
if (pass.colorAccess[i].id.IsValid)
{
UnculProducer(pass.colorAccess[i].id.AsResource(), passes);
}
}
// Un-cull producer of depth attachment
if (pass.depthAccess.id.IsValid)
{
UnculProducer(pass.depthAccess.id.AsResource(), passes);
}
// Un-cull producers of UAV resources (if not already in reads/writes)
for (var i = 0; i < pass.randomAccess.Count; i++)
{
UnculProducer(pass.randomAccess[i], passes);
}
}
/// <summary>
/// Un-culls the producer of a resource.
/// </summary>
private void UnculProducer(Identifier<RGResource> resource, List<RenderGraphPassBase> passes)
{
var res = _resources.GetResource(resource);
if (res.producerPass >= 0)
{
var producer = passes[res.producerPass];
if (producer.culled)
{
producer.culled = false;
UnculDependencies(producer, passes);
}
}
}
/// <summary>
/// Allocates GPU resources for the render graph.
/// </summary>
private void AllocateResources()
{
if (_resourceHeap.IsValid)
{
foreach (var res in _resources.Resources)
{
if (res.isImported)
{
continue;
}
_graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource);
}
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap);
}
if (_aliasingManager.Heap.size == 0)
{
return;
}
var allocationDesc = new AllocationDesc
{
Size = _aliasingManager.Heap.size + 64 * 1024, // Add 64KB padding to avoid potential overflows
Alignment = ResourceHeap.DEFAULT_ALIGNMENT,
HeapFlags = HeapFlags.AlowBufferAndTexture,
HeapType = HeapType.Default
};
_resourceHeap = _graphicsEngine.ResourceAllocator.Allocate(in allocationDesc, "RenderGraphResourceHeap");
for (var i = 0; i < _resources.Resources.Count; i++)
{
var placedIndex = _aliasingManager.GetPlacedResourceIndex(i);
var placed = _aliasingManager.GetPlacedResource(placedIndex);
if (placed == null)
{
continue;
}
var res = _resources.Resources[i];
var ops = new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = _resourceHeap,
Offset = placed.heapOffset,
};
if (res.type == RenderGraphResourceType.Texture)
{
var textureDesc = res.rgTextureDesc.ToTextureDesc(res.resolvedWidth, res.resolvedHeight);
res.backingResource = _graphicsEngine.ResourceAllocator.CreateTexture(in textureDesc, res.name, ops).AsResource();
}
else if (res.type == RenderGraphResourceType.Buffer)
{
res.backingResource = _graphicsEngine.ResourceAllocator.CreateBuffer(in res.bufferDesc, res.name, ops).AsResource();
}
else
{
throw new NotSupportedException();
}
_compilationCache.UpdateBackingResource(i, res.backingResource);
}
}
/// <summary>
/// Compiles all barriers needed for execution.
/// Delegates to RenderGraphBarriers for the actual compilation logic.
/// </summary>
private void CompileBarriers(List<RenderGraphPassBase> compiledPasses, List<CompiledBarrier> compiledBarriers)
{
RenderGraphBarriers.CompileBarriers(compiledPasses, compiledBarriers, _resources, _aliasingManager);
}
/// <summary>
/// Restores the render graph state from cached compilation results.
/// </summary>
private void RestoreFromCache(
CachedCompilation cached,
List<RenderGraphPassBase> compiledPasses,
List<RenderGraphPassBase> passes,
List<NativeRenderPass> nativePasses,
List<CompiledBarrier> compiledBarriers)
{
// Restore compiled pass list
compiledPasses.Clear();
for (var i = 0; i < cached.compiledPassIndices.Count; i++)
{
var passIndex = cached.compiledPassIndices[i];
compiledPasses.Add(passes[passIndex]);
}
// Restore culling flags
for (var i = 0; i < passes.Count && i < cached.passCulledFlags.Count; i++)
{
passes[i].culled = cached.passCulledFlags[i];
}
// Restore aliasing mappings (need to update ResourceAliasingManager)
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources);
// Restore compiled barriers (deep copy to avoid shared references)
compiledBarriers.Clear();
for (var i = 0; i < cached.compiledBarriers.Count; i++)
{
compiledBarriers.Add(cached.compiledBarriers[i]);
}
for (var i = 0; i < _resources.ResourceCount; i++)
{
var res = _resources.Resources[i];
if (!res.isImported)
{
res.backingResource = cached.backingResources[i];
}
}
_nativePassBuilder.BuildNativeRenderPasses(compiledPasses, nativePasses, compiledBarriers);
}
/// <summary>
/// Stores current compilation results in the cache.
/// </summary>
private void StoreInCache(
ulong graphHash,
in ViewState viewState,
List<RenderGraphPassBase> compiledPasses,
List<RenderGraphPassBase> passes,
List<CompiledBarrier> compiledBarriers)
{
var cacheData = new CachedCompilation();
// Store view state
cacheData.viewState = viewState;
// Store compiled pass indices
for (var i = 0; i < compiledPasses.Count; i++)
{
cacheData.compiledPassIndices.Add(compiledPasses[i].index);
}
// Store culling flags for all passes
for (var i = 0; i < passes.Count; i++)
{
cacheData.passCulledFlags.Add(passes[i].culled);
}
// Store aliasing mappings
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources);
// Store compiled barriers
for (var i = 0; i < compiledBarriers.Count; i++)
{
cacheData.compiledBarriers.Add(compiledBarriers[i]);
}
for (var i = 0; i < _resources.ResourceCount; i++)
{
var res = _resources.Resources[i];
cacheData.backingResources.Add(res.backingResource);
}
_compilationCache.Store(graphHash, cacheData);
}
/// <summary>
/// Releases allocated GPU resources.
/// </summary>
public void Dispose()
{
if (_resourceHeap.IsValid)
{
foreach (var res in _resources.Resources)
{
if (!res.isImported)
{
_graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource);
}
}
_graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap);
_resourceHeap = Handle<GPUResource>.Invalid;
}
}
}

View File

@@ -0,0 +1,223 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Handles execution of compiled render graphs, including barrier execution and native render passes.
/// </summary>
internal sealed class RenderGraphExecutor
{
private readonly IGraphicsEngine _graphicsEngine;
private readonly RenderGraphResourceRegistry _resources;
private readonly RenderGraphContext _context;
public RenderGraphExecutor(
IGraphicsEngine graphicsEngine,
RenderGraphResourceRegistry resources,
RenderGraphContext context)
{
_graphicsEngine = graphicsEngine;
_resources = resources;
_context = context;
}
/// <summary>
/// Executes all compiled passes using native render passes where possible.
/// </summary>
public unsafe void Execute(
ICommandBuffer cmd,
List<RenderGraphPassBase> compiledPasses,
List<NativeRenderPass> nativePasses,
List<CompiledBarrier> compiledBarriers)
{
var barrierIndex = 0;
var nativePassIndex = 0;
var logicalPassIndex = 0;
_context.SetCommandBuffer(cmd);
var pPassRTDescs = stackalloc PassRenderTargetDesc[8];
var pRtFormats = stackalloc TextureFormat[8];
while (logicalPassIndex < compiledPasses.Count)
{
var pass = compiledPasses[logicalPassIndex];
// Check if this pass is part of a native render pass
if (pass.type == RenderPassType.Raster && nativePassIndex < nativePasses.Count)
{
var nativePass = nativePasses[nativePassIndex];
// Build barriers for ALL merged passes before beginning the native render pass
for (var i = 0; i < nativePass.mergedPassIndices.Count; i++)
{
var mergedPassIdx = nativePass.mergedPassIndices[i];
ExecuteBarriersForPass(cmd, mergedPassIdx, ref barrierIndex, compiledBarriers);
}
// Begin native render pass
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{
var attachment = nativePass.colorAttachments[i];
pPassRTDescs[i] = new PassRenderTargetDesc
{
Texture = _resources.GetResource(attachment.texture).backingResource.AsTexture(),
ClearColor = attachment.clearColor,
LoadOp = attachment.loadOp,
StoreOp = attachment.storeOp
};
}
var depthDesc = new PassDepthStencilDesc
{
Texture = nativePass.hasDepthAttachment
? _resources.GetResource(nativePass.depthAttachment.texture).backingResource.AsTexture()
: Handle<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, compiledBarriers);
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,
List<CompiledBarrier> compiledBarriers)
{
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();
}
}

View File

@@ -0,0 +1,177 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Computes structural hashes of render graphs for compilation caching.
/// Hashes are based on graph topology and resource configurations, not runtime values.
/// </summary>
internal static class RenderGraphHasher
{
/// <summary>
/// Computes a hash of the entire render graph structure.
/// Used for cache invalidation - same hash means same compilation result.
/// </summary>
public static unsafe ulong ComputeGraphHash(List<RenderGraphPassBase> passes, RenderGraphResourceRegistry resources)
{
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, resources);
pData[offset] = (byte)pass.depthAccess.accessFlags;
offset += sizeof(AccessFlags);
*(int*)(pData + offset) = pass.maxColorIndex;
offset += sizeof(int);
for (var j = 0; j <= pass.maxColorIndex; j++)
{
offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id, resources);
pData[offset] = (byte)pass.colorAccess[j].accessFlags;
offset += sizeof(AccessFlags);
}
for (var j = 0; j < (int)RenderGraphResourceType.Count; j++)
{
var readList = pass.resourceReads[j];
var writeList = pass.resourceWrites[j];
var createList = pass.resourceCreates[j];
*(int*)(pData + offset) = readList.Count;
offset += sizeof(int);
for (var k = 0; k < readList.Count; k++)
{
*(int*)(pData + offset) = readList[k].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = writeList.Count;
offset += sizeof(int);
for (var k = 0; k < writeList.Count; k++)
{
*(int*)(pData + offset) = writeList[k].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = createList.Count;
offset += sizeof(int);
for (var k = 0; k < createList.Count; k++)
{
*(int*)(pData + offset) = createList[k].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = pass.randomAccess.Count;
offset += sizeof(int);
for (var k = 0; k < pass.randomAccess.Count; k++)
{
*(int*)(pData + offset) = pass.randomAccess[k].Value;
offset += sizeof(int);
}
}
*(int*)(pData + offset) = pass.GetRenderFuncHashCode();
offset += sizeof(int);
}
var span = new Span<byte>(pData, offset);
return XxHash64.HashToUInt64(span);
}
/// <summary>
/// Computes a hash of a texture resource's structural properties.
/// For imported textures, hashes the backing handle.
/// For transient textures, hashes the descriptor (respecting size mode).
/// </summary>
private static unsafe int ComputeTextureHash(byte* pData, int offset, Identifier<RGTexture> texture, RenderGraphResourceRegistry resources)
{
if (texture.IsInvalid)
{
return offset;
}
var resource = resources.GetResource(texture.AsResource());
// Hash imported flag
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
offset += sizeof(byte);
// For imported textures, hash the backing resource handle
if (resource.isImported)
{
*(int*)(pData + offset) = resource.backingResource.GetHashCode();
offset += sizeof(int);
return offset;
}
var desc = resource.rgTextureDesc;
// Hash format (structural)
*(TextureFormat*)(pData + offset) = desc.format;
offset += sizeof(TextureFormat);
// Hash size mode (structural)
*(RGTextureSizeMode*)(pData + offset) = desc.sizeMode;
offset += sizeof(RGTextureSizeMode);
// Hash size specification based on mode
if (desc.sizeMode == RGTextureSizeMode.Absolute)
{
// Absolute mode: hash actual dimensions
*(uint*)(pData + offset) = desc.width;
offset += sizeof(uint);
*(uint*)(pData + offset) = desc.height;
offset += sizeof(uint);
}
else
{
// Relative mode: hash scale factors (NOT resolved dimensions)
*(float*)(pData + offset) = desc.scaleX;
offset += sizeof(float);
*(float*)(pData + offset) = desc.scaleY;
offset += sizeof(float);
}
// Hash other structural properties
*(TextureDimension*)(pData + offset) = desc.dimension;
offset += sizeof(TextureDimension);
*(uint*)(pData + offset) = desc.mipLevels;
offset += sizeof(uint);
*(TextureUsage*)(pData + offset) = desc.usage;
offset += sizeof(TextureUsage);
*(bool*)(pData + offset) = desc.clearAtFirstUse;
offset += sizeof(bool);
*(bool*)(pData + offset) = desc.discardAtLastUse;
offset += sizeof(bool);
return offset;
}
}

View File

@@ -0,0 +1,370 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Builds native render passes by merging compatible consecutive raster passes.
/// Optimizes for tile-based deferred rendering (TBDR) GPUs by minimizing load/store operations.
/// </summary>
internal sealed class RenderGraphNativePassBuilder
{
private readonly RenderGraphObjectPool _objectPool;
private readonly RenderGraphResourceRegistry _resources;
public RenderGraphNativePassBuilder(RenderGraphObjectPool objectPool, RenderGraphResourceRegistry resources)
{
_objectPool = objectPool;
_resources = resources;
}
/// <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>
public void BuildNativeRenderPasses(
List<RenderGraphPassBase> compiledPasses,
List<NativeRenderPass> nativePasses,
List<CompiledBarrier> compiledBarriers)
{
// Clear previous native passes
for (var i = 0; i < nativePasses.Count; i++)
{
_objectPool.Return(nativePasses[i]);
}
nativePasses.Clear();
NativeRenderPass? currentNativePass = null;
for (var i = 0; i < compiledPasses.Count; i++)
{
var pass = compiledPasses[i];
// Only raster passes can be merged into native render passes
// Compute passes break the current native render pass
if (pass.type != RenderPassType.Raster)
{
// Close current native pass if open
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
currentNativePass = null;
}
continue; // Compute/Unsafe passes execute outside native render passes
}
// Check if we can merge with current native pass
if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i, compiledPasses, compiledBarriers))
{
// Merge into existing native pass
currentNativePass.mergedPassIndices.Add(i);
currentNativePass.lastLogicalPass = i;
}
else
{
// Start new native pass
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
}
currentNativePass = CreateNativePass(pass, i);
}
}
// Add final native pass
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
}
// Infer load/store operations for all native passes
for (var i = 0; i < nativePasses.Count; i++)
{
InferLoadStoreOps(nativePasses[i]);
}
}
/// <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 = 0; // Will be set by caller
nativePass.mergedPassIndices.Add(passIndex);
nativePass.firstLogicalPass = passIndex;
nativePass.lastLogicalPass = passIndex;
nativePass.allowUAVWrites = pass.randomAccess.Count > 0;
// Copy color attachments
nativePass.colorAttachmentCount = pass.maxColorIndex + 1;
for (var i = 0; i <= pass.maxColorIndex; i++)
{
var access = pass.colorAccess[i];
nativePass.colorAttachments[i] = new RenderTargetInfo
{
texture = access.id,
access = access.accessFlags
};
}
// Copy depth attachment
if (!pass.depthAccess.id.IsInvalid)
{
nativePass.hasDepthAttachment = true;
nativePass.depthAttachment = new DepthStencilInfo
{
texture = pass.depthAccess.id,
access = pass.depthAccess.accessFlags
};
}
return nativePass;
}
/// <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,
List<RenderGraphPassBase> compiledPasses,
List<CompiledBarrier> compiledBarriers)
{
// Don't merge if UAVs are involved (conservative)
if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites)
{
return false;
}
// Check if attachment configuration matches
if (!AttachmentsMatch(nativePass, pass))
{
return false;
}
// Check if barriers are needed between last merged pass and this pass
if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex, compiledPasses, compiledBarriers))
{
return false;
}
return true;
}
/// <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,
List<RenderGraphPassBase> compiledPasses,
List<CompiledBarrier> compiledBarriers)
{
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;
}
}
}
}