forked from Misaki/GhostEngine
Render graph: native pass merging & heap-based aliasing
Major architecture upgrade: - Add native render pass merging (hardware pass grouping, load/store op inference) - Implement heap-based aliasing for textures & buffers (D3D12-style) - Unify resource model: buffers and textures in one registry - Extend builder API for buffer creation/usage, access flags, hints - Improve barrier/state tracking (buffer hints, indirect argument state) - Update caching, hashing, and debug output for new model - Add enums/structs: AttachmentLoadOp, StoreOp, BufferHint, etc. - D3D12 backend: support named resources, temp upload buffers, correct heap usage - Update docs, benchmarks, and project files for new features Brings render graph closer to AAA engine standards, enabling efficient memory usage, lower driver overhead, and a more flexible API.
This commit is contained in:
@@ -1,44 +1,278 @@
|
||||
using Ghost.Core.Utilities;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.RenderGraph.Concept;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a physical GPU resource that can be aliased by multiple logical resources.
|
||||
/// Represents a memory block within a heap.
|
||||
/// </summary>
|
||||
internal sealed class PhysicalResource
|
||||
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 int width;
|
||||
public int height;
|
||||
public TextureFormat format;
|
||||
public int sizeInBytes;
|
||||
|
||||
public ulong size;
|
||||
private readonly List<MemoryBlock> _blocks = new(32);
|
||||
|
||||
// D3D12 heap alignment requirement (64KB for MSAA textures, 4KB for others)
|
||||
private const ulong DefaultAlignment = 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 = DefaultAlignment)
|
||||
{
|
||||
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 int heapIndex;
|
||||
public ulong heapOffset;
|
||||
public ulong sizeInBytes;
|
||||
|
||||
// Original descriptor
|
||||
public TextureDescriptor textureDesc;
|
||||
public BufferDescriptor bufferDesc;
|
||||
|
||||
// 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;
|
||||
width = 0;
|
||||
height = 0;
|
||||
format = TextureFormat.RGBA8;
|
||||
type = RenderGraphResourceType.Texture;
|
||||
heapIndex = -1;
|
||||
heapOffset = 0;
|
||||
sizeInBytes = 0;
|
||||
textureDesc = default;
|
||||
bufferDesc = default;
|
||||
firstUsePass = int.MaxValue;
|
||||
lastUsePass = -1;
|
||||
aliasedLogicalResources.Clear();
|
||||
}
|
||||
|
||||
public bool CanAlias(TextureDescriptor descriptor)
|
||||
{
|
||||
// For aliasing, resources must be identical in size and format
|
||||
// In a real implementation, you could be more flexible (e.g., same size but different format)
|
||||
return width == descriptor.width &&
|
||||
height == descriptor.height &&
|
||||
format == descriptor.format;
|
||||
memoryBlock = default;
|
||||
}
|
||||
|
||||
public void UpdateLifetime(int passIndex)
|
||||
@@ -46,148 +280,293 @@ internal sealed class PhysicalResource
|
||||
firstUsePass = Math.Min(firstUsePass, passIndex);
|
||||
lastUsePass = Math.Max(lastUsePass, passIndex);
|
||||
}
|
||||
|
||||
public bool IsAliveAt(int passIndex)
|
||||
{
|
||||
return passIndex >= firstUsePass && passIndex <= lastUsePass;
|
||||
}
|
||||
|
||||
public int CalculateSize()
|
||||
{
|
||||
int bytesPerPixel = format switch
|
||||
{
|
||||
TextureFormat.RGBA8 => 4,
|
||||
TextureFormat.RGBA16F => 8,
|
||||
TextureFormat.RGBA32F => 16,
|
||||
TextureFormat.Depth32F => 4,
|
||||
TextureFormat.Depth24Stencil8 => 4,
|
||||
_ => 4
|
||||
};
|
||||
return width * height * bytesPerPixel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages physical resource allocation and aliasing.
|
||||
/// Uses interval scheduling algorithm to minimize memory usage.
|
||||
/// 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 List<PhysicalResource> _physicalResources = new(32);
|
||||
private readonly List<ResourceHeap> _heaps = new(4);
|
||||
private readonly List<PlacedResource> _placedResources = new(32);
|
||||
private readonly RenderGraphObjectPool _pool = new();
|
||||
private int _physicalResourceCount;
|
||||
|
||||
// Mapping from logical resource index to physical resource index
|
||||
private readonly Dictionary<int, int> _logicalToPhysical = new(64);
|
||||
|
||||
// Mapping from logical resource index to placed resource index
|
||||
private readonly Dictionary<int, int> _logicalToPlaced = new(64);
|
||||
|
||||
// D3D12 alignment constants
|
||||
private const ulong DefaultTextureAlignment = 65536; // 64KB
|
||||
private const ulong DefaultBufferAlignment = 65536; // 64KB for D3D12
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_physicalResourceCount = 0;
|
||||
_logicalToPhysical.Clear();
|
||||
|
||||
// Reset physical resources but keep them in the pool
|
||||
for (int i = 0; i < _physicalResources.Count; i++)
|
||||
for (var i = 0; i < _placedResources.Count; i++)
|
||||
{
|
||||
_physicalResources[i].Reset();
|
||||
_pool.Return(_placedResources[i]);
|
||||
}
|
||||
|
||||
_placedResources.Clear();
|
||||
_logicalToPlaced.Clear();
|
||||
|
||||
// Reset heaps
|
||||
for (var i = 0; i < _heaps.Count; i++)
|
||||
{
|
||||
_heaps[i].Reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns physical resources to logical resources using greedy interval scheduling.
|
||||
/// This minimizes total GPU memory usage.
|
||||
/// 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)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine("\n=== Resource Aliasing Analysis ===");
|
||||
int totalLogicalSize = 0;
|
||||
Console.WriteLine("\n=== Heap-Based Resource Aliasing Analysis ===");
|
||||
ulong totalLogicalSize = 0;
|
||||
#endif
|
||||
|
||||
// Build list of all logical resources with their lifetimes
|
||||
// Build list of all logical resources (both textures and buffers) with their lifetimes
|
||||
var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent();
|
||||
|
||||
for (int i = 0; i < registry.TextureResourceCount; i++)
|
||||
|
||||
// Iterate through all resources in unified list
|
||||
for (var i = 0; i < registry.ResourceCount; i++)
|
||||
{
|
||||
var resource = registry.GetTextureResourceByIndex(i);
|
||||
var resource = registry.GetResourceByIndex(i);
|
||||
if (!resource.isImported) // Don't alias imported resources
|
||||
{
|
||||
logicalResources.Add((i, resource));
|
||||
logicalResources.Add((resource.index, resource));
|
||||
#if DEBUG
|
||||
int size = CalculateSize(resource.descriptor);
|
||||
var size = resource.type == RenderGraphResourceType.Texture
|
||||
? CalculateTextureSize(resource.textureDescriptor)
|
||||
: resource.bufferDescriptor.sizeInBytes;
|
||||
totalLogicalSize += size;
|
||||
Console.WriteLine($"Logical Resource {i}: {resource.descriptor.name}");
|
||||
|
||||
var typeName = resource.type == RenderGraphResourceType.Texture ? "Texture" : "Buffer";
|
||||
var name = resource.type == RenderGraphResourceType.Texture
|
||||
? resource.textureDescriptor.name
|
||||
: resource.bufferDescriptor.name;
|
||||
|
||||
Console.WriteLine($"Logical {typeName} {i}: {name}");
|
||||
Console.WriteLine($" Lifetime: Pass {resource.firstUsePass} -> {resource.lastUsePass}");
|
||||
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by first use pass (earlier resources first)
|
||||
logicalResources.Sort((a, b) => a.resource.firstUsePass.CompareTo(b.resource.firstUsePass));
|
||||
// Sort by size descending (larger resources first for better packing)
|
||||
logicalResources.Sort(static (a, b) =>
|
||||
{
|
||||
var sizeA = a.resource.type == RenderGraphResourceType.Texture
|
||||
? CalculateTextureSize(a.resource.textureDescriptor)
|
||||
: a.resource.bufferDescriptor.sizeInBytes;
|
||||
var sizeB = b.resource.type == RenderGraphResourceType.Texture
|
||||
? CalculateTextureSize(b.resource.textureDescriptor)
|
||||
: b.resource.bufferDescriptor.sizeInBytes;
|
||||
return sizeB.CompareTo(sizeA); // Descending
|
||||
});
|
||||
|
||||
// Greedy interval scheduling: assign each logical resource to a physical resource
|
||||
// ===== 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)
|
||||
{
|
||||
PhysicalResource? assignedPhysical = null;
|
||||
ulong size;
|
||||
ulong alignment;
|
||||
|
||||
// Try to find an existing physical resource that:
|
||||
// 1. Has compatible format/size
|
||||
// 2. Is not alive during this logical resource's lifetime
|
||||
for (int i = 0; i < _physicalResourceCount; i++)
|
||||
if (logicalResource.type == RenderGraphResourceType.Texture)
|
||||
{
|
||||
var physical = _physicalResources[i];
|
||||
|
||||
if (physical.CanAlias(logicalResource.descriptor) &&
|
||||
!HasLifetimeOverlap(physical, logicalResource))
|
||||
{
|
||||
assignedPhysical = physical;
|
||||
break;
|
||||
}
|
||||
size = CalculateTextureSize(logicalResource.textureDescriptor);
|
||||
alignment = DefaultTextureAlignment;
|
||||
}
|
||||
else // Buffer
|
||||
{
|
||||
size = logicalResource.bufferDescriptor.sizeInBytes;
|
||||
alignment = DefaultBufferAlignment;
|
||||
}
|
||||
|
||||
// No compatible physical resource found, allocate a new one
|
||||
if (assignedPhysical == null)
|
||||
var (success, offset, block) = simulationHeap.TryAllocate(
|
||||
size,
|
||||
logicalResource.firstUsePass,
|
||||
logicalResource.lastUsePass,
|
||||
logicalIndex,
|
||||
alignment);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
assignedPhysical = GetOrCreatePhysicalResource();
|
||||
assignedPhysical.index = _physicalResourceCount - 1;
|
||||
assignedPhysical.width = logicalResource.descriptor.width;
|
||||
assignedPhysical.height = logicalResource.descriptor.height;
|
||||
assignedPhysical.format = logicalResource.descriptor.format;
|
||||
assignedPhysical.sizeInBytes = assignedPhysical.CalculateSize();
|
||||
|
||||
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, DefaultTextureAlignment);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\nAllocated NEW Physical Resource {assignedPhysical.index}:");
|
||||
Console.WriteLine($" Size: {assignedPhysical.width}x{assignedPhysical.height}");
|
||||
Console.WriteLine($" Format: {assignedPhysical.format}");
|
||||
Console.WriteLine($" Memory: {assignedPhysical.sizeInBytes / 1024.0:F2} KB");
|
||||
Console.WriteLine($"\nPeak Memory Usage: {peakMemoryUsage / (1024.0 * 1024.0):F2} MB");
|
||||
#endif
|
||||
}
|
||||
|
||||
// ===== PASS 2: Create a single heap of the peak size and do the real allocation =====
|
||||
var mainHeap = new ResourceHeap(0, peakMemoryUsage);
|
||||
_heaps.Add(mainHeap);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"Created Single Heap:");
|
||||
Console.WriteLine($" Size: {peakMemoryUsage / (1024.0 * 1024.0):F2} MB\n");
|
||||
#endif
|
||||
|
||||
// Allocate each logical resource in the heap
|
||||
foreach (var (logicalIndex, logicalResource) in logicalResources)
|
||||
{
|
||||
ulong size;
|
||||
ulong alignment;
|
||||
|
||||
if (logicalResource.type == RenderGraphResourceType.Texture)
|
||||
{
|
||||
size = CalculateTextureSize(logicalResource.textureDescriptor);
|
||||
alignment = DefaultTextureAlignment;
|
||||
}
|
||||
else // Buffer
|
||||
{
|
||||
size = logicalResource.bufferDescriptor.sizeInBytes;
|
||||
alignment = DefaultBufferAlignment;
|
||||
}
|
||||
|
||||
var (success, offset, block) = mainHeap.TryAllocate(
|
||||
size,
|
||||
logicalResource.firstUsePass,
|
||||
logicalResource.lastUsePass,
|
||||
logicalIndex,
|
||||
alignment);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new InvalidOperationException("Real allocation failed - this should match simulation");
|
||||
}
|
||||
|
||||
var assignedHeapIndex = 0;
|
||||
var assignedOffset = offset;
|
||||
var assignedBlock = block;
|
||||
|
||||
var assignedPlaced = _pool.Rent<PlacedResource>();
|
||||
assignedPlaced.index = _placedResources.Count;
|
||||
assignedPlaced.type = logicalResource.type;
|
||||
assignedPlaced.heapIndex = assignedHeapIndex;
|
||||
assignedPlaced.heapOffset = assignedOffset;
|
||||
assignedPlaced.sizeInBytes = size;
|
||||
assignedPlaced.firstUsePass = logicalResource.firstUsePass;
|
||||
assignedPlaced.lastUsePass = logicalResource.lastUsePass;
|
||||
assignedPlaced.memoryBlock = assignedBlock;
|
||||
|
||||
if (logicalResource.type == RenderGraphResourceType.Texture)
|
||||
{
|
||||
assignedPlaced.textureDesc = logicalResource.textureDescriptor;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"\nALIASING: {logicalResource.descriptor.name} -> Physical Resource {assignedPhysical.index}");
|
||||
assignedPlaced.bufferDesc = logicalResource.bufferDescriptor;
|
||||
}
|
||||
|
||||
assignedPlaced.aliasedLogicalResources.Clear();
|
||||
assignedPlaced.aliasedLogicalResources.Add(logicalIndex);
|
||||
|
||||
_placedResources.Add(assignedPlaced);
|
||||
|
||||
#if DEBUG
|
||||
var isAliased = assignedBlock.logicalResourceIndex != logicalIndex && assignedBlock.logicalResourceIndex != -1;
|
||||
var name = logicalResource.type == RenderGraphResourceType.Texture
|
||||
? logicalResource.textureDescriptor.name
|
||||
: logicalResource.bufferDescriptor.name;
|
||||
var typeName = logicalResource.type == RenderGraphResourceType.Texture ? "Texture" : "Buffer";
|
||||
|
||||
if (isAliased)
|
||||
{
|
||||
Console.WriteLine($"\nALIASING {typeName}: {name}");
|
||||
Console.WriteLine($" Placed in Heap {assignedHeapIndex} at offset {assignedOffset} ({assignedOffset / 1024.0:F2} KB)");
|
||||
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"\nAllocated {typeName}: {name}");
|
||||
Console.WriteLine($" Heap {assignedHeapIndex}, Offset {assignedOffset} ({assignedOffset / 1024.0:F2} KB)");
|
||||
Console.WriteLine($" Size: {size / 1024.0:F2} KB");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update physical resource lifetime
|
||||
assignedPhysical.UpdateLifetime(logicalResource.firstUsePass);
|
||||
assignedPhysical.UpdateLifetime(logicalResource.lastUsePass);
|
||||
assignedPhysical.aliasedLogicalResources.Add(logicalIndex);
|
||||
|
||||
// Record the mapping
|
||||
_logicalToPhysical[logicalIndex] = assignedPhysical.index;
|
||||
_logicalToPlaced[logicalIndex] = assignedPlaced.index;
|
||||
}
|
||||
|
||||
// Second pass: Populate aliasedLogicalResources lists
|
||||
// For each placed resource, find all OTHER placed resources at the same heap+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 heap+offset
|
||||
if (other.heapIndex == placed.heapIndex && 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
int totalPhysicalSize = 0;
|
||||
for (int i = 0; i < _physicalResourceCount; i++)
|
||||
// Debug output: Show which resources alias with each other
|
||||
Console.WriteLine("\n=== Aliasing Groups ===");
|
||||
var processedOffsets = new HashSet<(int heapIndex, ulong offset)>();
|
||||
for (var i = 0; i < _placedResources.Count; i++)
|
||||
{
|
||||
totalPhysicalSize += _physicalResources[i].sizeInBytes;
|
||||
var placed = _placedResources[i];
|
||||
var key = (placed.heapIndex, placed.heapOffset);
|
||||
|
||||
if (!processedOffsets.Contains(key) && placed.aliasedLogicalResources.Count > 1)
|
||||
{
|
||||
processedOffsets.Add(key);
|
||||
Console.WriteLine($"Heap {placed.heapIndex} @ Offset {placed.heapOffset / 1024.0:F2} KB ({placed.aliasedLogicalResources.Count} resources):");
|
||||
|
||||
foreach (var logicalIdx in placed.aliasedLogicalResources)
|
||||
{
|
||||
var res = registry.GetResourceByIndex(logicalIdx);
|
||||
var name = res.type == RenderGraphResourceType.Texture
|
||||
? res.textureDescriptor.name
|
||||
: res.bufferDescriptor.name;
|
||||
Console.WriteLine($" - {name} (Pass {res.firstUsePass}-{res.lastUsePass})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== Aliasing Summary ===");
|
||||
Console.WriteLine("=======================\n");
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
ulong totalPhysicalSize = 0;
|
||||
for (var i = 0; i < _heaps.Count; i++)
|
||||
{
|
||||
totalPhysicalSize += _heaps[i].GetPeakUsage();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== Heap-Based Aliasing Summary ===");
|
||||
Console.WriteLine($"Logical Resources: {logicalResources.Count}");
|
||||
Console.WriteLine($"Physical Resources: {_physicalResourceCount}");
|
||||
Console.WriteLine($"Placed Resources: {_placedResources.Count}");
|
||||
Console.WriteLine($"Heaps: {_heaps.Count}");
|
||||
Console.WriteLine($"Total Logical Memory: {totalLogicalSize / 1024.0:F2} KB");
|
||||
Console.WriteLine($"Total Physical Memory: {totalPhysicalSize / 1024.0:F2} KB");
|
||||
Console.WriteLine($"Memory Saved: {(totalLogicalSize - totalPhysicalSize) / 1024.0:F2} KB ({(1.0 - (double)totalPhysicalSize / totalLogicalSize) * 100.0:F1}%)");
|
||||
@@ -197,48 +576,21 @@ internal sealed class ResourceAliasingManager
|
||||
ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources);
|
||||
}
|
||||
|
||||
public int GetPhysicalResourceIndex(int logicalIndex)
|
||||
public int GetPlacedResourceIndex(int logicalIndex)
|
||||
{
|
||||
return _logicalToPhysical.TryGetValue(logicalIndex, out var physicalIndex) ? physicalIndex : -1;
|
||||
return _logicalToPlaced.TryGetValue(logicalIndex, out var placedIndex) ? placedIndex : -1;
|
||||
}
|
||||
|
||||
public PhysicalResource? GetPhysicalResource(int physicalIndex)
|
||||
public PlacedResource? GetPlacedResource(int placedIndex)
|
||||
{
|
||||
return physicalIndex >= 0 && physicalIndex < _physicalResourceCount
|
||||
? _physicalResources[physicalIndex]
|
||||
return placedIndex >= 0 && placedIndex < _placedResources.Count
|
||||
? _placedResources[placedIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
private bool HasLifetimeOverlap(PhysicalResource physical, RenderGraphResource logical)
|
||||
private static ulong CalculateTextureSize(TextureDescriptor descriptor)
|
||||
{
|
||||
// Check if the lifetimes overlap
|
||||
// No overlap if: logical.First > physical.Last OR logical.Last < physical.First
|
||||
return !(logical.firstUsePass > physical.lastUsePass ||
|
||||
logical.lastUsePass < physical.firstUsePass);
|
||||
}
|
||||
|
||||
private PhysicalResource GetOrCreatePhysicalResource()
|
||||
{
|
||||
PhysicalResource resource;
|
||||
if (_physicalResourceCount < _physicalResources.Count)
|
||||
{
|
||||
resource = _physicalResources[_physicalResourceCount];
|
||||
resource.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
resource = _pool.Rent<PhysicalResource>();
|
||||
resource.Reset();
|
||||
_physicalResources.Add(resource);
|
||||
}
|
||||
|
||||
_physicalResourceCount++;
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static int CalculateSize(TextureDescriptor descriptor)
|
||||
{
|
||||
int bytesPerPixel = descriptor.format switch
|
||||
var bytesPerPixel = descriptor.format switch
|
||||
{
|
||||
TextureFormat.RGBA8 => 4,
|
||||
TextureFormat.RGBA16F => 8,
|
||||
@@ -247,82 +599,87 @@ internal sealed class ResourceAliasingManager
|
||||
TextureFormat.Depth24Stencil8 => 4,
|
||||
_ => 4
|
||||
};
|
||||
return descriptor.width * descriptor.height * bytesPerPixel;
|
||||
|
||||
// Add alignment padding (D3D12 requires 64KB alignment)
|
||||
var size = (ulong)(descriptor.width * descriptor.height * bytesPerPixel);
|
||||
return AlignUp(size, DefaultTextureAlignment);
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
{
|
||||
return (value + alignment - 1) & ~(alignment - 1);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (int i = 0; i < _physicalResources.Count; i++)
|
||||
for (var i = 0; i < _placedResources.Count; i++)
|
||||
{
|
||||
_pool.Return(_physicalResources[i]);
|
||||
_pool.Return(_placedResources[i]);
|
||||
}
|
||||
_physicalResources.Clear();
|
||||
_physicalResourceCount = 0;
|
||||
_logicalToPhysical.Clear();
|
||||
_placedResources.Clear();
|
||||
_logicalToPlaced.Clear();
|
||||
_heaps.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores aliasing state from cache.
|
||||
/// </summary>
|
||||
public void RestoreFromCache(Dictionary<int, int> logicalToPhysical, List<PhysicalResourceData> physicalData)
|
||||
public void RestoreFromCache(Dictionary<int, int> logicalToPlaced, List<PlacedResourceData> placedData)
|
||||
{
|
||||
_logicalToPhysical.Clear();
|
||||
foreach (var kvp in logicalToPhysical)
|
||||
_logicalToPlaced.Clear();
|
||||
foreach (var kvp in logicalToPlaced)
|
||||
{
|
||||
_logicalToPhysical[kvp.Key] = kvp.Value;
|
||||
_logicalToPlaced[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
// Restore physical resources
|
||||
_physicalResourceCount = physicalData.Count;
|
||||
for (int i = 0; i < physicalData.Count; i++)
|
||||
// Restore placed resources
|
||||
for (var i = 0; i < placedData.Count; i++)
|
||||
{
|
||||
PhysicalResource physical;
|
||||
if (i < _physicalResources.Count)
|
||||
{
|
||||
physical = _physicalResources[i];
|
||||
physical.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
physical = _pool.Rent<PhysicalResource>();
|
||||
physical.Reset();
|
||||
_physicalResources.Add(physical);
|
||||
}
|
||||
var placed = _pool.Rent<PlacedResource>();
|
||||
|
||||
var data = physicalData[i];
|
||||
physical.index = data.index;
|
||||
physical.width = data.width;
|
||||
physical.height = data.height;
|
||||
physical.format = data.format;
|
||||
physical.firstUsePass = data.firstUsePass;
|
||||
physical.lastUsePass = data.lastUsePass;
|
||||
physical.sizeInBytes = physical.CalculateSize();
|
||||
var data = placedData[i];
|
||||
placed.index = data.index;
|
||||
placed.type = data.type;
|
||||
placed.heapIndex = data.heapIndex;
|
||||
placed.heapOffset = data.heapOffset;
|
||||
placed.sizeInBytes = data.sizeInBytes;
|
||||
placed.textureDesc = data.textureDesc;
|
||||
placed.bufferDesc = data.bufferDesc;
|
||||
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> outLogicalToPhysical, List<PhysicalResourceData> outPhysicalData)
|
||||
public void StoreToCache(Dictionary<int, int> outLogicalToPlaced, List<PlacedResourceData> outPlacedData)
|
||||
{
|
||||
outLogicalToPhysical.Clear();
|
||||
foreach (var kvp in _logicalToPhysical)
|
||||
outLogicalToPlaced.Clear();
|
||||
foreach (var kvp in _logicalToPlaced)
|
||||
{
|
||||
outLogicalToPhysical[kvp.Key] = kvp.Value;
|
||||
outLogicalToPlaced[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
outPhysicalData.Clear();
|
||||
for (int i = 0; i < _physicalResourceCount; i++)
|
||||
outPlacedData.Clear();
|
||||
for (var i = 0; i < _placedResources.Count; i++)
|
||||
{
|
||||
var physical = _physicalResources[i];
|
||||
outPhysicalData.Add(new PhysicalResourceData
|
||||
var placed = _placedResources[i];
|
||||
outPlacedData.Add(new PlacedResourceData
|
||||
{
|
||||
index = physical.index,
|
||||
width = physical.width,
|
||||
height = physical.height,
|
||||
format = physical.format,
|
||||
firstUsePass = physical.firstUsePass,
|
||||
lastUsePass = physical.lastUsePass
|
||||
index = placed.index,
|
||||
type = placed.type,
|
||||
heapIndex = placed.heapIndex,
|
||||
heapOffset = placed.heapOffset,
|
||||
sizeInBytes = placed.sizeInBytes,
|
||||
textureDesc = placed.textureDesc,
|
||||
bufferDesc = placed.bufferDesc,
|
||||
firstUsePass = placed.firstUsePass,
|
||||
lastUsePass = placed.lastUsePass
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user