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> <DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,3 +1,5 @@
using Ghost.Core;
namespace Ghost.RenderGraph.Concept; namespace Ghost.RenderGraph.Concept;
// ===== Pass Data Structures ===== // ===== Pass Data Structures =====
@@ -5,44 +7,44 @@ namespace Ghost.RenderGraph.Concept;
public sealed class GBufferData : IPassData public sealed class GBufferData : IPassData
{ {
public RenderGraphTextureHandle Albedo; public Identifier<RGTexture> Albedo;
public RenderGraphTextureHandle Normal; public Identifier<RGTexture> Normal;
public RenderGraphTextureHandle Depth; public Identifier<RGTexture> Depth;
} }
public sealed class LightingPassData : IPassData public sealed class LightingPassData : IPassData
{ {
public RenderGraphTextureHandle GBufferAlbedo; public Identifier<RGTexture> GBufferAlbedo;
public RenderGraphTextureHandle GBufferNormal; public Identifier<RGTexture> GBufferNormal;
public RenderGraphTextureHandle GBufferDepth; public Identifier<RGTexture> GBufferDepth;
public RenderGraphTextureHandle OutputLighting; public Identifier<RGTexture> OutputLighting;
} }
public sealed class SSAOPassData : IPassData public sealed class SSAOPassData : IPassData
{ {
public RenderGraphTextureHandle GBufferDepth; public Identifier<RGTexture> GBufferDepth;
public RenderGraphTextureHandle GBufferNormal; public Identifier<RGTexture> GBufferNormal;
public RenderGraphTextureHandle OutputSSAO; public Identifier<RGTexture> OutputSSAO;
} }
public sealed class BloomDownsampleData : IPassData public sealed class BloomDownsampleData : IPassData
{ {
public RenderGraphTextureHandle Input; public Identifier<RGTexture> Input;
public RenderGraphTextureHandle Output; public Identifier<RGTexture> Output;
} }
public sealed class TAAPassData : IPassData public sealed class TAAPassData : IPassData
{ {
public RenderGraphTextureHandle InputLighting; public Identifier<RGTexture> InputLighting;
public RenderGraphTextureHandle OutputTAA; public Identifier<RGTexture> OutputTAA;
} }
public sealed class PostProcessingPassDataV2 : IPassData public sealed class PostProcessingPassDataV2 : IPassData
{ {
public RenderGraphTextureHandle InputTAA; public Identifier<RGTexture> InputTAA;
public RenderGraphTextureHandle InputSSAO; public Identifier<RGTexture> InputSSAO;
public RenderGraphTextureHandle InputBloom; public Identifier<RGTexture> InputBloom;
public RenderGraphTextureHandle OutputBackbuffer; public Identifier<RGTexture> OutputBackbuffer;
} }
public sealed class ProfilerMarkerData : IPassData public sealed class ProfilerMarkerData : IPassData
@@ -51,5 +53,5 @@ public sealed class ProfilerMarkerData : IPassData
public sealed class DebugPassData : 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;
using Ghost.RenderGraph.Concept.Benchmark;
var renderGraph = new RenderGraph(); var renderGraph = new RenderGraph();
#if !DEBUG #if !DEBUG
const int _ITERATION = 500;
for (var i = 0; i < _ITERATION; i++)
{
ExecuteGraph(renderGraph);
}
GC.Collect(); BenchmarkDotNet.Running.BenchmarkRunner.Run<RenderGraphBenchmark>();
GC.WaitForPendingFinalizers(); return;
// Thread.Sleep(1000); // Leave a gap in visual studio allocations timeline
var sw = new System.Diagnostics.Stopwatch();
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
sw.Start();
for (var i = 0; i < _ITERATION; i++) //const int _ITERATION = 500000;
{ //for (var i = 0; i < _ITERATION; i++)
ExecuteGraph(renderGraph); //{
} // RenderGraphBenchmark.ExecuteGraph(renderGraph);
//}
sw.Stop(); //GC.Collect();
var gcAfter = GC.GetAllocatedBytesForCurrentThread(); //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)"); //for (var i = 0; i < _ITERATION; i++)
Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / _ITERATION} bytes (per iteration)"); //{
// 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 #else
// Run twice to demonstrate cache hit // Run twice to demonstrate cache hit
Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ==="); Console.WriteLine("=== FRAME 1 (Cache Miss Expected) ===");
ExecuteGraph(renderGraph); RenderGraphBenchmark.ExecuteGraph(renderGraph);
Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ==="); Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
ExecuteGraph(renderGraph); RenderGraphBenchmark.ExecuteGraph(renderGraph);
#endif #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.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing; using System.IO.Hashing;
using System.Threading; using TerraFX.Interop.Windows;
namespace Ghost.RenderGraph.Concept; namespace Ghost.RenderGraph.Concept;
@@ -28,9 +29,6 @@ public sealed class RenderGraph
private readonly List<ResourceBarrier> _barriers = new(128); private readonly List<ResourceBarrier> _barriers = new(128);
private readonly RenderGraphCompilationCache _compilationCache = new(); private readonly RenderGraphCompilationCache _compilationCache = new();
private readonly XxHash64 _hasher = new();
private int _passCount;
private bool _compiled; private bool _compiled;
public RenderGraphBlackboard Blackboard { get; } = new(); public RenderGraphBlackboard Blackboard { get; } = new();
@@ -60,13 +58,13 @@ public sealed class RenderGraph
_barriers.Clear(); _barriers.Clear();
// Return passes to the pool and reset count // 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]; var pass = _passes[i];
pass.Clear(); pass.Reset(_objectPool);
_objectPool.Release(pass);
} }
_passCount = 0;
_passes.Clear();
// Clear compiled passes list // Clear compiled passes list
_compiledPasses.Clear(); _compiledPasses.Clear();
@@ -76,57 +74,34 @@ public sealed class RenderGraph
/// <summary> /// <summary>
/// Imports an external texture into the render graph. /// Imports an external texture into the render graph.
/// </summary> /// </summary>
public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor) public Identifier<RGTexture> ImportTexture(TextureDescriptor descriptor)
{ {
return _resources.ImportTexture(descriptor); return _resources.ImportTexture(descriptor);
} }
/// <summary> public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)
/// Adds a new render pass to the graph.
/// </summary>
public RenderGraphBuilder AddRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new() where TPassData : class, new()
{ {
// Get or create pass from pool var renderPass = _objectPool.Rent<RasterRenderGraphPass<TPassData>>();
RenderGraphPass<TPassData> pass; renderPass.Init(_passes.Count, _objectPool.Rent<TPassData>(), name, RenderPassType.Raster);
if (_passCount < _passes.Count) passData = renderPass.passData;
{
// 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);
}
// Initialize pass _passes.Add(renderPass);
pass.Name = name;
pass.Index = _passCount;
// Get or create pass data from pool _builder.Init(this, renderPass, _resources);
passData = _objectPool.Get<TPassData>(); return _builder;
pass.PassData = passData; }
_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 _passes.Add(renderPass);
_builder.Initialize(pass, _resources);
_builder.Init(this, renderPass, _resources);
return _builder; return _builder;
} }
@@ -138,82 +113,84 @@ public sealed class RenderGraph
private unsafe ulong ComputeGraphHash() private unsafe ulong ComputeGraphHash()
{ {
using var scope = AllocationManager.CreateStackScope(); using var scope = AllocationManager.CreateStackScope();
var bufferPool = new UnsafeList<byte>(4096, scope.AllocationHandle); var bufferPool = new UnsafeList<byte>(2048, scope.AllocationHandle);
int offset = 0;
var pData = (byte*)bufferPool.GetUnsafePtr(); var pData = (byte*)bufferPool.GetUnsafePtr();
var offset = 0;
_hasher.Reset();
// Hash pass count // Hash pass count
_hasher.AppendInt(_passCount); *(int*)(pData + offset) = _passes.Count;
offset += sizeof(int);
// Hash each pass structure (excluding names) // 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]; var pass = _passes[i];
// Save 0.004ms.
//// Hash pass properties that affect compilation *(RenderPassType*)(pData + offset) = pass.type;
//_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;
offset += sizeof(RenderPassType); offset += sizeof(RenderPassType);
*(bool*)(pData + offset) = pass.AllowCulling; *(bool*)(pData + offset) = pass.allowCulling;
offset += sizeof(bool); offset += sizeof(bool);
*(bool*)(pData + offset) = pass.AsyncCompute; *(bool*)(pData + offset) = pass.asyncCompute;
offset += sizeof(bool); 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); 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); offset += sizeof(int);
} }
*(int*)(pData + offset) = pass.TextureWrites.Count; *(int*)(pData + offset) = pass.resourceWrites.Count;
offset += sizeof(int); 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); offset += sizeof(int);
} }
*(int*)(pData + offset) = pass.TextureCreates.Count; *(int*)(pData + offset) = pass.resourceCreates.Count;
offset += sizeof(int); 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); offset += sizeof(int);
} }
} }
// Hash resource descriptors // Hash resource descriptors
for (int i = 0; i < _resources.TextureResourceCount; i++) for (var i = 0; i < _resources.TextureResourceCount; i++)
{ {
var resource = _resources.GetTextureResourceByIndex(i); var resource = _resources.GetTextureResourceByIndex(i);
*(int*)(pData + offset) = resource.Descriptor.Width; *(int*)(pData + offset) = resource.Descriptor.Width;
offset += sizeof(int); offset += sizeof(int);
*(int*)(pData + offset) = resource.Descriptor.Height; *(int*)(pData + offset) = resource.Descriptor.Height;
offset += sizeof(int); offset += sizeof(int);
*(TextureFormat*)(pData + offset) = resource.Descriptor.Format; *(TextureFormat*)(pData + offset) = resource.Descriptor.Format;
offset += sizeof(TextureFormat); offset += sizeof(TextureFormat);
*(bool*)(pData + offset) = resource.IsImported; *(bool*)(pData + offset) = resource.IsImported;
offset += sizeof(bool); offset += sizeof(bool);
} }
var span = new Span<byte>(pData, offset); var span = new Span<byte>(pData, offset);
_hasher.Append(span); return XxHash64.HashToUInt64(span);
return _hasher.GetCurrentHashAsUInt64();
} }
/// <summary> /// <summary>
@@ -222,14 +199,16 @@ public sealed class RenderGraph
public void Compile() public void Compile()
{ {
if (_compiled) if (_compiled)
{
return; return;
}
#if DEBUG #if DEBUG
var sw = System.Diagnostics.Stopwatch.StartNew(); var sw = System.Diagnostics.Stopwatch.StartNew();
#endif #endif
// Step 0: Check cache // Step 0: Check cache
ulong graphHash = ComputeGraphHash(); var graphHash = ComputeGraphHash(); // 1321433047288519964
#if DEBUG #if DEBUG
var hashTime = sw.Elapsed.TotalMicroseconds; var hashTime = sw.Elapsed.TotalMicroseconds;
@@ -257,18 +236,18 @@ public sealed class RenderGraph
_compiledPasses.Clear(); _compiledPasses.Clear();
// Step 1: Mark passes with side effects (writes to imported resources) // 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]; var pass = _passes[i];
// Check if this pass writes to any imported textures // 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 writeHandle = pass.resourceWrites[j];
var resource = _resources.GetTextureResource(writeHandle); var resource = _resources.GetResource(writeHandle);
if (resource.IsImported) if (resource.IsImported)
{ {
pass.HasSideEffects = true; pass.hasSideEffects = true;
break; break;
} }
} }
@@ -276,33 +255,33 @@ public sealed class RenderGraph
// Step 2: Cull passes based on dependency analysis // Step 2: Cull passes based on dependency analysis
// Mark all passes as culled initially // 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 // 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]; var pass = _passes[i];
if (!pass.Culled) if (!pass.culled)
{ {
UnculDependencies(pass); UnculDependencies(pass);
} }
} }
// Step 4: Build final pass list (only non-culled passes) // 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]; var pass = _passes[i];
if (!pass.Culled) if (!pass.culled)
{ {
_compiledPasses.Add(pass); _compiledPasses.Add(pass);
} }
} }
// Step 5: Perform resource aliasing to minimize memory usage // 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 // Step 6: Generate barriers for state transitions and aliasing
GenerateBarriers(); GenerateBarriers();
@@ -320,16 +299,16 @@ public sealed class RenderGraph
{ {
// Restore compiled pass list // Restore compiled pass list
_compiledPasses.Clear(); _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]); _compiledPasses.Add(_passes[passIndex]);
} }
// Restore culling flags // 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) // Restore aliasing mappings (need to update ResourceAliasingManager)
@@ -337,7 +316,7 @@ public sealed class RenderGraph
// Restore barriers (deep copy to avoid shared references) // Restore barriers (deep copy to avoid shared references)
_barriers.Clear(); _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]); _barriers.Add(cached.Barriers[i]);
} }
@@ -358,22 +337,22 @@ public sealed class RenderGraph
var cacheData = new CachedCompilation(); var cacheData = new CachedCompilation();
// Store compiled pass indices // 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 // 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 // Store aliasing mappings
_aliasingManager.StoreToCache(cacheData.LogicalToPhysical, cacheData.PhysicalResources); _aliasingManager.StoreToCache(cacheData.LogicalToPhysical, cacheData.PhysicalResources);
// Store barriers // Store barriers
for (int i = 0; i < _barriers.Count; i++) for (var i = 0; i < _barriers.Count; i++)
{ {
cacheData.Barriers.Add(_barriers[i]); cacheData.Barriers.Add(_barriers[i]);
} }
@@ -393,17 +372,17 @@ public sealed class RenderGraph
private void UnculDependencies(RenderGraphPassBase pass) private void UnculDependencies(RenderGraphPassBase pass)
{ {
// Un-cull all producers of textures we read // 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 readHandle = pass.resourceReads[i];
var resource = _resources.GetTextureResource(readHandle); var resource = _resources.GetResource(readHandle);
if (resource.ProducerPass >= 0) if (resource.ProducerPass >= 0)
{ {
var producer = _passes[resource.ProducerPass]; var producer = _passes[resource.ProducerPass];
if (producer.Culled) if (producer.culled)
{ {
producer.Culled = false; producer.culled = false;
UnculDependencies(producer); UnculDependencies(producer);
} }
} }
@@ -446,20 +425,20 @@ public sealed class RenderGraph
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
for (int i = 0; i < pass.TextureWrites.Count; i++) for (var i = 0; i < pass.resourceWrites.Count; i++)
{ {
var handle = pass.TextureWrites[i]; var id = pass.resourceWrites[i];
var resource = _resources.GetTextureResource(handle); var resource = _resources.GetResource(id);
// Skip imported resources // Skip imported resources
if (resource.IsImported) if (resource.IsImported)
continue; continue;
// Check if this is the first use of this logical resource // 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 // Rent the physical resource
int physicalIndex = _aliasingManager.GetPhysicalResourceIndex(handle.Index); var physicalIndex = _aliasingManager.GetPhysicalResourceIndex(id.Value);
if (physicalIndex >= 0) if (physicalIndex >= 0)
{ {
var physical = _aliasingManager.GetPhysicalResource(physicalIndex); var physical = _aliasingManager.GetPhysicalResource(physicalIndex);
@@ -469,23 +448,20 @@ public sealed class RenderGraph
if (physical != null && physical.AliasedLogicalResources.Count > 1) if (physical != null && physical.AliasedLogicalResources.Count > 1)
{ {
// Find the resource that used this physical memory most recently before this pass // Find the resource that used this physical memory most recently before this pass
RenderGraphTextureHandle resourceBefore = default; Identifier<RGResource> resourceBefore = default;
int mostRecentLastUse = -1; 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); var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts // Check if this resource finished before our resource starts
if (otherResource.LastUsePass < pass.Index && if (otherResource.LastUsePass < pass.index &&
otherResource.LastUsePass > mostRecentLastUse) otherResource.LastUsePass > mostRecentLastUse)
{ {
mostRecentLastUse = otherResource.LastUsePass; mostRecentLastUse = otherResource.LastUsePass;
resourceBefore = new RenderGraphTextureHandle( resourceBefore = otherLogicalIndex;
otherLogicalIndex,
otherResource.Version,
otherResource.Descriptor.Name);
} }
} }
} }
@@ -495,7 +471,7 @@ public sealed class RenderGraph
{ {
var barrier = ResourceBarrier.CreateAliasingBarrier( var barrier = ResourceBarrier.CreateAliasingBarrier(
resourceBefore, resourceBefore,
handle, id,
passIdx passIdx
); );
_barriers.Add(barrier); _barriers.Add(barrier);
@@ -516,42 +492,65 @@ public sealed class RenderGraph
private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx) private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx)
{ {
// Process reads (transition to shader resource) // 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); InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx);
} }
// Process writes (transition to render target or UAV) switch (pass.type)
for (var i = 0; i < pass.TextureWrites.Count; i++)
{ {
var handle = pass.TextureWrites[i]; case RenderPassType.Raster:
var targetState = ResourceState.RenderTarget; // Could be UAV for compute for (var i = 0; i <= pass.maxColorIndex; i++)
InsertTransitionIfNeeded(handle, targetState, passIdx); {
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> /// <summary>
/// Inserts a transition barrier if the resource state changes. /// Inserts a transition barrier if the resource state changes.
/// </summary> /// </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 // First time seeing this resource, assume undefined
currentState = ResourceState.Undefined; currentState = ResourceState.Common;
} }
if (currentState != newState) if (currentState != newState)
{ {
var barrier = ResourceBarrier.CreateTransitionBarrier( var barrier = ResourceBarrier.CreateTransitionBarrier(
handle, resource,
currentState, currentState,
newState, newState,
passIdx passIdx
); );
_barriers.Add(barrier); _barriers.Add(barrier);
_resourceStates[handle.Index] = newState; _resourceStates[resource.Value] = newState;
#if DEBUG #if DEBUG
Console.WriteLine($" {barrier}"); Console.WriteLine($" {barrier}");
@@ -570,8 +569,8 @@ public sealed class RenderGraph
} }
// Execute each non-culled pass // Execute each non-culled pass
int barrierIndex = 0; var barrierIndex = 0;
for (int i = 0; i < _compiledPasses.Count; i++) for (var i = 0; i < _compiledPasses.Count; i++)
{ {
var pass = _compiledPasses[i]; var pass = _compiledPasses[i];
@@ -584,10 +583,19 @@ public sealed class RenderGraph
#if DEBUG #if DEBUG
if (!hasBarriers) if (!hasBarriers)
{ {
Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.Name} ==="); Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.name} ===");
hasBarriers = true; hasBarriers = true;
} }
Console.WriteLine($" {_barriers[barrierIndex]}");
var barrier = _barriers[barrierIndex];
if (barrier.Type == BarrierType.Transition)
{
_commandBuffer.ResourceBarrier(
barrier.Resource,
barrier.StateBefore,
barrier.StateAfter
);
}
#endif #endif
// In a real implementation, you would execute the barrier here: // In a real implementation, you would execute the barrier here:
// ExecuteBarrier(_barriers[barrierIndex]); // ExecuteBarrier(_barriers[barrierIndex]);

View File

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

View File

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

View File

@@ -7,12 +7,13 @@ namespace Ghost.RenderGraph.Concept;
/// </summary> /// </summary>
public sealed class RenderGraphBlackboard public sealed class RenderGraphBlackboard
{ {
private readonly Dictionary<Type, object> _data = new(16); private readonly Dictionary<Type, IPassData> _data = new(16);
/// <summary> /// <summary>
/// Adds or updates pass data in the blackboard. /// Adds or updates pass data in the blackboard.
/// </summary> /// </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); var type = typeof(T);
_data[type] = data; _data[type] = data;
@@ -21,20 +22,23 @@ public sealed class RenderGraphBlackboard
/// <summary> /// <summary>
/// Retrieves pass data from the blackboard. /// Retrieves pass data from the blackboard.
/// </summary> /// </summary>
public T Get<T>() where T : class, IPassData public T Get<T>()
where T : class, IPassData
{ {
var type = typeof(T); var type = typeof(T);
if (_data.TryGetValue(type, out var obj)) if (_data.TryGetValue(type, out var obj))
{ {
return (T)obj; return (T)obj;
} }
throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard"); throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard");
} }
/// <summary> /// <summary>
/// Tries to get pass data from the blackboard. /// Tries to get pass data from the blackboard.
/// </summary> /// </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); var type = typeof(T);
if (_data.TryGetValue(type, out var obj)) if (_data.TryGetValue(type, out var obj))
@@ -42,6 +46,7 @@ public sealed class RenderGraphBlackboard
data = (T)obj; data = (T)obj;
return true; return true;
} }
data = null; data = null;
return false; 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; namespace Ghost.RenderGraph.Concept;
/// <summary> /// <summary>
/// Mock command buffer for recording GPU commands. /// Mock command buffer for recording GPU commands.
/// In a real implementation, this would wrap D3D12 command lists. /// In a real implementation, this would wrap D3D12 command lists.
/// </summary> /// </summary>
public sealed class MockCommandBuffer internal sealed class MockCommandBuffer
{ {
public void SetRenderTarget(string name) public void SetRenderTarget(Identifier<RGTexture> texture)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(SetRenderTarget) + ": " + name); Console.WriteLine(nameof(SetRenderTarget) + ": " + texture);
#endif #endif
} }
public void SetDepthStencil(string name) public void SetDepthStencil(Identifier<RGTexture> texture)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(SetDepthStencil) + ": " + name); Console.WriteLine(nameof(SetDepthStencil) + ": " + texture);
#endif #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 #if DEBUG
Console.WriteLine(nameof(ClearRenderTarget) + ": " + name); Console.WriteLine(nameof(ClearRenderTarget) + ": " + texture);
#endif #endif
} }
public void ClearDepth(string name, float depth) public void ClearDepth(Identifier<RGTexture> texture, float depth)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(ClearDepth) + ": " + name); Console.WriteLine(nameof(ClearDepth) + ": " + texture);
#endif #endif
} }
@@ -41,17 +43,17 @@ public sealed class MockCommandBuffer
#endif #endif
} }
public void BindShaderResource(string name, int slot) public void BindShaderResource(Identifier<RGResource> resource, int slot)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(BindShaderResource) + ": " + name + ", slot " + slot); Console.WriteLine(nameof(BindShaderResource) + ": " + resource + ", slot " + slot);
#endif #endif
} }
public void BindUnorderedAccess(string name, int slot) public void BindUnorderedAccess(Identifier<RGResource> resource, int slot)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(BindUnorderedAccess) + ": " + name + ", slot " + slot); Console.WriteLine(nameof(BindUnorderedAccess) + ": " + resource + ", slot " + slot);
#endif #endif
} }
@@ -62,14 +64,14 @@ public sealed class MockCommandBuffer
#endif #endif
} }
public void ResourceBarrier(string resourceName, string stateBefore, string stateAfter) public void ResourceBarrier(Identifier<RGResource> resource, ResourceState stateBefore, ResourceState stateAfter)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(ResourceBarrier) + ": " + resourceName + " from " + stateBefore + " to " + stateAfter); Console.WriteLine(nameof(ResourceBarrier) + ": " + resource + " from " + stateBefore + " to " + stateAfter);
#endif #endif
} }
public void AliasBarrier(string resourceBefore, string resourceAfter) public void AliasBarrier(Identifier<RGResource> resourceBefore, Identifier<RGResource> resourceAfter)
{ {
#if DEBUG #if DEBUG
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter); Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
@@ -85,18 +87,18 @@ public readonly struct RasterRenderContext
{ {
private readonly MockCommandBuffer _cmd; private readonly MockCommandBuffer _cmd;
public RasterRenderContext(MockCommandBuffer cmd) internal RasterRenderContext(MockCommandBuffer cmd)
{ {
_cmd = cmd; _cmd = cmd;
} }
// Expose command buffer methods directly // Expose command buffer methods directly
public void SetRenderTarget(string name) => _cmd.SetRenderTarget(name); public void SetRenderTarget(Identifier<RGTexture> texture) => _cmd.SetRenderTarget(texture);
public void SetDepthStencil(string name) => _cmd.SetDepthStencil(name); public void SetDepthStencil(Identifier<RGTexture> texture) => _cmd.SetDepthStencil(texture);
public void ClearRenderTarget(string name, float r, float g, float b, float a) => _cmd.ClearRenderTarget(name, r, g, b, a); public void ClearRenderTarget(Identifier<RGTexture> texture, float r, float g, float b, float a) => _cmd.ClearRenderTarget(texture, r, g, b, a);
public void ClearDepth(string name, float depth) => _cmd.ClearDepth(name, depth); public void ClearDepth(Identifier<RGTexture> texture, float depth) => _cmd.ClearDepth(texture, depth);
public void Draw(int vertexCount) => _cmd.Draw(vertexCount); 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> /// <summary>
@@ -107,14 +109,14 @@ public readonly struct ComputeRenderContext
{ {
private readonly MockCommandBuffer _cmd; private readonly MockCommandBuffer _cmd;
public ComputeRenderContext(MockCommandBuffer cmd) internal ComputeRenderContext(MockCommandBuffer cmd)
{ {
_cmd = cmd; _cmd = cmd;
} }
// Expose command buffer methods directly // Expose command buffer methods directly
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot); public void BindShaderResource(Identifier<RGResource> resource, int slot) => _cmd.BindShaderResource(resource, slot);
public void BindUnorderedAccess(string name, int slot) => _cmd.BindUnorderedAccess(name, 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); 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; namespace Ghost.RenderGraph.Concept;
/// <summary> /// <summary>
@@ -15,201 +18,112 @@ public enum RenderPassType : byte
/// </summary> /// </summary>
internal abstract class RenderGraphPassBase internal abstract class RenderGraphPassBase
{ {
public string Name = string.Empty; public string name = string.Empty;
public int Index; public int index;
public RenderPassType Type; public RenderPassType type;
public bool AllowCulling = true; public bool allowCulling = true;
public bool AsyncCompute; 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 // Resource dependencies
public readonly List<RenderGraphTextureHandle> TextureReads = new(8); public readonly List<Identifier<RGResource>> resourceReads = new(8);
public readonly List<RenderGraphTextureHandle> TextureWrites = new(4); public readonly List<Identifier<RGResource>> resourceWrites = new(4);
public readonly List<RenderGraphTextureHandle> TextureCreates = new(4); public readonly List<Identifier<RGResource>> resourceCreates = new(4);
// Execution state // Execution state
public bool Culled; public bool culled;
public bool HasSideEffects; public bool hasSideEffects;
public abstract void Execute(RenderContext context); public abstract void Execute(RenderContext context);
public abstract void Clear(); public abstract void Clear();
public abstract bool HasRenderFunc();
public virtual void Reset() public virtual void Reset(RenderGraphObjectPool pool)
{ {
Name = string.Empty; name = string.Empty;
Index = -1; index = -1;
Type = RenderPassType.Raster; type = RenderPassType.Raster;
AllowCulling = true; allowCulling = true;
AsyncCompute = false; asyncCompute = false;
TextureReads.Clear();
TextureWrites.Clear(); depthAccess = default;
TextureCreates.Clear(); colorAccess.AsSpan().Clear();
Culled = false; maxColorIndex = -1;
HasSideEffects = false;
randomAccess.Clear();
resourceReads.Clear();
resourceWrites.Clear();
resourceCreates.Clear();
culled = false;
hasSideEffects = false;
} }
} }
/// <summary> internal abstract class RenderGraphPassT<TPassData, TRenderContext> : RenderGraphPassBase
/// Typed render pass with user data.
/// </summary>
internal sealed class RenderGraphPass<TPassData> : RenderGraphPassBase
where TPassData : class, new() where TPassData : class, new()
{ {
public TPassData? PassData; public TPassData passData = null!;
public Action<TPassData, RasterRenderContext>? RasterRenderFunc; public Action<TPassData, TRenderContext>? renderFunc;
public Action<TPassData, ComputeRenderContext>? ComputeRenderFunc;
public override void Execute(RenderContext context) public void Init(int index, TPassData passData, string name, RenderPassType type)
{ {
if (PassData == null) this.index = index;
return; this.passData = passData;
this.name = name;
this.type = type;
}
if (Type == RenderPassType.Raster && RasterRenderFunc != null) public sealed override bool HasRenderFunc()
{ {
RasterRenderFunc(PassData, context.RasterContext); return renderFunc != null;
}
else if (Type == RenderPassType.Compute && ComputeRenderFunc != null)
{
ComputeRenderFunc(PassData, context.ComputeContext);
}
} }
public override void Clear() public override void Clear()
{ {
PassData = null; passData = null!;
RasterRenderFunc = null; renderFunc = null;
ComputeRenderFunc = null;
} }
public override void Reset() public override void Reset(RenderGraphObjectPool pool)
{ {
base.Reset(); base.Reset(pool);
pool.Return(passData);
Clear(); Clear();
} }
} }
/// <summary> internal sealed class RasterRenderGraphPass<TPassData> : RenderGraphPassT<TPassData, RasterRenderContext>
/// Builder for constructing render passes. where TPassData : class, new()
/// Implements IDisposable for using() pattern.
/// </summary>
public sealed class RenderGraphBuilder : IDisposable
{ {
private RenderGraphPassBase? _pass; public override void Execute(RenderContext context)
private RenderGraphResourceRegistry? _resources;
private bool _disposed;
internal void Initialize(RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
{ {
_pass = pass; renderFunc!(passData, context.RasterContext);
_resources = resources;
_disposed = false;
} }
/// <summary> public override void Reset(RenderGraphObjectPool pool)
/// Creates a new transient texture that only lives for this pass.
/// </summary>
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
{ {
ThrowIfDisposed(); base.Reset(pool);
var handle = _resources!.CreateTexture(descriptor); pool.Return(this);
_pass!.TextureCreates.Add(handle); }
_resources.SetProducer(handle, _pass.Index); }
return handle;
} internal sealed class ComputeRenderGraphPass<TPassData> : RenderGraphPassT<TPassData, ComputeRenderContext>
where TPassData : class, new()
/// <summary> {
/// Marks a texture as being read by this pass. public override void Execute(RenderContext context)
/// </summary> {
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle) renderFunc!(passData, context.ComputeContext);
{ }
ThrowIfDisposed();
_pass!.TextureReads.Add(handle); public override void Reset(RenderGraphObjectPool pool)
_resources!.AddConsumer(handle, _pass.Index); {
return handle; base.Reset(pool);
} pool.Return(this);
/// <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));
} }
} }

View File

@@ -1,3 +1,6 @@
using Ghost.Core;
using Misaki.HighPerformance.Buffer;
namespace Ghost.RenderGraph.Concept; namespace Ghost.RenderGraph.Concept;
/// <summary> /// <summary>
@@ -6,44 +9,76 @@ namespace Ghost.RenderGraph.Concept;
/// </summary> /// </summary>
internal sealed class RenderGraphObjectPool 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); public SharedObjectPoolBase() { }
if (_pools.TryGetValue(type, out var pool) && pool.Count > 0) public virtual void Clear() { }
{
return (T)pool.Pop();
}
return new T();
} }
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); private static ObjectPool<T> AllocatePool()
if (!_pools.TryGetValue(type, out var pool))
{ {
pool = new Stack<object>(16); var newPool = new ObjectPool<T>(() => new T());
_pools[type] = pool; // 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() public void Clear()
{ {
_pools.Clear(); for (var i = 0; i < s_allocatedPools.Count; i++)
{
s_allocatedPools[i].Clear();
}
} }
} }
/// <summary> /// <summary>
/// Represents a texture resource in the render graph. /// Represents a texture resource in the render graph.
/// </summary> /// </summary>
internal sealed class TextureResource internal sealed class RenderGraphResource
{ {
public RenderGraphResourceType type;
public int Index; public int Index;
public int Version;
public TextureDescriptor Descriptor; public TextureDescriptor Descriptor;
public bool IsImported; public bool IsImported;
public int FirstUsePass = -1; public int FirstUsePass = -1;
@@ -55,7 +90,6 @@ internal sealed class TextureResource
public void Reset() public void Reset()
{ {
Index = -1; Index = -1;
Version = 0;
Descriptor = default; Descriptor = default;
IsImported = false; IsImported = false;
FirstUsePass = -1; FirstUsePass = -1;
@@ -72,7 +106,7 @@ internal sealed class TextureResource
/// </summary> /// </summary>
internal sealed class RenderGraphResourceRegistry internal sealed class RenderGraphResourceRegistry
{ {
private readonly List<TextureResource> _textureResources = new(64); private readonly List<RenderGraphResource> _resources = new(64);
private readonly RenderGraphObjectPool _pool = new(); private readonly RenderGraphObjectPool _pool = new();
private int _textureResourceCount; private int _textureResourceCount;
@@ -85,76 +119,78 @@ internal sealed class RenderGraphResourceRegistry
_textureResourceCount = 0; _textureResourceCount = 0;
} }
public RenderGraphTextureHandle ImportTexture(TextureDescriptor descriptor) public Identifier<RGTexture> ImportTexture(TextureDescriptor descriptor)
{ {
var resource = GetOrCreateTextureResource(); var resource = GetOrCreateTextureResource();
resource.Index = _textureResourceCount - 1; resource.Index = _textureResourceCount - 1;
resource.Version = 0;
resource.Descriptor = descriptor; resource.Descriptor = descriptor;
resource.IsImported = true; 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(); var resource = GetOrCreateTextureResource();
resource.Index = _textureResourceCount - 1; resource.Index = _textureResourceCount - 1;
resource.Version = 0;
resource.Descriptor = descriptor; resource.Descriptor = descriptor;
resource.IsImported = false; 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) if (resource.Value < 0 || resource.Value >= _textureResourceCount)
throw new ArgumentException($"Invalid texture handle: {handle.Index}"); 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) if (index < 0 || index >= _textureResourceCount)
throw new ArgumentException($"Invalid texture index: {index}"); 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; resource.ProducerPass = passIndex;
if (resource.FirstUsePass < 0) if (resource.FirstUsePass < 0)
{
resource.FirstUsePass = passIndex; 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.ConsumerPasses.Add(passIndex);
resource.LastUsePass = passIndex; resource.LastUsePass = passIndex;
if (resource.FirstUsePass < 0) if (resource.FirstUsePass < 0)
{
resource.FirstUsePass = passIndex; resource.FirstUsePass = passIndex;
}
} }
private TextureResource GetOrCreateTextureResource() private RenderGraphResource GetOrCreateTextureResource()
{ {
TextureResource resource; RenderGraphResource resource;
if (_textureResourceCount < _textureResources.Count) if (_textureResourceCount < _resources.Count)
{ {
// Reuse existing slot // Reuse existing slot
resource = _textureResources[_textureResourceCount]; resource = _resources[_textureResourceCount];
resource.Reset(); resource.Reset();
} }
else else
{ {
// Need to grow the list // Need to grow the list
resource = _pool.Get<TextureResource>(); resource = _pool.Rent<RenderGraphResource>();
resource.Reset(); resource.Reset();
_textureResources.Add(resource); _resources.Add(resource);
} }
_textureResourceCount++; _textureResourceCount++;
@@ -163,11 +199,11 @@ internal sealed class RenderGraphResourceRegistry
public void Clear() 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; _textureResourceCount = 0;
} }
} }

View File

@@ -1,30 +1,57 @@
using Ghost.Core;
using System.Runtime.CompilerServices;
namespace Ghost.RenderGraph.Concept; namespace Ghost.RenderGraph.Concept;
/// <summary> internal enum RenderGraphResourceType
/// Opaque handle to a render graph texture resource.
/// </summary>
public readonly struct RenderGraphTextureHandle : IEquatable<RenderGraphTextureHandle>
{ {
public readonly int Index; Texture,
public readonly int Version; Buffer,
internal readonly string InternalName; 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; return new Identifier<RGResource>(texture.Value);
Version = version;
InternalName = name;
} }
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; [MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly bool Equals(object? obj) => obj is RenderGraphTextureHandle other && Equals(other); internal static Identifier<RGBuffer> AsBuffer(this Identifier<RGResource> resource)
public override readonly int GetHashCode() => HashCode.Combine(Index, Version); {
public static bool operator ==(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => left.Equals(right); return new Identifier<RGBuffer>(resource.Value);
public static bool operator !=(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => !left.Equals(right); }
}
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> /// <summary>