Introduces a full-featured render graph system with pass culling, resource aliasing, and automatic barrier generation. Refactors resource and barrier APIs, improves error handling, and unifies result types. Renderer and render passes now use the new graph-based workflow. Updates shader includes, adds a blit shader, and improves HLSL parsing. Removes dynamic descriptor heaps in favor of persistent ones. Project file now includes the render graph module. Lays the foundation for advanced rendering features and improved memory efficiency.
544 lines
18 KiB
C#
544 lines
18 KiB
C#
using Ghost.Core.Utilities;
|
|
using Ghost.Graphics.RHI;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Ghost.Graphics.RenderGraphModule;
|
|
|
|
/// <summary>
|
|
/// Represents a memory block within a heap.
|
|
/// </summary>
|
|
internal struct MemoryBlock
|
|
{
|
|
public ulong offset;
|
|
public ulong size;
|
|
public bool isFree;
|
|
public int firstUsePass;
|
|
public int lastUsePass;
|
|
public int logicalResourceIndex; // Which logical resource is currently using this block
|
|
|
|
public MemoryBlock(ulong offset, ulong size)
|
|
{
|
|
this.offset = offset;
|
|
this.size = size;
|
|
isFree = true;
|
|
firstUsePass = int.MaxValue;
|
|
lastUsePass = -1;
|
|
logicalResourceIndex = -1;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
isFree = true;
|
|
firstUsePass = int.MaxValue;
|
|
lastUsePass = -1;
|
|
logicalResourceIndex = -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a GPU memory heap for placed resources.
|
|
/// Supports D3D12-style heap tier 2 (buffers and textures can alias).
|
|
/// </summary>
|
|
internal sealed class ResourceHeap
|
|
{
|
|
public int index;
|
|
public ulong size;
|
|
private readonly List<MemoryBlock> _blocks = new(32);
|
|
|
|
// D3D12 heap alignment requirement (64KB for MSAA textures, 4KB for others)
|
|
public const ulong DEFAULT_ALIGNMENT = 65536; // 64KB
|
|
|
|
public ResourceHeap(int index, ulong initialSize = 16 * 1024 * 1024) // 16MB default
|
|
{
|
|
this.index = index;
|
|
this.size = initialSize;
|
|
|
|
// Initially one large free block
|
|
_blocks.Add(new MemoryBlock(0, initialSize));
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_blocks.Clear();
|
|
_blocks.Add(new MemoryBlock(0, size));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to allocate a block of the requested size with proper alignment.
|
|
/// Uses best-fit algorithm with lifetime-aware allocation.
|
|
/// </summary>
|
|
public (bool success, ulong offset, MemoryBlock block) TryAllocate(
|
|
ulong requestedSize,
|
|
int firstUsePass,
|
|
int lastUsePass,
|
|
int logicalResourceIndex,
|
|
ulong alignment = DEFAULT_ALIGNMENT)
|
|
{
|
|
var alignedSize = AlignUp(requestedSize, alignment);
|
|
|
|
var bestFitIndex = -1;
|
|
ulong bestFitOffset = 0;
|
|
var smallestWaste = ulong.MaxValue;
|
|
|
|
// Find the best fit block that doesn't overlap with lifetime
|
|
var blockSpan = CollectionsMarshal.AsSpan(_blocks);
|
|
for (var i = 0; i < blockSpan.Length; i++)
|
|
{
|
|
ref var block = ref blockSpan[i];
|
|
|
|
// Try to find space within this block
|
|
var alignedOffset = AlignUp(block.offset, alignment);
|
|
var endOffset = alignedOffset + alignedSize;
|
|
|
|
if (endOffset <= block.offset + block.size)
|
|
{
|
|
// Check if this offset range conflicts with ANY existing allocations
|
|
var canUseOffset = CanPlaceAtOffset(alignedOffset, alignedSize, firstUsePass, lastUsePass);
|
|
|
|
if (canUseOffset)
|
|
{
|
|
var waste = block.size - alignedSize;
|
|
|
|
if (waste < smallestWaste)
|
|
{
|
|
smallestWaste = waste;
|
|
bestFitIndex = i;
|
|
bestFitOffset = alignedOffset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestFitIndex == -1)
|
|
{
|
|
return (false, 0, default);
|
|
}
|
|
|
|
ref var bestFit = ref CollectionsMarshal.AsSpan(_blocks)[bestFitIndex];
|
|
|
|
// If the block is free, we need to split it
|
|
if (bestFit.isFree)
|
|
{
|
|
var remainingSize = (bestFit.offset + bestFit.size) - (bestFitOffset + alignedSize);
|
|
|
|
// Update the current block to be allocated
|
|
bestFit.offset = bestFitOffset;
|
|
bestFit.size = alignedSize;
|
|
bestFit.isFree = false;
|
|
bestFit.firstUsePass = firstUsePass;
|
|
bestFit.lastUsePass = lastUsePass;
|
|
bestFit.logicalResourceIndex = logicalResourceIndex;
|
|
|
|
// Create a new free block for the remaining space if there is any
|
|
if (remainingSize > 0)
|
|
{
|
|
var newBlock = new MemoryBlock(bestFitOffset + alignedSize, remainingSize);
|
|
_blocks.Insert(bestFitIndex + 1, newBlock);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Block is already allocated but lifetime doesn't overlap, we can alias it
|
|
// Create a new aliased block at the same location
|
|
var aliasedBlock = new MemoryBlock(bestFitOffset, alignedSize)
|
|
{
|
|
isFree = false,
|
|
firstUsePass = firstUsePass,
|
|
lastUsePass = lastUsePass,
|
|
logicalResourceIndex = logicalResourceIndex
|
|
};
|
|
|
|
// Insert in sorted order by offset
|
|
var insertIndex = 0;
|
|
for (var i = 0; i < _blocks.Count; i++)
|
|
{
|
|
if (_blocks[i].offset > bestFitOffset)
|
|
{
|
|
break;
|
|
}
|
|
insertIndex = i + 1;
|
|
}
|
|
_blocks.Insert(insertIndex, aliasedBlock);
|
|
// Update bestFit to point to the newly inserted block
|
|
bestFit = ref CollectionsMarshal.AsSpan(_blocks)[insertIndex];
|
|
}
|
|
|
|
return (true, bestFitOffset, bestFit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a resource can be placed at the given offset without lifetime conflicts.
|
|
/// Must check ALL blocks that overlap with this offset range.
|
|
/// </summary>
|
|
private bool CanPlaceAtOffset(ulong offset, ulong size, int firstUsePass, int lastUsePass)
|
|
{
|
|
var endOffset = offset + size;
|
|
|
|
foreach (var block in _blocks)
|
|
{
|
|
// Skip free blocks - they don't have lifetime constraints
|
|
if (block.isFree)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if this block's memory range overlaps with our target range
|
|
var blockEnd = block.offset + block.size;
|
|
var memoryOverlap = !(offset >= blockEnd || endOffset <= block.offset);
|
|
|
|
if (memoryOverlap)
|
|
{
|
|
// Memory ranges overlap, check if lifetimes also overlap
|
|
var lifetimeOverlap = !(firstUsePass > block.lastUsePass || lastUsePass < block.firstUsePass);
|
|
|
|
if (lifetimeOverlap)
|
|
{
|
|
// Both memory AND lifetime overlap - cannot place here!
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total memory that would be used if no aliasing occurred.
|
|
/// </summary>
|
|
public ulong GetTotalAllocatedWithoutAliasing()
|
|
{
|
|
ulong total = 0;
|
|
foreach (var block in _blocks)
|
|
{
|
|
if (!block.isFree)
|
|
{
|
|
total += block.size;
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the peak memory usage considering aliasing (max offset + size).
|
|
/// </summary>
|
|
public ulong GetPeakUsage()
|
|
{
|
|
ulong peak = 0;
|
|
foreach (var block in _blocks)
|
|
{
|
|
if (!block.isFree)
|
|
{
|
|
peak = Math.Max(peak, block.offset + block.size);
|
|
}
|
|
}
|
|
|
|
return peak;
|
|
}
|
|
|
|
private static ulong AlignUp(ulong value, ulong alignment)
|
|
{
|
|
return (value + alignment - 1) & ~(alignment - 1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a placed resource within a heap.
|
|
/// </summary>
|
|
internal sealed class PlacedResource
|
|
{
|
|
public int index;
|
|
public RenderGraphResourceType type;
|
|
public ulong heapOffset;
|
|
public ulong sizeInBytes;
|
|
|
|
// Lifetime tracking
|
|
public int firstUsePass = int.MaxValue;
|
|
public int lastUsePass = -1;
|
|
|
|
// Aliasing tracking
|
|
public readonly List<int> aliasedLogicalResources = new(4);
|
|
public MemoryBlock memoryBlock;
|
|
|
|
public void Reset()
|
|
{
|
|
index = -1;
|
|
type = RenderGraphResourceType.Texture;
|
|
heapOffset = 0;
|
|
sizeInBytes = 0;
|
|
firstUsePass = int.MaxValue;
|
|
lastUsePass = -1;
|
|
aliasedLogicalResources.Clear();
|
|
memoryBlock = default;
|
|
}
|
|
|
|
public void UpdateLifetime(int passIndex)
|
|
{
|
|
firstUsePass = Math.Min(firstUsePass, passIndex);
|
|
lastUsePass = Math.Max(lastUsePass, passIndex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages physical resource allocation and aliasing using heap-based allocation.
|
|
/// Supports D3D12 heap tier 2: buffers and textures can alias as long as lifetimes don't overlap.
|
|
/// </summary>
|
|
internal sealed class ResourceAliasingManager
|
|
{
|
|
private readonly RenderGraphObjectPool _pool;
|
|
|
|
private readonly ResourceHeap _heap;
|
|
private readonly List<PlacedResource> _placedResources;
|
|
// Mapping from logical resource index to placed resource index
|
|
private readonly Dictionary<int, int> _logicalToPlaced;
|
|
|
|
// D3D12 alignment constants
|
|
private const ulong _DEFAULT_TEXTURE_ALIGNMENT = 65536; // 64KB
|
|
private const ulong _DEFAULT_BUFFER_ALIGNMENT = 65536; // 64KB for D3D12
|
|
|
|
public ResourceHeap Heap => _heap;
|
|
|
|
/// <summary>
|
|
/// Helper method to get the size of a resource
|
|
/// </summary>
|
|
private ulong GetResourceSize(RenderGraphResource resource)
|
|
{
|
|
if (resource.type == RenderGraphResourceType.Texture)
|
|
{
|
|
var textureDesc = resource.rgTextureDesc.ToTextureDesc(resource.resolvedWidth, resource.resolvedHeight);
|
|
return AlignUp(textureDesc.GetTotalBytes(), _DEFAULT_TEXTURE_ALIGNMENT);
|
|
}
|
|
else // Buffer
|
|
{
|
|
return resource.bufferDesc.Size;
|
|
}
|
|
}
|
|
|
|
public ResourceAliasingManager(RenderGraphObjectPool pool)
|
|
{
|
|
_pool = pool;
|
|
_heap = new ResourceHeap(0);
|
|
_placedResources = new List<PlacedResource>(32);
|
|
_logicalToPlaced = new Dictionary<int, int>(64);
|
|
}
|
|
|
|
public void BeginFrame()
|
|
{
|
|
for (var i = 0; i < _placedResources.Count; i++)
|
|
{
|
|
_pool.Return(_placedResources[i]);
|
|
}
|
|
|
|
_placedResources.Clear();
|
|
_logicalToPlaced.Clear();
|
|
|
|
_heap.Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns physical resources (placed resources) to logical resources using heap-based allocation.
|
|
/// This is the modern D3D12 approach: check if resource fits in a hole, not if it matches size/format.
|
|
/// Uses a two-pass algorithm:
|
|
/// 1. First pass: Simulate allocation to determine peak memory usage
|
|
/// 2. Second pass: Create a single heap of the peak size and do the real allocation
|
|
/// </summary>
|
|
public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount)
|
|
{
|
|
// Build list of all logical resources (both textures and buffers) with their lifetimes
|
|
var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent();
|
|
|
|
// Iterate through all resources in unified list
|
|
for (var i = 0; i < registry.ResourceCount; i++)
|
|
{
|
|
var resource = registry.GetResourceByIndex(i);
|
|
if (!resource.isImported) // Don't alias imported resources
|
|
{
|
|
logicalResources.Add((resource.index, resource));
|
|
}
|
|
}
|
|
|
|
// Sort by size descending (larger resources first for better packing)
|
|
logicalResources.Sort((a, b) =>
|
|
{
|
|
var sizeA = GetResourceSize(a.resource);
|
|
var sizeB = GetResourceSize(b.resource);
|
|
return sizeB.CompareTo(sizeA); // Descending
|
|
});
|
|
|
|
// ===== PASS 1: Simulate allocation to determine peak memory usage =====
|
|
var simulationHeap = new ResourceHeap(0, ulong.MaxValue); // Unlimited size for simulation
|
|
foreach (var (logicalIndex, logicalResource) in logicalResources)
|
|
{
|
|
var size = GetResourceSize(logicalResource);
|
|
var alignment = logicalResource.type == RenderGraphResourceType.Texture
|
|
? _DEFAULT_TEXTURE_ALIGNMENT
|
|
: _DEFAULT_BUFFER_ALIGNMENT;
|
|
|
|
var (success, offset, block) = simulationHeap.TryAllocate(
|
|
size,
|
|
logicalResource.firstUsePass,
|
|
logicalResource.lastUsePass,
|
|
logicalIndex,
|
|
alignment);
|
|
|
|
if (!success)
|
|
{
|
|
throw new InvalidOperationException("Simulation allocation failed - this should never happen with unlimited heap");
|
|
}
|
|
}
|
|
|
|
// Get peak usage from simulation
|
|
var peakMemoryUsage = simulationHeap.GetPeakUsage();
|
|
|
|
// Align peak usage to 64KB (D3D12 requirement)
|
|
peakMemoryUsage = AlignUp(peakMemoryUsage, _DEFAULT_TEXTURE_ALIGNMENT);
|
|
|
|
// ===== PASS 2: Create a single heap of the peak size and do the real allocation =====
|
|
_heap.size = peakMemoryUsage;
|
|
_heap.Reset();
|
|
|
|
// Allocate each logical resource in the heap
|
|
foreach (var (logicalIndex, logicalResource) in logicalResources)
|
|
{
|
|
var size = GetResourceSize(logicalResource);
|
|
var alignment = logicalResource.type == RenderGraphResourceType.Texture
|
|
? _DEFAULT_TEXTURE_ALIGNMENT
|
|
: _DEFAULT_BUFFER_ALIGNMENT;
|
|
|
|
var (success, offset, block) = _heap.TryAllocate(
|
|
size,
|
|
logicalResource.firstUsePass,
|
|
logicalResource.lastUsePass,
|
|
logicalIndex,
|
|
alignment);
|
|
|
|
if (!success)
|
|
{
|
|
throw new InvalidOperationException("Real allocation failed - this should match simulation");
|
|
}
|
|
|
|
var assignedOffset = offset;
|
|
var assignedBlock = block;
|
|
|
|
var assignedPlaced = _pool.Rent<PlacedResource>();
|
|
assignedPlaced.index = _placedResources.Count;
|
|
assignedPlaced.type = logicalResource.type;
|
|
assignedPlaced.heapOffset = assignedOffset;
|
|
assignedPlaced.sizeInBytes = size;
|
|
assignedPlaced.firstUsePass = logicalResource.firstUsePass;
|
|
assignedPlaced.lastUsePass = logicalResource.lastUsePass;
|
|
assignedPlaced.memoryBlock = assignedBlock;
|
|
assignedPlaced.aliasedLogicalResources.Clear();
|
|
assignedPlaced.aliasedLogicalResources.Add(logicalIndex);
|
|
|
|
_placedResources.Add(assignedPlaced);
|
|
_logicalToPlaced[logicalIndex] = assignedPlaced.index;
|
|
}
|
|
|
|
// Second pass: Populate aliasedLogicalResources lists
|
|
// For each placed resource, find all OTHER placed resources at the same offset
|
|
for (var i = 0; i < _placedResources.Count; i++)
|
|
{
|
|
var placed = _placedResources[i];
|
|
|
|
// Find all logical resources that share the same heap location
|
|
for (var j = 0; j < _placedResources.Count; j++)
|
|
{
|
|
if (i == j)
|
|
{
|
|
continue; // Skip self
|
|
}
|
|
|
|
var other = _placedResources[j];
|
|
|
|
// Check if they're at the same offset
|
|
if (other.heapOffset == placed.heapOffset)
|
|
{
|
|
// Add the other's logical resource to this one's aliased list
|
|
var otherLogicalIndex = other.aliasedLogicalResources[0]; // Each has exactly one at this point
|
|
if (!placed.aliasedLogicalResources.Contains(otherLogicalIndex))
|
|
{
|
|
placed.aliasedLogicalResources.Add(otherLogicalIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources);
|
|
}
|
|
|
|
public int GetPlacedResourceIndex(int logicalIndex)
|
|
{
|
|
return _logicalToPlaced.TryGetValue(logicalIndex, out var placedIndex) ? placedIndex : -1;
|
|
}
|
|
|
|
public PlacedResource? GetPlacedResource(int placedIndex)
|
|
{
|
|
return placedIndex >= 0 && placedIndex < _placedResources.Count
|
|
? _placedResources[placedIndex]
|
|
: null;
|
|
}
|
|
|
|
private static ulong AlignUp(ulong value, ulong alignment)
|
|
{
|
|
return (value + alignment - 1) & ~(alignment - 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores aliasing state from cache.
|
|
/// </summary>
|
|
public void RestoreFromCache(Dictionary<int, int> logicalToPlaced, List<PlacedResourceData> placedData)
|
|
{
|
|
_logicalToPlaced.Clear();
|
|
foreach (var kvp in logicalToPlaced)
|
|
{
|
|
_logicalToPlaced[kvp.Key] = kvp.Value;
|
|
}
|
|
|
|
// Restore placed resources
|
|
for (var i = 0; i < placedData.Count; i++)
|
|
{
|
|
var placed = _pool.Rent<PlacedResource>();
|
|
|
|
var data = placedData[i];
|
|
placed.index = data.index;
|
|
placed.type = data.type;
|
|
placed.heapOffset = data.heapOffset;
|
|
placed.sizeInBytes = data.sizeInBytes;
|
|
placed.firstUsePass = data.firstUsePass;
|
|
placed.lastUsePass = data.lastUsePass;
|
|
|
|
placed.aliasedLogicalResources.Clear();
|
|
|
|
_placedResources.Add(placed);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores current aliasing state to cache.
|
|
/// </summary>
|
|
public void StoreToCache(Dictionary<int, int> outLogicalToPlaced, List<PlacedResourceData> outPlacedData)
|
|
{
|
|
outLogicalToPlaced.Clear();
|
|
foreach (var kvp in _logicalToPlaced)
|
|
{
|
|
outLogicalToPlaced[kvp.Key] = kvp.Value;
|
|
}
|
|
|
|
outPlacedData.Clear();
|
|
for (var i = 0; i < _placedResources.Count; i++)
|
|
{
|
|
var placed = _placedResources[i];
|
|
outPlacedData.Add(new PlacedResourceData
|
|
{
|
|
index = placed.index,
|
|
type = placed.type,
|
|
heapOffset = placed.heapOffset,
|
|
sizeInBytes = placed.sizeInBytes,
|
|
firstUsePass = placed.firstUsePass,
|
|
lastUsePass = placed.lastUsePass
|
|
});
|
|
}
|
|
}
|
|
}
|