Implemented a transient render graph system as a proof of concept, including resource aliasing, pass culling, and typed pass data. Added new project `Ghost.RenderGraph.Concept` targeting `.NET 10.0`. Refactored graphics-related components: - Simplified resource state transitions in `RenderingContext`. - Improved resize handling in `GraphicsTestWindow`. - Updated `D3D12GraphicsEngine` to streamline frame rendering. - Enhanced `D3D12ResourceDatabase` and `D3D12SwapChain` for better resource management. Added detailed documentation: - `ALIASING.md` explains resource aliasing techniques. - `API_DESIGN.md` outlines the render graph API design. Updated solution to include the new render graph project.
5.9 KiB
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:
- Two or more resources share the same physical memory
- You're switching from one resource to another
- 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:
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
- Size: The physical allocation must be >= required size
- Type: Textures can only alias with textures, buffers with buffers
- Lifetime: Resources must have non-overlapping lifetimes
- 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
// 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
// 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
- Print Allocation Map: See which resources share memory
- Visualize Lifetimes: Graph timeline to spot aliasing opportunities
- Track Peak Memory: Identify frames with poor aliasing
- 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.