GhostEngine Render Graph: major refactor & Unity RG ref
- Major architectural refactor for performance, extensibility, and feature completeness: resource pooling, pass culling, aliasing, and compilation caching. - Introduces type-safe builder and context APIs, blackboard pattern, and unified resource management. - Adds detailed documentation and cleans up obsolete files and APIs. - Includes (commented) Unity Render Graph source for reference; not compiled, for parity and future extension.
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,19 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
|
<DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DefineConstants>$(DefineConstants);PROFILING</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
|
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="New\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
public struct ResourceBarrierInfo
|
|
||||||
{
|
|
||||||
public string ResourceName;
|
|
||||||
public ResourceState BeforeState;
|
|
||||||
public ResourceState AfterState;
|
|
||||||
|
|
||||||
public ResourceBarrierInfo(string resourceName, ResourceState beforeState, ResourceState afterState)
|
|
||||||
{
|
|
||||||
ResourceName = resourceName;
|
|
||||||
BeforeState = beforeState;
|
|
||||||
AfterState = afterState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct AliasingBarrierInfo
|
|
||||||
{
|
|
||||||
public string BeforeResourceName;
|
|
||||||
public string AfterResourceName;
|
|
||||||
public string PhysicalAllocationName;
|
|
||||||
|
|
||||||
public AliasingBarrierInfo(string beforeResourceName, string afterResourceName, string physicalAllocationName)
|
|
||||||
{
|
|
||||||
BeforeResourceName = beforeResourceName;
|
|
||||||
AfterResourceName = afterResourceName;
|
|
||||||
PhysicalAllocationName = physicalAllocationName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ICommandBuffer
|
|
||||||
{
|
|
||||||
void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState);
|
|
||||||
void ResourceBarrier(Span<ResourceBarrierInfo> barriers);
|
|
||||||
void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName);
|
|
||||||
void AliasingBarrier(Span<AliasingBarrierInfo> barriers);
|
|
||||||
void BeginRenderPass(string passName);
|
|
||||||
void EndRenderPass();
|
|
||||||
void SetRenderTarget(string textureName);
|
|
||||||
void SetDepthStencil(string textureName);
|
|
||||||
void BindShaderResource(string resourceName, int slot);
|
|
||||||
void BindUnorderedAccess(string resourceName, int slot);
|
|
||||||
void Draw(int vertexCount);
|
|
||||||
void Dispatch(int x, int y, int z);
|
|
||||||
void ClearRenderTarget(string textureName, float r, float g, float b, float a);
|
|
||||||
void ClearDepth(string textureName, float depth);
|
|
||||||
void CopyTexture(string source, string destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SimulatedCommandBuffer : ICommandBuffer
|
|
||||||
{
|
|
||||||
public void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [BARRIER] Transition '{resourceName}' from {beforeState} to {afterState}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResourceBarrier(Span<ResourceBarrierInfo> barriers)
|
|
||||||
{
|
|
||||||
if (barriers.Length == 0) return;
|
|
||||||
//ConsoleAPI.WriteLine($" [BARRIER_BATCH] Processing {barriers.Length} transitions:");
|
|
||||||
foreach (var barrier in barriers)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" - Transition '{barrier.ResourceName}' from {barrier.BeforeState} to {barrier.AfterState}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [ALIAS_BARRIER] Alias '{physicalAllocationName}': '{beforeResourceName}' -> '{afterResourceName}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AliasingBarrier(Span<AliasingBarrierInfo> barriers)
|
|
||||||
{
|
|
||||||
if (barriers.Length == 0) return;
|
|
||||||
//ConsoleAPI.WriteLine($" [ALIAS_BARRIER_BATCH] Processing {barriers.Length} aliasing barriers:");
|
|
||||||
foreach (var barrier in barriers)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" - Alias '{barrier.PhysicalAllocationName}': '{barrier.BeforeResourceName}' -> '{barrier.AfterResourceName}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void BeginRenderPass(string passName)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [BEGIN] RenderPass '{passName}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndRenderPass()
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [END] RenderPass");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetRenderTarget(string textureName)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [RT] Set RenderTarget: '{textureName}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetDepthStencil(string textureName)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [DS] Set DepthStencil: '{textureName}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BindShaderResource(string resourceName, int slot)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BindUnorderedAccess(string resourceName, int slot)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw(int vertexCount)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [DRAW] Drawing {vertexCount} vertices");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispatch(int x, int y, int z)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [DISPATCH] Compute ({x}, {y}, {z})");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearRenderTarget(string textureName, float r, float g, float b, float a)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [CLEAR_RT] Clear '{textureName}' to ({r}, {g}, {b}, {a})");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearDepth(string textureName, float depth)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [CLEAR_DEPTH] Clear '{textureName}' to {depth}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CopyTexture(string source, string destination)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine($" [COPY] Copy from '{source}' to '{destination}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct RasterRenderContext
|
|
||||||
{
|
|
||||||
private readonly ICommandBuffer _cmd;
|
|
||||||
|
|
||||||
public RasterRenderContext(ICommandBuffer cmd)
|
|
||||||
{
|
|
||||||
_cmd = cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetRenderTarget(string textureName) => _cmd.SetRenderTarget(textureName);
|
|
||||||
public void SetDepthStencil(string textureName) => _cmd.SetDepthStencil(textureName);
|
|
||||||
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
|
|
||||||
public void Draw(int vertexCount) => _cmd.Draw(vertexCount);
|
|
||||||
public void ClearRenderTarget(string textureName, float r, float g, float b, float a) => _cmd.ClearRenderTarget(textureName, r, g, b, a);
|
|
||||||
public void ClearDepth(string textureName, float depth) => _cmd.ClearDepth(textureName, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct ComputeRenderContext
|
|
||||||
{
|
|
||||||
private readonly ICommandBuffer _cmd;
|
|
||||||
|
|
||||||
public ComputeRenderContext(ICommandBuffer cmd)
|
|
||||||
{
|
|
||||||
_cmd = cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
|
|
||||||
public void BindUnorderedAccess(string resourceName, int slot) => _cmd.BindUnorderedAccess(resourceName, slot);
|
|
||||||
public void Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
|
|
||||||
}
|
|
||||||
172
Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md
Normal file
172
Ghost.RenderGraph.Concept/IMPLEMENTATION_NOTES.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Ghost Render Graph - Implementation Notes
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a transient render graph implementation for GhostEngine, inspired by Unity's render graph architecture. The graph rebuilds every frame but uses aggressive pooling and memory reuse to minimize GC allocations.
|
||||||
|
|
||||||
|
## Key Design Principles
|
||||||
|
|
||||||
|
### 1. **Object Pooling**
|
||||||
|
- All passes and resources are pooled via `RenderGraphObjectPool`
|
||||||
|
- Lists are reused across frames (Clear() instead of new)
|
||||||
|
- Pre-allocated capacity based on expected usage (64 passes, etc.)
|
||||||
|
|
||||||
|
### 2. **Minimal Allocations**
|
||||||
|
- Avoid LINQ - use explicit for loops
|
||||||
|
- Avoid foreach over interfaces - use indexed access
|
||||||
|
- Reuse collections by resetting count instead of clearing
|
||||||
|
- Pool all user data structures
|
||||||
|
|
||||||
|
### 3. **Transient Resources**
|
||||||
|
- Resources only live for the duration of the frame
|
||||||
|
- Resource lifetimes determined by pass dependencies
|
||||||
|
- Automatic culling of unused passes and resources
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Types
|
||||||
|
|
||||||
|
#### RenderGraphTextureHandle
|
||||||
|
Opaque handle to a texture resource. Contains index, version, and name.
|
||||||
|
|
||||||
|
#### RenderGraphPassBase & RenderGraphPass<TPassData>
|
||||||
|
- Base class for all passes
|
||||||
|
- Typed subclass holds user data and render functions
|
||||||
|
- Tracks resource dependencies (reads/writes/creates)
|
||||||
|
|
||||||
|
#### RenderGraphBuilder
|
||||||
|
- Fluent API for building passes
|
||||||
|
- IDisposable pattern for using() blocks
|
||||||
|
- Methods: CreateTexture, ReadTexture, WriteTexture, SetRenderFunc, etc.
|
||||||
|
|
||||||
|
#### RenderGraphResourceRegistry
|
||||||
|
- Manages all texture resources
|
||||||
|
- Tracks producers and consumers
|
||||||
|
- Provides pooled resource allocation
|
||||||
|
|
||||||
|
#### RenderGraphBlackboard
|
||||||
|
- Key-value store for sharing data between passes
|
||||||
|
- Type-safe Get<T>/Add<T> API
|
||||||
|
- Reused across frames
|
||||||
|
|
||||||
|
### Execution Flow
|
||||||
|
|
||||||
|
1. **Reset** - Clear previous frame data, return objects to pools
|
||||||
|
2. **Build** - Add passes and declare resource dependencies
|
||||||
|
3. **Compile** - Cull unused passes via dependency analysis
|
||||||
|
4. **Execute** - Run non-culled passes in order
|
||||||
|
|
||||||
|
### Pass Culling Algorithm
|
||||||
|
|
||||||
|
1. Mark all passes as culled initially (if AllowCulling = true)
|
||||||
|
2. Mark passes with side effects (write to imported resources) as not culled
|
||||||
|
3. Recursively un-cull all dependencies of non-culled passes
|
||||||
|
4. Result: Only passes that contribute to final output are executed
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
**Current Results (Release build):**
|
||||||
|
- **Per iteration time:** 2,292 ns (~2.3 microseconds)
|
||||||
|
- **GC per iteration:** 571 bytes (after warmup)
|
||||||
|
|
||||||
|
**Comparison to Unity:**
|
||||||
|
- Unity first frame: ~700 KB
|
||||||
|
- Unity steady state: ~100 bytes
|
||||||
|
- Our implementation: ~571 bytes steady state
|
||||||
|
|
||||||
|
The 571 bytes likely comes from:
|
||||||
|
- String allocations in TextureDescriptor (40+ bytes each)
|
||||||
|
- Some residual closure captures
|
||||||
|
- Dictionary/List capacity adjustments
|
||||||
|
|
||||||
|
This is excellent performance for a complex graph with:
|
||||||
|
- 13 render passes
|
||||||
|
- 15+ texture resources
|
||||||
|
- Blackboard data sharing
|
||||||
|
- Pass culling
|
||||||
|
- Async compute support
|
||||||
|
|
||||||
|
## API Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var renderGraph = new RenderGraph();
|
||||||
|
|
||||||
|
// Reset for new frame
|
||||||
|
renderGraph.Reset();
|
||||||
|
|
||||||
|
// Import backbuffer
|
||||||
|
var backbuffer = renderGraph.ImportTexture("Backbuffer",
|
||||||
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
|
||||||
|
|
||||||
|
// Add a render pass
|
||||||
|
GBufferData gbufferData;
|
||||||
|
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
|
||||||
|
{
|
||||||
|
// Create transient textures
|
||||||
|
var albedo = builder.CreateTexture(
|
||||||
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
|
||||||
|
|
||||||
|
// Mark dependencies
|
||||||
|
gbufferData.Albedo = builder.WriteTexture(albedo);
|
||||||
|
|
||||||
|
// Set render function
|
||||||
|
builder.SetRenderFunc<GBufferData>((data, cmd) =>
|
||||||
|
{
|
||||||
|
cmd.SetRenderTarget(data.Albedo.Name);
|
||||||
|
cmd.Draw(36000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share data between passes
|
||||||
|
renderGraph.Blackboard.Add(gbufferData);
|
||||||
|
|
||||||
|
// Compile and execute
|
||||||
|
renderGraph.Compile();
|
||||||
|
renderGraph.Execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Optimizations
|
||||||
|
|
||||||
|
1. **Use ArrayPool or stackalloc** for temporary allocations
|
||||||
|
2. **Intern strings** for resource names to avoid duplicates
|
||||||
|
3. **Use struct-based** TextureDescriptor to avoid heap allocations
|
||||||
|
4. **Pre-size collections** more accurately based on profiling
|
||||||
|
5. **Use native collections** (Unity.Collections) for zero-alloc operations
|
||||||
|
6. **Cache compiled graphs** across similar frames
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `RenderGraphTypes.cs` - Core handle and descriptor types
|
||||||
|
- `RenderGraphResourcePool.cs` - Object pooling and resource management
|
||||||
|
- `RenderGraphPass.cs` - Pass types and builder
|
||||||
|
- `RenderGraphContext.cs` - Execution contexts
|
||||||
|
- `RenderGraphBlackboard.cs` - Inter-pass data sharing
|
||||||
|
- `RenderGraph.cs` - Main graph class
|
||||||
|
- `PassData.cs` - Example pass data structures
|
||||||
|
- `Program.cs` - Test/example code
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
**NOT thread-safe.** The render graph is designed to be called from a single thread (the render thread). Multi-threaded pass execution would require significant changes to the resource tracking system.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
1. No async/await support in render functions
|
||||||
|
2. No resource aliasing/reuse optimization yet
|
||||||
|
3. No render pass merging (could merge compatible passes)
|
||||||
|
4. Simple forward-only dependency tracking
|
||||||
|
5. No memory budgeting or OOM protection
|
||||||
|
|
||||||
|
## Differences from Unity
|
||||||
|
|
||||||
|
1. **Simpler API** - No multi-level builder hierarchy
|
||||||
|
2. **No native render pass support** - Could be added for tile-based GPUs
|
||||||
|
3. **No resource pooling** - Unity pools actual GPU resources
|
||||||
|
4. **No debug visualization** - Unity has render graph viewer
|
||||||
|
5. **Explicit type parameters** - Required due to C# lambda type inference
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This implementation demonstrates a production-ready transient render graph with excellent performance characteristics. The ~571 byte allocation per frame is well within acceptable bounds for a AAA game engine, especially considering the complexity of the graph being built.
|
||||||
|
|
||||||
|
The architecture is extensible and can be enhanced with additional optimizations like resource aliasing, pass merging, and GPU resource pooling as needed.
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
// Pass data structure for GBuffer outputs
|
// ===== Pass Data Structures =====
|
||||||
public class GBufferData
|
// These are user-defined data structures that get passed to render functions
|
||||||
|
|
||||||
|
public sealed class GBufferData : IPassData
|
||||||
{
|
{
|
||||||
public RenderGraphTextureHandle Albedo;
|
public RenderGraphTextureHandle Albedo;
|
||||||
public RenderGraphTextureHandle Normal;
|
public RenderGraphTextureHandle Normal;
|
||||||
public RenderGraphTextureHandle Depth;
|
public RenderGraphTextureHandle Depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LightingPassData
|
public sealed class LightingPassData : IPassData
|
||||||
{
|
{
|
||||||
public RenderGraphTextureHandle GBufferAlbedo;
|
public RenderGraphTextureHandle GBufferAlbedo;
|
||||||
public RenderGraphTextureHandle GBufferNormal;
|
public RenderGraphTextureHandle GBufferNormal;
|
||||||
@@ -16,43 +18,38 @@ public class LightingPassData
|
|||||||
public RenderGraphTextureHandle OutputLighting;
|
public RenderGraphTextureHandle OutputLighting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SSAOPassData
|
public sealed class SSAOPassData : IPassData
|
||||||
{
|
{
|
||||||
public RenderGraphTextureHandle GBufferDepth;
|
public RenderGraphTextureHandle GBufferDepth;
|
||||||
public RenderGraphTextureHandle GBufferNormal;
|
public RenderGraphTextureHandle GBufferNormal;
|
||||||
public RenderGraphTextureHandle OutputSSAO;
|
public RenderGraphTextureHandle OutputSSAO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TAAPassData
|
public sealed class BloomDownsampleData : IPassData
|
||||||
{
|
|
||||||
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 RenderGraphTextureHandle Input;
|
public RenderGraphTextureHandle Input;
|
||||||
public RenderGraphTextureHandle Output;
|
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 InputTAA;
|
||||||
public RenderGraphTextureHandle InputSSAO;
|
public RenderGraphTextureHandle InputSSAO;
|
||||||
public RenderGraphTextureHandle InputBloom;
|
public RenderGraphTextureHandle InputBloom;
|
||||||
public RenderGraphTextureHandle OutputBackbuffer;
|
public RenderGraphTextureHandle OutputBackbuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ProfilerMarkerData : IPassData
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DebugPassData : IPassData
|
||||||
|
{
|
||||||
|
public RenderGraphTextureHandle DebugTexture;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,43 @@
|
|||||||
using Ghost.RenderGraph.Concept;
|
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();
|
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 sw = new System.Diagnostics.Stopwatch();
|
||||||
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
|
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
|
||||||
sw.Start();
|
sw.Start();
|
||||||
|
|
||||||
for (int i = 0; i < 500; i++)
|
for (var i = 0; i < _ITERATION; i++)
|
||||||
{
|
{
|
||||||
BuildGraph(renderGraph);
|
ExecuteGraph(renderGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
//BuildGraph(renderGraph);
|
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
var gcAfter = GC.GetAllocatedBytesForCurrentThread();
|
var gcAfter = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / 500} ns");
|
Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / _ITERATION} ns (per iteration)");
|
||||||
Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / 500} bytes");
|
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.WriteLine("\n\n=== FRAME 2 (Cache Hit Expected) ===");
|
||||||
//Console.ReadKey();
|
ExecuteGraph(renderGraph);
|
||||||
|
#endif
|
||||||
|
|
||||||
static void BuildGraph(RenderGraph renderGraph)
|
static void ExecuteGraph(RenderGraph renderGraph)
|
||||||
{
|
{
|
||||||
renderGraph.Reset();
|
renderGraph.Reset(); // new RenderGraph()
|
||||||
|
|
||||||
// Import external resources
|
// Import external resources
|
||||||
var backbuffer = renderGraph.ImportTexture(
|
var backbuffer = renderGraph.ImportTexture(
|
||||||
@@ -56,7 +58,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
gbufferData.Normal = builder.WriteTexture(normal);
|
gbufferData.Normal = builder.WriteTexture(normal);
|
||||||
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
|
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<GBufferData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.SetRenderTarget(data.Albedo.Name);
|
cmd.SetRenderTarget(data.Albedo.Name);
|
||||||
cmd.SetRenderTarget(data.Normal.Name);
|
cmd.SetRenderTarget(data.Normal.Name);
|
||||||
@@ -87,7 +89,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
|
||||||
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
|
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<LightingPassData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
|
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
|
||||||
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
||||||
@@ -112,7 +114,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
|
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
|
||||||
|
|
||||||
// Use SetComputeFunc with asyncCompute: true
|
// Use SetComputeFunc with asyncCompute: true
|
||||||
builder.SetComputeFunc((data, cmd) =>
|
builder.SetComputeFunc<SSAOPassData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
|
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
|
||||||
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
||||||
@@ -132,7 +134,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
|
||||||
bloomData.Output = builder.WriteTexture(bloomOutput);
|
bloomData.Output = builder.WriteTexture(bloomOutput);
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<BloomDownsampleData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.Input.Name, 0);
|
cmd.BindShaderResource(data.Input.Name, 0);
|
||||||
cmd.SetRenderTarget(data.Output.Name);
|
cmd.SetRenderTarget(data.Output.Name);
|
||||||
@@ -150,7 +152,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
|
||||||
taaData.OutputTAA = builder.WriteTexture(taaOutput);
|
taaData.OutputTAA = builder.WriteTexture(taaOutput);
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<TAAPassData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.InputLighting.Name, 0);
|
cmd.BindShaderResource(data.InputLighting.Name, 0);
|
||||||
cmd.SetRenderTarget(data.OutputTAA.Name);
|
cmd.SetRenderTarget(data.OutputTAA.Name);
|
||||||
@@ -166,7 +168,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
postData.InputBloom = builder.ReadTexture(bloomOutput);
|
postData.InputBloom = builder.ReadTexture(bloomOutput);
|
||||||
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
|
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<PostProcessingPassDataV2>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.BindShaderResource(data.InputTAA.Name, 0);
|
cmd.BindShaderResource(data.InputTAA.Name, 0);
|
||||||
cmd.BindShaderResource(data.InputSSAO.Name, 1);
|
cmd.BindShaderResource(data.InputSSAO.Name, 1);
|
||||||
@@ -180,7 +182,7 @@ static void BuildGraph(RenderGraph renderGraph)
|
|||||||
using (var builder = renderGraph.AddRenderPass<ProfilerMarkerData>("GPU Profiler Begin Frame", out var profilerData))
|
using (var builder = renderGraph.AddRenderPass<ProfilerMarkerData>("GPU Profiler Begin Frame", out var profilerData))
|
||||||
{
|
{
|
||||||
builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling
|
builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<ProfilerMarkerData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
// Note: In a real implementation we would have specific profiler commands
|
// 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
|
// 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(
|
debugData.DebugTexture = builder.WriteTexture(
|
||||||
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
|
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
|
||||||
|
|
||||||
builder.SetRenderFunc((data, cmd) =>
|
builder.SetRenderFunc<DebugPassData>((data, cmd) =>
|
||||||
{
|
{
|
||||||
cmd.SetRenderTarget(data.DebugTexture.Name);
|
cmd.SetRenderTarget(data.DebugTexture.Name);
|
||||||
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
|
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
|
||||||
|
|||||||
197
Ghost.RenderGraph.Concept/README.md
Normal file
197
Ghost.RenderGraph.Concept/README.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# GhostEngine Render Graph
|
||||||
|
|
||||||
|
A high-performance, transient render graph implementation for GhostEngine, inspired by Unity, Unreal, and other AAA engines.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✅ **Transient Architecture** - Graph rebuilds every frame for maximum flexibility
|
||||||
|
✅ **Minimal GC** - Only ~571 bytes allocated per frame (after warmup)
|
||||||
|
✅ **Automatic Pass Culling** - Unused passes are automatically removed
|
||||||
|
✅ **Resource Tracking** - Automatic resource lifetime management
|
||||||
|
✅ **Blackboard Pattern** - Share data between passes easily
|
||||||
|
✅ **Async Compute Support** - Mark passes for async execution
|
||||||
|
✅ **Type-Safe API** - Strongly-typed pass data
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
```
|
||||||
|
Per iteration time: 2,292 ns (~2.3 microseconds)
|
||||||
|
GC allocated: 571 bytes per iteration (after warmup)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tested with a complex graph containing:
|
||||||
|
- 13 render passes (GBuffer, Lighting, SSAO, Bloom, TAA, Post-processing)
|
||||||
|
- 15+ transient textures
|
||||||
|
- Multiple read/write dependencies
|
||||||
|
- Blackboard data sharing
|
||||||
|
- Pass culling optimization
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var renderGraph = new RenderGraph();
|
||||||
|
|
||||||
|
// Each frame:
|
||||||
|
renderGraph.Reset();
|
||||||
|
|
||||||
|
// Import external resources
|
||||||
|
var backbuffer = renderGraph.ImportTexture("Backbuffer",
|
||||||
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
|
||||||
|
|
||||||
|
// Add a pass
|
||||||
|
GBufferData gbufferData;
|
||||||
|
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
|
||||||
|
{
|
||||||
|
// Create transient textures
|
||||||
|
var albedo = builder.CreateTexture(
|
||||||
|
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
|
||||||
|
|
||||||
|
// Mark resource usage
|
||||||
|
gbufferData.Albedo = builder.WriteTexture(albedo);
|
||||||
|
|
||||||
|
// Set the render function
|
||||||
|
builder.SetRenderFunc<GBufferData>((data, cmd) =>
|
||||||
|
{
|
||||||
|
cmd.SetRenderTarget(data.Albedo.Name);
|
||||||
|
cmd.Draw(36000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share data with other passes
|
||||||
|
renderGraph.Blackboard.Add(gbufferData);
|
||||||
|
|
||||||
|
// Read from blackboard in another pass
|
||||||
|
using (var builder = renderGraph.AddRenderPass<LightingData>("Lighting", out var lightingData))
|
||||||
|
{
|
||||||
|
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||||
|
lightingData.Albedo = builder.ReadTexture(gbuffer.Albedo);
|
||||||
|
// ... rest of pass setup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile and execute
|
||||||
|
renderGraph.Compile();
|
||||||
|
renderGraph.Execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### RenderGraph
|
||||||
|
|
||||||
|
**`void Reset()`**
|
||||||
|
Clears the graph for a new frame. Reuses allocations to minimize GC.
|
||||||
|
|
||||||
|
**`RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor desc)`**
|
||||||
|
Imports an external texture (like the backbuffer) into the graph.
|
||||||
|
|
||||||
|
**`RenderGraphBuilder AddRenderPass<TPassData>(string name, out TPassData data)`**
|
||||||
|
Adds a new render pass. Returns a builder for configuring the pass.
|
||||||
|
|
||||||
|
**`void Compile()`**
|
||||||
|
Analyzes dependencies and culls unused passes.
|
||||||
|
|
||||||
|
**`void Execute()`**
|
||||||
|
Executes all compiled (non-culled) passes.
|
||||||
|
|
||||||
|
**`RenderGraphBlackboard Blackboard { get; }`**
|
||||||
|
Access the blackboard for sharing data between passes.
|
||||||
|
|
||||||
|
### RenderGraphBuilder
|
||||||
|
|
||||||
|
**`RenderGraphTextureHandle CreateTexture(TextureDescriptor desc)`**
|
||||||
|
Creates a transient texture that only lives during this pass.
|
||||||
|
|
||||||
|
**`RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)`**
|
||||||
|
Marks a texture as read by this pass.
|
||||||
|
|
||||||
|
**`RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)`**
|
||||||
|
Marks a texture as written by this pass.
|
||||||
|
|
||||||
|
**`RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)`**
|
||||||
|
Sets up depth buffer usage for this pass.
|
||||||
|
|
||||||
|
**`void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> func)`**
|
||||||
|
Sets the raster render function for this pass.
|
||||||
|
|
||||||
|
**`void SetComputeFunc<TPassData>(Action<TPassData, ComputeRenderContext> func, bool asyncCompute = false)`**
|
||||||
|
Sets the compute function for this pass. Optionally mark as async.
|
||||||
|
|
||||||
|
**`void SetAllowCulling(bool allow)`**
|
||||||
|
Controls whether this pass can be culled if its outputs are unused.
|
||||||
|
|
||||||
|
### RenderGraphBlackboard
|
||||||
|
|
||||||
|
**`void Add<T>(T data) where T : class, IPassData`**
|
||||||
|
Stores pass data in the blackboard.
|
||||||
|
|
||||||
|
**`T Get<T>() where T : class, IPassData`**
|
||||||
|
Retrieves pass data from the blackboard.
|
||||||
|
|
||||||
|
**`bool TryGet<T>(out T data) where T : class, IPassData`**
|
||||||
|
Attempts to retrieve pass data from the blackboard.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The render graph uses several key patterns to achieve minimal GC:
|
||||||
|
|
||||||
|
1. **Object Pooling** - All passes and data structures are pooled and reused
|
||||||
|
2. **Collection Reuse** - Lists are cleared instead of reallocated
|
||||||
|
3. **Pre-allocation** - Capacity is pre-allocated based on expected usage
|
||||||
|
4. **Avoid LINQ** - Explicit loops instead of LINQ for zero allocation
|
||||||
|
5. **Struct Handles** - Resource handles are lightweight value types
|
||||||
|
|
||||||
|
### Pass Culling
|
||||||
|
|
||||||
|
The graph automatically removes unused passes:
|
||||||
|
|
||||||
|
1. Passes that write to imported resources have side effects (never culled)
|
||||||
|
2. All other passes are initially marked as culled
|
||||||
|
3. Dependencies of non-culled passes are recursively un-culled
|
||||||
|
4. Only passes contributing to the final output remain
|
||||||
|
|
||||||
|
This means you can freely add debug/visualization passes - they'll be automatically removed if unused.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs 500 warmup iterations, then measures 500 more to determine average performance.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
See [IMPLEMENTATION_NOTES.md](IMPLEMENTATION_NOTES.md) for detailed architecture documentation.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **Single-threaded** - Not thread-safe, designed for render thread only
|
||||||
|
- **No GPU resource pooling** - Currently uses mock command buffers
|
||||||
|
- **No render pass merging** - Compatible passes could be merged for better performance on tile-based GPUs
|
||||||
|
- **No resource aliasing** - Could reuse memory for non-overlapping resource lifetimes
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Resource aliasing for memory efficiency
|
||||||
|
- [ ] Native render pass merging (for tile-based GPUs)
|
||||||
|
- [ ] GPU resource pooling
|
||||||
|
- [ ] Async/await support in render functions
|
||||||
|
- [ ] Memory budgeting and OOM protection
|
||||||
|
- [ ] Debug visualization (like Unity's Render Graph Viewer)
|
||||||
|
- [ ] Multi-threaded pass recording
|
||||||
|
- [ ] Graph caching across similar frames
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of GhostEngine. See repository root for license information.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Unity Render Graph: https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@latest
|
||||||
|
- Unreal RDG: https://docs.unrealengine.com/5.0/en-US/render-dependency-graph-in-unreal-engine/
|
||||||
|
- Frostbite Frame Graph: https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rendering-Architecture-in
|
||||||
File diff suppressed because it is too large
Load Diff
329
Ghost.RenderGraph.Concept/RenderGraphAliasing.cs
Normal file
329
Ghost.RenderGraph.Concept/RenderGraphAliasing.cs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
using Ghost.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a physical GPU resource that can be aliased by multiple logical resources.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PhysicalResource
|
||||||
|
{
|
||||||
|
public int Index;
|
||||||
|
public int Width;
|
||||||
|
public int Height;
|
||||||
|
public TextureFormat Format;
|
||||||
|
public int SizeInBytes;
|
||||||
|
|
||||||
|
// Lifetime tracking
|
||||||
|
public int FirstUsePass = int.MaxValue;
|
||||||
|
public int LastUsePass = -1;
|
||||||
|
|
||||||
|
// Aliasing tracking
|
||||||
|
public readonly List<int> AliasedLogicalResources = new(4);
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Index = -1;
|
||||||
|
Width = 0;
|
||||||
|
Height = 0;
|
||||||
|
Format = TextureFormat.RGBA8;
|
||||||
|
SizeInBytes = 0;
|
||||||
|
FirstUsePass = int.MaxValue;
|
||||||
|
LastUsePass = -1;
|
||||||
|
AliasedLogicalResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanAlias(TextureDescriptor descriptor)
|
||||||
|
{
|
||||||
|
// For aliasing, resources must be identical in size and format
|
||||||
|
// In a real implementation, you could be more flexible (e.g., same size but different format)
|
||||||
|
return Width == descriptor.Width &&
|
||||||
|
Height == descriptor.Height &&
|
||||||
|
Format == descriptor.Format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLifetime(int passIndex)
|
||||||
|
{
|
||||||
|
FirstUsePass = Math.Min(FirstUsePass, passIndex);
|
||||||
|
LastUsePass = Math.Max(LastUsePass, passIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAliveAt(int passIndex)
|
||||||
|
{
|
||||||
|
return passIndex >= FirstUsePass && passIndex <= LastUsePass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CalculateSize()
|
||||||
|
{
|
||||||
|
int bytesPerPixel = Format switch
|
||||||
|
{
|
||||||
|
TextureFormat.RGBA8 => 4,
|
||||||
|
TextureFormat.RGBA16F => 8,
|
||||||
|
TextureFormat.RGBA32F => 16,
|
||||||
|
TextureFormat.Depth32F => 4,
|
||||||
|
TextureFormat.Depth24Stencil8 => 4,
|
||||||
|
_ => 4
|
||||||
|
};
|
||||||
|
return Width * Height * bytesPerPixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages physical resource allocation and aliasing.
|
||||||
|
/// Uses interval scheduling algorithm to minimize memory usage.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ResourceAliasingManager
|
||||||
|
{
|
||||||
|
private readonly List<PhysicalResource> _physicalResources = new(32);
|
||||||
|
private readonly RenderGraphObjectPool _pool = new();
|
||||||
|
private int _physicalResourceCount;
|
||||||
|
|
||||||
|
// Mapping from logical resource index to physical resource index
|
||||||
|
private readonly Dictionary<int, int> _logicalToPhysical = new(64);
|
||||||
|
|
||||||
|
public void BeginFrame()
|
||||||
|
{
|
||||||
|
_physicalResourceCount = 0;
|
||||||
|
_logicalToPhysical.Clear();
|
||||||
|
|
||||||
|
// Reset physical resources but keep them in the pool
|
||||||
|
for (int i = 0; i < _physicalResources.Count; i++)
|
||||||
|
{
|
||||||
|
_physicalResources[i].Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assigns physical resources to logical resources using greedy interval scheduling.
|
||||||
|
/// This minimizes total GPU memory usage.
|
||||||
|
/// </summary>
|
||||||
|
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine("\n=== Resource Aliasing Analysis ===");
|
||||||
|
int totalLogicalSize = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Build list of all logical resources with their lifetimes
|
||||||
|
var logicalResources = ListPool<(int index, TextureResource resource)>.Rent();
|
||||||
|
|
||||||
|
for (int i = 0; i < registry.TextureResourceCount; i++)
|
||||||
|
{
|
||||||
|
var resource = registry.GetTextureResourceByIndex(i);
|
||||||
|
if (!resource.IsImported) // Don't alias imported resources
|
||||||
|
{
|
||||||
|
logicalResources.Add((i, resource));
|
||||||
|
#if DEBUG
|
||||||
|
int size = CalculateSize(resource.Descriptor);
|
||||||
|
totalLogicalSize += size;
|
||||||
|
Console.WriteLine($"Logical Resource {i}: {resource.Descriptor.Name}");
|
||||||
|
Console.WriteLine($" Lifetime: Pass {resource.FirstUsePass} -> {resource.LastUsePass}");
|
||||||
|
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by first use pass (earlier resources first)
|
||||||
|
logicalResources.Sort((a, b) => a.resource.FirstUsePass.CompareTo(b.resource.FirstUsePass));
|
||||||
|
|
||||||
|
// Greedy interval scheduling: assign each logical resource to a physical resource
|
||||||
|
foreach (var (logicalIndex, logicalResource) in logicalResources)
|
||||||
|
{
|
||||||
|
PhysicalResource? assignedPhysical = null;
|
||||||
|
|
||||||
|
// Try to find an existing physical resource that:
|
||||||
|
// 1. Has compatible format/size
|
||||||
|
// 2. Is not alive during this logical resource's lifetime
|
||||||
|
for (int i = 0; i < _physicalResourceCount; i++)
|
||||||
|
{
|
||||||
|
var physical = _physicalResources[i];
|
||||||
|
|
||||||
|
if (physical.CanAlias(logicalResource.Descriptor) &&
|
||||||
|
!HasLifetimeOverlap(physical, logicalResource))
|
||||||
|
{
|
||||||
|
assignedPhysical = physical;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No compatible physical resource found, allocate a new one
|
||||||
|
if (assignedPhysical == null)
|
||||||
|
{
|
||||||
|
assignedPhysical = GetOrCreatePhysicalResource();
|
||||||
|
assignedPhysical.Index = _physicalResourceCount - 1;
|
||||||
|
assignedPhysical.Width = logicalResource.Descriptor.Width;
|
||||||
|
assignedPhysical.Height = logicalResource.Descriptor.Height;
|
||||||
|
assignedPhysical.Format = logicalResource.Descriptor.Format;
|
||||||
|
assignedPhysical.SizeInBytes = assignedPhysical.CalculateSize();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine($"\nAllocated NEW Physical Resource {assignedPhysical.Index}:");
|
||||||
|
Console.WriteLine($" Size: {assignedPhysical.Width}x{assignedPhysical.Height}");
|
||||||
|
Console.WriteLine($" Format: {assignedPhysical.Format}");
|
||||||
|
Console.WriteLine($" Memory: {assignedPhysical.SizeInBytes / 1024.0:F2} KB");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\nALIASING: {logicalResource.Descriptor.Name} -> Physical Resource {assignedPhysical.Index}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Update physical resource lifetime
|
||||||
|
assignedPhysical.UpdateLifetime(logicalResource.FirstUsePass);
|
||||||
|
assignedPhysical.UpdateLifetime(logicalResource.LastUsePass);
|
||||||
|
assignedPhysical.AliasedLogicalResources.Add(logicalIndex);
|
||||||
|
|
||||||
|
// Record the mapping
|
||||||
|
_logicalToPhysical[logicalIndex] = assignedPhysical.Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
int totalPhysicalSize = 0;
|
||||||
|
for (int i = 0; i < _physicalResourceCount; i++)
|
||||||
|
{
|
||||||
|
totalPhysicalSize += _physicalResources[i].SizeInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"\n=== Aliasing Summary ===");
|
||||||
|
Console.WriteLine($"Logical Resources: {logicalResources.Count}");
|
||||||
|
Console.WriteLine($"Physical Resources: {_physicalResourceCount}");
|
||||||
|
Console.WriteLine($"Total Logical Memory: {totalLogicalSize / 1024.0:F2} KB");
|
||||||
|
Console.WriteLine($"Total Physical Memory: {totalPhysicalSize / 1024.0:F2} KB");
|
||||||
|
Console.WriteLine($"Memory Saved: {(totalLogicalSize - totalPhysicalSize) / 1024.0:F2} KB ({(1.0 - (double)totalPhysicalSize / totalLogicalSize) * 100.0:F1}%)");
|
||||||
|
Console.WriteLine("================================\n");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ListPool<(int index, TextureResource resource)>.Return(logicalResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetPhysicalResourceIndex(int logicalIndex)
|
||||||
|
{
|
||||||
|
return _logicalToPhysical.TryGetValue(logicalIndex, out var physicalIndex) ? physicalIndex : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PhysicalResource? GetPhysicalResource(int physicalIndex)
|
||||||
|
{
|
||||||
|
return physicalIndex >= 0 && physicalIndex < _physicalResourceCount
|
||||||
|
? _physicalResources[physicalIndex]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasLifetimeOverlap(PhysicalResource physical, TextureResource logical)
|
||||||
|
{
|
||||||
|
// Check if the lifetimes overlap
|
||||||
|
// No overlap if: logical.First > physical.Last OR logical.Last < physical.First
|
||||||
|
return !(logical.FirstUsePass > physical.LastUsePass ||
|
||||||
|
logical.LastUsePass < physical.FirstUsePass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhysicalResource GetOrCreatePhysicalResource()
|
||||||
|
{
|
||||||
|
PhysicalResource resource;
|
||||||
|
if (_physicalResourceCount < _physicalResources.Count)
|
||||||
|
{
|
||||||
|
resource = _physicalResources[_physicalResourceCount];
|
||||||
|
resource.Reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resource = _pool.Get<PhysicalResource>();
|
||||||
|
resource.Reset();
|
||||||
|
_physicalResources.Add(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
_physicalResourceCount++;
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CalculateSize(TextureDescriptor descriptor)
|
||||||
|
{
|
||||||
|
int bytesPerPixel = descriptor.Format switch
|
||||||
|
{
|
||||||
|
TextureFormat.RGBA8 => 4,
|
||||||
|
TextureFormat.RGBA16F => 8,
|
||||||
|
TextureFormat.RGBA32F => 16,
|
||||||
|
TextureFormat.Depth32F => 4,
|
||||||
|
TextureFormat.Depth24Stencil8 => 4,
|
||||||
|
_ => 4
|
||||||
|
};
|
||||||
|
return descriptor.Width * descriptor.Height * bytesPerPixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _physicalResources.Count; i++)
|
||||||
|
{
|
||||||
|
_pool.Release(_physicalResources[i]);
|
||||||
|
}
|
||||||
|
_physicalResources.Clear();
|
||||||
|
_physicalResourceCount = 0;
|
||||||
|
_logicalToPhysical.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores aliasing state from cache.
|
||||||
|
/// </summary>
|
||||||
|
public void RestoreFromCache(Dictionary<int, int> logicalToPhysical, List<PhysicalResourceData> physicalData)
|
||||||
|
{
|
||||||
|
_logicalToPhysical.Clear();
|
||||||
|
foreach (var kvp in logicalToPhysical)
|
||||||
|
{
|
||||||
|
_logicalToPhysical[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore physical resources
|
||||||
|
_physicalResourceCount = physicalData.Count;
|
||||||
|
for (int i = 0; i < physicalData.Count; i++)
|
||||||
|
{
|
||||||
|
PhysicalResource physical;
|
||||||
|
if (i < _physicalResources.Count)
|
||||||
|
{
|
||||||
|
physical = _physicalResources[i];
|
||||||
|
physical.Reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
physical = _pool.Get<PhysicalResource>();
|
||||||
|
physical.Reset();
|
||||||
|
_physicalResources.Add(physical);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = physicalData[i];
|
||||||
|
physical.Index = data.Index;
|
||||||
|
physical.Width = data.Width;
|
||||||
|
physical.Height = data.Height;
|
||||||
|
physical.Format = data.Format;
|
||||||
|
physical.FirstUsePass = data.FirstUsePass;
|
||||||
|
physical.LastUsePass = data.LastUsePass;
|
||||||
|
physical.SizeInBytes = physical.CalculateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores current aliasing state to cache.
|
||||||
|
/// </summary>
|
||||||
|
public void StoreToCache(Dictionary<int, int> outLogicalToPhysical, List<PhysicalResourceData> outPhysicalData)
|
||||||
|
{
|
||||||
|
outLogicalToPhysical.Clear();
|
||||||
|
foreach (var kvp in _logicalToPhysical)
|
||||||
|
{
|
||||||
|
outLogicalToPhysical[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
outPhysicalData.Clear();
|
||||||
|
for (int i = 0; i < _physicalResourceCount; i++)
|
||||||
|
{
|
||||||
|
var physical = _physicalResources[i];
|
||||||
|
outPhysicalData.Add(new PhysicalResourceData
|
||||||
|
{
|
||||||
|
Index = physical.Index,
|
||||||
|
Width = physical.Width,
|
||||||
|
Height = physical.Height,
|
||||||
|
Format = physical.Format,
|
||||||
|
FirstUsePass = physical.FirstUsePass,
|
||||||
|
LastUsePass = physical.LastUsePass
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Ghost.RenderGraph.Concept/RenderGraphBarriers.cs
Normal file
154
Ghost.RenderGraph.Concept/RenderGraphBarriers.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GPU resource states for barrier tracking.
|
||||||
|
/// Based on D3D12 resource states.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum ResourceState
|
||||||
|
{
|
||||||
|
Undefined = 0,
|
||||||
|
RenderTarget = 1 << 0,
|
||||||
|
DepthWrite = 1 << 1,
|
||||||
|
DepthRead = 1 << 2,
|
||||||
|
ShaderResource = 1 << 3,
|
||||||
|
UnorderedAccess = 1 << 4,
|
||||||
|
CopySource = 1 << 5,
|
||||||
|
CopyDest = 1 << 6,
|
||||||
|
Present = 1 << 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of barriers that can be inserted.
|
||||||
|
/// </summary>
|
||||||
|
public enum BarrierType
|
||||||
|
{
|
||||||
|
Transition, // State transition (e.g., RenderTarget -> ShaderResource)
|
||||||
|
Aliasing, // Aliasing barrier (new resource reusing memory)
|
||||||
|
UAV, // UAV barrier (synchronize UAV access)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a resource barrier that needs to be inserted.
|
||||||
|
/// For D3D12 aliasing barriers: ResourceBefore is the old resource, ResourceAfter is the new resource.
|
||||||
|
/// </summary>
|
||||||
|
internal struct ResourceBarrier
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
private struct barrier_union
|
||||||
|
{
|
||||||
|
internal struct barrier_union_transition
|
||||||
|
{
|
||||||
|
public RenderGraphTextureHandle Resource;
|
||||||
|
public ResourceState StateBefore;
|
||||||
|
public ResourceState StateAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct barrier_union_aliasing
|
||||||
|
{
|
||||||
|
public RenderGraphTextureHandle ResourceBefore;
|
||||||
|
public RenderGraphTextureHandle ResourceAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: union can not have non-blittable types
|
||||||
|
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public barrier_union_transition Transition;
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public barrier_union_aliasing Aliasing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BarrierType Type;
|
||||||
|
|
||||||
|
// For Transition and UAV barriers
|
||||||
|
public RenderGraphTextureHandle Resource;
|
||||||
|
public ResourceState StateBefore;
|
||||||
|
public ResourceState StateAfter;
|
||||||
|
|
||||||
|
// For Aliasing barriers (D3D12_RESOURCE_BARRIER::Aliasing)
|
||||||
|
public RenderGraphTextureHandle ResourceBefore; // pResourceBefore
|
||||||
|
public RenderGraphTextureHandle ResourceAfter; // pResourceAfter
|
||||||
|
|
||||||
|
public int PassIndex;
|
||||||
|
|
||||||
|
// Constructor for Transition and UAV barriers
|
||||||
|
public ResourceBarrier(BarrierType type, RenderGraphTextureHandle resource,
|
||||||
|
ResourceState before, ResourceState after, int passIndex)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Resource = resource;
|
||||||
|
StateBefore = before;
|
||||||
|
StateAfter = after;
|
||||||
|
ResourceBefore = default;
|
||||||
|
ResourceAfter = default;
|
||||||
|
PassIndex = passIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for Aliasing barriers
|
||||||
|
public static ResourceBarrier CreateAliasingBarrier(
|
||||||
|
RenderGraphTextureHandle resourceBefore,
|
||||||
|
RenderGraphTextureHandle resourceAfter,
|
||||||
|
int passIndex)
|
||||||
|
{
|
||||||
|
return new ResourceBarrier
|
||||||
|
{
|
||||||
|
Type = BarrierType.Aliasing,
|
||||||
|
ResourceBefore = resourceBefore,
|
||||||
|
ResourceAfter = resourceAfter,
|
||||||
|
PassIndex = passIndex,
|
||||||
|
Resource = default,
|
||||||
|
StateBefore = ResourceState.Undefined,
|
||||||
|
StateAfter = ResourceState.Undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceBarrier CreateTransitionBarrier(
|
||||||
|
RenderGraphTextureHandle resource,
|
||||||
|
ResourceState before,
|
||||||
|
ResourceState after,
|
||||||
|
int passIndex)
|
||||||
|
{
|
||||||
|
return new ResourceBarrier
|
||||||
|
{
|
||||||
|
Type = BarrierType.Transition,
|
||||||
|
Resource = resource,
|
||||||
|
StateBefore = before,
|
||||||
|
StateAfter = after,
|
||||||
|
PassIndex = passIndex,
|
||||||
|
ResourceBefore = default,
|
||||||
|
ResourceAfter = default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
public override readonly string ToString()
|
||||||
|
{
|
||||||
|
return Type switch
|
||||||
|
{
|
||||||
|
BarrierType.Transition => $"[Pass {PassIndex}] TRANSITION: {Resource.Name} ({StateBefore} -> {StateAfter})",
|
||||||
|
BarrierType.Aliasing => $"[Pass {PassIndex}] ALIASING: {ResourceBefore.Name} -> {ResourceAfter.Name} (reusing physical memory)",
|
||||||
|
BarrierType.UAV => $"[Pass {PassIndex}] UAV: {Resource.Name}",
|
||||||
|
_ => $"[Pass {PassIndex}] UNKNOWN BARRIER"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks the current state of a resource across passes.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ResourceStateTracker
|
||||||
|
{
|
||||||
|
public int ResourceIndex;
|
||||||
|
public ResourceState CurrentState = ResourceState.Undefined;
|
||||||
|
public int LastAccessPass = -1;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
ResourceIndex = -1;
|
||||||
|
CurrentState = ResourceState.Undefined;
|
||||||
|
LastAccessPass = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
internal class RenderGraphBatch
|
|
||||||
{
|
|
||||||
public int ID { get; private set; }
|
|
||||||
public RenderQueueType QueueType { get; private set; }
|
|
||||||
public List<RenderGraphPass> Passes { get; } = new();
|
|
||||||
|
|
||||||
// Fences to wait on BEFORE executing this batch
|
|
||||||
public List<int> WaitFences { get; } = new();
|
|
||||||
|
|
||||||
// Fences to signal AFTER executing this batch
|
|
||||||
public List<int> SignalFences { get; } = new();
|
|
||||||
|
|
||||||
public RenderGraphBatch()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize(int id, RenderQueueType queueType)
|
|
||||||
{
|
|
||||||
ID = id;
|
|
||||||
QueueType = queueType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
Passes.Clear();
|
|
||||||
WaitFences.Clear();
|
|
||||||
SignalFences.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,43 @@
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
public class RenderGraphBlackboard
|
/// <summary>
|
||||||
|
/// Blackboard for sharing data between render passes.
|
||||||
|
/// Uses a dictionary with type keys to store different pass data types.
|
||||||
|
/// Avoids allocations by reusing the same dictionary across frames.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RenderGraphBlackboard
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Type, object> _data = new();
|
private readonly Dictionary<Type, object> _data = new(16);
|
||||||
|
|
||||||
public void Add<T>(T data) where T : class
|
/// <summary>
|
||||||
|
/// Adds or updates pass data in the blackboard.
|
||||||
|
/// </summary>
|
||||||
|
public void Add<T>(T data) where T : class, IPassData
|
||||||
{
|
{
|
||||||
_data[typeof(T)] = data;
|
var type = typeof(T);
|
||||||
|
_data[type] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T Get<T>() where T : class
|
/// <summary>
|
||||||
|
/// Retrieves pass data from the blackboard.
|
||||||
|
/// </summary>
|
||||||
|
public T Get<T>() where T : class, IPassData
|
||||||
{
|
{
|
||||||
if (_data.TryGetValue(typeof(T), out var data))
|
var type = typeof(T);
|
||||||
|
if (_data.TryGetValue(type, out var obj))
|
||||||
{
|
{
|
||||||
return (T)data;
|
return (T)obj;
|
||||||
}
|
}
|
||||||
throw new KeyNotFoundException($"Data of type {typeof(T).Name} not found in blackboard.");
|
throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGet<T>(out T? data) where T : class
|
/// <summary>
|
||||||
|
/// Tries to get pass data from the blackboard.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGet<T>(out T? data) where T : class, IPassData
|
||||||
{
|
{
|
||||||
if (_data.TryGetValue(typeof(T), out var obj))
|
var type = typeof(T);
|
||||||
|
if (_data.TryGetValue(type, out var obj))
|
||||||
{
|
{
|
||||||
data = (T)obj;
|
data = (T)obj;
|
||||||
return true;
|
return true;
|
||||||
@@ -29,6 +46,10 @@ public class RenderGraphBlackboard
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all data from the blackboard.
|
||||||
|
/// Does not deallocate the backing dictionary to avoid allocations.
|
||||||
|
/// </summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_data.Clear();
|
_data.Clear();
|
||||||
|
|||||||
27
Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs
Normal file
27
Ghost.RenderGraph.Concept/RenderGraphBuilderExtensions.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods to provide a cleaner API for setting render functions.
|
||||||
|
/// These avoid the need for explicit type annotations in lambdas.
|
||||||
|
/// </summary>
|
||||||
|
public static class RenderGraphBuilderExtensions
|
||||||
|
{
|
||||||
|
// Internal helper to cast and set
|
||||||
|
private static void SetRasterFunc<TPassData>(this RenderGraphBuilder builder, object pass, Action<TPassData, RasterRenderContext> func)
|
||||||
|
where TPassData : class, new()
|
||||||
|
{
|
||||||
|
if (pass is RenderGraphPass<TPassData> typedPass)
|
||||||
|
{
|
||||||
|
builder.SetRenderFunc(func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetCompFunc<TPassData>(this RenderGraphBuilder builder, object pass, Action<TPassData, ComputeRenderContext> func, bool async)
|
||||||
|
where TPassData : class, new()
|
||||||
|
{
|
||||||
|
if (pass is RenderGraphPass<TPassData> typedPass)
|
||||||
|
{
|
||||||
|
builder.SetComputeFunc(func, async);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs
Normal file
131
Ghost.RenderGraph.Concept/RenderGraphCompilationCache.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents cached compilation results for a render graph.
|
||||||
|
/// This avoids recompiling the graph when the structure hasn't changed.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CachedCompilation
|
||||||
|
{
|
||||||
|
// Compiled pass indices (indices into the _passes list)
|
||||||
|
public readonly List<int> CompiledPassIndices = new(64);
|
||||||
|
|
||||||
|
// Culling decisions for each pass
|
||||||
|
public readonly List<bool> PassCulledFlags = new(64);
|
||||||
|
|
||||||
|
// Physical resource aliasing mappings (logical index -> physical index)
|
||||||
|
public readonly Dictionary<int, int> LogicalToPhysical = new(128);
|
||||||
|
|
||||||
|
// Physical resource metadata
|
||||||
|
public readonly List<PhysicalResourceData> PhysicalResources = new(32);
|
||||||
|
|
||||||
|
// Resource barriers
|
||||||
|
public readonly List<ResourceBarrier> Barriers = new(128);
|
||||||
|
|
||||||
|
// Resource state mappings (for barrier generation)
|
||||||
|
public readonly Dictionary<int, ResourceState> ResourceStates = new(128);
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
CompiledPassIndices.Clear();
|
||||||
|
PassCulledFlags.Clear();
|
||||||
|
LogicalToPhysical.Clear();
|
||||||
|
PhysicalResources.Clear();
|
||||||
|
Barriers.Clear();
|
||||||
|
ResourceStates.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Physical resource data for caching.
|
||||||
|
/// </summary>
|
||||||
|
internal struct PhysicalResourceData
|
||||||
|
{
|
||||||
|
public int Index;
|
||||||
|
public int Width;
|
||||||
|
public int Height;
|
||||||
|
public TextureFormat Format;
|
||||||
|
public int FirstUsePass;
|
||||||
|
public int LastUsePass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages compilation caching for render graphs.
|
||||||
|
/// Stores compiled results and allows cache hits when graph structure is unchanged.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderGraphCompilationCache
|
||||||
|
{
|
||||||
|
private ulong _cachedHash;
|
||||||
|
private readonly CachedCompilation _cached = new();
|
||||||
|
private bool _hasCachedData;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
public int CacheHits { get; private set; }
|
||||||
|
public int CacheMisses { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve cached compilation results.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetCached(ulong hash, [MaybeNullWhen(false)] out CachedCompilation result)
|
||||||
|
{
|
||||||
|
if (_hasCachedData && _cachedHash == hash)
|
||||||
|
{
|
||||||
|
result = _cached;
|
||||||
|
CacheHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = null;
|
||||||
|
CacheMisses++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores compilation results in the cache.
|
||||||
|
/// </summary>
|
||||||
|
public void Store(ulong hash, CachedCompilation data)
|
||||||
|
{
|
||||||
|
_cachedHash = hash;
|
||||||
|
_hasCachedData = true;
|
||||||
|
|
||||||
|
// Deep copy the data
|
||||||
|
_cached.Clear();
|
||||||
|
|
||||||
|
_cached.CompiledPassIndices.AddRange(data.CompiledPassIndices);
|
||||||
|
_cached.PassCulledFlags.AddRange(data.PassCulledFlags);
|
||||||
|
|
||||||
|
foreach (var kvp in data.LogicalToPhysical)
|
||||||
|
{
|
||||||
|
_cached.LogicalToPhysical[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cached.PhysicalResources.AddRange(data.PhysicalResources);
|
||||||
|
_cached.Barriers.AddRange(data.Barriers);
|
||||||
|
|
||||||
|
foreach (var kvp in data.ResourceStates)
|
||||||
|
{
|
||||||
|
_cached.ResourceStates[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidates the cache, forcing recompilation on next Compile().
|
||||||
|
/// </summary>
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
_hasCachedData = false;
|
||||||
|
_cachedHash = 0;
|
||||||
|
_cached.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets cache statistics for debugging.
|
||||||
|
/// </summary>
|
||||||
|
public (int hits, int misses, double hitRate) GetStatistics()
|
||||||
|
{
|
||||||
|
int total = CacheHits + CacheMisses;
|
||||||
|
double hitRate = total > 0 ? (double)CacheHits / total : 0.0;
|
||||||
|
return (CacheHits, CacheMisses, hitRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Ghost.RenderGraph.Concept/RenderGraphContext.cs
Normal file
134
Ghost.RenderGraph.Concept/RenderGraphContext.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mock command buffer for recording GPU commands.
|
||||||
|
/// In a real implementation, this would wrap D3D12 command lists.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MockCommandBuffer
|
||||||
|
{
|
||||||
|
public void SetRenderTarget(string name)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(SetRenderTarget) + ": " + name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDepthStencil(string name)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(SetDepthStencil) + ": " + name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearRenderTarget(string name, float r, float g, float b, float a)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(ClearRenderTarget) + ": " + name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearDepth(string name, float depth)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(ClearDepth) + ": " + name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(int vertexCount)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(Draw) + ": " + vertexCount);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindShaderResource(string name, int slot)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(BindShaderResource) + ": " + name + ", slot " + slot);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindUnorderedAccess(string name, int slot)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(BindUnorderedAccess) + ": " + name + ", slot " + slot);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispatch(int x, int y, int z)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(Dispatch) + ": " + x + ", " + y + ", " + z);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResourceBarrier(string resourceName, string stateBefore, string stateAfter)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(ResourceBarrier) + ": " + resourceName + " from " + stateBefore + " to " + stateAfter);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AliasBarrier(string resourceBefore, string resourceAfter)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Console.WriteLine(nameof(AliasBarrier) + ": " + resourceBefore + " to " + resourceAfter);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context for raster rendering passes.
|
||||||
|
/// Directly exposes command buffer methods.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct RasterRenderContext
|
||||||
|
{
|
||||||
|
private readonly MockCommandBuffer _cmd;
|
||||||
|
|
||||||
|
public RasterRenderContext(MockCommandBuffer cmd)
|
||||||
|
{
|
||||||
|
_cmd = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose command buffer methods directly
|
||||||
|
public void SetRenderTarget(string name) => _cmd.SetRenderTarget(name);
|
||||||
|
public void SetDepthStencil(string name) => _cmd.SetDepthStencil(name);
|
||||||
|
public void ClearRenderTarget(string name, float r, float g, float b, float a) => _cmd.ClearRenderTarget(name, r, g, b, a);
|
||||||
|
public void ClearDepth(string name, float depth) => _cmd.ClearDepth(name, depth);
|
||||||
|
public void Draw(int vertexCount) => _cmd.Draw(vertexCount);
|
||||||
|
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context for compute rendering passes.
|
||||||
|
/// Directly exposes command buffer methods.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct ComputeRenderContext
|
||||||
|
{
|
||||||
|
private readonly MockCommandBuffer _cmd;
|
||||||
|
|
||||||
|
public ComputeRenderContext(MockCommandBuffer cmd)
|
||||||
|
{
|
||||||
|
_cmd = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose command buffer methods directly
|
||||||
|
public void BindShaderResource(string name, int slot) => _cmd.BindShaderResource(name, slot);
|
||||||
|
public void BindUnorderedAccess(string name, int slot) => _cmd.BindUnorderedAccess(name, slot);
|
||||||
|
public void Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unified render context containing both raster and compute contexts.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly struct RenderContext
|
||||||
|
{
|
||||||
|
public readonly RasterRenderContext RasterContext;
|
||||||
|
public readonly ComputeRenderContext ComputeContext;
|
||||||
|
|
||||||
|
public RenderContext(MockCommandBuffer cmd)
|
||||||
|
{
|
||||||
|
RasterContext = new RasterRenderContext(cmd);
|
||||||
|
ComputeContext = new ComputeRenderContext(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
public static class RenderGraphExtensions
|
|
||||||
{
|
|
||||||
// Cannot use RenderGraphPassBuilder in Action<> because it is a ref struct
|
|
||||||
// public static RenderGraphPassBuilder<TPassData> AddRenderPass<TPassData>(
|
|
||||||
// this RenderGraph renderGraph,
|
|
||||||
// string name,
|
|
||||||
// out TPassData passData,
|
|
||||||
// Action<RenderGraphPassBuilder<TPassData>> setup)
|
|
||||||
// where TPassData : class, new()
|
|
||||||
// {
|
|
||||||
// var builder = renderGraph.AddRenderPass<TPassData>(name, out passData);
|
|
||||||
// setup(builder);
|
|
||||||
// builder.Dispose();
|
|
||||||
// return builder;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RenderGraphPassScope<TPassData> : IDisposable
|
|
||||||
where TPassData : class, new()
|
|
||||||
{
|
|
||||||
// Cannot hold ref struct in class
|
|
||||||
// private readonly RenderGraphPassBuilder<TPassData> _builder;
|
|
||||||
private readonly string _passName;
|
|
||||||
|
|
||||||
// internal RenderGraphPassScope(RenderGraphPassBuilder<TPassData> builder, string passName)
|
|
||||||
// {
|
|
||||||
// _builder = builder;
|
|
||||||
// _passName = passName;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public RenderGraphPassBuilder<TPassData> Builder => _builder;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Commit the pass when the using block ends
|
|
||||||
// if (_builder.RenderFunc != null)
|
|
||||||
// {
|
|
||||||
// _builder.Dispose();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
68
Ghost.RenderGraph.Concept/RenderGraphHash.cs
Normal file
68
Ghost.RenderGraph.Concept/RenderGraphHash.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.IO.Hashing;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper extensions for XxHash3 to hash common types without string allocation.
|
||||||
|
/// Uses SIMD-optimized hashing via System.IO.Hashing.XxHash3.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RenderGraphHashExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Appends an int to the hash.
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendInt(this XxHash64 hash, int value)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<int> span = stackalloc int[1] { value };
|
||||||
|
hash.Append(MemoryMarshal.AsBytes(span));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a bool to the hash.
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendBool(this XxHash64 hash, bool value)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<bool> span = stackalloc bool[1] { value };
|
||||||
|
hash.Append(MemoryMarshal.AsBytes(span));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends an enum to the hash.
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendEnum<TEnum>(this XxHash64 hash, TEnum value) where TEnum : unmanaged, Enum
|
||||||
|
{
|
||||||
|
ReadOnlySpan<TEnum> span = stackalloc TEnum[1] { value };
|
||||||
|
hash.Append(MemoryMarshal.AsBytes(span));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a struct to the hash (must be unmanaged).
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendStruct<T>(this XxHash64 hash, in T value) where T : unmanaged
|
||||||
|
{
|
||||||
|
ReadOnlySpan<T> span = stackalloc T[1] { value };
|
||||||
|
hash.Append(MemoryMarshal.AsBytes(span));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a list of resource handle indices to the hash.
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendHandleList(this XxHash64 hash, List<RenderGraphTextureHandle> handles)
|
||||||
|
{
|
||||||
|
// Only hash the indices, not the versions (versions change but structure doesn't)
|
||||||
|
int count = handles.Count;
|
||||||
|
hash.AppendInt(count);
|
||||||
|
|
||||||
|
//for (int i = 0; i < count; i++)
|
||||||
|
//{
|
||||||
|
// hash.AppendInt(handles[i].Index);
|
||||||
|
//}
|
||||||
|
Span<int> indices = stackalloc int[count];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
indices[i] = handles[i].Index;
|
||||||
|
}
|
||||||
|
hash.Append(MemoryMarshal.AsBytes(indices));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,111 +1,215 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
public enum RenderQueueType
|
/// <summary>
|
||||||
|
/// Represents different types of render passes.
|
||||||
|
/// </summary>
|
||||||
|
public enum RenderPassType : byte
|
||||||
{
|
{
|
||||||
Graphics,
|
Raster,
|
||||||
Compute,
|
Compute
|
||||||
AsyncCompute,
|
|
||||||
Copy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class RenderGraphPass
|
/// <summary>
|
||||||
|
/// Base class for render passes.
|
||||||
|
/// Uses pooling to avoid allocations after the first frame.
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class RenderGraphPassBase
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name = string.Empty;
|
||||||
public int Index { get; set; }
|
public int Index;
|
||||||
public RenderQueueType QueueType { get; set; }
|
public RenderPassType Type;
|
||||||
public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; set; }
|
public bool AllowCulling = true;
|
||||||
public List<int> Dependencies { get; } = new();
|
public bool AsyncCompute;
|
||||||
public int RefCount { get; set; } = 0;
|
|
||||||
public bool AllowCulling { get; set; }
|
// Resource dependencies
|
||||||
|
public readonly List<RenderGraphTextureHandle> TextureReads = new(8);
|
||||||
|
public readonly List<RenderGraphTextureHandle> TextureWrites = new(4);
|
||||||
|
public readonly List<RenderGraphTextureHandle> TextureCreates = new(4);
|
||||||
|
|
||||||
|
// Execution state
|
||||||
|
public bool Culled;
|
||||||
|
public bool HasSideEffects;
|
||||||
|
|
||||||
protected RenderGraphPass(
|
public abstract void Execute(RenderContext context);
|
||||||
string name,
|
public abstract void Clear();
|
||||||
int index,
|
|
||||||
RenderQueueType queueType,
|
public virtual void Reset()
|
||||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
|
||||||
bool allowCulling)
|
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = string.Empty;
|
||||||
Index = index;
|
Index = -1;
|
||||||
QueueType = queueType;
|
Type = RenderPassType.Raster;
|
||||||
ResourceAccesses = resourceAccesses;
|
AllowCulling = true;
|
||||||
AllowCulling = allowCulling;
|
AsyncCompute = false;
|
||||||
|
TextureReads.Clear();
|
||||||
|
TextureWrites.Clear();
|
||||||
|
TextureCreates.Clear();
|
||||||
|
Culled = false;
|
||||||
|
HasSideEffects = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void InitializeBase(
|
|
||||||
string name,
|
|
||||||
int index,
|
|
||||||
RenderQueueType queueType,
|
|
||||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
|
||||||
bool allowCulling)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
Index = index;
|
|
||||||
QueueType = queueType;
|
|
||||||
ResourceAccesses = resourceAccesses;
|
|
||||||
AllowCulling = allowCulling;
|
|
||||||
Dependencies.Clear();
|
|
||||||
RefCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Execute(ICommandBuffer commandBuffer);
|
|
||||||
public abstract void Release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class RenderGraphPassPool<TPassData>
|
/// <summary>
|
||||||
where TPassData : class
|
/// Typed render pass with user data.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderGraphPass<TPassData> : RenderGraphPassBase
|
||||||
|
where TPassData : class, new()
|
||||||
{
|
{
|
||||||
public static readonly Stack<RenderGraphPass<TPassData>> Pool = new();
|
public TPassData? PassData;
|
||||||
|
public Action<TPassData, RasterRenderContext>? RasterRenderFunc;
|
||||||
|
public Action<TPassData, ComputeRenderContext>? ComputeRenderFunc;
|
||||||
|
|
||||||
|
public override void Execute(RenderContext context)
|
||||||
|
{
|
||||||
|
if (PassData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Type == RenderPassType.Raster && RasterRenderFunc != null)
|
||||||
|
{
|
||||||
|
RasterRenderFunc(PassData, context.RasterContext);
|
||||||
|
}
|
||||||
|
else if (Type == RenderPassType.Compute && ComputeRenderFunc != null)
|
||||||
|
{
|
||||||
|
ComputeRenderFunc(PassData, context.ComputeContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Clear()
|
||||||
|
{
|
||||||
|
PassData = null;
|
||||||
|
RasterRenderFunc = null;
|
||||||
|
ComputeRenderFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
base.Reset();
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class RenderGraphPass<TPassData> : RenderGraphPass
|
/// <summary>
|
||||||
where TPassData : class
|
/// Builder for constructing render passes.
|
||||||
|
/// Implements IDisposable for using() pattern.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RenderGraphBuilder : IDisposable
|
||||||
{
|
{
|
||||||
public TPassData PassData { get; private set; }
|
private RenderGraphPassBase? _pass;
|
||||||
public Action<TPassData, ICommandBuffer> RenderFunc { get; private set; }
|
private RenderGraphResourceRegistry? _resources;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public RenderGraphPass(
|
internal void Initialize(RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
|
||||||
string name,
|
|
||||||
int index,
|
|
||||||
RenderQueueType queueType,
|
|
||||||
TPassData passData,
|
|
||||||
Action<TPassData, ICommandBuffer> renderFunc,
|
|
||||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
|
||||||
bool allowCulling)
|
|
||||||
: base(name, index, queueType, resourceAccesses, allowCulling)
|
|
||||||
{
|
{
|
||||||
PassData = passData;
|
_pass = pass;
|
||||||
RenderFunc = renderFunc;
|
_resources = resources;
|
||||||
|
_disposed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize(
|
/// <summary>
|
||||||
string name,
|
/// Creates a new transient texture that only lives for this pass.
|
||||||
int index,
|
/// </summary>
|
||||||
RenderQueueType queueType,
|
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
|
||||||
TPassData passData,
|
|
||||||
Action<TPassData, ICommandBuffer> renderFunc,
|
|
||||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
|
||||||
bool allowCulling)
|
|
||||||
{
|
{
|
||||||
InitializeBase(name, index, queueType, resourceAccesses, allowCulling);
|
ThrowIfDisposed();
|
||||||
PassData = passData;
|
var handle = _resources!.CreateTexture(descriptor);
|
||||||
RenderFunc = renderFunc;
|
_pass!.TextureCreates.Add(handle);
|
||||||
|
_resources.SetProducer(handle, _pass.Index);
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Execute(ICommandBuffer commandBuffer)
|
/// <summary>
|
||||||
|
/// Marks a texture as being read by this pass.
|
||||||
|
/// </summary>
|
||||||
|
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
|
||||||
{
|
{
|
||||||
RenderFunc(PassData, commandBuffer);
|
ThrowIfDisposed();
|
||||||
|
_pass!.TextureReads.Add(handle);
|
||||||
|
_resources!.AddConsumer(handle, _pass.Index);
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Release()
|
/// <summary>
|
||||||
|
/// Marks a texture as being written by this pass.
|
||||||
|
/// </summary>
|
||||||
|
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
|
||||||
{
|
{
|
||||||
PassData = null!;
|
ThrowIfDisposed();
|
||||||
RenderFunc = null!;
|
_pass!.TextureWrites.Add(handle);
|
||||||
// ResourceAccesses list ownership is transferred back to RenderGraph
|
_resources!.SetProducer(handle, _pass.Index);
|
||||||
ResourceAccesses = null!;
|
return handle;
|
||||||
RenderGraphPassPool<TPassData>.Pool.Push(this);
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up a depth buffer for this pass.
|
||||||
|
/// </summary>
|
||||||
|
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (writeAccess)
|
||||||
|
{
|
||||||
|
_pass!.TextureWrites.Add(handle);
|
||||||
|
_resources!.SetProducer(handle, _pass.Index);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pass!.TextureReads.Add(handle);
|
||||||
|
_resources!.AddConsumer(handle, _pass.Index);
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the render function for a raster pass.
|
||||||
|
/// </summary>
|
||||||
|
public void SetRenderFunc<TPassData>(Action<TPassData, RasterRenderContext> renderFunc)
|
||||||
|
where TPassData : class, new()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (_pass is RenderGraphPass<TPassData> typedPass)
|
||||||
|
{
|
||||||
|
typedPass.RasterRenderFunc = renderFunc;
|
||||||
|
typedPass.Type = RenderPassType.Raster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the compute function for a compute pass.
|
||||||
|
/// </summary>
|
||||||
|
public void SetComputeFunc<TPassData>(Action<TPassData, ComputeRenderContext> computeFunc, bool asyncCompute = false)
|
||||||
|
where TPassData : class, new()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (_pass is RenderGraphPass<TPassData> typedPass)
|
||||||
|
{
|
||||||
|
typedPass.ComputeRenderFunc = computeFunc;
|
||||||
|
typedPass.Type = RenderPassType.Compute;
|
||||||
|
typedPass.AsyncCompute = asyncCompute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether this pass can be culled if its outputs are unused.
|
||||||
|
/// </summary>
|
||||||
|
public void SetAllowCulling(bool allow)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_pass!.AllowCulling = allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
_pass = null;
|
||||||
|
_resources = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed || _pass == null)
|
||||||
|
throw new ObjectDisposedException(nameof(RenderGraphBuilder));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
public interface IRenderGraphBuilder
|
|
||||||
{
|
|
||||||
RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle);
|
|
||||||
RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle);
|
|
||||||
RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess);
|
|
||||||
RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor);
|
|
||||||
RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle);
|
|
||||||
RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle);
|
|
||||||
RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ref struct RenderGraphPassBuilder<TPassData>
|
|
||||||
where TPassData : class, new()
|
|
||||||
{
|
|
||||||
private readonly RenderGraph _graph;
|
|
||||||
private readonly string _passName;
|
|
||||||
private readonly int _passIndex;
|
|
||||||
private RenderQueueType _queueType;
|
|
||||||
private readonly List<(RenderGraphResourceHandle handle, ResourceState state)> _resourceAccesses;
|
|
||||||
private Action<TPassData, ICommandBuffer>? _renderFunc;
|
|
||||||
private bool _committed;
|
|
||||||
private bool _allowCulling;
|
|
||||||
|
|
||||||
public TPassData PassData { get; }
|
|
||||||
|
|
||||||
internal RenderGraphPassBuilder(RenderGraph graph, string passName, int passIndex, List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses)
|
|
||||||
{
|
|
||||||
_graph = graph;
|
|
||||||
_passName = passName;
|
|
||||||
_passIndex = passIndex;
|
|
||||||
PassData = new TPassData();
|
|
||||||
_resourceAccesses = resourceAccesses;
|
|
||||||
_queueType = RenderQueueType.Graphics;
|
|
||||||
_allowCulling = true;
|
|
||||||
_committed = false;
|
|
||||||
_renderFunc = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal IReadOnlyList<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses => _resourceAccesses;
|
|
||||||
internal RenderQueueType QueueType => _queueType;
|
|
||||||
internal Action<TPassData, ICommandBuffer>? RenderFunc => _renderFunc;
|
|
||||||
internal bool AllowCulling => _allowCulling;
|
|
||||||
|
|
||||||
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
|
|
||||||
{
|
|
||||||
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
|
|
||||||
{
|
|
||||||
_resourceAccesses.Add((handle._handle, ResourceState.RenderTarget));
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
|
|
||||||
{
|
|
||||||
_resourceAccesses.Add((handle._handle, writeAccess ? ResourceState.DepthWrite : ResourceState.DepthRead));
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
|
|
||||||
{
|
|
||||||
var handle = _graph.CreateTransientTexture(descriptor);
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle)
|
|
||||||
{
|
|
||||||
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle)
|
|
||||||
{
|
|
||||||
_resourceAccesses.Add((handle._handle, ResourceState.UnorderedAccess));
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor)
|
|
||||||
{
|
|
||||||
var handle = _graph.CreateTransientBuffer(descriptor);
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetRenderFunc(Action<TPassData, RasterRenderContext> renderFunc)
|
|
||||||
{
|
|
||||||
_queueType = RenderQueueType.Graphics;
|
|
||||||
_renderFunc = (data, cmd) => renderFunc(data, new RasterRenderContext(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetComputeFunc(Action<TPassData, ComputeRenderContext> computeFunc, bool asyncCompute = false)
|
|
||||||
{
|
|
||||||
_queueType = asyncCompute ? RenderQueueType.AsyncCompute : RenderQueueType.Compute;
|
|
||||||
_renderFunc = (data, cmd) => computeFunc(data, new ComputeRenderContext(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controls whether this pass can be culled if it doesn't contribute to the final output.
|
|
||||||
/// Set to false for synchronization passes, debug markers, or async compute boundaries.
|
|
||||||
/// Default is true.
|
|
||||||
/// </summary>
|
|
||||||
public void SetAllowCulling(bool allowCulling)
|
|
||||||
{
|
|
||||||
_allowCulling = allowCulling;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Commit the pass when disposed (at end of using block)
|
|
||||||
if (!_committed)
|
|
||||||
{
|
|
||||||
_graph.CommitPass(this, _passName);
|
|
||||||
_committed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
173
Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs
Normal file
173
Ghost.RenderGraph.Concept/RenderGraphResourcePool.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Object pool for reusing allocated objects across frames.
|
||||||
|
/// This is key to minimizing GC allocations after the first frame.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderGraphObjectPool
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Type, Stack<object>> _pools = new();
|
||||||
|
|
||||||
|
public T Get<T>() where T : class, new()
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
if (_pools.TryGetValue(type, out var pool) && pool.Count > 0)
|
||||||
|
{
|
||||||
|
return (T)pool.Pop();
|
||||||
|
}
|
||||||
|
return new T();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release<T>(T obj) where T : class
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
|
||||||
|
var type = typeof(T);
|
||||||
|
if (!_pools.TryGetValue(type, out var pool))
|
||||||
|
{
|
||||||
|
pool = new Stack<object>(16);
|
||||||
|
_pools[type] = pool;
|
||||||
|
}
|
||||||
|
pool.Push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_pools.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a texture resource in the render graph.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TextureResource
|
||||||
|
{
|
||||||
|
public int Index;
|
||||||
|
public int Version;
|
||||||
|
public TextureDescriptor Descriptor;
|
||||||
|
public bool IsImported;
|
||||||
|
public int FirstUsePass = -1;
|
||||||
|
public int LastUsePass = -1;
|
||||||
|
public int ProducerPass = -1;
|
||||||
|
public List<int> ConsumerPasses = new(4);
|
||||||
|
public int RefCount;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Index = -1;
|
||||||
|
Version = 0;
|
||||||
|
Descriptor = default;
|
||||||
|
IsImported = false;
|
||||||
|
FirstUsePass = -1;
|
||||||
|
LastUsePass = -1;
|
||||||
|
ProducerPass = -1;
|
||||||
|
ConsumerPasses.Clear();
|
||||||
|
RefCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry for managing all resources in the render graph.
|
||||||
|
/// Uses pooling to minimize allocations after the first frame.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderGraphResourceRegistry
|
||||||
|
{
|
||||||
|
private readonly List<TextureResource> _textureResources = new(64);
|
||||||
|
private readonly RenderGraphObjectPool _pool = new();
|
||||||
|
private int _textureResourceCount;
|
||||||
|
|
||||||
|
public int TextureResourceCount => _textureResourceCount;
|
||||||
|
|
||||||
|
public void BeginFrame()
|
||||||
|
{
|
||||||
|
// Don't clear the lists, just reset the count
|
||||||
|
// This avoids reallocating the backing arrays
|
||||||
|
_textureResourceCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderGraphTextureHandle ImportTexture(TextureDescriptor descriptor)
|
||||||
|
{
|
||||||
|
var resource = GetOrCreateTextureResource();
|
||||||
|
resource.Index = _textureResourceCount - 1;
|
||||||
|
resource.Version = 0;
|
||||||
|
resource.Descriptor = descriptor;
|
||||||
|
resource.IsImported = true;
|
||||||
|
|
||||||
|
return new RenderGraphTextureHandle(resource.Index, resource.Version, descriptor.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
|
||||||
|
{
|
||||||
|
var resource = GetOrCreateTextureResource();
|
||||||
|
resource.Index = _textureResourceCount - 1;
|
||||||
|
resource.Version = 0;
|
||||||
|
resource.Descriptor = descriptor;
|
||||||
|
resource.IsImported = false;
|
||||||
|
|
||||||
|
return new RenderGraphTextureHandle(resource.Index, resource.Version, descriptor.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureResource GetTextureResource(RenderGraphTextureHandle handle)
|
||||||
|
{
|
||||||
|
if (handle.Index < 0 || handle.Index >= _textureResourceCount)
|
||||||
|
throw new ArgumentException($"Invalid texture handle: {handle.Index}");
|
||||||
|
|
||||||
|
return _textureResources[handle.Index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureResource GetTextureResourceByIndex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _textureResourceCount)
|
||||||
|
throw new ArgumentException($"Invalid texture index: {index}");
|
||||||
|
|
||||||
|
return _textureResources[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProducer(RenderGraphTextureHandle handle, int passIndex)
|
||||||
|
{
|
||||||
|
var resource = GetTextureResource(handle);
|
||||||
|
resource.ProducerPass = passIndex;
|
||||||
|
if (resource.FirstUsePass < 0)
|
||||||
|
resource.FirstUsePass = passIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddConsumer(RenderGraphTextureHandle handle, int passIndex)
|
||||||
|
{
|
||||||
|
var resource = GetTextureResource(handle);
|
||||||
|
resource.ConsumerPasses.Add(passIndex);
|
||||||
|
resource.LastUsePass = passIndex;
|
||||||
|
if (resource.FirstUsePass < 0)
|
||||||
|
resource.FirstUsePass = passIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextureResource GetOrCreateTextureResource()
|
||||||
|
{
|
||||||
|
TextureResource resource;
|
||||||
|
if (_textureResourceCount < _textureResources.Count)
|
||||||
|
{
|
||||||
|
// Reuse existing slot
|
||||||
|
resource = _textureResources[_textureResourceCount];
|
||||||
|
resource.Reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Need to grow the list
|
||||||
|
resource = _pool.Get<TextureResource>();
|
||||||
|
resource.Reset();
|
||||||
|
_textureResources.Add(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
_textureResourceCount++;
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _textureResources.Count; i++)
|
||||||
|
{
|
||||||
|
_pool.Release(_textureResources[i]);
|
||||||
|
}
|
||||||
|
_textureResources.Clear();
|
||||||
|
_textureResourceCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
Ghost.RenderGraph.Concept/RenderGraphTypes.cs
Normal file
75
Ghost.RenderGraph.Concept/RenderGraphTypes.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
namespace Ghost.RenderGraph.Concept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opaque handle to a render graph texture resource.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct RenderGraphTextureHandle : IEquatable<RenderGraphTextureHandle>
|
||||||
|
{
|
||||||
|
public readonly int Index;
|
||||||
|
public readonly int Version;
|
||||||
|
internal readonly string InternalName;
|
||||||
|
|
||||||
|
public RenderGraphTextureHandle(int index, int version, string name = "")
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
Version = version;
|
||||||
|
InternalName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => InternalName;
|
||||||
|
|
||||||
|
public bool IsValid() => Index >= 0;
|
||||||
|
|
||||||
|
public readonly bool Equals(RenderGraphTextureHandle other) => Index == other.Index && Version == other.Version;
|
||||||
|
public override readonly bool Equals(object? obj) => obj is RenderGraphTextureHandle other && Equals(other);
|
||||||
|
public override readonly int GetHashCode() => HashCode.Combine(Index, Version);
|
||||||
|
public static bool operator ==(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => left.Equals(right);
|
||||||
|
public static bool operator !=(RenderGraphTextureHandle left, RenderGraphTextureHandle right) => !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture formats supported by the render graph.
|
||||||
|
/// </summary>
|
||||||
|
public enum TextureFormat : int
|
||||||
|
{
|
||||||
|
RGBA8,
|
||||||
|
RGBA16F,
|
||||||
|
RGBA32F,
|
||||||
|
Depth32F,
|
||||||
|
Depth24Stencil8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Descriptor for creating a texture resource.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct TextureDescriptor : IEquatable<TextureDescriptor>
|
||||||
|
{
|
||||||
|
public readonly int Width;
|
||||||
|
public readonly int Height;
|
||||||
|
public readonly TextureFormat Format;
|
||||||
|
public readonly string Name;
|
||||||
|
|
||||||
|
public TextureDescriptor(int width, int height, TextureFormat format, string name)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
Format = format;
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Equals(TextureDescriptor other) =>
|
||||||
|
Width == other.Width &&
|
||||||
|
Height == other.Height &&
|
||||||
|
Format == other.Format &&
|
||||||
|
Name == other.Name;
|
||||||
|
|
||||||
|
public override readonly bool Equals(object? obj) => obj is TextureDescriptor other && Equals(other);
|
||||||
|
public override readonly int GetHashCode() => HashCode.Combine(Width, Height, Format, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base interface for pass data that can be stored in the blackboard.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPassData
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
using Ghost.Core.Utilities;
|
|
||||||
using ZLinq;
|
|
||||||
|
|
||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a physical memory allocation that can be shared by multiple transient resources
|
|
||||||
/// </summary>
|
|
||||||
internal struct PhysicalResourceAllocation
|
|
||||||
{
|
|
||||||
public int AllocationId { get; }
|
|
||||||
public ulong SizeInBytes { get; }
|
|
||||||
public ulong OffsetInBytes { get; }
|
|
||||||
public string DebugName { get; }
|
|
||||||
public List<RenderGraphResourceHandle> AliasedResources { get; } = new();
|
|
||||||
|
|
||||||
public PhysicalResourceAllocation(int allocationId, ulong sizeInBytes, ulong offsetInBytes, string debugName)
|
|
||||||
{
|
|
||||||
AllocationId = allocationId;
|
|
||||||
SizeInBytes = sizeInBytes;
|
|
||||||
OffsetInBytes = offsetInBytes;
|
|
||||||
DebugName = debugName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages memory allocation and aliasing for transient resources
|
|
||||||
/// </summary>
|
|
||||||
internal class ResourceAllocator
|
|
||||||
{
|
|
||||||
private readonly List<PhysicalResourceAllocation> _allocations = new();
|
|
||||||
private int _allocationIdCounter = 0;
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
_allocations.Clear();
|
|
||||||
_allocationIdCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<PhysicalResourceAllocation> Allocations => _allocations;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allocate physical memory for resources, enabling aliasing where possible
|
|
||||||
/// </summary>
|
|
||||||
public void AllocateResources(
|
|
||||||
IReadOnlyList<ResourceLifetime> resourceLifetimes,
|
|
||||||
List<RenderGraphPass> passes)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS =====");
|
|
||||||
|
|
||||||
// Separate imported and transient resources
|
|
||||||
// Sort by SIZE FIRST (descending), then by FIRST USE (ascending)
|
|
||||||
// This allows smaller resources (A, B) to alias into larger resources' (C) space
|
|
||||||
// Example: C=10MB[1..2], A=4MB[0..1], B=6MB[0..1] → Allocate C first, then A and B alias into C's space
|
|
||||||
|
|
||||||
// TODO: Avoid linq for performance-critical path
|
|
||||||
var transientResources = resourceLifetimes.AsValueEnumerable()
|
|
||||||
.Where(lt => !lt.Handle.IsImported && lt.FirstUse != int.MaxValue)
|
|
||||||
.OrderByDescending(lt => GetResourceSize(lt.Handle))
|
|
||||||
.ThenBy(lt => lt.FirstUse).ToArray();
|
|
||||||
|
|
||||||
if (transientResources.Length == 0)
|
|
||||||
{
|
|
||||||
//ConsoleAPI.WriteLine("No transient resources to allocate.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track which allocation slots are occupied at each pass
|
|
||||||
var allocationSlots = Core.Utilities.ListPool<AllocationSlot>.Rent();
|
|
||||||
|
|
||||||
foreach (var resource in transientResources)
|
|
||||||
{
|
|
||||||
var size = GetResourceSize(resource.Handle);
|
|
||||||
var alignment = GetResourceAlignment(resource.Handle);
|
|
||||||
|
|
||||||
// Find an existing allocation slot that:
|
|
||||||
// 1. Is large enough
|
|
||||||
// 2. Has no lifetime overlap
|
|
||||||
// 3. Matches resource type (texture/buffer)
|
|
||||||
AllocationSlot? reuseSlot = null;
|
|
||||||
foreach (var slot in allocationSlots)
|
|
||||||
{
|
|
||||||
if (CanAlias(slot, resource, size, alignment))
|
|
||||||
{
|
|
||||||
reuseSlot = slot;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reuseSlot != null)
|
|
||||||
{
|
|
||||||
// Reuse existing allocation - find offset within the allocation
|
|
||||||
ulong offsetInAllocation = reuseSlot.FindFreeOffset(size, alignment, resource);
|
|
||||||
reuseSlot.AddResource(resource, offsetInAllocation, size);
|
|
||||||
|
|
||||||
//ConsoleAPI.WriteLine($"[ALIAS] '{resource.Handle.Name}' aliases with '{reuseSlot.Allocation.DebugName}' " + $"(heap offset: {reuseSlot.Allocation.OffsetInBytes}, resource offset: {offsetInAllocation}, size: {size} bytes, " + $"lifetime: [{resource.FirstUse}..{resource.LastUse}])");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Create new allocation
|
|
||||||
// Calculate heap offset (simulated - in real D3D12MA this would be the actual heap offset)
|
|
||||||
ulong heapOffset = allocationSlots.Count > 0
|
|
||||||
? allocationSlots.Max(s => s.Allocation.OffsetInBytes + s.Allocation.SizeInBytes)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
var allocation = new PhysicalResourceAllocation(
|
|
||||||
_allocationIdCounter++,
|
|
||||||
size,
|
|
||||||
offsetInBytes: heapOffset,
|
|
||||||
$"Physical_{resource.Handle.Type}_{_allocationIdCounter}");
|
|
||||||
|
|
||||||
var newSlot = new AllocationSlot(allocation, resource.Handle.Type);
|
|
||||||
newSlot.AddResource(resource, 0, size); // Offset 0 within this new allocation
|
|
||||||
allocationSlots.Add(newSlot);
|
|
||||||
|
|
||||||
//ConsoleAPI.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " + $"(heap offset: {heapOffset}, size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var slot in allocationSlots)
|
|
||||||
{
|
|
||||||
_allocations.Add(slot.Allocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
ListPool<AllocationSlot>.Return(allocationSlots);
|
|
||||||
|
|
||||||
// Print summary
|
|
||||||
//ConsoleAPI.WriteLine($"\n[RG] Memory Statistics:");
|
|
||||||
var totalWithoutAliasing = transientResources.Sum(r => (long)GetResourceSize(r.Handle));
|
|
||||||
var totalWithAliasing = _allocations.Sum(a => (long)a.SizeInBytes);
|
|
||||||
var savedMemory = totalWithoutAliasing - totalWithAliasing;
|
|
||||||
var savingPercentage = totalWithoutAliasing > 0 ? (savedMemory * 100.0 / totalWithoutAliasing) : 0;
|
|
||||||
|
|
||||||
//ConsoleAPI.WriteLine($" Total memory without aliasing: {FormatBytes(totalWithoutAliasing)}");
|
|
||||||
//ConsoleAPI.WriteLine($" Total memory with aliasing: {FormatBytes(totalWithAliasing)}");
|
|
||||||
//ConsoleAPI.WriteLine($" Memory saved: {FormatBytes(savedMemory)} ({savingPercentage:F1}%)");
|
|
||||||
//ConsoleAPI.WriteLine($" Allocations: {_allocations.Count} physical allocations for {transientResources.Count()} resources");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanAlias(AllocationSlot slot, ResourceLifetime resource, ulong requiredSize, ulong requiredAlignment)
|
|
||||||
{
|
|
||||||
// Must be same resource type
|
|
||||||
if (slot.ResourceType != resource.Handle.Type)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Must be large enough
|
|
||||||
if (slot.Allocation.SizeInBytes < requiredSize)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check for lifetime overlap with any resource in this slot
|
|
||||||
foreach (var existingResource in slot.Resources)
|
|
||||||
{
|
|
||||||
if (LifetimesOverlap(existingResource, resource))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b)
|
|
||||||
{
|
|
||||||
// Two resources overlap if their lifetimes intersect
|
|
||||||
return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong GetResourceSize(RenderGraphResourceHandle handle)
|
|
||||||
{
|
|
||||||
return handle.Type switch
|
|
||||||
{
|
|
||||||
ResourceType.Texture => CalculateTextureSize(handle.Descriptor.texture),
|
|
||||||
ResourceType.Buffer => (ulong)handle.Descriptor.buffer.SizeInBytes,
|
|
||||||
_ => 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong GetResourceAlignment(RenderGraphResourceHandle handle)
|
|
||||||
{
|
|
||||||
// In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO
|
|
||||||
return handle.Type switch
|
|
||||||
{
|
|
||||||
ResourceType.Texture => 65536, // 64KB texture alignment (typical)
|
|
||||||
ResourceType.Buffer => 256, // 256 byte buffer alignment
|
|
||||||
_ => 256
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong CalculateTextureSize(TextureDescriptor desc)
|
|
||||||
{
|
|
||||||
// Simplified size calculation
|
|
||||||
var bytesPerPixel = desc.Format switch
|
|
||||||
{
|
|
||||||
TextureFormat.RGBA8 => 4,
|
|
||||||
TextureFormat.RGBA16F => 8,
|
|
||||||
TextureFormat.RGBA32F => 16,
|
|
||||||
TextureFormat.Depth32F => 4,
|
|
||||||
TextureFormat.R32Uint => 4,
|
|
||||||
_ => 4
|
|
||||||
};
|
|
||||||
|
|
||||||
return (ulong)(desc.Width * desc.Height * bytesPerPixel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatBytes(long bytes)
|
|
||||||
{
|
|
||||||
if (bytes < 1024)
|
|
||||||
return $"{bytes} B";
|
|
||||||
if (bytes < 1024 * 1024)
|
|
||||||
return $"{bytes / 1024.0:F2} KB";
|
|
||||||
return $"{bytes / (1024.0 * 1024.0):F2} MB";
|
|
||||||
}
|
|
||||||
|
|
||||||
public PhysicalResourceAllocation? GetAllocation(RenderGraphResourceHandle handle)
|
|
||||||
{
|
|
||||||
// return _allocations.FirstOrDefault(a => a.AliasedResources.Any(r => r.Id == handle.Id));
|
|
||||||
foreach (var allocation in _allocations)
|
|
||||||
{
|
|
||||||
foreach (var aliased in allocation.AliasedResources)
|
|
||||||
{
|
|
||||||
if (aliased.Id == handle.Id)
|
|
||||||
{
|
|
||||||
return allocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AllocationSlot
|
|
||||||
{
|
|
||||||
public PhysicalResourceAllocation Allocation { get; }
|
|
||||||
public ResourceType ResourceType { get; }
|
|
||||||
public List<ResourceLifetime> Resources { get; } = new();
|
|
||||||
|
|
||||||
// Track occupied regions within this allocation: (offset, size, lifetime)
|
|
||||||
private readonly List<(ulong Offset, ulong Size, ResourceLifetime Resource)> _occupiedRegions = new();
|
|
||||||
|
|
||||||
public AllocationSlot(PhysicalResourceAllocation allocation, ResourceType resourceType)
|
|
||||||
{
|
|
||||||
Allocation = allocation;
|
|
||||||
ResourceType = resourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Find a free offset within this allocation that can fit the required size
|
|
||||||
/// and doesn't conflict with active resources
|
|
||||||
/// </summary>
|
|
||||||
public ulong FindFreeOffset(ulong requiredSize, ulong alignment, ResourceLifetime newResource)
|
|
||||||
{
|
|
||||||
// If no resources yet, return 0
|
|
||||||
if (_occupiedRegions.Count == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort regions by offset
|
|
||||||
var sortedRegions = _occupiedRegions.AsValueEnumerable().OrderBy(r => r.Offset).ToArray();
|
|
||||||
|
|
||||||
// Try to fit at the beginning (offset 0)
|
|
||||||
ulong candidateOffset = 0;
|
|
||||||
bool fitsAtStart = true;
|
|
||||||
|
|
||||||
foreach (var (Offset, Size, Resource) in sortedRegions)
|
|
||||||
{
|
|
||||||
// Check if this region overlaps with our candidate position
|
|
||||||
if (Offset < requiredSize)
|
|
||||||
{
|
|
||||||
// Check lifetime - if no overlap, we can still use this space
|
|
||||||
if (LifetimesOverlap(Resource, newResource))
|
|
||||||
{
|
|
||||||
fitsAtStart = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fitsAtStart)
|
|
||||||
{
|
|
||||||
return AlignUp(0, alignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try gaps between regions
|
|
||||||
for (int i = 0; i < sortedRegions.Length; i++)
|
|
||||||
{
|
|
||||||
var current = sortedRegions[i];
|
|
||||||
|
|
||||||
// Skip if current region's lifetime overlaps with new resource
|
|
||||||
if (LifetimesOverlap(current.Resource, newResource))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try placing after this region
|
|
||||||
candidateOffset = AlignUp(current.Offset + current.Size, alignment);
|
|
||||||
|
|
||||||
// Check if it fits before the next region (or end of allocation)
|
|
||||||
ulong nextRegionStart = (i + 1 < sortedRegions.Length)
|
|
||||||
? sortedRegions[i + 1].Offset
|
|
||||||
: Allocation.SizeInBytes;
|
|
||||||
|
|
||||||
if (candidateOffset + requiredSize <= nextRegionStart)
|
|
||||||
{
|
|
||||||
// Check no lifetime conflicts with any regions in this range
|
|
||||||
bool hasConflict = false;
|
|
||||||
for (int j = i + 1; j < sortedRegions.Length; j++)
|
|
||||||
{
|
|
||||||
var other = sortedRegions[j];
|
|
||||||
if (other.Offset < candidateOffset + requiredSize)
|
|
||||||
{
|
|
||||||
if (LifetimesOverlap(other.Resource, newResource))
|
|
||||||
{
|
|
||||||
hasConflict = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasConflict)
|
|
||||||
{
|
|
||||||
return candidateOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try placing at the end
|
|
||||||
if (sortedRegions.Length > 0)
|
|
||||||
{
|
|
||||||
var last = sortedRegions[^1];
|
|
||||||
if (!LifetimesOverlap(last.Resource, newResource))
|
|
||||||
{
|
|
||||||
candidateOffset = AlignUp(last.Offset + last.Size, alignment);
|
|
||||||
if (candidateOffset + requiredSize <= Allocation.SizeInBytes)
|
|
||||||
{
|
|
||||||
return candidateOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No space found - caller should create new allocation
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b)
|
|
||||||
{
|
|
||||||
return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong AlignUp(ulong value, ulong alignment)
|
|
||||||
{
|
|
||||||
return (value + alignment - 1) / alignment * alignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddResource(ResourceLifetime resource, ulong offsetInAllocation, ulong size)
|
|
||||||
{
|
|
||||||
Resources.Add(resource);
|
|
||||||
_occupiedRegions.Add((offsetInAllocation, size, resource));
|
|
||||||
Allocation.AliasedResources.Add(resource.Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
namespace Ghost.RenderGraph.Concept;
|
|
||||||
|
|
||||||
internal struct ResourceUsage
|
|
||||||
{
|
|
||||||
public RenderGraphResourceHandle Handle { get; }
|
|
||||||
public ResourceState State { get; }
|
|
||||||
public int PassIndex { get; }
|
|
||||||
|
|
||||||
public ResourceUsage(RenderGraphResourceHandle handle, ResourceState state, int passIndex)
|
|
||||||
{
|
|
||||||
Handle = handle;
|
|
||||||
State = state;
|
|
||||||
PassIndex = passIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal struct ResourceLifetime
|
|
||||||
{
|
|
||||||
public RenderGraphResourceHandle Handle { get; private set; }
|
|
||||||
public int FirstUse { get; set; } = int.MaxValue;
|
|
||||||
public int LastUse { get; set; } = -1;
|
|
||||||
public List<ResourceUsage> Usages { get; } = new();
|
|
||||||
|
|
||||||
public ResourceLifetime()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize(RenderGraphResourceHandle handle)
|
|
||||||
{
|
|
||||||
Handle = handle;
|
|
||||||
FirstUse = int.MaxValue;
|
|
||||||
LastUse = -1;
|
|
||||||
Usages.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddUsage(ResourceState state, int passIndex)
|
|
||||||
{
|
|
||||||
Usages.Add(new ResourceUsage(Handle, state, passIndex));
|
|
||||||
FirstUse = Math.Min(FirstUse, passIndex);
|
|
||||||
LastUse = Math.Max(LastUse, passIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
2220
Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs
Normal file
2220
Ghost.RenderGraph.Concept/Unity/NativePassCompiler.cs
Normal file
File diff suppressed because it is too large
Load Diff
1692
Ghost.RenderGraph.Concept/Unity/RenderGraph.cs
Normal file
1692
Ghost.RenderGraph.Concept/Unity/RenderGraph.cs
Normal file
File diff suppressed because it is too large
Load Diff
598
Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs
Normal file
598
Ghost.RenderGraph.Concept/Unity/RenderGraphBuilders.cs
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
#if false
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using UnityEngine.Experimental.Rendering;
|
||||||
|
using static UnityEngine.Rendering.RenderGraphModule.RenderGraph;
|
||||||
|
|
||||||
|
namespace UnityEngine.Rendering.RenderGraphModule
|
||||||
|
{
|
||||||
|
// This is a class making it a struct wouldn't help as we pas it around as an interface which means it would be boxed/unboxed anyway
|
||||||
|
// Publicly this class has different faces to help the users with different pass types through type safety but internally
|
||||||
|
// we just have a single implementation for all builders
|
||||||
|
internal class RenderGraphBuilders : IBaseRenderGraphBuilder, IComputeRenderGraphBuilder, IRasterRenderGraphBuilder, IUnsafeRenderGraphBuilder
|
||||||
|
{
|
||||||
|
RenderGraphPass m_RenderPass;
|
||||||
|
RenderGraphResourceRegistry m_Resources;
|
||||||
|
RenderGraph m_RenderGraph;
|
||||||
|
bool m_Disposed;
|
||||||
|
|
||||||
|
public RenderGraphBuilders()
|
||||||
|
{
|
||||||
|
m_RenderPass = null;
|
||||||
|
m_Resources = null;
|
||||||
|
m_RenderGraph = null;
|
||||||
|
m_Disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Setup(RenderGraphPass renderPass, RenderGraphResourceRegistry resources, RenderGraph renderGraph)
|
||||||
|
{
|
||||||
|
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
||||||
|
// If the object is not disposed yet this is an error as the pass is not finished (only in the dispose we register it with the rendergraph)
|
||||||
|
// This is likely cause by a user not doing a clean using and then forgetting to manually dispose the object.
|
||||||
|
if (m_Disposed != true)
|
||||||
|
{
|
||||||
|
throw new Exception(RenderGraph.RenderGraphExceptionMessages.k_UndisposedBuilderPreviousPass);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
m_RenderPass = renderPass;
|
||||||
|
m_Resources = resources;
|
||||||
|
m_RenderGraph = renderGraph;
|
||||||
|
m_Disposed = false;
|
||||||
|
|
||||||
|
renderPass.useAllGlobalTextures = false;
|
||||||
|
|
||||||
|
if (renderPass.type == RenderGraphPassType.Raster)
|
||||||
|
{
|
||||||
|
CommandBuffer.ThrowOnSetRenderTarget = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void EnableAsyncCompute(bool value)
|
||||||
|
{
|
||||||
|
m_RenderPass.EnableAsyncCompute(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AllowPassCulling(bool value)
|
||||||
|
{
|
||||||
|
// This pass cannot be culled if it allows global state modifications
|
||||||
|
if (value && m_RenderPass.allowGlobalState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_RenderPass.AllowPassCulling(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AllowGlobalStateModification(bool value)
|
||||||
|
{
|
||||||
|
m_RenderPass.AllowGlobalState(value);
|
||||||
|
|
||||||
|
// This pass cannot be culled if it allows global state modifications
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
AllowPassCulling(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable foveated rendering for this pass.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">True to enable foveated rendering.</param>
|
||||||
|
public void EnableFoveatedRasterization(bool value)
|
||||||
|
{
|
||||||
|
m_RenderPass.EnableFoveatedRasterization(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferHandle CreateTransientBuffer(in BufferDesc desc)
|
||||||
|
{
|
||||||
|
var result = m_Resources.CreateBuffer(desc, m_RenderPass.index);
|
||||||
|
UseTransientResource(result.handle);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferHandle CreateTransientBuffer(in BufferHandle computebuffer)
|
||||||
|
{
|
||||||
|
ref readonly var desc = ref m_Resources.GetBufferResourceDesc(computebuffer.handle);
|
||||||
|
return CreateTransientBuffer(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureHandle CreateTransientTexture(in TextureDesc desc)
|
||||||
|
{
|
||||||
|
var result = m_Resources.CreateTexture(desc, m_RenderPass.index);
|
||||||
|
UseTransientResource(result.handle);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureHandle CreateTransientTexture(in TextureHandle texture)
|
||||||
|
{
|
||||||
|
ref readonly var desc = ref m_Resources.GetTextureResourceDesc(texture.handle);
|
||||||
|
return CreateTransientTexture(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GenerateDebugData(bool value)
|
||||||
|
{
|
||||||
|
m_RenderPass.GenerateDebugData(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (m_Disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
m_RenderGraph.RenderGraphState = RenderGraphState.RecordingGraph;
|
||||||
|
|
||||||
|
// Use all globals simply means this... we do a UseTexture on all globals so the pass has the correct dependencies.
|
||||||
|
// This of course goes to show how bad an idea shader-system wide globals really are dependency/lifetime tracking wise :-)
|
||||||
|
if (m_RenderPass.useAllGlobalTextures)
|
||||||
|
{
|
||||||
|
foreach (var texture in m_RenderGraph.AllGlobals())
|
||||||
|
{
|
||||||
|
if (texture.IsValid())
|
||||||
|
this.UseTexture(texture, AccessFlags.Read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set globals on the graph fronted side so subsequent passes can have pass dependencies on these global texture handles
|
||||||
|
foreach (var t in m_RenderPass.setGlobalsList)
|
||||||
|
{
|
||||||
|
m_RenderGraph.SetGlobal(t.Item1, t.Item2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (m_RenderPass.type == RenderGraphPassType.Raster)
|
||||||
|
{
|
||||||
|
CommandBuffer.ThrowOnSetRenderTarget = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_RenderPass = null;
|
||||||
|
m_Resources = null;
|
||||||
|
m_RenderGraph = null;
|
||||||
|
m_Disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
private void CheckWriteTo(in ResourceHandle handle)
|
||||||
|
{
|
||||||
|
if (RenderGraph.enableValidityChecks)
|
||||||
|
{
|
||||||
|
// Write by design generates a new version of the resource. However
|
||||||
|
// you could in theory write to v2 of the resource while there is already
|
||||||
|
// a v3 so this write would then introduce a new v4 of the resource.
|
||||||
|
// This would mean a divergence in the versioning history subsequent versions based on v2 and other subsequent versions based on v3
|
||||||
|
// this would be very confusing as they are all still refered to by the same texture just different versions
|
||||||
|
// so we decide to disallow this. It can always be (at zero cost) handled by using a "Move" pass to move the divergent version
|
||||||
|
// so it's own texture resource which again has it's own single history of versions.
|
||||||
|
if (handle.IsVersioned)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {handle.type} at index {handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_WriteToVersionedResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_RenderPass.IsWritten(handle))
|
||||||
|
{
|
||||||
|
// We bump the version and write count if you call USeResource with writing flags. So calling this several times
|
||||||
|
// would lead to the side effect of new versions for every call to UseResource leading to incorrect versions.
|
||||||
|
// In theory we could detect and ignore the second UseResource but we decided to just disallow is as it might also
|
||||||
|
// Stem from user confusion.
|
||||||
|
// It seems the most likely cause of such a situation would be something like:
|
||||||
|
// TextureHandle b = a;
|
||||||
|
// ...
|
||||||
|
// much much code in between, user lost track that a=b
|
||||||
|
// ...
|
||||||
|
// builder.WriteTexture(a)
|
||||||
|
// builder.WriteTexture(b)
|
||||||
|
// > Get this error they were probably thinking they were writing two separate outputs... but they are just two versions of resource 'a'
|
||||||
|
// where they can only differ between by careful management of versioned resources.
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {handle.type} at index {handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_WriteToResourceTwice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifetime of a transient resource is one render graph pass
|
||||||
|
private ResourceHandle UseTransientResource(in ResourceHandle inputHandle)
|
||||||
|
{
|
||||||
|
CheckResource(inputHandle);
|
||||||
|
|
||||||
|
ResourceHandle versionedHandle = inputHandle.IsVersioned ? inputHandle : m_Resources.GetLatestVersionHandle(inputHandle);
|
||||||
|
|
||||||
|
// Transient resources are always considered written and read in the render graph pass where they are used
|
||||||
|
// Compiler will take it into account later, no need to add them to read and write lists
|
||||||
|
m_RenderPass.AddTransientResource(versionedHandle);
|
||||||
|
|
||||||
|
return versionedHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceHandle UseResource(in ResourceHandle inputHandle, AccessFlags flags)
|
||||||
|
{
|
||||||
|
CheckResource(inputHandle);
|
||||||
|
|
||||||
|
bool discard = (flags & AccessFlags.Discard) != 0;
|
||||||
|
bool read = (flags & AccessFlags.Read) != 0;
|
||||||
|
bool write = (flags & AccessFlags.Write) != 0;
|
||||||
|
|
||||||
|
ResourceHandle versionedHandle = inputHandle.IsVersioned ? inputHandle : m_Resources.GetLatestVersionHandle(inputHandle);
|
||||||
|
|
||||||
|
// If we are not discarding the current version and its data, add a "read" dependency on it
|
||||||
|
// this is a bit of a misnomer it really means more like "Preserve existing content or read"
|
||||||
|
if (!discard)
|
||||||
|
{
|
||||||
|
m_Resources.IncrementReadCount(versionedHandle);
|
||||||
|
m_RenderPass.AddResourceRead(versionedHandle);
|
||||||
|
|
||||||
|
// Implicit read - not user-specified
|
||||||
|
if (!read)
|
||||||
|
{
|
||||||
|
m_RenderPass.implicitReadsList.Add(versionedHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We are discarding it but we still read it, so we add a dependency on version "0" of this resource
|
||||||
|
if (read)
|
||||||
|
{
|
||||||
|
var zeroVersionHandle = m_Resources.GetZeroVersionHandle(versionedHandle);
|
||||||
|
m_Resources.IncrementReadCount(zeroVersionHandle);
|
||||||
|
m_RenderPass.AddResourceRead(zeroVersionHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
CheckWriteTo(inputHandle);
|
||||||
|
// New versioned written by this render graph pass
|
||||||
|
versionedHandle = m_Resources.IncrementWriteCount(inputHandle);
|
||||||
|
m_RenderPass.AddResourceWrite(versionedHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionedHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferHandle UseBuffer(in BufferHandle input, AccessFlags flags)
|
||||||
|
{
|
||||||
|
UseResource(input.handle, flags);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTexture and SetRenderAttachment are currently forced to be mutually exclusive in the same pass
|
||||||
|
// check this.
|
||||||
|
// We currently ignore the version. In theory there might be some cases that are actually allowed with versioning
|
||||||
|
// for ample UseTexture(myTexV1, read) UseFragment(myTexV2, ReadWrite) as they are different versions
|
||||||
|
// but for now we don't allow any of that.
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
private void CheckNotUseFragment(in TextureHandle tex)
|
||||||
|
{
|
||||||
|
if (RenderGraph.enableValidityChecks)
|
||||||
|
{
|
||||||
|
bool usedAsFragment = (m_RenderPass.depthAccess.textureHandle.IsValid() && m_RenderPass.depthAccess.textureHandle.handle.index == tex.handle.index);
|
||||||
|
if (!usedAsFragment)
|
||||||
|
{
|
||||||
|
for (int i = 0; i <= m_RenderPass.colorBufferMaxIndex; i++)
|
||||||
|
{
|
||||||
|
if (m_RenderPass.colorBufferAccess[i].textureHandle.IsValid() && m_RenderPass.colorBufferAccess[i].textureHandle.handle.index == tex.handle.index)
|
||||||
|
{
|
||||||
|
usedAsFragment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedAsFragment)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_TextureAlreadyBeingUsedThroughSetAttachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
private void CheckTextureUVOriginIsValid(in ResourceHandle handle, TextureResource texRes)
|
||||||
|
{
|
||||||
|
if (texRes.textureUVOrigin == TextureUVOriginSelection.TopLeft)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(handle);
|
||||||
|
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type `{handle.type}` at index `{handle.index}` - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOriginUseTexture(texRes.textureUVOrigin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UseTexture(in TextureHandle input, AccessFlags flags)
|
||||||
|
{
|
||||||
|
CheckNotUseFragment(input);
|
||||||
|
UseResource(input.handle, flags);
|
||||||
|
|
||||||
|
if ((flags & AccessFlags.Read) == AccessFlags.Read)
|
||||||
|
{
|
||||||
|
if (m_RenderGraph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation)
|
||||||
|
{
|
||||||
|
TextureResource texRes = m_Resources.GetTextureResource(input.handle);
|
||||||
|
CheckTextureUVOriginIsValid(input.handle, texRes);
|
||||||
|
texRes.textureUVOrigin = TextureUVOriginSelection.BottomLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UseGlobalTexture(int propertyId, AccessFlags flags)
|
||||||
|
{
|
||||||
|
var h = m_RenderGraph.GetGlobal(propertyId);
|
||||||
|
if (h.IsValid())
|
||||||
|
{
|
||||||
|
UseTexture(h, flags);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// rose test this path
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(h.handle);
|
||||||
|
throw new ArgumentException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {h.handle.type} at index {h.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.NoGlobalTextureAtPropertyID(propertyId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UseAllGlobalTextures(bool enable)
|
||||||
|
{
|
||||||
|
m_RenderPass.useAllGlobalTextures = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGlobalTextureAfterPass(in TextureHandle input, int propertyId)
|
||||||
|
{
|
||||||
|
m_RenderPass.setGlobalsList.Add(ValueTuple.Create(input, propertyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared validation between SetRenderAttachment/SetRenderAttachmentDepth
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
private void CheckUseFragment(in TextureHandle tex, bool isDepth)
|
||||||
|
{
|
||||||
|
if (RenderGraph.enableValidityChecks)
|
||||||
|
{
|
||||||
|
// We ignore the version as we don't allow mixing UseTexture/UseFragment between different versions
|
||||||
|
// even though it should theoretically work (and we might do so in the future) for now we're overly strict.
|
||||||
|
bool alreadyUsed = false;
|
||||||
|
|
||||||
|
//TODO: Check grab textures here and allow if it's grabbed. For now
|
||||||
|
// SetRenderAttachment()
|
||||||
|
// UseTexture(grab)
|
||||||
|
// will work but not the other way around
|
||||||
|
for (int i = 0; i < m_RenderPass.resourceReadLists[tex.handle.iType].Count; i++)
|
||||||
|
{
|
||||||
|
if (m_RenderPass.resourceReadLists[tex.handle.iType][i].index == tex.handle.index)
|
||||||
|
{
|
||||||
|
alreadyUsed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_RenderPass.resourceWriteLists[tex.handle.iType].Count; i++)
|
||||||
|
{
|
||||||
|
if (m_RenderPass.resourceWriteLists[tex.handle.iType][i].index == tex.handle.index)
|
||||||
|
{
|
||||||
|
alreadyUsed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadyUsed)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentTextureAlreadyUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Resources.GetRenderTargetInfo(tex.handle, out var info);
|
||||||
|
|
||||||
|
bool isDepthFormat = GraphicsFormatUtility.IsDepthFormat(info.format);
|
||||||
|
bool formatMismatch = isDepth != isDepthFormat;
|
||||||
|
|
||||||
|
if (formatMismatch)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
var errorMsg = isDepth
|
||||||
|
? RenderGraph.RenderGraphExceptionMessages.UseDepthWithColorFormat(info.format)
|
||||||
|
: RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentOnDepthTexture;
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"In pass '{m_RenderPass.name}' when trying to use resource '{name}' " +
|
||||||
|
$"of type {tex.handle.type} at index {tex.handle.index} - {errorMsg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_RenderGraph.renderTextureUVOriginStrategy == RenderTextureUVOriginStrategy.PropagateAttachmentOrientation)
|
||||||
|
{
|
||||||
|
var texResource = m_Resources.GetTextureResource(tex.handle);
|
||||||
|
TextureResource checkTexResource = null;
|
||||||
|
for (int i = 0; i < m_RenderPass.fragmentInputMaxIndex + 1; ++i)
|
||||||
|
{
|
||||||
|
if (m_RenderPass.fragmentInputAccess[i].textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
ref readonly TextureHandle texCheck = ref m_RenderPass.fragmentInputAccess[i].textureHandle;
|
||||||
|
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
|
||||||
|
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "input", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_RenderPass.colorBufferMaxIndex + 1; ++i)
|
||||||
|
{
|
||||||
|
if (m_RenderPass.colorBufferAccess[i].textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
ref readonly TextureHandle texCheck = ref m_RenderPass.colorBufferAccess[i].textureHandle;
|
||||||
|
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
|
||||||
|
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "render", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDepth && m_RenderPass.depthAccess.textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
TextureHandle texCheck = m_RenderPass.depthAccess.textureHandle;
|
||||||
|
checkTexResource = m_Resources.GetTextureResource(texCheck.handle);
|
||||||
|
if (texResource.textureUVOrigin != TextureUVOriginSelection.Unknown && checkTexResource.textureUVOrigin != TextureUVOriginSelection.Unknown && texResource.textureUVOrigin != checkTexResource.textureUVOrigin)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
var checkName = m_Resources.GetRenderGraphResourceName(texCheck.handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.IncompatibleTextureUVOrigin(texResource.textureUVOrigin, "depth", checkName, texCheck.handle.type, texCheck.handle.index, checkTexResource.textureUVOrigin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var globalTex in m_RenderPass.setGlobalsList)
|
||||||
|
{
|
||||||
|
if (globalTex.Item1.handle.index == tex.handle.index)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(tex.handle);
|
||||||
|
throw new InvalidOperationException($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {tex.handle.type} at index {tex.handle.index} - " + RenderGraph.RenderGraphExceptionMessages.k_SetRenderAttachmentOnGlobalTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderAttachment(TextureHandle tex, int index, AccessFlags flags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
CheckUseFragment(tex, false);
|
||||||
|
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
|
||||||
|
m_RenderPass.SetColorBufferRaw(versionedTextureHandle, index, flags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInputAttachment(TextureHandle tex, int index, AccessFlags flags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
CheckFrameBufferFetchEmulationIsSupported(tex);
|
||||||
|
|
||||||
|
CheckUseFragment(tex, false);
|
||||||
|
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
|
||||||
|
m_RenderPass.SetFragmentInputRaw(versionedTextureHandle, index, flags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderAttachmentDepth(TextureHandle tex, AccessFlags flags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
CheckUseFragment(tex, true);
|
||||||
|
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, flags));
|
||||||
|
m_RenderPass.SetDepthBufferRaw(versionedTextureHandle, flags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureHandle SetRandomAccessAttachment(TextureHandle input, int index, AccessFlags flags = AccessFlags.Read)
|
||||||
|
{
|
||||||
|
CheckNotUseFragment(input);
|
||||||
|
ResourceHandle result = UseResource(input.handle, flags);
|
||||||
|
m_RenderPass.SetRandomWriteResourceRaw(result, index, false, flags);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateImageAttachment(in TextureHandle tex)
|
||||||
|
{
|
||||||
|
CheckNotUseFragment(tex);
|
||||||
|
var versionedTextureHandle = new TextureHandle(UseResource(tex.handle, AccessFlags.Read));
|
||||||
|
m_RenderPass.SetShadingRateImageRaw(versionedTextureHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferHandle UseBufferRandomAccess(BufferHandle input, int index, AccessFlags flags = AccessFlags.Read)
|
||||||
|
{
|
||||||
|
var h = UseBuffer(input, flags);
|
||||||
|
m_RenderPass.SetRandomWriteResourceRaw(h.handle, index, true, flags);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferHandle UseBufferRandomAccess(BufferHandle input, int index, bool preserveCounterValue, AccessFlags flags = AccessFlags.Read)
|
||||||
|
{
|
||||||
|
var h = UseBuffer(input, flags);
|
||||||
|
m_RenderPass.SetRandomWriteResourceRaw(h.handle, index, preserveCounterValue, flags);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, ComputeGraphContext> renderFunc) where PassData : class, new()
|
||||||
|
{
|
||||||
|
((ComputeRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, RasterGraphContext> renderFunc) where PassData : class, new()
|
||||||
|
{
|
||||||
|
((RasterRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderFunc<PassData>(BaseRenderFunc<PassData, UnsafeGraphContext> renderFunc) where PassData : class, new()
|
||||||
|
{
|
||||||
|
((UnsafeRenderGraphPass<PassData>)m_RenderPass).renderFunc = renderFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UseRendererList(in RendererListHandle input)
|
||||||
|
{
|
||||||
|
m_RenderPass.UseRendererList(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
void CheckResource(in ResourceHandle res, bool checkTransientReadWrite = false)
|
||||||
|
{
|
||||||
|
if (RenderGraph.enableValidityChecks)
|
||||||
|
{
|
||||||
|
if (res.IsValid())
|
||||||
|
{
|
||||||
|
int transientIndex = m_Resources.GetRenderGraphResourceTransientIndex(res);
|
||||||
|
// We have dontCheckTransientReadWrite here because users may want to use UseColorBuffer/UseDepthBuffer API to benefit from render target auto binding. In this case we don't want to raise the error.
|
||||||
|
if (transientIndex == m_RenderPass.index && checkTransientReadWrite)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(res);
|
||||||
|
Debug.LogError($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " + RenderGraph.RenderGraphExceptionMessages.k_ReadWriteTransient);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transientIndex != -1 && transientIndex != m_RenderPass.index)
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(res);
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " +
|
||||||
|
RenderGraph.RenderGraphExceptionMessages.UseTransientTextureInWrongPass(transientIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var name = m_Resources.GetRenderGraphResourceName(res);
|
||||||
|
throw new Exception($"In pass '{m_RenderPass.name}' when trying to use resource '{name}' of type {res.type} at index {res.index} - " + RenderGraph.RenderGraphExceptionMessages.k_InvalidResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
|
||||||
|
void CheckFrameBufferFetchEmulationIsSupported(in TextureHandle tex)
|
||||||
|
{
|
||||||
|
if (enableValidityChecks)
|
||||||
|
{
|
||||||
|
if (!Util.RenderGraphUtils.IsFramebufferFetchEmulationMSAASupportedOnCurrentPlatform())
|
||||||
|
{
|
||||||
|
var sourceInfo = m_RenderGraph.GetRenderTargetInfo(tex);
|
||||||
|
if (sourceInfo.bindMS)
|
||||||
|
throw new InvalidOperationException($"This API is not supported with MSAA attachments on the current platform: {SystemInfo.graphicsDeviceType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateFragmentSize(ShadingRateFragmentSize shadingRateFragmentSize)
|
||||||
|
{
|
||||||
|
m_RenderPass.SetShadingRateFragmentSize(shadingRateFragmentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateCombiner(ShadingRateCombinerStage stage, ShadingRateCombiner combiner)
|
||||||
|
{
|
||||||
|
m_RenderPass.SetShadingRateCombiner(stage, combiner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetExtendedFeatureFlags(ExtendedFeatureFlags extendedFeatureFlags)
|
||||||
|
{
|
||||||
|
m_RenderPass.SetExtendedFeatureFlags(extendedFeatureFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
752
Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs
Normal file
752
Ghost.RenderGraph.Concept/Unity/RenderGraphPass.cs
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
#if false
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace UnityEngine.Rendering.RenderGraphModule
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
abstract class RenderGraphPass
|
||||||
|
{
|
||||||
|
public abstract void Execute(InternalRenderGraphContext renderGraphContext);
|
||||||
|
public abstract void Release(RenderGraphObjectPool pool);
|
||||||
|
public abstract bool HasRenderFunc();
|
||||||
|
public abstract int GetRenderFuncHash();
|
||||||
|
|
||||||
|
public string name { get; protected set; }
|
||||||
|
public int index { get; protected set; }
|
||||||
|
public RenderGraphPassType type { get; internal set; }
|
||||||
|
public ProfilingSampler customSampler { get; protected set; }
|
||||||
|
public bool enableAsyncCompute { get; protected set; }
|
||||||
|
public bool allowPassCulling { get; protected set; }
|
||||||
|
public bool allowGlobalState { get; protected set; }
|
||||||
|
public bool enableFoveatedRasterization { get; protected set; }
|
||||||
|
public ExtendedFeatureFlags extendedFeatureFlags { get; protected set; }
|
||||||
|
|
||||||
|
// Before using the AccessFlags use resourceHandle.isValid()
|
||||||
|
// to make sure that the data in the colorBuffer/fragmentInput/randomAccessResource buffers are up to date
|
||||||
|
public TextureAccess depthAccess { get; protected set; }
|
||||||
|
|
||||||
|
public TextureAccess[] colorBufferAccess { get; protected set; } = new TextureAccess[RenderGraph.kMaxMRTCount];
|
||||||
|
public int colorBufferMaxIndex { get; protected set; } = -1;
|
||||||
|
|
||||||
|
public bool hasShadingRateImage { get; protected set; }
|
||||||
|
public TextureAccess shadingRateAccess { get; protected set; }
|
||||||
|
|
||||||
|
public bool hasShadingRateStates { get; protected set; }
|
||||||
|
public ShadingRateFragmentSize shadingRateFragmentSize { get; protected set; }
|
||||||
|
public ShadingRateCombiner primitiveShadingRateCombiner { get; protected set; }
|
||||||
|
public ShadingRateCombiner fragmentShadingRateCombiner { get; protected set; }
|
||||||
|
|
||||||
|
// Used by native pass compiler only
|
||||||
|
public TextureAccess[] fragmentInputAccess { get; protected set; } = new TextureAccess[RenderGraph.kMaxMRTCount];
|
||||||
|
public int fragmentInputMaxIndex { get; protected set; } = -1;
|
||||||
|
|
||||||
|
public struct RandomWriteResourceInfo
|
||||||
|
{
|
||||||
|
public ResourceHandle h;
|
||||||
|
public bool preserveCounterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This list can contain both texture and buffer resources based on their binding index.
|
||||||
|
public RandomWriteResourceInfo[] randomAccessResource { get; protected set; } = new RandomWriteResourceInfo[RenderGraph.kMaxMRTCount];
|
||||||
|
public int randomAccessResourceMaxIndex { get; protected set; } = -1;
|
||||||
|
|
||||||
|
public bool generateDebugData { get; protected set; }
|
||||||
|
|
||||||
|
public bool allowRendererListCulling { get; protected set; }
|
||||||
|
|
||||||
|
public List<ResourceHandle>[] resourceReadLists = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
|
||||||
|
public List<ResourceHandle>[] resourceWriteLists = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
|
||||||
|
public List<ResourceHandle>[] transientResourceList = new List<ResourceHandle>[(int)RenderGraphResourceType.Count];
|
||||||
|
|
||||||
|
public List<RendererListHandle> usedRendererListList = new List<RendererListHandle>();
|
||||||
|
|
||||||
|
public List<ValueTuple<TextureHandle, int>> setGlobalsList = new List<ValueTuple<TextureHandle, int>>();
|
||||||
|
public bool useAllGlobalTextures;
|
||||||
|
|
||||||
|
public List<ResourceHandle> implicitReadsList = new List<ResourceHandle>();
|
||||||
|
|
||||||
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||||
|
public RenderGraph.DebugData.PassScriptInfo debugScriptInfo { get; set; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public RenderGraphPass()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < (int)RenderGraphResourceType.Count; ++i)
|
||||||
|
{
|
||||||
|
resourceReadLists[i] = new List<ResourceHandle>();
|
||||||
|
resourceWriteLists[i] = new List<ResourceHandle>();
|
||||||
|
transientResourceList[i] = new List<ResourceHandle>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
name = "";
|
||||||
|
index = -1;
|
||||||
|
customSampler = null;
|
||||||
|
for (int i = 0; i < (int)RenderGraphResourceType.Count; ++i)
|
||||||
|
{
|
||||||
|
resourceReadLists[i].Clear();
|
||||||
|
resourceWriteLists[i].Clear();
|
||||||
|
transientResourceList[i].Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
usedRendererListList.Clear();
|
||||||
|
setGlobalsList.Clear();
|
||||||
|
useAllGlobalTextures = false;
|
||||||
|
implicitReadsList.Clear();
|
||||||
|
enableAsyncCompute = false;
|
||||||
|
allowPassCulling = true;
|
||||||
|
allowRendererListCulling = true;
|
||||||
|
allowGlobalState = false;
|
||||||
|
enableFoveatedRasterization = false;
|
||||||
|
generateDebugData = true;
|
||||||
|
|
||||||
|
// Invalidate the buffers without clearing them, as it is too costly
|
||||||
|
// Use IsValid() to make sure that the data in the colorBuffer/fragmentInput/randomAccessResource buffers are up to date
|
||||||
|
colorBufferMaxIndex = -1;
|
||||||
|
fragmentInputMaxIndex = -1;
|
||||||
|
randomAccessResourceMaxIndex = -1;
|
||||||
|
|
||||||
|
// We do not need to clear colorBufferAccess and fragmentInputAccess as we have the colorBufferMaxIndex and fragmentInputMaxIndex
|
||||||
|
// which are reset above so we only clear depthAccess here.
|
||||||
|
depthAccess = default(TextureAccess);
|
||||||
|
|
||||||
|
hasShadingRateImage = false;
|
||||||
|
hasShadingRateStates = false;
|
||||||
|
shadingRateFragmentSize = ShadingRateFragmentSize.FragmentSize1x1;
|
||||||
|
primitiveShadingRateCombiner = ShadingRateCombiner.Keep;
|
||||||
|
fragmentShadingRateCombiner = ShadingRateCombiner.Keep;
|
||||||
|
|
||||||
|
// Invalidate ExtendedFeatureFlags
|
||||||
|
extendedFeatureFlags = ExtendedFeatureFlags.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the pass has any render targets set-up
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool HasRenderAttachments()
|
||||||
|
{
|
||||||
|
return depthAccess.textureHandle.IsValid() || colorBufferAccess[0].textureHandle.IsValid() || colorBufferMaxIndex > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the resource is involved in this pass
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsTransient(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
// Versioning doesn't matter much for transient resources as they are only used within a single pass
|
||||||
|
for (int i = 0; i < transientResourceList[res.iType].Count; i++)
|
||||||
|
{
|
||||||
|
if (transientResourceList[res.iType][i].index == res.index)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsWritten(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
// You can only ever write to the latest version so we ignore it when looking in the list
|
||||||
|
for (int i = 0; i < resourceWriteLists[res.iType].Count; i++)
|
||||||
|
{
|
||||||
|
if (resourceWriteLists[res.iType][i].index == res.index)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsRead(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
if (res.IsVersioned)
|
||||||
|
{
|
||||||
|
return resourceReadLists[res.iType].Contains(res);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Just look if we are reading any version of this texture.
|
||||||
|
// Note that in theory this pass could read from several versions of the same texture
|
||||||
|
// e.g. ColorBuffer,v3 and ColorBuffer,v5 so this check is always conservative
|
||||||
|
for (int i = 0; i < resourceReadLists[res.iType].Count; i++)
|
||||||
|
{
|
||||||
|
if (resourceReadLists[res.iType][i].index == res.index)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsAttachment(in TextureHandle res)
|
||||||
|
{
|
||||||
|
// We ignore the version when checking, if any version is used it is considered a match
|
||||||
|
|
||||||
|
if (depthAccess.textureHandle.IsValid() && depthAccess.textureHandle.handle.index == res.handle.index) return true;
|
||||||
|
for (int i = 0; i < colorBufferAccess.Length; i++)
|
||||||
|
{
|
||||||
|
if (colorBufferAccess[i].textureHandle.IsValid() && colorBufferAccess[i].textureHandle.handle.index == res.handle.index) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AddResourceWrite(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
resourceWriteLists[res.iType].Add(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AddResourceRead(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
resourceReadLists[res.iType].Add(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AddTransientResource(in ResourceHandle res)
|
||||||
|
{
|
||||||
|
transientResourceList[res.iType].Add(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void UseRendererList(in RendererListHandle rendererList)
|
||||||
|
{
|
||||||
|
usedRendererListList.Add(rendererList);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void EnableAsyncCompute(bool value)
|
||||||
|
{
|
||||||
|
enableAsyncCompute = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AllowPassCulling(bool value)
|
||||||
|
{
|
||||||
|
allowPassCulling = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void EnableFoveatedRasterization(bool value)
|
||||||
|
{
|
||||||
|
enableFoveatedRasterization = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AllowRendererListCulling(bool value)
|
||||||
|
{
|
||||||
|
allowRendererListCulling = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AllowGlobalState(bool value)
|
||||||
|
{
|
||||||
|
allowGlobalState = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void GenerateDebugData(bool value)
|
||||||
|
{
|
||||||
|
generateDebugData = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetColorBuffer(in TextureHandle resource, int index)
|
||||||
|
{
|
||||||
|
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
|
||||||
|
colorBufferMaxIndex = Math.Max(colorBufferMaxIndex, index);
|
||||||
|
colorBufferAccess[index] = new TextureAccess(colorBufferAccess[index], resource);
|
||||||
|
AddResourceWrite(resource.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the color buffer for this pass but not any resource Read/Writes for it
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetColorBufferRaw(in TextureHandle resource, int index, AccessFlags accessFlags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
|
||||||
|
if (colorBufferAccess[index].textureHandle.handle.Equals(resource.handle) || !colorBufferAccess[index].textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
colorBufferMaxIndex = Math.Max(colorBufferMaxIndex, index);
|
||||||
|
colorBufferAccess[index] = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
||||||
|
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"In pass '{name}' when trying to call SetRenderAttachment with resource of type {resource.handle.type} at index {index} - " +
|
||||||
|
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneResourceForMRTIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the color buffer for this pass but not any resource Read/Writes for it
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetFragmentInputRaw(in TextureHandle resource, int index, AccessFlags accessFlags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
|
||||||
|
if (fragmentInputAccess[index].textureHandle.handle.Equals(resource.handle) || !fragmentInputAccess[index].textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
fragmentInputMaxIndex = Math.Max(fragmentInputMaxIndex, index);
|
||||||
|
fragmentInputAccess[index] = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
||||||
|
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"In pass '{name}' when trying to call SetInputAttachment with resource of type {resource.handle.type} at index {index} - " +
|
||||||
|
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneTextureForFragInputIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the color buffer for this pass but not any resource Read/Writes for it
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetRandomWriteResourceRaw(in ResourceHandle resource, int index, bool preserveCounterValue, AccessFlags accessFlags)
|
||||||
|
{
|
||||||
|
Debug.Assert(index < RenderGraph.kMaxMRTCount && index >= 0);
|
||||||
|
if (randomAccessResource[index].h.Equals(resource) || !randomAccessResource[index].h.IsValid())
|
||||||
|
{
|
||||||
|
randomAccessResourceMaxIndex = Math.Max(randomAccessResourceMaxIndex, index);
|
||||||
|
ref var info = ref randomAccessResource[index];
|
||||||
|
info.h = resource;
|
||||||
|
info.preserveCounterValue = preserveCounterValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// You tried to do SetRenderAttachment(tex1, 1, ..); SetRenderAttachment(tex2, 1, ..); that is not valid for different textures on the same index
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"In pass '{name}' when trying to call SetRandomAccessAttachment/UseBufferRandomAccess with resource of type {resource.type} at index {index} - " +
|
||||||
|
RenderGraph.RenderGraphExceptionMessages.k_MoreThanOneTextureRandomWriteInputIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetDepthBuffer(in TextureHandle resource, DepthAccess flags)
|
||||||
|
{
|
||||||
|
depthAccess = new TextureAccess(resource, (AccessFlags)flags, 0, 0);
|
||||||
|
if ((flags & DepthAccess.Read) != 0)
|
||||||
|
AddResourceRead(resource.handle);
|
||||||
|
if ((flags & DepthAccess.Write) != 0)
|
||||||
|
AddResourceWrite(resource.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the depth buffer for this pass but not any resource Read/Writes for it
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetDepthBufferRaw(in TextureHandle resource, AccessFlags accessFlags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
// If no depth buffer yet or if it is the same one as the previous one, allow the call otherwise log an error.
|
||||||
|
if (depthAccess.textureHandle.handle.Equals(resource.handle) || !depthAccess.textureHandle.IsValid())
|
||||||
|
{
|
||||||
|
depthAccess = new TextureAccess(resource, accessFlags, mipLevel, depthSlice);
|
||||||
|
}
|
||||||
|
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"In pass '{name}' when trying to call SetRenderAttachmentDepth with resource of type {resource.handle.type} at index {index} - " +
|
||||||
|
RenderGraph.RenderGraphExceptionMessages.k_MultipleDepthTextures);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we want to keep computation to a minimum and only hash what will influence NRP compiler: Pass merging, load/store actions etc.
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
void ComputeTextureHash(ref HashFNV1A32 generator, in ResourceHandle handle, RenderGraphResourceRegistry resources)
|
||||||
|
{
|
||||||
|
if (handle.index == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (resources.IsRenderGraphResourceImported(handle))
|
||||||
|
{
|
||||||
|
var res = resources.GetTextureResource(handle);
|
||||||
|
var graphicsResource = res.graphicsResource;
|
||||||
|
ref readonly var desc = ref res.desc;
|
||||||
|
|
||||||
|
var externalTexture = graphicsResource.externalTexture;
|
||||||
|
if (externalTexture != null) // External texture
|
||||||
|
{
|
||||||
|
generator.Append((int) externalTexture.graphicsFormat);
|
||||||
|
generator.Append((int) externalTexture.dimension);
|
||||||
|
generator.Append(externalTexture.width);
|
||||||
|
generator.Append(externalTexture.height);
|
||||||
|
if (externalTexture is RenderTexture externalRT)
|
||||||
|
generator.Append(externalRT.antiAliasing);
|
||||||
|
}
|
||||||
|
else if (graphicsResource.rt != null) // Regular RTHandle
|
||||||
|
{
|
||||||
|
var rt = graphicsResource.rt;
|
||||||
|
generator.Append((int) rt.graphicsFormat);
|
||||||
|
generator.Append((int) rt.dimension);
|
||||||
|
generator.Append(rt.antiAliasing);
|
||||||
|
if (graphicsResource.useScaling)
|
||||||
|
if (graphicsResource.scaleFunc != null)
|
||||||
|
generator.Append(DelegateHashCodeUtils.GetFuncHashCode(graphicsResource.scaleFunc));
|
||||||
|
else
|
||||||
|
generator.Append(graphicsResource.scaleFactor);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
generator.Append(rt.width);
|
||||||
|
generator.Append(rt.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (graphicsResource.nameID != default) // External RTI
|
||||||
|
{
|
||||||
|
// The only info we have is from the provided desc upon importing.
|
||||||
|
generator.Append((int) desc.format);
|
||||||
|
generator.Append((int) desc.dimension);
|
||||||
|
generator.Append((int) desc.msaaSamples);
|
||||||
|
generator.Append(desc.width);
|
||||||
|
generator.Append(desc.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the clear/discard buffer flags to the hash (used in all the cases above)
|
||||||
|
generator.Append(desc.clearBuffer);
|
||||||
|
generator.Append(desc.discardBuffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ref readonly var desc = ref resources.GetTextureResourceDesc(handle);
|
||||||
|
generator.Append((int) desc.format);
|
||||||
|
generator.Append((int) desc.dimension);
|
||||||
|
generator.Append((int) desc.msaaSamples);
|
||||||
|
generator.Append(desc.clearBuffer);
|
||||||
|
generator.Append(desc.discardBuffer);
|
||||||
|
switch (desc.sizeMode)
|
||||||
|
{
|
||||||
|
case TextureSizeMode.Explicit:
|
||||||
|
generator.Append(desc.width);
|
||||||
|
generator.Append(desc.height);
|
||||||
|
break;
|
||||||
|
case TextureSizeMode.Scale:
|
||||||
|
generator.Append(desc.scale);
|
||||||
|
break;
|
||||||
|
case TextureSizeMode.Functor:
|
||||||
|
generator.Append(DelegateHashCodeUtils.GetFuncHashCode(desc.func));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
static void ComputeHashForTextureAccess(ref HashFNV1A32 generator, in ResourceHandle handle, in TextureAccess textureAccess)
|
||||||
|
{
|
||||||
|
generator.Append(handle.index);
|
||||||
|
generator.Append((int) textureAccess.flags);
|
||||||
|
generator.Append(textureAccess.mipLevel);
|
||||||
|
generator.Append(textureAccess.depthSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is performance sensitive.
|
||||||
|
// Avoid mass function calls to get the hashCode and compute locally instead.
|
||||||
|
public void ComputeHash(ref HashFNV1A32 generator, RenderGraphResourceRegistry resources)
|
||||||
|
{
|
||||||
|
generator.Append((int) type);
|
||||||
|
generator.Append(enableAsyncCompute);
|
||||||
|
generator.Append(allowPassCulling);
|
||||||
|
generator.Append(allowGlobalState);
|
||||||
|
generator.Append(enableFoveatedRasterization);
|
||||||
|
generator.Append(extendedFeatureFlags);
|
||||||
|
|
||||||
|
var depthHandle = depthAccess.textureHandle.handle;
|
||||||
|
if (depthHandle.IsValid())
|
||||||
|
{
|
||||||
|
ComputeTextureHash(ref generator, depthHandle, resources);
|
||||||
|
ComputeHashForTextureAccess(ref generator, depthHandle, depthAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < colorBufferMaxIndex + 1; ++i)
|
||||||
|
{
|
||||||
|
var colorBufferAccessElement = colorBufferAccess[i];
|
||||||
|
var handle = colorBufferAccessElement.textureHandle.handle;
|
||||||
|
if (!handle.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ComputeTextureHash(ref generator, handle, resources);
|
||||||
|
ComputeHashForTextureAccess(ref generator, handle, colorBufferAccessElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.Append(colorBufferMaxIndex);
|
||||||
|
|
||||||
|
generator.Append(hasShadingRateImage);
|
||||||
|
if (hasShadingRateImage)
|
||||||
|
{
|
||||||
|
var handle = shadingRateAccess.textureHandle.handle;
|
||||||
|
if (handle.IsValid())
|
||||||
|
{
|
||||||
|
ComputeTextureHash(ref generator, handle, resources);
|
||||||
|
ComputeHashForTextureAccess(ref generator, handle, shadingRateAccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.Append(hasShadingRateStates);
|
||||||
|
generator.Append((int)shadingRateFragmentSize);
|
||||||
|
generator.Append((int)primitiveShadingRateCombiner);
|
||||||
|
generator.Append((int)fragmentShadingRateCombiner);
|
||||||
|
|
||||||
|
for (int i = 0; i < fragmentInputMaxIndex + 1; ++i)
|
||||||
|
{
|
||||||
|
var fragmentInputAccessElement = fragmentInputAccess[i];
|
||||||
|
var handle = fragmentInputAccessElement.textureHandle.handle;
|
||||||
|
if (!handle.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ComputeTextureHash(ref generator, handle, resources);
|
||||||
|
ComputeHashForTextureAccess(ref generator, handle, fragmentInputAccessElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < randomAccessResourceMaxIndex + 1; ++i)
|
||||||
|
{
|
||||||
|
var rar = randomAccessResource[i];
|
||||||
|
if (!rar.h.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
generator.Append(rar.h.index);
|
||||||
|
generator.Append(rar.preserveCounterValue);
|
||||||
|
}
|
||||||
|
generator.Append(randomAccessResourceMaxIndex);
|
||||||
|
generator.Append(fragmentInputMaxIndex);
|
||||||
|
generator.Append(generateDebugData);
|
||||||
|
generator.Append(allowRendererListCulling);
|
||||||
|
|
||||||
|
for (int resType = 0; resType < (int)RenderGraphResourceType.Count; resType++)
|
||||||
|
{
|
||||||
|
var resourceReads = resourceReadLists[resType];
|
||||||
|
var resourceReadsCount = resourceReads.Count;
|
||||||
|
for (int i = 0; i < resourceReadsCount; ++i)
|
||||||
|
generator.Append(resourceReads[i].index);
|
||||||
|
|
||||||
|
var resourceWrites = resourceWriteLists[resType];
|
||||||
|
var resourceWritesCount = resourceWrites.Count;
|
||||||
|
for (int i = 0; i < resourceWritesCount; ++i)
|
||||||
|
generator.Append(resourceWrites[i].index);
|
||||||
|
|
||||||
|
var resourceTransient = transientResourceList[resType];
|
||||||
|
var resourceTransientCount = resourceTransient.Count;
|
||||||
|
for (int i = 0; i < resourceTransientCount; ++i)
|
||||||
|
generator.Append(resourceTransient[i].index);
|
||||||
|
}
|
||||||
|
|
||||||
|
var usedRendererListListCount = usedRendererListList.Count;
|
||||||
|
for (int i = 0; i < usedRendererListListCount; ++i)
|
||||||
|
generator.Append(usedRendererListList[i].handle);
|
||||||
|
|
||||||
|
var setGlobalsListCount = setGlobalsList.Count;
|
||||||
|
for (int i = 0; i < setGlobalsListCount; ++i)
|
||||||
|
{
|
||||||
|
var global = setGlobalsList[i];
|
||||||
|
generator.Append(global.Item1.handle.index);
|
||||||
|
generator.Append(global.Item2);
|
||||||
|
}
|
||||||
|
generator.Append(useAllGlobalTextures);
|
||||||
|
|
||||||
|
var implicitReadsListCount = implicitReadsList.Count;
|
||||||
|
for (int i = 0; i < implicitReadsListCount; ++i)
|
||||||
|
generator.Append(implicitReadsList[i].index);
|
||||||
|
|
||||||
|
generator.Append(GetRenderFuncHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateImageRaw(in TextureHandle shadingRateImage)
|
||||||
|
{
|
||||||
|
if (ShadingRateInfo.supportsPerImageTile)
|
||||||
|
{
|
||||||
|
hasShadingRateImage = true;
|
||||||
|
// shading rate image access flag is always read, only 1 mip and 1 slice
|
||||||
|
shadingRateAccess = new TextureAccess(shadingRateImage, AccessFlags.Read, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateImage(in TextureHandle shadingRateImage, AccessFlags accessFlags, int mipLevel, int depthSlice)
|
||||||
|
{
|
||||||
|
if (ShadingRateInfo.supportsPerImageTile)
|
||||||
|
{
|
||||||
|
hasShadingRateImage = true;
|
||||||
|
shadingRateAccess = new TextureAccess(shadingRateImage, accessFlags, mipLevel, depthSlice);
|
||||||
|
AddResourceRead(shadingRateAccess.textureHandle.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateFragmentSize(ShadingRateFragmentSize shadingRateFragmentSize)
|
||||||
|
{
|
||||||
|
if (ShadingRateInfo.supportsPerDrawCall)
|
||||||
|
{
|
||||||
|
hasShadingRateStates = true;
|
||||||
|
this.shadingRateFragmentSize = shadingRateFragmentSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetShadingRateCombiner(ShadingRateCombinerStage stage, ShadingRateCombiner combiner)
|
||||||
|
{
|
||||||
|
if (ShadingRateInfo.supportsPerImageTile)
|
||||||
|
{
|
||||||
|
switch (stage)
|
||||||
|
{
|
||||||
|
case ShadingRateCombinerStage.Primitive:
|
||||||
|
hasShadingRateStates = true;
|
||||||
|
primitiveShadingRateCombiner = combiner;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShadingRateCombinerStage.Fragment:
|
||||||
|
hasShadingRateStates = true;
|
||||||
|
fragmentShadingRateCombiner = combiner;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetExtendedFeatureFlags(ExtendedFeatureFlags value)
|
||||||
|
{
|
||||||
|
extendedFeatureFlags |= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This used to have an extra generic argument 'RenderGraphContext' abstracting the context and avoiding
|
||||||
|
// the RenderGraphPass/ComputeRenderGraphPass/RasterRenderGraphPass/UnsafeRenderGraphPass classes below
|
||||||
|
// but this confuses IL2CPP and causes garbage when boxing the context created (even though they are structs)
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
internal abstract class BaseRenderGraphPass<PassData, TRenderGraphContext> : RenderGraphPass
|
||||||
|
where PassData : class, new()
|
||||||
|
{
|
||||||
|
internal PassData data;
|
||||||
|
internal BaseRenderFunc<PassData, TRenderGraphContext> renderFunc;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Initialize(int passIndex, PassData passData, string passName, RenderGraphPassType passType, ProfilingSampler sampler)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
index = passIndex;
|
||||||
|
data = passData;
|
||||||
|
name = passName;
|
||||||
|
type = passType;
|
||||||
|
customSampler = sampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Release(RenderGraphObjectPool pool)
|
||||||
|
{
|
||||||
|
pool.Release(data);
|
||||||
|
data = null;
|
||||||
|
renderFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override bool HasRenderFunc()
|
||||||
|
{
|
||||||
|
return renderFunc != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override int GetRenderFuncHash()
|
||||||
|
{
|
||||||
|
return renderFunc != null ? DelegateHashCodeUtils.GetFuncHashCode(renderFunc) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
[Obsolete("RenderGraphPass is deprecated, use RasterRenderGraphPass/ComputeRenderGraphPass/UnsafeRenderGraphPass instead.")]
|
||||||
|
internal sealed class RenderGraphPass<PassData> : BaseRenderGraphPass<PassData, RenderGraphContext>
|
||||||
|
where PassData : class, new()
|
||||||
|
{
|
||||||
|
internal static RenderGraphContext c = new RenderGraphContext();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Execute(InternalRenderGraphContext renderGraphContext)
|
||||||
|
{
|
||||||
|
c.FromInternalContext(renderGraphContext);
|
||||||
|
renderFunc(data, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Release(RenderGraphObjectPool pool)
|
||||||
|
{
|
||||||
|
base.Release(pool);
|
||||||
|
|
||||||
|
// We need to do the release from here because we need the final type.
|
||||||
|
pool.Release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
internal sealed class ComputeRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, ComputeGraphContext>
|
||||||
|
where PassData : class, new()
|
||||||
|
{
|
||||||
|
internal static ComputeGraphContext c = new ComputeGraphContext();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Execute(InternalRenderGraphContext renderGraphContext)
|
||||||
|
{
|
||||||
|
c.FromInternalContext(renderGraphContext);
|
||||||
|
renderFunc(data, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Release(RenderGraphObjectPool pool)
|
||||||
|
{
|
||||||
|
base.Release(pool);
|
||||||
|
|
||||||
|
// We need to do the release from here because we need the final type.
|
||||||
|
pool.Release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
internal sealed class RasterRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, RasterGraphContext>
|
||||||
|
where PassData : class, new()
|
||||||
|
{
|
||||||
|
internal static RasterGraphContext c = new RasterGraphContext();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Execute(InternalRenderGraphContext renderGraphContext)
|
||||||
|
{
|
||||||
|
c.FromInternalContext(renderGraphContext);
|
||||||
|
renderFunc(data, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Release(RenderGraphObjectPool pool)
|
||||||
|
{
|
||||||
|
base.Release(pool);
|
||||||
|
|
||||||
|
// We need to do the release from here because we need the final type.
|
||||||
|
pool.Release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("RenderPass: {name} (Index:{index} Async:{enableAsyncCompute})")]
|
||||||
|
internal sealed class UnsafeRenderGraphPass<PassData> : BaseRenderGraphPass<PassData, UnsafeGraphContext>
|
||||||
|
where PassData : class, new()
|
||||||
|
{
|
||||||
|
internal static UnsafeGraphContext c = new UnsafeGraphContext();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Execute(InternalRenderGraphContext renderGraphContext)
|
||||||
|
{
|
||||||
|
c.FromInternalContext(renderGraphContext);
|
||||||
|
renderFunc(data, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public override void Release(RenderGraphObjectPool pool)
|
||||||
|
{
|
||||||
|
base.Release(pool);
|
||||||
|
|
||||||
|
// We need to do the release from here because we need the final type.
|
||||||
|
pool.Release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user