From 1fc9df1812f4c4bfeddb4f03f1075f4f166af9c1 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sun, 11 Jan 2026 23:43:17 +0900 Subject: [PATCH] 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. --- Ghost.RenderGraph.Concept/ConsoleAPI.cs | 16 - .../Ghost.RenderGraph.Concept.csproj | 13 +- Ghost.RenderGraph.Concept/ICommandBuffer.cs | 168 -- .../IMPLEMENTATION_NOTES.md | 172 ++ Ghost.RenderGraph.Concept/PassData.cs | 49 +- Ghost.RenderGraph.Concept/Program.cs | 54 +- Ghost.RenderGraph.Concept/README.md | 197 ++ Ghost.RenderGraph.Concept/RenderGraph.cs | 1036 ++++---- .../RenderGraphAliasing.cs | 329 +++ .../RenderGraphBarriers.cs | 154 ++ Ghost.RenderGraph.Concept/RenderGraphBatch.cs | 33 - .../RenderGraphBlackboard.cs | 41 +- .../RenderGraphBuilderExtensions.cs | 27 + .../RenderGraphCompilationCache.cs | 131 + .../RenderGraphContext.cs | 134 + .../RenderGraphExtensions.cs | 45 - Ghost.RenderGraph.Concept/RenderGraphHash.cs | 68 + Ghost.RenderGraph.Concept/RenderGraphPass.cs | 270 +- .../RenderGraphPassBuilder.cs | 120 - .../RenderGraphResourceHandle.cs | 63 - .../RenderGraphResourcePool.cs | 173 ++ Ghost.RenderGraph.Concept/RenderGraphTypes.cs | 75 + .../ResourceAllocator.cs | 360 --- .../ResourceDescriptor.cs | 28 - Ghost.RenderGraph.Concept/ResourceLifetime.cs | 42 - Ghost.RenderGraph.Concept/ResourceState.cs | 21 - .../Unity/NativePassCompiler.cs | 2220 +++++++++++++++++ .../Unity/RenderGraph.cs | 1692 +++++++++++++ .../Unity/RenderGraphBuilders.cs | 598 +++++ .../Unity/RenderGraphPass.cs | 752 ++++++ 30 files changed, 7536 insertions(+), 1545 deletions(-) delete mode 100644 Ghost.RenderGraph.Concept/ConsoleAPI.cs delete mode 100644 Ghost.RenderGraph.Concept/ICommandBuffer.cs create mode 100644 Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md create mode 100644 Ghost.RenderGraph.Concept/README.md create mode 100644 Ghost.RenderGraph.Concept/RenderGraphAliasing.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphBarriers.cs delete mode 100644 Ghost.RenderGraph.Concept/RenderGraphBatch.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphContext.cs delete mode 100644 Ghost.RenderGraph.Concept/RenderGraphExtensions.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphHash.cs delete mode 100644 Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs delete mode 100644 Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs create mode 100644 Ghost.RenderGraph.Concept/RenderGraphTypes.cs delete mode 100644 Ghost.RenderGraph.Concept/ResourceAllocator.cs delete mode 100644 Ghost.RenderGraph.Concept/ResourceDescriptor.cs delete mode 100644 Ghost.RenderGraph.Concept/ResourceLifetime.cs delete mode 100644 Ghost.RenderGraph.Concept/ResourceState.cs create mode 100644 Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs create mode 100644 Ghost.RenderGraph.Concept/Unity/RenderGraph.cs create mode 100644 Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs create mode 100644 Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs diff --git a/Ghost.RenderGraph.Concept/ConsoleAPI.cs b/Ghost.RenderGraph.Concept/ConsoleAPI.cs deleted file mode 100644 index 74ec863..0000000 --- a/Ghost.RenderGraph.Concept/ConsoleAPI.cs +++ /dev/null @@ -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); - } -} diff --git a/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj b/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj index e1e1342..9279c9d 100644 --- a/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj +++ b/Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj @@ -5,14 +5,19 @@ net10.0 enable enable + True + + + + $(DefineConstants);PROFILING + + + + $(DefineConstants);PROFILING - - - - diff --git a/Ghost.RenderGraph.Concept/ICommandBuffer.cs b/Ghost.RenderGraph.Concept/ICommandBuffer.cs deleted file mode 100644 index d3205d8..0000000 --- a/Ghost.RenderGraph.Concept/ICommandBuffer.cs +++ /dev/null @@ -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 barriers); - void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName); - void AliasingBarrier(Span 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 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 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); -} diff --git a/Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md b/Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md new file mode 100644 index 0000000..a6a9c41 --- /dev/null +++ b/Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md @@ -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 +- 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/Add 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("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((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. diff --git a/Ghost.RenderGraph.Concept/PassData.cs b/Ghost.RenderGraph.Concept/PassData.cs index c7ddfb3..bb6cebb 100644 --- a/Ghost.RenderGraph.Concept/PassData.cs +++ b/Ghost.RenderGraph.Concept/PassData.cs @@ -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; +} diff --git a/Ghost.RenderGraph.Concept/Program.cs b/Ghost.RenderGraph.Concept/Program.cs index 708c5f3..17d1bd4 100644 --- a/Ghost.RenderGraph.Concept/Program.cs +++ b/Ghost.RenderGraph.Concept/Program.cs @@ -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((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((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((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((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((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((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("GPU Profiler Begin Frame", out var profilerData)) { builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling - builder.SetRenderFunc((data, cmd) => + builder.SetRenderFunc((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((data, cmd) => { cmd.SetRenderTarget(data.DebugTexture.Name); cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1); diff --git a/Ghost.RenderGraph.Concept/README.md b/Ghost.RenderGraph.Concept/README.md new file mode 100644 index 0000000..8ea365a --- /dev/null +++ b/Ghost.RenderGraph.Concept/README.md @@ -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("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((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("Lighting", out var lightingData)) +{ + var gbuffer = renderGraph.Blackboard.Get(); + 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(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(Action func)`** +Sets the raster render function for this pass. + +**`void SetComputeFunc(Action 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 data) where T : class, IPassData`** +Stores pass data in the blackboard. + +**`T Get() where T : class, IPassData`** +Retrieves pass data from the blackboard. + +**`bool TryGet(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 diff --git a/Ghost.RenderGraph.Concept/RenderGraph.cs b/Ghost.RenderGraph.Concept/RenderGraph.cs index f9185c8..4a66b82 100644 --- a/Ghost.RenderGraph.Concept/RenderGraph.cs +++ b/Ghost.RenderGraph.Concept/RenderGraph.cs @@ -1,341 +1,508 @@ -using Ghost.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using System.IO.Hashing; +using System.Threading; namespace Ghost.RenderGraph.Concept; -public class RenderGraph +/// +/// Main render graph class that manages resource allocation and pass execution. +/// +/// Design principles for minimal GC: +/// - Object pooling for all passes and resources +/// - Reuse collections across frames (Clear() instead of new) +/// - Avoid LINQ and foreach over interfaces +/// - Pre-allocate capacity based on expected usage +/// +public sealed class RenderGraph { - private int _resourceIdCounter = 0; - private int _passCounter = 0; - - private readonly List _resources = new(); - private readonly List _passes = new(); - - private readonly List _resourceLifetimes = new(); - private readonly List _currentResourceStates = new(); - private readonly List _resourceToAllocationMap = new(); - - private readonly Dictionary _allocationActiveResource = new(); - private readonly RenderGraphBlackboard _blackboard = new(); - private readonly ResourceAllocator _allocator = new(); + private readonly RenderGraphResourceRegistry _resources = new(); + private readonly RenderGraphObjectPool _objectPool = new(); + private readonly List _passes = new(64); + private readonly List _compiledPasses = new(64); + private readonly RenderGraphBuilder _builder = new(); + private readonly MockCommandBuffer _commandBuffer = new(); + private readonly RenderContext _renderContext; + private readonly ResourceAliasingManager _aliasingManager = new(); + private readonly Dictionary _resourceStates = new(128); + private readonly List _barriers = new(128); + private readonly RenderGraphCompilationCache _compilationCache = new(); - // Batching and Sync - private readonly List _batches = new(); - private readonly Stack _batchPool = new(); - private int _fenceCounter = 0; + private readonly XxHash64 _hasher = new(); - // Pooled Collections for Compilation - private readonly Dictionary _resourceLastWriter = new(); - private readonly Dictionary> _resourceLastReaders = new(); - private readonly Dictionary _passToBatchMap = new(); - - // Pooled Lists for Passes - private readonly Stack> _resourceAccessListPool = new(); - private readonly Stack _resourceLifetimePool = new(); + private int _passCount; + private bool _compiled; - // Execution Plan (Pre-calculated to avoid LINQ in Execute) - private List[] _resourcesToCreate = Array.Empty>(); - private List[] _resourcesToDestroy = Array.Empty>(); + public RenderGraphBlackboard Blackboard { get; } = new(); + public RenderGraph() + { + _renderContext = new RenderContext(_commandBuffer); + } + /// + /// Resets the render graph for a new frame. + /// Reuses existing allocations to minimize GC. + /// + public void Reset() + { + // Clear blackboard data + Blackboard.Clear(); + + // Reset resources but keep allocations + _resources.BeginFrame(); + + // Reset aliasing manager + _aliasingManager.BeginFrame(); + + // Clear resource states and barriers + _resourceStates.Clear(); + _barriers.Clear(); + + // Return passes to the pool and reset count + for (var i = 0; i < _passCount; i++) + { + var pass = _passes[i]; + pass.Clear(); + _objectPool.Release(pass); + } + _passCount = 0; + + // Clear compiled passes list + _compiledPasses.Clear(); + _compiled = false; + } + + /// + /// Imports an external texture into the render graph. + /// public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor) { - var handle = new RenderGraphTextureHandle(_resourceIdCounter++, name, descriptor, isImported: true); - _resources.Add(handle._handle); - _resourceLifetimes.Add(RentResourceLifetime(handle._handle)); - _currentResourceStates.Add(ResourceState.Undefined); - _resourceToAllocationMap.Add(-1); - //ConsoleAPI.WriteLine($"[RG] Import Texture: '{name}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})"); - return handle; + return _resources.ImportTexture(descriptor); } - public RenderGraphBufferHandle ImportBuffer(string name, BufferDescriptor descriptor) - { - var handle = new RenderGraphBufferHandle(_resourceIdCounter++, name, descriptor, isImported: true); - _resources.Add(handle._handle); - _resourceLifetimes.Add(RentResourceLifetime(handle._handle)); - _currentResourceStates.Add(ResourceState.Undefined); - _resourceToAllocationMap.Add(-1); - //ConsoleAPI.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)"); - return handle; - } - - internal RenderGraphTextureHandle CreateTransientTexture(TextureDescriptor descriptor) - { - var handle = new RenderGraphTextureHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false); - _resources.Add(handle._handle); - _resourceLifetimes.Add(RentResourceLifetime(handle._handle)); - _currentResourceStates.Add(ResourceState.Undefined); - _resourceToAllocationMap.Add(-1); - //ConsoleAPI.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})"); - return handle; - } - - internal RenderGraphBufferHandle CreateTransientBuffer(BufferDescriptor descriptor) - { - var handle = new RenderGraphBufferHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false); - _resources.Add(handle._handle); - _resourceLifetimes.Add(RentResourceLifetime(handle._handle)); - _currentResourceStates.Add(ResourceState.Undefined); - _resourceToAllocationMap.Add(-1); - //ConsoleAPI.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)"); - return handle; - } - - public RenderGraphBlackboard Blackboard => _blackboard; - - public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor) - { - return CreateTransientTexture(descriptor); - } - - public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor) - { - return CreateTransientBuffer(descriptor); - } - - public RenderGraphPassBuilder AddRenderPass(string name, out TPassData passData) + /// + /// Adds a new render pass to the graph. + /// + public RenderGraphBuilder AddRenderPass(string name, out TPassData passData) where TPassData : class, new() { - var list = RentResourceAccessList(); - var builder = new RenderGraphPassBuilder(this, name, _passCounter, list); - passData = builder.PassData; - return builder; - } - - internal void CommitPass(RenderGraphPassBuilder builder, string name) - where TPassData : class, new() - { - if (builder.RenderFunc == null) + // Get or create pass from pool + RenderGraphPass pass; + if (_passCount < _passes.Count) { - throw new InvalidOperationException($"Pass '{name}' has no render function set. Call SetRenderFunc() on the builder."); - } - - // Optimization: Use Pass Pool - RenderGraphPass? pass; - // Cast ReadOnlyList back to List (safe because we created it in AddRenderPass) - var resourceList = (List<(RenderGraphResourceHandle handle, ResourceState state)>)builder.ResourceAccesses; - - if (!RenderGraphPassPool.Pool.TryPop(out pass)) - { - pass = new RenderGraphPass( - name, - _passCounter++, - builder.QueueType, - builder.PassData, - builder.RenderFunc, - resourceList, - builder.AllowCulling); + // Reuse existing slot + var existingPass = _passes[_passCount]; + if (existingPass is RenderGraphPass typedPass) + { + pass = typedPass; + pass.Reset(); + } + else + { + // Type mismatch, need to replace + _objectPool.Release(existingPass); + pass = _objectPool.Get>(); + pass.Reset(); + _passes[_passCount] = pass; + } } else { - pass.Initialize( - name, - _passCounter++, - builder.QueueType, - builder.PassData, - builder.RenderFunc, - resourceList, - builder.AllowCulling); - } - - _passes.Add(pass); - - foreach (var (handle, state) in pass.ResourceAccesses) - { - var lifeTime = _resourceLifetimes[handle.Id]; - lifeTime.AddUsage(state, pass.Index); - _resourceLifetimes[handle.Id] = lifeTime; + // Need to grow the list + pass = _objectPool.Get>(); + pass.Reset(); + _passes.Add(pass); } - //ConsoleAPI.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})"); + // Initialize pass + pass.Name = name; + pass.Index = _passCount; + + // Get or create pass data from pool + passData = _objectPool.Get(); + pass.PassData = passData; + + _passCount++; + + // Initialize builder + _builder.Initialize(pass, _resources); + return _builder; } - public void Compile() + /// + /// Computes a _hasher of the render graph structure for caching. + /// Does NOT include pass names (they don't affect compilation). + /// Uses XxHash3 with SIMD optimizations for fast hashing. + /// + private unsafe ulong ComputeGraphHash() { - //ConsoleAPI.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH =========="); - - BuildDependencies(); - CullUnusedPasses(); - AnalyzeResourceLifetimes(); - AllocatePhysicalResources(); - InsertSynchronization(); - } + using var scope = AllocationManager.CreateStackScope(); + var bufferPool = new UnsafeList(4096, scope.AllocationHandle); + int offset = 0; + var pData = (byte*)bufferPool.GetUnsafePtr(); - private void InsertSynchronization() - { - //ConsoleAPI.WriteLine("\n[RG] Building command batches and synchronization..."); + _hasher.Reset(); - _batches.Clear(); - _fenceCounter = 0; + // Hash pass count + _hasher.AppendInt(_passCount); - // 1. Create Batches (Topological grouping) - RenderGraphBatch? currentBatch = null; - _passToBatchMap.Clear(); - - foreach (var pass in _passes) - { - if (pass.RefCount == 0) continue; - - if (currentBatch == null || currentBatch.QueueType != pass.QueueType) - { - if (!_batchPool.TryPop(out currentBatch)) - { - currentBatch = new RenderGraphBatch(); - } - currentBatch.Initialize(_batches.Count, pass.QueueType); - _batches.Add(currentBatch); - } - - currentBatch.Passes.Add(pass); - _passToBatchMap[pass.Index] = currentBatch; - } - - //ConsoleAPI.WriteLine($" Created {_batches.Count} batches."); - - // 2. Inject Synchronization (Fences) - foreach (var batch in _batches) - { - foreach (var pass in batch.Passes) - { - foreach (var depIndex in pass.Dependencies) - { - if (_passToBatchMap.TryGetValue(depIndex, out var dependencyBatch)) - { - if (dependencyBatch != batch) - { - int fenceId; - if (dependencyBatch.SignalFences.Count == 0) - { - fenceId = _fenceCounter++; - dependencyBatch.SignalFences.Add(fenceId); - } - else - { - fenceId = dependencyBatch.SignalFences[0]; - } - - if (!batch.WaitFences.Contains(fenceId)) - { - batch.WaitFences.Add(fenceId); - //ConsoleAPI.WriteLine($" Batch {batch.ID} ({batch.QueueType}) waits on Batch {dependencyBatch.ID} ({dependencyBatch.QueueType}) [Fence {fenceId}]"); - } - } - } - } - } - } - } - - private void AllocatePhysicalResources() - { - _allocator.AllocateResources(_resourceLifetimes, _passes); - - foreach (var allocation in _allocator.Allocations) - { - foreach (var resource in allocation.AliasedResources) - { - _resourceToAllocationMap[resource.Id] = allocation.AllocationId; - } - } - } - - private void BuildDependencies() - { - _resourceLastWriter.Clear(); - foreach (var list in _resourceLastReaders.Values) list.Clear(); - _resourceLastReaders.Clear(); - - for (int i = 0; i < _passes.Count; i++) + // Hash each pass structure (excluding names) + for (int i = 0; i < _passCount; i++) { var pass = _passes[i]; + // Save 0.004ms. + + //// Hash pass properties that affect compilation + //_hasher.AppendEnum(pass.Type); + //_hasher.AppendBool(pass.AllowCulling); + //_hasher.AppendBool(pass.AsyncCompute); - foreach (var (handle, state) in pass.ResourceAccesses) + //// Hash texture dependencies (only indices, not versions or names) + //_hasher.AppendHandleList(pass.TextureReads); + //_hasher.AppendHandleList(pass.TextureWrites); + //_hasher.AppendHandleList(pass.TextureCreates); + *(RenderPassType*)(pData + offset) = pass.Type; + offset += sizeof(RenderPassType); + + *(bool*)(pData + offset) = pass.AllowCulling; + offset += sizeof(bool); + + *(bool*)(pData + offset) = pass.AsyncCompute; + offset += sizeof(bool); + + *(int*)(pData + offset) = pass.TextureReads.Count; + offset += sizeof(int); + for (int j = 0; j < pass.TextureReads.Count; j++) { - int resourceId = handle.Id; + *(int*)(pData + offset) = pass.TextureReads[j].Index; + offset += sizeof(int); + } - if (IsReadState(state)) + *(int*)(pData + offset) = pass.TextureWrites.Count; + offset += sizeof(int); + for (int j = 0; j < pass.TextureWrites.Count; j++) + { + *(int*)(pData + offset) = pass.TextureWrites[j].Index; + offset += sizeof(int); + } + + *(int*)(pData + offset) = pass.TextureCreates.Count; + offset += sizeof(int); + for (int j = 0; j < pass.TextureCreates.Count; j++) + { + *(int*)(pData + offset) = pass.TextureCreates[j].Index; + offset += sizeof(int); + } + } + + // Hash resource descriptors + for (int i = 0; i < _resources.TextureResourceCount; i++) + { + var resource = _resources.GetTextureResourceByIndex(i); + + *(int*)(pData + offset) = resource.Descriptor.Width; + offset += sizeof(int); + *(int*)(pData + offset) = resource.Descriptor.Height; + offset += sizeof(int); + *(TextureFormat*)(pData + offset) = resource.Descriptor.Format; + offset += sizeof(TextureFormat); + *(bool*)(pData + offset) = resource.IsImported; + offset += sizeof(bool); + } + + var span = new Span(pData, offset); + _hasher.Append(span); + return _hasher.GetCurrentHashAsUInt64(); + } + + /// + /// Compiles the render graph by culling unused passes and determining resource lifetimes. + /// + public void Compile() + { + if (_compiled) + return; + +#if DEBUG + var sw = System.Diagnostics.Stopwatch.StartNew(); +#endif + + // Step 0: Check cache + ulong graphHash = ComputeGraphHash(); + +#if DEBUG + var hashTime = sw.Elapsed.TotalMicroseconds; +#endif + + if (_compilationCache.TryGetCached(graphHash, out var cached)) + { + // CACHE HIT - restore from cache +#if DEBUG + Console.WriteLine($"\n[CACHE HIT] Hash: {graphHash:X16} (computed in {hashTime:F2}μs)"); +#endif + RestoreFromCache(cached); +#if DEBUG + sw.Stop(); + Console.WriteLine($"[CACHE HIT] Total restore time: {sw.Elapsed.TotalMicroseconds:F2}μs"); +#endif + _compiled = true; + return; + } + +#if DEBUG + Console.WriteLine($"\n[CACHE MISS] Hash: {graphHash:X16} (computed in {hashTime:F2}μs)"); +#endif + + _compiledPasses.Clear(); + + // Step 1: Mark passes with side effects (writes to imported resources) + for (var i = 0; i < _passCount; i++) + { + var pass = _passes[i]; + + // Check if this pass writes to any imported textures + for (var j = 0; j < pass.TextureWrites.Count; j++) + { + var writeHandle = pass.TextureWrites[j]; + var resource = _resources.GetTextureResource(writeHandle); + if (resource.IsImported) { - if (_resourceLastWriter.TryGetValue(resourceId, out int lastWriterIndex)) - { - if (!pass.Dependencies.Contains(lastWriterIndex)) - { - pass.Dependencies.Add(lastWriterIndex); - } - } - - if (!_resourceLastReaders.TryGetValue(resourceId, out var readers)) - { - readers = new List(); // Optimization TODO: Pool these - _resourceLastReaders[resourceId] = readers; - } - readers.Add(i); + pass.HasSideEffects = true; + break; } + } + } - if (IsWriteState(state)) + // Step 2: Cull passes based on dependency analysis + // Mark all passes as culled initially + for (var i = 0; i < _passCount; i++) + { + _passes[i].Culled = _passes[i].AllowCulling && !_passes[i].HasSideEffects; + } + + // Step 3: Traverse backwards from passes with side effects + for (var i = _passCount - 1; i >= 0; i--) + { + var pass = _passes[i]; + if (!pass.Culled) + { + UnculDependencies(pass); + } + } + + // Step 4: Build final pass list (only non-culled passes) + for (var i = 0; i < _passCount; i++) + { + var pass = _passes[i]; + if (!pass.Culled) + { + _compiledPasses.Add(pass); + } + } + + // Step 5: Perform resource aliasing to minimize memory usage + _aliasingManager.AssignPhysicalResources(_resources, _passCount); + + // Step 6: Generate barriers for state transitions and aliasing + GenerateBarriers(); + + // Step 7: Store in cache for future frames + StoreInCache(graphHash); + + _compiled = true; + } + + /// + /// Restores the render graph state from cached compilation results. + /// + private void RestoreFromCache(CachedCompilation cached) + { + // Restore compiled pass list + _compiledPasses.Clear(); + for (int i = 0; i < cached.CompiledPassIndices.Count; i++) + { + int passIndex = cached.CompiledPassIndices[i]; + _compiledPasses.Add(_passes[passIndex]); + } + + // Restore culling flags + for (int i = 0; i < _passCount && i < cached.PassCulledFlags.Count; i++) + { + _passes[i].Culled = cached.PassCulledFlags[i]; + } + + // Restore aliasing mappings (need to update ResourceAliasingManager) + _aliasingManager.RestoreFromCache(cached.LogicalToPhysical, cached.PhysicalResources); + + // Restore barriers (deep copy to avoid shared references) + _barriers.Clear(); + for (int i = 0; i < cached.Barriers.Count; i++) + { + _barriers.Add(cached.Barriers[i]); + } + + // Restore resource states + _resourceStates.Clear(); + foreach (var kvp in cached.ResourceStates) + { + _resourceStates[kvp.Key] = kvp.Value; + } + } + + /// + /// Stores current compilation results in the cache. + /// + private void StoreInCache(ulong graphHash) + { + var cacheData = new CachedCompilation(); + + // Store compiled pass indices + for (int i = 0; i < _compiledPasses.Count; i++) + { + cacheData.CompiledPassIndices.Add(_compiledPasses[i].Index); + } + + // Store culling flags for all passes + for (int i = 0; i < _passCount; i++) + { + cacheData.PassCulledFlags.Add(_passes[i].Culled); + } + + // Store aliasing mappings + _aliasingManager.StoreToCache(cacheData.LogicalToPhysical, cacheData.PhysicalResources); + + // Store barriers + for (int i = 0; i < _barriers.Count; i++) + { + cacheData.Barriers.Add(_barriers[i]); + } + + // Store resource states + foreach (var kvp in _resourceStates) + { + cacheData.ResourceStates[kvp.Key] = kvp.Value; + } + + _compilationCache.Store(graphHash, cacheData); + } + + /// + /// Recursively un-cull passes that a given pass depends on. + /// + private void UnculDependencies(RenderGraphPassBase pass) + { + // Un-cull all producers of textures we read + for (var i = 0; i < pass.TextureReads.Count; i++) + { + var readHandle = pass.TextureReads[i]; + var resource = _resources.GetTextureResource(readHandle); + + if (resource.ProducerPass >= 0) + { + var producer = _passes[resource.ProducerPass]; + if (producer.Culled) { - if (_resourceLastWriter.TryGetValue(resourceId, out int lastWriterIndex)) - { - if (!pass.Dependencies.Contains(lastWriterIndex)) - { - pass.Dependencies.Add(lastWriterIndex); - } - } + producer.Culled = false; + UnculDependencies(producer); + } + } + } + } - if (_resourceLastReaders.TryGetValue(resourceId, out var readers)) + /// + /// Generates resource barriers for state transitions and aliasing. + /// + private void GenerateBarriers() + { + _barriers.Clear(); + _resourceStates.Clear(); + +#if DEBUG + Console.WriteLine("\n=== Barrier Generation ==="); +#endif + + // Process each compiled pass in order + for (var passIdx = 0; passIdx < _compiledPasses.Count; passIdx++) + { + var pass = _compiledPasses[passIdx]; + + // Insert aliasing barriers for resources that reuse physical memory + InsertAliasingBarriers(pass, passIdx); + + // Insert transition barriers for state changes + InsertTransitionBarriers(pass, passIdx); + } + +#if DEBUG + Console.WriteLine($"Total Barriers: {_barriers.Count}"); + Console.WriteLine("==========================\n"); +#endif + } + + /// + /// Inserts aliasing barriers when a physical resource is reused. + /// + private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx) + { + // Check all resources written by this pass + for (int i = 0; i < pass.TextureWrites.Count; i++) + { + var handle = pass.TextureWrites[i]; + var resource = _resources.GetTextureResource(handle); + + // Skip imported resources + if (resource.IsImported) + continue; + + // Check if this is the first use of this logical resource + if (resource.FirstUsePass == pass.Index) + { + // Get the physical resource + int physicalIndex = _aliasingManager.GetPhysicalResourceIndex(handle.Index); + if (physicalIndex >= 0) + { + var physical = _aliasingManager.GetPhysicalResource(physicalIndex); + + // If this physical resource has multiple aliased resources, + // we need an aliasing barrier when switching between them + if (physical != null && physical.AliasedLogicalResources.Count > 1) { - foreach (var readerIndex in readers) + // Find the resource that used this physical memory most recently before this pass + RenderGraphTextureHandle resourceBefore = default; + int mostRecentLastUse = -1; + + foreach (int otherLogicalIndex in physical.AliasedLogicalResources) { - if (readerIndex != i && !pass.Dependencies.Contains(readerIndex)) + if (otherLogicalIndex != handle.Index) { - pass.Dependencies.Add(readerIndex); + var otherResource = _resources.GetTextureResourceByIndex(otherLogicalIndex); + // Check if this resource finished before our resource starts + if (otherResource.LastUsePass < pass.Index && + otherResource.LastUsePass > mostRecentLastUse) + { + mostRecentLastUse = otherResource.LastUsePass; + resourceBefore = new RenderGraphTextureHandle( + otherLogicalIndex, + otherResource.Version, + otherResource.Descriptor.Name); + } } } - readers.Clear(); - } - _resourceLastWriter[resourceId] = i; - } - } - } - } - - private void CullUnusedPasses() - { - foreach (var pass in _passes) - { - foreach (var (handle, _) in pass.ResourceAccesses) - { - if (handle.IsImported) - { - pass.RefCount++; - } - } - - if (!pass.AllowCulling) - { - pass.RefCount++; - } - } - - bool changed = true; - while (changed) - { - changed = false; - foreach (var pass in _passes) - { - if (pass.RefCount > 0) - { - foreach (var depIndex in pass.Dependencies) - { - var depPass = _passes[depIndex]; - if (depPass.RefCount == 0) + // If we found a previous resource, insert aliasing barrier + if (mostRecentLastUse >= 0) { - depPass.RefCount++; - changed = true; + var barrier = ResourceBarrier.CreateAliasingBarrier( + resourceBefore, + handle, + passIdx + ); + _barriers.Add(barrier); + +#if DEBUG + Console.WriteLine($" {barrier}"); +#endif } } } @@ -343,229 +510,98 @@ public class RenderGraph } } - private void AnalyzeResourceLifetimes() + /// + /// Inserts transition barriers when a resource changes state. + /// + private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx) { - // Resize execution plan arrays if needed - int requiredSize = _passes.Count; - if (_resourcesToCreate.Length < requiredSize) + // Process reads (transition to shader resource) + for (var i = 0; i < pass.TextureReads.Count; i++) { - Array.Resize(ref _resourcesToCreate, requiredSize); - Array.Resize(ref _resourcesToDestroy, requiredSize); - - // Initialize new elements - for (int i = 0; i < requiredSize; i++) - { - if (_resourcesToCreate[i] == null) _resourcesToCreate[i] = new List(); - if (_resourcesToDestroy[i] == null) _resourcesToDestroy[i] = new List(); - } + var handle = pass.TextureReads[i]; + InsertTransitionIfNeeded(handle, ResourceState.ShaderResource, passIdx); } - // Clear previous plan - for (int i = 0; i < requiredSize; i++) + // Process writes (transition to render target or UAV) + for (var i = 0; i < pass.TextureWrites.Count; i++) { - _resourcesToCreate[i].Clear(); - _resourcesToDestroy[i].Clear(); - } - - // Populate plan - foreach (var lifetime in _resourceLifetimes) - { - if (lifetime.FirstUse != int.MaxValue && !lifetime.Handle.IsImported) - { - // Verify bounds to be safe - if (lifetime.FirstUse < requiredSize) - _resourcesToCreate[lifetime.FirstUse].Add(lifetime.Handle); - - if (lifetime.LastUse < requiredSize) - _resourcesToDestroy[lifetime.LastUse].Add(lifetime.Handle); - } + var handle = pass.TextureWrites[i]; + var targetState = ResourceState.RenderTarget; // Could be UAV for compute + InsertTransitionIfNeeded(handle, targetState, passIdx); } } + /// + /// Inserts a transition barrier if the resource state changes. + /// + private void InsertTransitionIfNeeded(RenderGraphTextureHandle handle, ResourceState newState, int passIdx) + { + if (!_resourceStates.TryGetValue(handle.Index, out var currentState)) + { + // First time seeing this resource, assume undefined + currentState = ResourceState.Undefined; + } + + if (currentState != newState) + { + var barrier = ResourceBarrier.CreateTransitionBarrier( + handle, + currentState, + newState, + passIdx + ); + _barriers.Add(barrier); + _resourceStates[handle.Index] = newState; + +#if DEBUG + Console.WriteLine($" {barrier}"); +#endif + } + } + + /// + /// Executes all compiled passes. + /// public void Execute() { - //ConsoleAPI.WriteLine("\n[RG] ========== EXECUTING RENDER GRAPH ==========\n"); - - var commandBuffer = new SimulatedCommandBuffer(); - - foreach (var batch in _batches) + if (!_compiled) { - //ConsoleAPI.WriteLine($"[BATCH {batch.ID}] Queue: {batch.QueueType} | Passes: {batch.Passes.Count}"); + Compile(); + } - foreach (var fenceId in batch.WaitFences) + // Execute each non-culled pass + int barrierIndex = 0; + for (int i = 0; i < _compiledPasses.Count; i++) + { + var pass = _compiledPasses[i]; + + // Execute all barriers for this pass +#if DEBUG + bool hasBarriers = false; +#endif + while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == i) { - //ConsoleAPI.WriteLine($" [SYNC] Wait for Fence {fenceId}"); - } - - foreach (var pass in batch.Passes) - { - //ConsoleAPI.WriteLine($" [PASS {pass.Index}] Executing: '{pass.Name}'"); +#if DEBUG + if (!hasBarriers) + { + Console.WriteLine($"\n=== Barriers before Pass {i}: {pass.Name} ==="); + hasBarriers = true; + } + Console.WriteLine($" {_barriers[barrierIndex]}"); +#endif + // In a real implementation, you would execute the barrier here: + // ExecuteBarrier(_barriers[barrierIndex]); - // Optimized: Use pre-calculated lists - var createList = _resourcesToCreate[pass.Index]; - foreach (var handle in createList) - { - CreateResource(handle); - } - - InsertBarriers(pass, commandBuffer); - - commandBuffer.BeginRenderPass(pass.Name); - pass.Execute(commandBuffer); - commandBuffer.EndRenderPass(); - - // Optimized: Use pre-calculated lists - var destroyList = _resourcesToDestroy[pass.Index]; - foreach (var handle in destroyList) - { - DestroyResource(handle); - } + barrierIndex++; } - - foreach (var fenceId in batch.SignalFences) +#if DEBUG + if (hasBarriers) { - //ConsoleAPI.WriteLine($" [SYNC] Signal Fence {fenceId}"); + Console.WriteLine("=====================================\n"); } +#endif + + pass.Execute(_renderContext); } - - //ConsoleAPI.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n"); - } - - private void CreateResource(RenderGraphResourceHandle handle) - { - var allocation = _allocator.GetAllocation(handle); - if (allocation != null) - { - // Logic... - } - - _currentResourceStates[handle.Id] = ResourceState.Undefined; - } - - private void DestroyResource(RenderGraphResourceHandle handle) - { - _currentResourceStates[handle.Id] = ResourceState.Undefined; - } - - private void InsertBarriers(RenderGraphPass pass, ICommandBuffer commandBuffer) - { - var _resourceBarriers = ListPool.Rent(); - var _aliasingBarriers = ListPool.Rent(); - - foreach (var (handle, targetState) in pass.ResourceAccesses) - { - var allocation = _allocator.GetAllocation(handle); - if (allocation != null) - { - if (_allocationActiveResource.TryGetValue(allocation.Value.AllocationId, out var activeResource)) - { - if (activeResource != null && activeResource.Value.Id != handle.Id) - { - _aliasingBarriers.Add(new AliasingBarrierInfo(activeResource.Value.Name, handle.Name, allocation.Value.DebugName)); - _currentResourceStates[activeResource.Value.Id] = ResourceState.Undefined; - } - } - _allocationActiveResource[allocation.Value.AllocationId] = handle; - } - - var currentState = _currentResourceStates[handle.Id]; - if (currentState != targetState) - { - _resourceBarriers.Add(new ResourceBarrierInfo(handle.Name, currentState, targetState)); - _currentResourceStates[handle.Id] = targetState; - } - } - - if (_aliasingBarriers.Count > 0) - { - commandBuffer.AliasingBarrier(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_aliasingBarriers)); - } - - if (_resourceBarriers.Count > 0) - { - commandBuffer.ResourceBarrier(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_resourceBarriers)); - } - - ListPool.Return(_resourceBarriers); - ListPool.Return(_aliasingBarriers); - } - - private static bool IsWriteState(ResourceState state) - { - return state.HasFlag(ResourceState.RenderTarget) || - state.HasFlag(ResourceState.DepthWrite) || - state.HasFlag(ResourceState.UnorderedAccess) || - state.HasFlag(ResourceState.CopyDest); - } - - private static bool IsReadState(ResourceState state) - { - return state.HasFlag(ResourceState.ShaderResource) || - state.HasFlag(ResourceState.DepthRead) || - state.HasFlag(ResourceState.CopySource); - } - - internal List<(RenderGraphResourceHandle, ResourceState)> RentResourceAccessList() - { - if (_resourceAccessListPool.TryPop(out var list)) - { - return list; - } - return new List<(RenderGraphResourceHandle, ResourceState)>(); - } - - internal void ReturnResourceAccessList(List<(RenderGraphResourceHandle, ResourceState)> list) - { - list.Clear(); - _resourceAccessListPool.Push(list); - } - - private ResourceLifetime RentResourceLifetime(RenderGraphResourceHandle handle) - { - if (!_resourceLifetimePool.TryPop(out var lifetime)) - { - lifetime = new ResourceLifetime(); - } - lifetime.Initialize(handle); - return lifetime; - } - - public void Reset() - { - foreach (var batch in _batches) - { - batch.Reset(); - _batchPool.Push(batch); - } - _batches.Clear(); - - foreach (var pass in _passes) - { - // ReturnResourceAccessList(pass.ResourceAccesses); - // Warning: pass.ResourceAccesses might be a copy in the current implementation of CommitPass? - // No, I'm going to fix CommitPass to use the pooled list. - // But right now builder.ResourceAccesses is a List. - // I need to ensure CommitPass takes ownership. - } - _passes.Clear(); - - _resources.Clear(); - foreach (var lifetime in _resourceLifetimes) - { - _resourceLifetimePool.Push(lifetime); - } - _resourceLifetimes.Clear(); - _currentResourceStates.Clear(); - _resourceToAllocationMap.Clear(); - _allocationActiveResource.Clear(); - _blackboard.Clear(); - _allocator.Reset(); - _passCounter = 0; - _resourceIdCounter = 0; - - _resourceLastWriter.Clear(); - foreach (var list in _resourceLastReaders.Values) list.Clear(); - _resourceLastReaders.Clear(); - _passToBatchMap.Clear(); } } diff --git a/Ghost.RenderGraph.Concept/RenderGraphAliasing.cs b/Ghost.RenderGraph.Concept/RenderGraphAliasing.cs new file mode 100644 index 0000000..23e892d --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphAliasing.cs @@ -0,0 +1,329 @@ +using Ghost.Core.Utilities; + +namespace Ghost.RenderGraph.Concept; + +/// +/// Represents a physical GPU resource that can be aliased by multiple logical resources. +/// +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 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; + } +} + +/// +/// Manages physical resource allocation and aliasing. +/// Uses interval scheduling algorithm to minimize memory usage. +/// +internal sealed class ResourceAliasingManager +{ + private readonly List _physicalResources = new(32); + private readonly RenderGraphObjectPool _pool = new(); + private int _physicalResourceCount; + + // Mapping from logical resource index to physical resource index + private readonly Dictionary _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(); + } + } + + /// + /// Assigns physical resources to logical resources using greedy interval scheduling. + /// This minimizes total GPU memory usage. + /// + 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(); + 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(); + } + + /// + /// Restores aliasing state from cache. + /// + public void RestoreFromCache(Dictionary logicalToPhysical, List 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(); + 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(); + } + } + + /// + /// Stores current aliasing state to cache. + /// + public void StoreToCache(Dictionary outLogicalToPhysical, List 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 + }); + } + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphBarriers.cs b/Ghost.RenderGraph.Concept/RenderGraphBarriers.cs new file mode 100644 index 0000000..d4126f2 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphBarriers.cs @@ -0,0 +1,154 @@ +using System.Runtime.InteropServices; + +namespace Ghost.RenderGraph.Concept; + +/// +/// GPU resource states for barrier tracking. +/// Based on D3D12 resource states. +/// +[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, +} + +/// +/// Types of barriers that can be inserted. +/// +public enum BarrierType +{ + Transition, // State transition (e.g., RenderTarget -> ShaderResource) + Aliasing, // Aliasing barrier (new resource reusing memory) + UAV, // UAV barrier (synchronize UAV access) +} + +/// +/// Represents a resource barrier that needs to be inserted. +/// For D3D12 aliasing barriers: ResourceBefore is the old resource, ResourceAfter is the new resource. +/// +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 +} + +/// +/// Tracks the current state of a resource across passes. +/// +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; + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphBatch.cs b/Ghost.RenderGraph.Concept/RenderGraphBatch.cs deleted file mode 100644 index f524965..0000000 --- a/Ghost.RenderGraph.Concept/RenderGraphBatch.cs +++ /dev/null @@ -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 Passes { get; } = new(); - - // Fences to wait on BEFORE executing this batch - public List WaitFences { get; } = new(); - - // Fences to signal AFTER executing this batch - public List 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(); - } -} diff --git a/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs b/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs index 8e615b4..e13a257 100644 --- a/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs +++ b/Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs @@ -1,26 +1,43 @@ namespace Ghost.RenderGraph.Concept; -public class RenderGraphBlackboard +/// +/// 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. +/// +public sealed class RenderGraphBlackboard { - private readonly Dictionary _data = new(); + private readonly Dictionary _data = new(16); - public void Add(T data) where T : class + /// + /// Adds or updates pass data in the blackboard. + /// + public void Add(T data) where T : class, IPassData { - _data[typeof(T)] = data; + var type = typeof(T); + _data[type] = data; } - public T Get() where T : class + /// + /// Retrieves pass data from the blackboard. + /// + public T Get() 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(out T? data) where T : class + /// + /// Tries to get pass data from the blackboard. + /// + public bool TryGet(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; } + /// + /// Clears all data from the blackboard. + /// Does not deallocate the backing dictionary to avoid allocations. + /// public void Clear() { _data.Clear(); diff --git a/Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs b/Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs new file mode 100644 index 0000000..6894736 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs @@ -0,0 +1,27 @@ +namespace Ghost.RenderGraph.Concept; + +/// +/// Extension methods to provide a cleaner API for setting render functions. +/// These avoid the need for explicit type annotations in lambdas. +/// +public static class RenderGraphBuilderExtensions +{ + // Internal helper to cast and set + private static void SetRasterFunc(this RenderGraphBuilder builder, object pass, Action func) + where TPassData : class, new() + { + if (pass is RenderGraphPass typedPass) + { + builder.SetRenderFunc(func); + } + } + + private static void SetCompFunc(this RenderGraphBuilder builder, object pass, Action func, bool async) + where TPassData : class, new() + { + if (pass is RenderGraphPass typedPass) + { + builder.SetComputeFunc(func, async); + } + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs b/Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs new file mode 100644 index 0000000..cbfe955 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs @@ -0,0 +1,131 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Ghost.RenderGraph.Concept; + +/// +/// Represents cached compilation results for a render graph. +/// This avoids recompiling the graph when the structure hasn't changed. +/// +internal sealed class CachedCompilation +{ + // Compiled pass indices (indices into the _passes list) + public readonly List CompiledPassIndices = new(64); + + // Culling decisions for each pass + public readonly List PassCulledFlags = new(64); + + // Physical resource aliasing mappings (logical index -> physical index) + public readonly Dictionary LogicalToPhysical = new(128); + + // Physical resource metadata + public readonly List PhysicalResources = new(32); + + // Resource barriers + public readonly List Barriers = new(128); + + // Resource state mappings (for barrier generation) + public readonly Dictionary ResourceStates = new(128); + + public void Clear() + { + CompiledPassIndices.Clear(); + PassCulledFlags.Clear(); + LogicalToPhysical.Clear(); + PhysicalResources.Clear(); + Barriers.Clear(); + ResourceStates.Clear(); + } +} + +/// +/// Physical resource data for caching. +/// +internal struct PhysicalResourceData +{ + public int Index; + public int Width; + public int Height; + public TextureFormat Format; + public int FirstUsePass; + public int LastUsePass; +} + +/// +/// Manages compilation caching for render graphs. +/// Stores compiled results and allows cache hits when graph structure is unchanged. +/// +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; } + + /// + /// Attempts to retrieve cached compilation results. + /// + public bool TryGetCached(ulong hash, [MaybeNullWhen(false)] out CachedCompilation result) + { + if (_hasCachedData && _cachedHash == hash) + { + result = _cached; + CacheHits++; + return true; + } + + result = null; + CacheMisses++; + return false; + } + + /// + /// Stores compilation results in the cache. + /// + 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; + } + } + + /// + /// Invalidates the cache, forcing recompilation on next Compile(). + /// + public void Invalidate() + { + _hasCachedData = false; + _cachedHash = 0; + _cached.Clear(); + } + + /// + /// Gets cache statistics for debugging. + /// + 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); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphContext.cs b/Ghost.RenderGraph.Concept/RenderGraphContext.cs new file mode 100644 index 0000000..1f1e8c2 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphContext.cs @@ -0,0 +1,134 @@ +namespace Ghost.RenderGraph.Concept; + +/// +/// Mock command buffer for recording GPU commands. +/// In a real implementation, this would wrap D3D12 command lists. +/// +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 + } +} + +/// +/// Context for raster rendering passes. +/// Directly exposes command buffer methods. +/// +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); +} + +/// +/// Context for compute rendering passes. +/// Directly exposes command buffer methods. +/// +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); +} + +/// +/// Unified render context containing both raster and compute contexts. +/// +internal readonly struct RenderContext +{ + public readonly RasterRenderContext RasterContext; + public readonly ComputeRenderContext ComputeContext; + + public RenderContext(MockCommandBuffer cmd) + { + RasterContext = new RasterRenderContext(cmd); + ComputeContext = new ComputeRenderContext(cmd); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs b/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs deleted file mode 100644 index 8dbf01e..0000000 --- a/Ghost.RenderGraph.Concept/RenderGraphExtensions.cs +++ /dev/null @@ -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 AddRenderPass( - // this RenderGraph renderGraph, - // string name, - // out TPassData passData, - // Action> setup) - // where TPassData : class, new() - // { - // var builder = renderGraph.AddRenderPass(name, out passData); - // setup(builder); - // builder.Dispose(); - // return builder; - // } -} - -public sealed class RenderGraphPassScope : IDisposable - where TPassData : class, new() -{ - // Cannot hold ref struct in class - // private readonly RenderGraphPassBuilder _builder; - private readonly string _passName; - - // internal RenderGraphPassScope(RenderGraphPassBuilder builder, string passName) - // { - // _builder = builder; - // _passName = passName; - // } - - // public RenderGraphPassBuilder Builder => _builder; - - public void Dispose() - { - // Commit the pass when the using block ends - // if (_builder.RenderFunc != null) - // { - // _builder.Dispose(); - // } - } -} -*/ diff --git a/Ghost.RenderGraph.Concept/RenderGraphHash.cs b/Ghost.RenderGraph.Concept/RenderGraphHash.cs new file mode 100644 index 0000000..e5f1c68 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphHash.cs @@ -0,0 +1,68 @@ +using System.IO.Hashing; +using System.Runtime.InteropServices; + +namespace Ghost.RenderGraph.Concept; + +/// +/// Helper extensions for XxHash3 to hash common types without string allocation. +/// Uses SIMD-optimized hashing via System.IO.Hashing.XxHash3. +/// +internal static class RenderGraphHashExtensions +{ + /// + /// Appends an int to the hash. + /// + public static void AppendInt(this XxHash64 hash, int value) + { + ReadOnlySpan span = stackalloc int[1] { value }; + hash.Append(MemoryMarshal.AsBytes(span)); + } + + /// + /// Appends a bool to the hash. + /// + public static void AppendBool(this XxHash64 hash, bool value) + { + ReadOnlySpan span = stackalloc bool[1] { value }; + hash.Append(MemoryMarshal.AsBytes(span)); + } + + /// + /// Appends an enum to the hash. + /// + public static void AppendEnum(this XxHash64 hash, TEnum value) where TEnum : unmanaged, Enum + { + ReadOnlySpan span = stackalloc TEnum[1] { value }; + hash.Append(MemoryMarshal.AsBytes(span)); + } + + /// + /// Appends a struct to the hash (must be unmanaged). + /// + public static void AppendStruct(this XxHash64 hash, in T value) where T : unmanaged + { + ReadOnlySpan span = stackalloc T[1] { value }; + hash.Append(MemoryMarshal.AsBytes(span)); + } + + /// + /// Appends a list of resource handle indices to the hash. + /// + public static void AppendHandleList(this XxHash64 hash, List 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 indices = stackalloc int[count]; + for (int i = 0; i < count; i++) + { + indices[i] = handles[i].Index; + } + hash.Append(MemoryMarshal.AsBytes(indices)); + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphPass.cs b/Ghost.RenderGraph.Concept/RenderGraphPass.cs index 1b460c8..fde3a05 100644 --- a/Ghost.RenderGraph.Concept/RenderGraphPass.cs +++ b/Ghost.RenderGraph.Concept/RenderGraphPass.cs @@ -1,111 +1,215 @@ -using System; -using System.Collections.Generic; - namespace Ghost.RenderGraph.Concept; -public enum RenderQueueType +/// +/// Represents different types of render passes. +/// +public enum RenderPassType : byte { - Graphics, - Compute, - AsyncCompute, - Copy + Raster, + Compute } -internal abstract class RenderGraphPass +/// +/// Base class for render passes. +/// Uses pooling to avoid allocations after the first frame. +/// +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 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 TextureReads = new(8); + public readonly List TextureWrites = new(4); + public readonly List 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 - where TPassData : class +/// +/// Typed render pass with user data. +/// +internal sealed class RenderGraphPass : RenderGraphPassBase + where TPassData : class, new() { - public static readonly Stack> Pool = new(); + public TPassData? PassData; + public Action? RasterRenderFunc; + public Action? 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 : RenderGraphPass - where TPassData : class +/// +/// Builder for constructing render passes. +/// Implements IDisposable for using() pattern. +/// +public sealed class RenderGraphBuilder : IDisposable { - public TPassData PassData { get; private set; } - public Action RenderFunc { get; private set; } + private RenderGraphPassBase? _pass; + private RenderGraphResourceRegistry? _resources; + private bool _disposed; - public RenderGraphPass( - string name, - int index, - RenderQueueType queueType, - TPassData passData, - Action 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 renderFunc, - List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses, - bool allowCulling) + /// + /// Creates a new transient texture that only lives for this pass. + /// + 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) + /// + /// Marks a texture as being read by this pass. + /// + public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle) { - RenderFunc(PassData, commandBuffer); + ThrowIfDisposed(); + _pass!.TextureReads.Add(handle); + _resources!.AddConsumer(handle, _pass.Index); + return handle; } - public override void Release() + /// + /// Marks a texture as being written by this pass. + /// + public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle) { - PassData = null!; - RenderFunc = null!; - // ResourceAccesses list ownership is transferred back to RenderGraph - ResourceAccesses = null!; - RenderGraphPassPool.Pool.Push(this); + ThrowIfDisposed(); + _pass!.TextureWrites.Add(handle); + _resources!.SetProducer(handle, _pass.Index); + return handle; + } + + /// + /// Sets up a depth buffer for this pass. + /// + 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; + } + + /// + /// Sets the render function for a raster pass. + /// + public void SetRenderFunc(Action renderFunc) + where TPassData : class, new() + { + ThrowIfDisposed(); + + if (_pass is RenderGraphPass typedPass) + { + typedPass.RasterRenderFunc = renderFunc; + typedPass.Type = RenderPassType.Raster; + } + } + + /// + /// Sets the compute function for a compute pass. + /// + public void SetComputeFunc(Action computeFunc, bool asyncCompute = false) + where TPassData : class, new() + { + ThrowIfDisposed(); + + if (_pass is RenderGraphPass typedPass) + { + typedPass.ComputeRenderFunc = computeFunc; + typedPass.Type = RenderPassType.Compute; + typedPass.AsyncCompute = asyncCompute; + } + } + + /// + /// Controls whether this pass can be culled if its outputs are unused. + /// + 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)); } } diff --git a/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs b/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs deleted file mode 100644 index c2c8446..0000000 --- a/Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs +++ /dev/null @@ -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 - 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? _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? 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 renderFunc) - { - _queueType = RenderQueueType.Graphics; - _renderFunc = (data, cmd) => renderFunc(data, new RasterRenderContext(cmd)); - } - - public void SetComputeFunc(Action computeFunc, bool asyncCompute = false) - { - _queueType = asyncCompute ? RenderQueueType.AsyncCompute : RenderQueueType.Compute; - _renderFunc = (data, cmd) => computeFunc(data, new ComputeRenderContext(cmd)); - } - - /// - /// 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. - /// - 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; - } - } -} diff --git a/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs b/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs deleted file mode 100644 index 20f06c6..0000000 --- a/Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs +++ /dev/null @@ -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 }); - } -} diff --git a/Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs b/Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs new file mode 100644 index 0000000..ae511a6 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs @@ -0,0 +1,173 @@ +namespace Ghost.RenderGraph.Concept; + +/// +/// Object pool for reusing allocated objects across frames. +/// This is key to minimizing GC allocations after the first frame. +/// +internal sealed class RenderGraphObjectPool +{ + private readonly Dictionary> _pools = new(); + + public T Get() 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 obj) where T : class + { + if (obj == null) return; + + var type = typeof(T); + if (!_pools.TryGetValue(type, out var pool)) + { + pool = new Stack(16); + _pools[type] = pool; + } + pool.Push(obj); + } + + public void Clear() + { + _pools.Clear(); + } +} + +/// +/// Represents a texture resource in the render graph. +/// +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 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; + } +} + +/// +/// Registry for managing all resources in the render graph. +/// Uses pooling to minimize allocations after the first frame. +/// +internal sealed class RenderGraphResourceRegistry +{ + private readonly List _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(); + 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; + } +} diff --git a/Ghost.RenderGraph.Concept/RenderGraphTypes.cs b/Ghost.RenderGraph.Concept/RenderGraphTypes.cs new file mode 100644 index 0000000..75242b7 --- /dev/null +++ b/Ghost.RenderGraph.Concept/RenderGraphTypes.cs @@ -0,0 +1,75 @@ +namespace Ghost.RenderGraph.Concept; + +/// +/// Opaque handle to a render graph texture resource. +/// +public readonly struct RenderGraphTextureHandle : IEquatable +{ + 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); +} + +/// +/// Texture formats supported by the render graph. +/// +public enum TextureFormat : int +{ + RGBA8, + RGBA16F, + RGBA32F, + Depth32F, + Depth24Stencil8 +} + +/// +/// Descriptor for creating a texture resource. +/// +public readonly struct TextureDescriptor : IEquatable +{ + 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); +} + +/// +/// Base interface for pass data that can be stored in the blackboard. +/// +public interface IPassData +{ +} diff --git a/Ghost.RenderGraph.Concept/ResourceAllocator.cs b/Ghost.RenderGraph.Concept/ResourceAllocator.cs deleted file mode 100644 index 2abbe64..0000000 --- a/Ghost.RenderGraph.Concept/ResourceAllocator.cs +++ /dev/null @@ -1,360 +0,0 @@ -using Ghost.Core.Utilities; -using ZLinq; - -namespace Ghost.RenderGraph.Concept; - -/// -/// Represents a physical memory allocation that can be shared by multiple transient resources -/// -internal struct PhysicalResourceAllocation -{ - public int AllocationId { get; } - public ulong SizeInBytes { get; } - public ulong OffsetInBytes { get; } - public string DebugName { get; } - public List AliasedResources { get; } = new(); - - public PhysicalResourceAllocation(int allocationId, ulong sizeInBytes, ulong offsetInBytes, string debugName) - { - AllocationId = allocationId; - SizeInBytes = sizeInBytes; - OffsetInBytes = offsetInBytes; - DebugName = debugName; - } -} - -/// -/// Manages memory allocation and aliasing for transient resources -/// -internal class ResourceAllocator -{ - private readonly List _allocations = new(); - private int _allocationIdCounter = 0; - - public void Reset() - { - _allocations.Clear(); - _allocationIdCounter = 0; - } - - public IReadOnlyList Allocations => _allocations; - - /// - /// Allocate physical memory for resources, enabling aliasing where possible - /// - public void AllocateResources( - IReadOnlyList resourceLifetimes, - List 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.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.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 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; - } - - /// - /// Find a free offset within this allocation that can fit the required size - /// and doesn't conflict with active resources - /// - 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); - } - } -} diff --git a/Ghost.RenderGraph.Concept/ResourceDescriptor.cs b/Ghost.RenderGraph.Concept/ResourceDescriptor.cs deleted file mode 100644 index 996e753..0000000 --- a/Ghost.RenderGraph.Concept/ResourceDescriptor.cs +++ /dev/null @@ -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" -); diff --git a/Ghost.RenderGraph.Concept/ResourceLifetime.cs b/Ghost.RenderGraph.Concept/ResourceLifetime.cs deleted file mode 100644 index bae5362..0000000 --- a/Ghost.RenderGraph.Concept/ResourceLifetime.cs +++ /dev/null @@ -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 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); - } -} diff --git a/Ghost.RenderGraph.Concept/ResourceState.cs b/Ghost.RenderGraph.Concept/ResourceState.cs deleted file mode 100644 index 1ebebdd..0000000 --- a/Ghost.RenderGraph.Concept/ResourceState.cs +++ /dev/null @@ -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) -} diff --git a/Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs b/Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs new file mode 100644 index 0000000..454d261 --- /dev/null +++ b/Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs @@ -0,0 +1,2220 @@ +#if false + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace UnityEngine.Rendering.RenderGraphModule.NativeRenderPassCompiler +{ + internal partial class NativePassCompiler : IDisposable + { + internal struct RenderGraphInputInfo + { + public RenderGraphResourceRegistry m_ResourcesForDebugOnly; + public List m_RenderPasses; + public string debugName; + public bool disablePassCulling; + public bool disablePassMerging; + public RenderTextureUVOriginStrategy renderTextureUVOriginStrategy; + } + + internal RenderGraphInputInfo graph; + internal CompilerContextData contextData = null; + internal CompilerContextData defaultContextData; + internal CommandBuffer previousCommandBuffer; + Stack m_HasSideEffectPassIdCullingStack; + List> m_UnusedVersionedResourceIdCullingStacks; + Dictionary> m_DelayedLastUseListPerPassMap; + + RenderGraphCompilationCache m_CompilationCache; + + RenderTargetIdentifier[][] m_TempMRTArrays = null; + + internal const int k_EstimatedPassCount = 100; + internal const int k_MaxSubpass = 8; // Needs to match with RenderPassSetup.h + + NativeList m_BeginRenderPassAttachments; + + // Contains the index of the non culled passes for native render passes that has at least one pass culled. + internal NativeList m_NonCulledPassIndicesForRasterPasses; + + internal static bool s_ForceGenerateAuditsForTests = false; + + public NativePassCompiler(RenderGraphCompilationCache cache) + { + m_CompilationCache = cache; + defaultContextData = new CompilerContextData(); + m_HasSideEffectPassIdCullingStack = new Stack(k_EstimatedPassCount); + + m_UnusedVersionedResourceIdCullingStacks = new List>(); + for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) + m_UnusedVersionedResourceIdCullingStacks.Add(new Stack()); + + m_DelayedLastUseListPerPassMap = new Dictionary>(k_EstimatedPassCount); + for (int passId = 0; passId < k_EstimatedPassCount; ++passId) + m_DelayedLastUseListPerPassMap.Add(passId, new List()); + + m_TempMRTArrays = new RenderTargetIdentifier[RenderGraph.kMaxMRTCount][]; + for (int i = 0; i < RenderGraph.kMaxMRTCount; ++i) + m_TempMRTArrays[i] = new RenderTargetIdentifier[i + 1]; + } + + // IDisposable implementation + + ~NativePassCompiler() => Cleanup(); + + public void Dispose() + { + Cleanup(); + GC.SuppressFinalize(this); + } + + public void Cleanup() + { + // If caching enabled, the two can be different + contextData?.Dispose(); + defaultContextData?.Dispose(); + + if (m_BeginRenderPassAttachments.IsCreated) + { + m_BeginRenderPassAttachments.Dispose(); + } + + if (m_NonCulledPassIndicesForRasterPasses.IsCreated) + { + m_NonCulledPassIndicesForRasterPasses.Dispose(); + } + } + + public bool Initialize(RenderGraphResourceRegistry resources, List renderPasses, RenderGraphDebugParams debugParams, string debugName, bool useCompilationCaching, + int graphHash, int frameIndex, RenderTextureUVOriginStrategy renderTextureUVOriginStrategy) + { + bool cached = false; + if (!useCompilationCaching) + contextData = defaultContextData; + else + cached = m_CompilationCache.GetCompilationCache(graphHash, frameIndex, out contextData); + + graph.m_ResourcesForDebugOnly = resources; + graph.m_RenderPasses = renderPasses; + graph.disablePassCulling = debugParams.disablePassCulling; + graph.disablePassMerging = debugParams.disablePassMerging; + graph.debugName = debugName; + graph.renderTextureUVOriginStrategy = renderTextureUVOriginStrategy; + + Clear(clearContextData: !useCompilationCaching); + + return cached; + } + + void HandleExtendedFeatureFlags() + { + for (int nativePassIndex = 0; nativePassIndex < contextData.nativePassData.Length; nativePassIndex++) + { + int firstNativeSubPass = contextData.nativePassData[nativePassIndex].firstNativeSubPass; + // Does this native pass have any sub passes. + if (firstNativeSubPass >= 0) + { + int firstGraphPass = contextData.nativePassData[nativePassIndex].firstGraphPass; + int graphPassIndex = 0; + for (int nativeSubPassIndex = 0; nativeSubPassIndex < contextData.nativePassData[nativePassIndex].numNativeSubPasses; nativeSubPassIndex++) + { + // Start with the MVPVV compatible flag set so that it can be used for the & operation later + SubPassFlags extendedSubPassFlags = SubPassFlags.MultiviewRenderRegionsCompatible; + // Iterate over all graph passes that got merged into this sub pass + while ((graphPassIndex < contextData.nativePassData[nativePassIndex].numGraphPasses) && (contextData.passData[graphPassIndex + firstGraphPass].nativeSubPassIndex == nativeSubPassIndex)) + { + if (contextData.passData[graphPassIndex + firstGraphPass].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.TileProperties)) + { + extendedSubPassFlags |= SubPassFlags.TileProperties; + } + // A native sub pass is MultiviewRenderRegionsCompatible only if all of its graph passes are compatible + if (!contextData.passData[graphPassIndex + firstGraphPass].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultiviewRenderRegionsCompatible)) + { + extendedSubPassFlags &= ~SubPassFlags.MultiviewRenderRegionsCompatible; + } + graphPassIndex++; + } + contextData.nativeSubPassData.ElementAt(firstNativeSubPass + nativeSubPassIndex).flags |= extendedSubPassFlags; + } + } + } + } + + public void Compile(RenderGraphResourceRegistry resources) + { + ValidatePasses(); + + SetupContextData(resources); + + BuildGraph(); + + CullUnusedRenderGraphPasses(); + + TryMergeNativePasses(); + + HandleExtendedFeatureFlags(); + + FindResourceUsageRangeAndSynchronization(); + + DetectMemoryLessResources(); + + PrepareNativeRenderPasses(); + + if (graph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation) + PropagateTextureUVOrigin(); + + CompactNonCulledPassesForRasterPasses(); + } + + public void Clear(bool clearContextData) + { + if (clearContextData) + contextData.Clear(); + + m_HasSideEffectPassIdCullingStack.Clear(); + + for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) + m_UnusedVersionedResourceIdCullingStacks[type].Clear(); + + foreach (var resListPerPassId in m_DelayedLastUseListPerPassMap) + resListPerPassId.Value.Clear(); + + m_DelayedLastUseListPerPassMap.Clear(); + } + + void SetPassStatesForNativePass(int nativePassId) + { + NativePassData.SetPassStatesForNativePass(contextData, nativePassId); + } + + internal enum NativeCompilerProfileId + { + NRPRGComp_PrepareNativePass, + NRPRGComp_SetupContextData, + NRPRGComp_BuildGraph, + NRPRGComp_CullNodes, + NRPRGComp_TryMergeNativePasses, + NRPRGComp_FindResourceUsageRanges, + NRPRGComp_DetectMemorylessResources, + NRPRGComp_PropagateTextureUVOrigin, + NRPRGComp_ExecuteInitializeResources, + NRPRGComp_ExecuteBeginRenderpassCommand, + NRPRGComp_ExecuteDestroyResources, + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void ValidatePasses() + { + if (RenderGraph.enableValidityChecks) + { + int tilePropertiesPassIndex = -1; + for (int passId = 0; passId < graph.m_RenderPasses.Count; passId++) + { + if (graph.m_RenderPasses[passId].extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.TileProperties)) + { + if (tilePropertiesPassIndex > -1) + { + throw new Exception($"ExtendedFeatureFlags.TileProperties can only be set once per render graph (render graph {graph.debugName}, pass {graph.m_RenderPasses[passId].name}), previously set at (pass {graph.m_RenderPasses[tilePropertiesPassIndex].name})."); + } + tilePropertiesPassIndex = passId; + } + } + } + } + + void SetupContextData(RenderGraphResourceRegistry resources) + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_SetupContextData))) + { + contextData.Initialize(resources, k_EstimatedPassCount); + } + } + + // Returns true if the RasterFragmentList is successfully set up + bool TrySetupRasterFragmentList(ref PassData ctxPass, ref RenderGraphPass inputPass, out string errorMessage) + { + errorMessage = null; + var ctx = contextData; + // Grab offset in context fragment list to begin building the fragment list + ctxPass.firstFragment = ctx.fragmentData.Length; + + // Depth attachment is always at index 0 + if (inputPass.depthAccess.textureHandle.handle.IsValid()) + { + ctxPass.fragmentInfoHasDepth = true; + + if (ctx.TryAddToFragmentList(inputPass.depthAccess, ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) + { + ctxPass.TryAddFragment(inputPass.depthAccess.textureHandle.handle, ctx, out errorMessage); + } + + if (errorMessage != null) + { + errorMessage = + $"when trying to add depth attachment of type {inputPass.depthAccess.textureHandle.handle.type} at index {inputPass.depthAccess.textureHandle.handle.index} - {errorMessage}"; + return false; + } + } + + for (var ci = 0; ci < inputPass.colorBufferMaxIndex + 1; ++ci) + { + // Skip unused color slots + if (!inputPass.colorBufferAccess[ci].textureHandle.handle.IsValid()) continue; + + if (ctx.TryAddToFragmentList(inputPass.colorBufferAccess[ci], ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) + { + ctxPass.TryAddFragment(inputPass.colorBufferAccess[ci].textureHandle.handle, ctx, out errorMessage); + } + + if (errorMessage != null) + { + errorMessage = + $"when trying to add render attachment of type {inputPass.colorBufferAccess[ci].textureHandle.handle.type} at index {inputPass.colorBufferAccess[ci].textureHandle.handle.index} - {errorMessage}"; + return false; + } + } + + // shading rate image - this is a specific type of attachment (more of an image resource that can't be sampled, only used by the rasterizer) + if (inputPass.hasShadingRateImage && inputPass.shadingRateAccess.textureHandle.handle.IsValid()) + { + if (ctx.TryAddToFragmentList(inputPass.shadingRateAccess, ctxPass.firstFragment, ctxPass.numFragments, out errorMessage)) + { + ctxPass.shadingRateImageIndex = ctx.fragmentData.Length - 1; + } + + if (errorMessage != null) + { + errorMessage = + $"when trying to add VRS attachment of type {inputPass.shadingRateAccess.textureHandle.handle.type} at index {inputPass.shadingRateAccess.textureHandle.handle.index} - {errorMessage}"; + return false; + } + } + + // Grab offset in context fragment list to begin building the fragment input list + ctxPass.firstFragmentInput = ctx.fragmentData.Length; + + for (var ci = 0; ci < inputPass.fragmentInputMaxIndex + 1; ++ci) + { + // Skip unused fragment input slots + if (!inputPass.fragmentInputAccess[ci].textureHandle.IsValid()) continue; + + if (ctx.TryAddToFragmentList(inputPass.fragmentInputAccess[ci], ctxPass.firstFragmentInput, ctxPass.numFragmentInputs, out errorMessage)) + { + ctxPass.TryAddFragmentInput(inputPass.fragmentInputAccess[ci].textureHandle.handle, ctx, out errorMessage); + } + + if (errorMessage != null) + { + errorMessage = + $"when trying to add input attachment of type {inputPass.fragmentInputAccess[ci].textureHandle.handle.type} at index {inputPass.fragmentInputAccess[ci].textureHandle.handle.index} - {errorMessage}"; + return false; + } + } + + // Grab offset in context random write list to begin building the per pass random write lists + ctxPass.firstRandomAccessResource = ctx.randomAccessResourceData.Length; + + for (var ci = 0; ci < inputPass.randomAccessResourceMaxIndex + 1; ++ci) + { + ref var uav = ref inputPass.randomAccessResource[ci]; + + // Skip unused random write slots + if (!uav.h.IsValid()) continue; + + if (ctx.TryAddToRandomAccessResourceList(uav.h, ci, uav.preserveCounterValue, ctxPass.firstRandomAccessResource, ctxPass.numRandomAccessResources, out errorMessage)) + { + ctxPass.AddRandomAccessResource(); + } + + if (errorMessage != null) + { + errorMessage = $"when trying to add random access attachment of type {uav.h.type} at index {uav.h.index} - {errorMessage}"; + return false; + } + } + + // This is suspicious, there are frame buffer fetch inputs but nothing is output. We don't allow this for now. + // In theory you could fb-fetch inputs and write something to a uav and output nothing? This needs to be investigated + // so don't allow it for now. + if (ctxPass.numFragments == 0) + { + Debug.Assert(ctxPass.numFragmentInputs == 0); + } + + return true; + } + + void BuildGraph() + { + var ctx = contextData; + List passes = graph.m_RenderPasses; + + // Not clearing data, we will do it right after in the for loop + // This is to prevent unnecessary costly copies of pass struct (128bytes) + ctx.passData.ResizeUninitialized(passes.Count); + + // Build up the context graph and keep track of nodes we encounter that can't be culled + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_BuildGraph))) + { + for (int passId = 0; passId < passes.Count; passId++) + { + var inputPass = passes[passId]; + + // Accessing already existing passData in place in the container through reference to avoid deep copy + // Make sure everything is reset and initialized or we will use obsolete data from previous frame + ref var ctxPass = ref ctx.passData.ElementAt(passId); + ctxPass.ResetAndInitialize(inputPass, passId); + + ctx.passNames.Add(new Name(inputPass.name, true)); + + if (ctxPass.hasSideEffects) + { + m_HasSideEffectPassIdCullingStack.Push(passId); + } + + // Set up the list of fragment attachments for this pass + // Note: This doesn't set up the resource reader/writer list as the fragment attachments + // will also be in the pass read/write lists accordingly + if (ctxPass.type == RenderGraphPassType.Raster) + { + if (!TrySetupRasterFragmentList(ref ctxPass, ref inputPass, out var errorMessage)) + { + throw new Exception($"In pass '{inputPass.name}', {errorMessage}"); + } + } + + // Set up per resource type read/write lists for this pass + ctxPass.firstInput = ctx.inputData.Length; // Grab offset in context input list + ctxPass.firstOutput = ctx.outputData.Length; // Grab offset in context output list + for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) + { + var resourceWrite = inputPass.resourceWriteLists[type]; + var resourceWriteCount = resourceWrite.Count; + for (var i = 0; i < resourceWriteCount; ++i) + { + var resource = resourceWrite[i]; + + // Writing to an imported resource is a side effect so mark the pass if needed + ref var resData = ref ctx.UnversionedResourceData(resource); + if (resData.isImported) + { + if (!ctxPass.hasSideEffects) + { + ctxPass.hasSideEffects = true; + m_HasSideEffectPassIdCullingStack.Push(passId); + } + } + + // Mark this pass as writing to this version of the resource + ctx.resources[resource].SetWritingPass(ctx, resource, passId); + + ctx.outputData.Add(new PassOutputData(resource)); + + ctxPass.numOutputs++; + } + + var resourceRead = inputPass.resourceReadLists[type]; + var resourceReadCount = resourceRead.Count; + for (var i = 0; i < resourceReadCount; ++i) + { + var resource = resourceRead[i]; + + // Mark this pass as reading from this version of the resource + ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs); + + ctx.inputData.Add(new PassInputData(resource)); + + ctxPass.numInputs++; + } + + var resourceTrans = inputPass.transientResourceList[type]; + var resourceTransCount = resourceTrans.Count; + + for (var i = 0; i < resourceTransCount; ++i) + { + var resource = resourceTrans[i]; + + // Mark this pass as reading from this version of the resource + ctx.resources[resource].RegisterReadingPass(ctx, resource, passId, ctxPass.numInputs); + + ctx.inputData.Add(new PassInputData(resource)); + + ctxPass.numInputs++; + + // Mark this pass as writing to this version of the resource + ctx.resources[resource].SetWritingPass(ctx, resource, passId); + + ctx.outputData.Add(new PassOutputData(resource)); + + ctxPass.numOutputs++; + } + + // For raster passes, we do an extra step to monitor textures sampled in the pass + // It can be a breaking change reason later on when building a native render pass + if (type == (int)RenderGraphResourceType.Texture && ctxPass.type == RenderGraphPassType.Raster) + { + ctxPass.firstSampledOnlyRaster = ctx.sampledData.Length; + foreach (ref readonly var input in ctxPass.Inputs(ctx)) + { + // Check if this input is the shading rate image + if (!ctxPass.IsUsedAsFragment(input.resource, ctx)) + { + ctx.sampledData.Add(input.resource); + ctxPass.numSampledOnlyRaster++; + } + } + } + } + } + } + } + + void CullUnusedRenderGraphPasses() + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_CullNodes))) + { + if (graph.disablePassCulling) + return; + + // Must come first + CullRenderGraphPassesWithNoSideEffect(); + + // Second step of the algorithm that comes later + CullRenderGraphPassesWritingOnlyUnusedResources(); + } + } + + void CullRenderGraphPassesWithNoSideEffect() + { + var ctx = contextData; + + // Cull all passes first + ctx.CullAllPasses(true); + + // Flood fill downstream algorithm using BFS, + // starting from the passes with side effects (writting to imported texture, not allowed to be culled, globals modification...) + // to all their dependencies + while (m_HasSideEffectPassIdCullingStack.Count != 0) + { + int passId = m_HasSideEffectPassIdCullingStack.Pop(); + + ref var passData = ref ctx.passData.ElementAt(passId); + + // We already found this node through another dependency chain + if (!passData.culled) continue; + + // Flow upstream from this node + foreach (ref readonly var input in passData.Inputs(ctx)) + { + ref var inputVersionedDataRes = ref ctx.resources[input.resource]; + + if (inputVersionedDataRes.written) + { + m_HasSideEffectPassIdCullingStack.Push(inputVersionedDataRes.writePassId); + } + } + + // We need this node, don't cull it + passData.culled = false; + } + + // Update graph based on freshly culled nodes, remove any connection to them + // We start from the latest passes to the first ones as we might need to decrement the version number of unwritten resources + var numPasses = ctx.passData.Length; + for (int passIndex = numPasses - 1; passIndex >= 0; passIndex--) + { + ref readonly var pass = ref ctx.passData.ElementAt(passIndex); + + // Remove the connections from the list so they won't be visited again + if (pass.culled) + { + pass.DisconnectFromResources(ctx); + } + } + } + + void CullRenderGraphPassesWritingOnlyUnusedResources() + { + var ctx = contextData; + + var numPasses = ctx.passData.Length; + for (int passIndex = 0; passIndex < numPasses; passIndex++) + { + ref var passData = ref ctx.passData.ElementAt(passIndex); + + // Use the generic tag to monitor the number of written resources that are used + passData.tag = passData.numOutputs; + + // Find all resources that are written by a pass but not read at all and add them to the stacks + foreach (ref readonly var output in passData.Outputs(ctx)) + { + ref readonly var outputResource = ref output.resource; + ref var outputVersionedDataRes = ref ctx.resources[outputResource]; + + if (outputVersionedDataRes.numReaders == 0) + m_UnusedVersionedResourceIdCullingStacks[outputResource.iType].Push(outputResource); + } + } + + // Go through each stack of unused resources and try to cull their producer + for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) + { + var unusedVersionedResourceIdCullingStack = m_UnusedVersionedResourceIdCullingStacks[type]; + + // Goal is to find the producers of the unused resources and culled them if they only write to unused resources + while (unusedVersionedResourceIdCullingStack.Count != 0) + { + var unusedResource = unusedVersionedResourceIdCullingStack.Pop(); + + ref var unusedUnversionedDataRes = ref ctx.resources.unversionedData[type].ElementAt(unusedResource.index); + if (unusedUnversionedDataRes.isImported) continue; // Not always unused as someone can read it outside the graph + + ref var unusedVersionedDataRes = ref ctx.resources[unusedResource]; + ref var producerData = ref ctx.passData.ElementAt(unusedVersionedDataRes.writePassId); + if (producerData.culled) continue; // Producer has been culled already + + // Decrement the number of written resources that are used for this pass + producerData.tag--; + + Debug.Assert(producerData.tag >= 0); + + // Producer is not necessary anymore, as it only writes to unused resources and has no side effects + if (producerData.tag == 0 && !producerData.hasSideEffects) + { + producerData.culled = true; + producerData.DisconnectFromResources(ctx, unusedVersionedResourceIdCullingStack, type); + } + else + { + // Producer is still necessary for now, but if the previous version is only implicitly read by it to write the unused resource + // then we can consider this version useless as well and add it to the stack. + // We purposefully keep the connection between the producer and this resource nevertheless to ensure proper lifetime handling and attachment setup. + // A more optimal approach memory-wise would be to cut the dependency, decrease the latestVersionNumber of the resource and release it earlier + // but we then need to create a transient resource with the right attachment properties and attach it to the non-culled producer or the native render pass setup will be incorrect. + + // We always add written resource to the stack so versionedIndex > 0 + var prevVersionedRes = new ResourceHandle(unusedResource, unusedResource.version - 1); + + bool isImplicitRead = graph.m_RenderPasses[producerData.passId].implicitReadsList.Contains(prevVersionedRes); + + if (isImplicitRead) + { + ref var prevVersionedDataRes = ref ctx.resources[prevVersionedRes]; + + // We add the previous version of the resource to the stack IF no other pass than current producer needs it + if (prevVersionedDataRes.written && prevVersionedDataRes.numReaders == 1) + { + unusedVersionedResourceIdCullingStack.Push(prevVersionedRes); + } + } + } + } + } + } + + void TryMergeNativePasses() + { + var ctx = contextData; + + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_TryMergeNativePasses))) + { + // Try to merge raster passes into the currently active native pass. This will not do pass reordering yet + // so it is simply greedy trying to add the next pass to a currently active one. + // In the future we want to try adding any available (i.e. that has no data dependencies on any future results) future pass + // and allow merging that into the pass thus greedily reordering passes. But reordering requires a lot of API validation + // that ensures rendering behaves accordingly with reordered passes so we don't allow that for now. + + // !!! Compilation caching warning !!! + // Merging of passes is highly dependent on render texture properties. + // When caching the render graph compilation, we hash a subset of those render texture properties to make sure we recompile the graph if needed. + // We only hash a subset for performance reason so if you add logic here that will change the behavior of pass merging, + // make sure that the relevant properties are hashed properly. See RenderGraphPass.ComputeHash() + + int activeNativePassId = -1; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + bool generatePassBreakAudits = RenderGraphDebugSession.hasActiveDebugSession || s_ForceGenerateAuditsForTests; +#endif + int indexSinceLastCulledPass = 0; + bool passWasCulled = false; + bool nonCulledPassIndicesListWasCleared = false; + + for (var passIdx = 0; passIdx < ctx.passData.Length; ++passIdx) + { + ref var passToAdd = ref ctx.passData.ElementAt(passIdx); + + // If the pass has been culled, just ignore it + if (passToAdd.culled) + { + passWasCulled = true; + continue; + } + + // If no active pass, there is nothing to merge... + if (activeNativePassId == -1) + { + //If raster, start a new native pass with the current pass + if (passToAdd.type == RenderGraphPassType.Raster) + { + // Allocate a stand-alone native renderpass based on the current pass + ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx)); + passToAdd.nativePassIndex = ctx.nativePassData.LastIndex(); + activeNativePassId = passToAdd.nativePassIndex; + + indexSinceLastCulledPass = passIdx; + passWasCulled = false; + } + } + // There is an native pass currently open, try to add the current graph pass to it + else + { + var mergeTestResult = graph.disablePassMerging ? new PassBreakAudit(PassBreakReason.PassMergingDisabled, passIdx) + : NativePassData.TryMerge(contextData, activeNativePassId, passIdx); + + // Merge failed, close current native render pass and create a new one + if (mergeTestResult.reason != PassBreakReason.Merged) + { + SetPassStatesForNativePass(activeNativePassId); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generatePassBreakAudits) + { + ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId); + nativePassData.breakAudit = mergeTestResult; + } +#endif + if (mergeTestResult.reason == PassBreakReason.NonRasterPass) + { + // Non-raster pass, no active native pass at all + activeNativePassId = -1; + } + else + { + // Raster but cannot be merged, allocate a new stand-alone native renderpass based on the current pass + ctx.nativePassData.Add(new NativePassData(ref passToAdd, ctx)); + passToAdd.nativePassIndex = ctx.nativePassData.LastIndex(); + activeNativePassId = passToAdd.nativePassIndex; + } + + if (passWasCulled) + { + CollectNonCulledPassIndicesForRasterPasses(passIdx, indexSinceLastCulledPass, mergeTestResult.reason != PassBreakReason.NonRasterPass, !nonCulledPassIndicesListWasCleared); + passWasCulled = false; + nonCulledPassIndicesListWasCleared = true; + } + + indexSinceLastCulledPass = passIdx; + } + } + } + + // Handle the last native pass + if (passWasCulled) + { + CollectNonCulledPassIndicesForRasterPasses(ctx.passData.Length, indexSinceLastCulledPass, clearList: !nonCulledPassIndicesListWasCleared); + nonCulledPassIndicesListWasCleared = true; + } + + // We need to clear this data to avoid reusing stale data from a previously compiled graph. + if (!nonCulledPassIndicesListWasCleared && m_NonCulledPassIndicesForRasterPasses.IsCreated) + { + m_NonCulledPassIndicesForRasterPasses.Clear(); + m_NonCulledPassIndicesForRasterPasses.SetCapacity(ctx.passData.Length); + } + + if (activeNativePassId >= 0) + { + // "Close" the last native pass by marking the last graph pass as end + SetPassStatesForNativePass(activeNativePassId); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generatePassBreakAudits) + { + ref var nativePassData = ref contextData.nativePassData.ElementAt(activeNativePassId); + nativePassData.breakAudit = new PassBreakAudit(PassBreakReason.EndOfGraph, -1); + } +#endif + } + } + } + + void CollectNonCulledPassIndicesForRasterPasses(int currentPassIdx, int indexSinceLastCulledPass, bool usePreviousNativePass = false, bool clearList = false) + { + var ctx = contextData; + + // In some cases, we create a new native render pass (we allocate a new stand-alone native renderpass based on the current pass) + // but we need to update the data of the previous native pass and not the newly one created. + var indexLastNativePassData = (usePreviousNativePass) ? ctx.nativePassData.LastIndex() - 1 : ctx.nativePassData.LastIndex(); + + // In case we have a graph without any render pass. + if (indexLastNativePassData == -1) + return; + + // Filling the attachments array to be sent to the rendering command buffer + if (!m_NonCulledPassIndicesForRasterPasses.IsCreated) + { + m_NonCulledPassIndicesForRasterPasses = new NativeList(ctx.passData.Length, Allocator.Persistent); + } + else if (clearList) + { + m_NonCulledPassIndicesForRasterPasses.Clear(); + m_NonCulledPassIndicesForRasterPasses.SetCapacity(ctx.passData.Length); + } + + ref var lastNativePassData = ref ctx.nativePassData.ElementAt(indexLastNativePassData); + lastNativePassData.firstCompactedNonCulledRasterPass = m_NonCulledPassIndicesForRasterPasses.Length; + + // The native pass has at least one pass culled, so we iterate over each pass to retrieve + // the index of the non culled pass, so they can be copied later on in a new NativeList that + // will be contiguous in memory. + for (var nonCulledPassIdx = 0; nonCulledPassIdx < currentPassIdx - indexSinceLastCulledPass; ++nonCulledPassIdx) + { + ref var passToCopy = ref ctx.passData.ElementAt(indexSinceLastCulledPass + nonCulledPassIdx); + + if (!passToCopy.culled) + { + m_NonCulledPassIndicesForRasterPasses.Add(indexSinceLastCulledPass + nonCulledPassIdx); + } + } + + lastNativePassData.lastCompactedNonCulledRasterPass = m_NonCulledPassIndicesForRasterPasses.Length - 1; + } + + // Must be called at the end of the compilation, when PassData is not modified anymore. + void CompactNonCulledPassesForRasterPasses() + { + if (!m_NonCulledPassIndicesForRasterPasses.IsCreated || m_NonCulledPassIndicesForRasterPasses.Length == 0) + return; + + var ctx = contextData; + ctx.compactedNonCulledRasterPasses.ResizeUninitialized(m_NonCulledPassIndicesForRasterPasses.Length); + + // Copy and cache only the PassData that were not contiguous in memory because of culling. + // They are copied in a new NativeArray that is contiguous in memory so we avoid further copies + // later at many places (Initialize, Destroy, etc.) by using directly a ReadOnlySpan of this array. + for (int i = 0; i < m_NonCulledPassIndicesForRasterPasses.Length; ++i) + { + ctx.compactedNonCulledRasterPasses[i] = ctx.passData.ElementAt(m_NonCulledPassIndicesForRasterPasses[i]); + } + } + + bool FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref PassData startAsyncPass, out int firstPassIdAwaiting) + { + var ctx = contextData; + + Debug.Assert(startAsyncPass.asyncCompute && !startAsyncPass.culled); + + firstPassIdAwaiting = startAsyncPass.awaitingMyGraphicsFencePassId; + + // This async pass has no one waiting for it, try the next async passes + if (firstPassIdAwaiting == -1) + { + var nextPassIndex = startAsyncPass.passId + 1; + var lastPassIndex = ctx.passData.Length - 1; + + // Find the first async pass that is synchronized by the graphics queue + while (firstPassIdAwaiting == -1 && nextPassIndex <= lastPassIndex) + { + ref var nextPass = ref ctx.passData.ElementAt(nextPassIndex); + + if (nextPass.asyncCompute && !nextPass.culled) + firstPassIdAwaiting = nextPass.awaitingMyGraphicsFencePassId; + + nextPassIndex++; + } + + // We didn't find any fence, this should not happen? + if (nextPassIndex > lastPassIndex) + { + // For now we fallback to the last pass of the graph + firstPassIdAwaiting = lastPassIndex; + return false; + } + } + + // Found a pass awaiting a fence + return true; + } + + int FindFirstNonCulledPassIdGoingBackward(int startPassId, bool startPassIsIncluded) + { + var ctx = contextData; + + Debug.Assert(startPassId >= 0 && startPassId < ctx.passData.Length); + + var currPassId = startPassIsIncluded ? startPassId : Math.Max(0, startPassId - 1); + + ref var currPass = ref ctx.passData.ElementAt(currPassId); + + // If this pre pass is culled, fallback to the first previous one not culled + while (currPass.culled && currPassId > 0) + { + currPass = ref ctx.passData.ElementAt(--currPassId); + } + + return currPass.passId; + } + + void FindResourceUsageRangeAndSynchronization() + { + var ctx = contextData; + + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_FindResourceUsageRanges))) + { + // Algorithm is in two steps, traversing the list of passes twice + + // First forward traversal: + // - we find the passes that first use a resource + // - we increase the refcount of the last version for each resource used + // - we find where fences must be added in case of async compute/gfx queues and the related async pass dependencies + for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++) + { + ref var pass = ref ctx.passData.ElementAt(passIndex); + + if (pass.culled) + continue; + + // In case of an async pass, we need to extend the lifetime of the resource to the first pass on the graphics queue that waits for this async pass to be completed. + // By doing so, we ensure that the resource will not be released back to the pool right after adding the async pass commands to the async queue, + // as it could create a data race condition if a non async pass reuses the resource from the pool while the async pass is still processing it + // As contextData must be filled incrementally per pass, we store temporarily these delayed releases in a list. + // Here we clear this list before using it later in the second foward traversal. + ClearDelayedLastUseListAtPass(passIndex); + + pass.waitOnGraphicsFencePassId = -1; + pass.awaitingMyGraphicsFencePassId = -1; + pass.insertGraphicsFence = false; + + // Loop over all the resources this pass needs (=inputs) + foreach (ref readonly var input in pass.Inputs(ctx)) + { + var inputResource = input.resource; + ref var pointTo = ref ctx.UnversionedResourceData(inputResource); + ref var pointToVer = ref ctx.VersionedResourceData(inputResource); + pointTo.lastUsePassID = -1; + + // If nobody else is using it yet, + // mark this pass as the first using the resource. + // It can happen that two passes use v0, e.g.: + // pass1.UseTex(v0,Read) -> this will clear the pass but keep it at v0 + // pass2.UseTex(v0,Read) -> "reads" v0 + if (pointTo.firstUsePassID < 0) + { + pointTo.firstUsePassID = pass.passId; + pass.AddFirstUse(inputResource, ctx); + } + + // This pass uses the last version of a resource increase the ref count of this resource + var last = pointTo.latestVersionNumber; + if (last == inputResource.version) + { + pointTo.tag++; //Refcount of how many passes are using the last version of a resource + } + + // Verify if this pass needs to wait on a fence due to its inputs + // If no RG pass writes to the resource, no need to wait for anyone + if (pointToVer.written) + { + ref var writingPass = ref ctx.passData.ElementAt(pointToVer.writePassId); + if (writingPass.asyncCompute != pass.asyncCompute) + { + // Find the last pass on the opposite queue that the current pass must wait on + var currWaitForPassId = pass.waitOnGraphicsFencePassId; + pass.waitOnGraphicsFencePassId = Math.Max(writingPass.passId, currWaitForPassId); + } + } + } + + //Also look at outputs (but with version 1) for edge case were we do a Write (but no read) to a texture and the pass is manually excluded from culling + //As it isn't read it won't be in the inputs array with V0 + foreach (ref readonly var output in pass.Outputs(ctx)) + { + var outputResource = output.resource; + ref var pointTo = ref ctx.UnversionedResourceData(outputResource); + ref var pointToVer = ref ctx.VersionedResourceData(outputResource); + + // If nobody else is using it yet (no explicit read), + // Mark this pass as the first using the resource. + // It can happen that two passes use v0, e.g.: + // pass1.UseTex(v0, Write) -> implicit read of v0, writes v1 - culled because none explicitly reads v1 + // pass3.UseTex(v1, Write) -> implicit read of v1, writes v2 - not culled because of unrelated reason + if (pointTo.firstUsePassID < 0) + { + pointTo.firstUsePassID = pass.passId; + pass.AddFirstUse(outputResource, ctx); + } + + // This pass outputs the last version of a resource track that + var last = pointTo.latestVersionNumber; + if (last == outputResource.version) + { + Debug.Assert(pointTo.lastWritePassID == -1); // Only one can be the last writer + pointTo.lastWritePassID = pass.passId; + } + + // Resolve if this pass should insert a fence for its outputs + var numReaders = pointToVer.numReaders; + for (var i = 0; i < numReaders; ++i) + { + var readerIndex = ctx.resources.IndexReader(outputResource, i); + ref var readerData = ref ctx.resources.readerData[outputResource.iType].ElementAt(readerIndex); + ref var readerPass = ref ctx.passData.ElementAt(readerData.passId); + if (pass.asyncCompute != readerPass.asyncCompute) + { + // A subsequent pass on the opposite queue will read this resource written by the current pass, + // so this subsequent pass needs to wait for the completion of the current pass + // to do so, the current pass will insert a fence on its queue after its execution + pass.insertGraphicsFence = true; + + // Different async passes can wait for different resources + // Find the first pass on the opposite queue that will wait for this fence + var currFirstPassId = pass.awaitingMyGraphicsFencePassId; + pass.awaitingMyGraphicsFencePassId = currFirstPassId == -1 ? readerData.passId : Math.Min(currFirstPassId, readerData.passId); + } + } + } + } + + // Second forward traversal: + // - we decrease the refcount to detect which is the last pass using the last version of a resource, i.e when we can release it + // - in case of async processing, we must delay the release to the first pass on gfx queue waiting for a fence + for (int passIndex = 0; passIndex < ctx.passData.Length; passIndex++) + { + ref var pass = ref ctx.passData.ElementAt(passIndex); + + if (pass.culled) + continue; + + bool isAsync = pass.asyncCompute; + + foreach (ref readonly var input in pass.Inputs(ctx)) + { + var inputResource = input.resource; + ref var pointTo = ref ctx.UnversionedResourceData(inputResource); + var last = pointTo.latestVersionNumber; + if (last == inputResource.version) + { + var refC = pointTo.tag - 1; //Decrease refcount this pass is done using it + if (refC == 0) // We're the last pass done using it, this pass should destroy it. + { + if (isAsync) + { + // If no fence found, we fallback to the last non culled pass of the graph on graphics queue, not ideal but safe + bool foundFence = FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref pass, out int firstWaitingOrLastPassId); + var delayLastUsedPassId = FindFirstNonCulledPassIdGoingBackward(firstWaitingOrLastPassId, !foundFence); + pointTo.lastUsePassID = delayLastUsedPassId; + AddDelayedLastUseToPass(inputResource, delayLastUsedPassId); + } + else + { + pointTo.lastUsePassID = pass.passId; + pass.AddLastUse(inputResource, ctx); + } + } + + pointTo.tag = refC; + } + } + + // We're outputting a resource that is never used. + // This can happen if this pass has multiple outputs and only a portion of them are used + // as some are used, the whole pass is not culled but the unused output still should be freed + foreach (ref readonly var output in pass.Outputs(ctx)) + { + var outputResource = output.resource; + ref var pointTo = ref ctx.UnversionedResourceData(outputResource); + ref var pointToVer = ref ctx.VersionedResourceData(outputResource); + var last = pointTo.latestVersionNumber; + if (last == outputResource.version && pointToVer.numReaders == 0) + { + if (isAsync) + { + // If no fence found, we fallback to the last non culled pass of the graph, not ideal but safe + bool foundFence = FindFirstPassIdOnGraphicsQueueAwaitingFenceGoingForward(ref pass, out int firstWaitingOrLastPassId); + var delayLastUsedPassId = FindFirstNonCulledPassIdGoingBackward(firstWaitingOrLastPassId, !foundFence); + pointTo.lastUsePassID = delayLastUsedPassId; + AddDelayedLastUseToPass(outputResource, delayLastUsedPassId); + } + else + { + pointTo.lastUsePassID = pass.passId; + pass.AddLastUse(outputResource, ctx); + } + } + } + + // Add any potential delayed resource releases to the contextData + AddLastUseFromDelayedList(ref pass); + } + } + } + + void ClearDelayedLastUseListAtPass(int passId) + { + if (m_DelayedLastUseListPerPassMap.TryGetValue(passId, out var lastUseListForPassId)) + { + lastUseListForPassId.Clear(); + } + } + + void AddDelayedLastUseToPass(in ResourceHandle releaseResource, int passId) + { + if (!m_DelayedLastUseListPerPassMap.TryGetValue(passId, out var lastUseListForPassId)) + { + lastUseListForPassId = new List(); + m_DelayedLastUseListPerPassMap.Add(passId, lastUseListForPassId); + } + + lastUseListForPassId.Add(releaseResource); + } + + public void AddLastUseFromDelayedList(ref PassData passData) + { + if (m_DelayedLastUseListPerPassMap.TryGetValue(passData.passId, out var lastUseListForPassId)) + { + foreach (var resource in lastUseListForPassId) + { + passData.AddLastUse(resource, contextData); + } + + lastUseListForPassId.Clear(); + } + } + + void PrepareNativeRenderPasses() + { + // Prepare all native render pass execution info: + for (var passIdx = 0; passIdx < contextData.nativePassData.Length; ++passIdx) + { + ref var nativePassData = ref contextData.nativePassData.ElementAt(passIdx); + DetermineLoadStoreActions(ref nativePassData); + } + } + + void PropagateTextureUVOrigin() + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_PropagateTextureUVOrigin))) + { + // Work backwards through the native pass list and propagate the texture uv origin we store with to + // any texture attachments that are not explicitly known (usually intermediate memoryless attachments). + for (int passIdx = contextData.nativePassData.Length - 1; passIdx >= 0; --passIdx) + { + ref NativePassData nativePassData = ref contextData.nativePassData.ElementAt(passIdx); + + // Find a texture attachment that is storing to find out the orientation for this pass. + int attachmentsCount = nativePassData.attachments.size; + int firstStoreAttachmentIndex = 0; + TextureUVOriginSelection storeUVOrigin = TextureUVOriginSelection.Unknown; + for (int attIdx = 0; attIdx < attachmentsCount; ++attIdx) + { + ref NativePassAttachment nativePassAttachment = ref nativePassData.attachments[attIdx]; + if (nativePassAttachment.storeAction != RenderBufferStoreAction.DontCare) + { + if (nativePassAttachment.handle.type == RenderGraphResourceType.Texture) // Only textures have orientation + { + ref ResourceUnversionedData resData = ref contextData.UnversionedResourceData(nativePassAttachment.handle); + storeUVOrigin = resData.textureUVOrigin; // Inherit the orientation of the store if we are currently storing to an unknown orientation. + firstStoreAttachmentIndex = attIdx; + break; + } + } + } + + // Update any texture attachments with an unknown uv origin to the one we are going to use for storing and validate + // we don't have a mixture of uv origins on the texture attachment list as this would mean something is going to be + // read/written upside down. + for (int attIdx = 0; attIdx < attachmentsCount; ++attIdx) + { + ref NativePassAttachment nativePassAttachment = ref nativePassData.attachments[attIdx]; + + if (nativePassAttachment.handle.type == RenderGraphResourceType.Texture) + { + ref ResourceUnversionedData resData = ref contextData.UnversionedResourceData(nativePassAttachment.handle); + if (storeUVOrigin != TextureUVOriginSelection.Unknown && resData.textureUVOrigin != TextureUVOriginSelection.Unknown && resData.textureUVOrigin != storeUVOrigin) + { + ref NativePassAttachment firstStoreNativePassAttachment = ref nativePassData.attachments[firstStoreAttachmentIndex]; + var firstStoreAttachmentName = graph.m_ResourcesForDebugOnly.GetRenderGraphResourceName(firstStoreNativePassAttachment.handle); + var name = graph.m_ResourcesForDebugOnly.GetRenderGraphResourceName(nativePassAttachment.handle); + + throw new InvalidOperationException($"From pass '{contextData.passNames[nativePassData.firstGraphPass]}' to pass '{contextData.passNames[nativePassData.lastGraphPass]}' when trying to store resource '{name}' of type {nativePassAttachment.handle.type} at index {nativePassAttachment.handle.index} - " + + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOriginStore(firstStoreAttachmentName, storeUVOrigin, name, resData.textureUVOrigin)); + } + + resData.textureUVOrigin = storeUVOrigin; + } + } + } + } + } + + static bool IsGlobalTextureInPass(RenderGraphPass pass, in ResourceHandle handle) + { + foreach (var g in pass.setGlobalsList) + { + if (g.Item1.handle.index == handle.index) + { + return true; + } + } + + return false; + } + + void DetectMemoryLessResources() + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_DetectMemorylessResources))) + { + // No need to go further if we don't support memoryless textures + if (!SystemInfo.supportsMemorylessTextures) + return; + + // Native renderpasses and create/destroy lists have now been set-up. Detect memoryless resources, i.e resources that are created/destroyed + // within the scope of an nrp + foreach (ref readonly var nativePass in contextData.NativePasses) + { + // Loop over all created resources by this nrp + for (int passIdx = nativePass.firstGraphPass; passIdx < nativePass.lastGraphPass + 1; ++passIdx) + { + ref var subPass = ref contextData.passData.ElementAt(passIdx); + if (subPass.culled) + continue; + + foreach (ref readonly var createdRes in subPass.FirstUsedResources(contextData)) + { + ref var createInfo = ref contextData.UnversionedResourceData(createdRes); + if (createdRes.type == RenderGraphResourceType.Texture && createInfo.isImported == false) + { + bool isGlobal = IsGlobalTextureInPass(graph.m_RenderPasses[subPass.passId], createdRes); + + // Note: You could think but what if the texture is used as a regular non-frambuffer attachment texture + // surely it can't be memoryless then? + // That is true, but it can never happen as the fact these passes got merged means the textures cannot be used + // as regular textures halfway through a pass. If that were the case they would never have been merged in the first place. + // Except! If the pass consists of a single pass, in that case a texture could be allocated and freed within the single pass + // This is a somewhat degenerate case (e.g. a pass with culling forced off doing a uav write that is never used anywhere) + // But to avoid execution errors we still need to create the resource in this case. + + // Check if it is in the destroy list of any of the subpasses > if yes > memoryless + for (int passIdx2 = nativePass.firstGraphPass; passIdx2 < nativePass.lastGraphPass + 1; ++passIdx2) + { + ref var subPass2 = ref contextData.passData.ElementAt(passIdx2); + if (subPass2.culled) + continue; + + foreach (ref readonly var destroyedRes in subPass2.LastUsedResources(contextData)) + { + ref var destInfo = ref contextData.UnversionedResourceData(destroyedRes); + if (destroyedRes.type == RenderGraphResourceType.Texture && destInfo.isImported == false) + { + if (createdRes.index == destroyedRes.index && !isGlobal) + { + // If a single pass in the native pass we need to check fragment attachment otherwise we're good + // we could always check this in theory but it's an optimization not to check it. + if (nativePass.numNativeSubPasses > 1 || subPass2.IsUsedAsFragment(createdRes, contextData)) + { + createInfo.memoryLess = true; + destInfo.memoryLess = true; + } + } + } + } + } + } + } + } + } + } + } + + internal static bool IsSameNativeSubPass(ref SubPassDescriptor a, ref SubPassDescriptor b) + { + const SubPassFlags k_SubPassMergeIgnoreMask = ~(SubPassFlags.TileProperties | SubPassFlags.MultiviewRenderRegionsCompatible); + // Mask out the flags we can ignore. + SubPassFlags aflags = a.flags & k_SubPassMergeIgnoreMask; + SubPassFlags bflags = b.flags & k_SubPassMergeIgnoreMask; + if (aflags != bflags + || a.colorOutputs.Length != b.colorOutputs.Length + || a.inputs.Length != b.inputs.Length) + { + return false; + } + + for (int i = 0; i < a.colorOutputs.Length; i++) + { + if (a.colorOutputs[i] != b.colorOutputs[i]) + { + return false; + } + } + + for (int i = 0; i < a.inputs.Length; i++) + { + if (a.inputs[i] != b.inputs[i]) + { + return false; + } + } + + return true; + } + + private bool ExecuteInitializeResource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in PassData pass) + { + bool haveGfxCommandsBeenAddedToCmd = false; + + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteInitializeResources))) + { + resources.forceManualClearOfResource = true; + + // For raster passes we need to create resources for all the subpasses at the beginning of the native renderpass + if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0) + { + if (pass.mergeState == PassMergeState.Begin || pass.mergeState == PassMergeState.None) + { + ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex); + foreach (ref readonly var subPass in nativePass.GraphPasses(contextData)) + { + foreach (ref readonly var res in subPass.FirstUsedResources(contextData)) + { + ref readonly var resInfo = ref contextData.UnversionedResourceData(res); + + bool usedAsFragmentThisPass = subPass.IsUsedAsFragment(res, contextData); + + // This resource is read for the first time as a regular texture and not as a framebuffer attachment + // so if requested we need to explicitly clear it, as loadAction.clear only works on framebuffer attachments + resources.forceManualClearOfResource = !usedAsFragmentThisPass; + + if (!resInfo.isImported) + { + // If the compiler has detected that this resource can be memoryless, + // we need to update the texture descriptor that will be used to create the memoryless RTHandle. + // Memoryless resources are created to allow implicit conversion from TextureHandle to RTHandle. + // Such conversions can happen on users side when manipulating texture handles. + if (resInfo.memoryLess) + { + resources.SetTextureAsMemoryLess(res); + } + + // We create the resources from a pool + // memoryless resources are also created but will not allocate in system memory + haveGfxCommandsBeenAddedToCmd |= resources.CreatePooledResource(rgContext, res.iType, res.index); + } + else // Imported resource + { + if (resInfo.clear && !resInfo.memoryLess && resources.forceManualClearOfResource) + { + haveGfxCommandsBeenAddedToCmd |= resources.ClearResource(rgContext, res.iType, res.index); + } + } + } + } + } + } + // Other passes just create them at the beginning of the individual pass + else + { + foreach (ref readonly var create in pass.FirstUsedResources(contextData)) + { + ref readonly var pointTo = ref contextData.UnversionedResourceData(create); + if (!pointTo.isImported) + { + haveGfxCommandsBeenAddedToCmd |= resources.CreatePooledResource(rgContext, create.iType, create.index); + } + else // Imported resource + { + if (pointTo.clear) + { + haveGfxCommandsBeenAddedToCmd |= resources.ClearResource(rgContext, create.iType, create.index); + } + } + } + } + + resources.forceManualClearOfResource = true; + } + + return haveGfxCommandsBeenAddedToCmd; + } + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + static LoadAudit s_EmptyLoadAudit = new LoadAudit(LoadReason.InvalidReason); + static StoreAudit s_EmptyStoreAudit = new StoreAudit(StoreReason.InvalidReason); +#endif + + void DetermineLoadStoreActions(ref NativePassData nativePass) + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_PrepareNativePass))) + { + ref readonly var firstGraphPass = ref contextData.passData.ElementAt(nativePass.firstGraphPass); + ref readonly var lastGraphPass = ref contextData.passData.ElementAt(nativePass.lastGraphPass); + + // Some passes don't do any rendering only state changes so just skip them + // If these passes trigger any drawing the raster command buffer will warn users no render targets are set-up for their rendering + if (nativePass.fragments.size <= 0) + return; + + // Some sanity checks, these should not happen + Debug.Assert(firstGraphPass.mergeState is PassMergeState.Begin or PassMergeState.None); + Debug.Assert(lastGraphPass.mergeState is PassMergeState.End or PassMergeState.None); + + ref readonly var fragmentList = ref nativePass.fragments; + + // determine load store actions + // This pass also contains the latest versions used within this pass + // As we have no pass reordering for now the merged passes are always a consecutive list and we can simply do a range + // check on the create/destroy passid to see if it's allocated/freed in this native renderpass +#if UNITY_EDITOR || DEVELOPMENT_BUILD + bool generateAudits = RenderGraphDebugSession.hasActiveDebugSession || s_ForceGenerateAuditsForTests; + ref var currLoadAudit = ref s_EmptyLoadAudit; + ref var currStoreAudit = ref s_EmptyStoreAudit; +#endif + + for (int fragmentId = 0; fragmentId < fragmentList.size; ++fragmentId) + { + ref readonly var fragment = ref fragmentList[fragmentId]; + + // Default values + ResourceHandle handle = fragment.resource; + bool memoryless = false; + int mipLevel = fragment.mipLevel; + int depthSlice = fragment.depthSlice; + // Don't care by default + RenderBufferLoadAction loadAction = RenderBufferLoadAction.DontCare; + RenderBufferStoreAction storeAction = RenderBufferStoreAction.DontCare; + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + { + nativePass.loadAudit.Add(new LoadAudit(LoadReason.FullyRewritten)); + currLoadAudit = ref nativePass.loadAudit[nativePass.loadAudit.size - 1]; // Get the last added element + + nativePass.storeAudit.Add(new StoreAudit(StoreReason.DiscardUnused)); + currStoreAudit = ref nativePass.storeAudit[nativePass.storeAudit.size - 1]; // Similarly for storeAudit + } +#endif + + // Writing by-default has to preserve the contents, think rendering only a few small triangles on top of a big framebuffer + // So it means we need to load/clear contents potentially. + // If a user pass knows it will write all pixels in a buffer (like a blit) it can use the WriteAll/Discard usage to indicate this to the graph + bool partialWrite = fragment.accessFlags.HasFlag(AccessFlags.Write) + && !fragment.accessFlags.HasFlag(AccessFlags.Discard); + + ref readonly var resourceData = ref contextData.UnversionedResourceData(fragment.resource); + bool isImported = resourceData.isImported; + + int destroyPassID = resourceData.lastUsePassID; + bool usedAfterThisNativePass = (destroyPassID >= (nativePass.lastGraphPass + 1)); + + // Read or partial-write logic + if (fragment.accessFlags.HasFlag(AccessFlags.Read) || partialWrite) + { + // The resource is already allocated before this pass so we need to load it + if (resourceData.firstUsePassID < nativePass.firstGraphPass) + { + loadAction = RenderBufferLoadAction.Load; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currLoadAudit = new LoadAudit(LoadReason.LoadPreviouslyWritten, resourceData.firstUsePassID); +#endif + // Once we decide to load a resource, we must default to the Store action if the resource is used after the current native pass. + // If we were to use the DontCare action in this case, the driver would be effectively be allowed to discard the + // contents of the resource. This is true even when we're only performing reads on it. + if (usedAfterThisNativePass) + { + storeAction = RenderBufferStoreAction.Store; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID); +#endif + } + } + // It's first used this native pass so we need to clear it so reads/partial writes return the correct clear value + // the clear colors are part of the resource description and set-up when executing the graph we don't need to care about that here. + else + { + if (isImported) + { + // Check if the user indicated he wanted clearing of his imported resource on it's first use by the graph + if (resourceData.clear) + { + loadAction = RenderBufferLoadAction.Clear; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currLoadAudit = new LoadAudit(LoadReason.ClearImported); +#endif + } + else + { + loadAction = RenderBufferLoadAction.Load; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currLoadAudit = new LoadAudit(LoadReason.LoadImported); +#endif + } + } + else + { + // Created by the graph internally clear on first read + loadAction = RenderBufferLoadAction.Clear; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currLoadAudit = new LoadAudit(LoadReason.ClearCreated); +#endif + } + } + } + + // Write logic + if (fragment.accessFlags.HasFlag(AccessFlags.Write)) + { + // Simple non-msaa case + if (nativePass.samples <= 1) + { + if (usedAfterThisNativePass) + { + // The resource is still used after this native pass so we need to store it. + storeAction = RenderBufferStoreAction.Store; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.StoreUsedByLaterPass, destroyPassID); +#endif + } + else + { + // This is the last native pass that uses the resource. + // If it's imported, we store it because its contents may be used outside the graph. + // Otherwise, we can safely discard its contents. + // + // The one exception to this, is the user declared discard flag which allows us to assume an imported + // resource is not used outside the graph. + if (isImported) + { + if (resourceData.discard) + { + storeAction = RenderBufferStoreAction.DontCare; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.DiscardImported); +#endif + } + else + { + storeAction = RenderBufferStoreAction.Store; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.StoreImported); +#endif + } + } + else + { + storeAction = RenderBufferStoreAction.DontCare; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.DiscardUnused); +#endif + } + } + } + // Complex msaa case + else + { + // The resource is still used after this native pass so we need to store it. + // as we don't know what happens with them and assume the contents are somewhow used outside the graph + // With MSAA we may access the resolved data for longer than the MSAA data so we track the destroyPass and lastPassThatNeedsUnresolved separately + // In theory the opposite could also be true (use MSAA after resolve data is no longer needed) but we consider it sufficiently strange to not + // consider it here. + storeAction = RenderBufferStoreAction.DontCare; + //Check if we're the last pass writing it by checking the output version of the current pass is the higherst version the resource will reach + bool lastWriter = (resourceData.latestVersionNumber == fragment.resource.version); + // Cheaper but same? = resourceData.lastWritePassID >= pass.firstGraphPass && resourceData.lastWritePassID < pass.firstGraphPass + pass.numSubPasses; + bool isImportedLastWriter = isImported && lastWriter; + + // Used outside this native render pass, we need to store something + if (destroyPassID >= nativePass.firstGraphPass + nativePass.numGraphPasses) + { + // Assume nothing is needed unless we are an imported texture (which doesn't require discarding) and we're the last ones writing it + bool needsMSAASamples = isImportedLastWriter && !resourceData.discard; + bool needsResolvedData = isImportedLastWriter && (resourceData.bindMS == false); + int userPassID = 0; + int msaaUserPassID = 0; + + // Check if we need msaa/resolved data by checking all the passes using this buffer + // Partial writes will register themselves as readers so this should be adequate + foreach (ref readonly var reader in contextData.Readers(fragment.resource)) + { + ref var readerPass = ref contextData.passData.ElementAt(reader.passId); + bool isFragmentUsed = readerPass.IsUsedAsFragment(fragment.resource, contextData); + + // Unsafe pass - we cannot know how it is used, so we need to both store and resolve + if (readerPass.type == RenderGraphPassType.Unsafe) + { + needsMSAASamples = true; + needsResolvedData = !resourceData.bindMS; + msaaUserPassID = reader.passId; + userPassID = reader.passId; + break; + } + // A fragment attachment use we need the msaa samples + if (isFragmentUsed) + { + needsMSAASamples = true; + msaaUserPassID = reader.passId; + } + else + { + // Used as a multisample-texture we need the msaa samples + if (resourceData.bindMS) + { + needsMSAASamples = true; + msaaUserPassID = reader.passId; + } + // Used as a regular non-multisample texture we need resolved data + else + { + needsResolvedData = true; + userPassID = reader.passId; + } + } + } + + if (needsMSAASamples && needsResolvedData) + { + storeAction = RenderBufferStoreAction.StoreAndResolve; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), + userPassID, + (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), + msaaUserPassID); +#endif + } + else if (needsResolvedData) + { + storeAction = RenderBufferStoreAction.Resolve; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), + userPassID, + StoreReason.DiscardUnused); +#endif + } + else if (needsMSAASamples) + { + storeAction = RenderBufferStoreAction.Store; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + (resourceData.bindMS ? StoreReason.DiscardBindMs : StoreReason.DiscardUnused), + -1, + (isImportedLastWriter ? StoreReason.StoreImported : StoreReason.StoreUsedByLaterPass), + msaaUserPassID); +#endif + } + else + { + Debug.Assert(false, "Resource was not destroyed but nobody seems to be using it?!"); + } + } + else if (isImportedLastWriter) + { + //It's an imported texture and we're the last ones writing it make sure to store the results + + // Used as a multisample-texture, we need the msaa samples only + if (resourceData.bindMS) + { + if (resourceData.discard) + { + storeAction = RenderBufferStoreAction.DontCare; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit(StoreReason.DiscardImported); +#endif + } + else + { + storeAction = RenderBufferStoreAction.Store; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + StoreReason.DiscardBindMs, -1, StoreReason.StoreImported); +#endif + } + } + // Used as a regular non-multisample texture, we need samples as resolved data + // we have no idea which one of them will be needed by the external users + else + { + if (resourceData.discard) + { + // Depth attachment always comes first if existing + bool isDepthAttachment = (nativePass.hasDepth && nativePass.attachments.size == 0); + + // For color attachment, we only discard the MSAA buffers and keep the resolve texture + // This is a design decision due to the restrictive ImportResourceParams API, it could be revised later + storeAction = isDepthAttachment + ? RenderBufferStoreAction.DontCare + : RenderBufferStoreAction.Resolve; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + StoreReason.DiscardImported, -1, StoreReason.DiscardImported); +#endif + } + else + { + storeAction = RenderBufferStoreAction.StoreAndResolve; +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (generateAudits) + currStoreAudit = new StoreAudit( + StoreReason.StoreImported, -1, StoreReason.StoreImported); +#endif + } + } + } + } + } + + if (resourceData.memoryLess) + { + memoryless = true; + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // Ensure load/store actions are actually valid for memory less + if (loadAction == RenderBufferLoadAction.Load) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_LoadingMemorylessResource); + if (storeAction != RenderBufferStoreAction.DontCare) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_ResolvignMemorylessResource); +#endif + } + + var newAttachment = new NativePassAttachment( + handle, + loadAction, + storeAction, + memoryless, + mipLevel, + depthSlice + ); + + nativePass.attachments.Add(newAttachment); + } + } + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + private void ValidateNativePass(in NativePassData nativePass, int width, int height, int depth, int samples, int attachmentCount) + { + if (RenderGraph.enableValidityChecks) + { + if (nativePass.attachments.size == 0 || nativePass.numNativeSubPasses == 0) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_RenderPassIsEmpty); + + if (width == 0 || height == 0 || depth == 0 || samples == 0 || nativePass.numNativeSubPasses == 0 || attachmentCount == 0) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_RenderPassHasInvalidProperties); + } + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + private void ValidateAttachment(in RenderTargetInfo attRenderTargetInfo, RenderGraphResourceRegistry resources, int nativePassWidth, int nativePassHeight, int nativePassMSAASamples, + bool isVrs, bool isShaderResolve) + { + if (RenderGraph.enableValidityChecks) + { + if (isVrs) + { + var tileSize = ShadingRateImage.GetAllocTileSize(nativePassWidth, nativePassHeight); + + if (attRenderTargetInfo.width != tileSize.x || attRenderTargetInfo.height != tileSize.y || attRenderTargetInfo.msaaSamples != 1) + { + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_ShadingRateImageAttachmentDoesNotMatch); + } + } + else + { + if (attRenderTargetInfo.width != nativePassWidth || attRenderTargetInfo.height != nativePassHeight || (attRenderTargetInfo.msaaSamples != nativePassMSAASamples && !isShaderResolve)) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_AttachmentsDoNotMatch); + } + } + } + + internal unsafe void ExecuteBeginRenderPass(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref NativePassData nativePass) + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteBeginRenderpassCommand))) + { + ref var attachments = ref nativePass.attachments; + var attachmentCount = attachments.size; + + var width = nativePass.width; + var height = nativePass.height; + var volumeDepth = nativePass.volumeDepth; + var samples = nativePass.samples; + var isShaderResolve = nativePass.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve); + ValidateNativePass(nativePass, width, height, volumeDepth, samples, attachmentCount); + + ref var nativeSubPasses = ref contextData.nativeSubPassData; + NativeArray nativeSubPassArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(nativeSubPasses.GetUnsafeReadOnlyPtr() + nativePass.firstNativeSubPass, nativePass.numNativeSubPasses, Allocator.None); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + var safetyHandle = AtomicSafetyHandle.Create(); + AtomicSafetyHandle.SetAllowReadOrWriteAccess(safetyHandle, true); + NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeSubPassArray, safetyHandle); +#endif + + if (nativePass.hasFoveatedRasterization) + { + rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Enabled); + } + + if (nativePass.hasShadingRateStates) + { + rgContext.cmd.SetShadingRateFragmentSize(nativePass.shadingRateFragmentSize); + rgContext.cmd.SetShadingRateCombiner(ShadingRateCombinerStage.Primitive, nativePass.primitiveShadingRateCombiner); + rgContext.cmd.SetShadingRateCombiner(ShadingRateCombinerStage.Fragment, nativePass.fragmentShadingRateCombiner); + } + + // Filling the attachments array to be sent to the rendering command buffer + if(!m_BeginRenderPassAttachments.IsCreated) + m_BeginRenderPassAttachments = new NativeList(FixedAttachmentArray.MaxAttachments, Allocator.Persistent); + + m_BeginRenderPassAttachments.Resize(attachmentCount, NativeArrayOptions.UninitializedMemory); + for (var i = 0; i < attachmentCount; ++i) + { + ref readonly var currAttachmentHandle = ref attachments[i].handle; + + resources.GetRenderTargetInfo(currAttachmentHandle, out var renderTargetInfo); + + bool isVrs = (i == nativePass.shadingRateImageIndex); + ValidateAttachment(renderTargetInfo, resources, width, height, samples, isVrs, isShaderResolve); + + ref var currBeginAttachment = ref m_BeginRenderPassAttachments.ElementAt(i); + currBeginAttachment = new AttachmentDescriptor(renderTargetInfo.format); + + // Set up the RT pointers + var rtHandle = resources.GetTexture(currAttachmentHandle.index); + + //HACK: Always set the loadstore target even if StoreAction == DontCare or Resolve + //and LoadAction == Clear or DontCare + //in these cases you could argue setting the loadStoreTarget to NULL and only set the resolveTarget + //but this confuses the backend (on vulkan) and in general is not how the lower level APIs tend to work. + //because of the RenderTexture duality where we always bundle store+resolve targets as one RTex + //it does become impossible to have a memoryless loadStore texture with a memoryfull resolve + //but that is why we mark this as a hack and future work to fix. + //The proper (and planned) solution would be to move away from the render texture duality. + RenderTargetIdentifier rtidAllSlices = rtHandle; + currBeginAttachment.loadStoreTarget = new RenderTargetIdentifier(rtidAllSlices, attachments[i].mipLevel, CubemapFace.Unknown, attachments[i].depthSlice); + + if (attachments[i].storeAction == RenderBufferStoreAction.Resolve || + attachments[i].storeAction == RenderBufferStoreAction.StoreAndResolve) + { + currBeginAttachment.resolveTarget = rtHandle; + } + + currBeginAttachment.loadAction = attachments[i].loadAction; + currBeginAttachment.storeAction = attachments[i].storeAction; + + // Set up clear colors if we have a clear load action + if (attachments[i].loadAction == RenderBufferLoadAction.Clear) + { + currBeginAttachment.clearColor = Color.red; + currBeginAttachment.clearDepth = 1.0f; + currBeginAttachment.clearStencil = 0; + ref readonly var desc = ref resources.GetTextureResourceDesc(currAttachmentHandle, true); + if (i == 0 && nativePass.hasDepth) + { + // TODO: There seems to be no clear depth specified ?!?! + currBeginAttachment.clearDepth = 1.0f; // desc.clearDepth; + } + else + { + currBeginAttachment.clearColor = desc.clearColor; + } + } + } + + if (nativePass.extendedFeatureFlags.HasFlag(ExtendedFeatureFlags.MultisampledShaderResolve)) + { + var lastSubpass = nativeSubPassArray[^1]; + + // All input attachments must be memoryless for the shader resolve enabled subpass. + for (int i = 0; i < lastSubpass.inputs.Length; i++) + { + int inputIndex = lastSubpass.inputs[i]; + ref var inputAttachment = ref m_BeginRenderPassAttachments.ElementAt(inputIndex); + if (inputAttachment.storeAction != RenderBufferStoreAction.DontCare) + { + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_MultisampledShaderResolveInputAttachmentNotMemoryless); + } + } + + // The last subpass in a native pass with shader resolve is required to be the subpass that handles the resolve, and this subpass can only have 1 color attachment. + if (lastSubpass.colorOutputs.Length != 1) + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_MultisampledShaderResolveInvalidAttachmentSetup); + + if (SystemInfo.supportsMultisampledShaderResolve) + { + int attachmentIndex = lastSubpass.colorOutputs[0]; + + ref var currBeginAttachment = ref m_BeginRenderPassAttachments.ElementAt(attachmentIndex); + currBeginAttachment.resolveTarget = currBeginAttachment.loadStoreTarget; + currBeginAttachment.loadStoreTarget = new RenderTargetIdentifier(BuiltinRenderTextureType.None); + currBeginAttachment.storeAction = RenderBufferStoreAction.Store; + } + } + + NativeArray attachmentDescArray = m_BeginRenderPassAttachments.AsArray(); + + var depthAttachmentIndex = nativePass.hasDepth ? 0 : -1; + + var graphPassNamesForDebugSpan = ReadOnlySpan.Empty; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (RenderGraph.enableValidityChecks) + { + graphPassNamesForDebug.Clear(); + + nativePass.GetGraphPassNames(contextData, graphPassNamesForDebug); + + int utf8CStrDebugNameLength = 0; + foreach (ref readonly Name graphPassName in graphPassNamesForDebug) + { + utf8CStrDebugNameLength += graphPassName.utf8ByteCount + 1; // +1 to add '/' between passes or the null terminator at the end + } + + var nameBytes = stackalloc byte[utf8CStrDebugNameLength]; + if (utf8CStrDebugNameLength > 0) + { + int startStr = 0; + foreach (ref readonly var graphPassName in graphPassNamesForDebug) + { + int strByteCount = graphPassName.utf8ByteCount; + System.Text.Encoding.UTF8.GetBytes(graphPassName.name.AsSpan(), new Span(nameBytes + startStr, strByteCount)); + startStr += strByteCount; + // Adding '/' in UTF8 + nameBytes[startStr++] = (byte)(0x2F); + } + + // Rewriting last '/' to be the null terminator + nameBytes[utf8CStrDebugNameLength - 1] = (byte)0; + } + + graphPassNamesForDebugSpan = new ReadOnlySpan(nameBytes, utf8CStrDebugNameLength); + } +#endif + + rgContext.cmd.BeginRenderPass(width, height, volumeDepth, samples, attachmentDescArray, depthAttachmentIndex, nativePass.shadingRateImageIndex, nativeSubPassArray, graphPassNamesForDebugSpan); + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.Release(safetyHandle); +#endif + CommandBuffer.ThrowOnSetRenderTarget = true; + } + } + + const int ArbitraryMaxNbMergedPasses = 16; + DynamicArray graphPassNamesForDebug = new DynamicArray(ArbitraryMaxNbMergedPasses); + + private void ExecuteDestroyResource(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, ref PassData pass) + { + using (new ProfilingScope(ProfilingSampler.Get(NativeCompilerProfileId.NRPRGComp_ExecuteDestroyResources))) + { + // Unsafe pass might soon use temporary render targets, + // users can also use temporary data in their render graph execute nodes using public RenderGraphObjectPool API + // In both cases, we need to release these resources after the node execution + rgContext.renderGraphPool.ReleaseAllTempAlloc(); + + if (pass.type == RenderGraphPassType.Raster && pass.nativePassIndex >= 0) + { + // For raster passes we need to destroy resources after all the subpasses at the end of the native renderpass + if (pass.mergeState == PassMergeState.End || pass.mergeState == PassMergeState.None) + { + ref var nativePass = ref contextData.nativePassData.ElementAt(pass.nativePassIndex); + foreach (ref readonly var subPass in nativePass.GraphPasses(contextData)) + { + foreach (ref readonly var res in subPass.LastUsedResources(contextData)) + { + ref readonly var resInfo = ref contextData.UnversionedResourceData(res); + if (resInfo.isImported == false) + { + resources.ReleasePooledResource(rgContext, res.iType, res.index); + } + } + } + } + } + else + { + foreach (ref readonly var destroy in pass.LastUsedResources(contextData)) + { + ref readonly var pointTo = ref contextData.UnversionedResourceData(destroy); + if (pointTo.isImported == false) + { + resources.ReleasePooledResource(rgContext, destroy.iType, destroy.index); + } + } + } + } + } + + private void ExecuteSetRenderTargets(RenderGraphPass pass, InternalRenderGraphContext rgContext) + { + var depthBufferIsValid = pass.depthAccess.textureHandle.IsValid(); + if (depthBufferIsValid || pass.colorBufferMaxIndex != -1) + { + var resources = graph.m_ResourcesForDebugOnly; + var colorBufferAccess = pass.colorBufferAccess; + if (pass.colorBufferMaxIndex > 0) + { + var mrtArray = m_TempMRTArrays[pass.colorBufferMaxIndex]; + + for (int i = 0; i <= pass.colorBufferMaxIndex; ++i) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (!colorBufferAccess[i].textureHandle.IsValid()) + throw new InvalidOperationException($"In pass {pass.name}, when trying to use {colorBufferAccess[i].textureHandle.handle.type} attachment at index {colorBufferAccess[i].textureHandle.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidMRTSetup); +#endif + mrtArray[i] = resources.GetTexture(colorBufferAccess[i].textureHandle); + } + + if (depthBufferIsValid) + { + CoreUtils.SetRenderTarget(rgContext.cmd, mrtArray, resources.GetTexture(pass.depthAccess.textureHandle)); + } + else + { + throw new InvalidOperationException($"In pass {pass.name} - " + RenderGraph.RenderGraphExceptionMessages.k_NoDepthBufferMRT); + } + } + else + { + if (depthBufferIsValid) + { + if (pass.colorBufferMaxIndex > -1) + { + CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.colorBufferAccess[0].textureHandle), + resources.GetTexture(pass.depthAccess.textureHandle)); + } + else + { + CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.depthAccess.textureHandle)); + } + } + else + { + if (pass.colorBufferAccess[0].textureHandle.IsValid()) + { + CoreUtils.SetRenderTarget(rgContext.cmd, resources.GetTexture(pass.colorBufferAccess[0].textureHandle)); + } + else + throw new InvalidOperationException($"In pass {pass.name} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidDepthAndColorTargets); + } + } + } + } + + internal unsafe void ExecuteSetRandomWriteTarget(in CommandBuffer cmd, RenderGraphResourceRegistry resources, int index, in ResourceHandle resource, bool preserveCounterValue = true) + { + if (resource.type == RenderGraphResourceType.Texture) + { + var tex = resources.GetTexture(resource.index); + cmd.SetRandomWriteTarget(index, tex); + } + else if (resource.type == RenderGraphResourceType.Buffer) + { + var buff = resources.GetBuffer(resource.index); + // Default is to preserve the value + if (preserveCounterValue) + { + cmd.SetRandomWriteTarget(index, buff); + } + else + { + cmd.SetRandomWriteTarget(index, buff, false); + } + } + else + { + var name = resources.GetRenderGraphResourceName(resource); + throw new Exception($"When trying to use resource '{name}' of type {resource.type} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidResourceType); + } + } + + internal void ExecuteRenderGraphPass(ref InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, RenderGraphPass pass) + { + + rgContext.executingPass = pass; + + if (!pass.HasRenderFunc()) + { + throw new InvalidOperationException($"In pass {pass.name} - " + + RenderGraph.RenderGraphExceptionMessages.k_NoRenderFunction); + } + + using (new ProfilingScope(rgContext.cmd, pass.customSampler)) + { + pass.Execute(rgContext); + + foreach (var tex in pass.setGlobalsList) + { + rgContext.cmd.SetGlobalTexture(tex.Item2, tex.Item1); + } + } + } + + public void ExecuteGraph(InternalRenderGraphContext rgContext, RenderGraphResourceRegistry resources, in List passes) + { + bool inRenderPass = false; + previousCommandBuffer = rgContext.cmd; + + // Having random access targets bound leads to all sorts of weird behavior so we clear them before executing the graph. + rgContext.cmd.ClearRandomWriteTargets(); + + for (int passIndex = 0; passIndex < contextData.passData.Length; passIndex++) + { + ref var passData = ref contextData.passData.ElementAt(passIndex); + if (passData.culled) + continue; + + bool nrpBegan = false; + bool haveGfxCommandsBeenAddedToCmdDuringResInit = ExecuteInitializeResource(rgContext, resources, passData); + + if (passData.type == RenderGraphPassType.Compute && passData.asyncCompute) + { + GraphicsFence previousFence = new GraphicsFence(); + // We add a fence to the gfx cmd if the async compute cmd needs to wait for some resources to be cleared + if (haveGfxCommandsBeenAddedToCmdDuringResInit) + { + previousFence = rgContext.cmd.CreateGraphicsFence(GraphicsFenceType.AsyncQueueSynchronisation, SynchronisationStageFlags.AllGPUOperations); + } + + if (!rgContext.contextlessTesting) + rgContext.renderContext.ExecuteCommandBuffer(rgContext.cmd); + rgContext.cmd.Clear(); + + var asyncCmd = CommandBufferPool.Get("async cmd"); + asyncCmd.SetExecutionFlags(CommandBufferExecutionFlags.AsyncCompute); + rgContext.cmd = asyncCmd; + + if (haveGfxCommandsBeenAddedToCmdDuringResInit) + { + rgContext.cmd.WaitOnAsyncGraphicsFence(previousFence, SynchronisationStageFlags.PixelProcessing); + } + } + + // also make sure to insert fence=waits for multiple queue syncs + if (passData.waitOnGraphicsFencePassId != -1) + { + rgContext.cmd.WaitOnAsyncGraphicsFence(contextData.fences[passData.waitOnGraphicsFencePassId], SynchronisationStageFlags.PixelProcessing); + } + + if (passData.type == RenderGraphPassType.Raster && passData.mergeState <= PassMergeState.Begin) + { + if (passData.nativePassIndex >= 0) + { + ref var nativePass = ref contextData.nativePassData.ElementAt(passData.nativePassIndex); + if (nativePass.fragments.size > 0) + { + ExecuteBeginRenderPass(rgContext, resources, ref nativePass); + nrpBegan = true; + inRenderPass = true; + } + } + } + else if (passData.type == RenderGraphPassType.Unsafe) + { + ExecuteSetRenderTargets(passes[passIndex], rgContext); + } + + if (passData.mergeState >= PassMergeState.SubPass) + { + if (passData.beginNativeSubpass) + { + if (!inRenderPass) + { + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_BeginNoActivePass); + } + + rgContext.cmd.NextSubPass(); + } + } + + if (passData.numRandomAccessResources > 0) + { + foreach (ref readonly var randomWriteAttachment in passData.RandomWriteTextures(contextData)) + { + ExecuteSetRandomWriteTarget(rgContext.cmd, resources, randomWriteAttachment.index, + randomWriteAttachment.resource); + } + } + + ExecuteRenderGraphPass(ref rgContext, resources, passes[passData.passId]); + EndRenderGraphPass(ref rgContext, ref passData, ref inRenderPass, resources, nrpBegan); + } + } + + void EndRenderGraphPass(ref InternalRenderGraphContext rgContext, ref PassData passData, + ref bool inRenderPass, RenderGraphResourceRegistry resources, bool nrpBegan) + { + // If we set any uavs clear them again so they are local to the pass + if (passData.numRandomAccessResources > 0) + { + rgContext.cmd.ClearRandomWriteTargets(); + } + + // should we insert a fence to sync between difference queues? + if (passData.insertGraphicsFence) + { + var fence = rgContext.cmd.CreateAsyncGraphicsFence(); + contextData.fences[passData.passId] = fence; + } + + if (passData.type == RenderGraphPassType.Raster) + { + var hasRenderPassEnded = (passData.mergeState == PassMergeState.None && nrpBegan) + || passData.mergeState == PassMergeState.End; + + if (hasRenderPassEnded) + { + if (passData.nativePassIndex >= 0) + { + ref var nativePass = ref contextData.nativePassData.ElementAt(passData.nativePassIndex); + if (nativePass.fragments.size > 0) + { + if (!inRenderPass) + { + throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_NoActivePassForSubpass); + } + + if (nativePass.hasFoveatedRasterization) + { + rgContext.cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled); + } + + rgContext.cmd.EndRenderPass(); + CommandBuffer.ThrowOnSetRenderTarget = false; + inRenderPass = false; + + // VRS ShadingRate(Image) cannot be set inside a render pass (cmdBuf). + // ShadingRate is set before BeginRenderPass and here we ResetShadingRate after EndRenderPass. + if (nativePass.hasShadingRateStates || nativePass.hasShadingRateImage) + { + rgContext.cmd.ResetShadingRate(); + } + } + } + } + } + else if (passData.type == RenderGraphPassType.Compute && passData.asyncCompute) + { + rgContext.renderContext.ExecuteCommandBufferAsync(rgContext.cmd, ComputeQueueType.Background); + CommandBufferPool.Release(rgContext.cmd); + rgContext.cmd = previousCommandBuffer; + } + + ExecuteDestroyResource(rgContext, resources, ref passData); + } + } +} + +#endif \ No newline at end of file diff --git a/Ghost.RenderGraph.Concept/Unity/RenderGraph.cs b/Ghost.RenderGraph.Concept/Unity/RenderGraph.cs new file mode 100644 index 0000000..6c76de0 --- /dev/null +++ b/Ghost.RenderGraph.Concept/Unity/RenderGraph.cs @@ -0,0 +1,1692 @@ +#if false + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using UnityEngine.Experimental.Rendering; +using UnityEngine.Scripting.APIUpdating; +// Typedef for the in-engine RendererList API (to avoid conflicts with the experimental version) +using CoreRendererListDesc = UnityEngine.Rendering.RendererUtils.RendererListDesc; + +namespace UnityEngine.Rendering.RenderGraphModule +{ + /// + /// Sets the read and write access for the depth buffer. + /// + [Flags][MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public enum DepthAccess + { + ///Read Access. + Read = 1 << 0, + ///Write Access. + Write = 1 << 1, + ///Read and Write Access. + ReadWrite = Read | Write, + } + + /// + /// Express the operations the rendergraph pass will do on a resource. + /// + [Flags][MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public enum AccessFlags + { + ///The pass does not access the resource at all. Calling Use* functions with none has no effect. + None = 0, + + ///This pass will read data the resource. Data in the resource should never be written unless one of the write flags is also present. Writing to a read-only resource may lead to undefined results, significant performance penaties, and GPU crashes. + Read = 1 << 0, + + ///This pass will at least write some data to the resource. Data in the resource should never be read unless one of the read flags is also present. Reading from a write-only resource may lead to undefined results, significant performance penaties, and GPU crashes. + Write = 1 << 1, + + ///Previous data in the resource is not preserved. The resource will contain undefined data at the beginning of the pass. + Discard = 1 << 2, + + ///All data in the resource will be written by this pass. Data in the resource should never be read. + WriteAll = Write | Discard, + + /// Shortcut for Read | Write + ReadWrite = Read | Write + } + + /// + /// Expresses additional pass properties that can be used to perform optimizations on some platforms. + /// + [Flags] + public enum ExtendedFeatureFlags + { + ///Default state with no extended features enabled. + None = 0, + ///On Meta XR, this flag can be set for the pass that performs the most 3D rendering to achieve better performance. + TileProperties = 1 << 0, + ///On XR, this flag can be set for passes that are compatible with Multiview Render Regions + MultiviewRenderRegionsCompatible = 1 << 1, + ///On Meta XR, this flag can be set to use MSAA shader resolve in the last subpass of a render pass. + MultisampledShaderResolve = 1 << 2, + } + + [Flags] + internal enum RenderGraphState + { + /// + /// Render Graph is not doing anything. + /// + Idle = 0, + + /// + /// Render Graph is recording the graph. + /// + RecordingGraph = 1 << 0, + + /// + /// Render Graph is recording a low level pass. + /// + RecordingPass = 1 << 1, + + /// + /// Render Graph is executing the graph. + /// + Executing = 1 << 2, + + /// + /// Utility flag to check if the graph is active. + /// + Active = RecordingGraph | RecordingPass | Executing + } + + /// + /// The strategy that the render pipeline should use to determine the UV origin of RenderTextures who have an Unknown TextureUVOrigin when rendering. + /// + public enum RenderTextureUVOriginStrategy + { + /// RenderTextures are always treated as bottom left orientation. + BottomLeft, + /// RenderTextures may inherit the backbuffer attachment orientation if they are only used via attachment reads. + PropagateAttachmentOrientation + } + + /// + /// An object representing the internal context of a rendergraph pass execution. + /// This object is public for technical reasons only and should not be used. + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public class InternalRenderGraphContext + { + internal ScriptableRenderContext renderContext; + internal CommandBuffer cmd; + internal RenderGraphObjectPool renderGraphPool; + internal RenderGraphDefaultResources defaultResources; + internal RenderGraphPass executingPass; + internal NativeRenderPassCompiler.CompilerContextData compilerContext; + internal bool contextlessTesting; + internal bool forceResourceCreation; + } + + // InternalRenderGraphContext is public (but all members are internal) + // only because the C# standard says that all interface member function implementations must be public. + // So below in for example the RasterGraphContext we can't implement the (internal) interface as + // internal void FromInternalContext(InternalRenderGraphContext context) { ... } + // So we have to make FromInternalContext public so InternalRenderGraphContext also becomes public. + // This seems an oversight in c# where Interfaces used as Generic constraints could very well be useful + // with internal only functions. + + /// + /// Interface implemented by the different render graph contexts provided at execution timeline (via SetRenderFunc()) + /// + internal interface IDerivedRendergraphContext + { + /// + /// This function is only public for techical resons of the c# language and should not be called outside the package. + /// + /// The context to convert + public void FromInternalContext(InternalRenderGraphContext context); + + /// + /// Retrieves the TextureUVOrigin of the Render Graph texture from its handle. + /// + /// + /// This function can only be called when using the Native Render Pass Compiler (enabled by default). + /// + /// The texture handle to query. + /// The TextureUVOrigin of the texture. + public TextureUVOrigin GetTextureUVOrigin(in TextureHandle textureHandle); + } + + /// + /// This class declares the context object passed to the execute function of a raster render pass. + /// + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public struct RasterGraphContext : IDerivedRendergraphContext + { + private InternalRenderGraphContext wrappedContext; + + ///Command Buffer used for rendering. + public RasterCommandBuffer cmd; + + ///Render Graph default resources. + public RenderGraphDefaultResources defaultResources { get => wrappedContext.defaultResources; } + + ///Render Graph pool used for temporary data. + public RenderGraphObjectPool renderGraphPool { get => wrappedContext.renderGraphPool; } + + static internal RasterCommandBuffer rastercmd = new RasterCommandBuffer(null, null, false); + + /// + public void FromInternalContext(InternalRenderGraphContext context) + { + wrappedContext = context; + rastercmd.m_WrappedCommandBuffer = wrappedContext.cmd; + rastercmd.m_ExecutingPass = context.executingPass; + cmd = rastercmd; + } + + /// + public readonly TextureUVOrigin GetTextureUVOrigin(in TextureHandle textureHandle) + { + if (!SystemInfo.graphicsUVStartsAtTop) + return TextureUVOrigin.BottomLeft; + + if (wrappedContext.compilerContext != null) + { + return wrappedContext.compilerContext.GetTextureUVOrigin(textureHandle); + } + return TextureUVOrigin.BottomLeft; + } + } + + /// + /// This class declares the context object passed to the execute function of a compute render pass. + /// + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public class ComputeGraphContext : IDerivedRendergraphContext + { + private InternalRenderGraphContext wrappedContext; + + ///Command Buffer used for rendering. + public ComputeCommandBuffer cmd; + + ///Render Graph default resources. + public RenderGraphDefaultResources defaultResources { get => wrappedContext.defaultResources; } + + ///Render Graph pool used for temporary data. + public RenderGraphObjectPool renderGraphPool { get => wrappedContext.renderGraphPool; } + + static internal ComputeCommandBuffer computecmd = new ComputeCommandBuffer(null, null, false); + + /// + public void FromInternalContext(InternalRenderGraphContext context) + { + wrappedContext = context; + computecmd.m_WrappedCommandBuffer = wrappedContext.cmd; + computecmd.m_ExecutingPass = context.executingPass; + cmd = computecmd; + } + + /// + public TextureUVOrigin GetTextureUVOrigin(in TextureHandle textureHandle) + { + if (!SystemInfo.graphicsUVStartsAtTop) + return TextureUVOrigin.BottomLeft; + + if (wrappedContext.compilerContext != null) + { + return wrappedContext.compilerContext.GetTextureUVOrigin(textureHandle); + } + return TextureUVOrigin.BottomLeft; + } + } + + /// + /// This class declares the context object passed to the execute function of an unsafe render pass. + /// + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public class UnsafeGraphContext : IDerivedRendergraphContext + { + private InternalRenderGraphContext wrappedContext; + + ///Unsafe Command Buffer used for rendering. + public UnsafeCommandBuffer cmd; + + ///Render Graph default resources. + public RenderGraphDefaultResources defaultResources { get => wrappedContext.defaultResources; } + + ///Render Graph pool used for temporary data. + public RenderGraphObjectPool renderGraphPool { get => wrappedContext.renderGraphPool; } + + internal static UnsafeCommandBuffer unsCmd = new UnsafeCommandBuffer(null, null, false); + /// + public void FromInternalContext(InternalRenderGraphContext context) + { + wrappedContext = context; + unsCmd.m_WrappedCommandBuffer = wrappedContext.cmd; + unsCmd.m_ExecutingPass = context.executingPass; + cmd = unsCmd; + } + + /// + public TextureUVOrigin GetTextureUVOrigin(in TextureHandle textureHandle) + { + if (!SystemInfo.graphicsUVStartsAtTop) + return TextureUVOrigin.BottomLeft; + + if (wrappedContext.compilerContext != null) + { + return wrappedContext.compilerContext.GetTextureUVOrigin(textureHandle); + } + return TextureUVOrigin.BottomLeft; + } + } + + /// + /// This struct contains properties which control the execution of the Render Graph. + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public struct RenderGraphParameters + { + ///Identifier for this render graph execution. + [Obsolete("Not used anymore. The debugging tools use the name of the object identified by executionId. #from(6000.3)")] + public string executionName; + ///Identifier for this render graph execution (i.e. EntityId of the Camera rendering). Used for debugging tools. + public EntityId executionId; + ///Whether the execution should generate debug data and be visible in Render Graph Viewer. + public bool generateDebugData; + ///Index of the current frame being rendered. + public int currentFrameIndex; + /// Controls whether to enable Renderer List culling or not. + [Obsolete("Not supported anymore. Syncing with culling system brings performance regressions in most cases. #from(6000.5)")] + public bool rendererListCulling; + ///Scriptable Render Context used by the render pipeline. + public ScriptableRenderContext scriptableRenderContext; + ///Command Buffer used to execute graphic commands. + public CommandBuffer commandBuffer; + ///When running tests indicate the context is intentionally invalid and all calls on it should just do nothing. + ///This allows you to run tests that rely on code execution the way to the pass render functions + ///This also changes some behaviours with exception handling and error logging so the test framework can act on exceptions to validate behaviour better. + internal bool invalidContextForTesting; + ///The strategy that the rendergraph should use to determine the texture uv origin if Unknown of RenderTextures when rendering. + public RenderTextureUVOriginStrategy renderTextureUVOriginStrategy; + } + + /// + /// The Render Pass rendering delegate to use with typed contexts. + /// + /// The type of the class used to provide data to the Render Pass. + /// The type of the context that will be passed to the render function. + /// Render Pass specific data. + /// Global Render Graph context. + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public delegate void BaseRenderFunc(PassData data, ContextType renderGraphContext) where PassData : class, new(); + + /// + /// This class is the main entry point of the Render Graph system. + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public partial class RenderGraph + { + ///Maximum number of MRTs supported by Render Graph. + public static readonly int kMaxMRTCount = 8; + + /// + /// Enable the use of the render pass API instead of the traditional SetRenderTarget workflow for AddRasterRenderPass() API. Enabled by default since 6000.3. No alternative since 6000.5. + /// + /// + /// When enabled, the render graph try to use render passes and supasses instead of relying on SetRendertarget. It + /// will try to aggressively optimize the number of BeginRenderPass+EndRenderPass calls as well as calls to NextSubPass. + /// This with the aim to maximize the time spent "on chip" on tile based renderers. + /// + /// The Graph will automatically determine when to break render passes as well as the load and store actions to apply to these render passes. + /// To do this, the graph will analyze the use of textures. E.g. when a texture is used twice in a row as a active render target, the two + /// render graph passes will be merged in a single render pass with two surpasses. On the other hand if a render target is sampled as a texture in + /// a later pass this render target will be stored (and possibly resolved) and the render pass will be broken up. + /// + /// When setting this setting to true some existing render graph API is no longer valid as it can't express detailed frame information needed to emit + /// native render pases. In particular: + /// - The ImportBackbuffer overload without a RenderTargetInfo argument. + /// - Any AddRenderPass overloads. The more specific AddRasterRenderPass/AddComputePass/AddUnsafePass functions should be used to register passes. + /// + /// In addition to this, additional validation will be done on the correctness of arguments of existing API that was not previously done. This could lead + /// to new errors when using existing render graph code with nativeRenderPassesEnabled. + /// + /// Note: that CommandBuffer.BeginRenderPass/EndRenderPass calls are different by design from SetRenderTarget so this could also have + /// effects outside of render graph (e.g. for code relying on the currently active render target as this will not be updated when using render passes). + /// + [Obsolete("RenderGraph always enables native render pass support. #from(6000.5)")] + public bool nativeRenderPassesEnabled { get; set; } = true; + + internal/*for tests*/ RenderGraphResourceRegistry m_Resources; + internal/*for tests*/ RenderGraphObjectPool m_RenderGraphPool = new RenderGraphObjectPool(); + RenderGraphBuilders m_builderInstance = new RenderGraphBuilders(); + internal/*for tests*/ List m_RenderPasses = new List(64); + List m_RendererLists = new List(32); + RenderGraphDebugParams m_DebugParameters = new RenderGraphDebugParams(); + RenderGraphDefaultResources m_DefaultResources = new RenderGraphDefaultResources(); + Dictionary m_DefaultProfilingSamplers = new Dictionary(); + InternalRenderGraphContext m_RenderGraphContext = new InternalRenderGraphContext(); + CommandBuffer m_PreviousCommandBuffer; + RenderGraphCompilationCache m_CompilationCache; + + EntityId m_CurrentExecutionId; + bool m_CurrentExecutionCanGenerateDebugData; + int m_ExecutionCount; + int m_CurrentFrameIndex; + bool m_ExecutionExceptionWasRaised; + bool m_EnableCompilationCaching; + internal/*for tests*/ static bool? s_EnableCompilationCachingForTests; + RenderGraphState m_RenderGraphState; + RenderTextureUVOriginStrategy m_renderTextureUVOriginStrategy; + + // Global container of registered render graphs, associated with the list of executions that have been registered for them. + // When a RenderGraph is created, an entry is added to this dictionary. When that RenderGraph renders something, + // and a debug session is active, an entry is added to the list of executions for that RenderGraph using the executionId + // as the key. So when you render multiple times with the same executionId, only one DebugExecutionItem is created. + static Dictionary> s_RegisteredExecutions = new (); + + #region Public Interface + /// Name of the Render Graph. + public string name { get; private set; } = "RenderGraph"; + + internal RenderGraphState RenderGraphState + { + get { return m_RenderGraphState; } + set { m_RenderGraphState = value; } + } + + /// The strategy the Render Graph will take for the uv origin of RenderTextures in the graph. + public RenderTextureUVOriginStrategy renderTextureUVOriginStrategy + { + get { return m_renderTextureUVOriginStrategy; } + internal set { m_renderTextureUVOriginStrategy = value; } + } + + /// If true, the Render Graph Viewer is active. + public static bool isRenderGraphViewerActive => RenderGraphDebugSession.hasActiveDebugSession; + + /// If true, the Render Graph will run its various validity checks while processing (not considered in release mode). + internal static bool enableValidityChecks { get; private set; } + + /// + /// Set of default resources usable in a pass rendering code. + /// + public RenderGraphDefaultResources defaultResources + { + get + { + return m_DefaultResources; + } + } + + /// + /// Render Graph constructor. + /// + /// Optional name used to identify the render graph instance. + public RenderGraph(string name = "RenderGraph") + { + this.name = name; + if (GraphicsSettings.TryGetRenderPipelineSettings(out var renderGraphGlobalSettings)) + { + m_EnableCompilationCaching = renderGraphGlobalSettings.enableCompilationCaching; + enableValidityChecks = renderGraphGlobalSettings.enableValidityChecks; + } + else // No SRP pipeline is present/active, it can happen with unit tests + { + enableValidityChecks = true; + } + + if (s_EnableCompilationCachingForTests.HasValue) + m_EnableCompilationCaching = s_EnableCompilationCachingForTests.Value; + + if (m_EnableCompilationCaching) + m_CompilationCache = new RenderGraphCompilationCache(); + + m_Resources = new RenderGraphResourceRegistry(m_DebugParameters); + RegisterGraph(); + + m_RenderGraphState = RenderGraphState.Idle; + + RenderGraph.RenderGraphExceptionMessages.enableCaller = true; + +#if !UNITY_EDITOR && DEVELOPMENT_BUILD + if (RenderGraphDebugSession.currentDebugSession == null) + RenderGraphDebugSession.Create(); +#endif + } + + // Internal, only for testing + // Useful when we need to clean when calling + // internal functions in tests even if Render Graph is active + // This API shouldn't be called when the render graph is active! + internal void CleanupResourcesAndGraph() + { + // Usually done at the end of Execute step + // Also doing it here in case RG stopped before it + ClearCurrentCompiledGraph(); + + m_Resources.Cleanup(); + m_DefaultResources.Cleanup(); + m_RenderGraphPool.Cleanup(); + nativeCompiler?.Cleanup(); + m_CompilationCache?.Clear(); + + DelegateHashCodeUtils.ClearCache(); + } + + /// + /// Free up all resources used internally by the Render Graph instance, and unregister it so it won't be visible in the Render Graph Viewer. + /// + public void Cleanup() + { + CheckNotUsedWhenActive(); + + // Dispose of the compiled graphs left over in the cache + m_CompilationCache?.Cleanup(); + + CleanupResourcesAndGraph(); + UnregisterGraph(); + } + + internal RenderGraphDebugParams debugParams => m_DebugParameters; + + internal List GetWidgetList() + { + return m_DebugParameters.GetWidgetList(name); + } + + internal bool areAnySettingsActive => m_DebugParameters.AreAnySettingsActive; + + /// + /// Register the render graph to the debug window. + /// + /// + /// This API cannot be called when Render Graph is active, please call it outside of RecordRenderGraph(). + /// + /// Optional debug panel to which the render graph debug parameters will be registered. + public void RegisterDebug(DebugUI.Panel panel = null) + { + CheckNotUsedWhenActive(); + + m_DebugParameters.RegisterDebug(name, panel); + } + + /// + /// Unregister render graph from the debug window. + /// + /// + /// This API cannot be called when Render Graph is active, please call it outside of RecordRenderGraph(). + /// + public void UnRegisterDebug() + { + CheckNotUsedWhenActive(); + + m_DebugParameters.UnRegisterDebug(this.name); + } + + /// + /// Get the list of all registered render graphs. + /// + /// The list of all registered render graphs. + public static List GetRegisteredRenderGraphs() + { + return new List(s_RegisteredExecutions.Keys); + } + + internal static Dictionary> GetRegisteredExecutions() => s_RegisteredExecutions; + + /// + /// End frame processing. Purge resources that have been used since last frame and resets internal states. + /// This need to be called once per frame. + /// + /// + /// This API cannot be called when Render Graph is active, please call it outside of RecordRenderGraph(). + /// + public void EndFrame() + { + CheckNotUsedWhenActive(); + + m_Resources.PurgeUnusedGraphicsResources(); + } + + /// + /// Import an external texture to the Render Graph. + /// Any pass writing to an imported texture will be considered having side effects and can't be automatically culled. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// External RTHandle that needs to be imported. + /// A new TextureHandle that represents the imported texture in the context of this rendergraph. + public TextureHandle ImportTexture(RTHandle rt) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportTexture(rt); + } + + /// + /// Import an external Variable Rate Shading (VRS) textures to the RenderGraph. + /// Any pass writing to an imported texture will be considered having side effects and can't be automatically culled. + /// + /// External shading rate image RTHandle that needs to be imported. + /// New TextureHandle that represents the imported shading rate images in the context of this rendergraph. + public TextureHandle ImportShadingRateImageTexture(RTHandle rt) + { + if (ShadingRateInfo.supportsPerImageTile) + return m_Resources.ImportTexture(rt); + + return TextureHandle.nullHandle; + } + + /// + /// Import an external texture to the Render Graph. + /// Any pass writing to an imported texture will be considered having side effects and can't be automatically culled. + /// + /// Note: RTHandles that wrap RenderTargetIdentifier will fail to import using this overload as render graph can't derive the render texture's properties. + /// In that case the overload taking a RenderTargetInfo argument should be used instead. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// External RTHandle that needs to be imported. + /// Info describing the clear behavior of imported textures. Clearing textures using importParams may be more efficient than manually clearing the texture using `cmd.Clear` on some hardware. + /// A new TextureHandle that represents the imported texture in the context of this rendergraph. + public TextureHandle ImportTexture(RTHandle rt, ImportResourceParams importParams) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportTexture(rt, importParams); + } + + /// + /// Import an external texture to the Render Graph. This overload should be used for RTHandles wrapping a RenderTargetIdentifier. + /// If the RTHandle is wrapping a RenderTargetIdentifer, Rendergraph can't derive the render texture's properties so the user has to provide this info to the graph through RenderTargetInfo. + /// + /// Any pass writing to an imported texture will be considered having side effects and can't be automatically culled. + /// + /// Note: To avoid inconsistencies between the passed in RenderTargetInfo and render texture this overload can only be used when the RTHandle is wrapping a RenderTargetIdentifier. + /// If this is not the case, the overload of ImportTexture without a RenderTargetInfo argument should be used instead. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// External RTHandle that needs to be imported. + /// The properties of the passed in RTHandle. + /// Info describing the clear behavior of imported textures. Clearing textures using importParams may be more efficient than manually clearing the texture using `cmd.Clear` on some hardware. + /// A new TextureHandle that represents the imported texture in the context of this rendergraph. + public TextureHandle ImportTexture(RTHandle rt, RenderTargetInfo info, ImportResourceParams importParams = new ImportResourceParams()) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportTexture(rt, info, importParams); + } + + /// + /// Import an external texture to the Render Graph and set the handle as builtin handle. This can only happen from within the graph module + /// so it is internal. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// External RTHandle that needs to be imported. + /// The handle is a builtin handle managed by RenderGraph internally. + /// A new TextureHandle that represents the imported texture in the context of this rendergraph. + internal TextureHandle ImportTexture(RTHandle rt, bool isBuiltin) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportTexture(rt, isBuiltin); + } + + /// + /// Import the final backbuffer to render graph. The rendergraph can't derive the properties of a RenderTargetIdentifier as it is an opaque handle so the user has to pass them in through the info argument. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Backbuffer render target identifier. + /// The properties of the passed in RTHandle. + /// Info describing the clear behavior of imported textures. Clearing textures using importParams may be more efficient than manually clearing the texture using `cmd.Clear` on some hardware. + /// A new TextureHandle that represents the imported texture in the context of this rendergraph. + public TextureHandle ImportBackbuffer(RenderTargetIdentifier rt, RenderTargetInfo info, ImportResourceParams importParams = new ImportResourceParams()) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportBackbuffer(rt, info, importParams); + } + + /// + /// Create a new Render Graph Texture resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Texture descriptor. + /// A new TextureHandle. + public TextureHandle CreateTexture(in TextureDesc desc) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateTexture(desc); + } + + /// + /// Create a new Render Graph Texture resource using the descriptor from another texture. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Texture from which the descriptor should be used. + /// A new TextureHandle. + public TextureHandle CreateTexture(TextureHandle texture) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateTexture(in m_Resources.GetTextureResourceDesc(texture.handle)); + } + + /// + /// Create a new Render Graph Texture resource using the descriptor from another texture. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Texture from which the descriptor should be used. + /// The destination texture name. + /// Texture needs to be cleared on first use. + /// A new TextureHandle. + public TextureHandle CreateTexture(TextureHandle texture, string name, bool clear = false) + { + CheckNotUsedWhenExecuting(); + + var destinationDesc = GetTextureDesc(texture); + destinationDesc.name = name; + destinationDesc.clearBuffer = clear; + + return m_Resources.CreateTexture(destinationDesc); + } + + /// + /// Create a new Render Graph Texture if the passed handle is invalid and use said handle as output. + /// If the passed handle is valid, no texture is created. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Desc used to create the texture. + /// Texture from which the descriptor should be used. + public void CreateTextureIfInvalid(in TextureDesc desc, ref TextureHandle texture) + { + CheckNotUsedWhenExecuting(); + + if (!texture.IsValid()) + texture = m_Resources.CreateTexture(desc); + } + + /// + /// Gets the descriptor of the specified Texture resource. + /// + /// Texture resource from which the descriptor is requested. + /// The input texture descriptor. + public TextureDesc GetTextureDesc(in TextureHandle texture) + { + return m_Resources.GetTextureResourceDesc(texture.handle); + } + + /// + /// Gets the descriptor of the specified Texture resource. + /// + /// Texture resource from which the descriptor is requested. + /// The input texture descriptor. + public RenderTargetInfo GetRenderTargetInfo(TextureHandle texture) + { + RenderTargetInfo info; + m_Resources.GetRenderTargetInfo(texture.handle, out info); + return info; + } + + /// + /// Creates a new Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Renderer List descriptor. + /// A new RendererListHandle. + public RendererListHandle CreateRendererList(in CoreRendererListDesc desc) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateRendererList(desc); + } + + /// + /// Creates a new Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Renderer List descriptor. + /// A new RendererListHandle. + public RendererListHandle CreateRendererList(in RendererListParams desc) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateRendererList(desc); + } + + /// + /// Creates a new Shadow Renderer List Render Graph resource. + /// + /// DrawSettings that describe the shadow drawcall. + /// A new RendererListHandle. + public RendererListHandle CreateShadowRendererList(ref ShadowDrawingSettings shadowDrawingSettings) + { + return m_Resources.CreateShadowRendererList(m_RenderGraphContext.renderContext, ref shadowDrawingSettings); + } + + /// + /// Creates a new Gizmo Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the Gizmo. + /// GizmoSubset that specifies whether gizmos render before or after postprocessing for a camera render. + /// A new RendererListHandle. + public RendererListHandle CreateGizmoRendererList(in Camera camera, in GizmoSubset gizmoSubset) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateGizmoRendererList(m_RenderGraphContext.renderContext, camera, gizmoSubset); + } + + /// + /// Creates a new UIOverlay Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the full UIOverlay. + /// A new RendererListHandle. + public RendererListHandle CreateUIOverlayRendererList(in Camera camera) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateUIOverlayRendererList(m_RenderGraphContext.renderContext, camera, UISubset.All); + } + + /// + /// Creates a new UIOverlay Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering some subset of the UIOverlay. + /// Enum flag that specifies which subset to render. + /// A new RendererListHandle. + public RendererListHandle CreateUIOverlayRendererList(in Camera camera, in UISubset uiSubset) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateUIOverlayRendererList(m_RenderGraphContext.renderContext, camera, uiSubset); + } + + /// + /// Creates a new WireOverlay Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the WireOverlay. + /// A new RendererListHandle. + public RendererListHandle CreateWireOverlayRendererList(in Camera camera) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateWireOverlayRendererList(m_RenderGraphContext.renderContext, camera); + } + + /// + /// Creates a new Skybox Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the Skybox. + /// A new RendererListHandle. + public RendererListHandle CreateSkyboxRendererList(in Camera camera) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateSkyboxRendererList(m_RenderGraphContext.renderContext, camera); + } + + /// + /// Creates a new Skybox Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the Skybox. + /// The projection matrix used during XR rendering of the skybox. + /// The view matrix used during XR rendering of the skybox. + /// A new RendererListHandle. + public RendererListHandle CreateSkyboxRendererList(in Camera camera, Matrix4x4 projectionMatrix, Matrix4x4 viewMatrix) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateSkyboxRendererList(m_RenderGraphContext.renderContext, camera, projectionMatrix, viewMatrix); + } + + /// + /// Creates a new Skybox Renderer List Render Graph resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// The camera that is used for rendering the Skybox. + /// The left eye projection matrix used during Legacy single pass XR rendering of the skybox. + /// The left eye view matrix used during Legacy single pass XR rendering of the skybox. + /// The right eye projection matrix used during Legacy single pass XR rendering of the skybox. + /// The right eye view matrix used during Legacy single pass XR rendering of the skybox. + /// A new RendererListHandle. + public RendererListHandle CreateSkyboxRendererList(in Camera camera, Matrix4x4 projectionMatrixL, Matrix4x4 viewMatrixL, Matrix4x4 projectionMatrixR, Matrix4x4 viewMatrixR) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateSkyboxRendererList(m_RenderGraphContext.renderContext, camera, projectionMatrixL, viewMatrixL, projectionMatrixR, viewMatrixR); + } + + /// + /// Import an external Graphics Buffer to the Render Graph. + /// Any pass writing to an imported graphics buffer will be considered having side effects and can't be automatically culled. + /// + /// External Graphics Buffer that needs to be imported. + /// A new GraphicsBufferHandle. + public BufferHandle ImportBuffer(GraphicsBuffer graphicsBuffer) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportBuffer(graphicsBuffer); + } + + /// + /// Create a new Render Graph Graphics Buffer resource. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Graphics Buffer descriptor. + /// A new GraphicsBufferHandle. + public BufferHandle CreateBuffer(in BufferDesc desc) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateBuffer(desc); + } + + /// + /// Create a new Render Graph Graphics Buffer resource using the descriptor from another graphics buffer. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// Graphics Buffer from which the descriptor should be used. + /// A new GraphicsBufferHandle. + public BufferHandle CreateBuffer(in BufferHandle graphicsBuffer) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.CreateBuffer(in m_Resources.GetBufferResourceDesc(graphicsBuffer.handle)); + } + + /// + /// Gets the descriptor of the specified Graphics Buffer resource. + /// + /// Graphics Buffer resource from which the descriptor is requested. + /// The input graphics buffer descriptor. + public BufferDesc GetBufferDesc(in BufferHandle graphicsBuffer) + { + return m_Resources.GetBufferResourceDesc(graphicsBuffer.handle); + } + + /// + /// Import an external RayTracingAccelerationStructure to the Render Graph. + /// Any pass writing to (building) an imported RayTracingAccelerationStructure will be considered having side effects and can't be automatically culled. + /// + /// + /// This API cannot be called during the Render Graph execution, please call it outside of SetRenderFunc(). + /// + /// External RayTracingAccelerationStructure that needs to be imported. + /// Optional name for identifying the RayTracingAccelerationStructure in the Render Graph. + /// A new RayTracingAccelerationStructureHandle. + public RayTracingAccelerationStructureHandle ImportRayTracingAccelerationStructure(in RayTracingAccelerationStructure accelStruct, string name = null) + { + CheckNotUsedWhenExecuting(); + + return m_Resources.ImportRayTracingAccelerationStructure(accelStruct, name); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenExecuting() + { + if (enableValidityChecks && m_RenderGraphState == RenderGraphState.Executing) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.Executing)); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenRecordingGraph() + { + if (enableValidityChecks && m_RenderGraphState == RenderGraphState.RecordingGraph) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.RecordingGraph)); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenRecordPassOrExecute() + { + if (enableValidityChecks && (m_RenderGraphState == RenderGraphState.RecordingPass || m_RenderGraphState == RenderGraphState.Executing)) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.RecordingPass | RenderGraphState.Executing)); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenRecordingPass() + { + if (enableValidityChecks && m_RenderGraphState == RenderGraphState.RecordingPass) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.RecordingPass)); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenActive() + { + if (enableValidityChecks && (m_RenderGraphState & RenderGraphState.Active) != RenderGraphState.Idle) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.Active)); + } + + [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] + void CheckNotUsedWhenIdle() + { + if (enableValidityChecks && m_RenderGraphState == RenderGraphState.Idle) + throw new InvalidOperationException(RenderGraphExceptionMessages.GetExceptionMessage(RenderGraphState.Active)); + } + + /// + /// Add a new Raster Render Pass to the Render Graph. Raster passes can execute rasterization workloads but cannot do other GPU work like copies or compute. + /// + /// + /// This API cannot be called when Render Graph records a pass, please call it within SetRenderFunc() or outside of AddUnsafePass()/AddComputePass()/AddRasterRenderPass(). + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IRasterRenderGraphBuilder used to setup the new Rasterization Render Pass. + public IRasterRenderGraphBuilder AddRasterRenderPass(string passName, out PassData passData +#if !CORE_PACKAGE_DOCTOOLS + , [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + return AddRasterRenderPass(passName, out passData, GetDefaultProfilingSampler(passName), file, line); + } + + /// + /// Add a new Raster Render Pass to the Render Graph. Raster passes can execute rasterization workloads but cannot do other GPU work like copies or compute. + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// Profiling sampler used around the pass. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IRasterRenderGraphBuilder used to setup the new Rasterization Render Pass. + public IRasterRenderGraphBuilder AddRasterRenderPass(string passName, out PassData passData, ProfilingSampler sampler +#if !CORE_PACKAGE_DOCTOOLS + ,[CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + CheckNotUsedWhenRecordingPass(); + + m_RenderGraphState = RenderGraphState.RecordingPass; + + var renderPass = m_RenderGraphPool.Get>(); + renderPass.Initialize(m_RenderPasses.Count, m_RenderGraphPool.Get(), passName, RenderGraphPassType.Raster, sampler); + + AddPassDebugMetadata(renderPass, file, line); + + passData = renderPass.data; + + m_RenderPasses.Add(renderPass); + + m_builderInstance.Setup(renderPass, m_Resources, this); + + return m_builderInstance; + } + + /// + /// Add a new Compute Render Pass to the Render Graph. Raster passes can execute rasterization workloads but cannot do other GPU work like copies or compute. + /// + /// + /// This API cannot be called when Render Graph records a pass, please call it within SetRenderFunc() or outside of AddUnsafePass()/AddComputePass()/AddRasterRenderPass(). + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IRasterRenderGraphBuilder used to setup the new Rasterization Render Pass. + public IComputeRenderGraphBuilder AddComputePass(string passName, out PassData passData +#if !CORE_PACKAGE_DOCTOOLS + , [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + return AddComputePass(passName, out passData, GetDefaultProfilingSampler(passName), file, line); + } + + /// + /// Add a new Compute Render Pass to the Render Graph. Compute passes can execute compute workloads but cannot do rasterization. + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// Profiling sampler used around the pass. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IComputeRenderGraphBuilder used to setup the new Compute Render Pass. + public IComputeRenderGraphBuilder AddComputePass(string passName, out PassData passData, ProfilingSampler sampler +#if !CORE_PACKAGE_DOCTOOLS + ,[CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + CheckNotUsedWhenRecordingPass(); + + m_RenderGraphState = RenderGraphState.RecordingPass; + + var renderPass = m_RenderGraphPool.Get>(); + renderPass.Initialize(m_RenderPasses.Count, m_RenderGraphPool.Get(), passName, RenderGraphPassType.Compute, sampler); + + AddPassDebugMetadata(renderPass, file, line); + + passData = renderPass.data; + + m_RenderPasses.Add(renderPass); + + m_builderInstance.Setup(renderPass, m_Resources, this); + + return m_builderInstance; + } + + /// + /// Add a new Unsafe Render Pass to the Render Graph. Unsafe passes can do certain operations compute/raster render passes cannot do and have + /// access to the full command buffer API. The unsafe API should be used sparingly as it has the following downsides: + /// - Limited automatic validation of the commands and resource dependencies. The user is responsible to ensure that all dependencies are correctly declared. + /// - All native render passes will be serialized out. + /// - In the future the render graph compiler may generate a sub-optimal command stream for unsafe passes. + /// When using a unsafe pass the graph will also not automatically set up graphics state like rendertargets. The pass should do this itself + /// using cmd.SetRenderTarget and related commands. + /// + /// + /// This API cannot be called when Render Graph records a pass, please call it within SetRenderFunc() or outside of AddUnsafePass()/AddComputePass()/AddRasterRenderPass(). + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IUnsafeRenderGraphBuilder used to setup the new Unsafe Render Pass. + public IUnsafeRenderGraphBuilder AddUnsafePass(string passName, out PassData passData +#if !CORE_PACKAGE_DOCTOOLS + , [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + return AddUnsafePass(passName, out passData, GetDefaultProfilingSampler(passName), file, line); + } + + /// + /// Add a new unsafe Render Pass to the Render Graph. Unsafe passes can do certain operations compute/raster render passes cannot do and have + /// access to the full command buffer API. The unsafe API should be used sparingly as it has the following downsides: + /// - Limited automatic validation of the commands and resource dependencies. The user is responsible to ensure that all dependencies are correctly declared. + /// - All native render passes will be serialized out. + /// - In the future the render graph compiler may generate a sub-optimal command stream for unsafe passes. + /// When using an unsafe pass the graph will also not automatically set up graphics state like rendertargets. The pass should do this itself + /// using cmd.SetRenderTarget and related commands. + /// + /// Type of the class to use to provide data to the Render Pass. + /// Name of the new Render Pass (this is also be used to generate a GPU profiling marker). + /// Instance of PassData that is passed to the render function and you must fill. + /// Profiling sampler used around the pass. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// A new instance of a IUnsafeRenderGraphBuilder used to setup the new unsafe Render Pass. + public IUnsafeRenderGraphBuilder AddUnsafePass(string passName, out PassData passData, ProfilingSampler sampler +#if !CORE_PACKAGE_DOCTOOLS + , [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) where PassData : class, new() +#endif + { + CheckNotUsedWhenRecordingPass(); + + m_RenderGraphState = RenderGraphState.RecordingPass; + + var renderPass = m_RenderGraphPool.Get>(); + renderPass.Initialize(m_RenderPasses.Count, m_RenderGraphPool.Get(), passName, RenderGraphPassType.Unsafe, sampler); + renderPass.AllowGlobalState(true); + + AddPassDebugMetadata(renderPass, file, line); + + passData = renderPass.data; + + m_RenderPasses.Add(renderPass); + + m_builderInstance.Setup(renderPass, m_Resources, this); + + return m_builderInstance; + } + + /// + /// Starts the recording of the render graph. + /// This must be called before adding any pass to the render graph. + /// + /// + /// This API cannot be called when Render Graph is active, please call it outside of RecordRenderGraph(). + /// + /// Parameters necessary for the render graph execution. + /// + /// Begin recording the Render Graph. + /// + /// renderGraph.BeginRecording(parameters) + /// // Add your render graph passes here. + /// renderGraph.EndRecordingAndExecute() + /// + /// + public void BeginRecording(in RenderGraphParameters parameters) + { + CheckNotUsedWhenActive(); + + m_ExecutionExceptionWasRaised = false; + m_RenderGraphState = RenderGraphState.RecordingGraph; + + m_CurrentFrameIndex = parameters.currentFrameIndex; + m_CurrentExecutionId = parameters.executionId; + + // Ignore preview cameras and render requests for debug data generation. They would cause the same camera to + // be rendered twice with different render graphs, confusing users and causing the RG Viewer to constantly update. + m_CurrentExecutionCanGenerateDebugData = parameters.generateDebugData && parameters.executionId != EntityId.None; + + m_renderTextureUVOriginStrategy = parameters.renderTextureUVOriginStrategy; + + m_Resources.BeginRenderGraph(m_ExecutionCount++); + + m_DefaultResources.InitializeForRendering(this); + + m_RenderGraphContext.cmd = parameters.commandBuffer; + m_RenderGraphContext.renderContext = parameters.scriptableRenderContext; + m_RenderGraphContext.contextlessTesting = parameters.invalidContextForTesting; + m_RenderGraphContext.renderGraphPool = m_RenderGraphPool; + m_RenderGraphContext.defaultResources = m_DefaultResources; + + // With the actual implementation of the Frame Debugger, we cannot re-use resources during the same frame + // or it breaks the rendering of the pass preview, since the FD copies the texture after the execution of the RG. + m_RenderGraphContext.forceResourceCreation = +#if UNITY_EDITOR || DEVELOPMENT_BUILD + FrameDebugger.enabled; +#else + false; +#endif + } + + /// + /// Ends the recording and executes the render graph. + /// This must be called once all passes have been added to the render graph. + /// + public void EndRecordingAndExecute() + { + CheckNotUsedWhenRecordPassOrExecute(); + + Execute(); + + ClearCompiledGraph(); + + m_Resources.EndExecute(); + + InvalidateContext(); + + m_RenderGraphState = RenderGraphState.Idle; + } + + /// + /// Catches and logs exceptions that could happen during the graph recording or execution. + /// + /// The exception thrown by the graph. + /// True if contexless testing is enabled, false otherwise. + public bool ResetGraphAndLogException(Exception e) + { + m_RenderGraphState = RenderGraphState.Idle; + + if (!m_RenderGraphContext.contextlessTesting) + { + // If we're not testing log the exception and swallow it. + // TODO: Do we really want to swallow exceptions here? Not a very c# thing to do. + Debug.LogError(RenderGraphExceptionMessages.k_RenderGraphExecutionError); + if (!m_ExecutionExceptionWasRaised) // Already logged. TODO: There is probably a better way in C# to handle that. + { + Debug.LogException(e); + } + + m_ExecutionExceptionWasRaised = true; + } + + CommandBuffer.ThrowOnSetRenderTarget = false; + + CleanupResourcesAndGraph(); + + // If there has been an error, we have to flush the command being built along the RG data structure, + // because otherwise the command might try to use resources we just deleted. + m_RenderGraphContext.cmd.Clear(); + return m_RenderGraphContext.contextlessTesting; + } + + /// + /// Execute the Render Graph in its current state. + /// + internal void Execute() + { + m_ExecutionExceptionWasRaised = false; + m_RenderGraphState = RenderGraphState.Executing; + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_RenderGraphContext.cmd == null) + throw new InvalidOperationException("RenderGraph.BeginRecording was not called before executing the render graph."); + + ClearCacheIfNewActiveDebugSession(); +#endif + + int graphHash = m_EnableCompilationCaching ? ComputeGraphHash() : 0; + + CompileNativeRenderGraph(graphHash); + + // Must be set after compilation when the compiler has been initialized + m_RenderGraphContext.compilerContext = nativeCompiler?.contextData; + + m_Resources.BeginExecute(m_CurrentFrameIndex); + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // Feeding Render Graph Viewer before resource deallocation at pass execution + GenerateDebugData(graphHash); +#endif + ExecuteNativeRenderGraph(); + + // Clear the shader bindings for all global textures to make sure bindings don't leak outside the graph + ClearGlobalBindings(); + } + + class ProfilingScopePassData + { + public ProfilingSampler sampler; + } + + const string k_BeginProfilingSamplerPassName = "BeginProfile"; + const string k_EndProfilingSamplerPassName = "EndProfile"; + + /// + /// Begin a profiling scope. + /// + /// Sampler used for profiling. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + public void BeginProfilingSampler(ProfilingSampler sampler, + [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) + { + if (sampler == null) + return; + + using (var builder = AddUnsafePass(k_BeginProfilingSamplerPassName, out var passData, (ProfilingSampler)null, file, line)) + { + passData.sampler = sampler; + builder.AllowPassCulling(false); + builder.GenerateDebugData(false); + builder.SetRenderFunc(static (ProfilingScopePassData data, UnsafeGraphContext ctx) => + { + data.sampler.Begin(CommandBufferHelpers.GetNativeCommandBuffer(ctx.cmd)); + }); + } + } + + /// + /// End a profiling scope. + /// + /// Sampler used for profiling. + /// File name of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + /// File line of the source file this function is called from. Used for debugging. This parameter is automatically generated by the compiler. Users do not need to pass it. + public void EndProfilingSampler(ProfilingSampler sampler, + [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0) + { + if (sampler == null) + return; + + using (var builder = AddUnsafePass(k_EndProfilingSamplerPassName, out var passData, (ProfilingSampler)null, file, line)) + { + passData.sampler = sampler; + builder.AllowPassCulling(false); + builder.GenerateDebugData(false); + builder.SetRenderFunc(static (ProfilingScopePassData data, UnsafeGraphContext ctx) => + { + data.sampler.End(CommandBufferHelpers.GetNativeCommandBuffer(ctx.cmd)); + }); + } + } + + #endregion + + #region Internal Interface + + // Internal for testing purpose only + internal void ClearCurrentCompiledGraph() + { + ClearCompiledGraph(); + } + + void ClearCompiledGraph() + { + ClearRenderPasses(); + m_Resources.Clear(m_ExecutionExceptionWasRaised); + m_RendererLists.Clear(); + registeredGlobals.Clear(); + } + + void InvalidateContext() + { + m_RenderGraphContext.cmd = null; + m_RenderGraphContext.renderGraphPool = null; + m_RenderGraphContext.defaultResources = null; + m_RenderGraphContext.compilerContext = null; + } + + internal delegate void OnGraphRegisteredDelegate(string graphName); + internal static event OnGraphRegisteredDelegate onGraphRegistered; + internal static event OnGraphRegisteredDelegate onGraphUnregistered; + internal delegate void OnExecutionRegisteredDelegate(string graphName, EntityId executionId, string executionName); + internal static event OnExecutionRegisteredDelegate onExecutionRegistered; + #endregion + + #region Private Interface + + // Internal for testing purpose only. + internal int ComputeGraphHash() + { + using (new ProfilingScope(ProfilingSampler.Get(RenderGraphProfileId.ComputeHashRenderGraph))) + { + var hash128 = HashFNV1A32.Create(); + for (int i = 0; i < m_RenderPasses.Count; ++i) + m_RenderPasses[i].ComputeHash(ref hash128, m_Resources); + + return hash128.value; + } + } + + internal bool GetImportedFallback(TextureDesc desc, out TextureHandle fallback) + { + fallback = TextureHandle.nullHandle; + + // We don't have any fallback texture with MSAA + if (!desc.bindTextureMS) + { + if (desc.depthBufferBits != DepthBits.None) + { + fallback = defaultResources.whiteTexture; + } + else if (desc.clearColor == Color.black || desc.clearColor == default) + { + if (desc.dimension == TextureXR.dimension) + fallback = defaultResources.blackTextureXR; + else if (desc.dimension == TextureDimension.Tex3D) + fallback = defaultResources.blackTexture3DXR; + else if (desc.dimension == TextureDimension.Tex2D) + fallback = defaultResources.blackTexture; + } + else if (desc.clearColor == Color.white) + { + if (desc.dimension == TextureXR.dimension) + fallback = defaultResources.whiteTextureXR; + else if (desc.dimension == TextureDimension.Tex2D) + fallback = defaultResources.whiteTexture; + } + } + + return fallback.IsValid(); + } + + void ClearRenderPasses() + { + foreach (var pass in m_RenderPasses) + pass.Release(m_RenderGraphPool); + + m_RenderPasses.Clear(); + } + + ProfilingSampler GetDefaultProfilingSampler(string name) + { + // In non-dev builds, ProfilingSampler.Get returns null, so we'd always end up executing this. + // To avoid that we also ifdef the code out here. +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int hash = name.GetHashCode(); + if (!m_DefaultProfilingSamplers.TryGetValue(hash, out var sampler)) + { + sampler = new ProfilingSampler(name); + m_DefaultProfilingSamplers.Add(hash, sampler); + } + + return sampler; +#else + return null; +#endif + } + + // Register the graph in the RenderGraph Viewer. + // Registered once on graph creation, indicates to the viewer that this render graph exists. + void RegisterGraph() + { + s_RegisteredExecutions.Add(this, new List()); + onGraphRegistered?.Invoke(this.name); + } + + // Unregister the graph from the render graph viewer. + // Should only be unregistered when it is destroyed, not before. + // Should not be called when an error happens because the existence of the graph doesn't depend on its state + // or whether it's been cleaned. + void UnregisterGraph() + { + s_RegisteredExecutions.Remove(this); + onGraphUnregistered?.Invoke(name); + } + + // Note: obj.name allocates so make sure you only call this when debug tools / options are active + static string GetExecutionNameAllocates(EntityId entityId) + { + var obj = Resources.EntityIdToObject(entityId); + return obj != null ? obj.name : $"RenderGraphExecution ({entityId})"; + } + + static bool s_DebugSessionWasActive; + + void ClearCacheIfNewActiveDebugSession() + { + if (RenderGraphDebugSession.hasActiveDebugSession && !s_DebugSessionWasActive) + { + // Invalidate cache whenever a debug session becomes active. This is because while the DebugSession is + // inactive, certain debug data (store/load/pass break audits) stored inside the compilation cache is + // not getting generated. Therefore we clear compilation cache to regenerate this debug data. + // This needs to be done before the compilation step. + m_CompilationCache?.Clear(); + } + s_DebugSessionWasActive = RenderGraphDebugSession.hasActiveDebugSession; + } + + void GenerateDebugData(int graphHash) + { + if (!RenderGraphDebugSession.hasActiveDebugSession || !m_CurrentExecutionCanGenerateDebugData || m_ExecutionExceptionWasRaised) + return; + + var registeredExecutions = s_RegisteredExecutions[this]; + + // The reverse loop prunes deleted cameras and checks if the current camera needs to be registered. + bool alreadyRegistered = false; + using var deletedExecutionIdsDisposable = ListPool.Get(out var deletedExecutionIds); + for (int i = registeredExecutions.Count - 1; i >= 0; i--) + { + DebugExecutionItem executionItem = registeredExecutions[i]; + if (Resources.EntityIdToObject(executionItem.id) == null) + { + registeredExecutions.RemoveAt(i); + deletedExecutionIds.Add(executionItem.id); + continue; + } + if (executionItem.id == m_CurrentExecutionId) + alreadyRegistered = true; + } + + var currentExecutionName = GetExecutionNameAllocates(m_CurrentExecutionId); + if (!alreadyRegistered) + { + registeredExecutions.Add(new DebugExecutionItem(m_CurrentExecutionId, currentExecutionName)); + } + + if (deletedExecutionIds.Count > 0) + RenderGraphDebugSession.DeleteExecutionIds(name, deletedExecutionIds); // Clear stale debug data entries for deleted cameras + + if (!alreadyRegistered) + { + onExecutionRegistered?.Invoke(name, m_CurrentExecutionId, currentExecutionName); + } + + // When compilation caching is disabled, we don't hash the graph. In order to avoid UI needing to update every + // frame, we still want to use the hash to know if debug data should be updated, so compute the hash now. + if (!m_EnableCompilationCaching) + graphHash = ComputeGraphHash(); + + var debugData = RenderGraphDebugSession.GetDebugData(name, m_CurrentExecutionId); + bool cameraWasRenamed = debugData.executionName != currentExecutionName; + if (debugData.valid && debugData.graphHash == graphHash && !cameraWasRenamed) + return; // No need to update + + debugData.Clear(); + debugData.executionName = currentExecutionName; + debugData.graphHash = graphHash; + + nativeCompiler.GenerateNativeCompilerDebugData(ref debugData); + + debugData.valid = true; + RenderGraphDebugSession.SetDebugData(name, m_CurrentExecutionId, debugData); + } + + #endregion + + Dictionary registeredGlobals = new Dictionary(); + + internal void SetGlobal(in TextureHandle h, int globalPropertyId) + { + if (!h.IsValid()) + throw new ArgumentException("Attempting to register an invalid texture handle as a global"); + + registeredGlobals[globalPropertyId] = h; + } + + internal bool IsGlobal(int globalPropertyId) + { + return registeredGlobals.ContainsKey(globalPropertyId); + } + + internal Dictionary.ValueCollection AllGlobals() + { + return registeredGlobals.Values; + } + + internal TextureHandle GetGlobal(int globalPropertyId) + { + TextureHandle h; + registeredGlobals.TryGetValue(globalPropertyId, out h); + return h; + } + + /// + /// Clears the shader bindings associated with the registered globals in the graph + /// + /// This prevents later rendering logic from accidentally relying on stale shader bindings that were set + /// earlier during graph execution. + /// + internal void ClearGlobalBindings() + { + // Set all the global texture shader bindings to the default black texture. + // This doesn't technically "clear" the shader bindings, but it's the closest we can do. + foreach (var globalTex in registeredGlobals) + { + m_RenderGraphContext.cmd.SetGlobalTexture(globalTex.Key, defaultResources.blackTexture); + } + } + } + + /// + /// Render Graph Scoped Profiling markers + /// + [MovedFrom(true, "UnityEngine.Experimental.Rendering.RenderGraphModule", "UnityEngine.Rendering.RenderGraphModule")] + public struct RenderGraphProfilingScope : IDisposable + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + ProfilingSampler m_Sampler; + RenderGraph m_RenderGraph; + bool m_Disposed; +#endif + + /// + /// Profiling Scope constructor + /// + /// Render Graph used for this scope. + /// Profiling Sampler to be used for this scope. + public RenderGraphProfilingScope(RenderGraph renderGraph, ProfilingSampler sampler) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_RenderGraph = renderGraph; + m_Sampler = sampler; + m_Disposed = false; + renderGraph.BeginProfilingSampler(sampler); +#endif + } + + /// + /// Dispose pattern implementation + /// + public void Dispose() + { + Dispose(true); + } + + // Protected implementation of Dispose pattern. + void Dispose(bool disposing) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_Disposed) + return; + + // As this is a struct, it could have been initialized using an empty constructor so we + // need to make sure `cmd` isn't null to avoid a crash. Switching to a class would fix + // this but will generate garbage on every frame (and this struct is used quite a lot). + if (disposing) + { + m_RenderGraph.EndProfilingSampler(m_Sampler); + } + + m_Disposed = true; +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs b/Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs new file mode 100644 index 0000000..a4f9fd9 --- /dev/null +++ b/Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs @@ -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); + } + } + + /// + /// Enable foveated rendering for this pass. + /// + /// True to enable foveated rendering. + 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(BaseRenderFunc renderFunc) where PassData : class, new() + { + ((ComputeRenderGraphPass)m_RenderPass).renderFunc = renderFunc; + } + + public void SetRenderFunc(BaseRenderFunc renderFunc) where PassData : class, new() + { + ((RasterRenderGraphPass)m_RenderPass).renderFunc = renderFunc; + } + + public void SetRenderFunc(BaseRenderFunc renderFunc) where PassData : class, new() + { + ((UnsafeRenderGraphPass)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 \ No newline at end of file diff --git a/Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs b/Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs new file mode 100644 index 0000000..723ff89 --- /dev/null +++ b/Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs @@ -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[] resourceReadLists = new List[(int)RenderGraphResourceType.Count]; + public List[] resourceWriteLists = new List[(int)RenderGraphResourceType.Count]; + public List[] transientResourceList = new List[(int)RenderGraphResourceType.Count]; + + public List usedRendererListList = new List(); + + public List> setGlobalsList = new List>(); + public bool useAllGlobalTextures; + + public List implicitReadsList = new List(); + +#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(); + resourceWriteLists[i] = new List(); + transientResourceList[i] = new List(); + } + } + + 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 : RenderGraphPass + where PassData : class, new() + { + internal PassData data; + internal BaseRenderFunc 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 : BaseRenderGraphPass + 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 : BaseRenderGraphPass + 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 : BaseRenderGraphPass + 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 : BaseRenderGraphPass + 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 \ No newline at end of file