Render graph: native pass merging & heap-based aliasing

Major architecture upgrade:
- Add native render pass merging (hardware pass grouping, load/store op inference)
- Implement heap-based aliasing for textures & buffers (D3D12-style)
- Unify resource model: buffers and textures in one registry
- Extend builder API for buffer creation/usage, access flags, hints
- Improve barrier/state tracking (buffer hints, indirect argument state)
- Update caching, hashing, and debug output for new model
- Add enums/structs: AttachmentLoadOp, StoreOp, BufferHint, etc.
- D3D12 backend: support named resources, temp upload buffers, correct heap usage
- Update docs, benchmarks, and project files for new features

Brings render graph closer to AAA engine standards, enabling efficient memory usage, lower driver overhead, and a more flexible API.
This commit is contained in:
2026-01-16 01:59:33 +09:00
parent ac36bbf8c7
commit 1c155f962c
51 changed files with 2002 additions and 2314 deletions

View File

@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Core.Utilities;
internal class EnumUtility
{
}

View File

@@ -2,8 +2,10 @@ using Misaki.HighPerformance.LowLevel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using TerraFX.Interop.Windows;
using TerraFX.Interop.WinRT;
namespace Ghost.Core.Utilities;

View File

@@ -4,10 +4,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Core.Controls">
<Style TargetType="local:Vector3Field">
<Style TargetType="local:Float3Field">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Vector3Field">
<ControlTemplate TargetType="local:Float3Field">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />

View File

@@ -57,7 +57,7 @@ public partial class World
var world = s_worlds[id.Value];
return world is null ? throw new InvalidOperationException("World not found.") : world;
#else
return s_worlds[id.value]!;
return s_worlds[id.Value]!;
#endif
}

View File

@@ -12,11 +12,6 @@
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Controls\DebugConsole.xaml" />
<None Remove="Windows\DebugOutputWindow.xaml" />
<None Remove="Windows\WorkGraphTestWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="UnitTestApp.xaml" />
@@ -48,8 +43,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
@@ -57,11 +52,6 @@
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\Ghost.Test.Core\Ghost.Test.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Windows\WorkGraphTestWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Graphics.Test.Windows;
using Microsoft.UI.Xaml;
@@ -49,13 +50,15 @@ public partial class UnitTestApp : Application
{
LoadDll();
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
_window = new GraphicsTestWindow();
_window.Activate();
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
UnhandledException += (sender, e) =>
{
Logger.LogError(e.Exception);
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
};
}
}

View File

@@ -34,14 +34,5 @@
Height="4"
HorizontalAlignment="Stretch"
Background="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
<!-- Debug Console -->
<Border
Grid.Row="2"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,1,0,0">
<controls:DebugConsole x:Name="DebugConsole" />
</Border>
</Grid>
</Window>

View File

@@ -71,9 +71,7 @@ public sealed partial class GraphicsTestWindow : Window
_swapChain?.Dispose();
_renderSystem?.Dispose();
#if DEBUG
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.Dispose();
#endif
}
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
@@ -110,10 +108,7 @@ public sealed partial class GraphicsTestWindow : Window
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
{
_renderSystem.SignalCPUReady();
});
_renderSystem.SignalCPUReady();
}
}
}

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.Graphics.Test.Windows.WorkGraphTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="WorkGraphTestWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<SwapChainPanel
x:Name="Panel"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</Window>

View File

@@ -1,16 +0,0 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Graphics.Test.Windows;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed unsafe partial class WorkGraphTestWindow : Window
{
public WorkGraphTestWindow()
{
InitializeComponent();
}
}

View File

@@ -4,7 +4,6 @@ global using static TerraFX.Interop.DirectX.DXGI;
global using static TerraFX.Interop.Windows.Windows;
using Ghost.Core.Attributes;
using Ghost.Core.Utilities;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
@@ -12,6 +11,7 @@ using System.Runtime.Versioning;
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
[assembly: InternalsVisibleTo("Ghost.Graphics.Test")]
[assembly: InternalsVisibleTo("Ghost.Graphics.Test-Winui")]
[assembly: SupportedOSPlatform("windows10.0.19041.0")]

View File

@@ -42,7 +42,7 @@ public unsafe struct LocalKeywordSet
public ulong GetHash64()
{
ulong hash = 14695981039346656037ul; // FNV offset basis
ulong hash = 14695981039346656037ul; // FNV Offset basis
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{

View File

@@ -111,7 +111,7 @@ public struct Material : IResourceReleasable
MemoryType = ResourceMemoryType.Default,
};
var buffer = allocator.CreateBuffer(ref desc);
var buffer = allocator.CreateBuffer(ref desc, "MaterialCBuffer");
_cBufferCache = new CBufferCache(buffer, shader.CBufferSize);
}
@@ -214,14 +214,15 @@ public struct Material : IResourceReleasable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void UploadData(ICommandBuffer cmb, bool pixelOnlyResource = true)
public readonly void UploadData(ICommandBuffer cmd, bool pixelOnlyResource = true)
{
cmb.UploadBuffer(_cBufferCache.GpuResource, _cBufferCache.CpuData.AsSpan());
cmd.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), ResourceState.CopyDest);
cmd.UploadBuffer(_cBufferCache.GpuResource, _cBufferCache.CpuData.AsSpan());
var state = pixelOnlyResource
? ResourceState.PixelShaderResource
: ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource;
cmb.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), state);
cmd.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), state);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -127,10 +127,10 @@ public readonly unsafe ref struct RenderingContext
_directCmd.ResourceBarrier(bufferHandle, ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource);
}
public Handle<Texture> CreateTexture<T>(ref readonly TextureDesc desc, ReadOnlySpan<T> data, bool tempResource = false)
public Handle<Texture> CreateTexture<T>(ref readonly TextureDesc desc, ReadOnlySpan<T> data, string name)
where T : unmanaged
{
var handle = ResourceAllocator.CreateTexture(in desc, tempResource);
var handle = ResourceAllocator.CreateTexture(in desc, name);
UploadTexture(handle, data);
return handle;

View File

@@ -1,54 +0,0 @@
using Ghost.Graphics.Contracts;
namespace Ghost.Graphics.Core;
internal readonly struct SwapChainPresenter
{
public enum TargetType
{
Composition,
Hwnd
}
public readonly TargetType Type
{
get;
}
public readonly ISwapChainPanelNative SwapChainPanelNative
{
get;
}
public readonly nint Hwnd
{
get;
}
public readonly uint Width
{
get;
}
public readonly uint Height
{
get;
}
public SwapChainPresenter(ISwapChainPanelNative swapChainPanelNative, uint width, uint height)
{
Type = TargetType.Composition;
SwapChainPanelNative = swapChainPanelNative;
Hwnd = nint.Zero;
Width = width;
Height = height;
}
public SwapChainPresenter(nint hwnd, uint width, uint height)
{
Type = TargetType.Hwnd;
Hwnd = hwnd;
Width = width;
Height = height;
}
}

View File

@@ -151,8 +151,8 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
// Set descriptor heaps for bindless resources and samplers
var heaps = stackalloc ID3D12DescriptorHeap*[2];
heaps[0] = _descriptorAllocator.GetCbvSrvUavHeap(); // Bindless resource heap
heaps[1] = _descriptorAllocator.GetSamplerHeap(); // Bindless sampler heap
heaps[0] = _descriptorAllocator.GetCbvSrvUavHeap(); // Bindless resource Heap
heaps[1] = _descriptorAllocator.GetSamplerHeap(); // Bindless sampler Heap
_commandList.Get()->SetDescriptorHeaps(2, heaps);
}
@@ -401,20 +401,39 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
var format = record.desc.TextureDescription.Format.ToDXGIFormat();
var clearColor = rtDesc.ClearColor;
// Map load operation
var loadAccessType = rtDesc.LoadOp switch
{
AttachmentLoadOp.Load => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE,
AttachmentLoadOp.Clear => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
AttachmentLoadOp.DontCare => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE
};
// Map store operation
var storeAccessType = rtDesc.StoreOp switch
{
AttachmentStoreOp.Store => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE,
AttachmentStoreOp.DontCare => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE
};
var desc = new D3D12_RENDER_PASS_RENDER_TARGET_DESC
{
cpuDescriptor = cpuHandle,
BeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
{
Type = D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
Clear = new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
{
ClearValue = new D3D12_CLEAR_VALUE(format, (float*)&clearColor)
}
Type = loadAccessType,
Clear = loadAccessType == D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR
? new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
{
ClearValue = new D3D12_CLEAR_VALUE(format, (float*)&clearColor)
}
: default
},
EndingAccess = new D3D12_RENDER_PASS_ENDING_ACCESS
{
Type = D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE
Type = storeAccessType
}
};
@@ -435,16 +454,70 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var format = record.desc.TextureDescription.Format.ToDXGIFormat();
// Map depth load operation
var depthLoadAccessType = depthDesc.DepthLoadOp switch
{
AttachmentLoadOp.Load => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE,
AttachmentLoadOp.Clear => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
AttachmentLoadOp.DontCare => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE
};
// Map depth store operation
var depthStoreAccessType = depthDesc.DepthStoreOp switch
{
AttachmentStoreOp.Store => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE,
AttachmentStoreOp.DontCare => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE
};
// Map stencil load operation
var stencilLoadAccessType = depthDesc.StencilLoadOp switch
{
AttachmentLoadOp.Load => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE,
AttachmentLoadOp.Clear => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
AttachmentLoadOp.DontCare => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_PRESERVE
};
// Map stencil store operation
var stencilStoreAccessType = depthDesc.StencilStoreOp switch
{
AttachmentStoreOp.Store => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE,
AttachmentStoreOp.DontCare => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_DISCARD,
_ => D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE
};
var desc = new D3D12_RENDER_PASS_DEPTH_STENCIL_DESC
{
cpuDescriptor = cpuHandle,
DepthBeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
{
Type = D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
Clear = new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
{
ClearValue = new D3D12_CLEAR_VALUE(format, depthDesc.ClearDepth, depthDesc.ClearStencil)
}
Type = depthLoadAccessType,
Clear = depthLoadAccessType == D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR
? new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
{
ClearValue = new D3D12_CLEAR_VALUE(format, depthDesc.ClearDepth, depthDesc.ClearStencil)
}
: default
},
DepthEndingAccess = new D3D12_RENDER_PASS_ENDING_ACCESS
{
Type = depthStoreAccessType
},
StencilBeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
{
Type = stencilLoadAccessType,
Clear = stencilLoadAccessType == D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR
? new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
{
ClearValue = new D3D12_CLEAR_VALUE(format, depthDesc.ClearDepth, depthDesc.ClearStencil)
}
: default
},
StencilEndingAccess = new D3D12_RENDER_PASS_ENDING_ACCESS
{
Type = stencilStoreAccessType
}
};
@@ -731,22 +804,19 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
var sizeInBytes = (uint)(data.Length * sizeof(T));
var uploadHandle = _resourceAllocator.CreateUploadBuffer(sizeInBytes);
var uploadHandle = _resourceAllocator.CreateTempUploadBuffer(sizeInBytes, out var offset);
var uploadResource = _resourceDatabase.GetResource(uploadHandle.AsResource());
void* pMappedData;
uploadResource.Get()->Map(0, null, &pMappedData);
fixed (T* pData = data)
{
MemoryUtility.MemCpy(pMappedData, pData, sizeInBytes);
MemoryUtility.MemCpy((byte*)pMappedData + offset, pData, sizeInBytes);
}
uploadResource.Get()->Unmap(0, null);
var pResource = _resourceDatabase.GetResource(buffer.AsResource());
_commandList.Get()->CopyBufferRegion(pResource, 0, uploadResource, 0, sizeInBytes);
// D3D12 transition resource to COPY_DEST when copying
_resourceDatabase.SetResourceState(buffer.AsResource(), ResourceState.CopyDest);
_commandList.Get()->CopyBufferRegion(pResource, 0, uploadResource, offset, sizeInBytes);
}
public void UploadTexture(Handle<Texture> texture, ReadOnlySpan<SubResourceData> subresources)
@@ -766,7 +836,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
var resourceDesc = resource.Get()->GetDesc();
var requiredSize = GetRequiredIntermediateSize(resource, 0, (uint)subresources.Length);
var uploadHandle = _resourceAllocator.CreateUploadBuffer(requiredSize);
var uploadHandle = _resourceAllocator.CreateTempUploadBuffer(requiredSize, out var offset);
var pUploadResource = _resourceDatabase.GetResource(uploadHandle.AsResource());
var d3d12Subresources = stackalloc D3D12_SUBRESOURCE_DATA[subresources.Length];
@@ -784,7 +854,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
(ID3D12GraphicsCommandList*)_commandList.Get(),
resource,
pUploadResource,
0,
offset,
0,
(uint)subresources.Length,
d3d12Subresources);

View File

@@ -386,22 +386,22 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable
#region Utility Methods
/// <summary>
/// Gets the RTV heap for binding to the command list.
/// Gets the RTV Heap for binding to the command list.
/// </summary>
public ID3D12DescriptorHeap* GetRTVHeap() => _rtvHeap.Heap;
/// <summary>
/// Gets the DSV heap for binding to the command list.
/// Gets the DSV Heap for binding to the command list.
/// </summary>
public ID3D12DescriptorHeap* GetDSVHeap() => _dsvHeap.Heap;
/// <summary>
/// Gets the CBV/SRV/UAV heap for binding to the command list.
/// Gets the CBV/SRV/UAV Heap for binding to the command list.
/// </summary>
public ID3D12DescriptorHeap* GetCbvSrvUavHeap() => _cbvSrvUavHeap.ShaderVisibleHeap;
/// <summary>
/// Gets the sampler heap for binding to the command list.
/// Gets the sampler Heap for binding to the command list.
/// </summary>
public ID3D12DescriptorHeap* GetSamplerHeap() => _samplerHeap.ShaderVisibleHeap;

View File

@@ -138,7 +138,7 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable
}
// NOTE: In dynamic allocation, we use arena-style allocation without freeing.
// We reset the offset at the beginning of each frame instead.
// We reset the Offset at the beginning of each frame instead.
lock (_lock)
{

View File

@@ -165,7 +165,7 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
}
var size = _library.Get()->GetSerializedSize();
using var buffer = new UnsafeArray<byte>((int)size, Allocator.Persistent); // We use persistent heap allocation instead of stack allocation to avoid stack overflow for large pipeline libraries.
using var buffer = new UnsafeArray<byte>((int)size, Allocator.Persistent); // We use persistent Heap allocation instead of stack allocation to avoid stack overflow for large pipeline libraries.
ThrowIfFailed(_library.Get()->Serialize(buffer.GetUnsafePtr(), size));

View File

@@ -115,6 +115,11 @@ internal unsafe class D3D12RenderDevice : IRenderDevice
{
support |= FeatureSupport.BindlessResources;
}
if (options.ResourceHeapTier == D3D12_RESOURCE_HEAP_TIER.D3D12_RESOURCE_HEAP_TIER_2)
{
support |= FeatureSupport.AliasBuffersAndTextures;
}
}
D3D12_FEATURE_DATA_D3D12_OPTIONS5 options5 = default;

View File

@@ -95,6 +95,8 @@ internal class D3D12Renderer : IRenderer
{
Texture = target,
ClearColor = clearColor,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.Store,
},
];
@@ -103,6 +105,10 @@ internal class D3D12Renderer : IRenderer
Texture = Handle<Texture>.Invalid,
ClearDepth = 1.0f,
ClearStencil = 0,
DepthLoadOp = AttachmentLoadOp.Clear,
StencilLoadOp = AttachmentLoadOp.Clear,
DepthStoreOp = AttachmentStoreOp.Store,
StencilStoreOp = AttachmentStoreOp.Store,
};
// NOTE: Testing only.

View File

@@ -5,9 +5,11 @@ using Ghost.Graphics.Core;
using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@@ -512,9 +514,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator
var state = D3D12_RESOURCE_STATE_COMMON;
#if true
// D3D12 does not support state other than COMMON for buffers at creation.
return state;
#else
// D3D12 does not support state other than COMMON for buffers at creation.
if (usage.HasFlag(BufferUsage.Vertex) || usage.HasFlag(BufferUsage.Constant))
{
// Vertex and Constant buffers can share this state
@@ -557,13 +559,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator
}
}
// TODO: Dedicated pool for copy, render graph, and persistent resources
// TODO: Thread safety for resource allocator
// A common solution is to use ticket. Each pAllocation request create a ticket and put it into a thread-safe queue. A dedicated thread process the queue and fulfill the requests.
internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
{
private const uint _UPLOAD_BATCH_SIZE = 64 * 1024 * 1024; // 64 MB
private const uint _MAX_RESOURCE_SIZE_TO_FIT_IN_UPLOAD_BATCH = 16 * 1024 * 1024; // 16 MB
private UniquePtr<D3D12MA_Allocator> _d3d12MA;
private readonly IFenceSynchronizer _fenceSynchronizer;
@@ -574,6 +574,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
private UnsafeQueue<Handle<GPUResource>> _tempResources;
private readonly Handle<GraphicsBuffer> _uploadBatch;
private ulong _uploadBatchOffset;
private bool _disposed;
public D3D12ResourceAllocator(
@@ -600,7 +603,18 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
_resourceDatabase = resourceDatabase;
_pipelineLibrary = pipelineLibrary;
_tempResources = new UnsafeQueue<Handle<GPUResource>>(64, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
_tempResources = new UnsafeQueue<Handle<GPUResource>>(64, Allocator.Persistent);
// Create an upload batch
var uploadDesc = new BufferDesc
{
Size = _UPLOAD_BATCH_SIZE,
Usage = BufferUsage.Upload,
MemoryType = ResourceMemoryType.Upload,
};
_uploadBatch = CreateBuffer(in uploadDesc, "D3D12ResourceAllocator_UploadBatch");
_uploadBatchOffset = 0;
}
~D3D12ResourceAllocator()
@@ -609,9 +623,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Handle<GPUResource> TrackResource(D3D12MA_Allocation* allocation, D3D12_RESOURCE_STATES state, ResourceViewGroup resourceDescriptor, ResourceDesc desc, bool isTemp)
private Handle<GPUResource> TrackResource(D3D12MA_Allocation* allocation, D3D12_RESOURCE_STATES state, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp)
{
var handle = _resourceDatabase.AddResource(allocation, _fenceSynchronizer.CPUFenceValue, D3D12Utility.ToResourceState(state) , resourceDescriptor, desc);
var handle = _resourceDatabase.AddAllocation(allocation, _fenceSynchronizer.CPUFenceValue, D3D12Utility.ToResourceState(state), resourceDescriptor, desc, name);
if (isTemp)
{
@@ -621,7 +635,70 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return handle;
}
public Handle<Texture> CreateTexture(ref readonly TextureDesc desc, bool isTemp = false)
private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, void** ppv)
{
var hr = S.S_OK;
var iid = IID.IID_NULL;
if (options.AllocationType == ResourceAllocationType.RenderGraphTransient)
{
// pAllocation should be the render graph Heap. ppvResource should be the out resource.
var result = _resourceDatabase.GetResourceRecord(options.Heap);
if (result.IsFailure)
{
return E.E_NOTFOUND;
}
hr = _d3d12MA.Get()->CreateAliasingResource(result.Value.resource.allocation.Get(), options.Offset, pResourceDesc, initialState, null, &iid, ppv);
}
else
{
hr = _d3d12MA.Get()->CreateResource(pAllocationDesc, pResourceDesc, initialState, null, (D3D12MA_Allocation**)ppv, &iid, null);
}
return hr;
}
public Handle<GPUResource> Allocate(ref readonly AllocationDesc desc, string name)
{
var allocDesc = new D3D12MA_ALLOCATION_DESC
{
HeapType = desc.HeapType switch
{
HeapType.Default => D3D12_HEAP_TYPE_DEFAULT,
HeapType.Upload => D3D12_HEAP_TYPE_UPLOAD,
HeapType.Readback => D3D12_HEAP_TYPE_READBACK,
_ => D3D12_HEAP_TYPE_DEFAULT
},
Flags = D3D12MA_ALLOCATION_FLAG_COMMITTED,
ExtraHeapFlags = desc.HeapFlags switch
{
HeapFlags.None => D3D12_HEAP_FLAG_NONE,
HeapFlags.AllowBuffers => D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS,
HeapFlags.AllowTextures => D3D12_HEAP_FLAG_ALLOW_ONLY_NON_RT_DS_TEXTURES,
HeapFlags.AllowRTAndDS => D3D12_HEAP_FLAG_ALLOW_ONLY_RT_DS_TEXTURES,
HeapFlags.AlowBufferAndTexture => D3D12_HEAP_FLAG_ALLOW_ALL_BUFFERS_AND_TEXTURES,
_ => D3D12_HEAP_FLAG_NONE
}
};
// SizeInBytes must be aligned to 64KB for committed resources
var allocInfo = new D3D12_RESOURCE_ALLOCATION_INFO
{
SizeInBytes = desc.Size + 65535 & ~65535u,
Alignment = desc.Alignment
};
D3D12MA_Allocation* alloc = default;
if (_d3d12MA.Get()->AllocateMemory(&allocDesc, &allocInfo, &alloc).FAILED)
{
return Handle<GPUResource>.Invalid;
}
return TrackResource(alloc, D3D12_RESOURCE_STATE_COMMON, ResourceViewGroup.Invalid, default, name, false);
}
public Handle<Texture> CreateTexture(ref readonly TextureDesc desc, string name, CreationOptions options = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -681,14 +758,17 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
var initialState = DetermineInitialTextureState(desc.Usage);
D3D12MA_Allocation* pAllocation = default;
var iid = IID.IID_NULL;
ThrowIfFailed(_d3d12MA.Get()->CreateResource(&allocationDesc, &resourceDesc, initialState, null, &pAllocation, &iid, null));
if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED)
{
return Handle<Texture>.Invalid;
}
var isTemp = options.AllocationType == ResourceAllocationType.Temporary;
var resourceDescriptor = ResourceViewGroup.Invalid;
if (desc.Usage.HasFlag(TextureUsage.ShaderResource))
{
resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(isTemp);
// TODO: Maybe use non-shader-visible descriptor first then batch copy to shader-visible heap later?
// TODO: Maybe use non-shader-visible descriptor first then batch copy to shader-visible Heap later?
var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.srv);
var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray;
@@ -724,20 +804,20 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
_device.NativeDevice.Get()->CreateUnorderedAccessView(pAllocation->GetResource(), null, &uavDesc, cpuHandle);
}
var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Texture(desc), isTemp);
var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Texture(desc), name, isTemp);
return handle.AsTexture();
}
public Handle<Texture> CreateRenderTarget(ref readonly RenderTargetDesc desc, bool isTemp = false)
public Handle<Texture> CreateRenderTarget(ref readonly RenderTargetDesc desc, string name, CreationOptions options = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var textureDesc = desc.ToTextureDescripton();
return CreateTexture(in textureDesc, isTemp);
return CreateTexture(in textureDesc, name, options);
}
public Handle<GraphicsBuffer> CreateBuffer(ref readonly BufferDesc desc, bool isTemp = false)
public Handle<GraphicsBuffer> CreateBuffer(ref readonly BufferDesc desc, string name, CreationOptions options = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
CheckBufferSize(desc.Size);
@@ -749,21 +829,24 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
alignedSize = (uint)(desc.Size + 255) & ~255u;
}
var resourceDescription = D3D12_RESOURCE_DESC.Buffer(alignedSize, ConvertBufferUsage(desc.Usage));
var resourceDesc = D3D12_RESOURCE_DESC.Buffer(alignedSize, ConvertBufferUsage(desc.Usage));
var isRaw = desc.Usage.HasFlag(BufferUsage.Raw);
var allocationDesc = new D3D12MA_ALLOCATION_DESC
{
HeapType = ConvertMemoryType(desc.MemoryType),
Flags = D3D12MA_ALLOCATION_FLAG_NONE
Flags = D3D12MA_ALLOCATION_FLAG_NONE,
};
var initialState = DetermineInitialBufferState(desc.Usage, desc.MemoryType);
D3D12MA_Allocation* pAllocation = default;
var iid = IID.IID_NULL;
ThrowIfFailed(_d3d12MA.Get()->CreateResource(&allocationDesc, &resourceDescription, initialState, null, &pAllocation, &iid, null));
if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED)
{
return Handle<GraphicsBuffer>.Invalid;
}
var isTemp = options.AllocationType == ResourceAllocationType.Temporary;
var resourceDescriptor = ResourceViewGroup.Invalid;
var pResource = pAllocation->GetResource();
@@ -798,22 +881,35 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
_device.NativeDevice.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
}
var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Buffer(desc), isTemp);
var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Buffer(desc), name, isTemp);
return handle.AsGraphicsBuffer();
}
public Handle<GraphicsBuffer> CreateUploadBuffer(ulong size, bool isTemp = true)
public Handle<GraphicsBuffer> CreateTempUploadBuffer(ulong sizeInBytes, out ulong offset)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var desc = new BufferDesc
if (sizeInBytes <= _MAX_RESOURCE_SIZE_TO_FIT_IN_UPLOAD_BATCH && sizeInBytes + _uploadBatchOffset <= _UPLOAD_BATCH_SIZE)
{
Size = size,
Usage = BufferUsage.Upload,
MemoryType = ResourceMemoryType.Upload,
};
offset = _uploadBatchOffset;
_uploadBatchOffset += sizeInBytes;
return _uploadBatch;
}
else
{
var bufferDesc = new BufferDesc
{
Size = (uint)sizeInBytes,
Usage = BufferUsage.Upload,
MemoryType = ResourceMemoryType.Upload,
};
return CreateBuffer(in desc, isTemp);
var options = new CreationOptions
{
AllocationType = ResourceAllocationType.Temporary,
};
offset = 0;
return CreateBuffer(in bufferDesc, "TempUploadBuffer", options);
}
}
public Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc)
@@ -873,9 +969,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
MemoryType = ResourceMemoryType.Default,
};
var vertexBuffer = CreateBuffer(in vertexBufferDesc);
var indexBuffer = CreateBuffer(in indexBufferDesc);
var objectBuffer = CreateBuffer(in objectBufferDesc);
var vertexBuffer = CreateBuffer(in vertexBufferDesc, "VertexBuffer");
var indexBuffer = CreateBuffer(in indexBufferDesc, "IndexBuffer");
var objectBuffer = CreateBuffer(in objectBufferDesc, "ObjectBuffer");
var data = new Mesh
{
@@ -935,6 +1031,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
_resourceDatabase.ReleaseResource(handle);
_tempResources.Dequeue();
}
_uploadBatchOffset = 0;
}
public void Dispose()
@@ -951,6 +1049,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
_resourceDatabase.ReleaseResource(handle);
}
_resourceDatabase.ReleaseResource(_uploadBatch.AsResource());
_d3d12MA.Dispose();
_tempResources.Dispose();

View File

@@ -36,17 +36,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
public ResourceDesc desc;
public ResourceViewGroup viewGroup;
public ResourceUnion resourceUnion;
public ResourceUnion resource;
public ResourceState state;
public uint cpuFenceValue;
public readonly bool isExternal;
public readonly bool Allocated => isExternal ? resourceUnion.resource.Get() != null : resourceUnion.allocation.Get() != null;
public readonly SharedPtr<ID3D12Resource> ResourcePtr => isExternal ? resourceUnion.resource.Get() : resourceUnion.allocation.Get()->GetResource();
public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null;
public readonly SharedPtr<ID3D12Resource> ResourcePtr => isExternal ? resource.resource.Get() : resource.allocation.Get()->GetResource();
public ResourceRecord(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState state, ResourceViewGroup resourceDescriptor, ResourceDesc desc)
{
this.resourceUnion = new ResourceUnion(allocation);
this.resource = new ResourceUnion(allocation);
this.isExternal = false;
this.viewGroup = resourceDescriptor;
@@ -57,7 +57,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
public ResourceRecord(ID3D12Resource* resource, ResourceState state, ResourceViewGroup viewGroup)
{
this.resourceUnion = new ResourceUnion(resource);
this.resource = new ResourceUnion(resource);
this.isExternal = true;
this.viewGroup = viewGroup;
@@ -73,17 +73,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
{
if (isExternal)
{
refCount = resourceUnion.resource.Get()->Release();
refCount = resource.resource.Get()->Release();
}
else
{
refCount = resourceUnion.allocation.Get()->Release();
refCount = resource.allocation.Get()->Release();
}
}
descriptorAllocator.Release(viewGroup);
resourceUnion = default;
resource = default;
viewGroup = default;
return refCount;
@@ -116,7 +116,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
_shaders = new DynamicArray<Shader>(16);
// _shaderPasses = new UnsafeHashMap<ShaderPassKey, ShaderPass>(32, Allocator.Persistent);
}
~D3D12ResourceDatabase()
@@ -149,7 +148,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
return handle;
}
public unsafe Handle<GPUResource> AddResource(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState initialState, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
public unsafe Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState initialState, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -160,6 +159,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
if (!string.IsNullOrEmpty(name))
{
allocation->SetName(name);
allocation->GetResource()->SetName(name);
_resourceName[handle] = name;
}
#endif
@@ -475,7 +475,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
_samplers.Dispose();
_meshes.Dispose();
_materials.Dispose();
// _shaderPasses.Dispose();
_disposed = true;

View File

@@ -11,7 +11,6 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Aliases.DXGI_Alias;
namespace Ghost.Graphics.D3D12;
@@ -71,7 +70,8 @@ internal unsafe class D3D12SwapChain : ISwapChain
CreateBackBuffers();
SetScale(desc.ScaleX, desc.ScaleY);
_compositionSurface = desc.Target.CompositionSurface;
if (desc.Target.Type == SwapChainTargetType.Composition)
_compositionSurface = desc.Target.CompositionSurface;
}
~D3D12SwapChain()
@@ -106,12 +106,12 @@ internal unsafe class D3D12SwapChain : ISwapChain
case SwapChainTargetType.Composition:
ThrowIfFailed(pFactory->CreateSwapChainForComposition((IUnknown*)pCommandQueue, &swapChainDesc, null, &pTempSwapChain));
// Set the composition surface
if (desc.Target.CompositionSurface != null)
{
using var swapChainPanelNative = ISwapChainPanelNative.FromSwapChainPanel(desc.Target.CompositionSurface);
swapChainPanelNative.SetSwapChain((IntPtr)pTempSwapChain);
using var compositionSurface = ISwapChainPanelNative.FromSwapChainPanel(desc.Target.CompositionSurface);
compositionSurface.SetSwapChain((nint)pTempSwapChain);
}
break;
case SwapChainTargetType.WindowHandle:
@@ -213,7 +213,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
var inverseScaleX = 1.0f / scaleX;
var inverseScaleY = 1.0f / scaleY;
DXGI_MATRIX_3X2_F inverseScaleMatrix = new DXGI_MATRIX_3X2_F
var inverseScaleMatrix = new DXGI_MATRIX_3X2_F
{
_11 = inverseScaleX, // Scale X
_22 = inverseScaleY, // Scale Y
@@ -238,8 +238,8 @@ internal unsafe class D3D12SwapChain : ISwapChain
if (_compositionSurface != null)
{
using var panelNative = ISwapChainPanelNative.FromSwapChainPanel(_compositionSurface);
panelNative.SetSwapChain(IntPtr.Zero);
using var compositionSurface = ISwapChainPanelNative.FromSwapChainPanel(_compositionSurface);
compositionSurface.SetSwapChain(0);
}
for (var i = 0; i < _backBuffers.Count; i++)

View File

@@ -79,62 +79,62 @@ internal unsafe static class D3D12Utility
{
var d3dStates = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_COMMON;
if (state.HasFlag(ResourceState.VertexAndConstantBuffer))
if ((state & ResourceState.VertexAndConstantBuffer) == ResourceState.VertexAndConstantBuffer)
{
d3dStates |= D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER;
}
if (state.HasFlag(ResourceState.IndexBuffer))
if ((state & ResourceState.IndexBuffer) == ResourceState.IndexBuffer)
{
d3dStates |= D3D12_RESOURCE_STATE_INDEX_BUFFER;
}
if (state.HasFlag(ResourceState.RenderTarget))
if ((state & ResourceState.RenderTarget) == ResourceState.RenderTarget)
{
d3dStates |= D3D12_RESOURCE_STATE_RENDER_TARGET;
}
if (state.HasFlag(ResourceState.UnorderedAccess))
if ((state & ResourceState.UnorderedAccess) == ResourceState.UnorderedAccess)
{
d3dStates |= D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
}
if (state.HasFlag(ResourceState.DepthWrite))
if ((state & ResourceState.DepthWrite) == ResourceState.DepthWrite)
{
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_WRITE;
}
if (state.HasFlag(ResourceState.DepthRead))
if ((state & ResourceState.DepthRead) == ResourceState.DepthRead)
{
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_READ;
}
if (state.HasFlag(ResourceState.PixelShaderResource))
if ((state & ResourceState.PixelShaderResource) == ResourceState.PixelShaderResource)
{
d3dStates |= D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
}
if (state.HasFlag(ResourceState.CopyDest))
if ((state & ResourceState.CopyDest) == ResourceState.CopyDest)
{
d3dStates |= D3D12_RESOURCE_STATE_COPY_DEST;
}
if (state.HasFlag(ResourceState.CopySource))
if ((state & ResourceState.CopySource) == ResourceState.CopySource)
{
d3dStates |= D3D12_RESOURCE_STATE_COPY_SOURCE;
}
if (state.HasFlag(ResourceState.GenericRead))
if ((state & ResourceState.GenericRead) == ResourceState.GenericRead)
{
d3dStates |= D3D12_RESOURCE_STATE_GENERIC_READ;
}
if (state.HasFlag(ResourceState.IndirectArgument))
if ((state & ResourceState.IndirectArgument) == ResourceState.IndirectArgument)
{
d3dStates |= D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT;
}
if (state.HasFlag(ResourceState.NonPixelShaderResource))
if ((state & ResourceState.NonPixelShaderResource) == ResourceState.NonPixelShaderResource)
{
d3dStates |= D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
}

View File

@@ -224,6 +224,22 @@ public struct PassRenderTargetDesc
get; set;
}
/// <summary>
/// Specifies how to load the render target at the start of the render pass.
/// </summary>
public AttachmentLoadOp LoadOp
{
get; set;
}
/// <summary>
/// Specifies how to store the render target at the end of the render pass.
/// </summary>
public AttachmentStoreOp StoreOp
{
get; set;
}
}
public struct PassDepthStencilDesc
@@ -243,6 +259,38 @@ public struct PassDepthStencilDesc
get; set;
}
/// <summary>
/// Specifies how to load the depth buffer at the start of the render pass.
/// </summary>
public AttachmentLoadOp DepthLoadOp
{
get; set;
}
/// <summary>
/// Specifies how to store the depth buffer at the end of the render pass.
/// </summary>
public AttachmentStoreOp DepthStoreOp
{
get; set;
}
/// <summary>
/// Specifies how to load the stencil buffer at the start of the render pass.
/// </summary>
public AttachmentLoadOp StencilLoadOp
{
get; set;
}
/// <summary>
/// Specifies how to store the stencil buffer at the end of the render pass.
/// </summary>
public AttachmentStoreOp StencilStoreOp
{
get; set;
}
}
@@ -704,7 +752,7 @@ public struct SwapChainTarget
{
Type = SwapChainTargetType.WindowHandle,
WindowHandle = hwnd,
CompositionSurface = null
CompositionSurface = 0
};
}
@@ -878,3 +926,42 @@ public enum ComparisonFunction
GreaterEqual,
Always
}
/// <summary>
/// Specifies how to load attachment contents at the start of a render pass.
/// </summary>
public enum AttachmentLoadOp
{
/// <summary>
/// Load existing contents from memory. Use when you need to preserve previous data.
/// </summary>
Load,
/// <summary>
/// Clear the attachment to a specified value. Use when you want to start with a clean slate.
/// </summary>
Clear,
/// <summary>
/// Don't care about previous contents. Use when you'll overwrite all pixels (fullscreen pass).
/// On tile-based deferred renderers (TBDR), this can save significant memory bandwidth.
/// </summary>
DontCare
}
/// <summary>
/// Specifies how to store attachment contents at the end of a render pass.
/// </summary>
public enum AttachmentStoreOp
{
/// <summary>
/// Store the contents to memory for later use.
/// </summary>
Store,
/// <summary>
/// Discard the contents (not needed after this pass).
/// On tile-based deferred renderers (TBDR), this can save significant memory bandwidth.
/// </summary>
DontCare
}

View File

@@ -118,7 +118,7 @@ public interface ICommandBuffer : IDisposable
/// </summary>
/// <param name="slot">The vertex buffer slot to bind to.</param>
/// <param name="buffer">The handle to the graphics buffer containing vertex data.</param>
/// <param name="offset">The offset in bytes from the start of the buffer.</param>
/// <param name="offset">The Offset in bytes from the start of the buffer.</param>
void SetVertexBuffer(uint slot, Handle<GraphicsBuffer> buffer, ulong offset = 0);
/// <summary>
@@ -126,7 +126,7 @@ public interface ICommandBuffer : IDisposable
/// </summary>
/// <param name="buffer">The handle to the graphics buffer containing index data.</param>
/// <param name="type">The space of indices (e.g., 16-bit or 32-bit).</param>
/// <param name="offset">The offset in bytes from the start of the buffer.</param>
/// <param name="offset">The Offset in bytes from the start of the buffer.</param>
void SetIndexBuffer(Handle<GraphicsBuffer> buffer, IndexType type, ulong offset = 0);
/// <summary>
@@ -140,7 +140,7 @@ public interface ICommandBuffer : IDisposable
/// </summary>
/// <param name="rootIndex">The zero-based index of the root parameter in the graphics root signature to set the constant for.</param>
/// <param name="constantBuffer">A read-only span containing the 32-bit constant values to set.</param>
/// <param name="offsetIn32Bits">The offset, in 32-bit values, from the start of the root parameter where the constants will be set.</param>
/// <param name="offsetIn32Bits">The Offset, in 32-bit values, from the start of the root parameter where the constants will be set.</param>
void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan<uint> constantBuffer, uint offsetIn32Bits = 0);
/// <summary>
@@ -209,8 +209,8 @@ public interface ICommandBuffer : IDisposable
/// </summary>
/// <param name="dest">The handle to the destination graphics buffer where data will be written. Cannot be null.</param>
/// <param name="src">The handle to the source graphics buffer from which data will be read. Cannot be null.</param>
/// <param name="destOffset">The byte offset in the destination buffer at which to begin writing. Must be zero or greater.</param>
/// <param name="srcOffset">The byte offset in the source buffer at which to begin reading. Must be zero or greater.</param>
/// <param name="destOffset">The byte Offset in the destination buffer at which to begin writing. Must be zero or greater.</param>
/// <param name="srcOffset">The byte Offset in the source buffer at which to begin reading. Must be zero or greater.</param>
/// <param name="numBytes">The number of bytes to copy. If zero, copies the remaining bytes from the source buffer starting at <paramref name="srcOffset"/>.</param>
void CopyBuffer(Handle<GraphicsBuffer> dest, Handle<GraphicsBuffer> src, ulong destOffset = 0, ulong srcOffset = 0, ulong numBytes = 0);
}

View File

@@ -10,6 +10,7 @@ public enum FeatureSupport
SamplerFeedback = 1 << 3,
BindlessResources = 1 << 4,
WorkGraphs = 1 << 5,
AliasBuffersAndTextures = 1 << 6,
}
/// <summary>

View File

@@ -1,32 +1,121 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Misaki.HighPerformance.LowLevel.Collections;
using Ghost.Graphics.Core;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.RHI;
public enum ResourceAllocationType
{
Default,
Temporary,
RenderGraphTransient,
}
public struct CreationOptions
{
public ResourceAllocationType AllocationType
{
get; set;
}
public Handle<GPUResource> Heap
{
get; set;
}
public ulong Offset
{
get; set;
}
}
public enum HeapType
{
Default,
Upload,
Readback
}
public enum HeapFlags
{
None = 0,
AllowBuffers,
AllowTextures,
AllowRTAndDS,
AlowBufferAndTexture,
}
public struct AllocationDesc
{
public ulong Size
{
get; set;
}
public ulong Alignment
{
get; set;
}
public HeapType HeapType
{
get; set;
}
public HeapFlags HeapFlags
{
get; set;
}
}
public interface IResourceAllocator : IDisposable
{
/// <summary>
/// Allocates a block of memory on the GPU
/// </summary>
/// <param name="desc">Allocation description</param>
/// <param name="name">Debug name of the allocation</param>
/// <returns>An <see cref="Handle{GPUResource}"/> point to the allocated memory</returns>
Handle<GPUResource> Allocate(ref readonly AllocationDesc desc, string name);
/// <summary>
/// Creates a texture resource
/// </summary>
/// <param name="desc">Texture description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{Texture}"/> point to the resource</returns>
Handle<Texture> CreateTexture(ref readonly TextureDesc desc, bool tempResource = false);
Handle<Texture> CreateTexture(ref readonly TextureDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a render Target for off-screen rendering
/// </summary>
/// <param name="desc">Render Target description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{Texture}"/> point to the resource</returns>
Handle<Texture> CreateRenderTarget(ref readonly RenderTargetDesc desc, bool tempResource = false);
Handle<Texture> CreateRenderTarget(ref readonly RenderTargetDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a buffer resource
/// </summary>
/// <param name="desc">Buffer description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{GraphicsBuffer}"/> point to the resource</returns>
Handle<GraphicsBuffer> CreateBuffer(ref readonly BufferDesc desc, bool tempResource = false);
Handle<GraphicsBuffer> CreateBuffer(ref readonly BufferDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a temporary upload buffer of the specified size in bytes.
/// </summary>
/// <remarks>
/// This method has been optimized for frequent calls during frame updates. It efficiently manages memory to minimize fragmentation and overhead.
/// </remarks>
/// <param name="sizeInBytes">The size of the upload buffer to create, in bytes.</param>
/// <param name="offset">The offset within the upload buffer where the allocation begins.</param>
/// <returns>An <see cref="Handle{GraphicsBuffer}"/> pointing to the created upload buffer.</returns>
Handle<GraphicsBuffer> CreateTempUploadBuffer(ulong sizeInBytes, out ulong offset);
/// <summary>
/// Creates a new sampler object using the specified sampler description.

View File

@@ -49,7 +49,7 @@ public interface ISwapChain : IDisposable
/// <summary>
/// Gets all back buffer textures
/// </summary>
/// <returns>All back buffer textures</returns>
/// <returns>AlowBufferAndTexture back buffer textures</returns>
ReadOnlySpan<Handle<Texture>> GetBackBuffers();
/// <summary>

View File

@@ -157,7 +157,7 @@ internal class MeshRenderPass : IRenderPass
Usage = TextureUsage.ShaderResource,
};
_textures[i] = ctx.CreateTexture(in desc, imageData.AsSpan());
_textures[i] = ctx.CreateTexture(in desc, imageData.AsSpan(), $"Texture_{i}");
}
var samplerDesc = new SamplerDesc
@@ -182,13 +182,9 @@ internal class MeshRenderPass : IRenderPass
tex_sampler = (uint)sampler.Value,
};
Debug.Assert(matRef.SetPropertyCache(in matProps) == ErrorStatus.None);
matRef.SetPropertyCache(in matProps).ThrowIfFailed();
matRef.UploadData(ctx.DirectCommandBuffer);
var pso = matRef.GetPassPipelineOverride(0);
pso.Cull = Cull.Back;
matRef.SetPassPipelineOverride(0, in pso);
_forwardPassID = Shader.GetPassID("Forward");
}

View File

@@ -264,8 +264,12 @@ internal class RenderSystem : IRenderSystem
var newSize = kvp.Value;
swapChain.Resize(newSize.x, newSize.y);
}
_resizeRequest.Clear();
}
frameResource.CommandAllocator.Reset();
var r = _graphicsEngine.RenderFrame(frameResource.CommandAllocator);
if (r.IsFailure)
{

View File

@@ -12,18 +12,20 @@ public class RenderGraphBenchmark
public void Setup()
{
_renderGraph = new RenderGraph();
// Warm up
ExecuteGraph(_renderGraph);
}
[Benchmark]
public void Execute()
{
_renderGraph.Reset();
ExecuteGraph(_renderGraph);
}
public static void ExecuteGraph(RenderGraph renderGraph)
public static void ExecuteGraph(RenderGraph renderGraph, int idx = 0)
{
renderGraph.Reset(); // new RenderGraph()
// Import external resources
var backbuffer = renderGraph.ImportTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
@@ -88,6 +90,7 @@ public class RenderGraphBenchmark
// ===== SSAO Pass (Async Compute) =====
Identifier<RGTexture> ssaoOutput;
Identifier<RGBuffer> ssaoBufferOutput;
using (var builder = renderGraph.AddComputeRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
{
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
@@ -99,6 +102,9 @@ public class RenderGraphBenchmark
ssaoOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
ssaoData.OutputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Write);
ssaoBufferOutput = builder.CreateBuffer(
new BufferDescriptor(1920 * 1080 * 4, sizeof(byte), BufferUsage.UnorderedAccess, "SSAO.Buffer"));
ssaoData.OutputSSAOBuffer = builder.UseBuffer(ssaoBufferOutput, AccessFlags.WriteAll);
builder.EnableAsyncCompute(true);
@@ -122,6 +128,7 @@ public class RenderGraphBenchmark
bloomOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
builder.SetColorAttachment(bloomOutput, 0);
builder.UseBuffer(ssaoBufferOutput, AccessFlags.Read);
bloomData.Output = bloomOutput;
@@ -152,14 +159,25 @@ public class RenderGraphBenchmark
}
// ===== Post Processing Pass =====
using (var builder = renderGraph.AddRasterRenderPass<PostProcessingPassDataV2>("Post Processing", out var postData))
using (var builder = renderGraph.AddRasterRenderPass<PostProcessingPassDataV1>("Post Processing 1", out var postData))
{
postData.InputLighting = lightingOutput;
builder.SetColorAttachment(backbuffer, 0);
builder.SetRenderFunc<PostProcessingPassDataV1>(static (data, cmd) =>
{
cmd.BindShaderResource(data.InputLighting.AsResource(), 0);
cmd.Draw(3);
});
}
using (var builder = renderGraph.AddRasterRenderPass<PostProcessingPassDataV2>("Post Processing 2", out var postData))
{
postData.InputTAA = builder.UseTexture(taaOutput, AccessFlags.Read);
postData.InputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Read);
postData.InputBloom = builder.UseTexture(bloomOutput, AccessFlags.Read);
builder.SetColorAttachment(backbuffer, 0);
builder.SetRenderFunc<PostProcessingPassDataV2>(static (data, cmd) =>
{
cmd.BindShaderResource(data.InputTAA.AsResource(), 0);

View File

@@ -1,172 +0,0 @@
# Ghost Render Graph - Implementation Notes
## Overview
This is a transient render graph implementation for GhostEngine, inspired by Unity's render graph architecture. The graph rebuilds every frame but uses aggressive pooling and memory reuse to minimize GC allocations.
## Key Design Principles
### 1. **Object Pooling**
- All passes and resources are pooled via `RenderGraphObjectPool`
- Lists are reused across frames (Clear() instead of new)
- Pre-allocated capacity based on expected usage (64 passes, etc.)
### 2. **Minimal Allocations**
- Avoid LINQ - use explicit for loops
- Avoid foreach over interfaces - use indexed access
- Reuse collections by resetting count instead of clearing
- Pool all user data structures
### 3. **Transient Resources**
- Resources only live for the duration of the frame
- Resource lifetimes determined by pass dependencies
- Automatic culling of unused passes and resources
## Architecture
### Core Types
#### RenderGraphTextureHandle
Opaque handle to a texture resource. Contains index, version, and name.
#### RenderGraphPassBase & RenderGraphPass<TPassData>
- Base class for all passes
- Typed subclass holds user data and render functions
- Tracks resource dependencies (reads/writes/creates)
#### RenderGraphBuilder
- Fluent API for building passes
- IDisposable pattern for using() blocks
- Methods: CreateTexture, ReadTexture, WriteTexture, SetRenderFunc, etc.
#### RenderGraphResourceRegistry
- Manages all texture resources
- Tracks producers and consumers
- Provides pooled resource allocation
#### RenderGraphBlackboard
- Key-value store for sharing data between passes
- Type-safe Get<T>/Add<T> API
- Reused across frames
### Execution Flow
1. **Reset** - Clear previous frame data, return objects to pools
2. **Build** - Add passes and declare resource dependencies
3. **Compile** - Cull unused passes via dependency analysis
4. **Execute** - Run non-culled passes in order
### Pass Culling Algorithm
1. Mark all passes as culled initially (if AllowCulling = true)
2. Mark passes with side effects (write to imported resources) as not culled
3. Recursively un-cull all dependencies of non-culled passes
4. Result: Only passes that contribute to final output are executed
## Performance
**Current Results (Release build):**
- **Per iteration time:** 2,292 ns (~2.3 microseconds)
- **GC per iteration:** 571 bytes (after warmup)
**Comparison to Unity:**
- Unity first frame: ~700 KB
- Unity steady state: ~100 bytes
- Our implementation: ~571 bytes steady state
The 571 bytes likely comes from:
- String allocations in TextureDescriptor (40+ bytes each)
- Some residual closure captures
- Dictionary/List capacity adjustments
This is excellent performance for a complex graph with:
- 13 render passes
- 15+ texture resources
- Blackboard data sharing
- Pass culling
- Async compute support
## API Example
```csharp
var renderGraph = new RenderGraph();
// Reset for new frame
renderGraph.Reset();
// Import backbuffer
var backbuffer = renderGraph.ImportTexture("Backbuffer",
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
// Add a render pass
GBufferData gbufferData;
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
{
// Create transient textures
var albedo = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
// Mark dependencies
gbufferData.Albedo = builder.WriteTexture(albedo);
// Set render function
builder.SetRenderFunc<GBufferData>((data, cmd) =>
{
cmd.SetRenderTarget(data.Albedo.Name);
cmd.Draw(36000);
});
}
// Share data between passes
renderGraph.Blackboard.Add(gbufferData);
// Compile and execute
renderGraph.Compile();
renderGraph.Execute();
```
## Future Optimizations
1. **Use ArrayPool or stackalloc** for temporary allocations
2. **Intern strings** for resource names to avoid duplicates
3. **Use struct-based** TextureDescriptor to avoid heap allocations
4. **Pre-size collections** more accurately based on profiling
5. **Use native collections** (Unity.Collections) for zero-alloc operations
6. **Cache compiled graphs** across similar frames
## Files
- `RenderGraphTypes.cs` - Core handle and descriptor types
- `RenderGraphResourcePool.cs` - Object pooling and resource management
- `RenderGraphPass.cs` - Pass types and builder
- `RenderGraphContext.cs` - Execution contexts
- `RenderGraphBlackboard.cs` - Inter-pass data sharing
- `RenderGraph.cs` - Main graph class
- `PassData.cs` - Example pass data structures
- `Program.cs` - Test/example code
## Thread Safety
**NOT thread-safe.** The render graph is designed to be called from a single thread (the render thread). Multi-threaded pass execution would require significant changes to the resource tracking system.
## Limitations
1. No async/await support in render functions
2. No resource aliasing/reuse optimization yet
3. No render pass merging (could merge compatible passes)
4. Simple forward-only dependency tracking
5. No memory budgeting or OOM protection
## Differences from Unity
1. **Simpler API** - No multi-level builder hierarchy
2. **No native render pass support** - Could be added for tile-based GPUs
3. **No resource pooling** - Unity pools actual GPU resources
4. **No debug visualization** - Unity has render graph viewer
5. **Explicit type parameters** - Required due to C# lambda type inference
## Conclusion
This implementation demonstrates a production-ready transient render graph with excellent performance characteristics. The ~571 byte allocation per frame is well within acceptable bounds for a AAA game engine, especially considering the complexity of the graph being built.
The architecture is extensible and can be enhanced with additional optimizations like resource aliasing, pass merging, and GPU resource pooling as needed.

View File

@@ -25,6 +25,7 @@ public sealed class SSAOPassData : IPassData
public Identifier<RGTexture> GBufferDepth;
public Identifier<RGTexture> GBufferNormal;
public Identifier<RGTexture> OutputSSAO;
public Identifier<RGBuffer> OutputSSAOBuffer;
}
public sealed class BloomDownsampleData : IPassData
@@ -39,6 +40,12 @@ public sealed class TAAPassData : IPassData
public Identifier<RGTexture> OutputTAA;
}
public sealed class PostProcessingPassDataV1 : IPassData
{
public Identifier<RGTexture> InputLighting;
public Identifier<RGTexture> OutputBackbuffer;
}
public sealed class PostProcessingPassDataV2 : IPassData
{
public Identifier<RGTexture> InputTAA;

View File

@@ -11,11 +11,12 @@ const int _ITERATION = 500000;
for (var i = 0; i < _ITERATION; i++)
{
RenderGraphBenchmark.ExecuteGraph(renderGraph);
renderGraph.Reset();
}
GC.Collect();
GC.WaitForPendingFinalizers();
//Thread.Sleep(1000); // Leave a gap in visual studio allocations timeline
Thread.Sleep(1000); // Leave a gap in visual studio allocations timeline
var sw = new System.Diagnostics.Stopwatch();
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
sw.Start();
@@ -23,6 +24,7 @@ sw.Start();
for (var i = 0; i < _ITERATION; i++)
{
RenderGraphBenchmark.ExecuteGraph(renderGraph);
renderGraph.Reset();
}
sw.Stop();
@@ -37,6 +39,9 @@ var renderGraph = new RenderGraph();
Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ===");
RenderGraphBenchmark.ExecuteGraph(renderGraph);
Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
RenderGraphBenchmark.ExecuteGraph(renderGraph);
//Thread.Sleep(5000);
//renderGraph.Reset();
//Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
//RenderGraphBenchmark.ExecuteGraph(renderGraph);
#endif

View File

@@ -1,197 +0,0 @@
# GhostEngine Render Graph
A high-performance, transient render graph implementation for GhostEngine, inspired by Unity, Unreal, and other AAA engines.
## Features
**Transient Architecture** - Graph rebuilds every frame for maximum flexibility
**Minimal GC** - Only ~571 bytes allocated per frame (after warmup)
**Automatic Pass Culling** - Unused passes are automatically removed
**Resource Tracking** - Automatic resource lifetime management
**Blackboard Pattern** - Share data between passes easily
**Async Compute Support** - Mark passes for async execution
**Type-Safe API** - Strongly-typed pass data
## Performance
```
Per iteration time: 2,292 ns (~2.3 microseconds)
GC allocated: 571 bytes per iteration (after warmup)
```
Tested with a complex graph containing:
- 13 render passes (GBuffer, Lighting, SSAO, Bloom, TAA, Post-processing)
- 15+ transient textures
- Multiple read/write dependencies
- Blackboard data sharing
- Pass culling optimization
## Quick Start
```csharp
var renderGraph = new RenderGraph();
// Each frame:
renderGraph.Reset();
// Import external resources
var backbuffer = renderGraph.ImportTexture("Backbuffer",
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
// Add a pass
GBufferData gbufferData;
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
{
// Create transient textures
var albedo = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
// Mark resource usage
gbufferData.Albedo = builder.WriteTexture(albedo);
// Set the render function
builder.SetRenderFunc<GBufferData>((data, cmd) =>
{
cmd.SetRenderTarget(data.Albedo.Name);
cmd.Draw(36000);
});
}
// Share data with other passes
renderGraph.Blackboard.Add(gbufferData);
// Read from blackboard in another pass
using (var builder = renderGraph.AddRenderPass<LightingData>("Lighting", out var lightingData))
{
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
lightingData.Albedo = builder.ReadTexture(gbuffer.Albedo);
// ... rest of pass setup
}
// Compile and execute
renderGraph.Compile();
renderGraph.Execute();
```
## API Reference
### RenderGraph
**`void Reset()`**
Clears the graph for a new frame. Reuses allocations to minimize GC.
**`RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor desc)`**
Imports an external texture (like the backbuffer) into the graph.
**`RenderGraphBuilder AddRenderPass<TPassData>(string name, out TPassData data)`**
Adds a new render pass. Returns a builder for configuring the pass.
**`void Compile()`**
Analyzes dependencies and culls unused passes.
**`void Execute()`**
Executes all compiled (non-culled) passes.
**`RenderGraphBlackboard Blackboard { get; }`**
Access the blackboard for sharing data between passes.
### RenderGraphBuilder
**`RenderGraphTextureHandle CreateTexture(TextureDescriptor desc)`**
Creates a transient texture that only lives during this pass.
**`RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)`**
Marks a texture as read by this pass.
**`RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)`**
Marks a texture as written by this pass.
**`RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)`**
Sets up depth buffer usage for this pass.
**`void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> func)`**
Sets the raster render function for this pass.
**`void SetComputeFunc<TPassData>(Action<TPassData, ComputeRenderContext> func, bool asyncCompute = false)`**
Sets the compute function for this pass. Optionally mark as async.
**`void SetAllowCulling(bool allow)`**
Controls whether this pass can be culled if its outputs are unused.
### RenderGraphBlackboard
**`void Add<T>(T data) where T : class, IPassData`**
Stores pass data in the blackboard.
**`T Get<T>() where T : class, IPassData`**
Retrieves pass data from the blackboard.
**`bool TryGet<T>(out T data) where T : class, IPassData`**
Attempts to retrieve pass data from the blackboard.
## Architecture
The render graph uses several key patterns to achieve minimal GC:
1. **Object Pooling** - All passes and data structures are pooled and reused
2. **Collection Reuse** - Lists are cleared instead of reallocated
3. **Pre-allocation** - Capacity is pre-allocated based on expected usage
4. **Avoid LINQ** - Explicit loops instead of LINQ for zero allocation
5. **Struct Handles** - Resource handles are lightweight value types
### Pass Culling
The graph automatically removes unused passes:
1. Passes that write to imported resources have side effects (never culled)
2. All other passes are initially marked as culled
3. Dependencies of non-culled passes are recursively un-culled
4. Only passes contributing to the final output remain
This means you can freely add debug/visualization passes - they'll be automatically removed if unused.
## Building
```bash
dotnet build Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj -c Release
```
## Running Tests
```bash
dotnet run --project Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj -c Release
```
This runs 500 warmup iterations, then measures 500 more to determine average performance.
## Implementation Notes
See [IMPLEMENTATION_NOTES.md](IMPLEMENTATION_NOTES.md) for detailed architecture documentation.
## Limitations
- **Single-threaded** - Not thread-safe, designed for render thread only
- **No GPU resource pooling** - Currently uses mock command buffers
- **No render pass merging** - Compatible passes could be merged for better performance on tile-based GPUs
- **No resource aliasing** - Could reuse memory for non-overlapping resource lifetimes
## Future Enhancements
- [ ] Resource aliasing for memory efficiency
- [ ] Native render pass merging (for tile-based GPUs)
- [ ] GPU resource pooling
- [ ] Async/await support in render functions
- [ ] Memory budgeting and OOM protection
- [ ] Debug visualization (like Unity's Render Graph Viewer)
- [ ] Multi-threaded pass recording
- [ ] Graph caching across similar frames
## License
Part of GhostEngine. See repository root for license information.
## References
- Unity Render Graph: https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@latest
- Unreal RDG: https://docs.unrealengine.com/5.0/en-US/render-dependency-graph-in-unreal-engine/
- Frostbite Frame Graph: https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rendering-Architecture-in

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using TerraFX.Interop.Windows;
namespace Ghost.RenderGraph.Concept;
@@ -21,6 +22,7 @@ public sealed class RenderGraph
private readonly RenderGraphObjectPool _objectPool = new();
private readonly List<RenderGraphPassBase> _passes = new(64);
private readonly List<RenderGraphPassBase> _compiledPasses = new(64);
private readonly List<NativeRenderPass> _nativePasses = new(32);
private readonly RenderGraphBuilder _builder = new();
private readonly MockCommandBuffer _commandBuffer = new();
private readonly RenderContext _renderContext;
@@ -68,8 +70,16 @@ public sealed class RenderGraph
// Clear compiled passes list
_compiledPasses.Clear();
// Return native passes to pool
for (var i = 0; i < _nativePasses.Count; i++)
{
_objectPool.Return(_nativePasses[i]);
}
_nativePasses.Clear();
_compiled = false;
}
}
/// <summary>
/// Imports an external texture into the render graph.
@@ -78,6 +88,14 @@ public sealed class RenderGraph
{
return _resources.ImportTexture(descriptor);
}
/// <summary>
/// Imports an external buffer into the render graph.
/// </summary>
public Identifier<RGBuffer> ImportBuffer(BufferDescriptor descriptor)
{
return _resources.ImportBuffer(descriptor);
}
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
@@ -119,13 +137,13 @@ public sealed class RenderGraph
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
offset += sizeof(byte);
*(TextureFormat*)(pData + offset) = resource.descriptor.format;
*(TextureFormat*)(pData + offset) = resource.textureDescriptor.format;
offset += sizeof(TextureFormat);
*(int*)(pData + offset) = resource.descriptor.width;
*(int*)(pData + offset) = resource.textureDescriptor.width;
offset += sizeof(int);
*(int*)(pData + offset) = resource.descriptor.height;
*(int*)(pData + offset) = resource.textureDescriptor.height;
offset += sizeof(int);
return offset;
@@ -159,11 +177,17 @@ public sealed class RenderGraph
// Hash depth attachment
offset = ComputeTextureHash(pData, offset, pass.depthAccess.id);
pData[offset] = (byte)pass.depthAccess.accessFlags;
offset += sizeof(AccessFlags);
*(int*)(pData + offset) = pass.maxColorIndex;
offset += sizeof(int);
for (var j = 0; j <= pass.maxColorIndex; j++)
{
offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id);
pData[offset] = (byte)pass.colorAccess[j].accessFlags;
offset += sizeof(AccessFlags);
}
for (var j = 0; j < (int)RenderGraphResourceType.Count; j++)
@@ -195,27 +219,31 @@ public sealed class RenderGraph
*(int*)(pData + offset) = createList[k].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = pass.randomAccess.Count;
offset += sizeof(int);
for (var k = 0; k < pass.randomAccess.Count; k++)
{
*(int*)(pData + offset) = pass.randomAccess[k].Value;
offset += sizeof(int);
}
// Hash buffer hints (important for correct barrier generation)
*(int*)(pData + offset) = pass.bufferHints.Count;
offset += sizeof(int);
foreach (var kvp in pass.bufferHints)
{
*(int*)(pData + offset) = kvp.Key; // Buffer resource ID
offset += sizeof(int);
*(int*)(pData + offset) = (int)kvp.Value; // BufferHint flags
offset += sizeof(int);
}
}
*(int*)(pData + offset) = pass.GetRenderFuncHashCode();
offset += sizeof(int);
}
//// Hash resource descriptors
//for (var j = 0; j < _resources.TextureResourceCount; j++)
//{
// var resource = _resources.GetTextureResourceByIndex(j);
// *(int*)(pData + offset) = resource.descriptor.width;
// offset += sizeof(int);
// *(int*)(pData + offset) = resource.descriptor.height;
// offset += sizeof(int);
// *(TextureFormat*)(pData + offset) = resource.descriptor.format;
// offset += sizeof(TextureFormat);
// *(bool*)(pData + offset) = resource.isImported;
// offset += sizeof(bool);
//}
var span = new Span<byte>(pData, offset);
return XxHash64.HashToUInt64(span);
}
@@ -316,8 +344,11 @@ public sealed class RenderGraph
// Step 6: Generate barriers for state transitions and aliasing
GenerateBarriers();
// Step 7: Build native render passes by merging compatible passes
BuildNativeRenderPasses();
// Step 7: Store in cache for future frames
// Step 8: Store in cache for future frames
StoreInCache(graphHash);
_compiled = true;
@@ -343,7 +374,7 @@ public sealed class RenderGraph
}
// Restore aliasing mappings (need to update ResourceAliasingManager)
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.physicalResources);
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources);
// Restore barriers (deep copy to avoid shared references)
_barriers.Clear();
@@ -380,7 +411,7 @@ public sealed class RenderGraph
}
// Store aliasing mappings
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.physicalResources);
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources);
// Store barriers
for (var i = 0; i < _barriers.Count; i++)
@@ -476,65 +507,73 @@ public sealed class RenderGraph
}
/// <summary>
/// Inserts aliasing barriers when a physical resource is reused.
/// Inserts aliasing barriers when a placed resource is reused.
/// </summary>
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
{
// Check all resources written by this pass
for (var i = 0; i < pass.resourceWrites.Count; i++)
// Check all resources written by this pass (both textures and buffers)
for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
{
var id = pass.resourceWrites[i];
var resource = _resources.GetResource(id);
// Skip imported resources
if (resource.isImported)
continue;
// Check if this is the first use of this logical resource
if (resource.firstUsePass == pass.index)
var writeList = pass.resourceWrites[resType];
for (var i = 0; i < writeList.Count; i++)
{
// Rent the physical resource
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
if (physicalIndex >= 0)
var id = writeList[i];
var resource = _resources.GetResource(id);
// Skip imported resources
if (resource.isImported)
{
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
continue;
}
// If this physical resource has multiple aliased resources,
// we need an aliasing barrier when switching between them
if (physical != null && physical.aliasedLogicalResources.Count > 1)
// Check if this is the first use of this logical resource
if (resource.firstUsePass == pass.index)
{
// Get the placed resource
var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value);
if (placedIndex >= 0)
{
// Find the resource that used this physical memory most recently before this pass
Identifier<RGResource> resourceBefore = default;
var mostRecentLastUse = -1;
var placed = _aliasingManager.GetPlacedResource(placedIndex);
foreach (var otherLogicalIndex in physical.aliasedLogicalResources)
// If this placed resource has multiple aliased resources,
// we need an aliasing barrier when switching between them
if (placed != null && placed.aliasedLogicalResources.Count > 1)
{
if (otherLogicalIndex != id.Value)
// Find the resource that used this placed memory most recently before this pass
Identifier<RGResource> resourceBefore = default;
var mostRecentLastUse = -1;
foreach (var otherLogicalIndex in placed.aliasedLogicalResources)
{
var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts
if (otherResource.lastUsePass < pass.index &&
otherResource.lastUsePass > mostRecentLastUse)
if (otherLogicalIndex != id.Value)
{
mostRecentLastUse = otherResource.lastUsePass;
resourceBefore = otherLogicalIndex;
// Get resource by global index
var otherResource = _resources.GetResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts
if (otherResource.lastUsePass < pass.index &&
otherResource.lastUsePass > mostRecentLastUse)
{
mostRecentLastUse = otherResource.lastUsePass;
resourceBefore = new Identifier<RGResource>(otherLogicalIndex);
}
}
}
}
// If we found a previous resource, insert aliasing barrier
if (mostRecentLastUse >= 0)
{
var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore,
id,
passIdx
);
_barriers.Add(barrier);
// If we found a previous resource, insert aliasing barrier
if (mostRecentLastUse >= 0)
{
var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore,
id,
passIdx
);
_barriers.Add(barrier);
#if DEBUG
Console.WriteLine($" {barrier}");
Console.WriteLine($" {barrier}");
#endif
}
}
}
}
@@ -547,14 +586,15 @@ public sealed class RenderGraph
/// </summary>
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
{
// Process reads (transition to shader resource)
// Process reads (transition to appropriate state based on resource type and hints)
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
{
var readList = pass.resourceReads[i];
for (var j = 0; j < readList.Count; j++)
{
var handle = readList[j];
InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx);
var state = GetBufferReadState(handle, pass, (RenderGraphResourceType)i);
InsertTransitionIfNeeded(handle, state, passIdx);
}
}
@@ -623,7 +663,386 @@ public sealed class RenderGraph
}
/// <summary>
/// Executes all compiled passes.
/// Determines the appropriate resource state for a buffer read operation based on usage hints.
/// </summary>
private ResourceState GetBufferReadState(Identifier<RGResource> handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType)
{
// Textures always use ShaderResource state
if (resourceType == RenderGraphResourceType.Texture)
{
return ResourceState.ShaderResource;
}
// Check for buffer-specific usage hints
if (pass.bufferHints.TryGetValue(handle.Value, out var hint))
{
if (hint.HasFlag(BufferHint.IndirectArgument))
{
return ResourceState.IndirectArgument;
}
}
// Default: ByteAddressBuffer read (SRV) - matches bindless architecture
return ResourceState.ShaderResource;
}
/// <summary>
/// Builds native render passes by merging compatible consecutive raster passes.
/// Uses conservative merging: only merge passes with identical attachments and no barriers between them.
/// </summary>
private void BuildNativeRenderPasses()
{
// Clear previous native passes
for (var i = 0; i < _nativePasses.Count; i++)
{
_objectPool.Return(_nativePasses[i]);
}
_nativePasses.Clear();
NativeRenderPass? currentNativePass = null;
for (var i = 0; i < _compiledPasses.Count; i++)
{
var pass = _compiledPasses[i];
// Only raster passes can be merged into native render passes
// Compute passes break the current native render pass
if (pass.type != RenderPassType.Raster)
{
// Close current native pass if open
if (currentNativePass != null)
{
_nativePasses.Add(currentNativePass);
currentNativePass = null;
}
continue; // Compute passes execute outside native render passes
}
// Check if we can merge with current native pass
if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i))
{
// Merge into existing native pass
currentNativePass.mergedPassIndices.Add(i);
currentNativePass.lastLogicalPass = i;
}
else
{
// Start new native pass
if (currentNativePass != null)
{
_nativePasses.Add(currentNativePass);
}
currentNativePass = CreateNativePass(pass, i);
}
}
// Add final native pass
if (currentNativePass != null)
{
_nativePasses.Add(currentNativePass);
}
// Infer load/store operations for all native passes
for (var i = 0; i < _nativePasses.Count; i++)
{
InferLoadStoreOps(_nativePasses[i]);
}
#if DEBUG
Console.WriteLine("\n=== Native Render Passes ===");
Console.WriteLine($"Logical passes: {_compiledPasses.Count}");
Console.WriteLine($"Native passes: {_nativePasses.Count}");
for (var i = 0; i < _nativePasses.Count; i++)
{
var nativePass = _nativePasses[i];
Console.WriteLine($"\nNative Pass {i}:");
Console.WriteLine($" Merged passes: [{string.Join(", ", nativePass.mergedPassIndices)}]");
Console.WriteLine($" Color attachments: {nativePass.colorAttachmentCount}");
for (var j = 0; j < nativePass.colorAttachmentCount; j++)
{
Console.WriteLine($" [{j}] {nativePass.colorAttachments[j].texture}");
}
if (nativePass.hasDepthAttachment)
{
Console.WriteLine($" Depth attachment: {nativePass.depthAttachment.texture}");
}
}
Console.WriteLine("============================\n");
#endif
}
/// <summary>
/// Creates a new native render pass from a logical pass.
/// </summary>
private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex)
{
var nativePass = _objectPool.Rent<NativeRenderPass>();
nativePass.Reset();
nativePass.index = _nativePasses.Count;
nativePass.mergedPassIndices.Add(passIndex);
nativePass.firstLogicalPass = passIndex;
nativePass.lastLogicalPass = passIndex;
nativePass.allowUAVWrites = pass.randomAccess.Count > 0;
// Copy color attachments
nativePass.colorAttachmentCount = pass.maxColorIndex + 1;
for (var i = 0; i <= pass.maxColorIndex; i++)
{
nativePass.colorAttachments[i] = new RenderTargetInfo
{
texture = pass.colorAccess[i].id,
access = pass.colorAccess[i].accessFlags
};
}
// Copy depth attachment
if (!pass.depthAccess.id.IsInvalid)
{
nativePass.hasDepthAttachment = true;
nativePass.depthAttachment = new DepthStencilInfo
{
texture = pass.depthAccess.id,
access = pass.depthAccess.accessFlags
};
}
return nativePass;
}
/// <summary>
/// Checks if a logical pass can be merged into an existing native render pass.
/// Conservative merging: only merge if attachments match and no barriers needed.
/// </summary>
private bool CanMergePasses(NativeRenderPass nativePass, RenderGraphPassBase pass, int passIndex)
{
// Don't merge if UAVs are involved (conservative)
if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites)
{
return false;
}
// Check if attachment configuration matches
if (!AttachmentsMatch(nativePass, pass))
{
return false;
}
// Check if barriers are needed between last merged pass and this pass
if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex))
{
return false;
}
return true;
}
/// <summary>
/// Checks if the attachment configuration of a pass matches the native pass.
/// </summary>
private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass)
{
// Check color attachment count
if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1)
{
return false;
}
// Check each color attachment
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{
if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id)
{
return false;
}
}
// Check depth attachment
if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid)
{
return false;
}
if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id)
{
return false;
}
return true;
}
/// <summary>
/// Checks if any barriers are required between two passes that would prevent merging.
/// Only barriers affecting render targets prevent merging; SRV barriers are fine.
/// </summary>
private bool RequiresBarrierBetweenPasses(int passA, int passB)
{
var laterPass = _compiledPasses[passB];
// Build a set of render target resource IDs (color + depth)
var renderTargets = new HashSet<Identifier<RGResource>>();
for (var i = 0; i <= laterPass.maxColorIndex; i++)
{
if (!laterPass.colorAccess[i].id.IsInvalid)
{
renderTargets.Add(laterPass.colorAccess[i].id.AsResource());
}
}
if (!laterPass.depthAccess.id.IsInvalid)
{
renderTargets.Add(laterPass.depthAccess.id.AsResource());
}
// Check if any barriers for passB affect render targets
for (var i = 0; i < _barriers.Count; i++)
{
if (_barriers[i].PassIndex == passB)
{
// Only prevent merge if barrier affects a render target
if (renderTargets.Contains(_barriers[i].Resource))
{
return true; // Barrier affects render target, cannot merge
}
}
if (_barriers[i].PassIndex > passB)
{
break; // No more barriers for this pass
}
}
return false;
}
/// <summary>
/// Infers optimal load/store operations for all attachments in a native render pass.
/// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs).
/// </summary>
private void InferLoadStoreOps(NativeRenderPass nativePass)
{
// Infer load/store ops for color attachments
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{
ref var attachment = ref nativePass.colorAttachments[i];
var resource = _resources.GetResource(attachment.texture);
var flags = attachment.access;
// ===== LOAD OP INFERENCE =====
// 1. WriteAll (Write | Discard): User guarantees full overwrite
if (flags.HasFlag(AccessFlags.Discard))
{
attachment.loadOp = AttachmentLoadOp.DontCare;
#if DEBUG
Console.WriteLine($" Color[{i}] LoadOp=DontCare (WriteAll/Discard flag)");
#endif
}
// 2. Read: Needs existing contents (e.g., blending)
else if (flags.HasFlag(AccessFlags.Read))
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Color[{i}] LoadOp=Load (Read flag - blending)");
#endif
}
// 3. First use: Could use DontCare, but user didn't specify Discard flag
// Conservative: use Load to avoid bugs
else if (resource.firstUsePass == nativePass.firstLogicalPass)
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Color[{i}] LoadOp=Load (first use, Write flag - conservative)");
#endif
}
// 4. Continuation from previous pass
else
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Color[{i}] LoadOp=Load (continuation from previous pass)");
#endif
}
// ===== STORE OP INFERENCE =====
// Last use: No one needs it after this native pass
if (resource.lastUsePass == nativePass.lastLogicalPass)
{
attachment.storeOp = AttachmentStoreOp.DontCare;
#if DEBUG
Console.WriteLine($" Color[{i}] StoreOp=DontCare (last use - discard)");
#endif
}
// Intermediate: Store for future passes
else
{
attachment.storeOp = AttachmentStoreOp.Store;
#if DEBUG
Console.WriteLine($" Color[{i}] StoreOp=Store (used by later passes)");
#endif
}
}
// Infer load/store ops for depth attachment
if (nativePass.hasDepthAttachment)
{
ref var attachment = ref nativePass.depthAttachment;
var resource = _resources.GetResource(attachment.texture);
var flags = attachment.access;
// ===== LOAD OP INFERENCE =====
if (flags.HasFlag(AccessFlags.Discard))
{
attachment.loadOp = AttachmentLoadOp.DontCare;
#if DEBUG
Console.WriteLine($" Depth LoadOp=DontCare (WriteAll/Discard flag)");
#endif
}
else if (flags.HasFlag(AccessFlags.Read))
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Depth LoadOp=Load (Read flag)");
#endif
}
else if (resource.firstUsePass == nativePass.firstLogicalPass)
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Depth LoadOp=Load (first use, Write flag - conservative)");
#endif
}
else
{
attachment.loadOp = AttachmentLoadOp.Load;
#if DEBUG
Console.WriteLine($" Depth LoadOp=Load (continuation)");
#endif
}
// ===== STORE OP INFERENCE =====
// Depth is commonly discarded (depth-only passes, intermediate depth)
if (resource.lastUsePass == nativePass.lastLogicalPass)
{
attachment.storeOp = AttachmentStoreOp.DontCare;
#if DEBUG
Console.WriteLine($" Depth StoreOp=DontCare (last use)");
#endif
}
else
{
attachment.storeOp = AttachmentStoreOp.Store;
#if DEBUG
Console.WriteLine($" Depth StoreOp=Store (used later)");
#endif
}
}
}
/// <summary>
/// Executes all compiled passes using native render passes where possible.
/// </summary>
public void Execute()
{
@@ -632,55 +1051,111 @@ public sealed class RenderGraph
Compile();
}
// Execute each non-culled pass
var barrierIndex = 0;
for (var i = 0; i < _compiledPasses.Count; i++)
var nativePassIndex = 0;
var logicalPassIndex = 0;
while (logicalPassIndex < _compiledPasses.Count)
{
var pass = _compiledPasses[i];
// Execute all barriers for this pass
#if DEBUG
bool hasBarriers = false;
#endif
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == i)
var pass = _compiledPasses[logicalPassIndex];
// Check if this pass is part of a native render pass
if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count)
{
#if DEBUG
if (!hasBarriers)
var nativePass = _nativePasses[nativePassIndex];
// Execute barriers for ALL merged passes before beginning the native render pass
foreach (var mergedPassIdx in nativePass.mergedPassIndices)
{
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
hasBarriers = true;
ExecuteBarriersForPass(mergedPassIdx, ref barrierIndex);
}
// Begin native render pass
_commandBuffer.BeginRenderPass(
nativePass.index,
nativePass.colorAttachmentCount,
nativePass.hasDepthAttachment
);
var barrier = _barriers[barrierIndex];
if (barrier.Type == BarrierType.Transition)
// Execute all merged logical passes within this native render pass
for (var i = 0; i < nativePass.mergedPassIndices.Count; i++)
{
_commandBuffer.ResourceBarrier(
barrier.Resource,
barrier.StateBefore,
barrier.StateAfter
);
}
else if (barrier.Type == BarrierType.Aliasing)
{
_commandBuffer.AliasBarrier(
barrier.ResourceBefore,
barrier.ResourceAfter
);
}
var mergedPassIdx = nativePass.mergedPassIndices[i];
var mergedPass = _compiledPasses[mergedPassIdx];
#if DEBUG
Console.WriteLine($"\n--- Executing Pass {mergedPassIdx}: {mergedPass.name} (in Native Pass {nativePass.index}) ---");
#endif
// In a real implementation, you would execute the barrier here:
// ExecuteBarrier(_barriers[barrierIndex]);
barrierIndex++;
mergedPass.Execute(_renderContext);
logicalPassIndex++;
}
// End native render pass
_commandBuffer.EndRenderPass();
nativePassIndex++;
}
#if DEBUG
if (hasBarriers)
else
{
Console.WriteLine("=====================================\n");
}
// Compute pass or standalone raster pass (not merged)
ExecuteBarriersForPass(logicalPassIndex, ref barrierIndex);
#if DEBUG
Console.WriteLine($"\n--- Executing Pass {logicalPassIndex}: {pass.name} (Standalone) ---");
#endif
pass.Execute(_renderContext);
pass.Execute(_renderContext);
logicalPassIndex++;
}
}
}
/// <summary>
/// Executes all barriers for a specific pass.
/// </summary>
private void ExecuteBarriersForPass(int passIndex, ref int barrierIndex)
{
#if DEBUG
bool hasBarriers = false;
#endif
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex)
{
#if DEBUG
if (!hasBarriers)
{
var pass = _compiledPasses[passIndex];
Console.WriteLine($"\n=== Barriers before Pass {passIndex}: {pass.name} ===");
hasBarriers = true;
}
var barrier = _barriers[barrierIndex];
if (barrier.Type == BarrierType.Transition)
{
_commandBuffer.ResourceBarrier(
barrier.Resource,
barrier.StateBefore,
barrier.StateAfter
);
}
else if (barrier.Type == BarrierType.Aliasing)
{
_commandBuffer.AliasBarrier(
barrier.ResourceBefore,
barrier.ResourceAfter
);
}
#endif
// In a real implementation, you would execute the barrier here:
// ExecuteBarrier(_barriers[barrierIndex]);
barrierIndex++;
}
#if DEBUG
if (hasBarriers)
{
Console.WriteLine("=====================================\n");
}
#endif
}
}

View File

@@ -1,663 +0,0 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
using TerraFX.Interop.Windows;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Main render graph class that manages resource allocation and pass execution.
///
/// Design principles for minimal GC:
/// - Object pooling for all passes and resources
/// - Reuse collections across frames (Clear() instead of new)
/// - Avoid LINQ and foreach over interfaces
/// - Pre-allocate capacity based on expected usage
/// </summary>
public sealed class RenderGraph
{
private readonly RenderGraphResourceRegistry _resources = new();
private readonly RenderGraphObjectPool _objectPool = new();
private readonly List<RenderGraphPassBase> _passes = new(64);
private readonly List<RenderGraphPassBase> _compiledPasses = new(64);
private readonly RenderGraphBuilder _builder = new();
private readonly MockCommandBuffer _commandBuffer = new();
private readonly RenderContext _renderContext;
private readonly ResourceAliasingManager _aliasingManager = new();
private readonly Dictionary<int, ResourceState> _resourceStates = new(128);
private readonly List<ResourceBarrier> _barriers = new(128);
private readonly RenderGraphCompilationCache _compilationCache = new();
private bool _compiled;
public RenderGraphBlackboard Blackboard { get; } = new();
public RenderGraph()
{
_renderContext = new RenderContext(_commandBuffer);
}
/// <summary>
/// Resets the render graph for a new frame.
/// Reuses existing allocations to minimize GC.
/// </summary>
public void Reset()
{
// Clear blackboard data
Blackboard.Clear();
// Reset resources but keep allocations
_resources.BeginFrame();
// Reset aliasing manager
_aliasingManager.BeginFrame();
// Clear resource states and barriers
_resourceStates.Clear();
_barriers.Clear();
// Return passes to the pool and reset count
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
pass.Reset(_objectPool);
}
_passes.Clear();
// Clear compiled passes list
_compiledPasses.Clear();
_compiled = false;
}
/// <summary>
/// Imports an external texture into the render graph.
/// </summary>
public Identifier<RGTexture> ImportTexture(TextureDescriptor descriptor)
{
return _resources.ImportTexture(descriptor);
}
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var renderPass = _objectPool.Rent<RasterRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Raster);
passData = renderPass.passData;
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
public IComputeRenderGraphBuilder AddComputeRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var renderPass = _objectPool.Rent<ComputeRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Compute);
passData = renderPass.passData;
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
private unsafe int ComputeTextureHash(byte* pData, int offset, Identifier<RGTexture> texture)
{
if (texture.IsInvalid)
{
return offset;
}
var resource = _resources.GetResource(texture.AsResource());
// In real implementation, we typically need to handle imported resources differently.
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
offset += sizeof(byte);
*(TextureFormat*)(pData + offset) = resource.descriptor.format;
offset += sizeof(TextureFormat);
*(int*)(pData + offset) = resource.descriptor.width;
offset += sizeof(int);
*(int*)(pData + offset) = resource.descriptor.height;
offset += sizeof(int);
return offset;
}
private unsafe ulong ComputeGraphHash()
{
using var scope = AllocationManager.CreateStackScope();
var bufferPool = new UnsafeList<byte>(2048, scope.AllocationHandle);
var pData = (byte*)bufferPool.GetUnsafePtr();
var offset = 0;
// Hash pass count
*(int*)(pData + offset) = _passes.Count;
offset += sizeof(int);
// Hash each pass structure (excluding names)
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
*(RenderPassType*)(pData + offset) = pass.type;
offset += sizeof(RenderPassType);
*(bool*)(pData + offset) = pass.allowCulling;
offset += sizeof(bool);
*(bool*)(pData + offset) = pass.asyncCompute;
offset += sizeof(bool);
// Hash depth attachment
offset = ComputeTextureHash(pData, offset, pass.depthAccess.id);
*(int*)(pData + offset) = pass.maxColorIndex;
offset += sizeof(int);
for (var j = 0; j <= pass.maxColorIndex; j++)
{
offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id);
}
*(int*)(pData + offset) = pass.resourceReads.Count;
offset += sizeof(int);
for (var j = 0; j < pass.resourceReads.Count; j++)
{
*(int*)(pData + offset) = pass.resourceReads[j].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = pass.resourceWrites.Count;
offset += sizeof(int);
for (var j = 0; j < pass.resourceWrites.Count; j++)
{
*(int*)(pData + offset) = pass.resourceWrites[j].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = pass.resourceCreates.Count;
offset += sizeof(int);
for (var j = 0; j < pass.resourceCreates.Count; j++)
{
*(int*)(pData + offset) = pass.resourceCreates[j].Value;
offset += sizeof(int);
}
}
//// Hash resource descriptors
//for (var i = 0; i < _resources.TextureResourceCount; i++)
//{
// var resource = _resources.GetTextureResourceByIndex(i);
// *(int*)(pData + offset) = resource.descriptor.width;
// offset += sizeof(int);
// *(int*)(pData + offset) = resource.descriptor.height;
// offset += sizeof(int);
// *(TextureFormat*)(pData + offset) = resource.descriptor.format;
// offset += sizeof(TextureFormat);
// *(bool*)(pData + offset) = resource.isImported;
// offset += sizeof(bool);
//}
var span = new Span<byte>(pData, offset);
return XxHash64.HashToUInt64(span);
}
/// <summary>
/// Compiles the render graph by culling unused passes and determining resource lifetimes.
/// </summary>
public void Compile()
{
if (_compiled)
{
return;
}
#if DEBUG
var sw = System.Diagnostics.Stopwatch.StartNew();
#endif
// Step 0: Check cache
var graphHash = ComputeGraphHash(); // 17020363347016000737
#if DEBUG
var hashTime = sw.Elapsed.TotalMicroseconds;
#endif
if (_compilationCache.TryGetCached(graphHash, out var cached))
{
// CACHE HIT - restore from cache
#if DEBUG
Console.WriteLine($"\n[CACHE HIT] Hash: {graphHash:X16} (computed in {hashTime:F2}μs)");
#endif
RestoreFromCache(cached);
#if DEBUG
sw.Stop();
Console.WriteLine($"[CACHE HIT] Total restore time: {sw.Elapsed.TotalMicroseconds:F2}μs");
#endif
_compiled = true;
return;
}
#if DEBUG
Console.WriteLine($"\n[CACHE MISS] Hash: {graphHash:X16} (computed in {hashTime:F2}μs)");
#endif
_compiledPasses.Clear();
// Step 1: Mark passes with side effects (writes to imported resources)
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
// Check if this pass writes to any imported textures
for (var j = 0; j < pass.resourceWrites.Count; j++)
{
var writeHandle = pass.resourceWrites[j];
var resource = _resources.GetResource(writeHandle);
if (resource.isImported)
{
pass.hasSideEffects = true;
break;
}
}
}
// Step 2: Cull passes based on dependency analysis
// Mark all passes as culled initially
for (var i = 0; i < _passes.Count; i++)
{
_passes[i].culled = _passes[i].allowCulling && !_passes[i].hasSideEffects;
}
// Step 3: 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);
}
}
// Step 4: 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);
}
}
// Step 5: Perform resource aliasing to minimize memory usage
_aliasingManager.AssignPhysicalResources(_resources, _passes.Count);
// Step 6: Generate barriers for state transitions and aliasing
GenerateBarriers();
// Step 7: Store in cache for future frames
StoreInCache(graphHash);
_compiled = true;
}
/// <summary>
/// Restores the render graph state from cached compilation results.
/// </summary>
private void RestoreFromCache(CachedCompilation cached)
{
// Restore compiled pass list
_compiledPasses.Clear();
for (var i = 0; i < cached.compiledPassIndices.Count; i++)
{
var passIndex = cached.compiledPassIndices[i];
_compiledPasses.Add(_passes[passIndex]);
}
// Restore culling flags
for (var i = 0; i < _passes.Count && i < cached.passCulledFlags.Count; i++)
{
_passes[i].culled = cached.passCulledFlags[i];
}
// Restore aliasing mappings (need to update ResourceAliasingManager)
_aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.physicalResources);
// Restore barriers (deep copy to avoid shared references)
_barriers.Clear();
for (var i = 0; i < cached.barriers.Count; i++)
{
_barriers.Add(cached.barriers[i]);
}
// Restore resource states
_resourceStates.Clear();
foreach (var kvp in cached.resourceStates)
{
_resourceStates[kvp.Key] = kvp.Value;
}
}
/// <summary>
/// Stores current compilation results in the cache.
/// </summary>
private void StoreInCache(ulong graphHash)
{
var cacheData = new CachedCompilation();
// 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.physicalResources);
// Store barriers
for (var i = 0; i < _barriers.Count; i++)
{
cacheData.barriers.Add(_barriers[i]);
}
// Store resource states
foreach (var kvp in _resourceStates)
{
cacheData.resourceStates[kvp.Key] = kvp.Value;
}
_compilationCache.Store(graphHash, cacheData);
}
private void UnculProducer(Identifier<RGResource> resource)
{
var res = _resources.GetResource(resource);
if (res.producerPass >= 0)
{
var producer = _passes[res.producerPass];
if (producer.culled)
{
producer.culled = false;
UnculDependencies(producer);
}
}
}
private void UnculDependencies(RenderGraphPassBase pass)
{
// Un-cull producers of read resources
for (var i = 0; i < pass.resourceReads.Count; i++)
{
UnculProducer(pass.resourceReads[i]);
}
// Un-cull producers of color attachments
for (var i = 0; i <= pass.maxColorIndex; i++)
{
if (pass.colorAccess[i].id.IsValid)
{
UnculProducer(pass.colorAccess[i].id.AsResource());
}
}
// Un-cull producer of depth attachment
if (pass.depthAccess.id.IsValid)
{
UnculProducer(pass.depthAccess.id.AsResource());
}
// Un-cull producers of UAV resources (if not already in reads/writes)
for (var i = 0; i < pass.randomAccess.Count; i++)
{
UnculProducer(pass.randomAccess[i]);
}
}
/// <summary>
/// Generates resource barriers for state transitions and aliasing.
/// </summary>
private void GenerateBarriers()
{
_barriers.Clear();
_resourceStates.Clear();
#if DEBUG
Console.WriteLine("\n=== Barrier Generation ===");
#endif
// Process each compiled pass in order
for (var passIdx = 0; passIdx < _compiledPasses.Count; passIdx++)
{
var pass = _compiledPasses[passIdx];
// Insert aliasing barriers for resources that reuse physical memory
InsertAliasingBarriers(pass, passIdx);
// Insert transition barriers for state changes
InsertTransitionBarriers(pass, passIdx);
}
#if DEBUG
Console.WriteLine($"Total Barriers: {_barriers.Count}");
Console.WriteLine("==========================\n");
#endif
}
/// <summary>
/// Inserts aliasing barriers when a physical resource is reused.
/// </summary>
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
{
// Check all resources written by this pass
for (var i = 0; i < pass.resourceWrites.Count; i++)
{
var id = pass.resourceWrites[i];
var resource = _resources.GetResource(id);
// Skip imported resources
if (resource.isImported)
continue;
// Check if this is the first use of this logical resource
if (resource.firstUsePass == pass.index)
{
// Rent the physical resource
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
if (physicalIndex >= 0)
{
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
// If this physical resource has multiple aliased resources,
// we need an aliasing barrier when switching between them
if (physical != null && physical.aliasedLogicalResources.Count > 1)
{
// Find the resource that used this physical memory most recently before this pass
Identifier<RGResource> resourceBefore = default;
var mostRecentLastUse = -1;
foreach (var otherLogicalIndex in physical.aliasedLogicalResources)
{
if (otherLogicalIndex != id.Value)
{
var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts
if (otherResource.lastUsePass < pass.index &&
otherResource.lastUsePass > mostRecentLastUse)
{
mostRecentLastUse = otherResource.lastUsePass;
resourceBefore = otherLogicalIndex;
}
}
}
// If we found a previous resource, insert aliasing barrier
if (mostRecentLastUse >= 0)
{
var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore,
id,
passIdx
);
_barriers.Add(barrier);
#if DEBUG
Console.WriteLine($" {barrier}");
#endif
}
}
}
}
}
}
/// <summary>
/// Inserts transition barriers when a resource changes state.
/// </summary>
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
{
// Process reads (transition to shader resource)
for (var i = 0; i < pass.resourceReads.Count; i++)
{
var handle = pass.resourceReads[i];
InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx);
}
switch (pass.type)
{
case RenderPassType.Raster:
for (var i = 0; i <= pass.maxColorIndex; i++)
{
var access = pass.colorAccess[i];
InsertTransitionIfNeeded(access.id.AsResource(), ResourceState.RenderTarget, passIdx);
}
if (pass.depthAccess.id.IsValid)
{
var depthAccess = pass.depthAccess;
InsertTransitionIfNeeded(depthAccess.id.AsResource(), ResourceState.DepthWrite, passIdx);
}
for (var i = 0; i < pass.randomAccess.Count; i++)
{
InsertTransitionIfNeeded(pass.randomAccess[i], ResourceState.UnorderedAccess, passIdx);
}
break;
case RenderPassType.Compute:
for (var i = 0; i < pass.resourceWrites.Count; i++)
{
var id = pass.resourceWrites[i];
InsertTransitionIfNeeded(id, ResourceState.UnorderedAccess, passIdx);
}
break;
}
}
/// <summary>
/// Inserts a transition barrier if the resource state changes.
/// </summary>
private void InsertTransitionIfNeeded(Identifier<RGResource> resource, ResourceState newState, int passIdx)
{
if (!_resourceStates.TryGetValue(resource.Value, out var currentState))
{
// First time seeing this resource, assume undefined
currentState = ResourceState.Common;
}
if (currentState != newState)
{
var barrier = ResourceBarrier.CreateTransitionBarrier(
resource,
currentState,
newState,
passIdx
);
_barriers.Add(barrier);
_resourceStates[resource.Value] = newState;
#if DEBUG
Console.WriteLine($" {barrier}");
#endif
}
}
/// <summary>
/// Executes all compiled passes.
/// </summary>
public void Execute()
{
if (!_compiled)
{
Compile();
}
// Execute each non-culled pass
var barrierIndex = 0;
for (var i = 0; i < _compiledPasses.Count; i++)
{
var pass = _compiledPasses[i];
// Execute all barriers for this pass
#if DEBUG
bool hasBarriers = false;
#endif
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == i)
{
#if DEBUG
if (!hasBarriers)
{
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
hasBarriers = true;
}
var barrier = _barriers[barrierIndex];
if (barrier.Type == BarrierType.Transition)
{
_commandBuffer.ResourceBarrier(
barrier.Resource,
barrier.StateBefore,
barrier.StateAfter
);
}
else if (barrier.Type == BarrierType.Aliasing)
{
_commandBuffer.AliasBarrier(
barrier.ResourceBefore,
barrier.ResourceAfter
);
}
#endif
// In a real implementation, you would execute the barrier here:
// ExecuteBarrier(_barriers[barrierIndex]);
barrierIndex++;
}
#if DEBUG
if (hasBarriers)
{
Console.WriteLine("=====================================\n");
}
#endif
pass.Execute(_renderContext);
}
}
}

View File

@@ -1,44 +1,278 @@
using Ghost.Core.Utilities;
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents a physical GPU resource that can be aliased by multiple logical resources.
/// Represents a memory block within a heap.
/// </summary>
internal sealed class PhysicalResource
internal struct MemoryBlock
{
public ulong offset;
public ulong size;
public bool isFree;
public int firstUsePass;
public int lastUsePass;
public int logicalResourceIndex; // Which logical resource is currently using this block
public MemoryBlock(ulong offset, ulong size)
{
this.offset = offset;
this.size = size;
isFree = true;
firstUsePass = int.MaxValue;
lastUsePass = -1;
logicalResourceIndex = -1;
}
public void Reset()
{
isFree = true;
firstUsePass = int.MaxValue;
lastUsePass = -1;
logicalResourceIndex = -1;
}
}
/// <summary>
/// Represents a GPU memory heap for placed resources.
/// Supports D3D12-style heap tier 2 (buffers and textures can alias).
/// </summary>
internal sealed class ResourceHeap
{
public int index;
public int width;
public int height;
public TextureFormat format;
public int sizeInBytes;
public ulong size;
private readonly List<MemoryBlock> _blocks = new(32);
// D3D12 heap alignment requirement (64KB for MSAA textures, 4KB for others)
private const ulong DefaultAlignment = 65536; // 64KB
public ResourceHeap(int index, ulong initialSize = 16 * 1024 * 1024) // 16MB default
{
this.index = index;
this.size = initialSize;
// Initially one large free block
_blocks.Add(new MemoryBlock(0, initialSize));
}
public void Reset()
{
_blocks.Clear();
_blocks.Add(new MemoryBlock(0, size));
}
/// <summary>
/// Attempts to allocate a block of the requested size with proper alignment.
/// Uses best-fit algorithm with lifetime-aware allocation.
/// </summary>
public (bool success, ulong offset, MemoryBlock block) TryAllocate(
ulong requestedSize,
int firstUsePass,
int lastUsePass,
int logicalResourceIndex,
ulong alignment = DefaultAlignment)
{
var alignedSize = AlignUp(requestedSize, alignment);
var bestFitIndex = -1;
ulong bestFitOffset = 0;
var smallestWaste = ulong.MaxValue;
// Find the best fit block that doesn't overlap with lifetime
var blockSpan = CollectionsMarshal.AsSpan(_blocks);
for (var i = 0; i < blockSpan.Length; i++)
{
ref var block = ref blockSpan[i];
// Try to find space within this block
var alignedOffset = AlignUp(block.offset, alignment);
var endOffset = alignedOffset + alignedSize;
if (endOffset <= block.offset + block.size)
{
// Check if this offset range conflicts with ANY existing allocations
var canUseOffset = CanPlaceAtOffset(alignedOffset, alignedSize, firstUsePass, lastUsePass);
if (canUseOffset)
{
var waste = block.size - alignedSize;
if (waste < smallestWaste)
{
smallestWaste = waste;
bestFitIndex = i;
bestFitOffset = alignedOffset;
}
}
}
}
if (bestFitIndex == -1)
{
return (false, 0, default);
}
ref var bestFit = ref CollectionsMarshal.AsSpan(_blocks)[bestFitIndex];
// If the block is free, we need to split it
if (bestFit.isFree)
{
var remainingSize = (bestFit.offset + bestFit.size) - (bestFitOffset + alignedSize);
// Update the current block to be allocated
bestFit.offset = bestFitOffset;
bestFit.size = alignedSize;
bestFit.isFree = false;
bestFit.firstUsePass = firstUsePass;
bestFit.lastUsePass = lastUsePass;
bestFit.logicalResourceIndex = logicalResourceIndex;
// Create a new free block for the remaining space if there is any
if (remainingSize > 0)
{
var newBlock = new MemoryBlock(bestFitOffset + alignedSize, remainingSize);
_blocks.Insert(bestFitIndex + 1, newBlock);
}
}
else
{
// Block is already allocated but lifetime doesn't overlap, we can alias it
// Create a new aliased block at the same location
var aliasedBlock = new MemoryBlock(bestFitOffset, alignedSize)
{
isFree = false,
firstUsePass = firstUsePass,
lastUsePass = lastUsePass,
logicalResourceIndex = logicalResourceIndex
};
// Insert in sorted order by offset
var insertIndex = 0;
for (var i = 0; i < _blocks.Count; i++)
{
if (_blocks[i].offset > bestFitOffset)
{
break;
}
insertIndex = i + 1;
}
_blocks.Insert(insertIndex, aliasedBlock);
// Update bestFit to point to the newly inserted block
bestFit = ref CollectionsMarshal.AsSpan(_blocks)[insertIndex];
}
return (true, bestFitOffset, bestFit);
}
/// <summary>
/// Checks if a resource can be placed at the given offset without lifetime conflicts.
/// Must check ALL blocks that overlap with this offset range.
/// </summary>
private bool CanPlaceAtOffset(ulong offset, ulong size, int firstUsePass, int lastUsePass)
{
var endOffset = offset + size;
foreach (var block in _blocks)
{
// Skip free blocks - they don't have lifetime constraints
if (block.isFree)
continue;
// Check if this block's memory range overlaps with our target range
var blockEnd = block.offset + block.size;
var memoryOverlap = !(offset >= blockEnd || endOffset <= block.offset);
if (memoryOverlap)
{
// Memory ranges overlap, check if lifetimes also overlap
var lifetimeOverlap = !(firstUsePass > block.lastUsePass || lastUsePass < block.firstUsePass);
if (lifetimeOverlap)
{
// Both memory AND lifetime overlap - cannot place here!
return false;
}
}
}
return true;
}
/// <summary>
/// Gets the total memory that would be used if no aliasing occurred.
/// </summary>
public ulong GetTotalAllocatedWithoutAliasing()
{
ulong total = 0;
foreach (var block in _blocks)
{
if (!block.isFree)
{
total += block.size;
}
}
return total;
}
/// <summary>
/// Gets the peak memory usage considering aliasing (max offset + size).
/// </summary>
public ulong GetPeakUsage()
{
ulong peak = 0;
foreach (var block in _blocks)
{
if (!block.isFree)
{
peak = Math.Max(peak, block.offset + block.size);
}
}
return peak;
}
private static ulong AlignUp(ulong value, ulong alignment)
{
return (value + alignment - 1) & ~(alignment - 1);
}
}
/// <summary>
/// Represents a placed resource within a heap.
/// </summary>
internal sealed class PlacedResource
{
public int index;
public RenderGraphResourceType type;
public int heapIndex;
public ulong heapOffset;
public ulong sizeInBytes;
// Original descriptor
public TextureDescriptor textureDesc;
public BufferDescriptor bufferDesc;
// Lifetime tracking
public int firstUsePass = int.MaxValue;
public int lastUsePass = -1;
// Aliasing tracking
public readonly List<int> aliasedLogicalResources = new(4);
public MemoryBlock memoryBlock;
public void Reset()
{
index = -1;
width = 0;
height = 0;
format = TextureFormat.RGBA8;
type = RenderGraphResourceType.Texture;
heapIndex = -1;
heapOffset = 0;
sizeInBytes = 0;
textureDesc = default;
bufferDesc = default;
firstUsePass = int.MaxValue;
lastUsePass = -1;
aliasedLogicalResources.Clear();
}
public bool CanAlias(TextureDescriptor descriptor)
{
// For aliasing, resources must be identical in size and format
// In a real implementation, you could be more flexible (e.g., same size but different format)
return width == descriptor.width &&
height == descriptor.height &&
format == descriptor.format;
memoryBlock = default;
}
public void UpdateLifetime(int passIndex)
@@ -46,148 +280,293 @@ internal sealed class PhysicalResource
firstUsePass = Math.Min(firstUsePass, passIndex);
lastUsePass = Math.Max(lastUsePass, passIndex);
}
public bool IsAliveAt(int passIndex)
{
return passIndex >= firstUsePass && passIndex <= lastUsePass;
}
public int CalculateSize()
{
int bytesPerPixel = format switch
{
TextureFormat.RGBA8 => 4,
TextureFormat.RGBA16F => 8,
TextureFormat.RGBA32F => 16,
TextureFormat.Depth32F => 4,
TextureFormat.Depth24Stencil8 => 4,
_ => 4
};
return width * height * bytesPerPixel;
}
}
/// <summary>
/// Manages physical resource allocation and aliasing.
/// Uses interval scheduling algorithm to minimize memory usage.
/// Manages physical resource allocation and aliasing using heap-based allocation.
/// Supports D3D12 heap tier 2: buffers and textures can alias as long as lifetimes don't overlap.
/// </summary>
internal sealed class ResourceAliasingManager
{
private readonly List<PhysicalResource> _physicalResources = new(32);
private readonly List<ResourceHeap> _heaps = new(4);
private readonly List<PlacedResource> _placedResources = new(32);
private readonly RenderGraphObjectPool _pool = new();
private int _physicalResourceCount;
// Mapping from logical resource index to physical resource index
private readonly Dictionary<int, int> _logicalToPhysical = new(64);
// Mapping from logical resource index to placed resource index
private readonly Dictionary<int, int> _logicalToPlaced = new(64);
// D3D12 alignment constants
private const ulong DefaultTextureAlignment = 65536; // 64KB
private const ulong DefaultBufferAlignment = 65536; // 64KB for D3D12
public void BeginFrame()
{
_physicalResourceCount = 0;
_logicalToPhysical.Clear();
// Reset physical resources but keep them in the pool
for (int i = 0; i < _physicalResources.Count; i++)
for (var i = 0; i < _placedResources.Count; i++)
{
_physicalResources[i].Reset();
_pool.Return(_placedResources[i]);
}
_placedResources.Clear();
_logicalToPlaced.Clear();
// Reset heaps
for (var i = 0; i < _heaps.Count; i++)
{
_heaps[i].Reset();
}
}
/// <summary>
/// Assigns physical resources to logical resources using greedy interval scheduling.
/// This minimizes total GPU memory usage.
/// Assigns physical resources (placed resources) to logical resources using heap-based allocation.
/// This is the modern D3D12 approach: check if resource fits in a hole, not if it matches size/format.
/// Uses a two-pass algorithm:
/// 1. First pass: Simulate allocation to determine peak memory usage
/// 2. Second pass: Create a single heap of the peak size and do the real allocation
/// </summary>
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
{
#if DEBUG
Console.WriteLine("\n=== Resource Aliasing Analysis ===");
int totalLogicalSize = 0;
Console.WriteLine("\n=== Heap-Based Resource Aliasing Analysis ===");
ulong totalLogicalSize = 0;
#endif
// Build list of all logical resources with their lifetimes
// Build list of all logical resources (both textures and buffers) with their lifetimes
var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent();
for (int i = 0; i < registry.TextureResourceCount; i++)
// Iterate through all resources in unified list
for (var i = 0; i < registry.ResourceCount; i++)
{
var resource = registry.GetTextureResourceByIndex(i);
var resource = registry.GetResourceByIndex(i);
if (!resource.isImported) // Don't alias imported resources
{
logicalResources.Add((i, resource));
logicalResources.Add((resource.index, resource));
#if DEBUG
int size = CalculateSize(resource.descriptor);
var size = resource.type == RenderGraphResourceType.Texture
? CalculateTextureSize(resource.textureDescriptor)
: resource.bufferDescriptor.sizeInBytes;
totalLogicalSize += size;
Console.WriteLine($"Logical Resource {i}: {resource.descriptor.name}");
var typeName = resource.type == RenderGraphResourceType.Texture ? "Texture" : "Buffer";
var name = resource.type == RenderGraphResourceType.Texture
? resource.textureDescriptor.name
: resource.bufferDescriptor.name;
Console.WriteLine($"Logical {typeName} {i}: {name}");
Console.WriteLine($" Lifetime: Pass {resource.firstUsePass} -> {resource.lastUsePass}");
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
#endif
}
}
// Sort by first use pass (earlier resources first)
logicalResources.Sort((a, b) => a.resource.firstUsePass.CompareTo(b.resource.firstUsePass));
// Sort by size descending (larger resources first for better packing)
logicalResources.Sort(static (a, b) =>
{
var sizeA = a.resource.type == RenderGraphResourceType.Texture
? CalculateTextureSize(a.resource.textureDescriptor)
: a.resource.bufferDescriptor.sizeInBytes;
var sizeB = b.resource.type == RenderGraphResourceType.Texture
? CalculateTextureSize(b.resource.textureDescriptor)
: b.resource.bufferDescriptor.sizeInBytes;
return sizeB.CompareTo(sizeA); // Descending
});
// Greedy interval scheduling: assign each logical resource to a physical resource
// ===== PASS 1: Simulate allocation to determine peak memory usage =====
var simulationHeap = new ResourceHeap(0, ulong.MaxValue); // Unlimited size for simulation
foreach (var (logicalIndex, logicalResource) in logicalResources)
{
PhysicalResource? assignedPhysical = null;
ulong size;
ulong alignment;
// Try to find an existing physical resource that:
// 1. Has compatible format/size
// 2. Is not alive during this logical resource's lifetime
for (int i = 0; i < _physicalResourceCount; i++)
if (logicalResource.type == RenderGraphResourceType.Texture)
{
var physical = _physicalResources[i];
if (physical.CanAlias(logicalResource.descriptor) &&
!HasLifetimeOverlap(physical, logicalResource))
{
assignedPhysical = physical;
break;
}
size = CalculateTextureSize(logicalResource.textureDescriptor);
alignment = DefaultTextureAlignment;
}
else // Buffer
{
size = logicalResource.bufferDescriptor.sizeInBytes;
alignment = DefaultBufferAlignment;
}
// No compatible physical resource found, allocate a new one
if (assignedPhysical == null)
var (success, offset, block) = simulationHeap.TryAllocate(
size,
logicalResource.firstUsePass,
logicalResource.lastUsePass,
logicalIndex,
alignment);
if (!success)
{
assignedPhysical = GetOrCreatePhysicalResource();
assignedPhysical.index = _physicalResourceCount - 1;
assignedPhysical.width = logicalResource.descriptor.width;
assignedPhysical.height = logicalResource.descriptor.height;
assignedPhysical.format = logicalResource.descriptor.format;
assignedPhysical.sizeInBytes = assignedPhysical.CalculateSize();
throw new InvalidOperationException("Simulation allocation failed - this should never happen with unlimited heap");
}
}
// Get peak usage from simulation
var peakMemoryUsage = simulationHeap.GetPeakUsage();
// Align peak usage to 64KB (D3D12 requirement)
peakMemoryUsage = AlignUp(peakMemoryUsage, DefaultTextureAlignment);
#if DEBUG
Console.WriteLine($"\nAllocated NEW Physical Resource {assignedPhysical.index}:");
Console.WriteLine($" Size: {assignedPhysical.width}x{assignedPhysical.height}");
Console.WriteLine($" Format: {assignedPhysical.format}");
Console.WriteLine($" Memory: {assignedPhysical.sizeInBytes / 1024.0:F2} KB");
Console.WriteLine($"\nPeak Memory Usage: {peakMemoryUsage / (1024.0 * 1024.0):F2} MB");
#endif
}
// ===== PASS 2: Create a single heap of the peak size and do the real allocation =====
var mainHeap = new ResourceHeap(0, peakMemoryUsage);
_heaps.Add(mainHeap);
#if DEBUG
Console.WriteLine($"Created Single Heap:");
Console.WriteLine($" Size: {peakMemoryUsage / (1024.0 * 1024.0):F2} MB\n");
#endif
// Allocate each logical resource in the heap
foreach (var (logicalIndex, logicalResource) in logicalResources)
{
ulong size;
ulong alignment;
if (logicalResource.type == RenderGraphResourceType.Texture)
{
size = CalculateTextureSize(logicalResource.textureDescriptor);
alignment = DefaultTextureAlignment;
}
else // Buffer
{
size = logicalResource.bufferDescriptor.sizeInBytes;
alignment = DefaultBufferAlignment;
}
var (success, offset, block) = mainHeap.TryAllocate(
size,
logicalResource.firstUsePass,
logicalResource.lastUsePass,
logicalIndex,
alignment);
if (!success)
{
throw new InvalidOperationException("Real allocation failed - this should match simulation");
}
var assignedHeapIndex = 0;
var assignedOffset = offset;
var assignedBlock = block;
var assignedPlaced = _pool.Rent<PlacedResource>();
assignedPlaced.index = _placedResources.Count;
assignedPlaced.type = logicalResource.type;
assignedPlaced.heapIndex = assignedHeapIndex;
assignedPlaced.heapOffset = assignedOffset;
assignedPlaced.sizeInBytes = size;
assignedPlaced.firstUsePass = logicalResource.firstUsePass;
assignedPlaced.lastUsePass = logicalResource.lastUsePass;
assignedPlaced.memoryBlock = assignedBlock;
if (logicalResource.type == RenderGraphResourceType.Texture)
{
assignedPlaced.textureDesc = logicalResource.textureDescriptor;
}
else
{
Console.WriteLine($"\nALIASING: {logicalResource.descriptor.name} -> Physical Resource {assignedPhysical.index}");
assignedPlaced.bufferDesc = logicalResource.bufferDescriptor;
}
assignedPlaced.aliasedLogicalResources.Clear();
assignedPlaced.aliasedLogicalResources.Add(logicalIndex);
_placedResources.Add(assignedPlaced);
#if DEBUG
var isAliased = assignedBlock.logicalResourceIndex != logicalIndex && assignedBlock.logicalResourceIndex != -1;
var name = logicalResource.type == RenderGraphResourceType.Texture
? logicalResource.textureDescriptor.name
: logicalResource.bufferDescriptor.name;
var typeName = logicalResource.type == RenderGraphResourceType.Texture ? "Texture" : "Buffer";
if (isAliased)
{
Console.WriteLine($"\nALIASING {typeName}: {name}");
Console.WriteLine($" Placed in Heap {assignedHeapIndex} at offset {assignedOffset} ({assignedOffset / 1024.0:F2} KB)");
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
}
else
{
Console.WriteLine($"\nAllocated {typeName}: {name}");
Console.WriteLine($" Heap {assignedHeapIndex}, Offset {assignedOffset} ({assignedOffset / 1024.0:F2} KB)");
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
}
#endif
// Update physical resource lifetime
assignedPhysical.UpdateLifetime(logicalResource.firstUsePass);
assignedPhysical.UpdateLifetime(logicalResource.lastUsePass);
assignedPhysical.aliasedLogicalResources.Add(logicalIndex);
// Record the mapping
_logicalToPhysical[logicalIndex] = assignedPhysical.index;
_logicalToPlaced[logicalIndex] = assignedPlaced.index;
}
// Second pass: Populate aliasedLogicalResources lists
// For each placed resource, find all OTHER placed resources at the same heap+offset
for (var i = 0; i < _placedResources.Count; i++)
{
var placed = _placedResources[i];
// Find all logical resources that share the same heap location
for (var j = 0; j < _placedResources.Count; j++)
{
if (i == j) continue; // Skip self
var other = _placedResources[j];
// Check if they're at the same heap+offset
if (other.heapIndex == placed.heapIndex && other.heapOffset == placed.heapOffset)
{
// Add the other's logical resource to this one's aliased list
var otherLogicalIndex = other.aliasedLogicalResources[0]; // Each has exactly one at this point
if (!placed.aliasedLogicalResources.Contains(otherLogicalIndex))
{
placed.aliasedLogicalResources.Add(otherLogicalIndex);
}
}
}
}
#if DEBUG
int totalPhysicalSize = 0;
for (int i = 0; i < _physicalResourceCount; i++)
// Debug output: Show which resources alias with each other
Console.WriteLine("\n=== Aliasing Groups ===");
var processedOffsets = new HashSet<(int heapIndex, ulong offset)>();
for (var i = 0; i < _placedResources.Count; i++)
{
totalPhysicalSize += _physicalResources[i].sizeInBytes;
var placed = _placedResources[i];
var key = (placed.heapIndex, placed.heapOffset);
if (!processedOffsets.Contains(key) && placed.aliasedLogicalResources.Count > 1)
{
processedOffsets.Add(key);
Console.WriteLine($"Heap {placed.heapIndex} @ Offset {placed.heapOffset / 1024.0:F2} KB ({placed.aliasedLogicalResources.Count} resources):");
foreach (var logicalIdx in placed.aliasedLogicalResources)
{
var res = registry.GetResourceByIndex(logicalIdx);
var name = res.type == RenderGraphResourceType.Texture
? res.textureDescriptor.name
: res.bufferDescriptor.name;
Console.WriteLine($" - {name} (Pass {res.firstUsePass}-{res.lastUsePass})");
}
}
}
Console.WriteLine($"\n=== Aliasing Summary ===");
Console.WriteLine("=======================\n");
#endif
#if DEBUG
ulong totalPhysicalSize = 0;
for (var i = 0; i < _heaps.Count; i++)
{
totalPhysicalSize += _heaps[i].GetPeakUsage();
}
Console.WriteLine($"\n=== Heap-Based Aliasing Summary ===");
Console.WriteLine($"Logical Resources: {logicalResources.Count}");
Console.WriteLine($"Physical Resources: {_physicalResourceCount}");
Console.WriteLine($"Placed Resources: {_placedResources.Count}");
Console.WriteLine($"Heaps: {_heaps.Count}");
Console.WriteLine($"Total Logical Memory: {totalLogicalSize / 1024.0:F2} KB");
Console.WriteLine($"Total Physical Memory: {totalPhysicalSize / 1024.0:F2} KB");
Console.WriteLine($"Memory Saved: {(totalLogicalSize - totalPhysicalSize) / 1024.0:F2} KB ({(1.0 - (double)totalPhysicalSize / totalLogicalSize) * 100.0:F1}%)");
@@ -197,48 +576,21 @@ internal sealed class ResourceAliasingManager
ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources);
}
public int GetPhysicalResourceIndex(int logicalIndex)
public int GetPlacedResourceIndex(int logicalIndex)
{
return _logicalToPhysical.TryGetValue(logicalIndex, out var physicalIndex) ? physicalIndex : -1;
return _logicalToPlaced.TryGetValue(logicalIndex, out var placedIndex) ? placedIndex : -1;
}
public PhysicalResource? GetPhysicalResource(int physicalIndex)
public PlacedResource? GetPlacedResource(int placedIndex)
{
return physicalIndex >= 0 && physicalIndex < _physicalResourceCount
? _physicalResources[physicalIndex]
return placedIndex >= 0 && placedIndex < _placedResources.Count
? _placedResources[placedIndex]
: null;
}
private bool HasLifetimeOverlap(PhysicalResource physical, RenderGraphResource logical)
private static ulong CalculateTextureSize(TextureDescriptor descriptor)
{
// Check if the lifetimes overlap
// No overlap if: logical.First > physical.Last OR logical.Last < physical.First
return !(logical.firstUsePass > physical.lastUsePass ||
logical.lastUsePass < physical.firstUsePass);
}
private PhysicalResource GetOrCreatePhysicalResource()
{
PhysicalResource resource;
if (_physicalResourceCount < _physicalResources.Count)
{
resource = _physicalResources[_physicalResourceCount];
resource.Reset();
}
else
{
resource = _pool.Rent<PhysicalResource>();
resource.Reset();
_physicalResources.Add(resource);
}
_physicalResourceCount++;
return resource;
}
private static int CalculateSize(TextureDescriptor descriptor)
{
int bytesPerPixel = descriptor.format switch
var bytesPerPixel = descriptor.format switch
{
TextureFormat.RGBA8 => 4,
TextureFormat.RGBA16F => 8,
@@ -247,82 +599,87 @@ internal sealed class ResourceAliasingManager
TextureFormat.Depth24Stencil8 => 4,
_ => 4
};
return descriptor.width * descriptor.height * bytesPerPixel;
// Add alignment padding (D3D12 requires 64KB alignment)
var size = (ulong)(descriptor.width * descriptor.height * bytesPerPixel);
return AlignUp(size, DefaultTextureAlignment);
}
private static ulong AlignUp(ulong value, ulong alignment)
{
return (value + alignment - 1) & ~(alignment - 1);
}
public void Clear()
{
for (int i = 0; i < _physicalResources.Count; i++)
for (var i = 0; i < _placedResources.Count; i++)
{
_pool.Return(_physicalResources[i]);
_pool.Return(_placedResources[i]);
}
_physicalResources.Clear();
_physicalResourceCount = 0;
_logicalToPhysical.Clear();
_placedResources.Clear();
_logicalToPlaced.Clear();
_heaps.Clear();
}
/// <summary>
/// Restores aliasing state from cache.
/// </summary>
public void RestoreFromCache(Dictionary<int, int> logicalToPhysical, List<PhysicalResourceData> physicalData)
public void RestoreFromCache(Dictionary<int, int> logicalToPlaced, List<PlacedResourceData> placedData)
{
_logicalToPhysical.Clear();
foreach (var kvp in logicalToPhysical)
_logicalToPlaced.Clear();
foreach (var kvp in logicalToPlaced)
{
_logicalToPhysical[kvp.Key] = kvp.Value;
_logicalToPlaced[kvp.Key] = kvp.Value;
}
// Restore physical resources
_physicalResourceCount = physicalData.Count;
for (int i = 0; i < physicalData.Count; i++)
// Restore placed resources
for (var i = 0; i < placedData.Count; i++)
{
PhysicalResource physical;
if (i < _physicalResources.Count)
{
physical = _physicalResources[i];
physical.Reset();
}
else
{
physical = _pool.Rent<PhysicalResource>();
physical.Reset();
_physicalResources.Add(physical);
}
var placed = _pool.Rent<PlacedResource>();
var data = physicalData[i];
physical.index = data.index;
physical.width = data.width;
physical.height = data.height;
physical.format = data.format;
physical.firstUsePass = data.firstUsePass;
physical.lastUsePass = data.lastUsePass;
physical.sizeInBytes = physical.CalculateSize();
var data = placedData[i];
placed.index = data.index;
placed.type = data.type;
placed.heapIndex = data.heapIndex;
placed.heapOffset = data.heapOffset;
placed.sizeInBytes = data.sizeInBytes;
placed.textureDesc = data.textureDesc;
placed.bufferDesc = data.bufferDesc;
placed.firstUsePass = data.firstUsePass;
placed.lastUsePass = data.lastUsePass;
placed.aliasedLogicalResources.Clear();
_placedResources.Add(placed);
}
}
/// <summary>
/// Stores current aliasing state to cache.
/// </summary>
public void StoreToCache(Dictionary<int, int> outLogicalToPhysical, List<PhysicalResourceData> outPhysicalData)
public void StoreToCache(Dictionary<int, int> outLogicalToPlaced, List<PlacedResourceData> outPlacedData)
{
outLogicalToPhysical.Clear();
foreach (var kvp in _logicalToPhysical)
outLogicalToPlaced.Clear();
foreach (var kvp in _logicalToPlaced)
{
outLogicalToPhysical[kvp.Key] = kvp.Value;
outLogicalToPlaced[kvp.Key] = kvp.Value;
}
outPhysicalData.Clear();
for (int i = 0; i < _physicalResourceCount; i++)
outPlacedData.Clear();
for (var i = 0; i < _placedResources.Count; i++)
{
var physical = _physicalResources[i];
outPhysicalData.Add(new PhysicalResourceData
var placed = _placedResources[i];
outPlacedData.Add(new PlacedResourceData
{
index = physical.index,
width = physical.width,
height = physical.height,
format = physical.format,
firstUsePass = physical.firstUsePass,
lastUsePass = physical.lastUsePass
index = placed.index,
type = placed.type,
heapIndex = placed.heapIndex,
heapOffset = placed.heapOffset,
sizeInBytes = placed.sizeInBytes,
textureDesc = placed.textureDesc,
bufferDesc = placed.bufferDesc,
firstUsePass = placed.firstUsePass,
lastUsePass = placed.lastUsePass
});
}
}

View File

@@ -1,329 +0,0 @@
using Ghost.Core.Utilities;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents a physical GPU resource that can be aliased by multiple logical resources.
/// </summary>
internal sealed class PhysicalResource
{
public int index;
public int width;
public int height;
public TextureFormat format;
public int sizeInBytes;
// Lifetime tracking
public int firstUsePass = int.MaxValue;
public int lastUsePass = -1;
// Aliasing tracking
public readonly List<int> aliasedLogicalResources = new(4);
public void Reset()
{
index = -1;
width = 0;
height = 0;
format = TextureFormat.RGBA8;
sizeInBytes = 0;
firstUsePass = int.MaxValue;
lastUsePass = -1;
aliasedLogicalResources.Clear();
}
public bool CanAlias(TextureDescriptor descriptor)
{
// For aliasing, resources must be identical in size and format
// In a real implementation, you could be more flexible (e.g., same size but different format)
return width == descriptor.width &&
height == descriptor.height &&
format == descriptor.format;
}
public void UpdateLifetime(int passIndex)
{
firstUsePass = Math.Min(firstUsePass, passIndex);
lastUsePass = Math.Max(lastUsePass, passIndex);
}
public bool IsAliveAt(int passIndex)
{
return passIndex >= firstUsePass && passIndex <= lastUsePass;
}
public int CalculateSize()
{
int bytesPerPixel = format switch
{
TextureFormat.RGBA8 => 4,
TextureFormat.RGBA16F => 8,
TextureFormat.RGBA32F => 16,
TextureFormat.Depth32F => 4,
TextureFormat.Depth24Stencil8 => 4,
_ => 4
};
return width * height * bytesPerPixel;
}
}
/// <summary>
/// Manages physical resource allocation and aliasing.
/// Uses interval scheduling algorithm to minimize memory usage.
/// </summary>
internal sealed class ResourceAliasingManager
{
private readonly List<PhysicalResource> _physicalResources = new(32);
private readonly RenderGraphObjectPool _pool = new();
private int _physicalResourceCount;
// Mapping from logical resource index to physical resource index
private readonly Dictionary<int, int> _logicalToPhysical = new(64);
public void BeginFrame()
{
_physicalResourceCount = 0;
_logicalToPhysical.Clear();
// Reset physical resources but keep them in the pool
for (int i = 0; i < _physicalResources.Count; i++)
{
_physicalResources[i].Reset();
}
}
/// <summary>
/// Assigns physical resources to logical resources using greedy interval scheduling.
/// This minimizes total GPU memory usage.
/// </summary>
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
{
#if DEBUG
Console.WriteLine("\n=== Resource Aliasing Analysis ===");
int totalLogicalSize = 0;
#endif
// Build list of all logical resources with their lifetimes
var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent();
for (int i = 0; i < registry.TextureResourceCount; i++)
{
var resource = registry.GetTextureResourceByIndex(i);
if (!resource.isImported) // Don't alias imported resources
{
logicalResources.Add((i, resource));
#if DEBUG
int size = CalculateSize(resource.descriptor);
totalLogicalSize += size;
Console.WriteLine($"Logical Resource {i}: {resource.descriptor.name}");
Console.WriteLine($" Lifetime: Pass {resource.firstUsePass} -> {resource.lastUsePass}");
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
#endif
}
}
// Sort by first use pass (earlier resources first)
logicalResources.Sort((a, b) => a.resource.firstUsePass.CompareTo(b.resource.firstUsePass));
// Greedy interval scheduling: assign each logical resource to a physical resource
foreach (var (logicalIndex, logicalResource) in logicalResources)
{
PhysicalResource? assignedPhysical = null;
// Try to find an existing physical resource that:
// 1. Has compatible format/size
// 2. Is not alive during this logical resource's lifetime
for (int i = 0; i < _physicalResourceCount; i++)
{
var physical = _physicalResources[i];
if (physical.CanAlias(logicalResource.descriptor) &&
!HasLifetimeOverlap(physical, logicalResource))
{
assignedPhysical = physical;
break;
}
}
// No compatible physical resource found, allocate a new one
if (assignedPhysical == null)
{
assignedPhysical = GetOrCreatePhysicalResource();
assignedPhysical.index = _physicalResourceCount - 1;
assignedPhysical.width = logicalResource.descriptor.width;
assignedPhysical.height = logicalResource.descriptor.height;
assignedPhysical.format = logicalResource.descriptor.format;
assignedPhysical.sizeInBytes = assignedPhysical.CalculateSize();
#if DEBUG
Console.WriteLine($"\nAllocated NEW Physical Resource {assignedPhysical.index}:");
Console.WriteLine($" Size: {assignedPhysical.width}x{assignedPhysical.height}");
Console.WriteLine($" Format: {assignedPhysical.format}");
Console.WriteLine($" Memory: {assignedPhysical.sizeInBytes / 1024.0:F2} KB");
#endif
}
#if DEBUG
else
{
Console.WriteLine($"\nALIASING: {logicalResource.descriptor.name} -> Physical Resource {assignedPhysical.index}");
}
#endif
// Update physical resource lifetime
assignedPhysical.UpdateLifetime(logicalResource.firstUsePass);
assignedPhysical.UpdateLifetime(logicalResource.lastUsePass);
assignedPhysical.aliasedLogicalResources.Add(logicalIndex);
// Record the mapping
_logicalToPhysical[logicalIndex] = assignedPhysical.index;
}
#if DEBUG
int totalPhysicalSize = 0;
for (int i = 0; i < _physicalResourceCount; i++)
{
totalPhysicalSize += _physicalResources[i].sizeInBytes;
}
Console.WriteLine($"\n=== Aliasing Summary ===");
Console.WriteLine($"Logical Resources: {logicalResources.Count}");
Console.WriteLine($"Physical Resources: {_physicalResourceCount}");
Console.WriteLine($"Total Logical Memory: {totalLogicalSize / 1024.0:F2} KB");
Console.WriteLine($"Total Physical Memory: {totalPhysicalSize / 1024.0:F2} KB");
Console.WriteLine($"Memory Saved: {(totalLogicalSize - totalPhysicalSize) / 1024.0:F2} KB ({(1.0 - (double)totalPhysicalSize / totalLogicalSize) * 100.0:F1}%)");
Console.WriteLine("================================\n");
#endif
ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources);
}
public int GetPhysicalResourceIndex(int logicalIndex)
{
return _logicalToPhysical.TryGetValue(logicalIndex, out var physicalIndex) ? physicalIndex : -1;
}
public PhysicalResource? GetPhysicalResource(int physicalIndex)
{
return physicalIndex >= 0 && physicalIndex < _physicalResourceCount
? _physicalResources[physicalIndex]
: null;
}
private bool HasLifetimeOverlap(PhysicalResource physical, RenderGraphResource logical)
{
// Check if the lifetimes overlap
// No overlap if: logical.First > physical.Last OR logical.Last < physical.First
return !(logical.firstUsePass > physical.lastUsePass ||
logical.lastUsePass < physical.firstUsePass);
}
private PhysicalResource GetOrCreatePhysicalResource()
{
PhysicalResource resource;
if (_physicalResourceCount < _physicalResources.Count)
{
resource = _physicalResources[_physicalResourceCount];
resource.Reset();
}
else
{
resource = _pool.Rent<PhysicalResource>();
resource.Reset();
_physicalResources.Add(resource);
}
_physicalResourceCount++;
return resource;
}
private static int CalculateSize(TextureDescriptor descriptor)
{
int bytesPerPixel = descriptor.format switch
{
TextureFormat.RGBA8 => 4,
TextureFormat.RGBA16F => 8,
TextureFormat.RGBA32F => 16,
TextureFormat.Depth32F => 4,
TextureFormat.Depth24Stencil8 => 4,
_ => 4
};
return descriptor.width * descriptor.height * bytesPerPixel;
}
public void Clear()
{
for (int i = 0; i < _physicalResources.Count; i++)
{
_pool.Return(_physicalResources[i]);
}
_physicalResources.Clear();
_physicalResourceCount = 0;
_logicalToPhysical.Clear();
}
/// <summary>
/// Restores aliasing state from cache.
/// </summary>
public void RestoreFromCache(Dictionary<int, int> logicalToPhysical, List<PhysicalResourceData> physicalData)
{
_logicalToPhysical.Clear();
foreach (var kvp in logicalToPhysical)
{
_logicalToPhysical[kvp.Key] = kvp.Value;
}
// Restore physical resources
_physicalResourceCount = physicalData.Count;
for (int i = 0; i < physicalData.Count; i++)
{
PhysicalResource physical;
if (i < _physicalResources.Count)
{
physical = _physicalResources[i];
physical.Reset();
}
else
{
physical = _pool.Rent<PhysicalResource>();
physical.Reset();
_physicalResources.Add(physical);
}
var data = physicalData[i];
physical.index = data.index;
physical.width = data.width;
physical.height = data.height;
physical.format = data.format;
physical.firstUsePass = data.firstUsePass;
physical.lastUsePass = data.lastUsePass;
physical.sizeInBytes = physical.CalculateSize();
}
}
/// <summary>
/// Stores current aliasing state to cache.
/// </summary>
public void StoreToCache(Dictionary<int, int> outLogicalToPhysical, List<PhysicalResourceData> outPhysicalData)
{
outLogicalToPhysical.Clear();
foreach (var kvp in _logicalToPhysical)
{
outLogicalToPhysical[kvp.Key] = kvp.Value;
}
outPhysicalData.Clear();
for (int i = 0; i < _physicalResourceCount; i++)
{
var physical = _physicalResources[i];
outPhysicalData.Add(new PhysicalResourceData
{
index = physical.index,
width = physical.width,
height = physical.height,
format = physical.format,
firstUsePass = physical.firstUsePass,
lastUsePass = physical.lastUsePass
});
}
}
}

View File

@@ -19,6 +19,7 @@ public enum ResourceState
CopySource = 1 << 5,
CopyDest = 1 << 6,
Present = 1 << 7,
IndirectArgument = 1 << 8,
}
/// <summary>
@@ -140,14 +141,14 @@ internal struct ResourceBarrier
/// </summary>
internal sealed class ResourceStateTracker
{
public int ResourceIndex;
public ResourceState CurrentState = ResourceState.Common;
public int LastAccessPass = -1;
public int resourceIndex;
public ResourceState currentState = ResourceState.Common;
public int lastAccessPass = -1;
public void Reset()
{
ResourceIndex = -1;
CurrentState = ResourceState.Common;
LastAccessPass = -1;
resourceIndex = -1;
currentState = ResourceState.Common;
lastAccessPass = -1;
}
}

View File

@@ -4,11 +4,14 @@ using System.Diagnostics;
namespace Ghost.RenderGraph.Concept;
[Flags]
public enum AccessFlags
public enum AccessFlags : byte
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Discard = 1 << 2,
WriteAll = Write | Discard,
ReadWrite = Read | Write,
}
@@ -26,6 +29,13 @@ public interface IRenderGraphBuilder : IDisposable
/// <param name="descriptor">A structure that defines the properties and configuration of the texture to create.</param>
/// <returns>An identifier for the newly created texture resource.</returns>
Identifier<RGTexture> CreateTexture(in TextureDescriptor descriptor);
/// <summary>
/// Creates a new buffer resource based on the specified descriptor.
/// </summary>
/// <param name="descriptor">A structure that defines the properties and configuration of the buffer to create.</param>
/// <returns>An identifier for the newly created buffer resource.</returns>
Identifier<RGBuffer> CreateBuffer(in BufferDescriptor descriptor);
/// <summary>
/// Registers the specified texture for use in the current render graph pass with the given access mode.
@@ -34,6 +44,15 @@ public interface IRenderGraphBuilder : IDisposable
/// <param name="accessMode">The access mode specifying how the texture will be read or written during the pass.</param>
/// <returns>An identifier for the texture.</returns>
Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags accessMode);
/// <summary>
/// Registers the specified buffer for use in the current render graph pass with the given access mode.
/// </summary>
/// <param name="buffer">The identifier of the buffer to be used in the render graph pass.</param>
/// <param name="accessMode">The access mode specifying how the buffer will be read or written during the pass.</param>
/// <param name="hint">Optional hint about how the buffer will be used (e.g., IndirectArgument). Default is None (ByteAddressBuffer SRV).</param>
/// <returns>An identifier for the buffer.</returns>
Identifier<RGBuffer> UseBuffer(Identifier<RGBuffer> buffer, AccessFlags accessMode, BufferHint hint = BufferHint.None);
}
public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
@@ -56,13 +75,15 @@ public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
/// </summary>
/// <param name="texture">The identifier of the texture to use as the color attachment.</param>
/// <param name="index">The zero-based index of the color attachment to set.</param>
void SetColorAttachment(Identifier<RGTexture> texture, int index);
/// <param name="flags">Access flags. Default is Write (assumes partial update). Use WriteAll for fullscreen passes.</param>
void SetColorAttachment(Identifier<RGTexture> texture, int index, AccessFlags flags = AccessFlags.Write);
/// <summary>
/// Sets the depth attachment for the current render pass using the specified texture.
/// </summary>
/// <param name="texture">The identifier of the texture to use as the depth attachment. Cannot be null.</param>
void SetDepthAttachment(Identifier<RGTexture> texture);
/// <param name="flags">Access flags. Default is ReadWrite (assumes partial update). Use WriteAll for fullscreen passes.</param>
void SetDepthAttachment(Identifier<RGTexture> texture, AccessFlags flags = AccessFlags.ReadWrite);
/// <summary>
/// Sets the function used to render a pass with the specified pass data and render context.
@@ -146,12 +167,35 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
_resources.SetProducer(handle.AsResource(), _pass.index);
return handle;
}
public Identifier<RGBuffer> CreateBuffer(in BufferDescriptor descriptor)
{
ThrowIfDisposed();
var handle = _resources.CreateBuffer(descriptor);
_pass.resourceCreates[(int)RenderGraphResourceType.Buffer].Add(handle.AsResource());
_resources.SetProducer(handle.AsResource(), _pass.index);
return handle;
}
public Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags flags)
{
ThrowIfDisposed();
return UseResource(texture.AsResource(), flags, RenderGraphResourceType.Texture).AsTexture();
}
public Identifier<RGBuffer> UseBuffer(Identifier<RGBuffer> buffer, AccessFlags flags, BufferHint hint = BufferHint.None)
{
ThrowIfDisposed();
// Store buffer hint if not None
if (hint != BufferHint.None)
{
_pass.bufferHints[buffer.AsResource().Value] = hint;
}
return UseResource(buffer.AsResource(), flags, RenderGraphResourceType.Buffer).AsBuffer();
}
public Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture)
{
@@ -173,17 +217,17 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
return buffer;
}
public void SetColorAttachment(Identifier<RGTexture> texture, int index)
public void SetColorAttachment(Identifier<RGTexture> texture, int index, AccessFlags flags = AccessFlags.Write)
{
ThrowIfDisposed();
Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range.");
var id = UseTexture(texture, AccessFlags.Write);
var id = UseTexture(texture, flags);
if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid)
{
_pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index);
_pass.colorAccess[index] = new TextureAccess(id, AccessFlags.Write);
_pass.colorAccess[index] = new TextureAccess(id, flags);
}
else
{
@@ -191,14 +235,14 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
}
}
public void SetDepthAttachment(Identifier<RGTexture> texture)
public void SetDepthAttachment(Identifier<RGTexture> texture, AccessFlags flags = AccessFlags.Write)
{
ThrowIfDisposed();
var id = UseTexture(texture, AccessFlags.Write);
var id = UseTexture(texture, flags);
if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid)
{
_pass.depthAccess = new TextureAccess(id, AccessFlags.Write);
_pass.depthAccess = new TextureAccess(id, flags);
}
else
{

View File

@@ -1,240 +0,0 @@
using Ghost.Core;
using System.Diagnostics;
namespace Ghost.RenderGraph.Concept;
[Flags]
public enum AccessFlags
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
ReadWrite = Read | Write,
}
public interface IRenderGraphBuilder : IDisposable
{
/// <summary>
/// Enables or disables pass culling for the current context.
/// </summary>
/// <param name="value">A value indicating whether pass culling is allowed.</param>
void AllowPassCulling(bool value);
/// <summary>
/// Creates a new texture resource based on the specified descriptor.
/// </summary>
/// <param name="descriptor">A structure that defines the properties and configuration of the texture to create.</param>
/// <returns>An identifier for the newly created texture resource.</returns>
Identifier<RGTexture> CreateTexture(in TextureDescriptor descriptor);
/// <summary>
/// Registers the specified texture for use in the current render graph pass with the given access mode.
/// </summary>
/// <param name="texture">The identifier of the texture to be used in the render graph pass.</param>
/// <param name="accessMode">The access mode specifying how the texture will be read or written during the pass.</param>
/// <returns>An identifier for the texture.</returns>
Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags accessMode);
}
public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
{
/// <summary>
/// Binds a texture for random access operations within the current rendering pass.
/// </summary>
/// <param name="texture">The identifier of the texture to be used for random access.</param>
/// <returns>An identifier for the texture.</returns>
Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture);
/// <summary>
/// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context.
/// </summary>
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer resource.</param>
/// <returns>An identifier for the buffer.</returns>
Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer);
/// <summary>
/// Sets the color attachment at the specified index to the given texture.
/// </summary>
/// <param name="texture">The identifier of the texture to use as the color attachment.</param>
/// <param name="index">The zero-based index of the color attachment to set.</param>
void SetColorAttachment(Identifier<RGTexture> texture, int index);
/// <summary>
/// Sets the depth attachment for the current render pass using the specified texture.
/// </summary>
/// <param name="texture">The identifier of the texture to use as the depth attachment. Cannot be null.</param>
void SetDepthAttachment(Identifier<RGTexture> texture);
/// <summary>
/// Sets the function used to render a pass with the specified pass data and render context.
/// </summary>
/// <typeparam name="TPassData">The type of data associated with the render pass.</typeparam>
/// <param name="renderFunc">The delegate that defines the rendering logic for the pass.</param>
void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> renderFunc)
where TPassData : class, new();
}
public interface IComputeRenderGraphBuilder : IRenderGraphBuilder
{
/// <summary>
/// Enables or disables asynchronous compute operations.
/// </summary>
/// <param name="value">true to enable asynchronous compute; otherwise, false.</param>
void EnableAsyncCompute(bool value);
/// <summary>
/// Sets the render function to be invoked during the compute rendering process.
/// </summary>
/// <typeparam name="TPassData">The type of the data object passed to the render function.</typeparam>
/// <param name="renderFunc">The delegate that defines the rendering logic to execute.</param>
void SetRenderFunc<TPassData>(Action<TPassData, ComputeRenderContext> renderFunc)
where TPassData : class, new();
}
internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGraphBuilder
{
private RenderGraph _graph = null!;
private RenderGraphPassBase _pass = null!;
private RenderGraphResourceRegistry _resources = null!;
private bool _disposed;
internal void Init(RenderGraph graph, RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
{
_graph = graph;
_pass = pass;
_resources = resources;
_disposed = false;
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
private Identifier<RGResource> UseResource(Identifier<RGResource> resource, AccessFlags accessFlags)
{
if (accessFlags.HasFlag(AccessFlags.Read))
{
_pass.resourceReads.Add(resource);
_resources.AddConsumer(resource, _pass.index);
}
if (accessFlags.HasFlag(AccessFlags.Write))
{
_pass.resourceWrites.Add(resource);
_resources.SetProducer(resource, _pass.index);
}
return resource;
}
public void AllowPassCulling(bool value)
{
_pass.allowCulling = value;
}
public void EnableAsyncCompute(bool value)
{
_pass.asyncCompute = value;
}
public Identifier<RGTexture> CreateTexture(in TextureDescriptor descriptor)
{
ThrowIfDisposed();
var handle = _resources.CreateTexture(descriptor);
_pass.resourceCreates.Add(handle.AsResource());
_resources.SetProducer(handle.AsResource(), _pass.index);
return handle;
}
public Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags flags)
{
ThrowIfDisposed();
return UseResource(texture.AsResource(), flags).AsTexture();
}
public Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture)
{
ThrowIfDisposed();
var resource = texture.AsResource();
UseResource(resource, AccessFlags.ReadWrite);
_pass.randomAccess.Add(resource);
return texture;
}
public Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer)
{
ThrowIfDisposed();
var resource = buffer.AsResource();
UseResource(resource, AccessFlags.ReadWrite);
_pass.randomAccess.Add(resource);
return buffer;
}
public void SetColorAttachment(Identifier<RGTexture> texture, int index)
{
ThrowIfDisposed();
Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range.");
var id = UseTexture(texture, AccessFlags.Write);
if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid)
{
_pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index);
_pass.colorAccess[index] = new TextureAccess(id, AccessFlags.Write);
}
else
{
throw new InvalidOperationException($"Color attachment at index {index} is already set to a different texture.");
}
}
public void SetDepthAttachment(Identifier<RGTexture> texture)
{
ThrowIfDisposed();
var id = UseTexture(texture, AccessFlags.Write);
if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid)
{
_pass.depthAccess = new TextureAccess(id, AccessFlags.Write);
}
else
{
throw new InvalidOperationException("Depth attachment is already set to a different texture.");
}
}
public void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> renderFunc)
where TPassData : class, new()
{
((RasterRenderGraphPass<TPassData>)_pass).renderFunc = renderFunc;
}
public void SetRenderFunc<TPassData>(Action<TPassData, ComputeRenderContext> renderFunc)
where TPassData : class, new()
{
((ComputeRenderGraphPass<TPassData>)_pass).renderFunc = renderFunc;
}
public void Dispose()
{
if (_disposed)
{
return;
}
if (!_pass.HasRenderFunc())
{
throw new InvalidOperationException("RenderGraphBuilder must be disposed after setting up the render function.");
}
_graph = null!;
_pass = null!;
_resources = null!;
_disposed = true;
}
}

View File

@@ -17,8 +17,8 @@ internal sealed class CachedCompilation
// Physical resource aliasing mappings (logical index -> physical index)
public readonly Dictionary<int, int> logicalToPhysical = new(128);
// Physical resource metadata
public readonly List<PhysicalResourceData> physicalResources = new(32);
// Placed resource metadata
public readonly List<PlacedResourceData> placedResources = new(32);
// Resource barriers
public readonly List<ResourceBarrier> barriers = new(128);
@@ -31,21 +31,24 @@ internal sealed class CachedCompilation
compiledPassIndices.Clear();
passCulledFlags.Clear();
logicalToPhysical.Clear();
physicalResources.Clear();
placedResources.Clear();
barriers.Clear();
resourceStates.Clear();
}
}
/// <summary>
/// Physical resource data for caching.
/// Placed resource data for caching.
/// </summary>
internal struct PhysicalResourceData
internal struct PlacedResourceData
{
public int index;
public int width;
public int height;
public TextureFormat format;
public RenderGraphResourceType type;
public int heapIndex;
public ulong heapOffset;
public ulong sizeInBytes;
public TextureDescriptor textureDesc;
public BufferDescriptor bufferDesc;
public int firstUsePass;
public int lastUsePass;
}
@@ -100,7 +103,7 @@ internal sealed class RenderGraphCompilationCache
_cached.logicalToPhysical[kvp.Key] = kvp.Value;
}
_cached.physicalResources.AddRange(data.physicalResources);
_cached.placedResources.AddRange(data.placedResources);
_cached.barriers.AddRange(data.barriers);
foreach (var kvp in data.resourceStates)

View File

@@ -75,6 +75,21 @@ internal sealed class MockCommandBuffer
{
#if DEBUG
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
#endif
}
public void BeginRenderPass(int nativePassIndex, int colorAttachmentCount, bool hasDepth)
{
#if DEBUG
Console.WriteLine($"\n=== BeginRenderPass (Native Pass {nativePassIndex}) ===");
Console.WriteLine($" Color attachments: {colorAttachmentCount}, Depth: {hasDepth}");
#endif
}
public void EndRenderPass()
{
#if DEBUG
Console.WriteLine("=== EndRenderPass ===\n");
#endif
}
}

View File

@@ -0,0 +1,52 @@
using Ghost.Core;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents a native render pass that can contain multiple merged logical passes.
/// Maps to D3D12 BeginRenderPass/EndRenderPass or Vulkan vkCmdBeginRenderPass/vkCmdEndRenderPass.
/// </summary>
internal sealed class NativeRenderPass
{
public int index;
/// <summary>
/// Indices of logical passes merged into this native render pass.
/// </summary>
public readonly List<int> mergedPassIndices = new(4);
/// <summary>
/// Color attachments shared across all merged passes.
/// </summary>
public RenderTargetInfo[] colorAttachments = new RenderTargetInfo[8];
public int colorAttachmentCount;
/// <summary>
/// Depth-stencil attachment (optional).
/// </summary>
public DepthStencilInfo depthAttachment;
public bool hasDepthAttachment;
/// <summary>
/// Range of logical passes included in this native pass.
/// </summary>
public int firstLogicalPass;
public int lastLogicalPass;
/// <summary>
/// Whether UAV writes are allowed during this render pass.
/// </summary>
public bool allowUAVWrites;
public void Reset()
{
index = -1;
mergedPassIndices.Clear();
colorAttachmentCount = 0;
hasDepthAttachment = false;
depthAttachment = default;
firstLogicalPass = int.MaxValue;
lastLogicalPass = -1;
allowUAVWrites = false;
}
}

View File

@@ -1,4 +1,5 @@
using Ghost.Core;
using System.Runtime.CompilerServices;
namespace Ghost.RenderGraph.Concept;
@@ -34,6 +35,9 @@ internal abstract class RenderGraphPassBase
public readonly List<Identifier<RGResource>>[] resourceWrites = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
public readonly List<Identifier<RGResource>>[] resourceCreates = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
// Buffer usage hints (maps buffer resource ID to hint)
public readonly Dictionary<int, BufferHint> bufferHints = new(8);
// Execution state
public bool culled;
public bool hasSideEffects;
@@ -49,8 +53,8 @@ internal abstract class RenderGraphPassBase
}
public abstract void Execute(RenderContext context);
public abstract void Clear();
public abstract bool HasRenderFunc();
public abstract int GetRenderFuncHashCode();
public virtual void Reset(RenderGraphObjectPool pool)
{
@@ -73,6 +77,8 @@ internal abstract class RenderGraphPassBase
resourceCreates[i].Clear();
}
bufferHints.Clear();
culled = false;
hasSideEffects = false;
}
@@ -97,17 +103,24 @@ internal abstract class RenderGraphPassT<TPassData, TRenderContext> : RenderGrap
return renderFunc != null;
}
public override void Clear()
public override int GetRenderFuncHashCode()
{
passData = null!;
renderFunc = null;
if (renderFunc == null)
{
return 0;
}
var methodHashCode = RuntimeHelpers.GetHashCode(renderFunc.Method);
return renderFunc.Target == null ? methodHashCode : methodHashCode ^ RuntimeHelpers.GetHashCode(renderFunc.Target); // static deleget does not have target
}
public override void Reset(RenderGraphObjectPool pool)
{
base.Reset(pool);
pool.Return(passData);
Clear();
passData = null!;
renderFunc = null;
}
}

View File

@@ -1,129 +0,0 @@
using Ghost.Core;
using System.IO;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents different types of render passes.
/// </summary>
public enum RenderPassType : byte
{
Raster,
Compute
}
/// <summary>
/// Base class for render passes.
/// Uses pooling to avoid allocations after the first frame.
/// </summary>
internal abstract class RenderGraphPassBase
{
public string name = string.Empty;
public int index;
public RenderPassType type;
public bool allowCulling = true;
public bool asyncCompute;
public TextureAccess depthAccess;
public TextureAccess[] colorAccess = new TextureAccess[8];
public int maxColorIndex = -1;
public List<Identifier<RGResource>> randomAccess = new(8);
// Resource dependencies
public readonly List<Identifier<RGResource>> resourceReads = new(8);
public readonly List<Identifier<RGResource>> resourceWrites = new(4);
public readonly List<Identifier<RGResource>> resourceCreates = new(4);
// Execution state
public bool culled;
public bool hasSideEffects;
public abstract void Execute(RenderContext context);
public abstract void Clear();
public abstract bool HasRenderFunc();
public virtual void Reset(RenderGraphObjectPool pool)
{
name = string.Empty;
index = -1;
type = RenderPassType.Raster;
allowCulling = true;
asyncCompute = false;
depthAccess = default;
colorAccess.AsSpan().Clear();
maxColorIndex = -1;
randomAccess.Clear();
resourceReads.Clear();
resourceWrites.Clear();
resourceCreates.Clear();
culled = false;
hasSideEffects = false;
}
}
internal abstract class RenderGraphPassT<TPassData, TRenderContext> : RenderGraphPassBase
where TPassData : class, new()
{
public TPassData passData = null!;
public Action<TPassData, TRenderContext>? renderFunc;
public void Init(int index, TPassData passData, string name, RenderPassType type)
{
this.index = index;
this.passData = passData;
this.name = name;
this.type = type;
}
public sealed override bool HasRenderFunc()
{
return renderFunc != null;
}
public override void Clear()
{
passData = null!;
renderFunc = null;
}
public override void Reset(RenderGraphObjectPool pool)
{
base.Reset(pool);
pool.Return(passData);
Clear();
}
}
internal sealed class RasterRenderGraphPass<TPassData> : RenderGraphPassT<TPassData, RasterRenderContext>
where TPassData : class, new()
{
public override void Execute(RenderContext context)
{
renderFunc!(passData, context.RasterContext);
}
public override void Reset(RenderGraphObjectPool pool)
{
base.Reset(pool);
pool.Return(this);
}
}
internal sealed class ComputeRenderGraphPass<TPassData> : RenderGraphPassT<TPassData, ComputeRenderContext>
where TPassData : class, new()
{
public override void Execute(RenderContext context)
{
renderFunc!(passData, context.ComputeContext);
}
public override void Reset(RenderGraphObjectPool pool)
{
base.Reset(pool);
pool.Return(this);
}
}

View File

@@ -75,13 +75,14 @@ internal sealed class RenderGraphObjectPool
}
/// <summary>
/// Represents a texture resource in the render graph.
/// Represents a resource in the render graph (texture or buffer).
/// </summary>
internal sealed class RenderGraphResource
{
public RenderGraphResourceType type;
public int index;
public TextureDescriptor descriptor;
public TextureDescriptor textureDescriptor;
public BufferDescriptor bufferDescriptor;
public bool isImported;
public int firstUsePass = -1;
public int lastUsePass = -1;
@@ -91,8 +92,10 @@ internal sealed class RenderGraphResource
public void Reset()
{
type = RenderGraphResourceType.Texture;
index = -1;
descriptor = default;
textureDescriptor = default;
bufferDescriptor = default;
isImported = false;
firstUsePass = -1;
lastUsePass = -1;
@@ -105,21 +108,48 @@ internal sealed class RenderGraphResource
/// <summary>
/// Registry for managing all resources in the render graph.
/// Uses pooling to minimize allocations after the first frame.
/// Uses a single unified list for both textures and buffers with global indexing.
/// </summary>
internal sealed class RenderGraphResourceRegistry
{
private readonly List<RenderGraphResource> _resources = new(64);
private readonly RenderGraphObjectPool _pool = new();
public int TextureResourceCount => _resources.Count;
public int ResourceCount => _resources.Count;
public int TextureResourceCount
{
get
{
int count = 0;
for (int i = 0; i < _resources.Count; i++)
{
if (_resources[i].type == RenderGraphResourceType.Texture)
count++;
}
return count;
}
}
public int BufferResourceCount
{
get
{
int count = 0;
for (int i = 0; i < _resources.Count; i++)
{
if (_resources[i].type == RenderGraphResourceType.Buffer)
count++;
}
return count;
}
}
public void BeginFrame()
{
// Return all resources to pool
for (var i = 0; i < _resources.Count; i++)
{
_pool.Return(_resources[i]);
}
_resources.Clear();
}
@@ -128,7 +158,7 @@ internal sealed class RenderGraphResourceRegistry
var resource = _pool.Rent<RenderGraphResource>();
resource.type = RenderGraphResourceType.Texture;
resource.index = _resources.Count;
resource.descriptor = descriptor;
resource.textureDescriptor = descriptor;
resource.isImported = true;
_resources.Add(resource);
@@ -141,20 +171,59 @@ internal sealed class RenderGraphResourceRegistry
var resource = _pool.Rent<RenderGraphResource>();
resource.type = RenderGraphResourceType.Texture;
resource.index = _resources.Count;
resource.descriptor = descriptor;
resource.textureDescriptor = descriptor;
resource.isImported = false;
_resources.Add(resource);
return new Identifier<RGTexture>(resource.index);
}
public Identifier<RGBuffer> ImportBuffer(BufferDescriptor descriptor)
{
var resource = _pool.Rent<RenderGraphResource>();
resource.type = RenderGraphResourceType.Buffer;
resource.index = _resources.Count;
resource.bufferDescriptor = descriptor;
resource.isImported = true;
_resources.Add(resource);
return new Identifier<RGBuffer>(resource.index);
}
public Identifier<RGBuffer> CreateBuffer(BufferDescriptor descriptor)
{
var resource = _pool.Rent<RenderGraphResource>();
resource.type = RenderGraphResourceType.Buffer;
resource.index = _resources.Count;
resource.bufferDescriptor = descriptor;
resource.isImported = false;
_resources.Add(resource);
return new Identifier<RGBuffer>(resource.index);
}
public RenderGraphResource GetResource(Identifier<RGResource> resource)
{
return _resources[resource.Value];
}
public RenderGraphResource GetTextureResourceByIndex(int index)
public RenderGraphResource GetResource(Identifier<RGTexture> texture)
{
return _resources[texture.Value];
}
public RenderGraphResource GetResource(Identifier<RGBuffer> buffer)
{
return _resources[buffer.Value];
}
/// <summary>
/// Gets resource by global index. Use this when iterating over all resources.
/// </summary>
public RenderGraphResource GetResourceByIndex(int index)
{
return _resources[index];
}

View File

@@ -42,6 +42,25 @@ public static class RGResourceExtensions
}
}
/// <summary>
/// Hints for how a buffer will be used in a pass.
/// Used to determine correct resource state transitions.
/// </summary>
[Flags]
public enum BufferHint
{
/// <summary>
/// No special usage - buffer will be used as shader resource (SRV) or UAV based on AccessFlags.
/// </summary>
None = 0,
/// <summary>
/// Buffer will be used as indirect argument buffer (ExecuteIndirect).
/// Requires ResourceState.IndirectArgument.
/// </summary>
IndirectArgument = 1 << 0,
}
internal readonly struct TextureAccess
{
public readonly Identifier<RGTexture> id;
@@ -54,6 +73,23 @@ internal readonly struct TextureAccess
}
}
/// <summary>
/// Tracks buffer access information including usage hints.
/// </summary>
internal readonly struct BufferAccess
{
public readonly Identifier<RGBuffer> id;
public readonly AccessFlags accessFlags;
public readonly BufferHint hint;
public BufferAccess(Identifier<RGBuffer> id, AccessFlags accessFlags, BufferHint hint = BufferHint.None)
{
this.id = id;
this.accessFlags = accessFlags;
this.hint = hint;
}
}
/// <summary>
/// Texture formats supported by the render graph.
/// </summary>
@@ -165,3 +201,70 @@ public readonly struct BufferDescriptor : IEquatable<BufferDescriptor>
public interface IPassData
{
}
/// <summary>
/// Specifies how to load attachment contents at the beginning of a render pass.
/// </summary>
public enum AttachmentLoadOp
{
/// <summary>
/// Load existing contents from memory. Required when reading previous data.
/// </summary>
Load,
/// <summary>
/// Clear attachment to a specified value.
/// </summary>
Clear,
/// <summary>
/// Don't care about previous contents (best performance on TBDR GPUs).
/// Use when you guarantee to overwrite all pixels.
/// </summary>
DontCare
}
/// <summary>
/// Specifies how to store attachment contents at the end of a render pass.
/// </summary>
public enum AttachmentStoreOp
{
/// <summary>
/// Store contents to memory. Required if resource is used after this pass.
/// </summary>
Store,
/// <summary>
/// Don't care about storing contents (best performance on TBDR GPUs).
/// Use when resource is not needed after this pass.
/// </summary>
DontCare
}
/// <summary>
/// Information about a render target attachment in a native render pass.
/// </summary>
internal struct RenderTargetInfo
{
public Identifier<RGTexture> texture;
public AccessFlags access;
public AttachmentLoadOp loadOp;
public AttachmentStoreOp storeOp;
public float clearR;
public float clearG;
public float clearB;
public float clearA;
}
/// <summary>
/// Information about a depth-stencil attachment in a native render pass.
/// </summary>
internal struct DepthStencilInfo
{
public Identifier<RGTexture> texture;
public AccessFlags access;
public AttachmentLoadOp loadOp;
public AttachmentStoreOp storeOp;
public float clearDepth;
public byte clearStencil;
}