Render graph: native pass merging & heap-based aliasing

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

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

View File

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

View File

@@ -2,8 +2,10 @@ using Misaki.HighPerformance.LowLevel;
using System.ComponentModel; using System.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;

View File

@@ -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" />

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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
};
} }
} }

View File

@@ -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>

View File

@@ -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();
});
} }
} }
} }

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ global using static TerraFX.Interop.DirectX.DXGI;
global using static TerraFX.Interop.Windows.Windows; 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")]

View File

@@ -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++)
{ {

View File

@@ -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)]

View File

@@ -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;

View File

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

View File

@@ -151,8 +151,8 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
// Set descriptor heaps for bindless resources and samplers // 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);

View File

@@ -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;

View File

@@ -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)
{ {

View File

@@ -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));

View File

@@ -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;

View File

@@ -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.

View File

@@ -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();

View File

@@ -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;

View File

@@ -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++)

View File

@@ -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;
} }

View File

@@ -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
}

View File

@@ -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);
} }

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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");
} }

View File

@@ -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)
{ {

View File

@@ -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);

View File

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

View File

@@ -25,6 +25,7 @@ public sealed class SSAOPassData : IPassData
public Identifier<RGTexture> GBufferDepth; public Identifier<RGTexture> 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;

View File

@@ -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

View File

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

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.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
}
} }

View File

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

View File

@@ -1,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
}); });
} }
} }

View File

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

View File

@@ -19,6 +19,7 @@ public enum ResourceState
CopySource = 1 << 5, 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;
} }
} }

View File

@@ -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
{ {

View File

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

View File

@@ -17,8 +17,8 @@ internal sealed class CachedCompilation
// Physical resource aliasing mappings (logical index -> physical index) // 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)

View File

@@ -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
} }
} }

View File

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

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using 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;
} }
} }

View File

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

View File

@@ -75,13 +75,14 @@ internal sealed class RenderGraphObjectPool
} }
/// <summary> /// <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];
} }

View File

@@ -42,6 +42,25 @@ public static class RGResourceExtensions
} }
} }
/// <summary>
/// Hints for how a buffer will be used in a pass.
/// Used to determine correct resource state transitions.
/// </summary>
[Flags]
public enum BufferHint
{
/// <summary>
/// No special usage - buffer will be used as shader resource (SRV) or UAV based on AccessFlags.
/// </summary>
None = 0,
/// <summary>
/// Buffer will be used as indirect argument buffer (ExecuteIndirect).
/// Requires ResourceState.IndirectArgument.
/// </summary>
IndirectArgument = 1 << 0,
}
internal readonly struct TextureAccess 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;
}