Add render graph proof of concept and refactor graphics
Implemented a transient render graph system as a proof of concept, including resource aliasing, pass culling, and typed pass data. Added new project `Ghost.RenderGraph.Concept` targeting `.NET 10.0`. Refactored graphics-related components: - Simplified resource state transitions in `RenderingContext`. - Improved resize handling in `GraphicsTestWindow`. - Updated `D3D12GraphicsEngine` to streamline frame rendering. - Enhanced `D3D12ResourceDatabase` and `D3D12SwapChain` for better resource management. Added detailed documentation: - `ALIASING.md` explains resource aliasing techniques. - `API_DESIGN.md` outlines the render graph API design. Updated solution to include the new render graph project.
This commit is contained in:
177
Ghost.RenderGraph.Concept/ALIASING.md
Normal file
177
Ghost.RenderGraph.Concept/ALIASING.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Resource Aliasing in Render Graph
|
||||
|
||||
## Overview
|
||||
|
||||
Resource aliasing is a memory optimization technique where multiple virtual resources share the same physical memory allocation. This significantly reduces memory usage for transient resources.
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Lifetime Analysis
|
||||
The render graph analyzes when each transient resource is first used and last used:
|
||||
```
|
||||
GBuffer.Albedo: [0..1] ━━━━━━━━
|
||||
SSAO: [2..4] ━━━━━━━━━━━
|
||||
```
|
||||
|
||||
### 2. Aliasing Detection
|
||||
Resources with non-overlapping lifetimes can share memory:
|
||||
```
|
||||
Physical_Texture_2:
|
||||
[0..1] GBuffer.Albedo ━━━━━━━━
|
||||
[2..4] SSAO ━━━━━━━━━━━ ← ALIAS!
|
||||
```
|
||||
|
||||
### 3. Memory Allocation
|
||||
Instead of creating 2 separate 8MB textures (16MB total), we create 1 physical allocation (8MB) that both virtual resources map to.
|
||||
|
||||
## Aliasing Barriers
|
||||
|
||||
In D3D12/Vulkan, when you reuse memory for a different resource, you must insert an **aliasing barrier** to inform the GPU that the memory interpretation has changed.
|
||||
|
||||
### When Aliasing Barriers Are Needed
|
||||
|
||||
An aliasing barrier is required when:
|
||||
1. Two or more resources share the same physical memory
|
||||
2. You're switching from one resource to another
|
||||
3. Both resources are accessed within overlapping command buffer scopes
|
||||
|
||||
In this implementation, aliasing barriers are automatically inserted when:
|
||||
- A pass accesses a resource that shares a physical allocation
|
||||
- A different resource was previously active on that allocation
|
||||
- The active resource hasn't been explicitly destroyed
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
[RG] ===== RESOURCE ALIASING ANALYSIS =====
|
||||
[ALLOC] 'GBuffer.Albedo' gets new allocation 'Physical_Texture_2' (size: 8.29 MB, lifetime: [0..1])
|
||||
[ALIAS] 'SSAO' aliases with 'Physical_Texture_2' (offset: 0, size: 8.29 MB, lifetime: [2..4])
|
||||
[ALIAS] 'BloomDownsample' aliases with 'Physical_Texture_1' (offset: 0, size: 8.29 MB, lifetime: [3..5])
|
||||
|
||||
[RG] Memory Statistics:
|
||||
Total memory without aliasing: 80.64 MB
|
||||
Total memory with aliasing: 47.46 MB
|
||||
Memory saved: 33.18 MB (41.1%)
|
||||
Allocations: 5 physical allocations for 8 resources
|
||||
```
|
||||
|
||||
## Aliasing Algorithm
|
||||
|
||||
The allocator uses a **First-Fit** strategy:
|
||||
|
||||
```csharp
|
||||
foreach (var resource in transientResources.OrderBy(FirstUse).ThenBy(Size))
|
||||
{
|
||||
// Try to find existing allocation
|
||||
foreach (var slot in allocationSlots)
|
||||
{
|
||||
if (slot.LargeEnough &&
|
||||
slot.SameType &&
|
||||
!HasLifetimeOverlap(slot, resource))
|
||||
{
|
||||
// REUSE!
|
||||
slot.AddResource(resource);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No compatible slot found, create new allocation
|
||||
CreateNewAllocation(resource);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Constraints
|
||||
|
||||
1. **Size**: The physical allocation must be >= required size
|
||||
2. **Type**: Textures can only alias with textures, buffers with buffers
|
||||
3. **Lifetime**: Resources must have non-overlapping lifetimes
|
||||
4. **Alignment**: Resources must satisfy GPU alignment requirements
|
||||
|
||||
## Real-World Benefits
|
||||
|
||||
### Deferred Rendering Pipeline
|
||||
|
||||
| Resource | Size | Lifetime | Physical Alloc |
|
||||
|----------|------|----------|----------------|
|
||||
| GBuffer.Albedo | 8MB | [0..1] | Physical_1 |
|
||||
| GBuffer.Normal | 16MB | [0..2] | Physical_2 |
|
||||
| GBuffer.Depth | 8MB | [0..2] | Physical_3 |
|
||||
| Lighting | 16MB | [1..3] | Physical_4 |
|
||||
| SSAO | 8MB | [2..4] | **Physical_1** ✓ |
|
||||
| TAA | 16MB | [3..4] | **Physical_2** ✓ |
|
||||
| Bloom | 8MB | [3..5] | **Physical_3** ✓ |
|
||||
|
||||
**Without aliasing**: 80MB
|
||||
**With aliasing**: 48MB
|
||||
**Savings**: 40% (32MB)
|
||||
|
||||
### At 4K Resolution (3840x2160)
|
||||
|
||||
| Format | Size (1080p) | Size (4K) |
|
||||
|--------|-------------|-----------|
|
||||
| RGBA8 | 8.3 MB | 33.2 MB |
|
||||
| RGBA16F | 16.6 MB | 66.4 MB |
|
||||
| Depth32F | 8.3 MB | 33.2 MB |
|
||||
|
||||
**4K Savings**: 128MB → Modern AAA games save **hundreds of megabytes** to **gigabytes** using this technique.
|
||||
|
||||
## Advanced Optimizations (Not Implemented)
|
||||
|
||||
### 1. Pool-Based Allocation
|
||||
Instead of individual allocations, use large memory pools and sub-allocate from them.
|
||||
|
||||
### 2. Heap-Aware Aliasing
|
||||
D3D12 has specific heap types (Default, Upload, Readback). Resources can only alias within the same heap type.
|
||||
|
||||
### 3. Subresource Aliasing
|
||||
Alias mip levels or array slices independently for more granular reuse.
|
||||
|
||||
### 4. Multi-Queue Aliasing
|
||||
Resources on different queues (Graphics, Compute, Copy) need special synchronization.
|
||||
|
||||
## Comparison with Production Systems
|
||||
|
||||
### Unreal Engine 5 RDG
|
||||
```cpp
|
||||
// Automatic aliasing based on lifetimes
|
||||
FRDGTextureRef TextureA = GraphBuilder.CreateTexture(Desc, TEXT("A"));
|
||||
FRDGTextureRef TextureB = GraphBuilder.CreateTexture(Desc, TEXT("B"));
|
||||
// RDG automatically aliases if lifetimes don't overlap
|
||||
```
|
||||
|
||||
### Frostbite Frame Graph
|
||||
- Uses explicit aliasing groups
|
||||
- Developers can hint which resources should share memory
|
||||
- More control but less automatic
|
||||
|
||||
### Unity HDRP Render Graph
|
||||
```csharp
|
||||
// Unity's approach (similar to ours)
|
||||
var tempRT = renderGraph.CreateTexture(desc);
|
||||
// Automatic aliasing through lifetime analysis
|
||||
```
|
||||
|
||||
## Performance Impact
|
||||
|
||||
**Memory**: 30-50% reduction typical
|
||||
**CPU Overhead**: <1ms during compile phase
|
||||
**GPU Performance**: Same or better (fewer allocations)
|
||||
**Bandwidth**: Reduced due to better cache locality
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. **Print Allocation Map**: See which resources share memory
|
||||
2. **Visualize Lifetimes**: Graph timeline to spot aliasing opportunities
|
||||
3. **Track Peak Memory**: Identify frames with poor aliasing
|
||||
4. **Monitor Aliasing Barriers**: Too many can hurt performance
|
||||
|
||||
## Conclusion
|
||||
|
||||
Resource aliasing is a critical optimization in modern rendering. This proof of concept demonstrates:
|
||||
- ✅ Automatic lifetime analysis
|
||||
- ✅ First-fit allocation strategy
|
||||
- ✅ Type-safe aliasing (textures vs buffers)
|
||||
- ✅ Memory savings tracking
|
||||
- ✅ Aliasing barrier insertion points (simulated)
|
||||
|
||||
For production use, integrate with actual D3D12/Vulkan memory heaps and implement proper aliasing barriers as defined by the API specifications.
|
||||
189
Ghost.RenderGraph.Concept/API_DESIGN.md
Normal file
189
Ghost.RenderGraph.Concept/API_DESIGN.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Render Graph API Design Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This render graph implementation uses a **production-grade API design** inspired by Unity HDRP's render graph, focusing on performance and usability.
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
### 1. **Typed Pass Data > String Lookups**
|
||||
|
||||
❌ **Anti-pattern** (slow, error-prone):
|
||||
```csharp
|
||||
builder.SetRenderFunc(cmd => {
|
||||
cmd.SetRenderTarget("GBuffer.Albedo"); // String lookup!
|
||||
});
|
||||
```
|
||||
|
||||
✅ **Best practice** (fast, type-safe):
|
||||
```csharp
|
||||
builder.SetRenderFunc((data, cmd) => {
|
||||
cmd.SetRenderTarget(data.Albedo.Name); // Direct field access!
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Blackboard for Complex Data**
|
||||
|
||||
When multiple passes need the same resources (like GBuffer with albedo, normal, depth):
|
||||
|
||||
```csharp
|
||||
class GBufferData {
|
||||
public RenderGraphTextureHandle Albedo = null!;
|
||||
public RenderGraphTextureHandle Normal = null!;
|
||||
public RenderGraphTextureHandle Depth = null!;
|
||||
}
|
||||
|
||||
// Producer pass
|
||||
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer", out var data)) {
|
||||
data.Albedo = builder.WriteTexture(builder.CreateTexture(...));
|
||||
data.Normal = builder.WriteTexture(builder.CreateTexture(...));
|
||||
data.Depth = builder.UseDepthBuffer(builder.CreateTexture(...), true);
|
||||
builder.SetRenderFunc((d, cmd) => { /* use d.Albedo, d.Normal, d.Depth */ });
|
||||
}
|
||||
renderGraph.Blackboard.Add(gbufferData);
|
||||
|
||||
// Consumer passes
|
||||
using (var builder = renderGraph.AddRenderPass<LightingData>("Lighting", out var data)) {
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
data.Albedo = builder.ReadTexture(gbuffer.Albedo);
|
||||
data.Normal = builder.ReadTexture(gbuffer.Normal);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Direct Handle Passing for Simple Cases**
|
||||
|
||||
When passing a single texture between two passes:
|
||||
|
||||
```csharp
|
||||
// Pass 1: Return handle
|
||||
RenderGraphTextureHandle lightingOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<LightingData>("Lighting", out var data)) {
|
||||
lightingOutput = builder.CreateTexture(...);
|
||||
data.Output = builder.WriteTexture(lightingOutput);
|
||||
builder.SetRenderFunc((d, cmd) => { /* ... */ });
|
||||
}
|
||||
|
||||
// Pass 2: Use handle directly
|
||||
using (var builder = renderGraph.AddRenderPass<TAAData>("TAA", out var data)) {
|
||||
data.Input = builder.ReadTexture(lightingOutput); // Direct pass!
|
||||
builder.SetRenderFunc((d, cmd) => { /* ... */ });
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
| Aspect | Traditional String-Based | Typed Pass Data |
|
||||
|--------|-------------------------|-----------------|
|
||||
| **Resource Access** | Dictionary lookup | Direct field access |
|
||||
| **Type Safety** | Runtime errors | Compile-time checks |
|
||||
| **Refactoring** | Find & Replace | Compiler-assisted |
|
||||
| **IDE Support** | Limited | Full IntelliSense |
|
||||
| **Performance** | Hash lookup overhead | Zero overhead |
|
||||
|
||||
## Real-World Example
|
||||
|
||||
Here's how a complete deferred rendering pipeline looks:
|
||||
|
||||
```csharp
|
||||
// 1. GBuffer Pass - produce multiple outputs
|
||||
GBufferData gbufferData;
|
||||
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer", out gbufferData)) {
|
||||
gbufferData.Albedo = builder.WriteTexture(builder.CreateTexture(...));
|
||||
gbufferData.Normal = builder.WriteTexture(builder.CreateTexture(...));
|
||||
gbufferData.Depth = builder.UseDepthBuffer(builder.CreateTexture(...), true);
|
||||
builder.SetRenderFunc((data, cmd) => { /* render geometry */ });
|
||||
}
|
||||
renderGraph.Blackboard.Add(gbufferData);
|
||||
|
||||
// 2. Lighting Pass - consume GBuffer, produce lighting
|
||||
RenderGraphTextureHandle lightingOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<LightingData>("Lighting", out var data)) {
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
data.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo);
|
||||
data.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
|
||||
data.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
|
||||
|
||||
lightingOutput = builder.CreateTexture(...);
|
||||
data.Output = builder.WriteTexture(lightingOutput);
|
||||
builder.SetRenderFunc((data, cmd) => { /* deferred lighting */ });
|
||||
}
|
||||
|
||||
// 3. SSAO Pass - also consume GBuffer
|
||||
RenderGraphTextureHandle ssaoOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<SSAOData>("SSAO", out var data)) {
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
data.Depth = builder.ReadTexture(gbuffer.Depth);
|
||||
data.Normal = builder.ReadTexture(gbuffer.Normal);
|
||||
|
||||
ssaoOutput = builder.CreateTexture(...);
|
||||
data.Output = builder.WriteTexture(ssaoOutput);
|
||||
builder.SetRenderFunc((data, cmd) => { /* SSAO */ });
|
||||
}
|
||||
|
||||
// 4. Post Processing - combine lighting + SSAO
|
||||
using (var builder = renderGraph.AddRenderPass<PostData>("Post", out var data)) {
|
||||
data.Lighting = builder.ReadTexture(lightingOutput); // Direct handle
|
||||
data.SSAO = builder.ReadTexture(ssaoOutput); // Direct handle
|
||||
data.Output = builder.WriteTexture(backbuffer);
|
||||
builder.SetRenderFunc((data, cmd) => { /* combine */ });
|
||||
}
|
||||
|
||||
renderGraph.Compile();
|
||||
renderGraph.Execute();
|
||||
```
|
||||
|
||||
## When to Use What?
|
||||
|
||||
| Scenario | Use | Example |
|
||||
|----------|-----|---------|
|
||||
| Multiple outputs used by many passes | **Blackboard** | GBuffer (albedo, normal, depth) |
|
||||
| Single texture passed to next pass | **Direct Handle** | Lighting → TAA |
|
||||
| Temporary working data | **Pass Data** | Intermediate blur textures |
|
||||
| Persistent frame data | **Import** | Backbuffer, history textures |
|
||||
|
||||
## Comparison with Other Systems
|
||||
|
||||
### Unity HDRP
|
||||
```csharp
|
||||
// Unity's API (very similar!)
|
||||
using (var builder = renderGraph.AddRenderPass<PassData>("MyPass", out var passData))
|
||||
{
|
||||
passData.input = builder.ReadTexture(inputHandle);
|
||||
passData.output = builder.WriteTexture(outputHandle);
|
||||
builder.SetRenderFunc((data, ctx) => { /* ... */ });
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal Engine 5 RDG
|
||||
```cpp
|
||||
// Unreal's API (similar concepts, different syntax)
|
||||
FRDGTextureRef Output = GraphBuilder.CreateTexture(Desc, TEXT("Output"));
|
||||
AddPass(GraphBuilder, "MyPass", Parameters,
|
||||
[Parameters](FRHICommandList& RHICmdList) {
|
||||
// Execute
|
||||
});
|
||||
```
|
||||
|
||||
### Frostbite
|
||||
```cpp
|
||||
// Frostbite (original frame graph paper)
|
||||
FrameGraphTextureHandle output = frameGraph.create("Output", desc);
|
||||
frameGraph.addPass("MyPass",
|
||||
[&](FrameGraphBuilder& builder) {
|
||||
builder.write(output);
|
||||
},
|
||||
[=](const Resources& resources, CommandBuffer& cmd) {
|
||||
// Execute
|
||||
});
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This API design prioritizes:
|
||||
1. **Performance**: Zero-cost abstractions with direct field access
|
||||
2. **Safety**: Compile-time type checking
|
||||
3. **Ergonomics**: Natural C# patterns (using blocks, typed data)
|
||||
4. **Flexibility**: Blackboard for complex data, handles for simple cases
|
||||
|
||||
It matches industry-standard patterns from Unity, Unreal, and Frostbite while leveraging C#'s type system for maximum safety and performance.
|
||||
10
Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj
Normal file
10
Ghost.RenderGraph.Concept/Ghost.RenderGraph.Concept.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
86
Ghost.RenderGraph.Concept/ICommandBuffer.cs
Normal file
86
Ghost.RenderGraph.Concept/ICommandBuffer.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public interface ICommandBuffer
|
||||
{
|
||||
void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState);
|
||||
void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName);
|
||||
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)
|
||||
{
|
||||
Console.WriteLine($" [BARRIER] Transition '{resourceName}' from {beforeState} to {afterState}");
|
||||
}
|
||||
|
||||
public void AliasingBarrier(string beforeResourceName, string afterResourceName, string physicalAllocationName)
|
||||
{
|
||||
Console.WriteLine($" [ALIAS_BARRIER] Alias '{physicalAllocationName}': '{beforeResourceName}' -> '{afterResourceName}'");
|
||||
}
|
||||
|
||||
public void BeginRenderPass(string passName)
|
||||
{
|
||||
Console.WriteLine($" [BEGIN] RenderPass '{passName}'");
|
||||
}
|
||||
|
||||
public void EndRenderPass()
|
||||
{
|
||||
Console.WriteLine($" [END] RenderPass");
|
||||
}
|
||||
|
||||
public void SetRenderTarget(string textureName)
|
||||
{
|
||||
Console.WriteLine($" [RT] Set RenderTarget: '{textureName}'");
|
||||
}
|
||||
|
||||
public void SetDepthStencil(string textureName)
|
||||
{
|
||||
Console.WriteLine($" [DS] Set DepthStencil: '{textureName}'");
|
||||
}
|
||||
|
||||
public void BindShaderResource(string resourceName, int slot)
|
||||
{
|
||||
Console.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}");
|
||||
}
|
||||
|
||||
public void BindUnorderedAccess(string resourceName, int slot)
|
||||
{
|
||||
Console.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}");
|
||||
}
|
||||
|
||||
public void Draw(int vertexCount)
|
||||
{
|
||||
Console.WriteLine($" [DRAW] Drawing {vertexCount} vertices");
|
||||
}
|
||||
|
||||
public void Dispatch(int x, int y, int z)
|
||||
{
|
||||
Console.WriteLine($" [DISPATCH] Compute ({x}, {y}, {z})");
|
||||
}
|
||||
|
||||
public void ClearRenderTarget(string textureName, float r, float g, float b, float a)
|
||||
{
|
||||
Console.WriteLine($" [CLEAR_RT] Clear '{textureName}' to ({r}, {g}, {b}, {a})");
|
||||
}
|
||||
|
||||
public void ClearDepth(string textureName, float depth)
|
||||
{
|
||||
Console.WriteLine($" [CLEAR_DEPTH] Clear '{textureName}' to {depth}");
|
||||
}
|
||||
|
||||
public void CopyTexture(string source, string destination)
|
||||
{
|
||||
Console.WriteLine($" [COPY] Copy from '{source}' to '{destination}'");
|
||||
}
|
||||
}
|
||||
58
Ghost.RenderGraph.Concept/PassData.cs
Normal file
58
Ghost.RenderGraph.Concept/PassData.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
// Pass data structure for GBuffer outputs
|
||||
public class GBufferData
|
||||
{
|
||||
public RenderGraphTextureHandle Albedo = null!;
|
||||
public RenderGraphTextureHandle Normal = null!;
|
||||
public RenderGraphTextureHandle Depth = null!;
|
||||
}
|
||||
|
||||
public class LightingPassData
|
||||
{
|
||||
public RenderGraphTextureHandle GBufferAlbedo = null!;
|
||||
public RenderGraphTextureHandle GBufferNormal = null!;
|
||||
public RenderGraphTextureHandle GBufferDepth = null!;
|
||||
public RenderGraphTextureHandle OutputLighting = null!;
|
||||
}
|
||||
|
||||
public class SSAOPassData
|
||||
{
|
||||
public RenderGraphTextureHandle GBufferDepth = null!;
|
||||
public RenderGraphTextureHandle GBufferNormal = null!;
|
||||
public RenderGraphTextureHandle OutputSSAO = null!;
|
||||
}
|
||||
|
||||
public class TAAPassData
|
||||
{
|
||||
public RenderGraphTextureHandle InputLighting = null!;
|
||||
public RenderGraphTextureHandle OutputTAA = null!;
|
||||
}
|
||||
|
||||
public class PostProcessingPassData
|
||||
{
|
||||
public RenderGraphTextureHandle InputTAA = null!;
|
||||
public RenderGraphTextureHandle InputSSAO = null!;
|
||||
public RenderGraphTextureHandle OutputBackbuffer = null!;
|
||||
}
|
||||
|
||||
public class DebugPassData
|
||||
{
|
||||
public RenderGraphTextureHandle DebugTexture = null!;
|
||||
}
|
||||
|
||||
public class ProfilerMarkerData { }
|
||||
|
||||
public class BloomDownsampleData
|
||||
{
|
||||
public RenderGraphTextureHandle Input = null!;
|
||||
public RenderGraphTextureHandle Output = null!;
|
||||
}
|
||||
|
||||
public class PostProcessingPassDataV2
|
||||
{
|
||||
public RenderGraphTextureHandle InputTAA = null!;
|
||||
public RenderGraphTextureHandle InputSSAO = null!;
|
||||
public RenderGraphTextureHandle InputBloom = null!;
|
||||
public RenderGraphTextureHandle OutputBackbuffer = null!;
|
||||
}
|
||||
177
Ghost.RenderGraph.Concept/Program.cs
Normal file
177
Ghost.RenderGraph.Concept/Program.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Ghost.RenderGraph.Concept;
|
||||
|
||||
Console.WriteLine("==================================================");
|
||||
Console.WriteLine(" Transient Render Graph - Proof of Concept");
|
||||
Console.WriteLine(" Using Typed Pass Data and Blackboard Pattern");
|
||||
Console.WriteLine("==================================================\n");
|
||||
|
||||
var renderGraph = new RenderGraph();
|
||||
|
||||
// Import external resources
|
||||
var backbuffer = renderGraph.ImportTexture(
|
||||
"Backbuffer",
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
|
||||
|
||||
// ===== GBuffer Pass =====
|
||||
GBufferData gbufferData;
|
||||
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
|
||||
{
|
||||
// Create GBuffer textures
|
||||
var albedo = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
|
||||
var normal = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal"));
|
||||
var depth = builder.CreateTexture(new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth"));
|
||||
|
||||
// Store in pass data and mark as written
|
||||
gbufferData.Albedo = builder.WriteTexture(albedo);
|
||||
gbufferData.Normal = builder.WriteTexture(normal);
|
||||
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.SetRenderTarget(data.Albedo.Name);
|
||||
cmd.SetRenderTarget(data.Normal.Name);
|
||||
cmd.SetDepthStencil(data.Depth.Name);
|
||||
cmd.ClearRenderTarget(data.Albedo.Name, 0, 0, 0, 1);
|
||||
cmd.ClearRenderTarget(data.Normal.Name, 0.5f, 0.5f, 1.0f, 1);
|
||||
cmd.ClearDepth(data.Depth.Name, 1.0f);
|
||||
cmd.Draw(36000);
|
||||
});
|
||||
}
|
||||
|
||||
// Store GBuffer data in blackboard for other passes
|
||||
renderGraph.Blackboard.Add(gbufferData);
|
||||
|
||||
// ===== Lighting Pass =====
|
||||
RenderGraphTextureHandle lightingOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<LightingPassData>("Lighting Pass", out var lightingData))
|
||||
{
|
||||
// Read GBuffer from blackboard
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
|
||||
lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo);
|
||||
lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
|
||||
lightingData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
|
||||
|
||||
// Create output texture
|
||||
lightingOutput = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
|
||||
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
|
||||
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
||||
cmd.BindShaderResource(data.GBufferDepth.Name, 2);
|
||||
cmd.SetRenderTarget(data.OutputLighting.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== SSAO Pass =====
|
||||
RenderGraphTextureHandle ssaoOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<SSAOPassData>("SSAO Pass", out var ssaoData))
|
||||
{
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
|
||||
ssaoData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
|
||||
ssaoData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
|
||||
|
||||
// This will reuse GBuffer.Albedo's memory allocation
|
||||
ssaoOutput = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
|
||||
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
|
||||
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
||||
cmd.SetRenderTarget(data.OutputSSAO.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Bloom Downsample Pass (will alias with albedo) =====
|
||||
RenderGraphTextureHandle bloomOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<BloomDownsampleData>("Bloom Downsample", out var bloomData))
|
||||
{
|
||||
bloomData.Input = builder.ReadTexture(lightingOutput);
|
||||
|
||||
// Create a texture that will alias with SSAO (same size, same format)
|
||||
bloomOutput = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "BloomDownsample"));
|
||||
bloomData.Output = builder.WriteTexture(bloomOutput);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.Input.Name, 0);
|
||||
cmd.SetRenderTarget(data.Output.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Temporal AA Pass =====
|
||||
RenderGraphTextureHandle taaOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<TAAPassData>("Temporal AA", out var taaData))
|
||||
{
|
||||
taaData.InputLighting = builder.ReadTexture(lightingOutput);
|
||||
|
||||
taaOutput = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
|
||||
taaData.OutputTAA = builder.WriteTexture(taaOutput);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.InputLighting.Name, 0);
|
||||
cmd.SetRenderTarget(data.OutputTAA.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Post Processing Pass =====
|
||||
using (var builder = renderGraph.AddRenderPass<PostProcessingPassDataV2>("Post Processing", out var postData))
|
||||
{
|
||||
postData.InputTAA = builder.ReadTexture(taaOutput);
|
||||
postData.InputSSAO = builder.ReadTexture(ssaoOutput);
|
||||
postData.InputBloom = builder.ReadTexture(bloomOutput);
|
||||
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.InputTAA.Name, 0);
|
||||
cmd.BindShaderResource(data.InputSSAO.Name, 1);
|
||||
cmd.BindShaderResource(data.InputBloom.Name, 2);
|
||||
cmd.SetRenderTarget(data.OutputBackbuffer.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== GPU Profiler Marker Pass (non-cullable, textureless) =====
|
||||
using (var builder = renderGraph.AddRenderPass<ProfilerMarkerData>("GPU Profiler Begin Frame", out var profilerData))
|
||||
{
|
||||
builder.SetAllowCulling(false); // Never cull this - it's for debugging/profiling
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
Console.WriteLine(" [PROFILER] BeginEvent('Frame')");
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Unused Debug Pass (will be culled) =====
|
||||
using (var builder = renderGraph.AddRenderPass<DebugPassData>("Unused Debug Pass", out var debugData))
|
||||
{
|
||||
debugData.DebugTexture = builder.WriteTexture(
|
||||
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.SetRenderTarget(data.DebugTexture.Name);
|
||||
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
|
||||
cmd.Draw(100);
|
||||
});
|
||||
}
|
||||
|
||||
// Compile and execute the render graph
|
||||
renderGraph.Compile();
|
||||
renderGraph.Execute();
|
||||
|
||||
Console.WriteLine("\nPress any key to exit...");
|
||||
Console.ReadKey();
|
||||
306
Ghost.RenderGraph.Concept/README.md
Normal file
306
Ghost.RenderGraph.Concept/README.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Transient Render Graph - Proof of Concept
|
||||
|
||||
This is a high-level proof of concept implementation of a modern transient render graph system, inspired by:
|
||||
- **Unreal Engine 5 RDG** (Render Dependency Graph)
|
||||
- **Frostbite Frame Graph**
|
||||
- **Unity HDRP Render Graph**
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Resource Virtualization**
|
||||
Resources are declared during setup but not physically created until execution. This allows the graph to analyze the entire frame before committing to resource allocation.
|
||||
|
||||
### 2. **Automatic Resource Lifetime Management**
|
||||
- Resources are created only when first needed (at their `FirstUse` pass)
|
||||
- Resources are destroyed immediately after their last use (at their `LastUse` pass)
|
||||
- Imported resources (like backbuffer) are never destroyed by the graph
|
||||
|
||||
### 3. **Automatic Barrier Insertion**
|
||||
The graph automatically inserts resource state transitions based on how resources are used:
|
||||
- Write operations: `RenderTarget`, `DepthWrite`, `UnorderedAccess`, `CopyDest`
|
||||
- Read operations: `ShaderResource`, `DepthRead`, `CopySource`
|
||||
|
||||
### 4. **Automatic Pass Dependencies**
|
||||
Dependencies are automatically inferred from resource usage patterns:
|
||||
- **Read-After-Write (RAW)**: Pass B reads what Pass A wrote
|
||||
- **Write-After-Read (WAR)**: Pass B writes to what Pass A read
|
||||
- **Write-After-Write (WAW)**: Pass B writes to what Pass A wrote
|
||||
|
||||
### 5. **Pass Culling**
|
||||
Passes that don't contribute to the final output are automatically culled:
|
||||
- Starts from imported resources (outputs)
|
||||
- Recursively marks all dependent passes as needed
|
||||
- Unused passes are not executed
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Classes
|
||||
|
||||
#### `RenderGraph`
|
||||
The main orchestrator that manages the entire frame graph:
|
||||
- **Setup Phase**: Declare resources and passes
|
||||
- **Compile Phase**: Build dependencies, cull passes, analyze lifetimes
|
||||
- **Execute Phase**: Create resources, insert barriers, execute passes, destroy resources
|
||||
|
||||
#### `RenderGraphResourceHandle`
|
||||
Handle representing a virtual resource:
|
||||
- `RenderGraphTextureHandle`: Textures with format, size
|
||||
- `RenderGraphBufferHandle`: Buffers with size
|
||||
|
||||
#### `IRenderGraphBuilder`
|
||||
Builder interface used during pass setup:
|
||||
- `ReadTexture()` / `ReadBuffer()`: Declare read access
|
||||
- `WriteTexture()` / `WriteBuffer()`: Declare write access
|
||||
- `CreateTransientTexture()` / `CreateTransientBuffer()`: Create temporary resources
|
||||
|
||||
#### `ICommandBuffer`
|
||||
Abstraction for executing graphics commands (simulated with `Console.WriteLine`)
|
||||
|
||||
### Resource Lifetime Example
|
||||
|
||||
```
|
||||
GBuffer.Albedo: [0..1] Created in pass 0, destroyed after pass 1
|
||||
GBuffer.Normal: [0..2] Created in pass 0, destroyed after pass 2
|
||||
GBuffer.Depth: [0..2] Created in pass 0, destroyed after pass 2
|
||||
LightingResult: [1..3] Created in pass 1, destroyed after pass 3
|
||||
SSAO: [2..4] Created in pass 2, destroyed after pass 4
|
||||
TAA.Result: [3..4] Created in pass 3, destroyed after pass 4
|
||||
Backbuffer: [4..4] Imported (never created/destroyed)
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
### Pattern 1: Using Typed Pass Data with Blackboard (Recommended)
|
||||
|
||||
```csharp
|
||||
var renderGraph = new RenderGraph();
|
||||
|
||||
// Import external resources (e.g., backbuffer)
|
||||
var backbuffer = renderGraph.ImportTexture(
|
||||
"Backbuffer",
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8));
|
||||
|
||||
// Define pass data structure
|
||||
class GBufferData
|
||||
{
|
||||
public RenderGraphTextureHandle Albedo = null!;
|
||||
public RenderGraphTextureHandle Normal = null!;
|
||||
public RenderGraphTextureHandle Depth = null!;
|
||||
}
|
||||
|
||||
// Create GBuffer pass with typed data
|
||||
GBufferData gbufferData;
|
||||
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
|
||||
{
|
||||
// Create transient resources
|
||||
var albedo = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "GBuffer.Albedo"));
|
||||
var normal = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "GBuffer.Normal"));
|
||||
var depth = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.Depth32F, "GBuffer.Depth"));
|
||||
|
||||
// Store in pass data and declare access
|
||||
gbufferData.Albedo = builder.WriteTexture(albedo);
|
||||
gbufferData.Normal = builder.WriteTexture(normal);
|
||||
gbufferData.Depth = builder.UseDepthBuffer(depth, writeAccess: true);
|
||||
|
||||
// Set render function with typed data
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.SetRenderTarget(data.Albedo.Name);
|
||||
cmd.SetRenderTarget(data.Normal.Name);
|
||||
cmd.SetDepthStencil(data.Depth.Name);
|
||||
cmd.Draw(36000);
|
||||
});
|
||||
}
|
||||
|
||||
// Store in blackboard for other passes
|
||||
renderGraph.Blackboard.Add(gbufferData);
|
||||
|
||||
// ===== Lighting Pass =====
|
||||
class LightingPassData
|
||||
{
|
||||
public RenderGraphTextureHandle GBufferAlbedo = null!;
|
||||
public RenderGraphTextureHandle GBufferNormal = null!;
|
||||
public RenderGraphTextureHandle OutputLighting = null!;
|
||||
}
|
||||
|
||||
RenderGraphTextureHandle lightingOutput;
|
||||
using (var builder = renderGraph.AddRenderPass<LightingPassData>("Lighting Pass", out var lightingData))
|
||||
{
|
||||
// Read GBuffer from blackboard
|
||||
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
|
||||
|
||||
lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo);
|
||||
lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
|
||||
|
||||
// Create and return output handle
|
||||
lightingOutput = builder.CreateTexture(
|
||||
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "Lighting"));
|
||||
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
|
||||
|
||||
builder.SetRenderFunc((data, cmd) =>
|
||||
{
|
||||
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
|
||||
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
|
||||
cmd.SetRenderTarget(data.OutputLighting.Name);
|
||||
cmd.Draw(3);
|
||||
});
|
||||
}
|
||||
|
||||
// Compile and execute
|
||||
renderGraph.Compile();
|
||||
renderGraph.Execute();
|
||||
```
|
||||
|
||||
### Pattern 2: Simple Handle Passing
|
||||
|
||||
For simple cases where you just need to pass one or two textures between passes, you can skip the blackboard:
|
||||
|
||||
```csharp
|
||||
// Create and return a handle
|
||||
RenderGraphTextureHandle myTexture;
|
||||
using (var builder = renderGraph.AddRenderPass<MyPassData>("Pass 1", out var data))
|
||||
{
|
||||
myTexture = builder.CreateTexture(new TextureDescriptor(...));
|
||||
data.Output = builder.WriteTexture(myTexture);
|
||||
builder.SetRenderFunc((d, cmd) => { /* ... */ });
|
||||
}
|
||||
|
||||
// Use the handle in next pass
|
||||
using (var builder = renderGraph.AddRenderPass<NextPassData>("Pass 2", out var data))
|
||||
{
|
||||
data.Input = builder.ReadTexture(myTexture);
|
||||
builder.SetRenderFunc((d, cmd) => { /* ... */ });
|
||||
}
|
||||
```
|
||||
|
||||
## Key API Design Features
|
||||
|
||||
### 1. **Typed Pass Data**
|
||||
Each pass has a strongly-typed data structure that holds all its resource handles. This:
|
||||
- Makes resource dependencies explicit and compile-time safe
|
||||
- Avoids string-based lookups during execution
|
||||
- Enables better IDE support and refactoring
|
||||
|
||||
### 2. **Blackboard Pattern**
|
||||
For sharing data structures between multiple passes:
|
||||
- Store complex data (like GBuffer with multiple textures) in the blackboard
|
||||
- Other passes retrieve it type-safely
|
||||
- Useful for resources used by many passes
|
||||
|
||||
### 3. **Direct Handle Passing**
|
||||
For simple cases:
|
||||
- Return a `RenderGraphTextureHandle` from a pass setup
|
||||
- Pass it directly to the next pass
|
||||
- No need for blackboard overhead
|
||||
|
||||
### 4. **Using Block Pattern**
|
||||
The `using` statement automatically commits the pass when the block ends:
|
||||
- Builder is disposed → pass is committed to the graph
|
||||
- Ensures all passes are properly registered
|
||||
- Mimics Unity HDRP's render graph API
|
||||
|
||||
## Benefits of Transient Resources
|
||||
|
||||
1. **Memory Efficiency**: Resources only exist when needed, allowing memory reuse
|
||||
2. **Automatic Synchronization**: Barriers inserted automatically based on usage
|
||||
3. **Self-Documenting**: Clear declaration of what each pass reads/writes
|
||||
4. **Type Safety**: Compile-time checking of pass data structures
|
||||
5. **Performance**: No string lookups or dictionary access during execution
|
||||
6. **Optimization Opportunities**: Graph can reorder passes (future work)
|
||||
7. **Resource Aliasing**: Multiple transient resources can share memory (future work)
|
||||
|
||||
## What's NOT Implemented (Intentionally)
|
||||
|
||||
This is a proof of concept focusing on core graph mechanics. Some features are fully implemented, others are intentionally omitted:
|
||||
|
||||
### ✅ Fully Implemented
|
||||
- ✅ **Resource aliasing/memory pooling** - Automatic memory reuse for non-overlapping lifetimes
|
||||
- ✅ **Typed pass data** - Zero-cost abstraction with compile-time safety
|
||||
- ✅ **Blackboard pattern** - Type-safe data sharing between passes
|
||||
- ✅ **Automatic barriers** - State transitions inferred from usage
|
||||
|
||||
### ❌ Not Implemented
|
||||
- ❌ Async compute queues
|
||||
- ❌ Pass reordering optimization
|
||||
- ❌ Subresource tracking (mip levels, array slices)
|
||||
- ❌ Multi-queue synchronization
|
||||
- ❌ GPU timeline profiling
|
||||
- ❌ Resource versioning
|
||||
- ❌ Graph visualization/debugging tools
|
||||
|
||||
## Resource Aliasing (Implemented!)
|
||||
|
||||
Transient resources with non-overlapping lifetimes automatically share the same physical memory:
|
||||
```
|
||||
GBuffer.Albedo [0..1] ━━━━━━━━━━━
|
||||
╰──> Reuse memory (34% savings!)
|
||||
SSAO [2..4] ━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
Example output:
|
||||
```
|
||||
[RG] Memory Statistics:
|
||||
Total memory without aliasing: 72.19 MB
|
||||
Total memory with aliasing: 47.46 MB
|
||||
Memory saved: 24.73 MB (34.3%)
|
||||
Allocations: 4 physical allocations for 7 resources
|
||||
```
|
||||
|
||||
See [ALIASING.md](ALIASING.md) for detailed documentation.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Async Compute
|
||||
Some passes can run on compute queue while graphics queue continues:
|
||||
```
|
||||
Graphics: ━━[GBuffer]━━[Lighting]━━━━━━━━[PostFX]━━
|
||||
║
|
||||
Compute: ╚═[SSAO]════╗
|
||||
║
|
||||
[Wait]───────╝
|
||||
```
|
||||
|
||||
### Pass Reordering
|
||||
Independent passes can be reordered for better GPU utilization or to enable more aliasing.
|
||||
|
||||
## Output Example
|
||||
|
||||
The demo program creates a deferred rendering pipeline and produces output like:
|
||||
|
||||
```
|
||||
[RG] Building pass dependencies...
|
||||
Pass 'Lighting Pass' depends on 'GBuffer Pass'
|
||||
Pass 'SSAO Pass' depends on 'GBuffer Pass'
|
||||
Pass 'Temporal AA' depends on 'Lighting Pass'
|
||||
Pass 'Post Processing' depends on 'TAA' and 'SSAO'
|
||||
|
||||
[RG] Culling unused passes...
|
||||
Culled unused pass: 'Unused Debug Pass'
|
||||
|
||||
[RG] Resource lifetimes:
|
||||
'GBuffer.Albedo': [0..1] (GBuffer Pass, Lighting Pass)
|
||||
'GBuffer.Normal': [0..2] (GBuffer Pass, Lighting Pass, SSAO Pass)
|
||||
...
|
||||
|
||||
[PASS 0] Executing: 'GBuffer Pass'
|
||||
[CREATE] Texture 'GBuffer.Albedo' (1920x1080, RGBA8)
|
||||
[BARRIER] Transition 'GBuffer.Albedo' from Undefined to RenderTarget
|
||||
[BEGIN] RenderPass 'GBuffer Pass'
|
||||
[RT] Set RenderTarget: 'GBuffer.Albedo'
|
||||
[DRAW] Drawing 36000 vertices
|
||||
[END] RenderPass
|
||||
|
||||
[PASS 1] Executing: 'Lighting Pass'
|
||||
[BARRIER] Transition 'GBuffer.Albedo' from RenderTarget to ShaderResource
|
||||
...
|
||||
[DESTROY] Resource 'GBuffer.Albedo'
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This proof of concept demonstrates the core principles of modern transient render graphs. The system automatically manages resource lifetimes, inserts synchronization barriers, builds dependency DAGs, and culls unused work—all from high-level declarative pass descriptions.
|
||||
|
||||
The architecture is designed to be extended with real graphics API integration (D3D12, Vulkan) while maintaining the same high-level interface.
|
||||
415
Ghost.RenderGraph.Concept/RenderGraph.cs
Normal file
415
Ghost.RenderGraph.Concept/RenderGraph.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public class RenderGraph
|
||||
{
|
||||
private int _resourceIdCounter = 0;
|
||||
private int _passCounter = 0;
|
||||
|
||||
private readonly List<RenderGraphResourceHandle> _resources = new();
|
||||
private readonly List<RenderGraphPass> _passes = new();
|
||||
|
||||
// Use List instead of Dictionary since resource IDs are sequential (0, 1, 2, ...)
|
||||
private readonly List<ResourceLifetime> _resourceLifetimes = new();
|
||||
private readonly List<ResourceState> _currentResourceStates = new();
|
||||
private readonly List<int> _resourceToAllocationMap = new();
|
||||
|
||||
private readonly Dictionary<int, RenderGraphResourceHandle?> _allocationActiveResource = new();
|
||||
private readonly RenderGraphBlackboard _blackboard = new();
|
||||
private readonly ResourceAllocator _allocator = new();
|
||||
|
||||
public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor)
|
||||
{
|
||||
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, name, descriptor, isImported: true);
|
||||
_resources.Add(handle);
|
||||
_resourceLifetimes.Add(new ResourceLifetime(handle));
|
||||
_currentResourceStates.Add(ResourceState.Undefined);
|
||||
_resourceToAllocationMap.Add(-1); // -1 means no allocation
|
||||
Console.WriteLine($"[RG] Import Texture: '{name}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphBufferHandle ImportBuffer(string name, BufferDescriptor descriptor)
|
||||
{
|
||||
var handle = new RenderGraphBufferHandle(_resourceIdCounter++, name, descriptor, isImported: true);
|
||||
_resources.Add(handle);
|
||||
_resourceLifetimes.Add(new ResourceLifetime(handle));
|
||||
_currentResourceStates.Add(ResourceState.Undefined);
|
||||
_resourceToAllocationMap.Add(-1);
|
||||
Console.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)");
|
||||
return handle;
|
||||
}
|
||||
|
||||
internal RenderGraphTextureHandle CreateTransientTexture(TextureDescriptor descriptor)
|
||||
{
|
||||
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
|
||||
_resources.Add(handle);
|
||||
_resourceLifetimes.Add(new ResourceLifetime(handle));
|
||||
_currentResourceStates.Add(ResourceState.Undefined);
|
||||
_resourceToAllocationMap.Add(-1);
|
||||
Console.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
|
||||
return handle;
|
||||
}
|
||||
|
||||
internal RenderGraphBufferHandle CreateTransientBuffer(BufferDescriptor descriptor)
|
||||
{
|
||||
var handle = new RenderGraphBufferHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
|
||||
_resources.Add(handle);
|
||||
_resourceLifetimes.Add(new ResourceLifetime(handle));
|
||||
_currentResourceStates.Add(ResourceState.Undefined);
|
||||
_resourceToAllocationMap.Add(-1);
|
||||
Console.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)");
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphBlackboard Blackboard => _blackboard;
|
||||
|
||||
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
|
||||
{
|
||||
return CreateTransientTexture(descriptor);
|
||||
}
|
||||
|
||||
public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor)
|
||||
{
|
||||
return CreateTransientBuffer(descriptor);
|
||||
}
|
||||
|
||||
public RenderGraphPassBuilder<TPassData> AddRenderPass<TPassData>(string name, out TPassData passData)
|
||||
where TPassData : class, new()
|
||||
{
|
||||
var builder = new RenderGraphPassBuilder<TPassData>(this, name, _passCounter);
|
||||
passData = builder.PassData;
|
||||
return builder;
|
||||
}
|
||||
|
||||
internal void CommitPass<TPassData>(RenderGraphPassBuilder<TPassData> builder, string name)
|
||||
where TPassData : class, new()
|
||||
{
|
||||
if (builder.RenderFunc == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Pass '{name}' has no render function set. Call SetRenderFunc() on the builder.");
|
||||
}
|
||||
|
||||
var pass = new RenderGraphPass<TPassData>(
|
||||
name,
|
||||
_passCounter++,
|
||||
builder.PassData,
|
||||
builder.RenderFunc,
|
||||
builder.ResourceAccesses.ToList(),
|
||||
builder.AllowCulling);
|
||||
|
||||
_passes.Add(pass);
|
||||
|
||||
foreach (var (handle, state) in pass.ResourceAccesses)
|
||||
{
|
||||
_resourceLifetimes[handle.Id].AddUsage(state, pass.Index);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})");
|
||||
foreach (var (handle, state) in pass.ResourceAccesses)
|
||||
{
|
||||
Console.WriteLine($" - {state}: '{handle.Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
public void Compile()
|
||||
{
|
||||
Console.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH ==========");
|
||||
|
||||
BuildDependencies();
|
||||
CullUnusedPasses();
|
||||
AnalyzeResourceLifetimes();
|
||||
AllocatePhysicalResources();
|
||||
}
|
||||
|
||||
private void AllocatePhysicalResources()
|
||||
{
|
||||
// Pass as IReadOnlyList since it's now a List
|
||||
_allocator.AllocateResources(_resourceLifetimes, _passes);
|
||||
|
||||
// Build mapping from virtual resource to physical allocation
|
||||
foreach (var allocation in _allocator.Allocations)
|
||||
{
|
||||
foreach (var resource in allocation.AliasedResources)
|
||||
{
|
||||
_resourceToAllocationMap[resource.Id] = allocation.AllocationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildDependencies()
|
||||
{
|
||||
Console.WriteLine("\n[RG] Building pass dependencies...");
|
||||
|
||||
for (int i = 0; i < _passes.Count; i++)
|
||||
{
|
||||
var pass = _passes[i];
|
||||
|
||||
var writtenResources = pass.ResourceAccesses
|
||||
.Where(access => IsWriteState(access.state))
|
||||
.Select(access => access.handle.Id)
|
||||
.ToHashSet();
|
||||
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
var previousPass = _passes[j];
|
||||
|
||||
var hasReadAfterWrite = previousPass.ResourceAccesses
|
||||
.Where(access => IsWriteState(access.state))
|
||||
.Any(access => pass.ResourceAccesses.Any(
|
||||
current => current.handle.Id == access.handle.Id && IsReadState(current.state)));
|
||||
|
||||
var hasWriteAfterRead = pass.ResourceAccesses
|
||||
.Where(access => IsWriteState(access.state))
|
||||
.Any(access => previousPass.ResourceAccesses.Any(
|
||||
prev => prev.handle.Id == access.handle.Id && IsReadState(prev.state)));
|
||||
|
||||
var hasWriteAfterWrite = previousPass.ResourceAccesses
|
||||
.Where(access => IsWriteState(access.state))
|
||||
.Any(access => writtenResources.Contains(access.handle.Id));
|
||||
|
||||
if (hasReadAfterWrite || hasWriteAfterRead || hasWriteAfterWrite)
|
||||
{
|
||||
if (!pass.Dependencies.Contains(j))
|
||||
{
|
||||
pass.Dependencies.Add(j);
|
||||
Console.WriteLine($" Pass '{pass.Name}' depends on '{previousPass.Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CullUnusedPasses()
|
||||
{
|
||||
Console.WriteLine("\n[RG] Culling unused passes...");
|
||||
|
||||
// Mark passes that contribute to imported resources or don't allow culling
|
||||
foreach (var pass in _passes)
|
||||
{
|
||||
foreach (var (handle, _) in pass.ResourceAccesses)
|
||||
{
|
||||
if (handle.IsImported)
|
||||
{
|
||||
pass.RefCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark passes that don't allow culling (synchronization, debug, etc.)
|
||||
if (!pass.AllowCulling)
|
||||
{
|
||||
pass.RefCount++;
|
||||
Console.WriteLine($" Pass '{pass.Name}' marked as non-cullable");
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate reference counts through dependencies
|
||||
bool changed = true;
|
||||
while (changed)
|
||||
{
|
||||
changed = false;
|
||||
foreach (var pass in _passes)
|
||||
{
|
||||
if (pass.RefCount > 0)
|
||||
{
|
||||
foreach (var depIndex in pass.Dependencies)
|
||||
{
|
||||
var depPass = _passes[depIndex];
|
||||
if (depPass.RefCount == 0)
|
||||
{
|
||||
depPass.RefCount++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var culledPasses = _passes.Where(p => p.RefCount == 0 && p.AllowCulling).ToList();
|
||||
if (culledPasses.Count != 0)
|
||||
{
|
||||
foreach (var pass in culledPasses)
|
||||
{
|
||||
Console.WriteLine($" Culled unused pass: '{pass.Name}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" No passes culled.");
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeResourceLifetimes()
|
||||
{
|
||||
Console.WriteLine("\n[RG] Resource lifetimes:");
|
||||
|
||||
foreach (var lifetime in _resourceLifetimes)
|
||||
{
|
||||
if (lifetime.FirstUse == int.MaxValue)
|
||||
{
|
||||
Console.WriteLine($" '{lifetime.Handle.Name}': UNUSED");
|
||||
}
|
||||
else
|
||||
{
|
||||
var passNames = _passes
|
||||
.Where(p => p.Index >= lifetime.FirstUse && p.Index <= lifetime.LastUse && p.RefCount > 0)
|
||||
.Select(p => p.Name);
|
||||
Console.WriteLine($" '{lifetime.Handle.Name}': [{lifetime.FirstUse}..{lifetime.LastUse}] ({string.Join(", ", passNames)})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
Console.WriteLine("\n[RG] ========== EXECUTING RENDER GRAPH ==========\n");
|
||||
|
||||
var commandBuffer = new SimulatedCommandBuffer();
|
||||
|
||||
foreach (var pass in _passes.Where(p => p.RefCount > 0).OrderBy(p => p.Index))
|
||||
{
|
||||
Console.WriteLine($"[PASS {pass.Index}] Executing: '{pass.Name}'");
|
||||
|
||||
var lifetime = _resourceLifetimes
|
||||
.Where(lt => lt.FirstUse == pass.Index)
|
||||
.ToList();
|
||||
|
||||
foreach (var lt in lifetime)
|
||||
{
|
||||
if (!lt.Handle.IsImported)
|
||||
{
|
||||
CreateResource(lt.Handle);
|
||||
}
|
||||
}
|
||||
|
||||
InsertBarriers(pass, commandBuffer);
|
||||
|
||||
commandBuffer.BeginRenderPass(pass.Name);
|
||||
pass.Execute(commandBuffer);
|
||||
commandBuffer.EndRenderPass();
|
||||
|
||||
var endLifetime = _resourceLifetimes
|
||||
.Where(lt => lt.LastUse == pass.Index && !lt.Handle.IsImported)
|
||||
.ToList();
|
||||
|
||||
foreach (var lt in endLifetime)
|
||||
{
|
||||
DestroyResource(lt.Handle);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
Console.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n");
|
||||
}
|
||||
|
||||
private void CreateResource(RenderGraphResourceHandle handle)
|
||||
{
|
||||
var allocation = _allocator.GetAllocation(handle);
|
||||
if (allocation != null)
|
||||
{
|
||||
if (handle is RenderGraphTextureHandle textureHandle)
|
||||
{
|
||||
var desc = textureHandle.Descriptor;
|
||||
Console.WriteLine($" [CREATE] Texture '{handle.Name}' using '{allocation.DebugName}' " +
|
||||
$"({desc.Width}x{desc.Height}, {desc.Format}, offset: {allocation.OffsetInBytes})");
|
||||
}
|
||||
else if (handle is RenderGraphBufferHandle bufferHandle)
|
||||
{
|
||||
var desc = bufferHandle.Descriptor;
|
||||
Console.WriteLine($" [CREATE] Buffer '{handle.Name}' using '{allocation.DebugName}' " +
|
||||
$"({desc.SizeInBytes} bytes, offset: {allocation.OffsetInBytes})");
|
||||
}
|
||||
|
||||
// Note: We do NOT set _allocationActiveResource here
|
||||
// That happens in InsertBarriers when the resource is first accessed
|
||||
}
|
||||
else
|
||||
{
|
||||
if (handle is RenderGraphTextureHandle textureHandle)
|
||||
{
|
||||
var desc = textureHandle.Descriptor;
|
||||
Console.WriteLine($" [CREATE] Texture '{handle.Name}' ({desc.Width}x{desc.Height}, {desc.Format})");
|
||||
}
|
||||
else if (handle is RenderGraphBufferHandle bufferHandle)
|
||||
{
|
||||
var desc = bufferHandle.Descriptor;
|
||||
Console.WriteLine($" [CREATE] Buffer '{handle.Name}' ({desc.SizeInBytes} bytes)");
|
||||
}
|
||||
}
|
||||
|
||||
_currentResourceStates[handle.Id] = ResourceState.Undefined;
|
||||
}
|
||||
|
||||
private void DestroyResource(RenderGraphResourceHandle handle)
|
||||
{
|
||||
Console.WriteLine($" [DESTROY] Resource '{handle.Name}'");
|
||||
_currentResourceStates[handle.Id] = ResourceState.Undefined;
|
||||
|
||||
// Note: We intentionally DO NOT clear _allocationActiveResource here
|
||||
// The allocation remains "owned" by this resource until another resource aliases it
|
||||
// This allows us to track aliasing barriers correctly
|
||||
}
|
||||
|
||||
private void InsertBarriers(RenderGraphPass pass, ICommandBuffer commandBuffer)
|
||||
{
|
||||
foreach (var (handle, targetState) in pass.ResourceAccesses)
|
||||
{
|
||||
// Check if this resource shares a physical allocation
|
||||
var allocation = _allocator.GetAllocation(handle);
|
||||
if (allocation != null)
|
||||
{
|
||||
// Check what resource is currently active on this allocation
|
||||
if (_allocationActiveResource.TryGetValue(allocation.AllocationId, out var activeResource))
|
||||
{
|
||||
// If a different resource is currently active on this allocation, insert aliasing barrier
|
||||
if (activeResource != null && activeResource.Id != handle.Id)
|
||||
{
|
||||
commandBuffer.AliasingBarrier(activeResource.Name, handle.Name, allocation.DebugName);
|
||||
|
||||
// Clear state for the old resource since it's being aliased away
|
||||
_currentResourceStates[activeResource.Id] = ResourceState.Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the active resource for this allocation
|
||||
_allocationActiveResource[allocation.AllocationId] = handle;
|
||||
}
|
||||
|
||||
var currentState = _currentResourceStates[handle.Id];
|
||||
|
||||
if (currentState != targetState)
|
||||
{
|
||||
commandBuffer.ResourceBarrier(handle.Name, currentState, targetState);
|
||||
_currentResourceStates[handle.Id] = targetState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWriteState(ResourceState state)
|
||||
{
|
||||
return state.HasFlag(ResourceState.RenderTarget) ||
|
||||
state.HasFlag(ResourceState.DepthWrite) ||
|
||||
state.HasFlag(ResourceState.UnorderedAccess) ||
|
||||
state.HasFlag(ResourceState.CopyDest);
|
||||
}
|
||||
|
||||
private static bool IsReadState(ResourceState state)
|
||||
{
|
||||
return state.HasFlag(ResourceState.ShaderResource) ||
|
||||
state.HasFlag(ResourceState.DepthRead) ||
|
||||
state.HasFlag(ResourceState.CopySource);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_passes.Clear();
|
||||
_resources.Clear();
|
||||
_resourceLifetimes.Clear();
|
||||
_currentResourceStates.Clear();
|
||||
_resourceToAllocationMap.Clear();
|
||||
_allocationActiveResource.Clear();
|
||||
_blackboard.Clear();
|
||||
_passCounter = 0;
|
||||
_resourceIdCounter = 0;
|
||||
Console.WriteLine("[RG] Render graph reset.");
|
||||
}
|
||||
}
|
||||
36
Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs
Normal file
36
Ghost.RenderGraph.Concept/RenderGraphBlackboard.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public class RenderGraphBlackboard
|
||||
{
|
||||
private readonly Dictionary<Type, object> _data = new();
|
||||
|
||||
public void Add<T>(T data) where T : class
|
||||
{
|
||||
_data[typeof(T)] = data;
|
||||
}
|
||||
|
||||
public T Get<T>() where T : class
|
||||
{
|
||||
if (_data.TryGetValue(typeof(T), out var data))
|
||||
{
|
||||
return (T)data;
|
||||
}
|
||||
throw new KeyNotFoundException($"Data of type {typeof(T).Name} not found in blackboard.");
|
||||
}
|
||||
|
||||
public bool TryGet<T>(out T? data) where T : class
|
||||
{
|
||||
if (_data.TryGetValue(typeof(T), out var obj))
|
||||
{
|
||||
data = (T)obj;
|
||||
return true;
|
||||
}
|
||||
data = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_data.Clear();
|
||||
}
|
||||
}
|
||||
41
Ghost.RenderGraph.Concept/RenderGraphExtensions.cs
Normal file
41
Ghost.RenderGraph.Concept/RenderGraphExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public static class RenderGraphExtensions
|
||||
{
|
||||
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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Ghost.RenderGraph.Concept/RenderGraphPass.cs
Normal file
50
Ghost.RenderGraph.Concept/RenderGraphPass.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
internal abstract class RenderGraphPass
|
||||
{
|
||||
public string Name { get; }
|
||||
public int Index { get; }
|
||||
public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; }
|
||||
public List<int> Dependencies { get; } = new();
|
||||
public int RefCount { get; set; } = 0;
|
||||
public bool AllowCulling { get; }
|
||||
|
||||
protected RenderGraphPass(
|
||||
string name,
|
||||
int index,
|
||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
||||
bool allowCulling)
|
||||
{
|
||||
Name = name;
|
||||
Index = index;
|
||||
ResourceAccesses = resourceAccesses;
|
||||
AllowCulling = allowCulling;
|
||||
}
|
||||
|
||||
public abstract void Execute(ICommandBuffer commandBuffer);
|
||||
}
|
||||
|
||||
internal class RenderGraphPass<TPassData> : RenderGraphPass
|
||||
where TPassData : class
|
||||
{
|
||||
public TPassData PassData { get; }
|
||||
public Action<TPassData, ICommandBuffer> RenderFunc { get; }
|
||||
|
||||
public RenderGraphPass(
|
||||
string name,
|
||||
int index,
|
||||
TPassData passData,
|
||||
Action<TPassData, ICommandBuffer> renderFunc,
|
||||
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
|
||||
bool allowCulling)
|
||||
: base(name, index, resourceAccesses, allowCulling)
|
||||
{
|
||||
PassData = passData;
|
||||
RenderFunc = renderFunc;
|
||||
}
|
||||
|
||||
public override void Execute(ICommandBuffer commandBuffer)
|
||||
{
|
||||
RenderFunc(PassData, commandBuffer);
|
||||
}
|
||||
}
|
||||
105
Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs
Normal file
105
Ghost.RenderGraph.Concept/RenderGraphPassBuilder.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
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 class RenderGraphPassBuilder<TPassData> : IRenderGraphBuilder, IDisposable
|
||||
where TPassData : class, new()
|
||||
{
|
||||
private readonly RenderGraph _graph;
|
||||
private readonly string _passName;
|
||||
private readonly int _passIndex;
|
||||
private readonly List<(RenderGraphResourceHandle handle, ResourceState state)> _resourceAccesses = new();
|
||||
private Action<TPassData, ICommandBuffer>? _renderFunc;
|
||||
private bool _committed;
|
||||
private bool _allowCulling = true;
|
||||
|
||||
public TPassData PassData { get; }
|
||||
|
||||
internal RenderGraphPassBuilder(RenderGraph graph, string passName, int passIndex)
|
||||
{
|
||||
_graph = graph;
|
||||
_passName = passName;
|
||||
_passIndex = passIndex;
|
||||
PassData = new TPassData();
|
||||
}
|
||||
|
||||
internal IReadOnlyList<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses => _resourceAccesses;
|
||||
internal Action<TPassData, ICommandBuffer>? RenderFunc => _renderFunc;
|
||||
internal bool AllowCulling => _allowCulling;
|
||||
|
||||
public RenderGraphTextureHandle ReadTexture(RenderGraphTextureHandle handle)
|
||||
{
|
||||
_resourceAccesses.Add((handle, ResourceState.ShaderResource));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
|
||||
{
|
||||
_resourceAccesses.Add((handle, ResourceState.RenderTarget));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
|
||||
{
|
||||
_resourceAccesses.Add((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, ResourceState.ShaderResource));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle)
|
||||
{
|
||||
_resourceAccesses.Add((handle, ResourceState.UnorderedAccess));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor)
|
||||
{
|
||||
var handle = _graph.CreateTransientBuffer(descriptor);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public void SetRenderFunc(Action<TPassData, ICommandBuffer> renderFunc)
|
||||
{
|
||||
_renderFunc = renderFunc;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs
Normal file
41
Ghost.RenderGraph.Concept/RenderGraphResourceHandle.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public class RenderGraphResourceHandle
|
||||
{
|
||||
internal int Id { get; }
|
||||
internal ResourceType Type { get; }
|
||||
internal string Name { get; }
|
||||
internal bool IsImported { get; }
|
||||
|
||||
internal RenderGraphResourceHandle(int id, ResourceType type, string name, bool isImported)
|
||||
{
|
||||
Id = id;
|
||||
Type = type;
|
||||
Name = name;
|
||||
IsImported = isImported;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public sealed class RenderGraphTextureHandle : RenderGraphResourceHandle
|
||||
{
|
||||
internal TextureDescriptor Descriptor { get; }
|
||||
|
||||
internal RenderGraphTextureHandle(int id, string name, TextureDescriptor descriptor, bool isImported)
|
||||
: base(id, ResourceType.Texture, name, isImported)
|
||||
{
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RenderGraphBufferHandle : RenderGraphResourceHandle
|
||||
{
|
||||
internal BufferDescriptor Descriptor { get; }
|
||||
|
||||
internal RenderGraphBufferHandle(int id, string name, BufferDescriptor descriptor, bool isImported)
|
||||
: base(id, ResourceType.Buffer, name, isImported)
|
||||
{
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
}
|
||||
213
Ghost.RenderGraph.Concept/ResourceAllocator.cs
Normal file
213
Ghost.RenderGraph.Concept/ResourceAllocator.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a physical memory allocation that can be shared by multiple transient resources
|
||||
/// </summary>
|
||||
internal class 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 IReadOnlyList<PhysicalResourceAllocation> Allocations => _allocations;
|
||||
|
||||
/// <summary>
|
||||
/// Allocate physical memory for resources, enabling aliasing where possible
|
||||
/// </summary>
|
||||
public void AllocateResources(
|
||||
IReadOnlyList<ResourceLifetime> resourceLifetimes,
|
||||
List<RenderGraphPass> passes)
|
||||
{
|
||||
Console.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS =====");
|
||||
|
||||
// Separate imported and transient resources
|
||||
var transientResources = resourceLifetimes
|
||||
.Where(lt => !lt.Handle.IsImported && lt.FirstUse != int.MaxValue)
|
||||
.OrderBy(lt => lt.FirstUse)
|
||||
.ThenByDescending(lt => GetResourceSize(lt.Handle))
|
||||
.ToList();
|
||||
|
||||
if (!transientResources.Any())
|
||||
{
|
||||
Console.WriteLine("No transient resources to allocate.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Track which allocation slots are occupied at each pass
|
||||
var allocationSlots = new List<AllocationSlot>();
|
||||
|
||||
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
|
||||
reuseSlot.AddResource(resource);
|
||||
Console.WriteLine($"[ALIAS] '{resource.Handle.Name}' aliases with '{reuseSlot.Allocation.DebugName}' " +
|
||||
$"(offset: {reuseSlot.Allocation.OffsetInBytes}, size: {size} bytes, " +
|
||||
$"lifetime: [{resource.FirstUse}..{resource.LastUse}])");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new allocation
|
||||
var allocation = new PhysicalResourceAllocation(
|
||||
_allocationIdCounter++,
|
||||
size,
|
||||
offsetInBytes: 0, // In a real implementation, this would be a heap offset
|
||||
$"Physical_{resource.Handle.Type}_{_allocationIdCounter}");
|
||||
|
||||
var newSlot = new AllocationSlot(allocation, resource.Handle.Type);
|
||||
newSlot.AddResource(resource);
|
||||
allocationSlots.Add(newSlot);
|
||||
|
||||
Console.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " +
|
||||
$"(size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])");
|
||||
}
|
||||
}
|
||||
|
||||
_allocations.AddRange(allocationSlots.Select(s => s.Allocation));
|
||||
|
||||
// Print summary
|
||||
Console.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;
|
||||
|
||||
Console.WriteLine($" Total memory without aliasing: {FormatBytes(totalWithoutAliasing)}");
|
||||
Console.WriteLine($" Total memory with aliasing: {FormatBytes(totalWithAliasing)}");
|
||||
Console.WriteLine($" Memory saved: {FormatBytes(savedMemory)} ({savingPercentage:F1}%)");
|
||||
Console.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 switch
|
||||
{
|
||||
RenderGraphTextureHandle texture => CalculateTextureSize(texture.Descriptor),
|
||||
RenderGraphBufferHandle buffer => (ulong)buffer.Descriptor.SizeInBytes,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private ulong GetResourceAlignment(RenderGraphResourceHandle handle)
|
||||
{
|
||||
// In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO
|
||||
return handle switch
|
||||
{
|
||||
RenderGraphTextureHandle => 65536, // 64KB texture alignment (typical)
|
||||
RenderGraphBufferHandle => 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));
|
||||
}
|
||||
|
||||
private class AllocationSlot
|
||||
{
|
||||
public PhysicalResourceAllocation Allocation { get; }
|
||||
public ResourceType ResourceType { get; }
|
||||
public List<ResourceLifetime> Resources { get; } = new();
|
||||
|
||||
public AllocationSlot(PhysicalResourceAllocation allocation, ResourceType resourceType)
|
||||
{
|
||||
Allocation = allocation;
|
||||
ResourceType = resourceType;
|
||||
}
|
||||
|
||||
public void AddResource(ResourceLifetime resource)
|
||||
{
|
||||
Resources.Add(resource);
|
||||
Allocation.AliasedResources.Add(resource.Handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Ghost.RenderGraph.Concept/ResourceDescriptor.cs
Normal file
28
Ghost.RenderGraph.Concept/ResourceDescriptor.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
public enum ResourceType
|
||||
{
|
||||
Texture,
|
||||
Buffer
|
||||
}
|
||||
|
||||
public enum TextureFormat
|
||||
{
|
||||
RGBA8,
|
||||
RGBA16F,
|
||||
RGBA32F,
|
||||
Depth32F,
|
||||
R32Uint
|
||||
}
|
||||
|
||||
public record TextureDescriptor(
|
||||
int Width,
|
||||
int Height,
|
||||
TextureFormat Format,
|
||||
string DebugName = "Unnamed Texture"
|
||||
);
|
||||
|
||||
public record BufferDescriptor(
|
||||
int SizeInBytes,
|
||||
string DebugName = "Unnamed Buffer"
|
||||
);
|
||||
35
Ghost.RenderGraph.Concept/ResourceLifetime.cs
Normal file
35
Ghost.RenderGraph.Concept/ResourceLifetime.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
internal class 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 class ResourceLifetime
|
||||
{
|
||||
public RenderGraphResourceHandle Handle { get; }
|
||||
public int FirstUse { get; set; } = int.MaxValue;
|
||||
public int LastUse { get; set; } = -1;
|
||||
public List<ResourceUsage> Usages { get; } = new();
|
||||
|
||||
public ResourceLifetime(RenderGraphResourceHandle handle)
|
||||
{
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
public void AddUsage(ResourceState state, int passIndex)
|
||||
{
|
||||
Usages.Add(new ResourceUsage(Handle, state, passIndex));
|
||||
FirstUse = Math.Min(FirstUse, passIndex);
|
||||
LastUse = Math.Max(LastUse, passIndex);
|
||||
}
|
||||
}
|
||||
21
Ghost.RenderGraph.Concept/ResourceState.cs
Normal file
21
Ghost.RenderGraph.Concept/ResourceState.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user