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

View File

@@ -435,40 +435,6 @@ internal sealed unsafe partial class D3D12ResourceAllocator
return uavDesc; return uavDesc;
} }
private static D3D12_RESOURCE_FLAGS ConvertTextureUsage(TextureUsage usage)
{
var flags = D3D12_RESOURCE_FLAG_NONE;
if (usage.HasFlag(TextureUsage.RenderTarget))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
}
if (usage.HasFlag(TextureUsage.DepthStencil))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
}
if (usage.HasFlag(TextureUsage.UnorderedAccess))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
return flags;
}
private static D3D12_RESOURCE_FLAGS ConvertBufferUsage(BufferUsage usage)
{
var flags = D3D12_RESOURCE_FLAG_NONE;
if (usage.HasFlag(BufferUsage.Raw) || usage.HasFlag(BufferUsage.UnorderedAccess))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
return flags;
}
private static D3D12_HEAP_TYPE ConvertMemoryType(ResourceMemoryType memoryType) private static D3D12_HEAP_TYPE ConvertMemoryType(ResourceMemoryType memoryType)
{ {
return memoryType switch return memoryType switch
@@ -559,7 +525,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, Guid* riid, void** ppv) private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, Guid* riid, void** ppv)
{ {
var hr = S.S_OK; HRESULT hr;
if (options.AllocationType == ResourceAllocationType.Suballocation) if (options.AllocationType == ResourceAllocationType.Suballocation)
{ {
@@ -581,6 +547,26 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return hr; return hr;
} }
public ResourceSizeInfo GetSizeInfo(ResourceDesc desc)
{
D3D12_RESOURCE_DESC d3d12Desc;
if (desc.Type == ResourceType.Texture)
{
d3d12Desc = desc.TextureDescription.ToD3D12ResourceDesc();
}
else
{
d3d12Desc = desc.BufferDescription.ToD3D12ResourceDesc();
}
var info = _device.NativeDevice.Get()->GetResourceAllocationInfo(0, 1, &d3d12Desc);
return new ResourceSizeInfo
{
Size = info.SizeInBytes,
Alignment = info.Alignment
};
}
public Handle<GPUResource> Allocate(ref readonly AllocationDesc desc, string name) public Handle<GPUResource> Allocate(ref readonly AllocationDesc desc, string name)
{ {
var allocDesc = new D3D12MA_ALLOCATION_DESC var allocDesc = new D3D12MA_ALLOCATION_DESC
@@ -633,51 +619,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
CheckTexture2DSize(desc.Width, desc.Height); CheckTexture2DSize(desc.Width, desc.Height);
var dxgiFormat = D3D12Utility.ToDXGIFormat(desc.Format); var resourceDesc = desc.ToD3D12ResourceDesc();
var maxDimension = Math.Max(desc.Width, Math.Max(desc.Height, desc.Slice));
var mipLevels = desc.MipLevels == 0
? (ushort)(1 + Math.Floor(Math.Log2(maxDimension)))
: (ushort)desc.MipLevels;
var resourceFlags = ConvertTextureUsage(desc.Usage);
var resourceDesc = desc.Dimension switch
{
TextureDimension.Texture2D => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
flags: resourceFlags),
TextureDimension.Texture3D => D3D12_RESOURCE_DESC.Tex3D(
dxgiFormat,
desc.Width,
desc.Height,
(ushort)desc.Slice,
flags: resourceFlags),
TextureDimension.TextureCube => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: 6,
flags: resourceFlags),
TextureDimension.Texture2DArray => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: (ushort)desc.Slice,
flags: resourceFlags),
TextureDimension.TextureCubeArray => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: (ushort)(desc.Slice * 6),
flags: resourceFlags),
_ => throw new ArgumentException($"Unsupported texture dimension: {desc.Dimension}"),
};
var allocationDesc = new D3D12MA_ALLOCATION_DESC var allocationDesc = new D3D12MA_ALLOCATION_DESC
{ {
HeapType = D3D12_HEAP_TYPE_DEFAULT, HeapType = D3D12_HEAP_TYPE_DEFAULT,
@@ -715,7 +657,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv); var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv);
var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray; var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray;
var srvDesc = CreateTextureSrvDesc(pResource, mipLevels, desc.Slice, isCubeMap); var srvDesc = CreateTextureSrvDesc(pResource, resourceDesc.MipLevels, resourceDesc.DepthOrArraySize, isCubeMap);
_device.NativeDevice.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle); _device.NativeDevice.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv); _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv);
@@ -782,14 +724,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
ObjectDisposedException.ThrowIf(_disposed, this); ObjectDisposedException.ThrowIf(_disposed, this);
CheckBufferSize(desc.Size); CheckBufferSize(desc.Size);
var alignedSize = desc.Size; var resourceDesc = desc.ToD3D12ResourceDesc();
if (desc.Usage.HasFlag(BufferUsage.Constant))
{
// D3D12 CBV size must be 256-byte aligned
alignedSize = (uint)(desc.Size + 255) & ~255u;
}
var resourceDesc = D3D12_RESOURCE_DESC.Buffer(alignedSize, ConvertBufferUsage(desc.Usage));
var isRaw = desc.Usage.HasFlag(BufferUsage.Raw); var isRaw = desc.Usage.HasFlag(BufferUsage.Raw);
var allocationDesc = new D3D12MA_ALLOCATION_DESC var allocationDesc = new D3D12MA_ALLOCATION_DESC
@@ -839,7 +774,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC
{ {
BufferLocation = pResource->GetGPUVirtualAddress(), BufferLocation = pResource->GetGPUVirtualAddress(),
SizeInBytes = (uint)alignedSize SizeInBytes = (uint)resourceDesc.Width,
}; };
_device.NativeDevice.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle); _device.NativeDevice.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle);

View File

@@ -294,7 +294,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
} }
info.Release(_descriptorAllocator); info.Release(_descriptorAllocator);
//System.Diagnostics.Debug.Assert(info.Release(_descriptorAllocator) == 0);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
_resourceName.Remove(handle, out var name); _resourceName.Remove(handle, out var name);
#endif #endif

View File

@@ -7,7 +7,7 @@ using static TerraFX.Aliases.DXGI_Alias;
namespace Ghost.Graphics.D3D12.Utilities; namespace Ghost.Graphics.D3D12.Utilities;
internal unsafe static class D3D12Utility internal static unsafe class D3D12Utility
{ {
public static void SetName<T>(ref this T obj, ReadOnlySpan<char> name) public static void SetName<T>(ref this T obj, ReadOnlySpan<char> name)
where T : unmanaged, ID3D12Object.Interface where T : unmanaged, ID3D12Object.Interface
@@ -278,6 +278,102 @@ internal unsafe static class D3D12Utility
}; };
} }
public static D3D12_RESOURCE_FLAGS ToD3D12ResourceFlag(this TextureUsage usage)
{
var flags = D3D12_RESOURCE_FLAG_NONE;
if (usage.HasFlag(TextureUsage.RenderTarget))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
}
if (usage.HasFlag(TextureUsage.DepthStencil))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
}
if (usage.HasFlag(TextureUsage.UnorderedAccess))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
return flags;
}
public static D3D12_RESOURCE_DESC ToD3D12ResourceDesc(this in TextureDesc desc)
{
var dxgiFormat = desc.Format.ToDXGIFormat();
var maxDimension = Math.Max(desc.Width, Math.Max(desc.Height, desc.Slice));
var mipLevels = desc.MipLevels == 0
? (ushort)(1 + Math.Floor(Math.Log2(maxDimension)))
: (ushort)desc.MipLevels;
var resourceFlags = desc.Usage.ToD3D12ResourceFlag();
return desc.Dimension switch
{
TextureDimension.Texture2D => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
flags: resourceFlags),
TextureDimension.Texture3D => D3D12_RESOURCE_DESC.Tex3D(
dxgiFormat,
desc.Width,
desc.Height,
(ushort)desc.Slice,
flags: resourceFlags),
TextureDimension.TextureCube => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: 6,
flags: resourceFlags),
TextureDimension.Texture2DArray => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: (ushort)desc.Slice,
flags: resourceFlags),
TextureDimension.TextureCubeArray => D3D12_RESOURCE_DESC.Tex2D(
dxgiFormat,
desc.Width,
desc.Height,
mipLevels: mipLevels,
arraySize: (ushort)(desc.Slice * 6),
flags: resourceFlags),
_ => throw new ArgumentException($"Unsupported texture dimension: {desc.Dimension}"),
};
}
public static D3D12_RESOURCE_FLAGS ToD3D12ResourceFlag(this BufferUsage usage)
{
var flags = D3D12_RESOURCE_FLAG_NONE;
if (usage.HasFlag(BufferUsage.Raw) || usage.HasFlag(BufferUsage.UnorderedAccess))
{
flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
return flags;
}
public static D3D12_RESOURCE_DESC ToD3D12ResourceDesc(this in BufferDesc desc)
{
var alignedSize = desc.Size;
if (desc.Usage.HasFlag(BufferUsage.Constant))
{
// D3D12 CBV size must be 256-byte aligned
alignedSize = (uint)(desc.Size + 255) & ~255u;
}
var resourceFlags = desc.Usage.ToD3D12ResourceFlag();
return D3D12_RESOURCE_DESC.Buffer(alignedSize, resourceFlags);
}
public static ResourceDesc ToResourceDesc(this D3D12_RESOURCE_DESC desc) public static ResourceDesc ToResourceDesc(this D3D12_RESOURCE_DESC desc)
{ {
if (desc.Dimension == D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER) if (desc.Dimension == D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER)

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Ghost.Core.Graphics; using Ghost.Core.Graphics;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -425,22 +426,44 @@ public struct ResourceDesc
internal resource_union _desc; internal resource_union _desc;
public ResourceType Type
{
get; init;
}
public TextureDesc TextureDescription public TextureDesc TextureDescription
{ {
readonly get => _desc.textureDescription; readonly get
set => _desc.textureDescription = value; {
Debug.Assert(Type == ResourceType.Texture);
return _desc.textureDescription;
}
set
{
Debug.Assert(Type == ResourceType.Texture);
_desc.textureDescription = value;
}
} }
public BufferDesc BufferDescription public BufferDesc BufferDescription
{ {
readonly get => _desc.bufferDescription; readonly get
set => _desc.bufferDescription = value; {
Debug.Assert(Type == ResourceType.Buffer);
return _desc.bufferDescription;
}
set
{
Debug.Assert(Type == ResourceType.Buffer);
_desc.bufferDescription = value;
}
} }
public static ResourceDesc Buffer(BufferDesc desc) public static ResourceDesc Buffer(BufferDesc desc)
{ {
return new ResourceDesc return new ResourceDesc
{ {
Type = ResourceType.Buffer,
BufferDescription = desc BufferDescription = desc
}; };
} }
@@ -449,6 +472,7 @@ public struct ResourceDesc
{ {
return new ResourceDesc return new ResourceDesc
{ {
Type = ResourceType.Texture,
TextureDescription = desc TextureDescription = desc
}; };
} }
@@ -990,6 +1014,12 @@ public enum ResourceMemoryType
Readback // GPU-to-CPU memory Readback // GPU-to-CPU memory
} }
public enum ResourceType
{
Texture,
Buffer
}
[Flags] [Flags]
public enum TextureUsage public enum TextureUsage
{ {

View File

@@ -69,8 +69,23 @@ public struct AllocationDesc
} }
} }
public readonly struct ResourceSizeInfo
{
public ulong Size
{
get; init;
}
public ulong Alignment
{
get; init;
}
}
public interface IResourceAllocator : IDisposable public interface IResourceAllocator : IDisposable
{ {
ResourceSizeInfo GetSizeInfo(ResourceDesc desc);
/// <summary> /// <summary>
/// Allocates a block of memory on the GPU /// Allocates a block of memory on the GPU
/// </summary> /// </summary>

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> /// </summary>
internal sealed class ResourceAliasingManager internal sealed class ResourceAliasingManager
{ {
private readonly IResourceAllocator _allocator;
private readonly RenderGraphObjectPool _pool; private readonly RenderGraphObjectPool _pool;
private readonly ResourceHeap _heap; private readonly ResourceHeap _heap;
@@ -306,17 +307,20 @@ internal sealed class ResourceAliasingManager
if (resource.type == RenderGraphResourceType.Texture) if (resource.type == RenderGraphResourceType.Texture)
{ {
var textureDesc = resource.rgTextureDesc.ToTextureDesc(resource.resolvedWidth, resource.resolvedHeight); 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 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; _pool = pool;
_heap = new ResourceHeap(0); _heap = new ResourceHeap(0);
_placedResources = new List<PlacedResource>(32); _placedResources = new List<PlacedResource>(32);
_logicalToPlaced = new Dictionary<int, int>(64); _logicalToPlaced = new Dictionary<int, int>(64);

View File

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