Refactor render graph & DSL; remove material system

- Major optimization of Ghost.RenderGraph.Concept: pooled resources, zero-allocation hot paths, explicit queue types, and batch barrier APIs.
- Migrated Ghost.DSL shader compiler to ANTLR4-based parser; removed hand-written parser, added grammar files and semantic model conversion.
- Added CollectionPool/ListPool for pooled list management.
- Updated documentation for new architecture and performance.
- Removed Ghost.Shader.Concept (material/material system) from repo and solution.
- README.md replaced with a brief project statement.
This commit is contained in:
2026-01-11 13:28:17 +09:00
parent d71bdb3fc9
commit 87e315a588
63 changed files with 1841 additions and 6085 deletions

View File

@@ -1,177 +0,0 @@
# 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.

View File

@@ -1,189 +0,0 @@
# 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.

View File

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

View File

@@ -7,4 +7,12 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="New\" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,39 @@
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);
@@ -21,66 +51,118 @@ public class SimulatedCommandBuffer : ICommandBuffer
{
public void ResourceBarrier(string resourceName, ResourceState beforeState, ResourceState afterState)
{
Console.WriteLine($" [BARRIER] Transition '{resourceName}' from {beforeState} to {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)
{
Console.WriteLine($" [ALIAS_BARRIER] Alias '{physicalAllocationName}': '{beforeResourceName}' -> '{afterResourceName}'");
//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)
{
Console.WriteLine($" [BEGIN] RenderPass '{passName}'");
//ConsoleAPI.WriteLine($" [BEGIN] RenderPass '{passName}'");
}
public void EndRenderPass()
{
Console.WriteLine($" [END] RenderPass");
//ConsoleAPI.WriteLine($" [END] RenderPass");
}
public void SetRenderTarget(string textureName)
{
Console.WriteLine($" [RT] Set RenderTarget: '{textureName}'");
//ConsoleAPI.WriteLine($" [RT] Set RenderTarget: '{textureName}'");
}
public void SetDepthStencil(string textureName)
{
Console.WriteLine($" [DS] Set DepthStencil: '{textureName}'");
//ConsoleAPI.WriteLine($" [DS] Set DepthStencil: '{textureName}'");
}
public void BindShaderResource(string resourceName, int slot)
{
Console.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}");
//ConsoleAPI.WriteLine($" [SRV] Bind ShaderResource: '{resourceName}' at slot {slot}");
}
public void BindUnorderedAccess(string resourceName, int slot)
{
Console.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}");
//ConsoleAPI.WriteLine($" [UAV] Bind UnorderedAccess: '{resourceName}' at slot {slot}");
}
public void Draw(int vertexCount)
{
Console.WriteLine($" [DRAW] Drawing {vertexCount} vertices");
//ConsoleAPI.WriteLine($" [DRAW] Drawing {vertexCount} vertices");
}
public void Dispatch(int x, int y, int z)
{
Console.WriteLine($" [DISPATCH] Compute ({x}, {y}, {z})");
//ConsoleAPI.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})");
//ConsoleAPI.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}");
//ConsoleAPI.WriteLine($" [CLEAR_DEPTH] Clear '{textureName}' to {depth}");
}
public void CopyTexture(string source, string destination)
{
Console.WriteLine($" [COPY] Copy from '{source}' to '{destination}'");
//ConsoleAPI.WriteLine($" [COPY] Copy from '{source}' to '{destination}'");
}
}
public readonly struct RasterRenderContext
{
private readonly ICommandBuffer _cmd;
public RasterRenderContext(ICommandBuffer cmd)
{
_cmd = cmd;
}
public void SetRenderTarget(string textureName) => _cmd.SetRenderTarget(textureName);
public void SetDepthStencil(string textureName) => _cmd.SetDepthStencil(textureName);
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
public void Draw(int vertexCount) => _cmd.Draw(vertexCount);
public void ClearRenderTarget(string textureName, float r, float g, float b, float a) => _cmd.ClearRenderTarget(textureName, r, g, b, a);
public void ClearDepth(string textureName, float depth) => _cmd.ClearDepth(textureName, depth);
}
public readonly struct ComputeRenderContext
{
private readonly ICommandBuffer _cmd;
public ComputeRenderContext(ICommandBuffer cmd)
{
_cmd = cmd;
}
public void BindShaderResource(string resourceName, int slot) => _cmd.BindShaderResource(resourceName, slot);
public void BindUnorderedAccess(string resourceName, int slot) => _cmd.BindUnorderedAccess(resourceName, slot);
public void Dispatch(int x, int y, int z) => _cmd.Dispatch(x, y, z);
}

View File

@@ -3,56 +3,56 @@ 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 RenderGraphTextureHandle Albedo;
public RenderGraphTextureHandle Normal;
public RenderGraphTextureHandle Depth;
}
public class LightingPassData
{
public RenderGraphTextureHandle GBufferAlbedo = null!;
public RenderGraphTextureHandle GBufferNormal = null!;
public RenderGraphTextureHandle GBufferDepth = null!;
public RenderGraphTextureHandle OutputLighting = null!;
public RenderGraphTextureHandle GBufferAlbedo;
public RenderGraphTextureHandle GBufferNormal;
public RenderGraphTextureHandle GBufferDepth;
public RenderGraphTextureHandle OutputLighting;
}
public class SSAOPassData
{
public RenderGraphTextureHandle GBufferDepth = null!;
public RenderGraphTextureHandle GBufferNormal = null!;
public RenderGraphTextureHandle OutputSSAO = null!;
public RenderGraphTextureHandle GBufferDepth;
public RenderGraphTextureHandle GBufferNormal;
public RenderGraphTextureHandle OutputSSAO;
}
public class TAAPassData
{
public RenderGraphTextureHandle InputLighting = null!;
public RenderGraphTextureHandle OutputTAA = null!;
public RenderGraphTextureHandle InputLighting;
public RenderGraphTextureHandle OutputTAA;
}
public class PostProcessingPassData
{
public RenderGraphTextureHandle InputTAA = null!;
public RenderGraphTextureHandle InputSSAO = null!;
public RenderGraphTextureHandle OutputBackbuffer = null!;
public RenderGraphTextureHandle InputTAA;
public RenderGraphTextureHandle InputSSAO;
public RenderGraphTextureHandle OutputBackbuffer;
}
public class DebugPassData
{
public RenderGraphTextureHandle DebugTexture = null!;
public RenderGraphTextureHandle DebugTexture;
}
public class ProfilerMarkerData { }
public class BloomDownsampleData
{
public RenderGraphTextureHandle Input = null!;
public RenderGraphTextureHandle Output = null!;
public RenderGraphTextureHandle Input;
public RenderGraphTextureHandle Output;
}
public class PostProcessingPassDataV2
{
public RenderGraphTextureHandle InputTAA = null!;
public RenderGraphTextureHandle InputSSAO = null!;
public RenderGraphTextureHandle InputBloom = null!;
public RenderGraphTextureHandle OutputBackbuffer = null!;
public RenderGraphTextureHandle InputTAA;
public RenderGraphTextureHandle InputSSAO;
public RenderGraphTextureHandle InputBloom;
public RenderGraphTextureHandle OutputBackbuffer;
}

View File

@@ -1,177 +1,208 @@
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");
//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();
// 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))
for (int i = 0; i < 500; i++)
{
// 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);
});
BuildGraph(renderGraph);
}
// Store GBuffer data in blackboard for other passes
renderGraph.Blackboard.Add(gbufferData);
var sw = new System.Diagnostics.Stopwatch();
var gcBefore = GC.GetAllocatedBytesForCurrentThread();
sw.Start();
// ===== Lighting Pass =====
RenderGraphTextureHandle lightingOutput;
using (var builder = renderGraph.AddRenderPass<LightingPassData>("Lighting Pass", out var lightingData))
for (int i = 0; i < 500; i++)
{
// 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);
});
BuildGraph(renderGraph);
}
// ===== SSAO Pass =====
RenderGraphTextureHandle ssaoOutput;
using (var builder = renderGraph.AddRenderPass<SSAOPassData>("SSAO Pass", out var ssaoData))
//BuildGraph(renderGraph);
sw.Stop();
var gcAfter = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"{sw.Elapsed.TotalNanoseconds / 500} ns");
Console.WriteLine($"GC Allocated Bytes: {(gcAfter - gcBefore) / 500} bytes");
//Console.WriteLine("\nPress any key to exit...");
//Console.ReadKey();
static void BuildGraph(RenderGraph renderGraph)
{
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
ssaoData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
ssaoData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
renderGraph.Reset();
// This will reuse GBuffer.Albedo's memory allocation
ssaoOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
// Import external resources
var backbuffer = renderGraph.ImportTexture(
"Backbuffer",
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "Backbuffer"));
builder.SetRenderFunc((data, cmd) =>
// ===== GBuffer Pass =====
GBufferData gbufferData;
using (var builder = renderGraph.AddRenderPass<GBufferData>("GBuffer Pass", out gbufferData))
{
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
cmd.SetRenderTarget(data.OutputSSAO.Name);
cmd.Draw(3);
});
}
// 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"));
// ===== 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);
// 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) =>
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))
{
cmd.BindShaderResource(data.Input.Name, 0);
cmd.SetRenderTarget(data.Output.Name);
cmd.Draw(3);
});
}
// Read GBuffer from blackboard
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
// ===== Temporal AA Pass =====
RenderGraphTextureHandle taaOutput;
using (var builder = renderGraph.AddRenderPass<TAAPassData>("Temporal AA", out var taaData))
{
taaData.InputLighting = builder.ReadTexture(lightingOutput);
lightingData.GBufferAlbedo = builder.ReadTexture(gbuffer.Albedo);
lightingData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
lightingData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
taaOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
taaData.OutputTAA = builder.WriteTexture(taaOutput);
// Create output texture
lightingOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "LightingResult"));
lightingData.OutputLighting = builder.WriteTexture(lightingOutput);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferAlbedo.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
cmd.BindShaderResource(data.GBufferDepth.Name, 2);
cmd.SetRenderTarget(data.OutputLighting.Name);
cmd.Draw(3);
});
}
// ===== SSAO Pass (Async Compute) =====
RenderGraphTextureHandle ssaoOutput;
using (var builder = renderGraph.AddRenderPass<SSAOPassData>("SSAO Pass (Async)", out var ssaoData))
{
cmd.BindShaderResource(data.InputLighting.Name, 0);
cmd.SetRenderTarget(data.OutputTAA.Name);
cmd.Draw(3);
});
}
var gbuffer = renderGraph.Blackboard.Get<GBufferData>();
// ===== 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);
ssaoData.GBufferDepth = builder.ReadTexture(gbuffer.Depth);
ssaoData.GBufferNormal = builder.ReadTexture(gbuffer.Normal);
builder.SetRenderFunc((data, cmd) =>
// SSAO Output
ssaoOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA8, "SSAO"));
ssaoData.OutputSSAO = builder.WriteTexture(ssaoOutput);
// Use SetComputeFunc with asyncCompute: true
builder.SetComputeFunc((data, cmd) =>
{
cmd.BindShaderResource(data.GBufferDepth.Name, 0);
cmd.BindShaderResource(data.GBufferNormal.Name, 1);
cmd.BindUnorderedAccess(data.OutputSSAO.Name, 0);
cmd.Dispatch(1920 / 8, 1080 / 8, 1);
}, asyncCompute: true);
}
// ===== Bloom Downsample Pass (will alias with albedo) =====
RenderGraphTextureHandle bloomOutput;
using (var builder = renderGraph.AddRenderPass<BloomDownsampleData>("Bloom Downsample", out var bloomData))
{
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);
});
}
bloomData.Input = builder.ReadTexture(lightingOutput);
// ===== 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) =>
// 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))
{
Console.WriteLine(" [PROFILER] BeginEvent('Frame')");
});
}
taaData.InputLighting = builder.ReadTexture(lightingOutput);
// ===== 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")));
taaOutput = builder.CreateTexture(
new TextureDescriptor(1920, 1080, TextureFormat.RGBA16F, "TAA.Result"));
taaData.OutputTAA = builder.WriteTexture(taaOutput);
builder.SetRenderFunc((data, cmd) =>
builder.SetRenderFunc((data, cmd) =>
{
cmd.BindShaderResource(data.InputLighting.Name, 0);
cmd.SetRenderTarget(data.OutputTAA.Name);
cmd.Draw(3);
});
}
// ===== Post Processing Pass =====
using (var builder = renderGraph.AddRenderPass<PostProcessingPassDataV2>("Post Processing", out var postData))
{
cmd.SetRenderTarget(data.DebugTexture.Name);
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
cmd.Draw(100);
});
}
postData.InputTAA = builder.ReadTexture(taaOutput);
postData.InputSSAO = builder.ReadTexture(ssaoOutput);
postData.InputBloom = builder.ReadTexture(bloomOutput);
postData.OutputBackbuffer = builder.WriteTexture(backbuffer);
// Compile and execute the render graph
renderGraph.Compile();
renderGraph.Execute();
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);
});
}
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
// ===== 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) =>
{
// Note: In a real implementation we would have specific profiler commands
// For now, since RasterRenderContext doesn't expose generic console write, we skip the print
// or we would add a specific Profiler method to the context
});
}
// ===== Unused Debug Pass (will be culled) =====
using (var builder = renderGraph.AddRenderPass<DebugPassData>("Unused Debug Pass", out var debugData))
{
debugData.DebugTexture = builder.WriteTexture(
builder.CreateTexture(new TextureDescriptor(512, 512, TextureFormat.RGBA8, "DebugTexture")));
builder.SetRenderFunc((data, cmd) =>
{
cmd.SetRenderTarget(data.DebugTexture.Name);
cmd.ClearRenderTarget(data.DebugTexture.Name, 1, 0, 1, 1);
cmd.Draw(100);
});
}
// Compile and execute the render graph
renderGraph.Compile();
renderGraph.Execute();
}

View File

@@ -1,306 +0,0 @@
# 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.

View File

@@ -1,3 +1,9 @@
using Ghost.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
public class RenderGraph
@@ -8,7 +14,6 @@ public class RenderGraph
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();
@@ -17,47 +22,66 @@ public class RenderGraph
private readonly RenderGraphBlackboard _blackboard = new();
private readonly ResourceAllocator _allocator = new();
// Batching and Sync
private readonly List<RenderGraphBatch> _batches = new();
private readonly Stack<RenderGraphBatch> _batchPool = new();
private int _fenceCounter = 0;
// Pooled Collections for Compilation
private readonly Dictionary<int, int> _resourceLastWriter = new();
private readonly Dictionary<int, List<int>> _resourceLastReaders = new();
private readonly Dictionary<int, RenderGraphBatch> _passToBatchMap = new();
// Pooled Lists for Passes
private readonly Stack<List<(RenderGraphResourceHandle, ResourceState)>> _resourceAccessListPool = new();
private readonly Stack<ResourceLifetime> _resourceLifetimePool = new();
// Execution Plan (Pre-calculated to avoid LINQ in Execute)
private List<RenderGraphResourceHandle>[] _resourcesToCreate = Array.Empty<List<RenderGraphResourceHandle>>();
private List<RenderGraphResourceHandle>[] _resourcesToDestroy = Array.Empty<List<RenderGraphResourceHandle>>();
public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor)
{
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, name, descriptor, isImported: true);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_resources.Add(handle._handle);
_resourceLifetimes.Add(RentResourceLifetime(handle._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})");
_resourceToAllocationMap.Add(-1);
//ConsoleAPI.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));
_resources.Add(handle._handle);
_resourceLifetimes.Add(RentResourceLifetime(handle._handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)");
//ConsoleAPI.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)");
return handle;
}
internal RenderGraphTextureHandle CreateTransientTexture(TextureDescriptor descriptor)
{
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_resources.Add(handle._handle);
_resourceLifetimes.Add(RentResourceLifetime(handle._handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
//ConsoleAPI.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
return handle;
}
internal RenderGraphBufferHandle CreateTransientBuffer(BufferDescriptor descriptor)
{
var handle = new RenderGraphBufferHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_resources.Add(handle._handle);
_resourceLifetimes.Add(RentResourceLifetime(handle._handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)");
//ConsoleAPI.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)");
return handle;
}
@@ -76,7 +100,8 @@ public class RenderGraph
public RenderGraphPassBuilder<TPassData> AddRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var builder = new RenderGraphPassBuilder<TPassData>(this, name, _passCounter);
var list = RentResourceAccessList();
var builder = new RenderGraphPassBuilder<TPassData>(this, name, _passCounter, list);
passData = builder.PassData;
return builder;
}
@@ -89,44 +114,126 @@ public class RenderGraph
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);
// Optimization: Use Pass Pool
RenderGraphPass<TPassData>? pass;
// Cast ReadOnlyList back to List (safe because we created it in AddRenderPass)
var resourceList = (List<(RenderGraphResourceHandle handle, ResourceState state)>)builder.ResourceAccesses;
if (!RenderGraphPassPool<TPassData>.Pool.TryPop(out pass))
{
pass = new RenderGraphPass<TPassData>(
name,
_passCounter++,
builder.QueueType,
builder.PassData,
builder.RenderFunc,
resourceList,
builder.AllowCulling);
}
else
{
pass.Initialize(
name,
_passCounter++,
builder.QueueType,
builder.PassData,
builder.RenderFunc,
resourceList,
builder.AllowCulling);
}
_passes.Add(pass);
foreach (var (handle, state) in pass.ResourceAccesses)
{
_resourceLifetimes[handle.Id].AddUsage(state, pass.Index);
var lifeTime = _resourceLifetimes[handle.Id];
lifeTime.AddUsage(state, pass.Index);
_resourceLifetimes[handle.Id] = lifeTime;
}
Console.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})");
foreach (var (handle, state) in pass.ResourceAccesses)
{
Console.WriteLine($" - {state}: '{handle.Name}'");
}
//ConsoleAPI.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})");
}
public void Compile()
{
Console.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH ==========");
//ConsoleAPI.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH ==========");
BuildDependencies();
CullUnusedPasses();
AnalyzeResourceLifetimes();
AllocatePhysicalResources();
InsertSynchronization();
}
private void InsertSynchronization()
{
//ConsoleAPI.WriteLine("\n[RG] Building command batches and synchronization...");
_batches.Clear();
_fenceCounter = 0;
// 1. Create Batches (Topological grouping)
RenderGraphBatch? currentBatch = null;
_passToBatchMap.Clear();
foreach (var pass in _passes)
{
if (pass.RefCount == 0) continue;
if (currentBatch == null || currentBatch.QueueType != pass.QueueType)
{
if (!_batchPool.TryPop(out currentBatch))
{
currentBatch = new RenderGraphBatch();
}
currentBatch.Initialize(_batches.Count, pass.QueueType);
_batches.Add(currentBatch);
}
currentBatch.Passes.Add(pass);
_passToBatchMap[pass.Index] = currentBatch;
}
//ConsoleAPI.WriteLine($" Created {_batches.Count} batches.");
// 2. Inject Synchronization (Fences)
foreach (var batch in _batches)
{
foreach (var pass in batch.Passes)
{
foreach (var depIndex in pass.Dependencies)
{
if (_passToBatchMap.TryGetValue(depIndex, out var dependencyBatch))
{
if (dependencyBatch != batch)
{
int fenceId;
if (dependencyBatch.SignalFences.Count == 0)
{
fenceId = _fenceCounter++;
dependencyBatch.SignalFences.Add(fenceId);
}
else
{
fenceId = dependencyBatch.SignalFences[0];
}
if (!batch.WaitFences.Contains(fenceId))
{
batch.WaitFences.Add(fenceId);
//ConsoleAPI.WriteLine($" Batch {batch.ID} ({batch.QueueType}) waits on Batch {dependencyBatch.ID} ({dependencyBatch.QueueType}) [Fence {fenceId}]");
}
}
}
}
}
}
}
private void AllocatePhysicalResources()
{
// 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)
@@ -138,42 +245,59 @@ public class RenderGraph
private void BuildDependencies()
{
Console.WriteLine("\n[RG] Building pass dependencies...");
_resourceLastWriter.Clear();
foreach (var list in _resourceLastReaders.Values) list.Clear();
_resourceLastReaders.Clear();
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++)
foreach (var (handle, state) in pass.ResourceAccesses)
{
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)));
int resourceId = handle.Id;
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 (IsReadState(state))
{
if (!pass.Dependencies.Contains(j))
if (_resourceLastWriter.TryGetValue(resourceId, out int lastWriterIndex))
{
pass.Dependencies.Add(j);
Console.WriteLine($" Pass '{pass.Name}' depends on '{previousPass.Name}'");
if (!pass.Dependencies.Contains(lastWriterIndex))
{
pass.Dependencies.Add(lastWriterIndex);
}
}
if (!_resourceLastReaders.TryGetValue(resourceId, out var readers))
{
readers = new List<int>(); // Optimization TODO: Pool these
_resourceLastReaders[resourceId] = readers;
}
readers.Add(i);
}
if (IsWriteState(state))
{
if (_resourceLastWriter.TryGetValue(resourceId, out int lastWriterIndex))
{
if (!pass.Dependencies.Contains(lastWriterIndex))
{
pass.Dependencies.Add(lastWriterIndex);
}
}
if (_resourceLastReaders.TryGetValue(resourceId, out var readers))
{
foreach (var readerIndex in readers)
{
if (readerIndex != i && !pass.Dependencies.Contains(readerIndex))
{
pass.Dependencies.Add(readerIndex);
}
}
readers.Clear();
}
_resourceLastWriter[resourceId] = i;
}
}
}
@@ -181,9 +305,6 @@ public class RenderGraph
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)
@@ -194,15 +315,12 @@ public class RenderGraph
}
}
// 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)
{
@@ -223,82 +341,94 @@ public class RenderGraph
}
}
}
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:");
// Resize execution plan arrays if needed
int requiredSize = _passes.Count;
if (_resourcesToCreate.Length < requiredSize)
{
Array.Resize(ref _resourcesToCreate, requiredSize);
Array.Resize(ref _resourcesToDestroy, requiredSize);
// Initialize new elements
for (int i = 0; i < requiredSize; i++)
{
if (_resourcesToCreate[i] == null) _resourcesToCreate[i] = new List<RenderGraphResourceHandle>();
if (_resourcesToDestroy[i] == null) _resourcesToDestroy[i] = new List<RenderGraphResourceHandle>();
}
}
// Clear previous plan
for (int i = 0; i < requiredSize; i++)
{
_resourcesToCreate[i].Clear();
_resourcesToDestroy[i].Clear();
}
// Populate plan
foreach (var lifetime in _resourceLifetimes)
{
if (lifetime.FirstUse == int.MaxValue)
if (lifetime.FirstUse != int.MaxValue && !lifetime.Handle.IsImported)
{
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)})");
// Verify bounds to be safe
if (lifetime.FirstUse < requiredSize)
_resourcesToCreate[lifetime.FirstUse].Add(lifetime.Handle);
if (lifetime.LastUse < requiredSize)
_resourcesToDestroy[lifetime.LastUse].Add(lifetime.Handle);
}
}
}
public void Execute()
{
Console.WriteLine("\n[RG] ========== EXECUTING RENDER GRAPH ==========\n");
//ConsoleAPI.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))
foreach (var batch in _batches)
{
Console.WriteLine($"[PASS {pass.Index}] Executing: '{pass.Name}'");
var lifetime = _resourceLifetimes
.Where(lt => lt.FirstUse == pass.Index)
.ToList();
//ConsoleAPI.WriteLine($"[BATCH {batch.ID}] Queue: {batch.QueueType} | Passes: {batch.Passes.Count}");
foreach (var lt in lifetime)
foreach (var fenceId in batch.WaitFences)
{
if (!lt.Handle.IsImported)
//ConsoleAPI.WriteLine($" [SYNC] Wait for Fence {fenceId}");
}
foreach (var pass in batch.Passes)
{
//ConsoleAPI.WriteLine($" [PASS {pass.Index}] Executing: '{pass.Name}'");
// Optimized: Use pre-calculated lists
var createList = _resourcesToCreate[pass.Index];
foreach (var handle in createList)
{
CreateResource(lt.Handle);
CreateResource(handle);
}
InsertBarriers(pass, commandBuffer);
commandBuffer.BeginRenderPass(pass.Name);
pass.Execute(commandBuffer);
commandBuffer.EndRenderPass();
// Optimized: Use pre-calculated lists
var destroyList = _resourcesToDestroy[pass.Index];
foreach (var handle in destroyList)
{
DestroyResource(handle);
}
}
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)
foreach (var fenceId in batch.SignalFences)
{
DestroyResource(lt.Handle);
//ConsoleAPI.WriteLine($" [SYNC] Signal Fence {fenceId}");
}
Console.WriteLine();
}
Console.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n");
//ConsoleAPI.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n");
}
private void CreateResource(RenderGraphResourceHandle handle)
@@ -306,34 +436,7 @@ public class RenderGraph
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)");
}
// Logic...
}
_currentResourceStates[handle.Id] = ResourceState.Undefined;
@@ -341,47 +444,50 @@ public class RenderGraph
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)
{
var _resourceBarriers = ListPool<ResourceBarrierInfo>.Rent();
var _aliasingBarriers = ListPool<AliasingBarrierInfo>.Rent();
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 (_allocationActiveResource.TryGetValue(allocation.Value.AllocationId, out var activeResource))
{
// If a different resource is currently active on this allocation, insert aliasing barrier
if (activeResource != null && activeResource.Id != handle.Id)
if (activeResource != null && activeResource.Value.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;
_aliasingBarriers.Add(new AliasingBarrierInfo(activeResource.Value.Name, handle.Name, allocation.Value.DebugName));
_currentResourceStates[activeResource.Value.Id] = ResourceState.Undefined;
}
}
}
// Update the active resource for this allocation
_allocationActiveResource[allocation.AllocationId] = handle;
_allocationActiveResource[allocation.Value.AllocationId] = handle;
}
var currentState = _currentResourceStates[handle.Id];
if (currentState != targetState)
{
commandBuffer.ResourceBarrier(handle.Name, currentState, targetState);
_resourceBarriers.Add(new ResourceBarrierInfo(handle.Name, currentState, targetState));
_currentResourceStates[handle.Id] = targetState;
}
}
if (_aliasingBarriers.Count > 0)
{
commandBuffer.AliasingBarrier(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_aliasingBarriers));
}
if (_resourceBarriers.Count > 0)
{
commandBuffer.ResourceBarrier(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_resourceBarriers));
}
ListPool<ResourceBarrierInfo>.Return(_resourceBarriers);
ListPool<AliasingBarrierInfo>.Return(_aliasingBarriers);
}
private static bool IsWriteState(ResourceState state)
@@ -399,17 +505,67 @@ public class RenderGraph
state.HasFlag(ResourceState.CopySource);
}
internal List<(RenderGraphResourceHandle, ResourceState)> RentResourceAccessList()
{
if (_resourceAccessListPool.TryPop(out var list))
{
return list;
}
return new List<(RenderGraphResourceHandle, ResourceState)>();
}
internal void ReturnResourceAccessList(List<(RenderGraphResourceHandle, ResourceState)> list)
{
list.Clear();
_resourceAccessListPool.Push(list);
}
private ResourceLifetime RentResourceLifetime(RenderGraphResourceHandle handle)
{
if (!_resourceLifetimePool.TryPop(out var lifetime))
{
lifetime = new ResourceLifetime();
}
lifetime.Initialize(handle);
return lifetime;
}
public void Reset()
{
foreach (var batch in _batches)
{
batch.Reset();
_batchPool.Push(batch);
}
_batches.Clear();
foreach (var pass in _passes)
{
// ReturnResourceAccessList(pass.ResourceAccesses);
// Warning: pass.ResourceAccesses might be a copy in the current implementation of CommitPass?
// No, I'm going to fix CommitPass to use the pooled list.
// But right now builder.ResourceAccesses is a List.
// I need to ensure CommitPass takes ownership.
}
_passes.Clear();
_resources.Clear();
foreach (var lifetime in _resourceLifetimes)
{
_resourceLifetimePool.Push(lifetime);
}
_resourceLifetimes.Clear();
_currentResourceStates.Clear();
_resourceToAllocationMap.Clear();
_allocationActiveResource.Clear();
_blackboard.Clear();
_allocator.Reset();
_passCounter = 0;
_resourceIdCounter = 0;
Console.WriteLine("[RG] Render graph reset.");
_resourceLastWriter.Clear();
foreach (var list in _resourceLastReaders.Values) list.Clear();
_resourceLastReaders.Clear();
_passToBatchMap.Clear();
}
}

View File

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

View File

@@ -1,41 +1,45 @@
/*
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;
}
// 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()
{
private readonly RenderGraphPassBuilder<TPassData> _builder;
// 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;
}
// internal RenderGraphPassScope(RenderGraphPassBuilder<TPassData> builder, string passName)
// {
// _builder = builder;
// _passName = passName;
// }
public RenderGraphPassBuilder<TPassData> Builder => _builder;
// public RenderGraphPassBuilder<TPassData> Builder => _builder;
public void Dispose()
{
// Commit the pass when the using block ends
if (_builder.RenderFunc != null)
{
_builder.Dispose();
}
// if (_builder.RenderFunc != null)
// {
// _builder.Dispose();
// }
}
}
*/

View File

@@ -1,50 +1,111 @@
using System;
using System.Collections.Generic;
namespace Ghost.RenderGraph.Concept;
public enum RenderQueueType
{
Graphics,
Compute,
AsyncCompute,
Copy
}
internal abstract class RenderGraphPass
{
public string Name { get; }
public int Index { get; }
public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; }
public string Name { get; set; } = string.Empty;
public int Index { get; set; }
public RenderQueueType QueueType { get; set; }
public List<(RenderGraphResourceHandle handle, ResourceState state)> ResourceAccesses { get; set; }
public List<int> Dependencies { get; } = new();
public int RefCount { get; set; } = 0;
public bool AllowCulling { get; }
public bool AllowCulling { get; set; }
protected RenderGraphPass(
string name,
int index,
RenderQueueType queueType,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
{
Name = name;
Index = index;
QueueType = queueType;
ResourceAccesses = resourceAccesses;
AllowCulling = allowCulling;
}
protected void InitializeBase(
string name,
int index,
RenderQueueType queueType,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
{
Name = name;
Index = index;
QueueType = queueType;
ResourceAccesses = resourceAccesses;
AllowCulling = allowCulling;
Dependencies.Clear();
RefCount = 0;
}
public abstract void Execute(ICommandBuffer commandBuffer);
public abstract void Release();
}
internal static class RenderGraphPassPool<TPassData>
where TPassData : class
{
public static readonly Stack<RenderGraphPass<TPassData>> Pool = new();
}
internal class RenderGraphPass<TPassData> : RenderGraphPass
where TPassData : class
{
public TPassData PassData { get; }
public Action<TPassData, ICommandBuffer> RenderFunc { get; }
public TPassData PassData { get; private set; }
public Action<TPassData, ICommandBuffer> RenderFunc { get; private set; }
public RenderGraphPass(
string name,
int index,
RenderQueueType queueType,
TPassData passData,
Action<TPassData, ICommandBuffer> renderFunc,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
: base(name, index, resourceAccesses, allowCulling)
: base(name, index, queueType, resourceAccesses, allowCulling)
{
PassData = passData;
RenderFunc = renderFunc;
}
public void Initialize(
string name,
int index,
RenderQueueType queueType,
TPassData passData,
Action<TPassData, ICommandBuffer> renderFunc,
List<(RenderGraphResourceHandle handle, ResourceState state)> resourceAccesses,
bool allowCulling)
{
InitializeBase(name, index, queueType, resourceAccesses, allowCulling);
PassData = passData;
RenderFunc = renderFunc;
}
public override void Execute(ICommandBuffer commandBuffer)
{
RenderFunc(PassData, commandBuffer);
}
public override void Release()
{
PassData = null!;
RenderFunc = null!;
// ResourceAccesses list ownership is transferred back to RenderGraph
ResourceAccesses = null!;
RenderGraphPassPool<TPassData>.Pool.Push(this);
}
}

View File

@@ -11,46 +11,53 @@ public interface IRenderGraphBuilder
RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor);
}
public class RenderGraphPassBuilder<TPassData> : IRenderGraphBuilder, IDisposable
public ref struct RenderGraphPassBuilder<TPassData>
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 RenderQueueType _queueType;
private readonly List<(RenderGraphResourceHandle handle, ResourceState state)> _resourceAccesses;
private Action<TPassData, ICommandBuffer>? _renderFunc;
private bool _committed;
private bool _allowCulling = true;
private bool _allowCulling;
public TPassData PassData { get; }
internal RenderGraphPassBuilder(RenderGraph graph, string passName, int passIndex)
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, ResourceState.ShaderResource));
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
return handle;
}
public RenderGraphTextureHandle WriteTexture(RenderGraphTextureHandle handle)
{
_resourceAccesses.Add((handle, ResourceState.RenderTarget));
_resourceAccesses.Add((handle._handle, ResourceState.RenderTarget));
return handle;
}
public RenderGraphTextureHandle UseDepthBuffer(RenderGraphTextureHandle handle, bool writeAccess)
{
_resourceAccesses.Add((handle, writeAccess ? ResourceState.DepthWrite : ResourceState.DepthRead));
_resourceAccesses.Add((handle._handle, writeAccess ? ResourceState.DepthWrite : ResourceState.DepthRead));
return handle;
}
@@ -62,13 +69,13 @@ public class RenderGraphPassBuilder<TPassData> : IRenderGraphBuilder, IDisposabl
public RenderGraphBufferHandle ReadBuffer(RenderGraphBufferHandle handle)
{
_resourceAccesses.Add((handle, ResourceState.ShaderResource));
_resourceAccesses.Add((handle._handle, ResourceState.ShaderResource));
return handle;
}
public RenderGraphBufferHandle WriteBuffer(RenderGraphBufferHandle handle)
{
_resourceAccesses.Add((handle, ResourceState.UnorderedAccess));
_resourceAccesses.Add((handle._handle, ResourceState.UnorderedAccess));
return handle;
}
@@ -78,9 +85,16 @@ public class RenderGraphPassBuilder<TPassData> : IRenderGraphBuilder, IDisposabl
return handle;
}
public void SetRenderFunc(Action<TPassData, ICommandBuffer> renderFunc)
public void SetRenderFunc(Action<TPassData, RasterRenderContext> renderFunc)
{
_renderFunc = 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>
@@ -93,6 +107,7 @@ public class RenderGraphPassBuilder<TPassData> : IRenderGraphBuilder, IDisposabl
_allowCulling = allowCulling;
}
public void Dispose()
{
// Commit the pass when disposed (at end of using block)

View File

@@ -1,41 +1,63 @@
using System.Runtime.InteropServices;
namespace Ghost.RenderGraph.Concept;
public class RenderGraphResourceHandle
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)
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 sealed class RenderGraphTextureHandle : RenderGraphResourceHandle
public struct RenderGraphTextureHandle
{
internal TextureDescriptor Descriptor { get; }
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)
: base(id, ResourceType.Texture, name, isImported)
{
Descriptor = descriptor;
_handle = new RenderGraphResourceHandle(id, ResourceType.Texture, name, isImported, new RenderGraphResourceHandle.descriptor_union() { texture = descriptor });
}
}
public sealed class RenderGraphBufferHandle : RenderGraphResourceHandle
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)
: base(id, ResourceType.Buffer, name, isImported)
{
Descriptor = descriptor;
_handle = new RenderGraphResourceHandle(id, ResourceType.Buffer, name, isImported, new RenderGraphResourceHandle.descriptor_union() { buffer = descriptor });
}
}

View File

@@ -1,9 +1,12 @@
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 class PhysicalResourceAllocation
internal struct PhysicalResourceAllocation
{
public int AllocationId { get; }
public ulong SizeInBytes { get; }
@@ -28,6 +31,12 @@ 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>
@@ -37,26 +46,27 @@ internal class ResourceAllocator
IReadOnlyList<ResourceLifetime> resourceLifetimes,
List<RenderGraphPass> passes)
{
Console.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS =====");
//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
var transientResources = resourceLifetimes
// 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)
.ToList();
.ThenBy(lt => lt.FirstUse).ToArray();
if (!transientResources.Any())
if (transientResources.Length == 0)
{
Console.WriteLine("No transient resources to allocate.");
//ConsoleAPI.WriteLine("No transient resources to allocate.");
return;
}
// Track which allocation slots are occupied at each pass
var allocationSlots = new List<AllocationSlot>();
var allocationSlots = Core.Utilities.ListPool<AllocationSlot>.Rent();
foreach (var resource in transientResources)
{
@@ -83,9 +93,7 @@ internal class ResourceAllocator
ulong offsetInAllocation = reuseSlot.FindFreeOffset(size, alignment, resource);
reuseSlot.AddResource(resource, offsetInAllocation, size);
Console.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}])");
//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
{
@@ -105,24 +113,28 @@ internal class ResourceAllocator
newSlot.AddResource(resource, 0, size); // Offset 0 within this new allocation
allocationSlots.Add(newSlot);
Console.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " +
$"(heap offset: {heapOffset}, size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])");
//ConsoleAPI.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " + $"(heap offset: {heapOffset}, size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])");
}
}
_allocations.AddRange(allocationSlots.Select(s => s.Allocation));
foreach (var slot in allocationSlots)
{
_allocations.Add(slot.Allocation);
}
ListPool<AllocationSlot>.Return(allocationSlots);
// Print summary
Console.WriteLine($"\n[RG] Memory Statistics:");
//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;
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");
//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)
@@ -153,10 +165,10 @@ internal class ResourceAllocator
private ulong GetResourceSize(RenderGraphResourceHandle handle)
{
return handle switch
return handle.Type switch
{
RenderGraphTextureHandle texture => CalculateTextureSize(texture.Descriptor),
RenderGraphBufferHandle buffer => (ulong)buffer.Descriptor.SizeInBytes,
ResourceType.Texture => CalculateTextureSize(handle.Descriptor.texture),
ResourceType.Buffer => (ulong)handle.Descriptor.buffer.SizeInBytes,
_ => 0
};
}
@@ -164,10 +176,10 @@ internal class ResourceAllocator
private ulong GetResourceAlignment(RenderGraphResourceHandle handle)
{
// In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO
return handle switch
return handle.Type switch
{
RenderGraphTextureHandle => 65536, // 64KB texture alignment (typical)
RenderGraphBufferHandle => 256, // 256 byte buffer alignment
ResourceType.Texture => 65536, // 64KB texture alignment (typical)
ResourceType.Buffer => 256, // 256 byte buffer alignment
_ => 256
};
}
@@ -199,7 +211,19 @@ internal class ResourceAllocator
public PhysicalResourceAllocation? GetAllocation(RenderGraphResourceHandle handle)
{
return _allocations.FirstOrDefault(a => a.AliasedResources.Any(r => r.Id == handle.Id));
// 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
@@ -230,19 +254,19 @@ internal class ResourceAllocator
}
// Sort regions by offset
var sortedRegions = _occupiedRegions.OrderBy(r => r.Offset).ToList();
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 region in sortedRegions)
foreach (var (Offset, Size, Resource) in sortedRegions)
{
// Check if this region overlaps with our candidate position
if (region.Offset < requiredSize)
if (Offset < requiredSize)
{
// Check lifetime - if no overlap, we can still use this space
if (LifetimesOverlap(region.Resource, newResource))
if (LifetimesOverlap(Resource, newResource))
{
fitsAtStart = false;
break;
@@ -256,7 +280,7 @@ internal class ResourceAllocator
}
// Try gaps between regions
for (int i = 0; i < sortedRegions.Count; i++)
for (int i = 0; i < sortedRegions.Length; i++)
{
var current = sortedRegions[i];
@@ -270,7 +294,7 @@ internal class ResourceAllocator
candidateOffset = AlignUp(current.Offset + current.Size, alignment);
// Check if it fits before the next region (or end of allocation)
ulong nextRegionStart = (i + 1 < sortedRegions.Count)
ulong nextRegionStart = (i + 1 < sortedRegions.Length)
? sortedRegions[i + 1].Offset
: Allocation.SizeInBytes;
@@ -278,7 +302,7 @@ internal class ResourceAllocator
{
// Check no lifetime conflicts with any regions in this range
bool hasConflict = false;
for (int j = i + 1; j < sortedRegions.Count; j++)
for (int j = i + 1; j < sortedRegions.Length; j++)
{
var other = sortedRegions[j];
if (other.Offset < candidateOffset + requiredSize)
@@ -299,7 +323,7 @@ internal class ResourceAllocator
}
// Try placing at the end
if (sortedRegions.Count > 0)
if (sortedRegions.Length > 0)
{
var last = sortedRegions[^1];
if (!LifetimesOverlap(last.Resource, newResource))

View File

@@ -15,14 +15,14 @@ public enum TextureFormat
R32Uint
}
public record TextureDescriptor(
public record struct TextureDescriptor(
int Width,
int Height,
TextureFormat Format,
string DebugName = "Unnamed Texture"
);
public record BufferDescriptor(
public record struct BufferDescriptor(
int SizeInBytes,
string DebugName = "Unnamed Buffer"
);

View File

@@ -1,6 +1,6 @@
namespace Ghost.RenderGraph.Concept;
internal class ResourceUsage
internal struct ResourceUsage
{
public RenderGraphResourceHandle Handle { get; }
public ResourceState State { get; }
@@ -14,16 +14,23 @@ internal class ResourceUsage
}
}
internal class ResourceLifetime
internal struct ResourceLifetime
{
public RenderGraphResourceHandle Handle { get; }
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(RenderGraphResourceHandle handle)
public ResourceLifetime()
{
}
public void Initialize(RenderGraphResourceHandle handle)
{
Handle = handle;
FirstUse = int.MaxValue;
LastUse = -1;
Usages.Clear();
}
public void AddUsage(ResourceState state, int passIndex)

View File

@@ -1,160 +0,0 @@
# Resource Allocator Improvements: Size-First Sorting
## What Changed
### Before: First-Use Then Size Sorting
```csharp
.OrderBy(lt => lt.FirstUse)
.ThenByDescending(lt => GetResourceSize(lt.Handle))
```
**Order**: GBuffer.Albedo[0] → GBuffer.Normal[0] → GBuffer.Depth[0] → Lighting[1] → ...
**Result**: Smaller resources allocated first, harder for larger resources to find space.
### After: Size-First Then First-Use Sorting
```csharp
.OrderByDescending(lt => GetResourceSize(lt.Handle))
.ThenBy(lt => lt.FirstUse)
```
**Order**: GBuffer.Normal(16.6MB) → LightingResult(16.6MB) → GBuffer.Albedo(8.3MB) → GBuffer.Depth(8.3MB) → ...
**Result**: Larger resources get allocated first, smaller resources naturally alias into their space.
## Benefits
### 1. Better Aliasing for C > A and C > B, C < A+B Case
**Scenario**:
- Resource A: 4MB, lifetime [0..1]
- Resource B: 6MB, lifetime [0..1]
- Resource C: 10MB, lifetime [2..3]
**Old Sorting (First-Use)**:
```
Pass 0-1: [A: 4MB] [B: 6MB]
Pass 2-3: [C: 10MB] ← NEW ALLOCATION (doesn't fit in A or B)
Total: 4MB + 6MB + 10MB = 20MB
```
**New Sorting (Size-First)**:
```
Pass 0-1: [C's space: 10MB] ← Allocated first
[A: 4MB at offset 0] ← Aliases into C's space
[B: 6MB at offset 4MB] ← Aliases into C's space (or new if > 6MB left)
Pass 2-3: [C: 10MB] ← Reuses its original allocation
Total: 10MB (optimal!)
```
### 2. Improved Memory Savings
**Current Demo Output**:
```
[ALLOC] 'GBuffer.Normal' gets new allocation 'Physical_Texture_1'
(heap offset: 0, size: 16.6 MB, lifetime: [0..2])
[ALLOC] 'LightingResult' gets new allocation 'Physical_Texture_2'
(heap offset: 16.6 MB, size: 16.6 MB, lifetime: [1..4])
[ALIAS] 'TAA.Result' aliases with 'Physical_Texture_1'
(heap offset: 0, resource offset: 0, size: 16.6 MB, lifetime: [4..5])
[ALLOC] 'GBuffer.Albedo' gets new allocation 'Physical_Texture_3'
(heap offset: 33.2 MB, size: 8.3 MB, lifetime: [0..1])
[ALIAS] 'SSAO' aliases with 'Physical_Texture_3'
(heap offset: 33.2 MB, resource offset: 0, size: 8.3 MB, lifetime: [2..5])
```
**Memory saved: 32.64 MB (40.7%)**
### 3. Proper Heap Offset Calculation
**New Feature**: Each physical allocation now has a correct heap offset:
```csharp
// Calculate cumulative heap offset
ulong heapOffset = allocationSlots.Count > 0
? allocationSlots.Max(s => s.Allocation.OffsetInBytes + s.Allocation.SizeInBytes)
: 0;
```
**Visual Representation**:
```
Heap Layout:
├─ [0 MB .. 16.6 MB] Physical_Texture_1 (GBuffer.Normal, TAA.Result)
├─ [16.6 MB .. 33.2 MB] Physical_Texture_2 (LightingResult)
├─ [33.2 MB .. 41.5 MB] Physical_Texture_3 (GBuffer.Albedo, SSAO)
└─ [41.5 MB .. 49.8 MB] Physical_Texture_4 (GBuffer.Depth, BloomDownsample)
```
### 4. Sub-Allocation Support
**New Feature**: `AllocationSlot.FindFreeOffset()` can now find gaps within allocations:
```csharp
public ulong FindFreeOffset(ulong requiredSize, ulong alignment, ResourceLifetime newResource)
{
// Tries to fit resource:
// 1. At offset 0 (if no lifetime conflicts)
// 2. In gaps between existing resources
// 3. After the last resource
// 4. Returns 0 if no space (caller creates new allocation)
}
```
This enables **true sub-allocation** where multiple resources can share the same allocation at different offsets.
## Real-World D3D12 Mapping
```csharp
// Our simulated heap:
Physical_Texture_1 at heap offset 0
// Maps to D3D12:
ID3D12Heap* heap = d3d12ma->AllocateHeap(256MB);
// Place resources:
device->CreatePlacedResource(
heap,
0, // ← Our "heap offset: 0"
&gbufferNormalDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&gbufferNormal));
// Later, alias:
device->CreatePlacedResource(
heap,
0, // ← Same offset, aliased!
&taaResultDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&taaResult));
// Insert aliasing barrier before using taaResult
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_ALIASING;
barrier.Aliasing.pResourceBefore = gbufferNormal;
barrier.Aliasing.pResourceAfter = taaResult;
```
## Performance Impact
### CPU
- Sorting: O(N log N) → No change
- Allocation: O(N × M) where M = slots → **Improved** (fewer slots due to better packing)
### Memory
- **40.7% savings** in demo (32.64 MB saved)
- Scales better with mixed resource sizes
### GPU
- Fewer physical allocations = less heap fragmentation
- Better cache locality (larger resources grouped together)
## Conclusion
By sorting resources **size-first**, we enable:
1.**Better handling of C > A, C > B, C < A+B scenarios**
2.**Proper heap offset tracking**
3.**Sub-allocation within physical allocations**
4.**Production-ready D3D12MA integration path**
The allocator now matches industry-standard behavior from Unreal, Unity, and Frostbite!