Refactor Render Graph: unified resources, benchmarking

Major overhaul of Render Graph system:
- Replaced texture handles with generic Identifier<T> for unified, type-safe resource management (textures, buffers, etc.)
- Refactored resource registry and pooling for performance and extensibility
- Added AccessFlags and TextureAccess for precise resource usage tracking
- Split passes into Raster and Compute types; introduced builder interfaces for safer pass construction
- Modernized pass setup API (SetColorAttachment, UseTexture, etc.)
- Updated command buffer and context structs to use new resource system
- Refactored barrier and aliasing logic for improved correctness
- Integrated BenchmarkDotNet for performance/memory benchmarking
- Improved blackboard type safety and removed obsolete code/extensions
- Added BenchmarkDotNet NuGet package

These changes make the Render Graph more extensible, efficient, and ready for future resource types and advanced features.
This commit is contained in:
2026-01-12 23:48:56 +09:00
parent 1fc9df1812
commit 954e3756aa
15 changed files with 940 additions and 776 deletions

View File

@@ -0,0 +1,200 @@
using BenchmarkDotNet.Attributes;
using Ghost.Core;
namespace Ghost.RenderGraph.Concept.Benchmark;
[MemoryDiagnoser]
public class RenderGraphBenchmark
{
private RenderGraph _renderGraph = null!;
[GlobalSetup]
public void Setup()
{
_renderGraph = new RenderGraph();
}
[Benchmark]
public void Execute()
{
ExecuteGraph(_renderGraph);
}
public static void ExecuteGraph(RenderGraph renderGraph)
{
renderGraph.Reset(); // new RenderGraph()
// Import external resources
var backbuffer = renderGraph.ImportTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
// ===== GBuffer Pass =====
GBufferData gbufferData;
using (var builder = renderGraph.AddRasterRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
{
// Create GBuffer textures
gbufferData.Albedo = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
gbufferData.Normal = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal"));
gbufferData.Depth = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth"));
// Store in pass data and mark as written
builder.SetColorAttachment(gbufferData.Albedo, 0);
builder.SetColorAttachment(gbufferData.Normal, 1);
builder.SetDepthAttachment(gbufferData.Depth);
builder.SetRenderFunc<GBufferData>(static (data, cmd) =>
{
// New api will handle render target setup and clearing automatically
//cmd.SetRenderTarget(data.Albedo.Name);
//cmd.SetRenderTarget(data.Normal.Name);
//cmd.SetDepthStencil(data.Depth.Name);
//cmd.ClearRenderTarget(data.Albedo.Name, 0, 0, 0, 1);
//cmd.ClearRenderTarget(data.Normal.Name, 0.5f, 0.5f, 1.0f, 1);
//cmd.ClearDepth(data.Depth.Name, 1.0f);
cmd.Draw(36000);
});
}
// Store GBuffer data in blackboard for other passes
renderGraph.Blackboard.Add(gbufferData);
// ===== Lighting Pass =====
Identifier<RGTexture> lightingOutput;
using (var builder = renderGraph.AddRasterRenderPass<LightingPassData>("Lighting Pass", out var lightingData))
{
// Read GBuffer from blackboard
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
lightingData.GBufferAlbedo = builder.UseTexture(gbuffer.Albedo, AccessFlags.Read);
lightingData.GBufferNormal = builder.UseTexture(gbuffer.Normal, AccessFlags.Read);
lightingData.GBufferDepth = builder.UseTexture(gbuffer.Depth, AccessFlags.Read);
// Create output texture
lightingOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
builder.SetColorAttachment(lightingOutput, 1);
lightingData.OutputLighting = lightingOutput;
builder.SetRenderFunc<LightingPassData>(static (data, cmd) =>
{
cmd.BindShaderResource(data.GBufferAlbedo.AsResource(), 0);
cmd.BindShaderResource(data.GBufferNormal.AsResource(), 1);
cmd.BindShaderResource(data.GBufferDepth.AsResource(), 2);
cmd.Draw(3);
});
}
// ===== SSAO Pass (Async Compute) =====
Identifier<RGTexture> ssaoOutput;
using (var builder = renderGraph.AddComputeRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
{
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
ssaoData.GBufferDepth = builder.UseTexture(gbuffer.Depth, AccessFlags.Read);
ssaoData.GBufferNormal = builder.UseTexture(gbuffer.Normal, AccessFlags.Read);
// SSAO Output
ssaoOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
ssaoData.OutputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Write);
builder.EnableAsyncCompute(true);
// Use SetComputeFunc with asyncCompute: true
builder.SetRenderFunc<SSAOPassData>(static (data, cmd) =>
{
cmd.BindShaderResource(data.GBufferDepth.AsResource(), 0);
cmd.BindShaderResource(data.GBufferNormal.AsResource(), 1);
cmd.BindUnorderedAccess(data.OutputSSAO.AsResource(), 0);
cmd.Dispatch(1920 / 8, 1080 / 8, 1);
});
}
// ===== Bloom Downsample Pass (will alias with albedo) =====
Identifier<RGTexture> bloomOutput;
using (var builder = renderGraph.AddRasterRenderPass<BloomDownsampleData>("Bloom Downsample", out var bloomData))
{
bloomData.Input = builder.UseTexture(lightingOutput, AccessFlags.Read);
// Create a texture that will alias with SSAO (same size, same format)
bloomOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
builder.SetColorAttachment(bloomOutput, 0);
bloomData.Output = bloomOutput;
builder.SetRenderFunc<BloomDownsampleData>(static (data, cmd) =>
{
cmd.BindShaderResource(data.Input.AsResource(), 0);
cmd.Draw(3);
});
}
// ===== Temporal AA Pass =====
Identifier<RGTexture> taaOutput;
using (var builder = renderGraph.AddRasterRenderPass<TAAPassData>("Temporal AA", out var taaData))
{
taaData.InputLighting = builder.UseTexture(lightingOutput, AccessFlags.Read);
taaOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
builder.SetColorAttachment(taaOutput, 0);
taaData.OutputTAA = taaOutput;
builder.SetRenderFunc<TAAPassData>(static (data, cmd) =>
{
cmd.BindShaderResource(data.InputLighting.AsResource(), 0);
cmd.Draw(3);
});
}
// ===== Post Processing Pass =====
using (var builder = renderGraph.AddRasterRenderPass<PostProcessingPassDataV2>("Post Processing", out var postData))
{
postData.InputTAA = builder.UseTexture(taaOutput, AccessFlags.Read);
postData.InputSSAO = builder.UseTexture(ssaoOutput, AccessFlags.Read);
postData.InputBloom = builder.UseTexture(bloomOutput, AccessFlags.Read);
builder.SetColorAttachment(backbuffer, 0);
builder.SetRenderFunc<PostProcessingPassDataV2>(static (data, cmd) =>
{
cmd.BindShaderResource(data.InputTAA.AsResource(), 0);
cmd.BindShaderResource(data.InputSSAO.AsResource(), 1);
cmd.BindShaderResource(data.InputBloom.AsResource(), 2);
cmd.Draw(3);
});
}
// ===== GPU Profiler Marker Pass (non-cullable, textureless) =====
using (var builder = renderGraph.AddRasterRenderPass<ProfilerMarkerData>("GPU Profiler Begin Frame", out var profilerData))
{
builder.AllowPassCulling(false); // Never cull this - it's for debugging/profiling
builder.SetRenderFunc<ProfilerMarkerData>(static (data, cmd) =>
{
// Note: In a real implementation we would have specific profiler commands
// For now, since RasterRenderContext doesn't expose generic console write, we skip the print
// or we would add a specific Profiler method to the context
});
}
// ===== Unused Debug Pass (will be culled) =====
using (var builder = renderGraph.AddRasterRenderPass<DebugPassData>("Unused Debug Pass", out var debugData))
{
builder.SetColorAttachment(
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")), 0);
builder.SetRenderFunc<DebugPassData>(static (data, cmd) =>
{
cmd.Draw(100);
});
}
// Compile and execute the render graph
renderGraph.Compile();
renderGraph.Execute();
}
}

View File

@@ -16,6 +16,10 @@
<DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>

View File

@@ -1,3 +1,5 @@
using Ghost.Core;
namespace Ghost.RenderGraph.Concept;
// ===== Pass Data Structures =====
@@ -5,44 +7,44 @@ namespace Ghost.RenderGraph.Concept;
public sealed class GBufferData : IPassData
{
public RenderGraphTextureHandle Albedo;
public RenderGraphTextureHandle Normal;
public RenderGraphTextureHandle Depth;
public Identifier<RGTexture> Albedo;
public Identifier<RGTexture> Normal;
public Identifier<RGTexture> Depth;
}
public sealed class LightingPassData : IPassData
{
public RenderGraphTextureHandle GBufferAlbedo;
public RenderGraphTextureHandle GBufferNormal;
public RenderGraphTextureHandle GBufferDepth;
public RenderGraphTextureHandle OutputLighting;
public Identifier<RGTexture> GBufferAlbedo;
public Identifier<RGTexture> GBufferNormal;
public Identifier<RGTexture> GBufferDepth;
public Identifier<RGTexture> OutputLighting;
}
public sealed class SSAOPassData : IPassData
{
public RenderGraphTextureHandle GBufferDepth;
public RenderGraphTextureHandle GBufferNormal;
public RenderGraphTextureHandle OutputSSAO;
public Identifier<RGTexture> GBufferDepth;
public Identifier<RGTexture> GBufferNormal;
public Identifier<RGTexture> OutputSSAO;
}
public sealed class BloomDownsampleData : IPassData
{
public RenderGraphTextureHandle Input;
public RenderGraphTextureHandle Output;
public Identifier<RGTexture> Input;
public Identifier<RGTexture> Output;
}
public sealed class TAAPassData : IPassData
{
public RenderGraphTextureHandle InputLighting;
public RenderGraphTextureHandle OutputTAA;
public Identifier<RGTexture> InputLighting;
public Identifier<RGTexture> OutputTAA;
}
public sealed class PostProcessingPassDataV2 : IPassData
{
public RenderGraphTextureHandle InputTAA;
public RenderGraphTextureHandle InputSSAO;
public RenderGraphTextureHandle InputBloom;
public RenderGraphTextureHandle OutputBackbuffer;
public Identifier<RGTexture> InputTAA;
public Identifier<RGTexture> InputSSAO;
public Identifier<RGTexture> InputBloom;
public Identifier<RGTexture> OutputBackbuffer;
}
public sealed class ProfilerMarkerData : IPassData
@@ -51,5 +53,5 @@ public sealed class ProfilerMarkerData : IPassData
public sealed class DebugPassData : IPassData
{
public RenderGraphTextureHandle DebugTexture;
public Identifier<RGTexture> DebugTexture;
}

View File

@@ -1,210 +1,42 @@
using Ghost.Core;
using Ghost.RenderGraph.Concept;
using Ghost.RenderGraph.Concept.Benchmark;
var renderGraph = new RenderGraph();
#if !DEBUG
const int _ITERATION = 500;
for (var i = 0; i < _ITERATION; i++)
{
ExecuteGraph(renderGraph);
}
GC.Collect();
GC.WaitForPendingFinalizers();
// Thread.Sleep(1000); // Leave a gap in visual studio allocations timeline
var sw = new System.Diagnostics.Stopwatch();
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
sw.Start();
BenchmarkDotNet.Running.BenchmarkRunner.Run<RenderGraphBenchmark>();
return;
for (var i = 0; i < _ITERATION; i++)
{
ExecuteGraph(renderGraph);
}
//const int _ITERATION = 500000;
//for (var i = 0; i < _ITERATION; i++)
//{
// RenderGraphBenchmark.ExecuteGraph(renderGraph);
//}
sw.Stop();
var gcAfter = GC.GetAllocatedBytesForCurrentThread();
//GC.Collect();
//GC.WaitForPendingFinalizers();
////Thread.Sleep(1000); // Leave a gap in visual studio allocations timeline
//var sw = new System.Diagnostics.Stopwatch();
//var gcBefore = GC.GetAllocatedBytesForCurrentThread();
//sw.Start();
Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / _ITERATION} ns (per iteration)");
Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / _ITERATION} bytes (per iteration)");
//for (var i = 0; i < _ITERATION; i++)
//{
// RenderGraphBenchmark.ExecuteGraph(renderGraph);
//}
//sw.Stop();
//var gcAfter = GC.GetAllocatedBytesForCurrentThread();
//Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / _ITERATION} ns (per iteration)");
//Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / _ITERATION} bytes (per iteration)");
#else
// Run twice to demonstrate cache hit
Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ===");
ExecuteGraph(renderGraph);
RenderGraphBenchmark.ExecuteGraph(renderGraph);
Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
ExecuteGraph(renderGraph);
RenderGraphBenchmark.ExecuteGraph(renderGraph);
#endif
static void ExecuteGraph(RenderGraph renderGraph)
{
renderGraph.Reset(); // new RenderGraph()
// Import external resources
var backbuffer = renderGraph.ImportTexture(
"Backbuffer",
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
// ===== GBuffer Pass =====
GBufferData gbufferData;
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
{
// Create GBuffer textures
var albedo = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
var normal = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal"));
var depth = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth"));
// Store in pass data and mark as written
gbufferData.Albedo = builder.WriteTexture(albedo);
gbufferData.Normal = builder.WriteTexture(normal);
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
builder.SetRenderFunc<GBufferData>((data, cmd) =>
{
cmd.SetRenderTarget(data.Albedo.Name);
cmd.SetRenderTarget(data.Normal.Name);
cmd.SetDepthStencil(data.Depth.Name);
cmd.ClearRenderTarget(data.Albedo.Name, 0, 0, 0, 1);
cmd.ClearRenderTarget(data.Normal.Name, 0.5f, 0.5f, 1.0f, 1);
cmd.ClearDepth(data.Depth.Name, 1.0f);
cmd.Draw(36000);
});
}
// Store GBuffer data in blackboard for other passes
renderGraph.Blackboard.Add(gbufferData);
// ===== Lighting Pass =====
RenderGraphTextureHandle lightingOutput;
using (var builder = renderGraph.AddRenderPass<LightingPassData>("Lighting Pass", out var lightingData))
{
// Read GBuffer from blackboard
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo);
lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
lightingData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
// Create output texture
lightingOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
builder.SetRenderFunc<LightingPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
cmd.BindShaderResource(data.GBufferDepth.Name, 2);
cmd.SetRenderTarget(data.OutputLighting.Name);
cmd.Draw(3);
});
}
// ===== SSAO Pass (Async Compute) =====
RenderGraphTextureHandle ssaoOutput;
using (var builder = renderGraph.AddRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
{
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
ssaoData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
ssaoData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
// SSAO Output
ssaoOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
// Use SetComputeFunc with asyncCompute: true
builder.SetComputeFunc<SSAOPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
cmd.BindUnorderedAccess(data.OutputSSAO.Name, 0);
cmd.Dispatch(1920 / 8, 1080 / 8, 1);
}, asyncCompute: true);
}
// ===== Bloom Downsample Pass (will alias with albedo) =====
RenderGraphTextureHandle bloomOutput;
using (var builder = renderGraph.AddRenderPass<BloomDownsampleData>("Bloom Downsample", out var bloomData))
{
bloomData.Input = builder.ReadTexture(lightingOutput);
// Create a texture that will alias with SSAO (same size, same format)
bloomOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
bloomData.Output = builder.WriteTexture(bloomOutput);
builder.SetRenderFunc<BloomDownsampleData>((data, cmd) =>
{
cmd.BindShaderResource(data.Input.Name, 0);
cmd.SetRenderTarget(data.Output.Name);
cmd.Draw(3);
});
}
// ===== Temporal AA Pass =====
RenderGraphTextureHandle taaOutput;
using (var builder = renderGraph.AddRenderPass<TAAPassData>("Temporal AA", out var taaData))
{
taaData.InputLighting = builder.ReadTexture(lightingOutput);
taaOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
taaData.OutputTAA = builder.WriteTexture(taaOutput);
builder.SetRenderFunc<TAAPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.InputLighting.Name, 0);
cmd.SetRenderTarget(data.OutputTAA.Name);
cmd.Draw(3);
});
}
// ===== Post Processing Pass =====
using (var builder = renderGraph.AddRenderPass<PostProcessingPassDataV2>("Post Processing", out var postData))
{
postData.InputTAA = builder.ReadTexture(taaOutput);
postData.InputSSAO = builder.ReadTexture(ssaoOutput);
postData.InputBloom = builder.ReadTexture(bloomOutput);
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
builder.SetRenderFunc<PostProcessingPassDataV2>((data, cmd) =>
{
cmd.BindShaderResource(data.InputTAA.Name, 0);
cmd.BindShaderResource(data.InputSSAO.Name, 1);
cmd.BindShaderResource(data.InputBloom.Name, 2);
cmd.SetRenderTarget(data.OutputBackbuffer.Name);
cmd.Draw(3);
});
}
// ===== GPU Profiler Marker Pass (non-cullable, textureless) =====
using (var builder = renderGraph.AddRenderPass<ProfilerMarkerData>("GPU Profiler Begin Frame", out var profilerData))
{
builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling
builder.SetRenderFunc<ProfilerMarkerData>((data, cmd) =>
{
// Note: In a real implementation we would have specific profiler commands
// For now, since RasterRenderContext doesn't expose generic console write, we skip the print
// or we would add a specific Profiler method to the context
});
}
// ===== Unused Debug Pass (will be culled) =====
using (var builder = renderGraph.AddRenderPass<DebugPassData>("Unused Debug Pass", out var debugData))
{
debugData.DebugTexture = builder.WriteTexture(
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
builder.SetRenderFunc<DebugPassData>((data, cmd) =>
{
cmd.SetRenderTarget(data.DebugTexture.Name);
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
cmd.Draw(100);
});
}
// Compile and execute the render graph
renderGraph.Compile();
renderGraph.Execute();
}

View File

@@ -1,7 +1,8 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
using System.Threading;
using TerraFX.Interop.Windows;
namespace Ghost.RenderGraph.Concept;
@@ -28,9 +29,6 @@ public sealed class RenderGraph
private readonly List<ResourceBarrier> _barriers = new(128);
private readonly RenderGraphCompilationCache _compilationCache = new();
private readonly XxHash64 _hasher = new();
private int _passCount;
private bool _compiled;
public RenderGraphBlackboard Blackboard { get; } = new();
@@ -60,13 +58,13 @@ public sealed class RenderGraph
_barriers.Clear();
// Return passes to the pool and reset count
for (var i = 0; i < _passCount; i++)
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
pass.Clear();
_objectPool.Release(pass);
pass.Reset(_objectPool);
}
_passCount = 0;
_passes.Clear();
// Clear compiled passes list
_compiledPasses.Clear();
@@ -76,57 +74,34 @@ public sealed class RenderGraph
/// <summary>
/// Imports an external texture into the render graph.
/// </summary>
public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor)
public Identifier<RGTexture> ImportTexture(TextureDescriptor descriptor)
{
return _resources.ImportTexture(descriptor);
}
/// <summary>
/// Adds a new render pass to the graph.
/// </summary>
public RenderGraphBuilder AddRenderPass<TPassData>(string name, out TPassData passData)
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
// Get or create pass from pool
RenderGraphPass<TPassData> pass;
if (_passCount < _passes.Count)
{
// Reuse existing slot
var existingPass = _passes[_passCount];
if (existingPass is RenderGraphPass<TPassData> typedPass)
{
pass = typedPass;
pass.Reset();
}
else
{
// Type mismatch, need to replace
_objectPool.Release(existingPass);
pass = _objectPool.Get<RenderGraphPass<TPassData>>();
pass.Reset();
_passes[_passCount] = pass;
}
}
else
{
// Need to grow the list
pass = _objectPool.Get<RenderGraphPass<TPassData>>();
pass.Reset();
_passes.Add(pass);
}
var renderPass = _objectPool.Rent<RasterRenderGraphPass<TPassData>>();
renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Raster);
passData = renderPass.passData;
// Initialize pass
pass.Name = name;
pass.Index = _passCount;
_passes.Add(renderPass);
// Get or create pass data from pool
passData = _objectPool.Get<TPassData>();
pass.PassData = passData;
_builder.Init(this, renderPass, _resources);
return _builder;
}
_passCount++;
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;
// Initialize builder
_builder.Initialize(pass, _resources);
_passes.Add(renderPass);
_builder.Init(this, renderPass, _resources);
return _builder;
}
@@ -138,82 +113,84 @@ public sealed class RenderGraph
private unsafe ulong ComputeGraphHash()
{
using var scope = AllocationManager.CreateStackScope();
var bufferPool = new UnsafeList<byte>(4096, scope.AllocationHandle);
int offset = 0;
var bufferPool = new UnsafeList<byte>(2048, scope.AllocationHandle);
var pData = (byte*)bufferPool.GetUnsafePtr();
_hasher.Reset();
var offset = 0;
// Hash pass count
_hasher.AppendInt(_passCount);
*(int*)(pData + offset) = _passes.Count;
offset += sizeof(int);
// Hash each pass structure (excluding names)
for (int i = 0; i < _passCount; i++)
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
// Save 0.004ms.
//// Hash pass properties that affect compilation
//_hasher.AppendEnum(pass.Type);
//_hasher.AppendBool(pass.AllowCulling);
//_hasher.AppendBool(pass.AsyncCompute);
//// Hash texture dependencies (only indices, not versions or names)
//_hasher.AppendHandleList(pass.TextureReads);
//_hasher.AppendHandleList(pass.TextureWrites);
//_hasher.AppendHandleList(pass.TextureCreates);
*(RenderPassType*)(pData + offset) = pass.Type;
*(RenderPassType*)(pData + offset) = pass.type;
offset += sizeof(RenderPassType);
*(bool*)(pData + offset) = pass.AllowCulling;
*(bool*)(pData + offset) = pass.allowCulling;
offset += sizeof(bool);
*(bool*)(pData + offset) = pass.AsyncCompute;
*(bool*)(pData + offset) = pass.asyncCompute;
offset += sizeof(bool);
*(int*)(pData + offset) = pass.TextureReads.Count;
*(TextureAccess*)(pData + offset) = pass.depthAccess;
offset += sizeof(TextureAccess);
*(int*)(pData + offset) = pass.maxColorIndex;
offset += sizeof(int);
for (int j = 0; j < pass.TextureReads.Count; j++)
for (var j = 0; j <= pass.maxColorIndex; j++)
{
*(int*)(pData + offset) = pass.TextureReads[j].Index;
*(TextureAccess*)(pData + offset) = pass.colorAccess[j];
offset += sizeof(TextureAccess);
}
*(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.TextureWrites.Count;
*(int*)(pData + offset) = pass.resourceWrites.Count;
offset += sizeof(int);
for (int j = 0; j < pass.TextureWrites.Count; j++)
for (var j = 0; j < pass.resourceWrites.Count; j++)
{
*(int*)(pData + offset) = pass.TextureWrites[j].Index;
*(int*)(pData + offset) = pass.resourceWrites[j].Value;
offset += sizeof(int);
}
*(int*)(pData + offset) = pass.TextureCreates.Count;
*(int*)(pData + offset) = pass.resourceCreates.Count;
offset += sizeof(int);
for (int j = 0; j < pass.TextureCreates.Count; j++)
for (var j = 0; j < pass.resourceCreates.Count; j++)
{
*(int*)(pData + offset) = pass.TextureCreates[j].Index;
*(int*)(pData + offset) = pass.resourceCreates[j].Value;
offset += sizeof(int);
}
}
// Hash resource descriptors
for (int i = 0; i < _resources.TextureResourceCount; i++)
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);
_hasher.Append(span);
return _hasher.GetCurrentHashAsUInt64();
return XxHash64.HashToUInt64(span);
}
/// <summary>
@@ -222,14 +199,16 @@ public sealed class RenderGraph
public void Compile()
{
if (_compiled)
{
return;
}
#if DEBUG
var sw = System.Diagnostics.Stopwatch.StartNew();
#endif
// Step 0: Check cache
ulong graphHash = ComputeGraphHash();
var graphHash = ComputeGraphHash(); // 1321433047288519964
#if DEBUG
var hashTime = sw.Elapsed.TotalMicroseconds;
@@ -257,18 +236,18 @@ public sealed class RenderGraph
_compiledPasses.Clear();
// Step 1: Mark passes with side effects (writes to imported resources)
for (var i = 0; i < _passCount; i++)
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.TextureWrites.Count; j++)
for (var j = 0; j < pass.resourceWrites.Count; j++)
{
var writeHandle = pass.TextureWrites[j];
var resource = _resources.GetTextureResource(writeHandle);
var writeHandle = pass.resourceWrites[j];
var resource = _resources.GetResource(writeHandle);
if (resource.IsImported)
{
pass.HasSideEffects = true;
pass.hasSideEffects = true;
break;
}
}
@@ -276,33 +255,33 @@ public sealed class RenderGraph
// Step 2: Cull passes based on dependency analysis
// Mark all passes as culled initially
for (var i = 0; i < _passCount; i++)
for (var i = 0; i < _passes.Count; i++)
{
_passes[i].Culled = _passes[i].AllowCulling && !_passes[i].HasSideEffects;
_passes[i].culled = _passes[i].allowCulling && !_passes[i].hasSideEffects;
}
// Step 3: Traverse backwards from passes with side effects
for (var i = _passCount - 1; i >= 0; i--)
for (var i = _passes.Count - 1; i >= 0; i--)
{
var pass = _passes[i];
if (!pass.Culled)
if (!pass.culled)
{
UnculDependencies(pass);
}
}
// Step 4: Build final pass list (only non-culled passes)
for (var i = 0; i < _passCount; i++)
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
if (!pass.Culled)
if (!pass.culled)
{
_compiledPasses.Add(pass);
}
}
// Step 5: Perform resource aliasing to minimize memory usage
_aliasingManager.AssignPhysicalResources(_resources, _passCount);
_aliasingManager.AssignPhysicalResources(_resources, _passes.Count);
// Step 6: Generate barriers for state transitions and aliasing
GenerateBarriers();
@@ -320,16 +299,16 @@ public sealed class RenderGraph
{
// Restore compiled pass list
_compiledPasses.Clear();
for (int i = 0; i < cached.CompiledPassIndices.Count; i++)
for (var i = 0; i < cached.CompiledPassIndices.Count; i++)
{
int passIndex = cached.CompiledPassIndices[i];
var passIndex = cached.CompiledPassIndices[i];
_compiledPasses.Add(_passes[passIndex]);
}
// Restore culling flags
for (int i = 0; i < _passCount && i < cached.PassCulledFlags.Count; i++)
for (var i = 0; i < _passes.Count && i < cached.PassCulledFlags.Count; i++)
{
_passes[i].Culled = cached.PassCulledFlags[i];
_passes[i].culled = cached.PassCulledFlags[i];
}
// Restore aliasing mappings (need to update ResourceAliasingManager)
@@ -337,7 +316,7 @@ public sealed class RenderGraph
// Restore barriers (deep copy to avoid shared references)
_barriers.Clear();
for (int i = 0; i < cached.Barriers.Count; i++)
for (var i = 0; i < cached.Barriers.Count; i++)
{
_barriers.Add(cached.Barriers[i]);
}
@@ -358,22 +337,22 @@ public sealed class RenderGraph
var cacheData = new CachedCompilation();
// Store compiled pass indices
for (int i = 0; i < _compiledPasses.Count; i++)
for (var i = 0; i < _compiledPasses.Count; i++)
{
cacheData.CompiledPassIndices.Add(_compiledPasses[i].Index);
cacheData.CompiledPassIndices.Add(_compiledPasses[i].index);
}
// Store culling flags for all passes
for (int i = 0; i < _passCount; i++)
for (var i = 0; i < _passes.Count; i++)
{
cacheData.PassCulledFlags.Add(_passes[i].Culled);
cacheData.PassCulledFlags.Add(_passes[i].culled);
}
// Store aliasing mappings
_aliasingManager.StoreToCache(cacheData.LogicalToPhysical, cacheData.PhysicalResources);
// Store barriers
for (int i = 0; i < _barriers.Count; i++)
for (var i = 0; i < _barriers.Count; i++)
{
cacheData.Barriers.Add(_barriers[i]);
}
@@ -393,17 +372,17 @@ public sealed class RenderGraph
private void UnculDependencies(RenderGraphPassBase pass)
{
// Un-cull all producers of textures we read
for (var i = 0; i < pass.TextureReads.Count; i++)
for (var i = 0; i < pass.resourceReads.Count; i++)
{
var readHandle = pass.TextureReads[i];
var resource = _resources.GetTextureResource(readHandle);
var readHandle = pass.resourceReads[i];
var resource = _resources.GetResource(readHandle);
if (resource.ProducerPass >= 0)
{
var producer = _passes[resource.ProducerPass];
if (producer.Culled)
if (producer.culled)
{
producer.Culled = false;
producer.culled = false;
UnculDependencies(producer);
}
}
@@ -446,20 +425,20 @@ public sealed class RenderGraph
private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx)
{
// Check all resources written by this pass
for (int i = 0; i < pass.TextureWrites.Count; i++)
for (var i = 0; i < pass.resourceWrites.Count; i++)
{
var handle = pass.TextureWrites[i];
var resource = _resources.GetTextureResource(handle);
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)
if (resource.FirstUsePass == pass.index)
{
// Get the physical resource
int physicalIndex = _aliasingManager.GetPhysicalResourceIndex(handle.Index);
// Rent the physical resource
var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
if (physicalIndex >= 0)
{
var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
@@ -469,23 +448,20 @@ public sealed class RenderGraph
if (physical != null && physical.AliasedLogicalResources.Count > 1)
{
// Find the resource that used this physical memory most recently before this pass
RenderGraphTextureHandle resourceBefore = default;
int mostRecentLastUse = -1;
Identifier<RGResource> resourceBefore = default;
var mostRecentLastUse = -1;
foreach (int otherLogicalIndex in physical.AliasedLogicalResources)
foreach (var otherLogicalIndex in physical.AliasedLogicalResources)
{
if (otherLogicalIndex != handle.Index)
if (otherLogicalIndex != id.Value)
{
var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts
if (otherResource.LastUsePass < pass.Index &&
if (otherResource.LastUsePass < pass.index &&
otherResource.LastUsePass > mostRecentLastUse)
{
mostRecentLastUse = otherResource.LastUsePass;
resourceBefore = new RenderGraphTextureHandle(
otherLogicalIndex,
otherResource.Version,
otherResource.Descriptor.Name);
resourceBefore = otherLogicalIndex;
}
}
}
@@ -495,7 +471,7 @@ public sealed class RenderGraph
{
var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore,
handle,
id,
passIdx
);
_barriers.Add(barrier);
@@ -516,42 +492,65 @@ public sealed class RenderGraph
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
{
// Process reads (transition to shader resource)
for (var i = 0; i < pass.TextureReads.Count; i++)
for (var i = 0; i < pass.resourceReads.Count; i++)
{
var handle = pass.TextureReads[i];
var handle = pass.resourceReads[i];
InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx);
}
// Process writes (transition to render target or UAV)
for (var i = 0; i < pass.TextureWrites.Count; i++)
switch (pass.type)
{
var handle = pass.TextureWrites[i];
var targetState = ResourceState.RenderTarget; // Could be UAV for compute
InsertTransitionIfNeeded(handle, targetState, passIdx);
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(RenderGraphTextureHandle handle, ResourceState newState, int passIdx)
private void InsertTransitionIfNeeded(Identifier<RGResource> resource, ResourceState newState, int passIdx)
{
if (!_resourceStates.TryGetValue(handle.Index, out var currentState))
if (!_resourceStates.TryGetValue(resource.Value, out var currentState))
{
// First time seeing this resource, assume undefined
currentState = ResourceState.Undefined;
currentState = ResourceState.Common;
}
if (currentState != newState)
{
var barrier = ResourceBarrier.CreateTransitionBarrier(
handle,
resource,
currentState,
newState,
passIdx
);
_barriers.Add(barrier);
_resourceStates[handle.Index] = newState;
_resourceStates[resource.Value] = newState;
#if DEBUG
Console.WriteLine($" {barrier}");
@@ -570,8 +569,8 @@ public sealed class RenderGraph
}
// Execute each non-culled pass
int barrierIndex = 0;
for (int i = 0; i < _compiledPasses.Count; i++)
var barrierIndex = 0;
for (var i = 0; i < _compiledPasses.Count; i++)
{
var pass = _compiledPasses[i];
@@ -584,10 +583,19 @@ public sealed class RenderGraph
#if DEBUG
if (!hasBarriers)
{
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.Name} ===");
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
hasBarriers = true;
}
Console.WriteLine($" {_barriers[barrierIndex]}");
var barrier = _barriers[barrierIndex];
if (barrier.Type == BarrierType.Transition)
{
_commandBuffer.ResourceBarrier(
barrier.Resource,
barrier.StateBefore,
barrier.StateAfter
);
}
#endif
// In a real implementation, you would execute the barrier here:
// ExecuteBarrier(_barriers[barrierIndex]);

View File

@@ -104,7 +104,7 @@ internal sealed class ResourceAliasingManager
#endif
// Build list of all logical resources with their lifetimes
var logicalResources = ListPool<(int index, TextureResource resource)>.Rent();
var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent();
for (int i = 0; i < registry.TextureResourceCount; i++)
{
@@ -194,7 +194,7 @@ internal sealed class ResourceAliasingManager
Console.WriteLine("================================\n");
#endif
ListPool<(int index, TextureResource resource)>.Return(logicalResources);
ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources);
}
public int GetPhysicalResourceIndex(int logicalIndex)
@@ -209,7 +209,7 @@ internal sealed class ResourceAliasingManager
: null;
}
private bool HasLifetimeOverlap(PhysicalResource physical, TextureResource logical)
private bool HasLifetimeOverlap(PhysicalResource physical, RenderGraphResource logical)
{
// Check if the lifetimes overlap
// No overlap if: logical.First > physical.Last OR logical.Last < physical.First
@@ -227,7 +227,7 @@ internal sealed class ResourceAliasingManager
}
else
{
resource = _pool.Get<PhysicalResource>();
resource = _pool.Rent<PhysicalResource>();
resource.Reset();
_physicalResources.Add(resource);
}
@@ -254,7 +254,7 @@ internal sealed class ResourceAliasingManager
{
for (int i = 0; i < _physicalResources.Count; i++)
{
_pool.Release(_physicalResources[i]);
_pool.Return(_physicalResources[i]);
}
_physicalResources.Clear();
_physicalResourceCount = 0;
@@ -284,7 +284,7 @@ internal sealed class ResourceAliasingManager
}
else
{
physical = _pool.Get<PhysicalResource>();
physical = _pool.Rent<PhysicalResource>();
physical.Reset();
_physicalResources.Add(physical);
}

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
@@ -9,7 +10,7 @@ namespace Ghost.RenderGraph.Concept;
[Flags]
public enum ResourceState
{
Undefined = 0,
Common = 0,
RenderTarget = 1 << 0,
DepthWrite = 1 << 1,
DepthRead = 1 << 2,
@@ -27,7 +28,6 @@ public enum BarrierType
{
Transition, // State transition (e.g., RenderTarget -> ShaderResource)
Aliasing, // Aliasing barrier (new resource reusing memory)
UAV, // UAV barrier (synchronize UAV access)
}
/// <summary>
@@ -41,71 +41,69 @@ internal struct ResourceBarrier
{
internal struct barrier_union_transition
{
public RenderGraphTextureHandle Resource;
public ResourceState StateBefore;
public ResourceState StateAfter;
public Identifier<RGResource> resource;
public ResourceState stateBefore;
public ResourceState stateAfter;
}
internal struct barrier_union_aliasing
{
public RenderGraphTextureHandle ResourceBefore;
public RenderGraphTextureHandle ResourceAfter;
public Identifier<RGResource> resourceBefore;
public Identifier<RGResource> resourceAfter;
}
// TODO: union can not have non-blittable types
[FieldOffset(0)]
public barrier_union_transition Transition;
public barrier_union_transition transition;
[FieldOffset(0)]
public barrier_union_aliasing Aliasing;
public barrier_union_aliasing aliasing;
}
public BarrierType Type;
private barrier_union _union;
public BarrierType Type
{
get; init;
}
public int PassIndex
{
get; init;
}
// For Transition and UAV barriers
public RenderGraphTextureHandle Resource;
public ResourceState StateBefore;
public ResourceState StateAfter;
public readonly Identifier<RGResource> Resource => _union.transition.resource;
public readonly ResourceState StateBefore => _union.transition.stateBefore;
public readonly ResourceState StateAfter => _union.transition.stateAfter;
// For Aliasing barriers (D3D12_RESOURCE_BARRIER::Aliasing)
public RenderGraphTextureHandle ResourceBefore; // pResourceBefore
public RenderGraphTextureHandle ResourceAfter; // pResourceAfter
public int PassIndex;
// Constructor for Transition and UAV barriers
public ResourceBarrier(BarrierType type, RenderGraphTextureHandle resource,
ResourceState before, ResourceState after, int passIndex)
{
Type = type;
Resource = resource;
StateBefore = before;
StateAfter = after;
ResourceBefore = default;
ResourceAfter = default;
PassIndex = passIndex;
}
public readonly Identifier<RGResource> ResourceBefore => _union.aliasing.resourceBefore;
public readonly Identifier<RGResource> ResourceAfter => _union.aliasing.resourceAfter;
// Constructor for Aliasing barriers
public static ResourceBarrier CreateAliasingBarrier(
RenderGraphTextureHandle resourceBefore,
RenderGraphTextureHandle resourceAfter,
Identifier<RGResource> resourceBefore,
Identifier<RGResource> resourceAfter,
int passIndex)
{
return new ResourceBarrier
{
Type = BarrierType.Aliasing,
ResourceBefore = resourceBefore,
ResourceAfter = resourceAfter,
PassIndex = passIndex,
Resource = default,
StateBefore = ResourceState.Undefined,
StateAfter = ResourceState.Undefined
_union = new barrier_union
{
aliasing = new barrier_union.barrier_union_aliasing
{
resourceBefore = resourceBefore,
resourceAfter = resourceAfter
}
}
};
}
public static ResourceBarrier CreateTransitionBarrier(
RenderGraphTextureHandle resource,
Identifier<RGResource> resource,
ResourceState before,
ResourceState after,
int passIndex)
@@ -113,27 +111,18 @@ internal struct ResourceBarrier
return new ResourceBarrier
{
Type = BarrierType.Transition,
Resource = resource,
StateBefore = before,
StateAfter = after,
PassIndex = passIndex,
ResourceBefore = default,
ResourceAfter = default
_union = new barrier_union
{
transition = new barrier_union.barrier_union_transition
{
resource = resource,
stateBefore = before,
stateAfter = after
}
}
};
}
#if DEBUG
public override readonly string ToString()
{
return Type switch
{
BarrierType.Transition => $"[Pass {PassIndex}] TRANSITION: {Resource.Name} ({StateBefore} -> {StateAfter})",
BarrierType.Aliasing => $"[Pass {PassIndex}] ALIASING: {ResourceBefore.Name} -> {ResourceAfter.Name} (reusing physical memory)",
BarrierType.UAV => $"[Pass {PassIndex}] UAV: {Resource.Name}",
_ => $"[Pass {PassIndex}] UNKNOWN BARRIER"
};
}
#endif
}
/// <summary>
@@ -142,13 +131,13 @@ internal struct ResourceBarrier
internal sealed class ResourceStateTracker
{
public int ResourceIndex;
public ResourceState CurrentState = ResourceState.Undefined;
public ResourceState CurrentState = ResourceState.Common;
public int LastAccessPass = -1;
public void Reset()
{
ResourceIndex = -1;
CurrentState = ResourceState.Undefined;
CurrentState = ResourceState.Common;
LastAccessPass = -1;
}
}

View File

@@ -7,12 +7,13 @@ namespace Ghost.RenderGraph.Concept;
/// </summary>
public sealed class RenderGraphBlackboard
{
private readonly Dictionary<Type, object> _data = new(16);
private readonly Dictionary<Type, IPassData> _data = new(16);
/// <summary>
/// Adds or updates pass data in the blackboard.
/// </summary>
public void Add<T>(T data) where T : class, IPassData
public void Add<T>(T data)
where T : class, IPassData
{
var type = typeof(T);
_data[type] = data;
@@ -21,20 +22,23 @@ public sealed class RenderGraphBlackboard
/// <summary>
/// Retrieves pass data from the blackboard.
/// </summary>
public T Get<T>() where T : class, IPassData
public T Get<T>()
where T : class, IPassData
{
var type = typeof(T);
if (_data.TryGetValue(type, out var obj))
{
return (T)obj;
}
throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard");
}
/// <summary>
/// Tries to get pass data from the blackboard.
/// </summary>
public bool TryGet<T>(out T? data) where T : class, IPassData
public bool TryGet<T>(out T? data)
where T : class, IPassData
{
var type = typeof(T);
if (_data.TryGetValue(type, out var obj))
@@ -42,6 +46,7 @@ public sealed class RenderGraphBlackboard
data = (T)obj;
return true;
}
data = null;
return false;
}

View File

@@ -0,0 +1,240 @@
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

@@ -1,27 +0,0 @@
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Extension methods to provide a cleaner API for setting render functions.
/// These avoid the need for explicit type annotations in lambdas.
/// </summary>
public static class RenderGraphBuilderExtensions
{
// Internal helper to cast and set
private static void SetRasterFunc<TPassData>(this RenderGraphBuilder builder, object pass, Action<TPassData, RasterRenderContext> func)
where TPassData : class, new()
{
if (pass is RenderGraphPass<TPassData> typedPass)
{
builder.SetRenderFunc(func);
}
}
private static void SetCompFunc<TPassData>(this RenderGraphBuilder builder, object pass, Action<TPassData, ComputeRenderContext> func, bool async)
where TPassData : class, new()
{
if (pass is RenderGraphPass<TPassData> typedPass)
{
builder.SetComputeFunc(func, async);
}
}
}

View File

@@ -1,36 +1,38 @@
using Ghost.Core;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Mock command buffer for recording GPU commands.
/// In a real implementation, this would wrap D3D12 command lists.
/// </summary>
public sealed class MockCommandBuffer
internal sealed class MockCommandBuffer
{
public void SetRenderTarget(string name)
public void SetRenderTarget(Identifier<RGTexture> texture)
{
#if DEBUG
Console.WriteLine(nameof(SetRenderTarget) + ": " + name);
Console.WriteLine(nameof(SetRenderTarget) + ": " + texture);
#endif
}
public void SetDepthStencil(string name)
public void SetDepthStencil(Identifier<RGTexture> texture)
{
#if DEBUG
Console.WriteLine(nameof(SetDepthStencil) + ": " + name);
Console.WriteLine(nameof(SetDepthStencil) + ": " + texture);
#endif
}
public void ClearRenderTarget(string name, float r, float g, float b, float a)
public void ClearRenderTarget(Identifier<RGTexture> texture, float r, float g, float b, float a)
{
#if DEBUG
Console.WriteLine(nameof(ClearRenderTarget) + ": " + name);
Console.WriteLine(nameof(ClearRenderTarget) + ": " + texture);
#endif
}
public void ClearDepth(string name, float depth)
public void ClearDepth(Identifier<RGTexture> texture, float depth)
{
#if DEBUG
Console.WriteLine(nameof(ClearDepth) + ": " + name);
Console.WriteLine(nameof(ClearDepth) + ": " + texture);
#endif
}
@@ -41,17 +43,17 @@ public sealed class MockCommandBuffer
#endif
}
public void BindShaderResource(string name, int slot)
public void BindShaderResource(Identifier<RGResource> resource, int slot)
{
#if DEBUG
Console.WriteLine(nameof(BindShaderResource) + ": " + name + ", slot " + slot);
Console.WriteLine(nameof(BindShaderResource) + ": " + resource + ", slot " + slot);
#endif
}
public void BindUnorderedAccess(string name, int slot)
public void BindUnorderedAccess(Identifier<RGResource> resource, int slot)
{
#if DEBUG
Console.WriteLine(nameof(BindUnorderedAccess) + ": " + name + ", slot " + slot);
Console.WriteLine(nameof(BindUnorderedAccess) + ": " + resource + ", slot " + slot);
#endif
}
@@ -62,14 +64,14 @@ public sealed class MockCommandBuffer
#endif
}
public void ResourceBarrier(string resourceName, string stateBefore, string stateAfter)
public void ResourceBarrier(Identifier<RGResource> resource, ResourceState stateBefore, ResourceState stateAfter)
{
#if DEBUG
Console.WriteLine(nameof(ResourceBarrier) + ": " + resourceName + " from " + stateBefore + " to " + stateAfter);
Console.WriteLine(nameof(ResourceBarrier) + ": " + resource + " from " + stateBefore + " to " + stateAfter);
#endif
}
public void AliasBarrier(string resourceBefore, string resourceAfter)
public void AliasBarrier(Identifier<RGResource> resourceBefore, Identifier<RGResource> resourceAfter)
{
#if DEBUG
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
@@ -85,18 +87,18 @@ public readonly struct RasterRenderContext
{
private readonly MockCommandBuffer _cmd;
public RasterRenderContext(MockCommandBuffer cmd)
internal RasterRenderContext(MockCommandBuffer cmd)
{
_cmd = cmd;
}
// Expose command buffer methods directly
public void SetRenderTarget(string name) => _cmd.SetRenderTarget(name);
public void SetDepthStencil(string name) => _cmd.SetDepthStencil(name);
public void ClearRenderTarget(string name, float r, float g, float b, float a) => _cmd.ClearRenderTarget(name, r, g, b, a);
public void ClearDepth(string name, float depth) => _cmd.ClearDepth(name, depth);
public void SetRenderTarget(Identifier<RGTexture> texture) => _cmd.SetRenderTarget(texture);
public void SetDepthStencil(Identifier<RGTexture> texture) => _cmd.SetDepthStencil(texture);
public void ClearRenderTarget(Identifier<RGTexture> texture, float r, float g, float b, float a) => _cmd.ClearRenderTarget(texture, r, g, b, a);
public void ClearDepth(Identifier<RGTexture> texture, float depth) => _cmd.ClearDepth(texture, depth);
public void Draw(int vertexCount) => _cmd.Draw(vertexCount);
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot);
public void BindShaderResource(Identifier<RGResource> resource, int slot) => _cmd.BindShaderResource(resource, slot);
}
/// <summary>
@@ -107,14 +109,14 @@ public readonly struct ComputeRenderContext
{
private readonly MockCommandBuffer _cmd;
public ComputeRenderContext(MockCommandBuffer cmd)
internal ComputeRenderContext(MockCommandBuffer cmd)
{
_cmd = cmd;
}
// Expose command buffer methods directly
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot);
public void BindUnorderedAccess(string name, int slot) => _cmd.BindUnorderedAccess(name, slot);
public void BindShaderResource(Identifier<RGResource> resource, int slot) => _cmd.BindShaderResource(resource, slot);
public void BindUnorderedAccess(Identifier<RGResource> resource, int slot) => _cmd.BindUnorderedAccess(resource, slot);
public void Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
}

View File

@@ -1,68 +0,0 @@
using System.IO.Hashing;
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Helper extensions for XxHash3 to hash common types without string allocation.
/// Uses SIMD-optimized hashing via System.IO.Hashing.XxHash3.
/// </summary>
internal static class RenderGraphHashExtensions
{
/// <summary>
/// Appends an int to the hash.
/// </summary>
public static void AppendInt(this XxHash64 hash, int value)
{
ReadOnlySpan<int> span = stackalloc int[1] { value };
hash.Append(MemoryMarshal.AsBytes(span));
}
/// <summary>
/// Appends a bool to the hash.
/// </summary>
public static void AppendBool(this XxHash64 hash, bool value)
{
ReadOnlySpan<bool> span = stackalloc bool[1] { value };
hash.Append(MemoryMarshal.AsBytes(span));
}
/// <summary>
/// Appends an enum to the hash.
/// </summary>
public static void AppendEnum<TEnum>(this XxHash64 hash, TEnum value) where TEnum : unmanaged, Enum
{
ReadOnlySpan<TEnum> span = stackalloc TEnum[1] { value };
hash.Append(MemoryMarshal.AsBytes(span));
}
/// <summary>
/// Appends a struct to the hash (must be unmanaged).
/// </summary>
public static void AppendStruct<T>(this XxHash64 hash, in T value) where T : unmanaged
{
ReadOnlySpan<T> span = stackalloc T[1] { value };
hash.Append(MemoryMarshal.AsBytes(span));
}
/// <summary>
/// Appends a list of resource handle indices to the hash.
/// </summary>
public static void AppendHandleList(this XxHash64 hash, List<RenderGraphTextureHandle> handles)
{
// Only hash the indices, not the versions (versions change but structure doesn't)
int count = handles.Count;
hash.AppendInt(count);
//for (int i = 0; i < count; i++)
//{
// hash.AppendInt(handles[i].Index);
//}
Span<int> indices = stackalloc int[count];
for (int i = 0; i < count; i++)
{
indices[i] = handles[i].Index;
}
hash.Append(MemoryMarshal.AsBytes(indices));
}
}

View File

@@ -1,3 +1,6 @@
using Ghost.Core;
using System.IO;
namespace Ghost.RenderGraph.Concept;
/// <summary>
@@ -15,201 +18,112 @@ public enum RenderPassType : byte
/// </summary>
internal abstract class RenderGraphPassBase
{
public string Name = string.Empty;
public int Index;
public RenderPassType Type;
public bool AllowCulling = true;
public bool AsyncCompute;
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<RenderGraphTextureHandle> TextureReads = new(8);
public readonly List<RenderGraphTextureHandle> TextureWrites = new(4);
public readonly List<RenderGraphTextureHandle> TextureCreates = new(4);
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 bool culled;
public bool hasSideEffects;
public abstract void Execute(RenderContext context);
public abstract void Clear();
public abstract bool HasRenderFunc();
public virtual void Reset()
public virtual void Reset(RenderGraphObjectPool pool)
{
Name = string.Empty;
Index = -1;
Type = RenderPassType.Raster;
AllowCulling = true;
AsyncCompute = false;
TextureReads.Clear();
TextureWrites.Clear();
TextureCreates.Clear();
Culled = false;
HasSideEffects = false;
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;
}
}
/// <summary>
/// Typed render pass with user data.
/// </summary>
internal sealed class RenderGraphPass<TPassData> : RenderGraphPassBase
internal abstract class RenderGraphPassT<TPassData, TRenderContext> : RenderGraphPassBase
where TPassData : class, new()
{
public TPassData? PassData;
public Action<TPassData, RasterRenderContext>? RasterRenderFunc;
public Action<TPassData, ComputeRenderContext>? ComputeRenderFunc;
public TPassData passData = null!;
public Action<TPassData, TRenderContext>? renderFunc;
public override void Execute(RenderContext context)
public void Init(int index, TPassData passData, string name, RenderPassType type)
{
if (PassData == null)
return;
this.index = index;
this.passData = passData;
this.name = name;
this.type = type;
}
if (Type == RenderPassType.Raster && RasterRenderFunc != null)
{
RasterRenderFunc(PassData, context.RasterContext);
}
else if (Type == RenderPassType.Compute && ComputeRenderFunc != null)
{
ComputeRenderFunc(PassData, context.ComputeContext);
}
public sealed override bool HasRenderFunc()
{
return renderFunc != null;
}
public override void Clear()
{
PassData = null;
RasterRenderFunc = null;
ComputeRenderFunc = null;
passData = null!;
renderFunc = null;
}
public override void Reset()
public override void Reset(RenderGraphObjectPool pool)
{
base.Reset();
base.Reset(pool);
pool.Return(passData);
Clear();
}
}
/// <summary>
/// Builder for constructing render passes.
/// Implements IDisposable for using() pattern.
/// </summary>
public sealed class RenderGraphBuilder : IDisposable
internal sealed class RasterRenderGraphPass<TPassData> : RenderGraphPassT<TPassData, RasterRenderContext>
where TPassData : class, new()
{
private RenderGraphPassBase? _pass;
private RenderGraphResourceRegistry? _resources;
private bool _disposed;
internal void Initialize(RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
public override void Execute(RenderContext context)
{
_pass = pass;
_resources = resources;
_disposed = false;
renderFunc!(passData, context.RasterContext);
}
/// <summary>
/// Creates a new transient texture that only lives for this pass.
/// </summary>
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
public override void Reset(RenderGraphObjectPool pool)
{
ThrowIfDisposed();
var handle = _resources!.CreateTexture(descriptor);
_pass!.TextureCreates.Add(handle);
_resources.SetProducer(handle, _pass.Index);
return handle;
}
/// <summary>
/// Marks a texture as being read by this pass.
/// </summary>
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
{
ThrowIfDisposed();
_pass!.TextureReads.Add(handle);
_resources!.AddConsumer(handle, _pass.Index);
return handle;
}
/// <summary>
/// Marks a texture as being written by this pass.
/// </summary>
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
{
ThrowIfDisposed();
_pass!.TextureWrites.Add(handle);
_resources!.SetProducer(handle, _pass.Index);
return handle;
}
/// <summary>
/// Sets up a depth buffer for this pass.
/// </summary>
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
{
ThrowIfDisposed();
if (writeAccess)
{
_pass!.TextureWrites.Add(handle);
_resources!.SetProducer(handle, _pass.Index);
}
else
{
_pass!.TextureReads.Add(handle);
_resources!.AddConsumer(handle, _pass.Index);
}
return handle;
}
/// <summary>
/// Sets the render function for a raster pass.
/// </summary>
public void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> renderFunc)
where TPassData : class, new()
{
ThrowIfDisposed();
if (_pass is RenderGraphPass<TPassData> typedPass)
{
typedPass.RasterRenderFunc = renderFunc;
typedPass.Type = RenderPassType.Raster;
}
}
/// <summary>
/// Sets the compute function for a compute pass.
/// </summary>
public void SetComputeFunc<TPassData>(Action<TPassData, ComputeRenderContext> computeFunc, bool asyncCompute = false)
where TPassData : class, new()
{
ThrowIfDisposed();
if (_pass is RenderGraphPass<TPassData> typedPass)
{
typedPass.ComputeRenderFunc = computeFunc;
typedPass.Type = RenderPassType.Compute;
typedPass.AsyncCompute = asyncCompute;
}
}
/// <summary>
/// Controls whether this pass can be culled if its outputs are unused.
/// </summary>
public void SetAllowCulling(bool allow)
{
ThrowIfDisposed();
_pass!.AllowCulling = allow;
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_pass = null;
_resources = null;
}
}
private void ThrowIfDisposed()
{
if (_disposed || _pass == null)
throw new ObjectDisposedException(nameof(RenderGraphBuilder));
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

@@ -1,3 +1,6 @@
using Ghost.Core;
using Misaki.HighPerformance.Buffer;
namespace Ghost.RenderGraph.Concept;
/// <summary>
@@ -6,44 +9,76 @@ namespace Ghost.RenderGraph.Concept;
/// </summary>
internal sealed class RenderGraphObjectPool
{
private readonly Dictionary<Type, Stack<object>> _pools = new();
private static readonly List<SharedObjectPoolBase> s_allocatedPools = new();
public T Get<T>() where T : class, new()
private class SharedObjectPoolBase
{
var type = typeof(T);
if (_pools.TryGetValue(type, out var pool) && pool.Count > 0)
{
return (T)pool.Pop();
}
return new T();
public SharedObjectPoolBase() { }
public virtual void Clear() { }
}
public void Release<T>(T obj) where T : class
private class SharedObjectPool<T> : SharedObjectPoolBase where T : class, new()
{
if (obj == null) return;
private static readonly ObjectPool<T> s_pool = AllocatePool();
var type = typeof(T);
if (!_pools.TryGetValue(type, out var pool))
private static ObjectPool<T> AllocatePool()
{
pool = new Stack<object>(16);
_pools[type] = pool;
var newPool = new ObjectPool<T>(() => new T());
// Storing instance to clear the static pool of the same type if needed
s_allocatedPools.Add(new SharedObjectPool<T>());
return newPool;
}
pool.Push(obj);
/// <summary>
/// Clear the pool using SharedObjectPool instance.
/// </summary>
/// <returns></returns>
public override void Clear()
{
s_pool.Reset();
}
/// <summary>
/// Rent a new instance from the pool.
/// </summary>
/// <returns></returns>
public static T Rent() => s_pool.Rent();
/// <summary>
/// Return an object to the pool.
/// </summary>
/// <param name="toRelease">instance to release.</param>
public static void Return(T toRelease) => s_pool.Return(toRelease);
}
public T Rent<T>()
where T : class, new()
{
return SharedObjectPool<T>.Rent();
}
public void Return<T>(T obj)
where T : class, new()
{
SharedObjectPool<T>.Return(obj);
}
public void Clear()
{
_pools.Clear();
for (var i = 0; i < s_allocatedPools.Count; i++)
{
s_allocatedPools[i].Clear();
}
}
}
/// <summary>
/// Represents a texture resource in the render graph.
/// </summary>
internal sealed class TextureResource
internal sealed class RenderGraphResource
{
public RenderGraphResourceType type;
public int Index;
public int Version;
public TextureDescriptor Descriptor;
public bool IsImported;
public int FirstUsePass = -1;
@@ -55,7 +90,6 @@ internal sealed class TextureResource
public void Reset()
{
Index = -1;
Version = 0;
Descriptor = default;
IsImported = false;
FirstUsePass = -1;
@@ -72,7 +106,7 @@ internal sealed class TextureResource
/// </summary>
internal sealed class RenderGraphResourceRegistry
{
private readonly List<TextureResource> _textureResources = new(64);
private readonly List<RenderGraphResource> _resources = new(64);
private readonly RenderGraphObjectPool _pool = new();
private int _textureResourceCount;
@@ -85,76 +119,78 @@ internal sealed class RenderGraphResourceRegistry
_textureResourceCount = 0;
}
public RenderGraphTextureHandle ImportTexture(TextureDescriptor descriptor)
public Identifier<RGTexture> ImportTexture(TextureDescriptor descriptor)
{
var resource = GetOrCreateTextureResource();
resource.Index = _textureResourceCount - 1;
resource.Version = 0;
resource.Descriptor = descriptor;
resource.IsImported = true;
return new RenderGraphTextureHandle(resource.Index, resource.Version, descriptor.Name);
return new Identifier<RGTexture>(resource.Index);
}
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
public Identifier<RGTexture> CreateTexture(TextureDescriptor descriptor)
{
var resource = GetOrCreateTextureResource();
resource.Index = _textureResourceCount - 1;
resource.Version = 0;
resource.Descriptor = descriptor;
resource.IsImported = false;
return new RenderGraphTextureHandle(resource.Index, resource.Version, descriptor.Name);
return new Identifier<RGTexture>(resource.Index);
}
public TextureResource GetTextureResource(RenderGraphTextureHandle handle)
public RenderGraphResource GetResource(Identifier<RGResource> resource)
{
if (handle.Index < 0 || handle.Index >= _textureResourceCount)
throw new ArgumentException($"Invalid texture handle: {handle.Index}");
if (resource.Value < 0 || resource.Value >= _textureResourceCount)
throw new ArgumentException($"Invalid texture handle: {resource}");
return _textureResources[handle.Index];
return _resources[resource.Value];
}
public TextureResource GetTextureResourceByIndex(int index)
public RenderGraphResource GetTextureResourceByIndex(int index)
{
if (index < 0 || index >= _textureResourceCount)
throw new ArgumentException($"Invalid texture index: {index}");
return _textureResources[index];
return _resources[index];
}
public void SetProducer(RenderGraphTextureHandle handle, int passIndex)
public void SetProducer(Identifier<RGResource> resourceID, int passIndex)
{
var resource = GetTextureResource(handle);
var resource = GetResource(resourceID);
resource.ProducerPass = passIndex;
if (resource.FirstUsePass < 0)
{
resource.FirstUsePass = passIndex;
}
}
public void AddConsumer(RenderGraphTextureHandle handle, int passIndex)
public void AddConsumer(Identifier<RGResource> resourceID, int passIndex)
{
var resource = GetTextureResource(handle);
var resource = GetResource(resourceID);
resource.ConsumerPasses.Add(passIndex);
resource.LastUsePass = passIndex;
if (resource.FirstUsePass < 0)
{
resource.FirstUsePass = passIndex;
}
}
private TextureResource GetOrCreateTextureResource()
private RenderGraphResource GetOrCreateTextureResource()
{
TextureResource resource;
if (_textureResourceCount < _textureResources.Count)
RenderGraphResource resource;
if (_textureResourceCount < _resources.Count)
{
// Reuse existing slot
resource = _textureResources[_textureResourceCount];
resource = _resources[_textureResourceCount];
resource.Reset();
}
else
{
// Need to grow the list
resource = _pool.Get<TextureResource>();
resource = _pool.Rent<RenderGraphResource>();
resource.Reset();
_textureResources.Add(resource);
_resources.Add(resource);
}
_textureResourceCount++;
@@ -163,11 +199,11 @@ internal sealed class RenderGraphResourceRegistry
public void Clear()
{
for (int i = 0; i < _textureResources.Count; i++)
for (var i = 0; i < _resources.Count; i++)
{
_pool.Release(_textureResources[i]);
_pool.Return(_resources[i]);
}
_textureResources.Clear();
_resources.Clear();
_textureResourceCount = 0;
}
}

View File

@@ -1,30 +1,57 @@
using Ghost.Core;
using System.Runtime.CompilerServices;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Opaque handle to a render graph texture resource.
/// </summary>
public readonly struct RenderGraphTextureHandle : IEquatable<RenderGraphTextureHandle>
internal enum RenderGraphResourceType
{
public readonly int Index;
public readonly int Version;
internal readonly string InternalName;
Texture,
Buffer,
AccelerationStructure,
Count
}
public RenderGraphTextureHandle(int index, int version, string name = "")
public struct RGResource;
public struct RGTexture;
public struct RGBuffer;
public static class RGResourceExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Identifier<RGResource> AsResource(this Identifier<RGTexture> texture)
{
Index = index;
Version = version;
InternalName = name;
return new Identifier<RGResource>(texture.Value);
}
public string Name => InternalName;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Identifier<RGResource> AsResource(this Identifier<RGBuffer> buffer)
{
return new Identifier<RGResource>(buffer.Value);
}
public bool IsValid() => Index >= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Identifier<RGTexture> AsTexture(this Identifier<RGResource> resource)
{
return new Identifier<RGTexture>(resource.Value);
}
public readonly bool Equals(RenderGraphTextureHandle other) => Index == other.Index && Version == other.Version;
public override readonly bool Equals(object? obj) => obj is RenderGraphTextureHandle other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(Index, Version);
public static bool operator ==(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => left.Equals(right);
public static bool operator !=(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => !left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Identifier<RGBuffer> AsBuffer(this Identifier<RGResource> resource)
{
return new Identifier<RGBuffer>(resource.Value);
}
}
internal readonly struct TextureAccess
{
public readonly Identifier<RGTexture> id;
public readonly AccessFlags accessFlags;
public TextureAccess(Identifier<RGTexture> id, AccessFlags accessFlags)
{
this.id = id;
this.accessFlags = accessFlags;
}
}
/// <summary>