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:
9
Ghost.Core/Utilities/EnumUtility.cs
Normal file
9
Ghost.Core/Utilities/EnumUtility.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
|
||||
internal class EnumUtility
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -109,11 +107,8 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
}
|
||||
|
||||
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
|
||||
{
|
||||
_renderSystem.SignalCPUReady();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,6 +70,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
||||
CreateBackBuffers();
|
||||
SetScale(desc.ScaleX, desc.ScaleY);
|
||||
|
||||
if (desc.Target.Type == SwapChainTargetType.Composition)
|
||||
_compositionSurface = desc.Target.CompositionSurface;
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public enum FeatureSupport
|
||||
SamplerFeedback = 1 << 3,
|
||||
BindlessResources = 1 << 4,
|
||||
WorkGraphs = 1 << 5,
|
||||
AliasBuffersAndTextures = 1 << 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,6 +70,14 @@ 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;
|
||||
}
|
||||
|
||||
@@ -79,6 +89,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,26 +219,30 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
//// 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);
|
||||
//}
|
||||
*(int*)(pData + offset) = pass.GetRenderFuncHashCode();
|
||||
offset += sizeof(int);
|
||||
}
|
||||
|
||||
var span = new Span<byte>(pData, offset);
|
||||
return XxHash64.HashToUInt64(span);
|
||||
@@ -317,7 +345,10 @@ public sealed class RenderGraph
|
||||
// Step 6: Generate barriers for state transitions and aliasing
|
||||
GenerateBarriers();
|
||||
|
||||
// Step 7: Store in cache for future frames
|
||||
// Step 7: Build native render passes by merging compatible passes
|
||||
BuildNativeRenderPasses();
|
||||
|
||||
// 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,48 +507,55 @@ 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 writeList = pass.resourceWrites[resType];
|
||||
for (var i = 0; i < writeList.Count; i++)
|
||||
{
|
||||
var id = writeList[i];
|
||||
var resource = _resources.GetResource(id);
|
||||
|
||||
// Skip imported resources
|
||||
if (resource.isImported)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is the first use of this logical resource
|
||||
if (resource.firstUsePass == pass.index)
|
||||
{
|
||||
// Rent the physical resource
|
||||
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
|
||||
if (physicalIndex >= 0)
|
||||
// Get the placed resource
|
||||
var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value);
|
||||
if (placedIndex >= 0)
|
||||
{
|
||||
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
|
||||
var placed = _aliasingManager.GetPlacedResource(placedIndex);
|
||||
|
||||
// If this physical resource has multiple aliased resources,
|
||||
// If this placed resource has multiple aliased resources,
|
||||
// we need an aliasing barrier when switching between them
|
||||
if (physical != null && physical.aliasedLogicalResources.Count > 1)
|
||||
if (placed != null && placed.aliasedLogicalResources.Count > 1)
|
||||
{
|
||||
// Find the resource that used this physical memory most recently before this pass
|
||||
// Find the resource that used this placed memory most recently before this pass
|
||||
Identifier<RGResource> resourceBefore = default;
|
||||
var mostRecentLastUse = -1;
|
||||
|
||||
foreach (var otherLogicalIndex in physical.aliasedLogicalResources)
|
||||
foreach (var otherLogicalIndex in placed.aliasedLogicalResources)
|
||||
{
|
||||
if (otherLogicalIndex != id.Value)
|
||||
{
|
||||
var otherResource = _resources.GetTextureResourceByIndex(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 = otherLogicalIndex;
|
||||
resourceBefore = new Identifier<RGResource>(otherLogicalIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,20 +579,22 @@ public sealed class RenderGraph
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts transition barriers when a resource changes state.
|
||||
/// </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,22 +1051,81 @@ public sealed class RenderGraph
|
||||
Compile();
|
||||
}
|
||||
|
||||
// Execute each non-culled pass
|
||||
var barrierIndex = 0;
|
||||
for (var i = 0; i < _compiledPasses.Count; i++)
|
||||
{
|
||||
var pass = _compiledPasses[i];
|
||||
var nativePassIndex = 0;
|
||||
var logicalPassIndex = 0;
|
||||
|
||||
// Execute all barriers for this pass
|
||||
while (logicalPassIndex < _compiledPasses.Count)
|
||||
{
|
||||
var pass = _compiledPasses[logicalPassIndex];
|
||||
|
||||
// Check if this pass is part of a native render pass
|
||||
if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count)
|
||||
{
|
||||
var nativePass = _nativePasses[nativePassIndex];
|
||||
|
||||
// Execute barriers for ALL merged passes before beginning the native render pass
|
||||
foreach (var mergedPassIdx in nativePass.mergedPassIndices)
|
||||
{
|
||||
ExecuteBarriersForPass(mergedPassIdx, ref barrierIndex);
|
||||
}
|
||||
|
||||
// Begin native render pass
|
||||
_commandBuffer.BeginRenderPass(
|
||||
nativePass.index,
|
||||
nativePass.colorAttachmentCount,
|
||||
nativePass.hasDepthAttachment
|
||||
);
|
||||
|
||||
// Execute all merged logical passes within this native render pass
|
||||
for (var i = 0; i < nativePass.mergedPassIndices.Count; i++)
|
||||
{
|
||||
var mergedPassIdx = nativePass.mergedPassIndices[i];
|
||||
var mergedPass = _compiledPasses[mergedPassIdx];
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\n--- Executing Pass {mergedPassIdx}: {mergedPass.name} (in Native Pass {nativePass.index}) ---");
|
||||
#endif
|
||||
|
||||
mergedPass.Execute(_renderContext);
|
||||
logicalPassIndex++;
|
||||
}
|
||||
|
||||
// End native render pass
|
||||
_commandBuffer.EndRenderPass();
|
||||
|
||||
nativePassIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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);
|
||||
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 == i)
|
||||
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!hasBarriers)
|
||||
{
|
||||
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
|
||||
var pass = _compiledPasses[passIndex];
|
||||
Console.WriteLine($"\n=== Barriers before Pass {passIndex}: {pass.name} ===");
|
||||
hasBarriers = true;
|
||||
}
|
||||
|
||||
@@ -679,8 +1157,5 @@ public sealed class RenderGraph
|
||||
Console.WriteLine("=====================================\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
pass.Execute(_renderContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,256 @@
|
||||
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;
|
||||
@@ -19,26 +258,21 @@ internal sealed class PhysicalResource
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// ===== PASS 1: Simulate allocation to determine peak memory usage =====
|
||||
var simulationHeap = new ResourceHeap(0, ulong.MaxValue); // Unlimited size for simulation
|
||||
|
||||
// Greedy interval scheduling: assign each logical resource to a physical resource
|
||||
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];
|
||||
size = CalculateTextureSize(logicalResource.textureDescriptor);
|
||||
alignment = DefaultTextureAlignment;
|
||||
}
|
||||
else // Buffer
|
||||
{
|
||||
size = logicalResource.bufferDescriptor.sizeInBytes;
|
||||
alignment = DefaultBufferAlignment;
|
||||
}
|
||||
|
||||
if (physical.CanAlias(logicalResource.descriptor) &&
|
||||
!HasLifetimeOverlap(physical, logicalResource))
|
||||
var (success, offset, block) = simulationHeap.TryAllocate(
|
||||
size,
|
||||
logicalResource.firstUsePass,
|
||||
logicalResource.lastUsePass,
|
||||
logicalIndex,
|
||||
alignment);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
assignedPhysical = physical;
|
||||
break;
|
||||
throw new InvalidOperationException("Simulation allocation failed - this should never happen with unlimited heap");
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
// 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");
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
ulong totalPhysicalSize = 0;
|
||||
for (var i = 0; i < _heaps.Count; i++)
|
||||
{
|
||||
totalPhysicalSize += _heaps[i].GetPeakUsage();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== Aliasing Summary ===");
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -27,6 +30,13 @@ public interface IRenderGraphBuilder : IDisposable
|
||||
/// <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.
|
||||
/// </summary>
|
||||
@@ -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.
|
||||
@@ -147,12 +168,35 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
|
||||
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)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
52
Ghost.RenderGraph.Concept/RenderGraphNativePass.cs
Normal file
52
Ghost.RenderGraph.Concept/RenderGraphNativePass.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,7 +171,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 = false;
|
||||
|
||||
_resources.Add(resource);
|
||||
@@ -149,12 +179,51 @@ internal sealed class RenderGraphResourceRegistry
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user