GhostEngine Render Graph: major refactor & Unity RG ref

- Major architectural refactor for performance, extensibility, and feature completeness: resource pooling, pass culling, aliasing, and compilation caching.
- Introduces type-safe builder and context APIs, blackboard pattern, and unified resource management.
- Adds detailed documentation and cleans up obsolete files and APIs.
- Includes (commented) Unity Render Graph source for reference; not compiled, for parity and future extension.
This commit is contained in:
2026-01-11 23:43:17 +09:00
parent 87e315a588
commit 1fc9df1812
30 changed files with 7536 additions and 1545 deletions

View File

@@ -1,16 +0,0 @@
namespace Ghost.RenderGraph.Concept;
internal static class ConsoleAPI
{
[System.Diagnostics.Conditional("DEBUG")]
public static void WriteLine()
{
Console.WriteLine();
}
[System.Diagnostics.Conditional("DEBUG")]
public static void WriteLine(string? message)
{
Console.WriteLine(message);
}
}

View File

@@ -5,14 +5,19 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="New\" />
</ItemGroup>
</Project>

View File

@@ -1,168 +0,0 @@
namespace Ghost.RenderGraph.Concept;
public struct ResourceBarrierInfo
{
public string ResourceName;
public ResourceState BeforeState;
public ResourceState AfterState;
public ResourceBarrierInfo(string resourceName, ResourceState beforeState, ResourceState afterState)
{
ResourceName = resourceName;
BeforeState = beforeState;
AfterState = afterState;
}
}
public struct AliasingBarrierInfo
{
public string BeforeResourceName;
public string AfterResourceName;
public string PhysicalAllocationName;
public AliasingBarrierInfo(string beforeResourceName, string afterResourceName, string physicalAllocationName)
{
BeforeResourceName = beforeResourceName;
AfterResourceName = afterResourceName;
PhysicalAllocationName = physicalAllocationName;
}
}
public interface ICommandBuffer
{
void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState);
void ResourceBarrier(Span<ResourceBarrierInfo> barriers);
void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName);
void AliasingBarrier(Span<AliasingBarrierInfo> barriers);
void BeginRenderPass(string passName);
void EndRenderPass();
void SetRenderTarget(string textureName);
void SetDepthStencil(string textureName);
void BindShaderResource(string resourceName, int slot);
void BindUnorderedAccess(string resourceName, int slot);
void Draw(int vertexCount);
void Dispatch(int x, int y, int z);
void ClearRenderTarget(string textureName, float r, float g, float b, float a);
void ClearDepth(string textureName, float depth);
void CopyTexture(string source, string destination);
}
public class SimulatedCommandBuffer : ICommandBuffer
{
public void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState)
{
//ConsoleAPI.WriteLine($" [BARRIER] Transition '{resourceName}' from {beforeState} to {afterState}");
}
public void ResourceBarrier(Span<ResourceBarrierInfo> barriers)
{
if (barriers.Length == 0) return;
//ConsoleAPI.WriteLine($" [BARRIER_BATCH] Processing {barriers.Length} transitions:");
foreach (var barrier in barriers)
{
//ConsoleAPI.WriteLine($" - Transition '{barrier.ResourceName}' from {barrier.BeforeState} to {barrier.AfterState}");
}
}
public void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName)
{
//ConsoleAPI.WriteLine($" [ALIAS_BARRIER] Alias '{physicalAllocationName}': '{beforeResourceName}' -> '{afterResourceName}'");
}
public void AliasingBarrier(Span<AliasingBarrierInfo> barriers)
{
if (barriers.Length == 0) return;
//ConsoleAPI.WriteLine($" [ALIAS_BARRIER_BATCH] Processing {barriers.Length} aliasing barriers:");
foreach (var barrier in barriers)
{
//ConsoleAPI.WriteLine($" - Alias '{barrier.PhysicalAllocationName}': '{barrier.BeforeResourceName}' -> '{barrier.AfterResourceName}'");
}
}
public void BeginRenderPass(string passName)
{
//ConsoleAPI.WriteLine($" [BEGIN] RenderPass '{passName}'");
}
public void EndRenderPass()
{
//ConsoleAPI.WriteLine($" [END] RenderPass");
}
public void SetRenderTarget(string textureName)
{
//ConsoleAPI.WriteLine($" [RT] Set RenderTarget: '{textureName}'");
}
public void SetDepthStencil(string textureName)
{
//ConsoleAPI.WriteLine($" [DS] Set DepthStencil: '{textureName}'");
}
public void BindShaderResource(string resourceName, int slot)
{
//ConsoleAPI.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}");
}
public void BindUnorderedAccess(string resourceName, int slot)
{
//ConsoleAPI.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}");
}
public void Draw(int vertexCount)
{
//ConsoleAPI.WriteLine($" [DRAW] Drawing {vertexCount} vertices");
}
public void Dispatch(int x, int y, int z)
{
//ConsoleAPI.WriteLine($" [DISPATCH] Compute ({x}, {y}, {z})");
}
public void ClearRenderTarget(string textureName, float r, float g, float b, float a)
{
//ConsoleAPI.WriteLine($" [CLEAR_RT] Clear '{textureName}' to ({r}, {g}, {b}, {a})");
}
public void ClearDepth(string textureName, float depth)
{
//ConsoleAPI.WriteLine($" [CLEAR_DEPTH] Clear '{textureName}' to {depth}");
}
public void CopyTexture(string source, string destination)
{
//ConsoleAPI.WriteLine($" [COPY] Copy from '{source}' to '{destination}'");
}
}
public readonly struct RasterRenderContext
{
private readonly ICommandBuffer _cmd;
public RasterRenderContext(ICommandBuffer cmd)
{
_cmd = cmd;
}
public void SetRenderTarget(string textureName) => _cmd.SetRenderTarget(textureName);
public void SetDepthStencil(string textureName) => _cmd.SetDepthStencil(textureName);
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
public void Draw(int vertexCount) => _cmd.Draw(vertexCount);
public void ClearRenderTarget(string textureName, float r, float g, float b, float a) => _cmd.ClearRenderTarget(textureName, r, g, b, a);
public void ClearDepth(string textureName, float depth) => _cmd.ClearDepth(textureName, depth);
}
public readonly struct ComputeRenderContext
{
private readonly ICommandBuffer _cmd;
public ComputeRenderContext(ICommandBuffer cmd)
{
_cmd = cmd;
}
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
public void BindUnorderedAccess(string resourceName, int slot) => _cmd.BindUnorderedAccess(resourceName, slot);
public void Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
}

View File

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

View File

@@ -1,14 +1,16 @@
namespace Ghost.RenderGraph.Concept;
// Pass data structure for GBuffer outputs
public class GBufferData
// ===== Pass Data Structures =====
// These are user-defined data structures that get passed to render functions
public sealed class GBufferData : IPassData
{
public RenderGraphTextureHandle Albedo;
public RenderGraphTextureHandle Normal;
public RenderGraphTextureHandle Depth;
}
public class LightingPassData
public sealed class LightingPassData : IPassData
{
public RenderGraphTextureHandle GBufferAlbedo;
public RenderGraphTextureHandle GBufferNormal;
@@ -16,43 +18,38 @@ public class LightingPassData
public RenderGraphTextureHandle OutputLighting;
}
public class SSAOPassData
public sealed class SSAOPassData : IPassData
{
public RenderGraphTextureHandle GBufferDepth;
public RenderGraphTextureHandle GBufferNormal;
public RenderGraphTextureHandle OutputSSAO;
}
public class TAAPassData
{
public RenderGraphTextureHandle InputLighting;
public RenderGraphTextureHandle OutputTAA;
}
public class PostProcessingPassData
{
public RenderGraphTextureHandle InputTAA;
public RenderGraphTextureHandle InputSSAO;
public RenderGraphTextureHandle OutputBackbuffer;
}
public class DebugPassData
{
public RenderGraphTextureHandle DebugTexture;
}
public class ProfilerMarkerData { }
public class BloomDownsampleData
public sealed class BloomDownsampleData : IPassData
{
public RenderGraphTextureHandle Input;
public RenderGraphTextureHandle Output;
}
public class PostProcessingPassDataV2
public sealed class TAAPassData : IPassData
{
public RenderGraphTextureHandle InputLighting;
public RenderGraphTextureHandle OutputTAA;
}
public sealed class PostProcessingPassDataV2 : IPassData
{
public RenderGraphTextureHandle InputTAA;
public RenderGraphTextureHandle InputSSAO;
public RenderGraphTextureHandle InputBloom;
public RenderGraphTextureHandle OutputBackbuffer;
}
public sealed class ProfilerMarkerData : IPassData
{
}
public sealed class DebugPassData : IPassData
{
public RenderGraphTextureHandle DebugTexture;
}

View File

@@ -1,41 +1,43 @@
using Ghost.RenderGraph.Concept;
//ConsoleAPI.WriteLine("==================================================");
//ConsoleAPI.WriteLine(" Transient Render Graph - Proof of Concept");
//ConsoleAPI.WriteLine(" Using Typed Pass Data and Blackboard Pattern");
//ConsoleAPI.WriteLine("==================================================\n");
var renderGraph = new RenderGraph();
for (int i = 0; i < 500; i++)
#if !DEBUG
const int _ITERATION = 500;
for (var i = 0; i < _ITERATION; i++)
{
BuildGraph(renderGraph);
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();
for (int i = 0; i < 500; i++)
for (var i = 0; i < _ITERATION; i++)
{
BuildGraph(renderGraph);
ExecuteGraph(renderGraph);
}
//BuildGraph(renderGraph);
sw.Stop();
var gcAfter = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / 500} ns");
Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / 500} bytes");
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);
//Console.WriteLine("\nPress any key to exit...");
//Console.ReadKey();
Console.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
ExecuteGraph(renderGraph);
#endif
static void BuildGraph(RenderGraph renderGraph)
static void ExecuteGraph(RenderGraph renderGraph)
{
renderGraph.Reset();
renderGraph.Reset(); // new RenderGraph()
// Import external resources
var backbuffer = renderGraph.ImportTexture(
@@ -56,7 +58,7 @@ static void BuildGraph(RenderGraph renderGraph)
gbufferData.Normal = builder.WriteTexture(normal);
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<GBufferData>((data, cmd) =>
{
cmd.SetRenderTarget(data.Albedo.Name);
cmd.SetRenderTarget(data.Normal.Name);
@@ -87,7 +89,7 @@ static void BuildGraph(RenderGraph renderGraph)
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<LightingPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
@@ -112,7 +114,7 @@ static void BuildGraph(RenderGraph renderGraph)
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
// Use SetComputeFunc with asyncCompute: true
builder.SetComputeFunc((data, cmd) =>
builder.SetComputeFunc<SSAOPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
@@ -132,7 +134,7 @@ static void BuildGraph(RenderGraph renderGraph)
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
bloomData.Output = builder.WriteTexture(bloomOutput);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<BloomDownsampleData>((data, cmd) =>
{
cmd.BindShaderResource(data.Input.Name, 0);
cmd.SetRenderTarget(data.Output.Name);
@@ -150,7 +152,7 @@ static void BuildGraph(RenderGraph renderGraph)
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
taaData.OutputTAA = builder.WriteTexture(taaOutput);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<TAAPassData>((data, cmd) =>
{
cmd.BindShaderResource(data.InputLighting.Name, 0);
cmd.SetRenderTarget(data.OutputTAA.Name);
@@ -166,7 +168,7 @@ static void BuildGraph(RenderGraph renderGraph)
postData.InputBloom = builder.ReadTexture(bloomOutput);
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<PostProcessingPassDataV2>((data, cmd) =>
{
cmd.BindShaderResource(data.InputTAA.Name, 0);
cmd.BindShaderResource(data.InputSSAO.Name, 1);
@@ -180,7 +182,7 @@ static void BuildGraph(RenderGraph renderGraph)
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((data, cmd) =>
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
@@ -194,7 +196,7 @@ static void BuildGraph(RenderGraph renderGraph)
debugData.DebugTexture = builder.WriteTexture(
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc<DebugPassData>((data, cmd) =>
{
cmd.SetRenderTarget(data.DebugTexture.Name);
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,154 @@
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// GPU resource states for barrier tracking.
/// Based on D3D12 resource states.
/// </summary>
[Flags]
public enum ResourceState
{
Undefined = 0,
RenderTarget = 1 << 0,
DepthWrite = 1 << 1,
DepthRead = 1 << 2,
ShaderResource = 1 << 3,
UnorderedAccess = 1 << 4,
CopySource = 1 << 5,
CopyDest = 1 << 6,
Present = 1 << 7,
}
/// <summary>
/// Types of barriers that can be inserted.
/// </summary>
public enum BarrierType
{
Transition, // State transition (e.g., RenderTarget -> ShaderResource)
Aliasing, // Aliasing barrier (new resource reusing memory)
UAV, // UAV barrier (synchronize UAV access)
}
/// <summary>
/// Represents a resource barrier that needs to be inserted.
/// For D3D12 aliasing barriers: ResourceBefore is the old resource, ResourceAfter is the new resource.
/// </summary>
internal struct ResourceBarrier
{
[StructLayout(LayoutKind.Explicit)]
private struct barrier_union
{
internal struct barrier_union_transition
{
public RenderGraphTextureHandle Resource;
public ResourceState StateBefore;
public ResourceState StateAfter;
}
internal struct barrier_union_aliasing
{
public RenderGraphTextureHandle ResourceBefore;
public RenderGraphTextureHandle ResourceAfter;
}
// TODO: union can not have non-blittable types
[FieldOffset(0)]
public barrier_union_transition Transition;
[FieldOffset(0)]
public barrier_union_aliasing Aliasing;
}
public BarrierType Type;
// For Transition and UAV barriers
public RenderGraphTextureHandle Resource;
public ResourceState StateBefore;
public ResourceState 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;
}
// Constructor for Aliasing barriers
public static ResourceBarrier CreateAliasingBarrier(
RenderGraphTextureHandle resourceBefore,
RenderGraphTextureHandle resourceAfter,
int passIndex)
{
return new ResourceBarrier
{
Type = BarrierType.Aliasing,
ResourceBefore = resourceBefore,
ResourceAfter = resourceAfter,
PassIndex = passIndex,
Resource = default,
StateBefore = ResourceState.Undefined,
StateAfter = ResourceState.Undefined
};
}
public static ResourceBarrier CreateTransitionBarrier(
RenderGraphTextureHandle resource,
ResourceState before,
ResourceState after,
int passIndex)
{
return new ResourceBarrier
{
Type = BarrierType.Transition,
Resource = resource,
StateBefore = before,
StateAfter = after,
PassIndex = passIndex,
ResourceBefore = default,
ResourceAfter = default
};
}
#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>
/// Tracks the current state of a resource across passes.
/// </summary>
internal sealed class ResourceStateTracker
{
public int ResourceIndex;
public ResourceState CurrentState = ResourceState.Undefined;
public int LastAccessPass = -1;
public void Reset()
{
ResourceIndex = -1;
CurrentState = ResourceState.Undefined;
LastAccessPass = -1;
}
}

View File

@@ -1,33 +0,0 @@
using System.Collections.Generic;
namespace Ghost.RenderGraph.Concept;
internal class RenderGraphBatch
{
public int ID { get; private set; }
public RenderQueueType QueueType { get; private set; }
public List<RenderGraphPass> Passes { get; } = new();
// Fences to wait on BEFORE executing this batch
public List<int> WaitFences { get; } = new();
// Fences to signal AFTER executing this batch
public List<int> SignalFences { get; } = new();
public RenderGraphBatch()
{
}
public void Initialize(int id, RenderQueueType queueType)
{
ID = id;
QueueType = queueType;
}
public void Reset()
{
Passes.Clear();
WaitFences.Clear();
SignalFences.Clear();
}
}

View File

@@ -1,26 +1,43 @@
namespace Ghost.RenderGraph.Concept;
public class RenderGraphBlackboard
/// <summary>
/// Blackboard for sharing data between render passes.
/// Uses a dictionary with type keys to store different pass data types.
/// Avoids allocations by reusing the same dictionary across frames.
/// </summary>
public sealed class RenderGraphBlackboard
{
private readonly Dictionary<Type, object> _data = new();
private readonly Dictionary<Type, object> _data = new(16);
public void Add<T>(T data) where T : class
/// <summary>
/// Adds or updates pass data in the blackboard.
/// </summary>
public void Add<T>(T data) where T : class, IPassData
{
_data[typeof(T)] = data;
var type = typeof(T);
_data[type] = data;
}
public T Get<T>() where T : class
/// <summary>
/// Retrieves pass data from the blackboard.
/// </summary>
public T Get<T>() where T : class, IPassData
{
if (_data.TryGetValue(typeof(T), out var data))
var type = typeof(T);
if (_data.TryGetValue(type, out var obj))
{
return (T)data;
return (T)obj;
}
throw new KeyNotFoundException($"Data of type {typeof(T).Name} not found in blackboard.");
throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard");
}
public bool TryGet<T>(out T? data) where T : class
/// <summary>
/// Tries to get pass data from the blackboard.
/// </summary>
public bool TryGet<T>(out T? data) where T : class, IPassData
{
if (_data.TryGetValue(typeof(T), out var obj))
var type = typeof(T);
if (_data.TryGetValue(type, out var obj))
{
data = (T)obj;
return true;
@@ -29,6 +46,10 @@ public class RenderGraphBlackboard
return false;
}
/// <summary>
/// Clears all data from the blackboard.
/// Does not deallocate the backing dictionary to avoid allocations.
/// </summary>
public void Clear()
{
_data.Clear();

View File

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

@@ -0,0 +1,131 @@
using System.Diagnostics.CodeAnalysis;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents cached compilation results for a render graph.
/// This avoids recompiling the graph when the structure hasn't changed.
/// </summary>
internal sealed class CachedCompilation
{
// Compiled pass indices (indices into the _passes list)
public readonly List<int> CompiledPassIndices = new(64);
// Culling decisions for each pass
public readonly List<bool> PassCulledFlags = new(64);
// Physical resource aliasing mappings (logical index -> physical index)
public readonly Dictionary<int, int> LogicalToPhysical = new(128);
// Physical resource metadata
public readonly List<PhysicalResourceData> PhysicalResources = new(32);
// Resource barriers
public readonly List<ResourceBarrier> Barriers = new(128);
// Resource state mappings (for barrier generation)
public readonly Dictionary<int, ResourceState> ResourceStates = new(128);
public void Clear()
{
CompiledPassIndices.Clear();
PassCulledFlags.Clear();
LogicalToPhysical.Clear();
PhysicalResources.Clear();
Barriers.Clear();
ResourceStates.Clear();
}
}
/// <summary>
/// Physical resource data for caching.
/// </summary>
internal struct PhysicalResourceData
{
public int Index;
public int Width;
public int Height;
public TextureFormat Format;
public int FirstUsePass;
public int LastUsePass;
}
/// <summary>
/// Manages compilation caching for render graphs.
/// Stores compiled results and allows cache hits when graph structure is unchanged.
/// </summary>
internal sealed class RenderGraphCompilationCache
{
private ulong _cachedHash;
private readonly CachedCompilation _cached = new();
private bool _hasCachedData;
// Statistics
public int CacheHits { get; private set; }
public int CacheMisses { get; private set; }
/// <summary>
/// Attempts to retrieve cached compilation results.
/// </summary>
public bool TryGetCached(ulong hash, [MaybeNullWhen(false)] out CachedCompilation result)
{
if (_hasCachedData && _cachedHash == hash)
{
result = _cached;
CacheHits++;
return true;
}
result = null;
CacheMisses++;
return false;
}
/// <summary>
/// Stores compilation results in the cache.
/// </summary>
public void Store(ulong hash, CachedCompilation data)
{
_cachedHash = hash;
_hasCachedData = true;
// Deep copy the data
_cached.Clear();
_cached.CompiledPassIndices.AddRange(data.CompiledPassIndices);
_cached.PassCulledFlags.AddRange(data.PassCulledFlags);
foreach (var kvp in data.LogicalToPhysical)
{
_cached.LogicalToPhysical[kvp.Key] = kvp.Value;
}
_cached.PhysicalResources.AddRange(data.PhysicalResources);
_cached.Barriers.AddRange(data.Barriers);
foreach (var kvp in data.ResourceStates)
{
_cached.ResourceStates[kvp.Key] = kvp.Value;
}
}
/// <summary>
/// Invalidates the cache, forcing recompilation on next Compile().
/// </summary>
public void Invalidate()
{
_hasCachedData = false;
_cachedHash = 0;
_cached.Clear();
}
/// <summary>
/// Gets cache statistics for debugging.
/// </summary>
public (int hits, int misses, double hitRate) GetStatistics()
{
int total = CacheHits + CacheMisses;
double hitRate = total > 0 ? (double)CacheHits / total : 0.0;
return (CacheHits, CacheMisses, hitRate);
}
}

View File

@@ -0,0 +1,134 @@
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
{
public void SetRenderTarget(string name)
{
#if DEBUG
Console.WriteLine(nameof(SetRenderTarget) + ": " + name);
#endif
}
public void SetDepthStencil(string name)
{
#if DEBUG
Console.WriteLine(nameof(SetDepthStencil) + ": " + name);
#endif
}
public void ClearRenderTarget(string name, float r, float g, float b, float a)
{
#if DEBUG
Console.WriteLine(nameof(ClearRenderTarget) + ": " + name);
#endif
}
public void ClearDepth(string name, float depth)
{
#if DEBUG
Console.WriteLine(nameof(ClearDepth) + ": " + name);
#endif
}
public void Draw(int vertexCount)
{
#if DEBUG
Console.WriteLine(nameof(Draw) + ": " + vertexCount);
#endif
}
public void BindShaderResource(string name, int slot)
{
#if DEBUG
Console.WriteLine(nameof(BindShaderResource) + ": " + name + ", slot " + slot);
#endif
}
public void BindUnorderedAccess(string name, int slot)
{
#if DEBUG
Console.WriteLine(nameof(BindUnorderedAccess) + ": " + name + ", slot " + slot);
#endif
}
public void Dispatch(int x, int y, int z)
{
#if DEBUG
Console.WriteLine(nameof(Dispatch) + ": " + x + ", " + y + ", " + z);
#endif
}
public void ResourceBarrier(string resourceName, string stateBefore, string stateAfter)
{
#if DEBUG
Console.WriteLine(nameof(ResourceBarrier) + ": " + resourceName + " from " + stateBefore + " to " + stateAfter);
#endif
}
public void AliasBarrier(string resourceBefore, string resourceAfter)
{
#if DEBUG
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
#endif
}
}
/// <summary>
/// Context for raster rendering passes.
/// Directly exposes command buffer methods.
/// </summary>
public readonly struct RasterRenderContext
{
private readonly MockCommandBuffer _cmd;
public 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 Draw(int vertexCount) => _cmd.Draw(vertexCount);
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot);
}
/// <summary>
/// Context for compute rendering passes.
/// Directly exposes command buffer methods.
/// </summary>
public readonly struct ComputeRenderContext
{
private readonly MockCommandBuffer _cmd;
public 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 Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
}
/// <summary>
/// Unified render context containing both raster and compute contexts.
/// </summary>
internal readonly struct RenderContext
{
public readonly RasterRenderContext RasterContext;
public readonly ComputeRenderContext ComputeContext;
public RenderContext(MockCommandBuffer cmd)
{
RasterContext = new RasterRenderContext(cmd);
ComputeContext = new ComputeRenderContext(cmd);
}
}

View File

@@ -1,45 +0,0 @@
/*
namespace Ghost.RenderGraph.Concept;
public static class RenderGraphExtensions
{
// Cannot use RenderGraphPassBuilder in Action<> because it is a ref struct
// public static RenderGraphPassBuilder<TPassData> AddRenderPass<TPassData>(
// this RenderGraph renderGraph,
// string name,
// out TPassData passData,
// Action<RenderGraphPassBuilder<TPassData>> setup)
// where TPassData : class, new()
// {
// var builder = renderGraph.AddRenderPass<TPassData>(name, out passData);
// setup(builder);
// builder.Dispose();
// return builder;
// }
}
public sealed class RenderGraphPassScope<TPassData> : IDisposable
where TPassData : class, new()
{
// Cannot hold ref struct in class
// private readonly RenderGraphPassBuilder<TPassData> _builder;
private readonly string _passName;
// internal RenderGraphPassScope(RenderGraphPassBuilder<TPassData> builder, string passName)
// {
// _builder = builder;
// _passName = passName;
// }
// public RenderGraphPassBuilder<TPassData> Builder => _builder;
public void Dispose()
{
// Commit the pass when the using block ends
// if (_builder.RenderFunc != null)
// {
// _builder.Dispose();
// }
}
}
*/

View File

@@ -0,0 +1,68 @@
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,111 +1,215 @@
using System;
using System.Collections.Generic;
namespace Ghost.RenderGraph.Concept;
public enum RenderQueueType
/// <summary>
/// Represents different types of render passes.
/// </summary>
public enum RenderPassType : byte
{
Graphics,
Compute,
AsyncCompute,
Copy
Raster,
Compute
}
internal abstract class RenderGraphPass
/// <summary>
/// Base class for render passes.
/// Uses pooling to avoid allocations after the first frame.
/// </summary>
internal abstract class RenderGraphPassBase
{
public string Name { get; set; } = string.Empty;
public int Index { get; set; }
public RenderQueueType QueueType { get; set; }
public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; set; }
public List<int> Dependencies { get; } = new();
public int RefCount { get; set; } = 0;
public bool AllowCulling { get; set; }
public string Name = string.Empty;
public int Index;
public RenderPassType Type;
public bool AllowCulling = true;
public bool AsyncCompute;
// Resource dependencies
public readonly List<RenderGraphTextureHandle> TextureReads = new(8);
public readonly List<RenderGraphTextureHandle> TextureWrites = new(4);
public readonly List<RenderGraphTextureHandle> TextureCreates = new(4);
// Execution state
public bool Culled;
public bool HasSideEffects;
protected RenderGraphPass(
string name,
int index,
RenderQueueType queueType,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
public abstract void Execute(RenderContext context);
public abstract void Clear();
public virtual void Reset()
{
Name = name;
Index = index;
QueueType = queueType;
ResourceAccesses = resourceAccesses;
AllowCulling = allowCulling;
Name = string.Empty;
Index = -1;
Type = RenderPassType.Raster;
AllowCulling = true;
AsyncCompute = false;
TextureReads.Clear();
TextureWrites.Clear();
TextureCreates.Clear();
Culled = false;
HasSideEffects = false;
}
protected void InitializeBase(
string name,
int index,
RenderQueueType queueType,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
{
Name = name;
Index = index;
QueueType = queueType;
ResourceAccesses = resourceAccesses;
AllowCulling = allowCulling;
Dependencies.Clear();
RefCount = 0;
}
public abstract void Execute(ICommandBuffer commandBuffer);
public abstract void Release();
}
internal static class RenderGraphPassPool<TPassData>
where TPassData : class
/// <summary>
/// Typed render pass with user data.
/// </summary>
internal sealed class RenderGraphPass<TPassData> : RenderGraphPassBase
where TPassData : class, new()
{
public static readonly Stack<RenderGraphPass<TPassData>> Pool = new();
public TPassData? PassData;
public Action<TPassData, RasterRenderContext>? RasterRenderFunc;
public Action<TPassData, ComputeRenderContext>? ComputeRenderFunc;
public override void Execute(RenderContext context)
{
if (PassData == null)
return;
if (Type == RenderPassType.Raster && RasterRenderFunc != null)
{
RasterRenderFunc(PassData, context.RasterContext);
}
else if (Type == RenderPassType.Compute && ComputeRenderFunc != null)
{
ComputeRenderFunc(PassData, context.ComputeContext);
}
}
public override void Clear()
{
PassData = null;
RasterRenderFunc = null;
ComputeRenderFunc = null;
}
public override void Reset()
{
base.Reset();
Clear();
}
}
internal class RenderGraphPass<TPassData> : RenderGraphPass
where TPassData : class
/// <summary>
/// Builder for constructing render passes.
/// Implements IDisposable for using() pattern.
/// </summary>
public sealed class RenderGraphBuilder : IDisposable
{
public TPassData PassData { get; private set; }
public Action<TPassData, ICommandBuffer> RenderFunc { get; private set; }
private RenderGraphPassBase? _pass;
private RenderGraphResourceRegistry? _resources;
private bool _disposed;
public RenderGraphPass(
string name,
int index,
RenderQueueType queueType,
TPassData passData,
Action<TPassData, ICommandBuffer> renderFunc,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
: base(name, index, queueType, resourceAccesses, allowCulling)
internal void Initialize(RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
{
PassData = passData;
RenderFunc = renderFunc;
_pass = pass;
_resources = resources;
_disposed = false;
}
public void Initialize(
string name,
int index,
RenderQueueType queueType,
TPassData passData,
Action<TPassData, ICommandBuffer> renderFunc,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
/// <summary>
/// Creates a new transient texture that only lives for this pass.
/// </summary>
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
{
InitializeBase(name, index, queueType, resourceAccesses, allowCulling);
PassData = passData;
RenderFunc = renderFunc;
ThrowIfDisposed();
var handle = _resources!.CreateTexture(descriptor);
_pass!.TextureCreates.Add(handle);
_resources.SetProducer(handle, _pass.Index);
return handle;
}
public override void Execute(ICommandBuffer commandBuffer)
/// <summary>
/// Marks a texture as being read by this pass.
/// </summary>
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
{
RenderFunc(PassData, commandBuffer);
ThrowIfDisposed();
_pass!.TextureReads.Add(handle);
_resources!.AddConsumer(handle, _pass.Index);
return handle;
}
public override void Release()
/// <summary>
/// Marks a texture as being written by this pass.
/// </summary>
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
{
PassData = null!;
RenderFunc = null!;
// ResourceAccesses list ownership is transferred back to RenderGraph
ResourceAccesses = null!;
RenderGraphPassPool<TPassData>.Pool.Push(this);
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,120 +0,0 @@
namespace Ghost.RenderGraph.Concept;
public interface IRenderGraphBuilder
{
RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle);
RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle);
RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess);
RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor);
RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle);
RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle);
RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor);
}
public ref struct RenderGraphPassBuilder<TPassData>
where TPassData : class, new()
{
private readonly RenderGraph _graph;
private readonly string _passName;
private readonly int _passIndex;
private RenderQueueType _queueType;
private readonly List<(RenderGraphResourceHandle handle, ResourceState state)> _resourceAccesses;
private Action<TPassData, ICommandBuffer>? _renderFunc;
private bool _committed;
private bool _allowCulling;
public TPassData PassData { get; }
internal RenderGraphPassBuilder(RenderGraph graph, string passName, int passIndex, List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses)
{
_graph = graph;
_passName = passName;
_passIndex = passIndex;
PassData = new TPassData();
_resourceAccesses = resourceAccesses;
_queueType = RenderQueueType.Graphics;
_allowCulling = true;
_committed = false;
_renderFunc = null;
}
internal IReadOnlyList<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses => _resourceAccesses;
internal RenderQueueType QueueType => _queueType;
internal Action<TPassData, ICommandBuffer>? RenderFunc => _renderFunc;
internal bool AllowCulling => _allowCulling;
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
{
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
return handle;
}
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
{
_resourceAccesses.Add((handle._handle, ResourceState.RenderTarget));
return handle;
}
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
{
_resourceAccesses.Add((handle._handle, writeAccess ? ResourceState.DepthWrite : ResourceState.DepthRead));
return handle;
}
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
{
var handle = _graph.CreateTransientTexture(descriptor);
return handle;
}
public RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle)
{
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
return handle;
}
public RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle)
{
_resourceAccesses.Add((handle._handle, ResourceState.UnorderedAccess));
return handle;
}
public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor)
{
var handle = _graph.CreateTransientBuffer(descriptor);
return handle;
}
public void SetRenderFunc(Action<TPassData, RasterRenderContext> renderFunc)
{
_queueType = RenderQueueType.Graphics;
_renderFunc = (data, cmd) => renderFunc(data, new RasterRenderContext(cmd));
}
public void SetComputeFunc(Action<TPassData, ComputeRenderContext> computeFunc, bool asyncCompute = false)
{
_queueType = asyncCompute ? RenderQueueType.AsyncCompute : RenderQueueType.Compute;
_renderFunc = (data, cmd) => computeFunc(data, new ComputeRenderContext(cmd));
}
/// <summary>
/// Controls whether this pass can be culled if it doesn't contribute to the final output.
/// Set to false for synchronization passes, debug markers, or async compute boundaries.
/// Default is true.
/// </summary>
public void SetAllowCulling(bool allowCulling)
{
_allowCulling = allowCulling;
}
public void Dispose()
{
// Commit the pass when disposed (at end of using block)
if (!_committed)
{
_graph.CommitPass(this, _passName);
_committed = true;
}
}
}

View File

@@ -1,63 +0,0 @@
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
public struct RenderGraphResourceHandle
{
[StructLayout(LayoutKind.Explicit)]
internal struct descriptor_union
{
[FieldOffset(0)]
public TextureDescriptor texture;
[FieldOffset(0)]
public BufferDescriptor buffer;
}
internal int Id { get; }
internal ResourceType Type { get; }
internal string Name { get; }
internal bool IsImported { get; }
internal descriptor_union Descriptor { get; }
internal RenderGraphResourceHandle(int id, ResourceType type, string name, bool isImported, descriptor_union descriptor)
{
Id = id;
Type = type;
Name = name;
IsImported = isImported;
Descriptor = descriptor;
}
public override string ToString() => Name;
}
public struct RenderGraphTextureHandle
{
internal readonly RenderGraphResourceHandle _handle;
internal int Id => _handle.Id;
internal ResourceType Type => _handle.Type;
internal string Name => _handle.Name;
internal bool IsImported => _handle.IsImported;
internal RenderGraphTextureHandle(int id, string name, TextureDescriptor descriptor, bool isImported)
{
_handle = new RenderGraphResourceHandle(id, ResourceType.Texture, name, isImported, new RenderGraphResourceHandle.descriptor_union() { texture = descriptor });
}
}
public struct RenderGraphBufferHandle
{
internal readonly RenderGraphResourceHandle _handle;
internal BufferDescriptor Descriptor { get; }
internal int Id => _handle.Id;
internal ResourceType Type => _handle.Type;
internal string Name => _handle.Name;
internal bool IsImported => _handle.IsImported;
internal RenderGraphBufferHandle(int id, string name, BufferDescriptor descriptor, bool isImported)
{
_handle = new RenderGraphResourceHandle(id, ResourceType.Buffer, name, isImported, new RenderGraphResourceHandle.descriptor_union() { buffer = descriptor });
}
}

View File

@@ -0,0 +1,173 @@
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Object pool for reusing allocated objects across frames.
/// This is key to minimizing GC allocations after the first frame.
/// </summary>
internal sealed class RenderGraphObjectPool
{
private readonly Dictionary<Type, Stack<object>> _pools = new();
public T Get<T>() where T : class, new()
{
var type = typeof(T);
if (_pools.TryGetValue(type, out var pool) && pool.Count > 0)
{
return (T)pool.Pop();
}
return new T();
}
public void Release<T>(T obj) where T : class
{
if (obj == null) return;
var type = typeof(T);
if (!_pools.TryGetValue(type, out var pool))
{
pool = new Stack<object>(16);
_pools[type] = pool;
}
pool.Push(obj);
}
public void Clear()
{
_pools.Clear();
}
}
/// <summary>
/// Represents a texture resource in the render graph.
/// </summary>
internal sealed class TextureResource
{
public int Index;
public int Version;
public TextureDescriptor Descriptor;
public bool IsImported;
public int FirstUsePass = -1;
public int LastUsePass = -1;
public int ProducerPass = -1;
public List<int> ConsumerPasses = new(4);
public int RefCount;
public void Reset()
{
Index = -1;
Version = 0;
Descriptor = default;
IsImported = false;
FirstUsePass = -1;
LastUsePass = -1;
ProducerPass = -1;
ConsumerPasses.Clear();
RefCount = 0;
}
}
/// <summary>
/// Registry for managing all resources in the render graph.
/// Uses pooling to minimize allocations after the first frame.
/// </summary>
internal sealed class RenderGraphResourceRegistry
{
private readonly List<TextureResource> _textureResources = new(64);
private readonly RenderGraphObjectPool _pool = new();
private int _textureResourceCount;
public int TextureResourceCount => _textureResourceCount;
public void BeginFrame()
{
// Don't clear the lists, just reset the count
// This avoids reallocating the backing arrays
_textureResourceCount = 0;
}
public RenderGraphTextureHandle 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);
}
public RenderGraphTextureHandle 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);
}
public TextureResource GetTextureResource(RenderGraphTextureHandle handle)
{
if (handle.Index < 0 || handle.Index >= _textureResourceCount)
throw new ArgumentException($"Invalid texture handle: {handle.Index}");
return _textureResources[handle.Index];
}
public TextureResource GetTextureResourceByIndex(int index)
{
if (index < 0 || index >= _textureResourceCount)
throw new ArgumentException($"Invalid texture index: {index}");
return _textureResources[index];
}
public void SetProducer(RenderGraphTextureHandle handle, int passIndex)
{
var resource = GetTextureResource(handle);
resource.ProducerPass = passIndex;
if (resource.FirstUsePass < 0)
resource.FirstUsePass = passIndex;
}
public void AddConsumer(RenderGraphTextureHandle handle, int passIndex)
{
var resource = GetTextureResource(handle);
resource.ConsumerPasses.Add(passIndex);
resource.LastUsePass = passIndex;
if (resource.FirstUsePass < 0)
resource.FirstUsePass = passIndex;
}
private TextureResource GetOrCreateTextureResource()
{
TextureResource resource;
if (_textureResourceCount < _textureResources.Count)
{
// Reuse existing slot
resource = _textureResources[_textureResourceCount];
resource.Reset();
}
else
{
// Need to grow the list
resource = _pool.Get<TextureResource>();
resource.Reset();
_textureResources.Add(resource);
}
_textureResourceCount++;
return resource;
}
public void Clear()
{
for (int i = 0; i < _textureResources.Count; i++)
{
_pool.Release(_textureResources[i]);
}
_textureResources.Clear();
_textureResourceCount = 0;
}
}

View File

@@ -0,0 +1,75 @@
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Opaque handle to a render graph texture resource.
/// </summary>
public readonly struct RenderGraphTextureHandle : IEquatable<RenderGraphTextureHandle>
{
public readonly int Index;
public readonly int Version;
internal readonly string InternalName;
public RenderGraphTextureHandle(int index, int version, string name = "")
{
Index = index;
Version = version;
InternalName = name;
}
public string Name => InternalName;
public bool IsValid() => Index >= 0;
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);
}
/// <summary>
/// Texture formats supported by the render graph.
/// </summary>
public enum TextureFormat : int
{
RGBA8,
RGBA16F,
RGBA32F,
Depth32F,
Depth24Stencil8
}
/// <summary>
/// Descriptor for creating a texture resource.
/// </summary>
public readonly struct TextureDescriptor : IEquatable<TextureDescriptor>
{
public readonly int Width;
public readonly int Height;
public readonly TextureFormat Format;
public readonly string Name;
public TextureDescriptor(int width, int height, TextureFormat format, string name)
{
Width = width;
Height = height;
Format = format;
Name = name;
}
public readonly bool Equals(TextureDescriptor other) =>
Width == other.Width &&
Height == other.Height &&
Format == other.Format &&
Name == other.Name;
public override readonly bool Equals(object? obj) => obj is TextureDescriptor other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(Width, Height, Format, Name);
}
/// <summary>
/// Base interface for pass data that can be stored in the blackboard.
/// </summary>
public interface IPassData
{
}

View File

@@ -1,360 +0,0 @@
using Ghost.Core.Utilities;
using ZLinq;
namespace Ghost.RenderGraph.Concept;
/// <summary>
/// Represents a physical memory allocation that can be shared by multiple transient resources
/// </summary>
internal struct PhysicalResourceAllocation
{
public int AllocationId { get; }
public ulong SizeInBytes { get; }
public ulong OffsetInBytes { get; }
public string DebugName { get; }
public List<RenderGraphResourceHandle> AliasedResources { get; } = new();
public PhysicalResourceAllocation(int allocationId, ulong sizeInBytes, ulong offsetInBytes, string debugName)
{
AllocationId = allocationId;
SizeInBytes = sizeInBytes;
OffsetInBytes = offsetInBytes;
DebugName = debugName;
}
}
/// <summary>
/// Manages memory allocation and aliasing for transient resources
/// </summary>
internal class ResourceAllocator
{
private readonly List<PhysicalResourceAllocation> _allocations = new();
private int _allocationIdCounter = 0;
public void Reset()
{
_allocations.Clear();
_allocationIdCounter = 0;
}
public IReadOnlyList<PhysicalResourceAllocation> Allocations => _allocations;
/// <summary>
/// Allocate physical memory for resources, enabling aliasing where possible
/// </summary>
public void AllocateResources(
IReadOnlyList<ResourceLifetime> resourceLifetimes,
List<RenderGraphPass> passes)
{
//ConsoleAPI.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS =====");
// Separate imported and transient resources
// Sort by SIZE FIRST (descending), then by FIRST USE (ascending)
// This allows smaller resources (A, B) to alias into larger resources' (C) space
// Example: C=10MB[1..2], A=4MB[0..1], B=6MB[0..1] → Allocate C first, then A and B alias into C's space
// TODO: Avoid linq for performance-critical path
var transientResources = resourceLifetimes.AsValueEnumerable()
.Where(lt => !lt.Handle.IsImported && lt.FirstUse != int.MaxValue)
.OrderByDescending(lt => GetResourceSize(lt.Handle))
.ThenBy(lt => lt.FirstUse).ToArray();
if (transientResources.Length == 0)
{
//ConsoleAPI.WriteLine("No transient resources to allocate.");
return;
}
// Track which allocation slots are occupied at each pass
var allocationSlots = Core.Utilities.ListPool<AllocationSlot>.Rent();
foreach (var resource in transientResources)
{
var size = GetResourceSize(resource.Handle);
var alignment = GetResourceAlignment(resource.Handle);
// Find an existing allocation slot that:
// 1. Is large enough
// 2. Has no lifetime overlap
// 3. Matches resource type (texture/buffer)
AllocationSlot? reuseSlot = null;
foreach (var slot in allocationSlots)
{
if (CanAlias(slot, resource, size, alignment))
{
reuseSlot = slot;
break;
}
}
if (reuseSlot != null)
{
// Reuse existing allocation - find offset within the allocation
ulong offsetInAllocation = reuseSlot.FindFreeOffset(size, alignment, resource);
reuseSlot.AddResource(resource, offsetInAllocation, size);
//ConsoleAPI.WriteLine($"[ALIAS] '{resource.Handle.Name}' aliases with '{reuseSlot.Allocation.DebugName}' " + $"(heap offset: {reuseSlot.Allocation.OffsetInBytes}, resource offset: {offsetInAllocation}, size: {size} bytes, " + $"lifetime: [{resource.FirstUse}..{resource.LastUse}])");
}
else
{
// Create new allocation
// Calculate heap offset (simulated - in real D3D12MA this would be the actual heap offset)
ulong heapOffset = allocationSlots.Count > 0
? allocationSlots.Max(s => s.Allocation.OffsetInBytes + s.Allocation.SizeInBytes)
: 0;
var allocation = new PhysicalResourceAllocation(
_allocationIdCounter++,
size,
offsetInBytes: heapOffset,
$"Physical_{resource.Handle.Type}_{_allocationIdCounter}");
var newSlot = new AllocationSlot(allocation, resource.Handle.Type);
newSlot.AddResource(resource, 0, size); // Offset 0 within this new allocation
allocationSlots.Add(newSlot);
//ConsoleAPI.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " + $"(heap offset: {heapOffset}, size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])");
}
}
foreach (var slot in allocationSlots)
{
_allocations.Add(slot.Allocation);
}
ListPool<AllocationSlot>.Return(allocationSlots);
// Print summary
//ConsoleAPI.WriteLine($"\n[RG] Memory Statistics:");
var totalWithoutAliasing = transientResources.Sum(r => (long)GetResourceSize(r.Handle));
var totalWithAliasing = _allocations.Sum(a => (long)a.SizeInBytes);
var savedMemory = totalWithoutAliasing - totalWithAliasing;
var savingPercentage = totalWithoutAliasing > 0 ? (savedMemory * 100.0 / totalWithoutAliasing) : 0;
//ConsoleAPI.WriteLine($" Total memory without aliasing: {FormatBytes(totalWithoutAliasing)}");
//ConsoleAPI.WriteLine($" Total memory with aliasing: {FormatBytes(totalWithAliasing)}");
//ConsoleAPI.WriteLine($" Memory saved: {FormatBytes(savedMemory)} ({savingPercentage:F1}%)");
//ConsoleAPI.WriteLine($" Allocations: {_allocations.Count} physical allocations for {transientResources.Count()} resources");
}
private bool CanAlias(AllocationSlot slot, ResourceLifetime resource, ulong requiredSize, ulong requiredAlignment)
{
// Must be same resource type
if (slot.ResourceType != resource.Handle.Type)
return false;
// Must be large enough
if (slot.Allocation.SizeInBytes < requiredSize)
return false;
// Check for lifetime overlap with any resource in this slot
foreach (var existingResource in slot.Resources)
{
if (LifetimesOverlap(existingResource, resource))
return false;
}
return true;
}
private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b)
{
// Two resources overlap if their lifetimes intersect
return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse);
}
private ulong GetResourceSize(RenderGraphResourceHandle handle)
{
return handle.Type switch
{
ResourceType.Texture => CalculateTextureSize(handle.Descriptor.texture),
ResourceType.Buffer => (ulong)handle.Descriptor.buffer.SizeInBytes,
_ => 0
};
}
private ulong GetResourceAlignment(RenderGraphResourceHandle handle)
{
// In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO
return handle.Type switch
{
ResourceType.Texture => 65536, // 64KB texture alignment (typical)
ResourceType.Buffer => 256, // 256 byte buffer alignment
_ => 256
};
}
private ulong CalculateTextureSize(TextureDescriptor desc)
{
// Simplified size calculation
var bytesPerPixel = desc.Format switch
{
TextureFormat.RGBA8 => 4,
TextureFormat.RGBA16F => 8,
TextureFormat.RGBA32F => 16,
TextureFormat.Depth32F => 4,
TextureFormat.R32Uint => 4,
_ => 4
};
return (ulong)(desc.Width * desc.Height * bytesPerPixel);
}
private string FormatBytes(long bytes)
{
if (bytes < 1024)
return $"{bytes} B";
if (bytes < 1024 * 1024)
return $"{bytes / 1024.0:F2} KB";
return $"{bytes / (1024.0 * 1024.0):F2} MB";
}
public PhysicalResourceAllocation? GetAllocation(RenderGraphResourceHandle handle)
{
// return _allocations.FirstOrDefault(a => a.AliasedResources.Any(r => r.Id == handle.Id));
foreach (var allocation in _allocations)
{
foreach (var aliased in allocation.AliasedResources)
{
if (aliased.Id == handle.Id)
{
return allocation;
}
}
}
return null;
}
private class AllocationSlot
{
public PhysicalResourceAllocation Allocation { get; }
public ResourceType ResourceType { get; }
public List<ResourceLifetime> Resources { get; } = new();
// Track occupied regions within this allocation: (offset, size, lifetime)
private readonly List<(ulong Offset, ulong Size, ResourceLifetime Resource)> _occupiedRegions = new();
public AllocationSlot(PhysicalResourceAllocation allocation, ResourceType resourceType)
{
Allocation = allocation;
ResourceType = resourceType;
}
/// <summary>
/// Find a free offset within this allocation that can fit the required size
/// and doesn't conflict with active resources
/// </summary>
public ulong FindFreeOffset(ulong requiredSize, ulong alignment, ResourceLifetime newResource)
{
// If no resources yet, return 0
if (_occupiedRegions.Count == 0)
{
return 0;
}
// Sort regions by offset
var sortedRegions = _occupiedRegions.AsValueEnumerable().OrderBy(r => r.Offset).ToArray();
// Try to fit at the beginning (offset 0)
ulong candidateOffset = 0;
bool fitsAtStart = true;
foreach (var (Offset, Size, Resource) in sortedRegions)
{
// Check if this region overlaps with our candidate position
if (Offset < requiredSize)
{
// Check lifetime - if no overlap, we can still use this space
if (LifetimesOverlap(Resource, newResource))
{
fitsAtStart = false;
break;
}
}
}
if (fitsAtStart)
{
return AlignUp(0, alignment);
}
// Try gaps between regions
for (int i = 0; i < sortedRegions.Length; i++)
{
var current = sortedRegions[i];
// Skip if current region's lifetime overlaps with new resource
if (LifetimesOverlap(current.Resource, newResource))
{
continue;
}
// Try placing after this region
candidateOffset = AlignUp(current.Offset + current.Size, alignment);
// Check if it fits before the next region (or end of allocation)
ulong nextRegionStart = (i + 1 < sortedRegions.Length)
? sortedRegions[i + 1].Offset
: Allocation.SizeInBytes;
if (candidateOffset + requiredSize <= nextRegionStart)
{
// Check no lifetime conflicts with any regions in this range
bool hasConflict = false;
for (int j = i + 1; j < sortedRegions.Length; j++)
{
var other = sortedRegions[j];
if (other.Offset < candidateOffset + requiredSize)
{
if (LifetimesOverlap(other.Resource, newResource))
{
hasConflict = true;
break;
}
}
}
if (!hasConflict)
{
return candidateOffset;
}
}
}
// Try placing at the end
if (sortedRegions.Length > 0)
{
var last = sortedRegions[^1];
if (!LifetimesOverlap(last.Resource, newResource))
{
candidateOffset = AlignUp(last.Offset + last.Size, alignment);
if (candidateOffset + requiredSize <= Allocation.SizeInBytes)
{
return candidateOffset;
}
}
}
// No space found - caller should create new allocation
return 0;
}
private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b)
{
return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse);
}
private ulong AlignUp(ulong value, ulong alignment)
{
return (value + alignment - 1) / alignment * alignment;
}
public void AddResource(ResourceLifetime resource, ulong offsetInAllocation, ulong size)
{
Resources.Add(resource);
_occupiedRegions.Add((offsetInAllocation, size, resource));
Allocation.AliasedResources.Add(resource.Handle);
}
}
}

View File

@@ -1,28 +0,0 @@
namespace Ghost.RenderGraph.Concept;
public enum ResourceType
{
Texture,
Buffer
}
public enum TextureFormat
{
RGBA8,
RGBA16F,
RGBA32F,
Depth32F,
R32Uint
}
public record struct TextureDescriptor(
int Width,
int Height,
TextureFormat Format,
string DebugName = "Unnamed Texture"
);
public record struct BufferDescriptor(
int SizeInBytes,
string DebugName = "Unnamed Buffer"
);

View File

@@ -1,42 +0,0 @@
namespace Ghost.RenderGraph.Concept;
internal struct ResourceUsage
{
public RenderGraphResourceHandle Handle { get; }
public ResourceState State { get; }
public int PassIndex { get; }
public ResourceUsage(RenderGraphResourceHandle handle, ResourceState state, int passIndex)
{
Handle = handle;
State = state;
PassIndex = passIndex;
}
}
internal struct ResourceLifetime
{
public RenderGraphResourceHandle Handle { get; private set; }
public int FirstUse { get; set; } = int.MaxValue;
public int LastUse { get; set; } = -1;
public List<ResourceUsage> Usages { get; } = new();
public ResourceLifetime()
{
}
public void Initialize(RenderGraphResourceHandle handle)
{
Handle = handle;
FirstUse = int.MaxValue;
LastUse = -1;
Usages.Clear();
}
public void AddUsage(ResourceState state, int passIndex)
{
Usages.Add(new ResourceUsage(Handle, state, passIndex));
FirstUse = Math.Min(FirstUse, passIndex);
LastUse = Math.Max(LastUse, passIndex);
}
}

View File

@@ -1,21 +0,0 @@
namespace Ghost.RenderGraph.Concept;
[Flags]
public enum ResourceState
{
Undefined = 0,
RenderTarget = 1 << 0,
DepthWrite = 1 << 1,
DepthRead = 1 << 2,
ShaderResource = 1 << 3,
UnorderedAccess = 1 << 4,
CopySource = 1 << 5,
CopyDest = 1 << 6,
Present = 1 << 7
}
public enum BarrierType
{
Transition, // Regular state transition
Aliasing // Aliasing barrier (resource is being reused)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,598 @@
#if false
using System;
using System.Diagnostics;
using UnityEngine.Experimental.Rendering;
using static UnityEngine.Rendering.RenderGraphModule.RenderGraph;
namespace UnityEngine.Rendering.RenderGraphModule
{
// This is a class making it a struct wouldn't help as we pas it around as an interface which means it would be boxed/unboxed anyway
// Publicly this class has different faces to help the users with different pass types through type safety but internally
// we just have a single implementation for all builders
internal class RenderGraphBuilders : IBaseRenderGraphBuilder, IComputeRenderGraphBuilder, IRasterRenderGraphBuilder, IUnsafeRenderGraphBuilder
{
RenderGraphPass m_RenderPass;
RenderGraphResourceRegistry m_Resources;
RenderGraph m_RenderGraph;
bool m_Disposed;
public RenderGraphBuilders()
{
m_RenderPass = null;
m_Resources = null;
m_RenderGraph = null;
m_Disposed = true;
}
public void Setup(RenderGraphPass renderPass, RenderGraphResourceRegistry resources, RenderGraph renderGraph)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// If the object is not disposed yet this is an error as the pass is not finished (only in the dispose we register it with the rendergraph)
// This is likely cause by a user not doing a clean using and then forgetting to manually dispose the object.
if (m_Disposed != true)
{
throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_UndisposedBuilderPreviousPass);
}
#endif
m_RenderPass = renderPass;
m_Resources = resources;
m_RenderGraph = renderGraph;
m_Disposed = false;
renderPass.useAllGlobalTextures = false;
if (renderPass.type == RenderGraphPassType.Raster)
{
CommandBuffer.ThrowOnSetRenderTarget = true;
}
}
public void EnableAsyncCompute(bool value)
{
m_RenderPass.EnableAsyncCompute(value);
}
public void AllowPassCulling(bool value)
{
// This pass cannot be culled if it allows global state modifications
if (value && m_RenderPass.allowGlobalState)
return;
m_RenderPass.AllowPassCulling(value);
}
public void AllowGlobalStateModification(bool value)
{
m_RenderPass.AllowGlobalState(value);
// This pass cannot be culled if it allows global state modifications
if (value)
{
AllowPassCulling(false);
}
}
/// <summary>
/// Enable foveated rendering for this pass.
/// </summary>
/// <param name="value">True to enable foveated rendering.</param>
public void EnableFoveatedRasterization(bool value)
{
m_RenderPass.EnableFoveatedRasterization(value);
}
public BufferHandle CreateTransientBuffer(in BufferDesc desc)
{
var result = m_Resources.CreateBuffer(desc, m_RenderPass.index);
UseTransientResource(result.handle);
return result;
}
public BufferHandle CreateTransientBuffer(in BufferHandle computebuffer)
{
ref readonly var desc = ref m_Resources.GetBufferResourceDesc(computebuffer.handle);
return CreateTransientBuffer(desc);
}
public TextureHandle CreateTransientTexture(in TextureDesc desc)
{
var result = m_Resources.CreateTexture(desc, m_RenderPass.index);
UseTransientResource(result.handle);
return result;
}
public TextureHandle CreateTransientTexture(in TextureHandle texture)
{
ref readonly var desc = ref m_Resources.GetTextureResourceDesc(texture.handle);
return CreateTransientTexture(desc);
}
public void GenerateDebugData(bool value)
{
m_RenderPass.GenerateDebugData(value);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (m_Disposed)
return;
try
{
if (disposing)
{
m_RenderGraph.RenderGraphState = RenderGraphState.RecordingGraph;
// Use all globals simply means this... we do a UseTexture on all globals so the pass has the correct dependencies.
// This of course goes to show how bad an idea shader-system wide globals really are dependency/lifetime tracking wise :-)
if (m_RenderPass.useAllGlobalTextures)
{
foreach (var texture in m_RenderGraph.AllGlobals())
{
if (texture.IsValid())
this.UseTexture(texture, AccessFlags.Read);
}
}
// Set globals on the graph fronted side so subsequent passes can have pass dependencies on these global texture handles
foreach (var t in m_RenderPass.setGlobalsList)
{
m_RenderGraph.SetGlobal(t.Item1, t.Item2);
}
}
}
finally
{
if (m_RenderPass.type == RenderGraphPassType.Raster)
{
CommandBuffer.ThrowOnSetRenderTarget = false;
}
m_RenderPass = null;
m_Resources = null;
m_RenderGraph = null;
m_Disposed = true;
}
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
private void CheckWriteTo(in ResourceHandle handle)
{
if (RenderGraph.enableValidityChecks)
{
// Write by design generates a new version of the resource. However
// you could in theory write to v2 of the resource while there is already
// a v3 so this write would then introduce a new v4 of the resource.
// This would mean a divergence in the versioning history subsequent versions based on v2 and other subsequent versions based on v3
// this would be very confusing as they are all still refered to by the same texture just different versions
// so we decide to disallow this. It can always be (at zero cost) handled by using a "Move" pass to move the divergent version
// so it's own texture resource which again has it's own single history of versions.
if (handle.IsVersioned)
{
var name = m_Resources.GetRenderGraphResourceName(handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {handle.type} at index {handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_WriteToVersionedResource);
}
if (m_RenderPass.IsWritten(handle))
{
// We bump the version and write count if you call USeResource with writing flags. So calling this several times
// would lead to the side effect of new versions for every call to UseResource leading to incorrect versions.
// In theory we could detect and ignore the second UseResource but we decided to just disallow is as it might also
// Stem from user confusion.
// It seems the most likely cause of such a situation would be something like:
// TextureHandle b = a;
// ...
// much much code in between, user lost track that a=b
// ...
// builder.WriteTexture(a)
// builder.WriteTexture(b)
// > Get this error they were probably thinking they were writing two separate outputs... but they are just two versions of resource 'a'
// where they can only differ between by careful management of versioned resources.
var name = m_Resources.GetRenderGraphResourceName(handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {handle.type} at index {handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_WriteToResourceTwice);
}
}
}
// Lifetime of a transient resource is one render graph pass
private ResourceHandle UseTransientResource(in ResourceHandle inputHandle)
{
CheckResource(inputHandle);
ResourceHandle versionedHandle = inputHandle.IsVersioned ? inputHandle : m_Resources.GetLatestVersionHandle(inputHandle);
// Transient resources are always considered written and read in the render graph pass where they are used
// Compiler will take it into account later, no need to add them to read and write lists
m_RenderPass.AddTransientResource(versionedHandle);
return versionedHandle;
}
private ResourceHandle UseResource(in ResourceHandle inputHandle, AccessFlags flags)
{
CheckResource(inputHandle);
bool discard = (flags & AccessFlags.Discard) != 0;
bool read = (flags & AccessFlags.Read) != 0;
bool write = (flags & AccessFlags.Write) != 0;
ResourceHandle versionedHandle = inputHandle.IsVersioned ? inputHandle : m_Resources.GetLatestVersionHandle(inputHandle);
// If we are not discarding the current version and its data, add a "read" dependency on it
// this is a bit of a misnomer it really means more like "Preserve existing content or read"
if (!discard)
{
m_Resources.IncrementReadCount(versionedHandle);
m_RenderPass.AddResourceRead(versionedHandle);
// Implicit read - not user-specified
if (!read)
{
m_RenderPass.implicitReadsList.Add(versionedHandle);
}
}
else
{
// We are discarding it but we still read it, so we add a dependency on version "0" of this resource
if (read)
{
var zeroVersionHandle = m_Resources.GetZeroVersionHandle(versionedHandle);
m_Resources.IncrementReadCount(zeroVersionHandle);
m_RenderPass.AddResourceRead(zeroVersionHandle);
}
}
if (write)
{
CheckWriteTo(inputHandle);
// New versioned written by this render graph pass
versionedHandle = m_Resources.IncrementWriteCount(inputHandle);
m_RenderPass.AddResourceWrite(versionedHandle);
}
return versionedHandle;
}
public BufferHandle UseBuffer(in BufferHandle input, AccessFlags flags)
{
UseResource(input.handle, flags);
return input;
}
// UseTexture and SetRenderAttachment are currently forced to be mutually exclusive in the same pass
// check this.
// We currently ignore the version. In theory there might be some cases that are actually allowed with versioning
// for ample UseTexture(myTexV1, read) UseFragment(myTexV2, ReadWrite) as they are different versions
// but for now we don't allow any of that.
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
private void CheckNotUseFragment(in TextureHandle tex)
{
if (RenderGraph.enableValidityChecks)
{
bool usedAsFragment = (m_RenderPass.depthAccess.textureHandle.IsValid() && m_RenderPass.depthAccess.textureHandle.handle.index == tex.handle.index);
if (!usedAsFragment)
{
for (int i = 0; i <= m_RenderPass.colorBufferMaxIndex; i++)
{
if (m_RenderPass.colorBufferAccess[i].textureHandle.IsValid() && m_RenderPass.colorBufferAccess[i].textureHandle.handle.index == tex.handle.index)
{
usedAsFragment = true;
break;
}
}
}
if (usedAsFragment)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_TextureAlreadyBeingUsedThroughSetAttachment);
}
}
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
private void CheckTextureUVOriginIsValid(in ResourceHandle handle, TextureResource texRes)
{
if (texRes.textureUVOrigin == TextureUVOriginSelection.TopLeft)
{
var name = m_Resources.GetRenderGraphResourceName(handle);
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type `{handle.type}` at index `{handle.index}` - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOriginUseTexture(texRes.textureUVOrigin));
}
}
public void UseTexture(in TextureHandle input, AccessFlags flags)
{
CheckNotUseFragment(input);
UseResource(input.handle, flags);
if ((flags & AccessFlags.Read) == AccessFlags.Read)
{
if (m_RenderGraph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation)
{
TextureResource texRes = m_Resources.GetTextureResource(input.handle);
CheckTextureUVOriginIsValid(input.handle, texRes);
texRes.textureUVOrigin = TextureUVOriginSelection.BottomLeft;
}
}
}
public void UseGlobalTexture(int propertyId, AccessFlags flags)
{
var h = m_RenderGraph.GetGlobal(propertyId);
if (h.IsValid())
{
UseTexture(h, flags);
}
else
{
// rose test this path
var name = m_Resources.GetRenderGraphResourceName(h.handle);
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {h.handle.type} at index {h.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.NoGlobalTextureAtPropertyID(propertyId));
}
}
public void UseAllGlobalTextures(bool enable)
{
m_RenderPass.useAllGlobalTextures = enable;
}
public void SetGlobalTextureAfterPass(in TextureHandle input, int propertyId)
{
m_RenderPass.setGlobalsList.Add(ValueTuple.Create(input, propertyId));
}
// Shared validation between SetRenderAttachment/SetRenderAttachmentDepth
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
private void CheckUseFragment(in TextureHandle tex, bool isDepth)
{
if (RenderGraph.enableValidityChecks)
{
// We ignore the version as we don't allow mixing UseTexture/UseFragment between different versions
// even though it should theoretically work (and we might do so in the future) for now we're overly strict.
bool alreadyUsed = false;
//TODO: Check grab textures here and allow if it's grabbed. For now
// SetRenderAttachment()
// UseTexture(grab)
// will work but not the other way around
for (int i = 0; i < m_RenderPass.resourceReadLists[tex.handle.iType].Count; i++)
{
if (m_RenderPass.resourceReadLists[tex.handle.iType][i].index == tex.handle.index)
{
alreadyUsed = true;
break;
}
}
for (int i = 0; i < m_RenderPass.resourceWriteLists[tex.handle.iType].Count; i++)
{
if (m_RenderPass.resourceWriteLists[tex.handle.iType][i].index == tex.handle.index)
{
alreadyUsed = true;
break;
}
}
if (alreadyUsed)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentTextureAlreadyUsed);
}
m_Resources.GetRenderTargetInfo(tex.handle, out var info);
bool isDepthFormat = GraphicsFormatUtility.IsDepthFormat(info.format);
bool formatMismatch = isDepth != isDepthFormat;
if (formatMismatch)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
var errorMsg = isDepth
? RenderGraph.RenderGraphExceptionMessages.UseDepthWithColorFormat(info.format)
: RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentOnDepthTexture;
throw new InvalidOperationException(
$"In pass '{m_RenderPass.name}' when trying to use resource '{name}' " +
$"of type {tex.handle.type} at index {tex.handle.index} - {errorMsg}");
}
if (m_RenderGraph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation)
{
var texResource = m_Resources.GetTextureResource(tex.handle);
TextureResource checkTexResource = null;
for (int i = 0; i < m_RenderPass.fragmentInputMaxIndex + 1; ++i)
{
if (m_RenderPass.fragmentInputAccess[i].textureHandle.IsValid())
{
ref readonly TextureHandle texCheck = ref m_RenderPass.fragmentInputAccess[i].textureHandle;
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "input", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
}
}
}
for (int i = 0; i < m_RenderPass.colorBufferMaxIndex + 1; ++i)
{
if (m_RenderPass.colorBufferAccess[i].textureHandle.IsValid())
{
ref readonly TextureHandle texCheck = ref m_RenderPass.colorBufferAccess[i].textureHandle;
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "render", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
}
}
}
if (!isDepth && m_RenderPass.depthAccess.textureHandle.IsValid())
{
TextureHandle texCheck = m_RenderPass.depthAccess.textureHandle;
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "depth", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
}
}
}
foreach (var globalTex in m_RenderPass.setGlobalsList)
{
if (globalTex.Item1.handle.index == tex.handle.index)
{
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentOnGlobalTexture);
}
}
}
}
public void SetRenderAttachment(TextureHandle tex, int index, AccessFlags flags, int mipLevel, int depthSlice)
{
CheckUseFragment(tex, false);
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
m_RenderPass.SetColorBufferRaw(versionedTextureHandle, index, flags, mipLevel, depthSlice);
}
public void SetInputAttachment(TextureHandle tex, int index, AccessFlags flags, int mipLevel, int depthSlice)
{
CheckFrameBufferFetchEmulationIsSupported(tex);
CheckUseFragment(tex, false);
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
m_RenderPass.SetFragmentInputRaw(versionedTextureHandle, index, flags, mipLevel, depthSlice);
}
public void SetRenderAttachmentDepth(TextureHandle tex, AccessFlags flags, int mipLevel, int depthSlice)
{
CheckUseFragment(tex, true);
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
m_RenderPass.SetDepthBufferRaw(versionedTextureHandle, flags, mipLevel, depthSlice);
}
public TextureHandle SetRandomAccessAttachment(TextureHandle input, int index, AccessFlags flags = AccessFlags.Read)
{
CheckNotUseFragment(input);
ResourceHandle result = UseResource(input.handle, flags);
m_RenderPass.SetRandomWriteResourceRaw(result, index, false, flags);
return input;
}
public void SetShadingRateImageAttachment(in TextureHandle tex)
{
CheckNotUseFragment(tex);
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, AccessFlags.Read));
m_RenderPass.SetShadingRateImageRaw(versionedTextureHandle);
}
public BufferHandle UseBufferRandomAccess(BufferHandle input, int index, AccessFlags flags = AccessFlags.Read)
{
var h = UseBuffer(input, flags);
m_RenderPass.SetRandomWriteResourceRaw(h.handle, index, true, flags);
return input;
}
public BufferHandle UseBufferRandomAccess(BufferHandle input, int index, bool preserveCounterValue, AccessFlags flags = AccessFlags.Read)
{
var h = UseBuffer(input, flags);
m_RenderPass.SetRandomWriteResourceRaw(h.handle, index, preserveCounterValue, flags);
return input;
}
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, ComputeGraphContext> renderFunc) where PassData : class, new()
{
((ComputeRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
}
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, RasterGraphContext> renderFunc) where PassData : class, new()
{
((RasterRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
}
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, UnsafeGraphContext> renderFunc) where PassData : class, new()
{
((UnsafeRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
}
public void UseRendererList(in RendererListHandle input)
{
m_RenderPass.UseRendererList(input);
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
void CheckResource(in ResourceHandle res, bool checkTransientReadWrite = false)
{
if (RenderGraph.enableValidityChecks)
{
if (res.IsValid())
{
int transientIndex = m_Resources.GetRenderGraphResourceTransientIndex(res);
// We have dontCheckTransientReadWrite here because users may want to use UseColorBuffer/UseDepthBuffer API to benefit from render target auto binding. In this case we don't want to raise the error.
if (transientIndex == m_RenderPass.index && checkTransientReadWrite)
{
var name = m_Resources.GetRenderGraphResourceName(res);
Debug.LogError($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " + RenderGraph.RenderGraphExceptionMessages.k_ReadWriteTransient);
}
if (transientIndex != -1 && transientIndex != m_RenderPass.index)
{
var name = m_Resources.GetRenderGraphResourceName(res);
throw new ArgumentException(
$"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " +
RenderGraph.RenderGraphExceptionMessages.UseTransientTextureInWrongPass(transientIndex));
}
}
else
{
var name = m_Resources.GetRenderGraphResourceName(res);
throw new Exception($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidResource);
}
}
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
void CheckFrameBufferFetchEmulationIsSupported(in TextureHandle tex)
{
if (enableValidityChecks)
{
if (!Util.RenderGraphUtils.IsFramebufferFetchEmulationMSAASupportedOnCurrentPlatform())
{
var sourceInfo = m_RenderGraph.GetRenderTargetInfo(tex);
if (sourceInfo.bindMS)
throw new InvalidOperationException($"This API is not supported with MSAA attachments on the current platform: {SystemInfo.graphicsDeviceType}");
}
}
}
public void SetShadingRateFragmentSize(ShadingRateFragmentSize shadingRateFragmentSize)
{
m_RenderPass.SetShadingRateFragmentSize(shadingRateFragmentSize);
}
public void SetShadingRateCombiner(ShadingRateCombinerStage stage, ShadingRateCombiner combiner)
{
m_RenderPass.SetShadingRateCombiner(stage, combiner);
}
public void SetExtendedFeatureFlags(ExtendedFeatureFlags extendedFeatureFlags)
{
m_RenderPass.SetExtendedFeatureFlags(extendedFeatureFlags);
}
}
}
#endif

View File

@@ -0,0 +1,752 @@
#if false
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace UnityEngine.Rendering.RenderGraphModule
{
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
abstract class RenderGraphPass
{
public abstract void Execute(InternalRenderGraphContext renderGraphContext);
public abstract void Release(RenderGraphObjectPool pool);
public abstract bool HasRenderFunc();
public abstract int GetRenderFuncHash();
public string name { get; protected set; }
public int index { get; protected set; }
public RenderGraphPassType type { get; internal set; }
public ProfilingSampler customSampler { get; protected set; }
public bool enableAsyncCompute { get; protected set; }
public bool allowPassCulling { get; protected set; }
public bool allowGlobalState { get; protected set; }
public bool enableFoveatedRasterization { get; protected set; }
public ExtendedFeatureFlags extendedFeatureFlags { get; protected set; }
// Before using the AccessFlags use resourceHandle.isValid()
// to make sure that the data in the colorBuffer/fragmentInput/randomAccessResource buffers are up to date
public TextureAccess depthAccess { get; protected set; }
public TextureAccess[] colorBufferAccess { get; protected set; } = new TextureAccess[RenderGraph.kMaxMRTCount];
public int colorBufferMaxIndex { get; protected set; } = -1;
public bool hasShadingRateImage { get; protected set; }
public TextureAccess shadingRateAccess { get; protected set; }
public bool hasShadingRateStates { get; protected set; }
public ShadingRateFragmentSize shadingRateFragmentSize { get; protected set; }
public ShadingRateCombiner primitiveShadingRateCombiner { get; protected set; }
public ShadingRateCombiner fragmentShadingRateCombiner { get; protected set; }
// Used by native pass compiler only
public TextureAccess[] fragmentInputAccess { get; protected set; } = new TextureAccess[RenderGraph.kMaxMRTCount];
public int fragmentInputMaxIndex { get; protected set; } = -1;
public struct RandomWriteResourceInfo
{
public ResourceHandle h;
public bool preserveCounterValue;
}
// This list can contain both texture and buffer resources based on their binding index.
public RandomWriteResourceInfo[] randomAccessResource { get; protected set; } = new RandomWriteResourceInfo[RenderGraph.kMaxMRTCount];
public int randomAccessResourceMaxIndex { get; protected set; } = -1;
public bool generateDebugData { get; protected set; }
public bool allowRendererListCulling { get; protected set; }
public List<ResourceHandle>[] resourceReadLists = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
public List<ResourceHandle>[] resourceWriteLists = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
public List<ResourceHandle>[] transientResourceList = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
public List<RendererListHandle> usedRendererListList = new List<RendererListHandle>();
public List<ValueTuple<TextureHandle, int>> setGlobalsList = new List<ValueTuple<TextureHandle, int>>();
public bool useAllGlobalTextures;
public List<ResourceHandle> implicitReadsList = new List<ResourceHandle>();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
public RenderGraph.DebugData.PassScriptInfo debugScriptInfo { get; set; }
#endif
public RenderGraphPass()
{
for (int i = 0; i < (int)RenderGraphResourceType.Count; ++i)
{
resourceReadLists[i] = new List<ResourceHandle>();
resourceWriteLists[i] = new List<ResourceHandle>();
transientResourceList[i] = new List<ResourceHandle>();
}
}
public void Clear()
{
name = "";
index = -1;
customSampler = null;
for (int i = 0; i < (int)RenderGraphResourceType.Count; ++i)
{
resourceReadLists[i].Clear();
resourceWriteLists[i].Clear();
transientResourceList[i].Clear();
}
usedRendererListList.Clear();
setGlobalsList.Clear();
useAllGlobalTextures = false;
implicitReadsList.Clear();
enableAsyncCompute = false;
allowPassCulling = true;
allowRendererListCulling = true;
allowGlobalState = false;
enableFoveatedRasterization = false;
generateDebugData = true;
// Invalidate the buffers without clearing them, as it is too costly
// Use IsValid() to make sure that the data in the colorBuffer/fragmentInput/randomAccessResource buffers are up to date
colorBufferMaxIndex = -1;
fragmentInputMaxIndex = -1;
randomAccessResourceMaxIndex = -1;
// We do not need to clear colorBufferAccess and fragmentInputAccess as we have the colorBufferMaxIndex and fragmentInputMaxIndex
// which are reset above so we only clear depthAccess here.
depthAccess = default(TextureAccess);
hasShadingRateImage = false;
hasShadingRateStates = false;
shadingRateFragmentSize = ShadingRateFragmentSize.FragmentSize1x1;
primitiveShadingRateCombiner = ShadingRateCombiner.Keep;
fragmentShadingRateCombiner = ShadingRateCombiner.Keep;
// Invalidate ExtendedFeatureFlags
extendedFeatureFlags = ExtendedFeatureFlags.None;
}
// Check if the pass has any render targets set-up
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasRenderAttachments()
{
return depthAccess.textureHandle.IsValid() || colorBufferAccess[0].textureHandle.IsValid() || colorBufferMaxIndex > 0;
}
// Checks if the resource is involved in this pass
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsTransient(in ResourceHandle res)
{
// Versioning doesn't matter much for transient resources as they are only used within a single pass
for (int i = 0; i < transientResourceList[res.iType].Count; i++)
{
if (transientResourceList[res.iType][i].index == res.index)
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsWritten(in ResourceHandle res)
{
// You can only ever write to the latest version so we ignore it when looking in the list
for (int i = 0; i < resourceWriteLists[res.iType].Count; i++)
{
if (resourceWriteLists[res.iType][i].index == res.index)
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsRead(in ResourceHandle res)
{
if (res.IsVersioned)
{
return resourceReadLists[res.iType].Contains(res);
}
else
{
// Just look if we are reading any version of this texture.
// Note that in theory this pass could read from several versions of the same texture
// e.g. ColorBuffer,v3 and ColorBuffer,v5 so this check is always conservative
for (int i = 0; i < resourceReadLists[res.iType].Count; i++)
{
if (resourceReadLists[res.iType][i].index == res.index)
{
return true;
}
}
return false;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsAttachment(in TextureHandle res)
{
// We ignore the version when checking, if any version is used it is considered a match
if (depthAccess.textureHandle.IsValid() && depthAccess.textureHandle.handle.index == res.handle.index) return true;
for (int i = 0; i < colorBufferAccess.Length; i++)
{
if (colorBufferAccess[i].textureHandle.IsValid() && colorBufferAccess[i].textureHandle.handle.index == res.handle.index) return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddResourceWrite(in ResourceHandle res)
{
resourceWriteLists[res.iType].Add(res);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddResourceRead(in ResourceHandle res)
{
resourceReadLists[res.iType].Add(res);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTransientResource(in ResourceHandle res)
{
transientResourceList[res.iType].Add(res);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UseRendererList(in RendererListHandle rendererList)
{
usedRendererListList.Add(rendererList);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnableAsyncCompute(bool value)
{
enableAsyncCompute = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllowPassCulling(bool value)
{
allowPassCulling = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnableFoveatedRasterization(bool value)
{
enableFoveatedRasterization = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllowRendererListCulling(bool value)
{
allowRendererListCulling = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllowGlobalState(bool value)
{
allowGlobalState = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GenerateDebugData(bool value)
{
generateDebugData = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetColorBuffer(in TextureHandle resource, int index)
{
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
colorBufferMaxIndex = Math.Max(colorBufferMaxIndex, index);
colorBufferAccess[index] = new TextureAccess(colorBufferAccess[index], resource);
AddResourceWrite(resource.handle);
}
// Sets up the color buffer for this pass but not any resource Read/Writes for it
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetColorBufferRaw(in TextureHandle resource, int index, AccessFlags accessFlags, int mipLevel, int depthSlice)
{
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
if (colorBufferAccess[index].textureHandle.handle.Equals(resource.handle) || !colorBufferAccess[index].textureHandle.IsValid())
{
colorBufferMaxIndex = Math.Max(colorBufferMaxIndex, index);
colorBufferAccess[index] = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
}
else
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
throw new InvalidOperationException(
$"In pass '{name}' when trying to call SetRenderAttachment with resource of type {resource.handle.type} at index {index} - " +
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneResourceForMRTIndex);
#endif
}
}
// Sets up the color buffer for this pass but not any resource Read/Writes for it
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetFragmentInputRaw(in TextureHandle resource, int index, AccessFlags accessFlags, int mipLevel, int depthSlice)
{
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
if (fragmentInputAccess[index].textureHandle.handle.Equals(resource.handle) || !fragmentInputAccess[index].textureHandle.IsValid())
{
fragmentInputMaxIndex = Math.Max(fragmentInputMaxIndex, index);
fragmentInputAccess[index] = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
}
else
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
throw new InvalidOperationException(
$"In pass '{name}' when trying to call SetInputAttachment with resource of type {resource.handle.type} at index {index} - " +
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneTextureForFragInputIndex);
#endif
}
}
// Sets up the color buffer for this pass but not any resource Read/Writes for it
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetRandomWriteResourceRaw(in ResourceHandle resource, int index, bool preserveCounterValue, AccessFlags accessFlags)
{
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
if (randomAccessResource[index].h.Equals(resource) || !randomAccessResource[index].h.IsValid())
{
randomAccessResourceMaxIndex = Math.Max(randomAccessResourceMaxIndex, index);
ref var info = ref randomAccessResource[index];
info.h = resource;
info.preserveCounterValue = preserveCounterValue;
}
else
{
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
throw new InvalidOperationException(
$"In pass '{name}' when trying to call SetRandomAccessAttachment/UseBufferRandomAccess with resource of type {resource.type} at index {index} - " +
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneTextureRandomWriteInputIndex);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetDepthBuffer(in TextureHandle resource, DepthAccess flags)
{
depthAccess = new TextureAccess(resource, (AccessFlags)flags, 0, 0);
if ((flags & DepthAccess.Read) != 0)
AddResourceRead(resource.handle);
if ((flags & DepthAccess.Write) != 0)
AddResourceWrite(resource.handle);
}
// Sets up the depth buffer for this pass but not any resource Read/Writes for it
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetDepthBufferRaw(in TextureHandle resource, AccessFlags accessFlags, int mipLevel, int depthSlice)
{
// If no depth buffer yet or if it is the same one as the previous one, allow the call otherwise log an error.
if (depthAccess.textureHandle.handle.Equals(resource.handle) || !depthAccess.textureHandle.IsValid())
{
depthAccess = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
else
{
throw new InvalidOperationException(
$"In pass '{name}' when trying to call SetRenderAttachmentDepth with resource of type {resource.handle.type} at index {index} - " +
RenderGraph.RenderGraphExceptionMessages.k_MultipleDepthTextures);
}
#endif
}
// Here we want to keep computation to a minimum and only hash what will influence NRP compiler: Pass merging, load/store actions etc.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ComputeTextureHash(ref HashFNV1A32 generator, in ResourceHandle handle, RenderGraphResourceRegistry resources)
{
if (handle.index == 0)
return;
if (resources.IsRenderGraphResourceImported(handle))
{
var res = resources.GetTextureResource(handle);
var graphicsResource = res.graphicsResource;
ref readonly var desc = ref res.desc;
var externalTexture = graphicsResource.externalTexture;
if (externalTexture != null) // External texture
{
generator.Append((int) externalTexture.graphicsFormat);
generator.Append((int) externalTexture.dimension);
generator.Append(externalTexture.width);
generator.Append(externalTexture.height);
if (externalTexture is RenderTexture externalRT)
generator.Append(externalRT.antiAliasing);
}
else if (graphicsResource.rt != null) // Regular RTHandle
{
var rt = graphicsResource.rt;
generator.Append((int) rt.graphicsFormat);
generator.Append((int) rt.dimension);
generator.Append(rt.antiAliasing);
if (graphicsResource.useScaling)
if (graphicsResource.scaleFunc != null)
generator.Append(DelegateHashCodeUtils.GetFuncHashCode(graphicsResource.scaleFunc));
else
generator.Append(graphicsResource.scaleFactor);
else
{
generator.Append(rt.width);
generator.Append(rt.height);
}
}
else if (graphicsResource.nameID != default) // External RTI
{
// The only info we have is from the provided desc upon importing.
generator.Append((int) desc.format);
generator.Append((int) desc.dimension);
generator.Append((int) desc.msaaSamples);
generator.Append(desc.width);
generator.Append(desc.height);
}
// Add the clear/discard buffer flags to the hash (used in all the cases above)
generator.Append(desc.clearBuffer);
generator.Append(desc.discardBuffer);
}
else
{
ref readonly var desc = ref resources.GetTextureResourceDesc(handle);
generator.Append((int) desc.format);
generator.Append((int) desc.dimension);
generator.Append((int) desc.msaaSamples);
generator.Append(desc.clearBuffer);
generator.Append(desc.discardBuffer);
switch (desc.sizeMode)
{
case TextureSizeMode.Explicit:
generator.Append(desc.width);
generator.Append(desc.height);
break;
case TextureSizeMode.Scale:
generator.Append(desc.scale);
break;
case TextureSizeMode.Functor:
generator.Append(DelegateHashCodeUtils.GetFuncHashCode(desc.func));
break;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void ComputeHashForTextureAccess(ref HashFNV1A32 generator, in ResourceHandle handle, in TextureAccess textureAccess)
{
generator.Append(handle.index);
generator.Append((int) textureAccess.flags);
generator.Append(textureAccess.mipLevel);
generator.Append(textureAccess.depthSlice);
}
// This function is performance sensitive.
// Avoid mass function calls to get the hashCode and compute locally instead.
public void ComputeHash(ref HashFNV1A32 generator, RenderGraphResourceRegistry resources)
{
generator.Append((int) type);
generator.Append(enableAsyncCompute);
generator.Append(allowPassCulling);
generator.Append(allowGlobalState);
generator.Append(enableFoveatedRasterization);
generator.Append(extendedFeatureFlags);
var depthHandle = depthAccess.textureHandle.handle;
if (depthHandle.IsValid())
{
ComputeTextureHash(ref generator, depthHandle, resources);
ComputeHashForTextureAccess(ref generator, depthHandle, depthAccess);
}
for (int i = 0; i < colorBufferMaxIndex + 1; ++i)
{
var colorBufferAccessElement = colorBufferAccess[i];
var handle = colorBufferAccessElement.textureHandle.handle;
if (!handle.IsValid())
continue;
ComputeTextureHash(ref generator, handle, resources);
ComputeHashForTextureAccess(ref generator, handle, colorBufferAccessElement);
}
generator.Append(colorBufferMaxIndex);
generator.Append(hasShadingRateImage);
if (hasShadingRateImage)
{
var handle = shadingRateAccess.textureHandle.handle;
if (handle.IsValid())
{
ComputeTextureHash(ref generator, handle, resources);
ComputeHashForTextureAccess(ref generator, handle, shadingRateAccess);
}
}
generator.Append(hasShadingRateStates);
generator.Append((int)shadingRateFragmentSize);
generator.Append((int)primitiveShadingRateCombiner);
generator.Append((int)fragmentShadingRateCombiner);
for (int i = 0; i < fragmentInputMaxIndex + 1; ++i)
{
var fragmentInputAccessElement = fragmentInputAccess[i];
var handle = fragmentInputAccessElement.textureHandle.handle;
if (!handle.IsValid())
continue;
ComputeTextureHash(ref generator, handle, resources);
ComputeHashForTextureAccess(ref generator, handle, fragmentInputAccessElement);
}
for (int i = 0; i < randomAccessResourceMaxIndex + 1; ++i)
{
var rar = randomAccessResource[i];
if (!rar.h.IsValid())
continue;
generator.Append(rar.h.index);
generator.Append(rar.preserveCounterValue);
}
generator.Append(randomAccessResourceMaxIndex);
generator.Append(fragmentInputMaxIndex);
generator.Append(generateDebugData);
generator.Append(allowRendererListCulling);
for (int resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
{
var resourceReads = resourceReadLists[resType];
var resourceReadsCount = resourceReads.Count;
for (int i = 0; i < resourceReadsCount; ++i)
generator.Append(resourceReads[i].index);
var resourceWrites = resourceWriteLists[resType];
var resourceWritesCount = resourceWrites.Count;
for (int i = 0; i < resourceWritesCount; ++i)
generator.Append(resourceWrites[i].index);
var resourceTransient = transientResourceList[resType];
var resourceTransientCount = resourceTransient.Count;
for (int i = 0; i < resourceTransientCount; ++i)
generator.Append(resourceTransient[i].index);
}
var usedRendererListListCount = usedRendererListList.Count;
for (int i = 0; i < usedRendererListListCount; ++i)
generator.Append(usedRendererListList[i].handle);
var setGlobalsListCount = setGlobalsList.Count;
for (int i = 0; i < setGlobalsListCount; ++i)
{
var global = setGlobalsList[i];
generator.Append(global.Item1.handle.index);
generator.Append(global.Item2);
}
generator.Append(useAllGlobalTextures);
var implicitReadsListCount = implicitReadsList.Count;
for (int i = 0; i < implicitReadsListCount; ++i)
generator.Append(implicitReadsList[i].index);
generator.Append(GetRenderFuncHash());
}
public void SetShadingRateImageRaw(in TextureHandle shadingRateImage)
{
if (ShadingRateInfo.supportsPerImageTile)
{
hasShadingRateImage = true;
// shading rate image access flag is always read, only 1 mip and 1 slice
shadingRateAccess = new TextureAccess(shadingRateImage, AccessFlags.Read, 0, 0);
}
}
public void SetShadingRateImage(in TextureHandle shadingRateImage, AccessFlags accessFlags, int mipLevel, int depthSlice)
{
if (ShadingRateInfo.supportsPerImageTile)
{
hasShadingRateImage = true;
shadingRateAccess = new TextureAccess(shadingRateImage, accessFlags, mipLevel, depthSlice);
AddResourceRead(shadingRateAccess.textureHandle.handle);
}
}
public void SetShadingRateFragmentSize(ShadingRateFragmentSize shadingRateFragmentSize)
{
if (ShadingRateInfo.supportsPerDrawCall)
{
hasShadingRateStates = true;
this.shadingRateFragmentSize = shadingRateFragmentSize;
}
}
public void SetShadingRateCombiner(ShadingRateCombinerStage stage, ShadingRateCombiner combiner)
{
if (ShadingRateInfo.supportsPerImageTile)
{
switch (stage)
{
case ShadingRateCombinerStage.Primitive:
hasShadingRateStates = true;
primitiveShadingRateCombiner = combiner;
break;
case ShadingRateCombinerStage.Fragment:
hasShadingRateStates = true;
fragmentShadingRateCombiner = combiner;
break;
}
}
}
public void SetExtendedFeatureFlags(ExtendedFeatureFlags value)
{
extendedFeatureFlags |= value;
}
}
// This used to have an extra generic argument 'RenderGraphContext' abstracting the context and avoiding
// the RenderGraphPass/ComputeRenderGraphPass/RasterRenderGraphPass/UnsafeRenderGraphPass classes below
// but this confuses IL2CPP and causes garbage when boxing the context created (even though they are structs)
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
internal abstract class BaseRenderGraphPass<PassData, TRenderGraphContext> : RenderGraphPass
where PassData : class, new()
{
internal PassData data;
internal BaseRenderFunc<PassData, TRenderGraphContext> renderFunc;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Initialize(int passIndex, PassData passData, string passName, RenderGraphPassType passType, ProfilingSampler sampler)
{
Clear();
index = passIndex;
data = passData;
name = passName;
type = passType;
customSampler = sampler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Release(RenderGraphObjectPool pool)
{
pool.Release(data);
data = null;
renderFunc = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool HasRenderFunc()
{
return renderFunc != null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetRenderFuncHash()
{
return renderFunc != null ? DelegateHashCodeUtils.GetFuncHashCode(renderFunc) : 0;
}
}
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
[Obsolete("RenderGraphPass is deprecated, use RasterRenderGraphPass/ComputeRenderGraphPass/UnsafeRenderGraphPass instead.")]
internal sealed class RenderGraphPass<PassData> : BaseRenderGraphPass<PassData, RenderGraphContext>
where PassData : class, new()
{
internal static RenderGraphContext c = new RenderGraphContext();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Execute(InternalRenderGraphContext renderGraphContext)
{
c.FromInternalContext(renderGraphContext);
renderFunc(data, c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Release(RenderGraphObjectPool pool)
{
base.Release(pool);
// We need to do the release from here because we need the final type.
pool.Release(this);
}
}
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
internal sealed class ComputeRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, ComputeGraphContext>
where PassData : class, new()
{
internal static ComputeGraphContext c = new ComputeGraphContext();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Execute(InternalRenderGraphContext renderGraphContext)
{
c.FromInternalContext(renderGraphContext);
renderFunc(data, c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Release(RenderGraphObjectPool pool)
{
base.Release(pool);
// We need to do the release from here because we need the final type.
pool.Release(this);
}
}
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
internal sealed class RasterRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, RasterGraphContext>
where PassData : class, new()
{
internal static RasterGraphContext c = new RasterGraphContext();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Execute(InternalRenderGraphContext renderGraphContext)
{
c.FromInternalContext(renderGraphContext);
renderFunc(data, c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Release(RenderGraphObjectPool pool)
{
base.Release(pool);
// We need to do the release from here because we need the final type.
pool.Release(this);
}
}
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
internal sealed class UnsafeRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, UnsafeGraphContext>
where PassData : class, new()
{
internal static UnsafeGraphContext c = new UnsafeGraphContext();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Execute(InternalRenderGraphContext renderGraphContext)
{
c.FromInternalContext(renderGraphContext);
renderFunc(data, c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Release(RenderGraphObjectPool pool)
{
base.Release(pool);
// We need to do the release from here because we need the final type.
pool.Release(this);
}
}
}
#endif