forked from Misaki/GhostEngine
Render graph: native pass merging & heap-based aliasing
Major architecture upgrade: - Add native render pass merging (hardware pass grouping, load/store op inference) - Implement heap-based aliasing for textures & buffers (D3D12-style) - Unify resource model: buffers and textures in one registry - Extend builder API for buffer creation/usage, access flags, hints - Improve barrier/state tracking (buffer hints, indirect argument state) - Update caching, hashing, and debug output for new model - Add enums/structs: AttachmentLoadOp, StoreOp, BufferHint, etc. - D3D12 backend: support named resources, temp upload buffers, correct heap usage - Update docs, benchmarks, and project files for new features Brings render graph closer to AAA engine standards, enabling efficient memory usage, lower driver overhead, and a more flexible API.
This commit is contained in:
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.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using TerraFX.Interop.Windows;
|
using TerraFX.Interop.Windows;
|
||||||
|
using TerraFX.Interop.WinRT;
|
||||||
|
|
||||||
namespace Ghost.Core.Utilities;
|
namespace Ghost.Core.Utilities;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="using:Ghost.Editor.Core.Controls">
|
xmlns:local="using:Ghost.Editor.Core.Controls">
|
||||||
|
|
||||||
<Style TargetType="local:Vector3Field">
|
<Style TargetType="local:Float3Field">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="local:Vector3Field">
|
<ControlTemplate TargetType="local:Float3Field">
|
||||||
<Grid ColumnSpacing="4">
|
<Grid ColumnSpacing="4">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public partial class World
|
|||||||
var world = s_worlds[id.Value];
|
var world = s_worlds[id.Value];
|
||||||
return world is null ? throw new InvalidOperationException("World not found.") : world;
|
return world is null ? throw new InvalidOperationException("World not found.") : world;
|
||||||
#else
|
#else
|
||||||
return s_worlds[id.value]!;
|
return s_worlds[id.Value]!;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,6 @@
|
|||||||
<EnableMsixTooling>true</EnableMsixTooling>
|
<EnableMsixTooling>true</EnableMsixTooling>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="Controls\DebugConsole.xaml" />
|
|
||||||
<None Remove="Windows\DebugOutputWindow.xaml" />
|
|
||||||
<None Remove="Windows\WorkGraphTestWindow.xaml" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Remove="UnitTestApp.xaml" />
|
<Page Remove="UnitTestApp.xaml" />
|
||||||
@@ -48,8 +43,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.1" />
|
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.1" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
|
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
|
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -57,11 +52,6 @@
|
|||||||
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
|
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
|
||||||
<ProjectReference Include="..\Ghost.Test.Core\Ghost.Test.Core.csproj" />
|
<ProjectReference Include="..\Ghost.Test.Core\Ghost.Test.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Page Update="Windows\WorkGraphTestWindow.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ghost.Core;
|
||||||
using Ghost.Graphics.Test.Windows;
|
using Ghost.Graphics.Test.Windows;
|
||||||
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
@@ -49,13 +50,15 @@ public partial class UnitTestApp : Application
|
|||||||
{
|
{
|
||||||
LoadDll();
|
LoadDll();
|
||||||
|
|
||||||
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
|
|
||||||
|
|
||||||
_window = new GraphicsTestWindow();
|
_window = new GraphicsTestWindow();
|
||||||
_window.Activate();
|
_window.Activate();
|
||||||
|
|
||||||
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
|
UnhandledException += (sender, e) =>
|
||||||
|
{
|
||||||
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
|
Logger.LogError(e.Exception);
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debugger.Break();
|
||||||
|
#endif
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,5 @@
|
|||||||
Height="4"
|
Height="4"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Background="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
|
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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ public sealed partial class GraphicsTestWindow : Window
|
|||||||
_swapChain?.Dispose();
|
_swapChain?.Dispose();
|
||||||
_renderSystem?.Dispose();
|
_renderSystem?.Dispose();
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.Dispose();
|
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.Dispose();
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
@@ -110,10 +108,7 @@ public sealed partial class GraphicsTestWindow : Window
|
|||||||
|
|
||||||
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
|
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
|
||||||
{
|
{
|
||||||
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
|
_renderSystem.SignalCPUReady();
|
||||||
{
|
|
||||||
_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;
|
global using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
using Ghost.Core.Attributes;
|
using Ghost.Core.Attributes;
|
||||||
using Ghost.Core.Utilities;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ using System.Runtime.Versioning;
|
|||||||
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
|
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Graphics.Test")]
|
[assembly: InternalsVisibleTo("Ghost.Graphics.Test")]
|
||||||
|
[assembly: InternalsVisibleTo("Ghost.Graphics.Test-Winui")]
|
||||||
[assembly: SupportedOSPlatform("windows10.0.19041.0")]
|
[assembly: SupportedOSPlatform("windows10.0.19041.0")]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public unsafe struct LocalKeywordSet
|
|||||||
|
|
||||||
public ulong GetHash64()
|
public ulong GetHash64()
|
||||||
{
|
{
|
||||||
ulong hash = 14695981039346656037ul; // FNV offset basis
|
ulong hash = 14695981039346656037ul; // FNV Offset basis
|
||||||
|
|
||||||
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
|
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public struct Material : IResourceReleasable
|
|||||||
MemoryType = ResourceMemoryType.Default,
|
MemoryType = ResourceMemoryType.Default,
|
||||||
};
|
};
|
||||||
|
|
||||||
var buffer = allocator.CreateBuffer(ref desc);
|
var buffer = allocator.CreateBuffer(ref desc, "MaterialCBuffer");
|
||||||
_cBufferCache = new CBufferCache(buffer, shader.CBufferSize);
|
_cBufferCache = new CBufferCache(buffer, shader.CBufferSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,14 +214,15 @@ public struct Material : IResourceReleasable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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
|
var state = pixelOnlyResource
|
||||||
? ResourceState.PixelShaderResource
|
? ResourceState.PixelShaderResource
|
||||||
: ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource;
|
: ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource;
|
||||||
cmb.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), state);
|
cmd.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ public readonly unsafe ref struct RenderingContext
|
|||||||
_directCmd.ResourceBarrier(bufferHandle, ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource);
|
_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
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
var handle = ResourceAllocator.CreateTexture(in desc, tempResource);
|
var handle = ResourceAllocator.CreateTexture(in desc, name);
|
||||||
UploadTexture(handle, data);
|
UploadTexture(handle, data);
|
||||||
|
|
||||||
return handle;
|
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
|
// Set descriptor heaps for bindless resources and samplers
|
||||||
|
|
||||||
var heaps = stackalloc ID3D12DescriptorHeap*[2];
|
var heaps = stackalloc ID3D12DescriptorHeap*[2];
|
||||||
heaps[0] = _descriptorAllocator.GetCbvSrvUavHeap(); // Bindless resource heap
|
heaps[0] = _descriptorAllocator.GetCbvSrvUavHeap(); // Bindless resource Heap
|
||||||
heaps[1] = _descriptorAllocator.GetSamplerHeap(); // Bindless sampler heap
|
heaps[1] = _descriptorAllocator.GetSamplerHeap(); // Bindless sampler Heap
|
||||||
_commandList.Get()->SetDescriptorHeaps(2, heaps);
|
_commandList.Get()->SetDescriptorHeaps(2, heaps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,20 +401,39 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
|
|||||||
var format = record.desc.TextureDescription.Format.ToDXGIFormat();
|
var format = record.desc.TextureDescription.Format.ToDXGIFormat();
|
||||||
var clearColor = rtDesc.ClearColor;
|
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
|
var desc = new D3D12_RENDER_PASS_RENDER_TARGET_DESC
|
||||||
{
|
{
|
||||||
cpuDescriptor = cpuHandle,
|
cpuDescriptor = cpuHandle,
|
||||||
BeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
|
BeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
|
||||||
{
|
{
|
||||||
Type = D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
|
Type = loadAccessType,
|
||||||
Clear = new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
|
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)
|
{
|
||||||
}
|
ClearValue = new D3D12_CLEAR_VALUE(format, (float*)&clearColor)
|
||||||
|
}
|
||||||
|
: default
|
||||||
},
|
},
|
||||||
EndingAccess = new D3D12_RENDER_PASS_ENDING_ACCESS
|
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 cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
|
||||||
var format = record.desc.TextureDescription.Format.ToDXGIFormat();
|
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
|
var desc = new D3D12_RENDER_PASS_DEPTH_STENCIL_DESC
|
||||||
{
|
{
|
||||||
cpuDescriptor = cpuHandle,
|
cpuDescriptor = cpuHandle,
|
||||||
DepthBeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
|
DepthBeginningAccess = new D3D12_RENDER_PASS_BEGINNING_ACCESS
|
||||||
{
|
{
|
||||||
Type = D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR,
|
Type = depthLoadAccessType,
|
||||||
Clear = new D3D12_RENDER_PASS_BEGINNING_ACCESS_CLEAR_PARAMETERS
|
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)
|
{
|
||||||
}
|
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 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());
|
var uploadResource = _resourceDatabase.GetResource(uploadHandle.AsResource());
|
||||||
|
|
||||||
void* pMappedData;
|
void* pMappedData;
|
||||||
uploadResource.Get()->Map(0, null, &pMappedData);
|
uploadResource.Get()->Map(0, null, &pMappedData);
|
||||||
fixed (T* pData = data)
|
fixed (T* pData = data)
|
||||||
{
|
{
|
||||||
MemoryUtility.MemCpy(pMappedData, pData, sizeInBytes);
|
MemoryUtility.MemCpy((byte*)pMappedData + offset, pData, sizeInBytes);
|
||||||
}
|
}
|
||||||
uploadResource.Get()->Unmap(0, null);
|
uploadResource.Get()->Unmap(0, null);
|
||||||
|
|
||||||
var pResource = _resourceDatabase.GetResource(buffer.AsResource());
|
var pResource = _resourceDatabase.GetResource(buffer.AsResource());
|
||||||
|
_commandList.Get()->CopyBufferRegion(pResource, 0, uploadResource, offset, sizeInBytes);
|
||||||
_commandList.Get()->CopyBufferRegion(pResource, 0, uploadResource, 0, sizeInBytes);
|
|
||||||
// D3D12 transition resource to COPY_DEST when copying
|
|
||||||
_resourceDatabase.SetResourceState(buffer.AsResource(), ResourceState.CopyDest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UploadTexture(Handle<Texture> texture, ReadOnlySpan<SubResourceData> subresources)
|
public void UploadTexture(Handle<Texture> texture, ReadOnlySpan<SubResourceData> subresources)
|
||||||
@@ -766,7 +836,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
|
|||||||
var resourceDesc = resource.Get()->GetDesc();
|
var resourceDesc = resource.Get()->GetDesc();
|
||||||
var requiredSize = GetRequiredIntermediateSize(resource, 0, (uint)subresources.Length);
|
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 pUploadResource = _resourceDatabase.GetResource(uploadHandle.AsResource());
|
||||||
|
|
||||||
var d3d12Subresources = stackalloc D3D12_SUBRESOURCE_DATA[subresources.Length];
|
var d3d12Subresources = stackalloc D3D12_SUBRESOURCE_DATA[subresources.Length];
|
||||||
@@ -784,7 +854,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
|
|||||||
(ID3D12GraphicsCommandList*)_commandList.Get(),
|
(ID3D12GraphicsCommandList*)_commandList.Get(),
|
||||||
resource,
|
resource,
|
||||||
pUploadResource,
|
pUploadResource,
|
||||||
0,
|
offset,
|
||||||
0,
|
0,
|
||||||
(uint)subresources.Length,
|
(uint)subresources.Length,
|
||||||
d3d12Subresources);
|
d3d12Subresources);
|
||||||
|
|||||||
@@ -386,22 +386,22 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable
|
|||||||
#region Utility Methods
|
#region Utility Methods
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the RTV heap for binding to the command list.
|
/// Gets the RTV Heap for binding to the command list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ID3D12DescriptorHeap* GetRTVHeap() => _rtvHeap.Heap;
|
public ID3D12DescriptorHeap* GetRTVHeap() => _rtvHeap.Heap;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the DSV heap for binding to the command list.
|
/// Gets the DSV Heap for binding to the command list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ID3D12DescriptorHeap* GetDSVHeap() => _dsvHeap.Heap;
|
public ID3D12DescriptorHeap* GetDSVHeap() => _dsvHeap.Heap;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public ID3D12DescriptorHeap* GetCbvSrvUavHeap() => _cbvSrvUavHeap.ShaderVisibleHeap;
|
public ID3D12DescriptorHeap* GetCbvSrvUavHeap() => _cbvSrvUavHeap.ShaderVisibleHeap;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the sampler heap for binding to the command list.
|
/// Gets the sampler Heap for binding to the command list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ID3D12DescriptorHeap* GetSamplerHeap() => _samplerHeap.ShaderVisibleHeap;
|
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.
|
// 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)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
|
|||||||
}
|
}
|
||||||
|
|
||||||
var size = _library.Get()->GetSerializedSize();
|
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));
|
ThrowIfFailed(_library.Get()->Serialize(buffer.GetUnsafePtr(), size));
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ internal unsafe class D3D12RenderDevice : IRenderDevice
|
|||||||
{
|
{
|
||||||
support |= FeatureSupport.BindlessResources;
|
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;
|
D3D12_FEATURE_DATA_D3D12_OPTIONS5 options5 = default;
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ internal class D3D12Renderer : IRenderer
|
|||||||
{
|
{
|
||||||
Texture = target,
|
Texture = target,
|
||||||
ClearColor = clearColor,
|
ClearColor = clearColor,
|
||||||
|
LoadOp = AttachmentLoadOp.Clear,
|
||||||
|
StoreOp = AttachmentStoreOp.Store,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -103,6 +105,10 @@ internal class D3D12Renderer : IRenderer
|
|||||||
Texture = Handle<Texture>.Invalid,
|
Texture = Handle<Texture>.Invalid,
|
||||||
ClearDepth = 1.0f,
|
ClearDepth = 1.0f,
|
||||||
ClearStencil = 0,
|
ClearStencil = 0,
|
||||||
|
DepthLoadOp = AttachmentLoadOp.Clear,
|
||||||
|
StencilLoadOp = AttachmentLoadOp.Clear,
|
||||||
|
DepthStoreOp = AttachmentStoreOp.Store,
|
||||||
|
StencilStoreOp = AttachmentStoreOp.Store,
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: Testing only.
|
// NOTE: Testing only.
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ using Ghost.Graphics.Core;
|
|||||||
using Ghost.Graphics.D3D12.Utilities;
|
using Ghost.Graphics.D3D12.Utilities;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.LowLevel;
|
using Misaki.HighPerformance.LowLevel;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Xml.Linq;
|
||||||
using TerraFX.Interop.DirectX;
|
using TerraFX.Interop.DirectX;
|
||||||
using TerraFX.Interop.Windows;
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
@@ -512,9 +514,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator
|
|||||||
|
|
||||||
var state = D3D12_RESOURCE_STATE_COMMON;
|
var state = D3D12_RESOURCE_STATE_COMMON;
|
||||||
#if true
|
#if true
|
||||||
|
// D3D12 does not support state other than COMMON for buffers at creation.
|
||||||
return state;
|
return state;
|
||||||
#else
|
#else
|
||||||
// D3D12 does not support state other than COMMON for buffers at creation.
|
|
||||||
if (usage.HasFlag(BufferUsage.Vertex) || usage.HasFlag(BufferUsage.Constant))
|
if (usage.HasFlag(BufferUsage.Vertex) || usage.HasFlag(BufferUsage.Constant))
|
||||||
{
|
{
|
||||||
// Vertex and Constant buffers can share this state
|
// 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
|
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 UniquePtr<D3D12MA_Allocator> _d3d12MA;
|
||||||
|
|
||||||
private readonly IFenceSynchronizer _fenceSynchronizer;
|
private readonly IFenceSynchronizer _fenceSynchronizer;
|
||||||
@@ -574,6 +574,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
|
|
||||||
private UnsafeQueue<Handle<GPUResource>> _tempResources;
|
private UnsafeQueue<Handle<GPUResource>> _tempResources;
|
||||||
|
|
||||||
|
private readonly Handle<GraphicsBuffer> _uploadBatch;
|
||||||
|
private ulong _uploadBatchOffset;
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public D3D12ResourceAllocator(
|
public D3D12ResourceAllocator(
|
||||||
@@ -600,7 +603,18 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
_resourceDatabase = resourceDatabase;
|
_resourceDatabase = resourceDatabase;
|
||||||
_pipelineLibrary = pipelineLibrary;
|
_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()
|
~D3D12ResourceAllocator()
|
||||||
@@ -609,9 +623,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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)
|
if (isTemp)
|
||||||
{
|
{
|
||||||
@@ -621,7 +635,70 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
return handle;
|
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);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
@@ -681,14 +758,17 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
var initialState = DetermineInitialTextureState(desc.Usage);
|
var initialState = DetermineInitialTextureState(desc.Usage);
|
||||||
|
|
||||||
D3D12MA_Allocation* pAllocation = default;
|
D3D12MA_Allocation* pAllocation = default;
|
||||||
var iid = IID.IID_NULL;
|
if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED)
|
||||||
ThrowIfFailed(_d3d12MA.Get()->CreateResource(&allocationDesc, &resourceDesc, initialState, null, &pAllocation, &iid, null));
|
{
|
||||||
|
return Handle<Texture>.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTemp = options.AllocationType == ResourceAllocationType.Temporary;
|
||||||
var resourceDescriptor = ResourceViewGroup.Invalid;
|
var resourceDescriptor = ResourceViewGroup.Invalid;
|
||||||
if (desc.Usage.HasFlag(TextureUsage.ShaderResource))
|
if (desc.Usage.HasFlag(TextureUsage.ShaderResource))
|
||||||
{
|
{
|
||||||
resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(isTemp);
|
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 cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.srv);
|
||||||
|
|
||||||
var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray;
|
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);
|
_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();
|
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);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
var textureDesc = desc.ToTextureDescripton();
|
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);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
CheckBufferSize(desc.Size);
|
CheckBufferSize(desc.Size);
|
||||||
@@ -749,21 +829,24 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
alignedSize = (uint)(desc.Size + 255) & ~255u;
|
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 isRaw = desc.Usage.HasFlag(BufferUsage.Raw);
|
||||||
|
|
||||||
var allocationDesc = new D3D12MA_ALLOCATION_DESC
|
var allocationDesc = new D3D12MA_ALLOCATION_DESC
|
||||||
{
|
{
|
||||||
HeapType = ConvertMemoryType(desc.MemoryType),
|
HeapType = ConvertMemoryType(desc.MemoryType),
|
||||||
Flags = D3D12MA_ALLOCATION_FLAG_NONE
|
Flags = D3D12MA_ALLOCATION_FLAG_NONE,
|
||||||
};
|
};
|
||||||
|
|
||||||
var initialState = DetermineInitialBufferState(desc.Usage, desc.MemoryType);
|
var initialState = DetermineInitialBufferState(desc.Usage, desc.MemoryType);
|
||||||
|
|
||||||
D3D12MA_Allocation* pAllocation = default;
|
D3D12MA_Allocation* pAllocation = default;
|
||||||
var iid = IID.IID_NULL;
|
if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED)
|
||||||
ThrowIfFailed(_d3d12MA.Get()->CreateResource(&allocationDesc, &resourceDescription, initialState, null, &pAllocation, &iid, null));
|
{
|
||||||
|
return Handle<GraphicsBuffer>.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTemp = options.AllocationType == ResourceAllocationType.Temporary;
|
||||||
var resourceDescriptor = ResourceViewGroup.Invalid;
|
var resourceDescriptor = ResourceViewGroup.Invalid;
|
||||||
var pResource = pAllocation->GetResource();
|
var pResource = pAllocation->GetResource();
|
||||||
|
|
||||||
@@ -798,22 +881,35 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
_device.NativeDevice.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
|
_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();
|
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);
|
if (sizeInBytes <= _MAX_RESOURCE_SIZE_TO_FIT_IN_UPLOAD_BATCH && sizeInBytes + _uploadBatchOffset <= _UPLOAD_BATCH_SIZE)
|
||||||
|
|
||||||
var desc = new BufferDesc
|
|
||||||
{
|
{
|
||||||
Size = size,
|
offset = _uploadBatchOffset;
|
||||||
Usage = BufferUsage.Upload,
|
_uploadBatchOffset += sizeInBytes;
|
||||||
MemoryType = ResourceMemoryType.Upload,
|
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)
|
public Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc)
|
||||||
@@ -873,9 +969,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
MemoryType = ResourceMemoryType.Default,
|
MemoryType = ResourceMemoryType.Default,
|
||||||
};
|
};
|
||||||
|
|
||||||
var vertexBuffer = CreateBuffer(in vertexBufferDesc);
|
var vertexBuffer = CreateBuffer(in vertexBufferDesc, "VertexBuffer");
|
||||||
var indexBuffer = CreateBuffer(in indexBufferDesc);
|
var indexBuffer = CreateBuffer(in indexBufferDesc, "IndexBuffer");
|
||||||
var objectBuffer = CreateBuffer(in objectBufferDesc);
|
var objectBuffer = CreateBuffer(in objectBufferDesc, "ObjectBuffer");
|
||||||
|
|
||||||
var data = new Mesh
|
var data = new Mesh
|
||||||
{
|
{
|
||||||
@@ -935,6 +1031,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
_resourceDatabase.ReleaseResource(handle);
|
_resourceDatabase.ReleaseResource(handle);
|
||||||
_tempResources.Dequeue();
|
_tempResources.Dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_uploadBatchOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -951,6 +1049,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
|
|||||||
_resourceDatabase.ReleaseResource(handle);
|
_resourceDatabase.ReleaseResource(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resourceDatabase.ReleaseResource(_uploadBatch.AsResource());
|
||||||
|
|
||||||
_d3d12MA.Dispose();
|
_d3d12MA.Dispose();
|
||||||
_tempResources.Dispose();
|
_tempResources.Dispose();
|
||||||
|
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
|
|
||||||
public ResourceDesc desc;
|
public ResourceDesc desc;
|
||||||
public ResourceViewGroup viewGroup;
|
public ResourceViewGroup viewGroup;
|
||||||
public ResourceUnion resourceUnion;
|
public ResourceUnion resource;
|
||||||
public ResourceState state;
|
public ResourceState state;
|
||||||
public uint cpuFenceValue;
|
public uint cpuFenceValue;
|
||||||
public readonly bool isExternal;
|
public readonly bool isExternal;
|
||||||
|
|
||||||
public readonly bool Allocated => isExternal ? resourceUnion.resource.Get() != null : resourceUnion.allocation.Get() != null;
|
public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null;
|
||||||
public readonly SharedPtr<ID3D12Resource> ResourcePtr => isExternal ? resourceUnion.resource.Get() : resourceUnion.allocation.Get()->GetResource();
|
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)
|
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.isExternal = false;
|
||||||
|
|
||||||
this.viewGroup = resourceDescriptor;
|
this.viewGroup = resourceDescriptor;
|
||||||
@@ -57,7 +57,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
|
|
||||||
public ResourceRecord(ID3D12Resource* resource, ResourceState state, ResourceViewGroup viewGroup)
|
public ResourceRecord(ID3D12Resource* resource, ResourceState state, ResourceViewGroup viewGroup)
|
||||||
{
|
{
|
||||||
this.resourceUnion = new ResourceUnion(resource);
|
this.resource = new ResourceUnion(resource);
|
||||||
this.isExternal = true;
|
this.isExternal = true;
|
||||||
|
|
||||||
this.viewGroup = viewGroup;
|
this.viewGroup = viewGroup;
|
||||||
@@ -73,17 +73,17 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
{
|
{
|
||||||
if (isExternal)
|
if (isExternal)
|
||||||
{
|
{
|
||||||
refCount = resourceUnion.resource.Get()->Release();
|
refCount = resource.resource.Get()->Release();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
refCount = resourceUnion.allocation.Get()->Release();
|
refCount = resource.allocation.Get()->Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptorAllocator.Release(viewGroup);
|
descriptorAllocator.Release(viewGroup);
|
||||||
|
|
||||||
resourceUnion = default;
|
resource = default;
|
||||||
viewGroup = default;
|
viewGroup = default;
|
||||||
|
|
||||||
return refCount;
|
return refCount;
|
||||||
@@ -116,7 +116,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
|
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
|
||||||
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
|
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
|
||||||
_shaders = new DynamicArray<Shader>(16);
|
_shaders = new DynamicArray<Shader>(16);
|
||||||
// _shaderPasses = new UnsafeHashMap<ShaderPassKey, ShaderPass>(32, Allocator.Persistent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~D3D12ResourceDatabase()
|
~D3D12ResourceDatabase()
|
||||||
@@ -149,7 +148,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
return handle;
|
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);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
@@ -160,6 +159,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
allocation->SetName(name);
|
allocation->SetName(name);
|
||||||
|
allocation->GetResource()->SetName(name);
|
||||||
_resourceName[handle] = name;
|
_resourceName[handle] = name;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -475,7 +475,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
|
|||||||
_samplers.Dispose();
|
_samplers.Dispose();
|
||||||
_meshes.Dispose();
|
_meshes.Dispose();
|
||||||
_materials.Dispose();
|
_materials.Dispose();
|
||||||
// _shaderPasses.Dispose();
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using System.Diagnostics;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using TerraFX.Interop.DirectX;
|
using TerraFX.Interop.DirectX;
|
||||||
using TerraFX.Interop.Windows;
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
using static TerraFX.Aliases.DXGI_Alias;
|
using static TerraFX.Aliases.DXGI_Alias;
|
||||||
|
|
||||||
namespace Ghost.Graphics.D3D12;
|
namespace Ghost.Graphics.D3D12;
|
||||||
@@ -71,7 +70,8 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
|||||||
CreateBackBuffers();
|
CreateBackBuffers();
|
||||||
SetScale(desc.ScaleX, desc.ScaleY);
|
SetScale(desc.ScaleX, desc.ScaleY);
|
||||||
|
|
||||||
_compositionSurface = desc.Target.CompositionSurface;
|
if (desc.Target.Type == SwapChainTargetType.Composition)
|
||||||
|
_compositionSurface = desc.Target.CompositionSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
~D3D12SwapChain()
|
~D3D12SwapChain()
|
||||||
@@ -106,12 +106,12 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
|||||||
case SwapChainTargetType.Composition:
|
case SwapChainTargetType.Composition:
|
||||||
ThrowIfFailed(pFactory->CreateSwapChainForComposition((IUnknown*)pCommandQueue, &swapChainDesc, null, &pTempSwapChain));
|
ThrowIfFailed(pFactory->CreateSwapChainForComposition((IUnknown*)pCommandQueue, &swapChainDesc, null, &pTempSwapChain));
|
||||||
|
|
||||||
// Set the composition surface
|
|
||||||
if (desc.Target.CompositionSurface != null)
|
if (desc.Target.CompositionSurface != null)
|
||||||
{
|
{
|
||||||
using var swapChainPanelNative = ISwapChainPanelNative.FromSwapChainPanel(desc.Target.CompositionSurface);
|
using var compositionSurface = ISwapChainPanelNative.FromSwapChainPanel(desc.Target.CompositionSurface);
|
||||||
swapChainPanelNative.SetSwapChain((IntPtr)pTempSwapChain);
|
compositionSurface.SetSwapChain((nint)pTempSwapChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SwapChainTargetType.WindowHandle:
|
case SwapChainTargetType.WindowHandle:
|
||||||
@@ -213,7 +213,7 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
|||||||
var inverseScaleX = 1.0f / scaleX;
|
var inverseScaleX = 1.0f / scaleX;
|
||||||
var inverseScaleY = 1.0f / scaleY;
|
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
|
_11 = inverseScaleX, // Scale X
|
||||||
_22 = inverseScaleY, // Scale Y
|
_22 = inverseScaleY, // Scale Y
|
||||||
@@ -238,8 +238,8 @@ internal unsafe class D3D12SwapChain : ISwapChain
|
|||||||
|
|
||||||
if (_compositionSurface != null)
|
if (_compositionSurface != null)
|
||||||
{
|
{
|
||||||
using var panelNative = ISwapChainPanelNative.FromSwapChainPanel(_compositionSurface);
|
using var compositionSurface = ISwapChainPanelNative.FromSwapChainPanel(_compositionSurface);
|
||||||
panelNative.SetSwapChain(IntPtr.Zero);
|
compositionSurface.SetSwapChain(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < _backBuffers.Count; i++)
|
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;
|
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;
|
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;
|
d3dStates |= D3D12_RESOURCE_STATE_INDEX_BUFFER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.RenderTarget))
|
if ((state & ResourceState.RenderTarget) == ResourceState.RenderTarget)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_RENDER_TARGET;
|
d3dStates |= D3D12_RESOURCE_STATE_RENDER_TARGET;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.UnorderedAccess))
|
if ((state & ResourceState.UnorderedAccess) == ResourceState.UnorderedAccess)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
|
d3dStates |= D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.DepthWrite))
|
if ((state & ResourceState.DepthWrite) == ResourceState.DepthWrite)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_WRITE;
|
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_WRITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.DepthRead))
|
if ((state & ResourceState.DepthRead) == ResourceState.DepthRead)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_READ;
|
d3dStates |= D3D12_RESOURCE_STATE_DEPTH_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.PixelShaderResource))
|
if ((state & ResourceState.PixelShaderResource) == ResourceState.PixelShaderResource)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
|
d3dStates |= D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.CopyDest))
|
if ((state & ResourceState.CopyDest) == ResourceState.CopyDest)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_COPY_DEST;
|
d3dStates |= D3D12_RESOURCE_STATE_COPY_DEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.CopySource))
|
if ((state & ResourceState.CopySource) == ResourceState.CopySource)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_COPY_SOURCE;
|
d3dStates |= D3D12_RESOURCE_STATE_COPY_SOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.GenericRead))
|
if ((state & ResourceState.GenericRead) == ResourceState.GenericRead)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_GENERIC_READ;
|
d3dStates |= D3D12_RESOURCE_STATE_GENERIC_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HasFlag(ResourceState.IndirectArgument))
|
if ((state & ResourceState.IndirectArgument) == ResourceState.IndirectArgument)
|
||||||
{
|
{
|
||||||
d3dStates |= D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT;
|
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;
|
d3dStates |= D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,6 +224,22 @@ public struct PassRenderTargetDesc
|
|||||||
get; set;
|
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
|
public struct PassDepthStencilDesc
|
||||||
@@ -243,6 +259,38 @@ public struct PassDepthStencilDesc
|
|||||||
get; set;
|
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,
|
Type = SwapChainTargetType.WindowHandle,
|
||||||
WindowHandle = hwnd,
|
WindowHandle = hwnd,
|
||||||
CompositionSurface = null
|
CompositionSurface = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,3 +926,42 @@ public enum ComparisonFunction
|
|||||||
GreaterEqual,
|
GreaterEqual,
|
||||||
Always
|
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>
|
/// </summary>
|
||||||
/// <param name="slot">The vertex buffer slot to bind to.</param>
|
/// <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="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);
|
void SetVertexBuffer(uint slot, Handle<GraphicsBuffer> buffer, ulong offset = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -126,7 +126,7 @@ public interface ICommandBuffer : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="buffer">The handle to the graphics buffer containing index data.</param>
|
/// <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="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);
|
void SetIndexBuffer(Handle<GraphicsBuffer> buffer, IndexType type, ulong offset = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -140,7 +140,7 @@ public interface ICommandBuffer : IDisposable
|
|||||||
/// </summary>
|
/// </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="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="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);
|
void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan<uint> constantBuffer, uint offsetIn32Bits = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -209,8 +209,8 @@ public interface ICommandBuffer : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dest">The handle to the destination graphics buffer where data will be written. Cannot be null.</param>
|
/// <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="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="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="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>
|
/// <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);
|
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,
|
SamplerFeedback = 1 << 3,
|
||||||
BindlessResources = 1 << 4,
|
BindlessResources = 1 << 4,
|
||||||
WorkGraphs = 1 << 5,
|
WorkGraphs = 1 << 5,
|
||||||
|
AliasBuffersAndTextures = 1 << 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,32 +1,121 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Core.Graphics;
|
using Ghost.Core.Graphics;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
|
||||||
using Ghost.Graphics.Core;
|
using Ghost.Graphics.Core;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
namespace Ghost.Graphics.RHI;
|
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
|
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>
|
/// <summary>
|
||||||
/// Creates a texture resource
|
/// Creates a texture resource
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="desc">Texture description</param>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Creates a render Target for off-screen rendering
|
/// Creates a render Target for off-screen rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="desc">Render Target description</param>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Creates a buffer resource
|
/// Creates a buffer resource
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="desc">Buffer description</param>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Creates a new sampler object using the specified sampler description.
|
/// Creates a new sampler object using the specified sampler description.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public interface ISwapChain : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all back buffer textures
|
/// Gets all back buffer textures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>All back buffer textures</returns>
|
/// <returns>AlowBufferAndTexture back buffer textures</returns>
|
||||||
ReadOnlySpan<Handle<Texture>> GetBackBuffers();
|
ReadOnlySpan<Handle<Texture>> GetBackBuffers();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ internal class MeshRenderPass : IRenderPass
|
|||||||
Usage = TextureUsage.ShaderResource,
|
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
|
var samplerDesc = new SamplerDesc
|
||||||
@@ -182,13 +182,9 @@ internal class MeshRenderPass : IRenderPass
|
|||||||
tex_sampler = (uint)sampler.Value,
|
tex_sampler = (uint)sampler.Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
Debug.Assert(matRef.SetPropertyCache(in matProps) == ErrorStatus.None);
|
matRef.SetPropertyCache(in matProps).ThrowIfFailed();
|
||||||
matRef.UploadData(ctx.DirectCommandBuffer);
|
matRef.UploadData(ctx.DirectCommandBuffer);
|
||||||
|
|
||||||
var pso = matRef.GetPassPipelineOverride(0);
|
|
||||||
pso.Cull = Cull.Back;
|
|
||||||
matRef.SetPassPipelineOverride(0, in pso);
|
|
||||||
|
|
||||||
_forwardPassID = Shader.GetPassID("Forward");
|
_forwardPassID = Shader.GetPassID("Forward");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,8 +264,12 @@ internal class RenderSystem : IRenderSystem
|
|||||||
var newSize = kvp.Value;
|
var newSize = kvp.Value;
|
||||||
swapChain.Resize(newSize.x, newSize.y);
|
swapChain.Resize(newSize.x, newSize.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resizeRequest.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameResource.CommandAllocator.Reset();
|
||||||
|
|
||||||
var r = _graphicsEngine.RenderFrame(frameResource.CommandAllocator);
|
var r = _graphicsEngine.RenderFrame(frameResource.CommandAllocator);
|
||||||
if (r.IsFailure)
|
if (r.IsFailure)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ public class RenderGraphBenchmark
|
|||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_renderGraph = new RenderGraph();
|
_renderGraph = new RenderGraph();
|
||||||
|
|
||||||
|
// Warm up
|
||||||
|
ExecuteGraph(_renderGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
|
_renderGraph.Reset();
|
||||||
ExecuteGraph(_renderGraph);
|
ExecuteGraph(_renderGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ExecuteGraph(RenderGraph renderGraph)
|
public static void ExecuteGraph(RenderGraph renderGraph, int idx = 0)
|
||||||
{
|
{
|
||||||
renderGraph.Reset(); // new RenderGraph()
|
|
||||||
|
|
||||||
// Import external resources
|
// Import external resources
|
||||||
var backbuffer = renderGraph.ImportTexture(
|
var backbuffer = renderGraph.ImportTexture(
|
||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
|
||||||
@@ -88,6 +90,7 @@ public class RenderGraphBenchmark
|
|||||||
|
|
||||||
// ===== SSAO Pass (Async Compute) =====
|
// ===== SSAO Pass (Async Compute) =====
|
||||||
Identifier<RGTexture> ssaoOutput;
|
Identifier<RGTexture> ssaoOutput;
|
||||||
|
Identifier<RGBuffer> ssaoBufferOutput;
|
||||||
using (var builder = renderGraph.AddComputeRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
|
using (var builder = renderGraph.AddComputeRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
|
||||||
{
|
{
|
||||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||||
@@ -99,6 +102,9 @@ public class RenderGraphBenchmark
|
|||||||
ssaoOutput = builder.CreateTexture(
|
ssaoOutput = builder.CreateTexture(
|
||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
|
||||||
ssaoData.OutputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Write);
|
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);
|
builder.EnableAsyncCompute(true);
|
||||||
|
|
||||||
@@ -122,6 +128,7 @@ public class RenderGraphBenchmark
|
|||||||
bloomOutput = builder.CreateTexture(
|
bloomOutput = builder.CreateTexture(
|
||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
|
||||||
builder.SetColorAttachment(bloomOutput, 0);
|
builder.SetColorAttachment(bloomOutput, 0);
|
||||||
|
builder.UseBuffer(ssaoBufferOutput, AccessFlags.Read);
|
||||||
|
|
||||||
bloomData.Output = bloomOutput;
|
bloomData.Output = bloomOutput;
|
||||||
|
|
||||||
@@ -152,14 +159,25 @@ public class RenderGraphBenchmark
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Post Processing Pass =====
|
// ===== 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.InputTAA = builder.UseTexture(taaOutput, AccessFlags.Read);
|
||||||
postData.InputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Read);
|
postData.InputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Read);
|
||||||
postData.InputBloom = builder.UseTexture(bloomOutput, AccessFlags.Read);
|
postData.InputBloom = builder.UseTexture(bloomOutput, AccessFlags.Read);
|
||||||
|
|
||||||
builder.SetColorAttachment(backbuffer, 0);
|
builder.SetColorAttachment(backbuffer, 0);
|
||||||
|
|
||||||
builder.SetRenderFunc<PostProcessingPassDataV2>(static (data, cmd) =>
|
builder.SetRenderFunc<PostProcessingPassDataV2>(static (data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.InputTAA.AsResource(), 0);
|
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> GBufferDepth;
|
||||||
public Identifier<RGTexture> GBufferNormal;
|
public Identifier<RGTexture> GBufferNormal;
|
||||||
public Identifier<RGTexture> OutputSSAO;
|
public Identifier<RGTexture> OutputSSAO;
|
||||||
|
public Identifier<RGBuffer> OutputSSAOBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BloomDownsampleData : IPassData
|
public sealed class BloomDownsampleData : IPassData
|
||||||
@@ -39,6 +40,12 @@ public sealed class TAAPassData : IPassData
|
|||||||
public Identifier<RGTexture> OutputTAA;
|
public Identifier<RGTexture> OutputTAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class PostProcessingPassDataV1 : IPassData
|
||||||
|
{
|
||||||
|
public Identifier<RGTexture> InputLighting;
|
||||||
|
public Identifier<RGTexture> OutputBackbuffer;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PostProcessingPassDataV2 : IPassData
|
public sealed class PostProcessingPassDataV2 : IPassData
|
||||||
{
|
{
|
||||||
public Identifier<RGTexture> InputTAA;
|
public Identifier<RGTexture> InputTAA;
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ const int _ITERATION = 500000;
|
|||||||
for (var i = 0; i < _ITERATION; i++)
|
for (var i = 0; i < _ITERATION; i++)
|
||||||
{
|
{
|
||||||
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
||||||
|
renderGraph.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
GC.WaitForPendingFinalizers();
|
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 sw = new System.Diagnostics.Stopwatch();
|
||||||
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
|
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
|
||||||
sw.Start();
|
sw.Start();
|
||||||
@@ -23,6 +24,7 @@ sw.Start();
|
|||||||
for (var i = 0; i < _ITERATION; i++)
|
for (var i = 0; i < _ITERATION; i++)
|
||||||
{
|
{
|
||||||
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
||||||
|
renderGraph.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
@@ -37,6 +39,9 @@ var renderGraph = new RenderGraph();
|
|||||||
Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ===");
|
Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ===");
|
||||||
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
||||||
|
|
||||||
Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
|
//Thread.Sleep(5000);
|
||||||
RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
|
||||||
|
//renderGraph.Reset();
|
||||||
|
//Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
|
||||||
|
//RenderGraphBenchmark.ExecuteGraph(renderGraph);
|
||||||
#endif
|
#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.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using TerraFX.Interop.Windows;
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
@@ -21,6 +22,7 @@ public sealed class RenderGraph
|
|||||||
private readonly RenderGraphObjectPool _objectPool = new();
|
private readonly RenderGraphObjectPool _objectPool = new();
|
||||||
private readonly List<RenderGraphPassBase> _passes = new(64);
|
private readonly List<RenderGraphPassBase> _passes = new(64);
|
||||||
private readonly List<RenderGraphPassBase> _compiledPasses = new(64);
|
private readonly List<RenderGraphPassBase> _compiledPasses = new(64);
|
||||||
|
private readonly List<NativeRenderPass> _nativePasses = new(32);
|
||||||
private readonly RenderGraphBuilder _builder = new();
|
private readonly RenderGraphBuilder _builder = new();
|
||||||
private readonly MockCommandBuffer _commandBuffer = new();
|
private readonly MockCommandBuffer _commandBuffer = new();
|
||||||
private readonly RenderContext _renderContext;
|
private readonly RenderContext _renderContext;
|
||||||
@@ -68,6 +70,14 @@ public sealed class RenderGraph
|
|||||||
|
|
||||||
// Clear compiled passes list
|
// Clear compiled passes list
|
||||||
_compiledPasses.Clear();
|
_compiledPasses.Clear();
|
||||||
|
|
||||||
|
// Return native passes to pool
|
||||||
|
for (var i = 0; i < _nativePasses.Count; i++)
|
||||||
|
{
|
||||||
|
_objectPool.Return(_nativePasses[i]);
|
||||||
|
}
|
||||||
|
_nativePasses.Clear();
|
||||||
|
|
||||||
_compiled = false;
|
_compiled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +89,14 @@ public sealed class RenderGraph
|
|||||||
return _resources.ImportTexture(descriptor);
|
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)
|
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
|
||||||
where TPassData : class, new()
|
where TPassData : class, new()
|
||||||
{
|
{
|
||||||
@@ -119,13 +137,13 @@ public sealed class RenderGraph
|
|||||||
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
|
*(pData + offset) = resource.isImported ? (byte)1 : (byte)0;
|
||||||
offset += sizeof(byte);
|
offset += sizeof(byte);
|
||||||
|
|
||||||
*(TextureFormat*)(pData + offset) = resource.descriptor.format;
|
*(TextureFormat*)(pData + offset) = resource.textureDescriptor.format;
|
||||||
offset += sizeof(TextureFormat);
|
offset += sizeof(TextureFormat);
|
||||||
|
|
||||||
*(int*)(pData + offset) = resource.descriptor.width;
|
*(int*)(pData + offset) = resource.textureDescriptor.width;
|
||||||
offset += sizeof(int);
|
offset += sizeof(int);
|
||||||
|
|
||||||
*(int*)(pData + offset) = resource.descriptor.height;
|
*(int*)(pData + offset) = resource.textureDescriptor.height;
|
||||||
offset += sizeof(int);
|
offset += sizeof(int);
|
||||||
|
|
||||||
return offset;
|
return offset;
|
||||||
@@ -159,11 +177,17 @@ public sealed class RenderGraph
|
|||||||
// Hash depth attachment
|
// Hash depth attachment
|
||||||
offset = ComputeTextureHash(pData, offset, pass.depthAccess.id);
|
offset = ComputeTextureHash(pData, offset, pass.depthAccess.id);
|
||||||
|
|
||||||
|
pData[offset] = (byte)pass.depthAccess.accessFlags;
|
||||||
|
offset += sizeof(AccessFlags);
|
||||||
|
|
||||||
*(int*)(pData + offset) = pass.maxColorIndex;
|
*(int*)(pData + offset) = pass.maxColorIndex;
|
||||||
offset += sizeof(int);
|
offset += sizeof(int);
|
||||||
for (var j = 0; j <= pass.maxColorIndex; j++)
|
for (var j = 0; j <= pass.maxColorIndex; j++)
|
||||||
{
|
{
|
||||||
offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id);
|
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++)
|
for (var j = 0; j < (int)RenderGraphResourceType.Count; j++)
|
||||||
@@ -195,27 +219,31 @@ public sealed class RenderGraph
|
|||||||
*(int*)(pData + offset) = createList[k].Value;
|
*(int*)(pData + offset) = createList[k].Value;
|
||||||
offset += sizeof(int);
|
offset += sizeof(int);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*(int*)(pData + offset) = pass.randomAccess.Count;
|
||||||
|
offset += sizeof(int);
|
||||||
|
for (var k = 0; k < pass.randomAccess.Count; k++)
|
||||||
|
{
|
||||||
|
*(int*)(pData + offset) = pass.randomAccess[k].Value;
|
||||||
|
offset += sizeof(int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash buffer hints (important for correct barrier generation)
|
||||||
|
*(int*)(pData + offset) = pass.bufferHints.Count;
|
||||||
|
offset += sizeof(int);
|
||||||
|
foreach (var kvp in pass.bufferHints)
|
||||||
|
{
|
||||||
|
*(int*)(pData + offset) = kvp.Key; // Buffer resource ID
|
||||||
|
offset += sizeof(int);
|
||||||
|
*(int*)(pData + offset) = (int)kvp.Value; // BufferHint flags
|
||||||
|
offset += sizeof(int);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*(int*)(pData + offset) = pass.GetRenderFuncHashCode();
|
||||||
|
offset += sizeof(int);
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Hash resource descriptors
|
|
||||||
//for (var j = 0; j < _resources.TextureResourceCount; j++)
|
|
||||||
//{
|
|
||||||
// var resource = _resources.GetTextureResourceByIndex(j);
|
|
||||||
|
|
||||||
// *(int*)(pData + offset) = resource.descriptor.width;
|
|
||||||
// offset += sizeof(int);
|
|
||||||
|
|
||||||
// *(int*)(pData + offset) = resource.descriptor.height;
|
|
||||||
// offset += sizeof(int);
|
|
||||||
|
|
||||||
// *(TextureFormat*)(pData + offset) = resource.descriptor.format;
|
|
||||||
// offset += sizeof(TextureFormat);
|
|
||||||
|
|
||||||
// *(bool*)(pData + offset) = resource.isImported;
|
|
||||||
// offset += sizeof(bool);
|
|
||||||
//}
|
|
||||||
|
|
||||||
var span = new Span<byte>(pData, offset);
|
var span = new Span<byte>(pData, offset);
|
||||||
return XxHash64.HashToUInt64(span);
|
return XxHash64.HashToUInt64(span);
|
||||||
}
|
}
|
||||||
@@ -317,7 +345,10 @@ public sealed class RenderGraph
|
|||||||
// Step 6: Generate barriers for state transitions and aliasing
|
// Step 6: Generate barriers for state transitions and aliasing
|
||||||
GenerateBarriers();
|
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);
|
StoreInCache(graphHash);
|
||||||
|
|
||||||
_compiled = true;
|
_compiled = true;
|
||||||
@@ -343,7 +374,7 @@ public sealed class RenderGraph
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore aliasing mappings (need to update ResourceAliasingManager)
|
// 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)
|
// Restore barriers (deep copy to avoid shared references)
|
||||||
_barriers.Clear();
|
_barriers.Clear();
|
||||||
@@ -380,7 +411,7 @@ public sealed class RenderGraph
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store aliasing mappings
|
// Store aliasing mappings
|
||||||
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.physicalResources);
|
_aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources);
|
||||||
|
|
||||||
// Store barriers
|
// Store barriers
|
||||||
for (var i = 0; i < _barriers.Count; i++)
|
for (var i = 0; i < _barriers.Count; i++)
|
||||||
@@ -476,65 +507,73 @@ public sealed class RenderGraph
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inserts aliasing barriers when a physical resource is reused.
|
/// Inserts aliasing barriers when a placed resource is reused.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
|
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
|
||||||
{
|
{
|
||||||
// Check all resources written by this pass
|
// Check all resources written by this pass (both textures and buffers)
|
||||||
for (var i = 0; i < pass.resourceWrites.Count; i++)
|
for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
|
||||||
{
|
{
|
||||||
var id = pass.resourceWrites[i];
|
var writeList = pass.resourceWrites[resType];
|
||||||
var resource = _resources.GetResource(id);
|
for (var i = 0; i < writeList.Count; i++)
|
||||||
|
|
||||||
// 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 id = writeList[i];
|
||||||
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
|
var resource = _resources.GetResource(id);
|
||||||
if (physicalIndex >= 0)
|
|
||||||
|
// Skip imported resources
|
||||||
|
if (resource.isImported)
|
||||||
{
|
{
|
||||||
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If this physical resource has multiple aliased resources,
|
// Check if this is the first use of this logical resource
|
||||||
// we need an aliasing barrier when switching between them
|
if (resource.firstUsePass == pass.index)
|
||||||
if (physical != null && physical.aliasedLogicalResources.Count > 1)
|
{
|
||||||
|
// Get the placed resource
|
||||||
|
var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value);
|
||||||
|
if (placedIndex >= 0)
|
||||||
{
|
{
|
||||||
// Find the resource that used this physical memory most recently before this pass
|
var placed = _aliasingManager.GetPlacedResource(placedIndex);
|
||||||
Identifier<RGResource> resourceBefore = default;
|
|
||||||
var mostRecentLastUse = -1;
|
|
||||||
|
|
||||||
foreach (var otherLogicalIndex in physical.aliasedLogicalResources)
|
// If this placed resource has multiple aliased resources,
|
||||||
|
// we need an aliasing barrier when switching between them
|
||||||
|
if (placed != null && placed.aliasedLogicalResources.Count > 1)
|
||||||
{
|
{
|
||||||
if (otherLogicalIndex != id.Value)
|
// Find the resource that used this placed memory most recently before this pass
|
||||||
|
Identifier<RGResource> resourceBefore = default;
|
||||||
|
var mostRecentLastUse = -1;
|
||||||
|
|
||||||
|
foreach (var otherLogicalIndex in placed.aliasedLogicalResources)
|
||||||
{
|
{
|
||||||
var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
|
if (otherLogicalIndex != id.Value)
|
||||||
// Check if this resource finished before our resource starts
|
|
||||||
if (otherResource.lastUsePass < pass.index &&
|
|
||||||
otherResource.lastUsePass > mostRecentLastUse)
|
|
||||||
{
|
{
|
||||||
mostRecentLastUse = otherResource.lastUsePass;
|
// Get resource by global index
|
||||||
resourceBefore = otherLogicalIndex;
|
var otherResource = _resources.GetResourceByIndex(otherLogicalIndex);
|
||||||
|
|
||||||
|
// Check if this resource finished before our resource starts
|
||||||
|
if (otherResource.lastUsePass < pass.index &&
|
||||||
|
otherResource.lastUsePass > mostRecentLastUse)
|
||||||
|
{
|
||||||
|
mostRecentLastUse = otherResource.lastUsePass;
|
||||||
|
resourceBefore = new Identifier<RGResource>(otherLogicalIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If we found a previous resource, insert aliasing barrier
|
// If we found a previous resource, insert aliasing barrier
|
||||||
if (mostRecentLastUse >= 0)
|
if (mostRecentLastUse >= 0)
|
||||||
{
|
{
|
||||||
var barrier = ResourceBarrier.CreateAliasingBarrier(
|
var barrier = ResourceBarrier.CreateAliasingBarrier(
|
||||||
resourceBefore,
|
resourceBefore,
|
||||||
id,
|
id,
|
||||||
passIdx
|
passIdx
|
||||||
);
|
);
|
||||||
_barriers.Add(barrier);
|
_barriers.Add(barrier);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine($" {barrier}");
|
Console.WriteLine($" {barrier}");
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,14 +586,15 @@ public sealed class RenderGraph
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
|
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++)
|
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
|
||||||
{
|
{
|
||||||
var readList = pass.resourceReads[i];
|
var readList = pass.resourceReads[i];
|
||||||
for (var j = 0; j < readList.Count; j++)
|
for (var j = 0; j < readList.Count; j++)
|
||||||
{
|
{
|
||||||
var handle = readList[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>
|
/// <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>
|
/// </summary>
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
@@ -632,55 +1051,111 @@ public sealed class RenderGraph
|
|||||||
Compile();
|
Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute each non-culled pass
|
|
||||||
var barrierIndex = 0;
|
var barrierIndex = 0;
|
||||||
for (var i = 0; i < _compiledPasses.Count; i++)
|
var nativePassIndex = 0;
|
||||||
|
var logicalPassIndex = 0;
|
||||||
|
|
||||||
|
while (logicalPassIndex < _compiledPasses.Count)
|
||||||
{
|
{
|
||||||
var pass = _compiledPasses[i];
|
var pass = _compiledPasses[logicalPassIndex];
|
||||||
|
|
||||||
// Execute all barriers for this pass
|
// Check if this pass is part of a native render pass
|
||||||
#if DEBUG
|
if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count)
|
||||||
bool hasBarriers = false;
|
|
||||||
#endif
|
|
||||||
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == i)
|
|
||||||
{
|
{
|
||||||
#if DEBUG
|
var nativePass = _nativePasses[nativePassIndex];
|
||||||
if (!hasBarriers)
|
|
||||||
|
// Execute barriers for ALL merged passes before beginning the native render pass
|
||||||
|
foreach (var mergedPassIdx in nativePass.mergedPassIndices)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
|
ExecuteBarriersForPass(mergedPassIdx, ref barrierIndex);
|
||||||
hasBarriers = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var barrier = _barriers[barrierIndex];
|
// Begin native render pass
|
||||||
if (barrier.Type == BarrierType.Transition)
|
_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++)
|
||||||
{
|
{
|
||||||
_commandBuffer.ResourceBarrier(
|
var mergedPassIdx = nativePass.mergedPassIndices[i];
|
||||||
barrier.Resource,
|
var mergedPass = _compiledPasses[mergedPassIdx];
|
||||||
barrier.StateBefore,
|
|
||||||
barrier.StateAfter
|
#if DEBUG
|
||||||
);
|
Console.WriteLine($"\n--- Executing Pass {mergedPassIdx}: {mergedPass.name} (in Native Pass {nativePass.index}) ---");
|
||||||
}
|
|
||||||
else if (barrier.Type == BarrierType.Aliasing)
|
|
||||||
{
|
|
||||||
_commandBuffer.AliasBarrier(
|
|
||||||
barrier.ResourceBefore,
|
|
||||||
barrier.ResourceAfter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
// In a real implementation, you would execute the barrier here:
|
|
||||||
// ExecuteBarrier(_barriers[barrierIndex]);
|
|
||||||
|
|
||||||
barrierIndex++;
|
mergedPass.Execute(_renderContext);
|
||||||
|
logicalPassIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End native render pass
|
||||||
|
_commandBuffer.EndRenderPass();
|
||||||
|
|
||||||
|
nativePassIndex++;
|
||||||
}
|
}
|
||||||
#if DEBUG
|
else
|
||||||
if (hasBarriers)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("=====================================\n");
|
// Compute pass or standalone raster pass (not merged)
|
||||||
}
|
ExecuteBarriersForPass(logicalPassIndex, ref barrierIndex);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine($"\n--- Executing Pass {logicalPassIndex}: {pass.name} (Standalone) ---");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
pass.Execute(_renderContext);
|
pass.Execute(_renderContext);
|
||||||
|
logicalPassIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes all barriers for a specific pass.
|
||||||
|
/// </summary>
|
||||||
|
private void ExecuteBarriersForPass(int passIndex, ref int barrierIndex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
bool hasBarriers = false;
|
||||||
|
#endif
|
||||||
|
while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (!hasBarriers)
|
||||||
|
{
|
||||||
|
var pass = _compiledPasses[passIndex];
|
||||||
|
Console.WriteLine($"\n=== Barriers before Pass {passIndex}: {pass.name} ===");
|
||||||
|
hasBarriers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var barrier = _barriers[barrierIndex];
|
||||||
|
if (barrier.Type == BarrierType.Transition)
|
||||||
|
{
|
||||||
|
_commandBuffer.ResourceBarrier(
|
||||||
|
barrier.Resource,
|
||||||
|
barrier.StateBefore,
|
||||||
|
barrier.StateAfter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (barrier.Type == BarrierType.Aliasing)
|
||||||
|
{
|
||||||
|
_commandBuffer.AliasBarrier(
|
||||||
|
barrier.ResourceBefore,
|
||||||
|
barrier.ResourceAfter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// In a real implementation, you would execute the barrier here:
|
||||||
|
// ExecuteBarrier(_barriers[barrierIndex]);
|
||||||
|
|
||||||
|
barrierIndex++;
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
if (hasBarriers)
|
||||||
|
{
|
||||||
|
Console.WriteLine("=====================================\n");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 Ghost.Core.Utilities;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a physical GPU resource that can be aliased by multiple logical resources.
|
/// Represents a memory block within a heap.
|
||||||
/// </summary>
|
/// </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 index;
|
||||||
public int width;
|
public ulong size;
|
||||||
public int height;
|
private readonly List<MemoryBlock> _blocks = new(32);
|
||||||
public TextureFormat format;
|
|
||||||
public int sizeInBytes;
|
// 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
|
// Lifetime tracking
|
||||||
public int firstUsePass = int.MaxValue;
|
public int firstUsePass = int.MaxValue;
|
||||||
@@ -19,26 +258,21 @@ internal sealed class PhysicalResource
|
|||||||
|
|
||||||
// Aliasing tracking
|
// Aliasing tracking
|
||||||
public readonly List<int> aliasedLogicalResources = new(4);
|
public readonly List<int> aliasedLogicalResources = new(4);
|
||||||
|
public MemoryBlock memoryBlock;
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
index = -1;
|
index = -1;
|
||||||
width = 0;
|
type = RenderGraphResourceType.Texture;
|
||||||
height = 0;
|
heapIndex = -1;
|
||||||
format = TextureFormat.RGBA8;
|
heapOffset = 0;
|
||||||
sizeInBytes = 0;
|
sizeInBytes = 0;
|
||||||
|
textureDesc = default;
|
||||||
|
bufferDesc = default;
|
||||||
firstUsePass = int.MaxValue;
|
firstUsePass = int.MaxValue;
|
||||||
lastUsePass = -1;
|
lastUsePass = -1;
|
||||||
aliasedLogicalResources.Clear();
|
aliasedLogicalResources.Clear();
|
||||||
}
|
memoryBlock = default;
|
||||||
|
|
||||||
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)
|
public void UpdateLifetime(int passIndex)
|
||||||
@@ -46,148 +280,293 @@ internal sealed class PhysicalResource
|
|||||||
firstUsePass = Math.Min(firstUsePass, passIndex);
|
firstUsePass = Math.Min(firstUsePass, passIndex);
|
||||||
lastUsePass = Math.Max(lastUsePass, 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>
|
/// <summary>
|
||||||
/// Manages physical resource allocation and aliasing.
|
/// Manages physical resource allocation and aliasing using heap-based allocation.
|
||||||
/// Uses interval scheduling algorithm to minimize memory usage.
|
/// Supports D3D12 heap tier 2: buffers and textures can alias as long as lifetimes don't overlap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ResourceAliasingManager
|
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 readonly RenderGraphObjectPool _pool = new();
|
||||||
private int _physicalResourceCount;
|
|
||||||
|
|
||||||
// Mapping from logical resource index to physical resource index
|
// Mapping from logical resource index to placed resource index
|
||||||
private readonly Dictionary<int, int> _logicalToPhysical = new(64);
|
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()
|
public void BeginFrame()
|
||||||
{
|
{
|
||||||
_physicalResourceCount = 0;
|
for (var i = 0; i < _placedResources.Count; i++)
|
||||||
_logicalToPhysical.Clear();
|
|
||||||
|
|
||||||
// Reset physical resources but keep them in the pool
|
|
||||||
for (int i = 0; i < _physicalResources.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>
|
/// <summary>
|
||||||
/// Assigns physical resources to logical resources using greedy interval scheduling.
|
/// Assigns physical resources (placed resources) to logical resources using heap-based allocation.
|
||||||
/// This minimizes total GPU memory usage.
|
/// 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>
|
/// </summary>
|
||||||
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
|
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine("\n=== Resource Aliasing Analysis ===");
|
Console.WriteLine("\n=== Heap-Based Resource Aliasing Analysis ===");
|
||||||
int totalLogicalSize = 0;
|
ulong totalLogicalSize = 0;
|
||||||
#endif
|
#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();
|
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
|
if (!resource.isImported) // Don't alias imported resources
|
||||||
{
|
{
|
||||||
logicalResources.Add((i, resource));
|
logicalResources.Add((resource.index, resource));
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
int size = CalculateSize(resource.descriptor);
|
var size = resource.type == RenderGraphResourceType.Texture
|
||||||
|
? CalculateTextureSize(resource.textureDescriptor)
|
||||||
|
: resource.bufferDescriptor.sizeInBytes;
|
||||||
totalLogicalSize += size;
|
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($" Lifetime: Pass {resource.firstUsePass} -> {resource.lastUsePass}");
|
||||||
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by first use pass (earlier resources first)
|
// Sort by size descending (larger resources first for better packing)
|
||||||
logicalResources.Sort((a, b) => a.resource.firstUsePass.CompareTo(b.resource.firstUsePass));
|
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)
|
foreach (var (logicalIndex, logicalResource) in logicalResources)
|
||||||
{
|
{
|
||||||
PhysicalResource? assignedPhysical = null;
|
ulong size;
|
||||||
|
ulong alignment;
|
||||||
|
|
||||||
// Try to find an existing physical resource that:
|
if (logicalResource.type == RenderGraphResourceType.Texture)
|
||||||
// 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];
|
size = CalculateTextureSize(logicalResource.textureDescriptor);
|
||||||
|
alignment = DefaultTextureAlignment;
|
||||||
if (physical.CanAlias(logicalResource.descriptor) &&
|
}
|
||||||
!HasLifetimeOverlap(physical, logicalResource))
|
else // Buffer
|
||||||
{
|
{
|
||||||
assignedPhysical = physical;
|
size = logicalResource.bufferDescriptor.sizeInBytes;
|
||||||
break;
|
alignment = DefaultBufferAlignment;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No compatible physical resource found, allocate a new one
|
var (success, offset, block) = simulationHeap.TryAllocate(
|
||||||
if (assignedPhysical == null)
|
size,
|
||||||
|
logicalResource.firstUsePass,
|
||||||
|
logicalResource.lastUsePass,
|
||||||
|
logicalIndex,
|
||||||
|
alignment);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
{
|
{
|
||||||
assignedPhysical = GetOrCreatePhysicalResource();
|
throw new InvalidOperationException("Simulation allocation failed - this should never happen with unlimited heap");
|
||||||
assignedPhysical.index = _physicalResourceCount - 1;
|
}
|
||||||
assignedPhysical.width = logicalResource.descriptor.width;
|
}
|
||||||
assignedPhysical.height = logicalResource.descriptor.height;
|
|
||||||
assignedPhysical.format = logicalResource.descriptor.format;
|
// Get peak usage from simulation
|
||||||
assignedPhysical.sizeInBytes = assignedPhysical.CalculateSize();
|
var peakMemoryUsage = simulationHeap.GetPeakUsage();
|
||||||
|
|
||||||
|
// Align peak usage to 64KB (D3D12 requirement)
|
||||||
|
peakMemoryUsage = AlignUp(peakMemoryUsage, DefaultTextureAlignment);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine($"\nAllocated NEW Physical Resource {assignedPhysical.index}:");
|
Console.WriteLine($"\nPeak Memory Usage: {peakMemoryUsage / (1024.0 * 1024.0):F2} MB");
|
||||||
Console.WriteLine($" Size: {assignedPhysical.width}x{assignedPhysical.height}");
|
|
||||||
Console.WriteLine($" Format: {assignedPhysical.format}");
|
|
||||||
Console.WriteLine($" Memory: {assignedPhysical.sizeInBytes / 1024.0:F2} KB");
|
|
||||||
#endif
|
#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
|
#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
|
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
|
#endif
|
||||||
|
|
||||||
// Update physical resource lifetime
|
|
||||||
assignedPhysical.UpdateLifetime(logicalResource.firstUsePass);
|
|
||||||
assignedPhysical.UpdateLifetime(logicalResource.lastUsePass);
|
|
||||||
assignedPhysical.aliasedLogicalResources.Add(logicalIndex);
|
|
||||||
|
|
||||||
// Record the mapping
|
// 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
|
#if DEBUG
|
||||||
int totalPhysicalSize = 0;
|
// Debug output: Show which resources alias with each other
|
||||||
for (int i = 0; i < _physicalResourceCount; i++)
|
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($"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 Logical Memory: {totalLogicalSize / 1024.0:F2} KB");
|
||||||
Console.WriteLine($"Total Physical Memory: {totalPhysicalSize / 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($"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);
|
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
|
return placedIndex >= 0 && placedIndex < _placedResources.Count
|
||||||
? _physicalResources[physicalIndex]
|
? _placedResources[placedIndex]
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasLifetimeOverlap(PhysicalResource physical, RenderGraphResource logical)
|
private static ulong CalculateTextureSize(TextureDescriptor descriptor)
|
||||||
{
|
{
|
||||||
// Check if the lifetimes overlap
|
var bytesPerPixel = descriptor.format switch
|
||||||
// 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.RGBA8 => 4,
|
||||||
TextureFormat.RGBA16F => 8,
|
TextureFormat.RGBA16F => 8,
|
||||||
@@ -247,82 +599,87 @@ internal sealed class ResourceAliasingManager
|
|||||||
TextureFormat.Depth24Stencil8 => 4,
|
TextureFormat.Depth24Stencil8 => 4,
|
||||||
_ => 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()
|
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();
|
_placedResources.Clear();
|
||||||
_physicalResourceCount = 0;
|
_logicalToPlaced.Clear();
|
||||||
_logicalToPhysical.Clear();
|
_heaps.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restores aliasing state from cache.
|
/// Restores aliasing state from cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RestoreFromCache(Dictionary<int, int> logicalToPhysical, List<PhysicalResourceData> physicalData)
|
public void RestoreFromCache(Dictionary<int, int> logicalToPlaced, List<PlacedResourceData> placedData)
|
||||||
{
|
{
|
||||||
_logicalToPhysical.Clear();
|
_logicalToPlaced.Clear();
|
||||||
foreach (var kvp in logicalToPhysical)
|
foreach (var kvp in logicalToPlaced)
|
||||||
{
|
{
|
||||||
_logicalToPhysical[kvp.Key] = kvp.Value;
|
_logicalToPlaced[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore physical resources
|
// Restore placed resources
|
||||||
_physicalResourceCount = physicalData.Count;
|
for (var i = 0; i < placedData.Count; i++)
|
||||||
for (int i = 0; i < physicalData.Count; i++)
|
|
||||||
{
|
{
|
||||||
PhysicalResource physical;
|
var placed = _pool.Rent<PlacedResource>();
|
||||||
if (i < _physicalResources.Count)
|
|
||||||
{
|
|
||||||
physical = _physicalResources[i];
|
|
||||||
physical.Reset();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
physical = _pool.Rent<PhysicalResource>();
|
|
||||||
physical.Reset();
|
|
||||||
_physicalResources.Add(physical);
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = physicalData[i];
|
var data = placedData[i];
|
||||||
physical.index = data.index;
|
placed.index = data.index;
|
||||||
physical.width = data.width;
|
placed.type = data.type;
|
||||||
physical.height = data.height;
|
placed.heapIndex = data.heapIndex;
|
||||||
physical.format = data.format;
|
placed.heapOffset = data.heapOffset;
|
||||||
physical.firstUsePass = data.firstUsePass;
|
placed.sizeInBytes = data.sizeInBytes;
|
||||||
physical.lastUsePass = data.lastUsePass;
|
placed.textureDesc = data.textureDesc;
|
||||||
physical.sizeInBytes = physical.CalculateSize();
|
placed.bufferDesc = data.bufferDesc;
|
||||||
|
placed.firstUsePass = data.firstUsePass;
|
||||||
|
placed.lastUsePass = data.lastUsePass;
|
||||||
|
|
||||||
|
placed.aliasedLogicalResources.Clear();
|
||||||
|
|
||||||
|
_placedResources.Add(placed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores current aliasing state to cache.
|
/// Stores current aliasing state to cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void StoreToCache(Dictionary<int, int> outLogicalToPhysical, List<PhysicalResourceData> outPhysicalData)
|
public void StoreToCache(Dictionary<int, int> outLogicalToPlaced, List<PlacedResourceData> outPlacedData)
|
||||||
{
|
{
|
||||||
outLogicalToPhysical.Clear();
|
outLogicalToPlaced.Clear();
|
||||||
foreach (var kvp in _logicalToPhysical)
|
foreach (var kvp in _logicalToPlaced)
|
||||||
{
|
{
|
||||||
outLogicalToPhysical[kvp.Key] = kvp.Value;
|
outLogicalToPlaced[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
outPhysicalData.Clear();
|
outPlacedData.Clear();
|
||||||
for (int i = 0; i < _physicalResourceCount; i++)
|
for (var i = 0; i < _placedResources.Count; i++)
|
||||||
{
|
{
|
||||||
var physical = _physicalResources[i];
|
var placed = _placedResources[i];
|
||||||
outPhysicalData.Add(new PhysicalResourceData
|
outPlacedData.Add(new PlacedResourceData
|
||||||
{
|
{
|
||||||
index = physical.index,
|
index = placed.index,
|
||||||
width = physical.width,
|
type = placed.type,
|
||||||
height = physical.height,
|
heapIndex = placed.heapIndex,
|
||||||
format = physical.format,
|
heapOffset = placed.heapOffset,
|
||||||
firstUsePass = physical.firstUsePass,
|
sizeInBytes = placed.sizeInBytes,
|
||||||
lastUsePass = physical.lastUsePass
|
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,
|
CopySource = 1 << 5,
|
||||||
CopyDest = 1 << 6,
|
CopyDest = 1 << 6,
|
||||||
Present = 1 << 7,
|
Present = 1 << 7,
|
||||||
|
IndirectArgument = 1 << 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -140,14 +141,14 @@ internal struct ResourceBarrier
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ResourceStateTracker
|
internal sealed class ResourceStateTracker
|
||||||
{
|
{
|
||||||
public int ResourceIndex;
|
public int resourceIndex;
|
||||||
public ResourceState CurrentState = ResourceState.Common;
|
public ResourceState currentState = ResourceState.Common;
|
||||||
public int LastAccessPass = -1;
|
public int lastAccessPass = -1;
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
ResourceIndex = -1;
|
resourceIndex = -1;
|
||||||
CurrentState = ResourceState.Common;
|
currentState = ResourceState.Common;
|
||||||
LastAccessPass = -1;
|
lastAccessPass = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ using System.Diagnostics;
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum AccessFlags
|
public enum AccessFlags : byte
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
Read = 1 << 0,
|
Read = 1 << 0,
|
||||||
Write = 1 << 1,
|
Write = 1 << 1,
|
||||||
|
Discard = 1 << 2,
|
||||||
|
|
||||||
|
WriteAll = Write | Discard,
|
||||||
ReadWrite = Read | Write,
|
ReadWrite = Read | Write,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,13 @@ public interface IRenderGraphBuilder : IDisposable
|
|||||||
/// <returns>An identifier for the newly created texture resource.</returns>
|
/// <returns>An identifier for the newly created texture resource.</returns>
|
||||||
Identifier<RGTexture> CreateTexture(in TextureDescriptor descriptor);
|
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>
|
/// <summary>
|
||||||
/// Registers the specified texture for use in the current render graph pass with the given access mode.
|
/// Registers the specified texture for use in the current render graph pass with the given access mode.
|
||||||
/// </summary>
|
/// </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>
|
/// <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>
|
/// <returns>An identifier for the texture.</returns>
|
||||||
Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags accessMode);
|
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
|
public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
|
||||||
@@ -56,13 +75,15 @@ public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texture">The identifier of the texture to use as the color attachment.</param>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// Sets the depth attachment for the current render pass using the specified texture.
|
/// Sets the depth attachment for the current render pass using the specified texture.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texture">The identifier of the texture to use as the depth attachment. Cannot be null.</param>
|
/// <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>
|
/// <summary>
|
||||||
/// Sets the function used to render a pass with the specified pass data and render context.
|
/// 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;
|
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)
|
public Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags flags)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
return UseResource(texture.AsResource(), flags, RenderGraphResourceType.Texture).AsTexture();
|
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)
|
public Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
@@ -173,17 +217,17 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
|
|||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetColorAttachment(Identifier<RGTexture> texture, int index)
|
public void SetColorAttachment(Identifier<RGTexture> texture, int index, AccessFlags flags = AccessFlags.Write)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
||||||
Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range.");
|
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)
|
if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid)
|
||||||
{
|
{
|
||||||
_pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index);
|
_pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index);
|
||||||
_pass.colorAccess[index] = new TextureAccess(id, AccessFlags.Write);
|
_pass.colorAccess[index] = new TextureAccess(id, flags);
|
||||||
}
|
}
|
||||||
else
|
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();
|
ThrowIfDisposed();
|
||||||
|
|
||||||
var id = UseTexture(texture, AccessFlags.Write);
|
var id = UseTexture(texture, flags);
|
||||||
if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid)
|
if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid)
|
||||||
{
|
{
|
||||||
_pass.depthAccess = new TextureAccess(id, AccessFlags.Write);
|
_pass.depthAccess = new TextureAccess(id, flags);
|
||||||
}
|
}
|
||||||
else
|
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)
|
// Physical resource aliasing mappings (logical index -> physical index)
|
||||||
public readonly Dictionary<int, int> logicalToPhysical = new(128);
|
public readonly Dictionary<int, int> logicalToPhysical = new(128);
|
||||||
|
|
||||||
// Physical resource metadata
|
// Placed resource metadata
|
||||||
public readonly List<PhysicalResourceData> physicalResources = new(32);
|
public readonly List<PlacedResourceData> placedResources = new(32);
|
||||||
|
|
||||||
// Resource barriers
|
// Resource barriers
|
||||||
public readonly List<ResourceBarrier> barriers = new(128);
|
public readonly List<ResourceBarrier> barriers = new(128);
|
||||||
@@ -31,21 +31,24 @@ internal sealed class CachedCompilation
|
|||||||
compiledPassIndices.Clear();
|
compiledPassIndices.Clear();
|
||||||
passCulledFlags.Clear();
|
passCulledFlags.Clear();
|
||||||
logicalToPhysical.Clear();
|
logicalToPhysical.Clear();
|
||||||
physicalResources.Clear();
|
placedResources.Clear();
|
||||||
barriers.Clear();
|
barriers.Clear();
|
||||||
resourceStates.Clear();
|
resourceStates.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Physical resource data for caching.
|
/// Placed resource data for caching.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal struct PhysicalResourceData
|
internal struct PlacedResourceData
|
||||||
{
|
{
|
||||||
public int index;
|
public int index;
|
||||||
public int width;
|
public RenderGraphResourceType type;
|
||||||
public int height;
|
public int heapIndex;
|
||||||
public TextureFormat format;
|
public ulong heapOffset;
|
||||||
|
public ulong sizeInBytes;
|
||||||
|
public TextureDescriptor textureDesc;
|
||||||
|
public BufferDescriptor bufferDesc;
|
||||||
public int firstUsePass;
|
public int firstUsePass;
|
||||||
public int lastUsePass;
|
public int lastUsePass;
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,7 @@ internal sealed class RenderGraphCompilationCache
|
|||||||
_cached.logicalToPhysical[kvp.Key] = kvp.Value;
|
_cached.logicalToPhysical[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cached.physicalResources.AddRange(data.physicalResources);
|
_cached.placedResources.AddRange(data.placedResources);
|
||||||
_cached.barriers.AddRange(data.barriers);
|
_cached.barriers.AddRange(data.barriers);
|
||||||
|
|
||||||
foreach (var kvp in data.resourceStates)
|
foreach (var kvp in data.resourceStates)
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ internal sealed class MockCommandBuffer
|
|||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
|
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
|
#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 Ghost.Core;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
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>>[] resourceWrites = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
|
||||||
public readonly List<Identifier<RGResource>>[] resourceCreates = 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
|
// Execution state
|
||||||
public bool culled;
|
public bool culled;
|
||||||
public bool hasSideEffects;
|
public bool hasSideEffects;
|
||||||
@@ -49,8 +53,8 @@ internal abstract class RenderGraphPassBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Execute(RenderContext context);
|
public abstract void Execute(RenderContext context);
|
||||||
public abstract void Clear();
|
|
||||||
public abstract bool HasRenderFunc();
|
public abstract bool HasRenderFunc();
|
||||||
|
public abstract int GetRenderFuncHashCode();
|
||||||
|
|
||||||
public virtual void Reset(RenderGraphObjectPool pool)
|
public virtual void Reset(RenderGraphObjectPool pool)
|
||||||
{
|
{
|
||||||
@@ -73,6 +77,8 @@ internal abstract class RenderGraphPassBase
|
|||||||
resourceCreates[i].Clear();
|
resourceCreates[i].Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufferHints.Clear();
|
||||||
|
|
||||||
culled = false;
|
culled = false;
|
||||||
hasSideEffects = false;
|
hasSideEffects = false;
|
||||||
}
|
}
|
||||||
@@ -97,17 +103,24 @@ internal abstract class RenderGraphPassT<TPassData, TRenderContext> : RenderGrap
|
|||||||
return renderFunc != null;
|
return renderFunc != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Clear()
|
public override int GetRenderFuncHashCode()
|
||||||
{
|
{
|
||||||
passData = null!;
|
if (renderFunc == null)
|
||||||
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)
|
public override void Reset(RenderGraphObjectPool pool)
|
||||||
{
|
{
|
||||||
base.Reset(pool);
|
base.Reset(pool);
|
||||||
pool.Return(passData);
|
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>
|
/// <summary>
|
||||||
/// Represents a texture resource in the render graph.
|
/// Represents a resource in the render graph (texture or buffer).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RenderGraphResource
|
internal sealed class RenderGraphResource
|
||||||
{
|
{
|
||||||
public RenderGraphResourceType type;
|
public RenderGraphResourceType type;
|
||||||
public int index;
|
public int index;
|
||||||
public TextureDescriptor descriptor;
|
public TextureDescriptor textureDescriptor;
|
||||||
|
public BufferDescriptor bufferDescriptor;
|
||||||
public bool isImported;
|
public bool isImported;
|
||||||
public int firstUsePass = -1;
|
public int firstUsePass = -1;
|
||||||
public int lastUsePass = -1;
|
public int lastUsePass = -1;
|
||||||
@@ -91,8 +92,10 @@ internal sealed class RenderGraphResource
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
type = RenderGraphResourceType.Texture;
|
||||||
index = -1;
|
index = -1;
|
||||||
descriptor = default;
|
textureDescriptor = default;
|
||||||
|
bufferDescriptor = default;
|
||||||
isImported = false;
|
isImported = false;
|
||||||
firstUsePass = -1;
|
firstUsePass = -1;
|
||||||
lastUsePass = -1;
|
lastUsePass = -1;
|
||||||
@@ -105,21 +108,48 @@ internal sealed class RenderGraphResource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registry for managing all resources in the render graph.
|
/// Registry for managing all resources in the render graph.
|
||||||
/// Uses pooling to minimize allocations after the first frame.
|
/// Uses pooling to minimize allocations after the first frame.
|
||||||
|
/// Uses a single unified list for both textures and buffers with global indexing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RenderGraphResourceRegistry
|
internal sealed class RenderGraphResourceRegistry
|
||||||
{
|
{
|
||||||
private readonly List<RenderGraphResource> _resources = new(64);
|
private readonly List<RenderGraphResource> _resources = new(64);
|
||||||
private readonly RenderGraphObjectPool _pool = new();
|
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()
|
public void BeginFrame()
|
||||||
{
|
{
|
||||||
|
// Return all resources to pool
|
||||||
for (var i = 0; i < _resources.Count; i++)
|
for (var i = 0; i < _resources.Count; i++)
|
||||||
{
|
{
|
||||||
_pool.Return(_resources[i]);
|
_pool.Return(_resources[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resources.Clear();
|
_resources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +158,7 @@ internal sealed class RenderGraphResourceRegistry
|
|||||||
var resource = _pool.Rent<RenderGraphResource>();
|
var resource = _pool.Rent<RenderGraphResource>();
|
||||||
resource.type = RenderGraphResourceType.Texture;
|
resource.type = RenderGraphResourceType.Texture;
|
||||||
resource.index = _resources.Count;
|
resource.index = _resources.Count;
|
||||||
resource.descriptor = descriptor;
|
resource.textureDescriptor = descriptor;
|
||||||
resource.isImported = true;
|
resource.isImported = true;
|
||||||
|
|
||||||
_resources.Add(resource);
|
_resources.Add(resource);
|
||||||
@@ -141,7 +171,7 @@ internal sealed class RenderGraphResourceRegistry
|
|||||||
var resource = _pool.Rent<RenderGraphResource>();
|
var resource = _pool.Rent<RenderGraphResource>();
|
||||||
resource.type = RenderGraphResourceType.Texture;
|
resource.type = RenderGraphResourceType.Texture;
|
||||||
resource.index = _resources.Count;
|
resource.index = _resources.Count;
|
||||||
resource.descriptor = descriptor;
|
resource.textureDescriptor = descriptor;
|
||||||
resource.isImported = false;
|
resource.isImported = false;
|
||||||
|
|
||||||
_resources.Add(resource);
|
_resources.Add(resource);
|
||||||
@@ -149,12 +179,51 @@ internal sealed class RenderGraphResourceRegistry
|
|||||||
return new Identifier<RGTexture>(resource.index);
|
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)
|
public RenderGraphResource GetResource(Identifier<RGResource> resource)
|
||||||
{
|
{
|
||||||
return _resources[resource.Value];
|
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];
|
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
|
internal readonly struct TextureAccess
|
||||||
{
|
{
|
||||||
public readonly Identifier<RGTexture> id;
|
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>
|
/// <summary>
|
||||||
/// Texture formats supported by the render graph.
|
/// Texture formats supported by the render graph.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -165,3 +201,70 @@ public readonly struct BufferDescriptor : IEquatable<BufferDescriptor>
|
|||||||
public interface IPassData
|
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