Render graph integration and resource management refactor
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.
This commit is contained in:
1386
Ghost.Graphics/RenderGraphModule/RenderGraph.cs
Normal file
1386
Ghost.Graphics/RenderGraphModule/RenderGraph.cs
Normal file
File diff suppressed because it is too large
Load Diff
543
Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs
Normal file
543
Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs
Normal file
@@ -0,0 +1,543 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
117
Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs
Normal file
117
Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a resource barrier that needs to be inserted.
|
||||
/// For D3D12 aliasing barriers: ResourceBefore is the old resource, ResourceAfter is the new resource.
|
||||
/// </summary>
|
||||
internal struct ResourceBarrier
|
||||
{
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct barrier_union
|
||||
{
|
||||
internal struct barrier_union_transition
|
||||
{
|
||||
public Identifier<RGResource> resource;
|
||||
public ResourceState stateBefore;
|
||||
public ResourceState stateAfter;
|
||||
}
|
||||
|
||||
internal struct barrier_union_aliasing
|
||||
{
|
||||
public Identifier<RGResource> resourceBefore;
|
||||
public Identifier<RGResource> resourceAfter;
|
||||
}
|
||||
|
||||
// TODO: union can not have non-blittable types
|
||||
|
||||
[FieldOffset(0)]
|
||||
public barrier_union_transition transition;
|
||||
[FieldOffset(0)]
|
||||
public barrier_union_aliasing aliasing;
|
||||
}
|
||||
|
||||
private barrier_union _union;
|
||||
|
||||
public BarrierType Type
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public int PassIndex
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
// For Transition and UAV barriers
|
||||
public readonly Identifier<RGResource> Resource => _union.transition.resource;
|
||||
public readonly ResourceState StateBefore => _union.transition.stateBefore;
|
||||
public readonly ResourceState StateAfter => _union.transition.stateAfter;
|
||||
|
||||
// For Aliasing barriers (D3D12_RESOURCE_BARRIER::Aliasing)
|
||||
public readonly Identifier<RGResource> ResourceBefore => _union.aliasing.resourceBefore;
|
||||
public readonly Identifier<RGResource> ResourceAfter => _union.aliasing.resourceAfter;
|
||||
|
||||
// Constructor for Aliasing barriers
|
||||
public static ResourceBarrier CreateAliasingBarrier(
|
||||
Identifier<RGResource> resourceBefore,
|
||||
Identifier<RGResource> resourceAfter,
|
||||
int passIndex)
|
||||
{
|
||||
return new ResourceBarrier
|
||||
{
|
||||
Type = BarrierType.Aliasing,
|
||||
PassIndex = passIndex,
|
||||
_union = new barrier_union
|
||||
{
|
||||
aliasing = new barrier_union.barrier_union_aliasing
|
||||
{
|
||||
resourceBefore = resourceBefore,
|
||||
resourceAfter = resourceAfter
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ResourceBarrier CreateTransitionBarrier(
|
||||
Identifier<RGResource> resource,
|
||||
ResourceState before,
|
||||
ResourceState after,
|
||||
int passIndex)
|
||||
{
|
||||
return new ResourceBarrier
|
||||
{
|
||||
Type = BarrierType.Transition,
|
||||
PassIndex = passIndex,
|
||||
_union = new barrier_union
|
||||
{
|
||||
transition = new barrier_union.barrier_union_transition
|
||||
{
|
||||
resource = resource,
|
||||
stateBefore = before,
|
||||
stateAfter = after
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the current state of a resource across passes.
|
||||
/// </summary>
|
||||
internal sealed class ResourceStateTracker
|
||||
{
|
||||
public int resourceIndex;
|
||||
public ResourceState currentState = ResourceState.Common;
|
||||
public int lastAccessPass = -1;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
resourceIndex = -1;
|
||||
currentState = ResourceState.Common;
|
||||
lastAccessPass = -1;
|
||||
}
|
||||
}
|
||||
62
Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs
Normal file
62
Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Blackboard for sharing data between render passes.
|
||||
/// Uses a dictionary with type keys to store different pass data types.
|
||||
/// Avoids allocations by reusing the same dictionary across frames.
|
||||
/// </summary>
|
||||
public sealed class RenderGraphBlackboard
|
||||
{
|
||||
private readonly Dictionary<Type, IPassData> _data = new(16);
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates pass data in the blackboard.
|
||||
/// </summary>
|
||||
public void Add<T>(T data)
|
||||
where T : class, IPassData
|
||||
{
|
||||
var type = typeof(T);
|
||||
_data[type] = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves pass data from the blackboard.
|
||||
/// </summary>
|
||||
public T Get<T>()
|
||||
where T : class, IPassData
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (_data.TryGetValue(type, out var obj))
|
||||
{
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get pass data from the blackboard.
|
||||
/// </summary>
|
||||
public bool TryGet<T>(out T? data)
|
||||
where T : class, IPassData
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (_data.TryGetValue(type, out var obj))
|
||||
{
|
||||
data = (T)obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
data = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all data from the blackboard.
|
||||
/// Does not deallocate the backing dictionary to avoid allocations.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_data.Clear();
|
||||
}
|
||||
}
|
||||
318
Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs
Normal file
318
Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
[Flags]
|
||||
public enum AccessFlags : byte
|
||||
{
|
||||
None = 0,
|
||||
Read = 1 << 0,
|
||||
Write = 1 << 1,
|
||||
Discard = 1 << 2,
|
||||
|
||||
WriteAll = Write | Discard,
|
||||
ReadWrite = Read | Write,
|
||||
}
|
||||
|
||||
public interface IRenderGraphBuilder : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables or disables pass culling for the current context.
|
||||
/// </summary>
|
||||
/// <param name="value">A value indicating whether pass culling is allowed.</param>
|
||||
void AllowPassCulling(bool value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new texture resource based on the specified desc.
|
||||
/// </summary>
|
||||
/// <param name="desc">A structure that defines the properties and configuration of the texture to create.</param>
|
||||
/// <param name="name">The name of the texture resource.</param>
|
||||
/// <returns>An identifier for the newly created texture resource.</returns>
|
||||
Identifier<RGTexture> CreateTexture(in RGTextureDesc desc, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new buffer resource based on the specified desc.
|
||||
/// </summary>
|
||||
/// <param name="desc">A structure that defines the properties and configuration of the buffer to create.</param>
|
||||
/// <param name="name">The name of the buffer resource.</param>
|
||||
/// <returns>An identifier for the newly created buffer resource.</returns>
|
||||
Identifier<RGBuffer> CreateBuffer(in BufferDesc desc, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified texture for use in the current render graph pass with the given access mode.
|
||||
/// </summary>
|
||||
/// <param name="texture">The identifier of the texture to be used in the render graph pass.</param>
|
||||
/// <param name="accessMode">The access mode specifying how the texture will be read or written during the pass.</param>
|
||||
/// <returns>An identifier for the texture.</returns>
|
||||
Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags accessMode);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified buffer for use in the current render graph pass with the given access mode.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The identifier of the buffer to be used in the render graph pass.</param>
|
||||
/// <param name="accessMode">The access mode specifying how the buffer will be read or written during the pass.</param>
|
||||
/// <param name="hint">Optional hint about how the buffer will be used (e.g., IndirectArgument). Default is None (ByteAddressBuffer SRV).</param>
|
||||
/// <returns>An identifier for the buffer.</returns>
|
||||
Identifier<RGBuffer> UseBuffer(Identifier<RGBuffer> buffer, AccessFlags accessMode, BufferHint hint = BufferHint.None);
|
||||
}
|
||||
|
||||
public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds a texture for random access operations within the current rendering pass.
|
||||
/// </summary>
|
||||
/// <param name="texture">The identifier of the texture to be used for random access.</param>
|
||||
/// <returns>An identifier for the texture.</returns>
|
||||
Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture);
|
||||
/// <summary>
|
||||
/// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context.
|
||||
/// </summary>
|
||||
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer resource.</param>
|
||||
/// <returns>An identifier for the buffer.</returns>
|
||||
Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the color attachment at the specified index to the given texture.
|
||||
/// </summary>
|
||||
/// <param name="texture">The identifier of the texture to use as the color attachment.</param>
|
||||
/// <param name="index">The zero-based index of the color attachment to set.</param>
|
||||
/// <param name="flags">Access flags. Default is Write (assumes partial update). Use WriteAll for fullscreen passes.</param>
|
||||
void SetColorAttachment(Identifier<RGTexture> texture, int index, AccessFlags flags = AccessFlags.Write);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the depth attachment for the current render pass using the specified texture.
|
||||
/// </summary>
|
||||
/// <param name="texture">The identifier of the texture to use as the depth attachment. Cannot be null.</param>
|
||||
/// <param name="flags">Access flags. Default is ReadWrite (assumes partial update). Use WriteAll for fullscreen passes.</param>
|
||||
void SetDepthAttachment(Identifier<RGTexture> texture, AccessFlags flags = AccessFlags.ReadWrite);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the function used to render a pass with the specified pass data and render context.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPassData">The type of data associated with the render pass.</typeparam>
|
||||
/// <param name="renderFunc">The delegate that defines the rendering logic for the pass.</param>
|
||||
void SetRenderFunc<TPassData>(Action<TPassData, IRasterRenderContext> renderFunc)
|
||||
where TPassData : class, new();
|
||||
}
|
||||
|
||||
public interface IComputeRenderGraphBuilder : IRenderGraphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables or disables asynchronous compute operations.
|
||||
/// </summary>
|
||||
/// <param name="value">true to enable asynchronous compute; otherwise, false.</param>
|
||||
void EnableAsyncCompute(bool value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the render function to be invoked during the compute rendering process.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPassData">The type of the data object passed to the render function.</typeparam>
|
||||
/// <param name="renderFunc">The delegate that defines the rendering logic to execute.</param>
|
||||
void SetRenderFunc<TPassData>(Action<TPassData, IComputeRenderContext> renderFunc)
|
||||
where TPassData : class, new();
|
||||
}
|
||||
|
||||
public interface IUnsafeRenderGraphBuilder : IRenderGraphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds a texture for random access operations within the current rendering pass.
|
||||
/// </summary>
|
||||
/// <param name="texture">The identifier of the texture to be used for random access.</param>
|
||||
/// <returns>An identifier for the texture.</returns>
|
||||
Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture);
|
||||
/// <summary>
|
||||
/// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context.
|
||||
/// </summary>
|
||||
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer resource.</param>
|
||||
/// <returns>An identifier for the buffer.</returns>
|
||||
Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the function used to render a pass with the specified pass data and render context.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPassData">The type of data associated with the render pass.</typeparam>
|
||||
/// <param name="renderFunc">The delegate that defines the rendering logic for the pass.</param>
|
||||
void SetRenderFunc<TPassData>(Action<TPassData, IUnsafeRenderContext> renderFunc)
|
||||
where TPassData : class, new();
|
||||
}
|
||||
|
||||
internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGraphBuilder, IUnsafeRenderGraphBuilder
|
||||
{
|
||||
|
||||
private RenderGraph _graph = null!;
|
||||
private RenderGraphPassBase _pass = null!;
|
||||
private RenderGraphResourceRegistry _resources = null!;
|
||||
private bool _disposed;
|
||||
|
||||
internal void Init(RenderGraph graph, RenderGraphPassBase pass, RenderGraphResourceRegistry resources)
|
||||
{
|
||||
_graph = graph;
|
||||
_pass = pass;
|
||||
_resources = resources;
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
private Identifier<RGResource> UseResource(Identifier<RGResource> resource, AccessFlags accessFlags, RenderGraphResourceType type)
|
||||
{
|
||||
if (accessFlags.HasFlag(AccessFlags.Read))
|
||||
{
|
||||
_pass.resourceReads[(int)type].Add(resource);
|
||||
_resources.AddConsumer(resource, _pass.index);
|
||||
}
|
||||
|
||||
if (accessFlags.HasFlag(AccessFlags.Write))
|
||||
{
|
||||
_pass.resourceWrites[(int)type].Add(resource);
|
||||
_resources.SetProducer(resource, _pass.index);
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public void AllowPassCulling(bool value)
|
||||
{
|
||||
_pass.allowCulling = value;
|
||||
}
|
||||
|
||||
public void EnableAsyncCompute(bool value)
|
||||
{
|
||||
_pass.asyncCompute = value;
|
||||
}
|
||||
|
||||
public Identifier<RGTexture> CreateTexture(in RGTextureDesc desc, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var handle = _resources.CreateTexture(in desc, name);
|
||||
_pass.resourceCreates[(int)RenderGraphResourceType.Texture].Add(handle.AsResource());
|
||||
_resources.SetProducer(handle.AsResource(), _pass.index);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public Identifier<RGBuffer> CreateBuffer(in BufferDesc desc, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var handle = _resources.CreateBuffer(in desc, name);
|
||||
_pass.resourceCreates[(int)RenderGraphResourceType.Buffer].Add(handle.AsResource());
|
||||
_resources.SetProducer(handle.AsResource(), _pass.index);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public Identifier<RGTexture> UseTexture(Identifier<RGTexture> texture, AccessFlags flags)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return UseResource(texture.AsResource(), flags, RenderGraphResourceType.Texture).AsTexture();
|
||||
}
|
||||
|
||||
public Identifier<RGBuffer> UseBuffer(Identifier<RGBuffer> buffer, AccessFlags flags, BufferHint hint = BufferHint.None)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
// Store buffer hint if not None
|
||||
if (hint != BufferHint.None)
|
||||
{
|
||||
_pass.bufferHints[buffer.AsResource().Value] = hint;
|
||||
}
|
||||
|
||||
return UseResource(buffer.AsResource(), flags, RenderGraphResourceType.Buffer).AsBuffer();
|
||||
}
|
||||
|
||||
public Identifier<RGTexture> UseRandomAccessTexture(Identifier<RGTexture> texture)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var resource = texture.AsResource();
|
||||
UseResource(resource, AccessFlags.ReadWrite, RenderGraphResourceType.Texture);
|
||||
_pass.randomAccess.Add(resource);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var resource = buffer.AsResource();
|
||||
UseResource(resource, AccessFlags.ReadWrite, RenderGraphResourceType.Buffer);
|
||||
_pass.randomAccess.Add(resource);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void SetColorAttachment(Identifier<RGTexture> texture, int index, AccessFlags flags = AccessFlags.Write)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range.");
|
||||
|
||||
var id = UseTexture(texture, flags);
|
||||
if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid)
|
||||
{
|
||||
_pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index);
|
||||
_pass.colorAccess[index] = new TextureAccess(id, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Color attachment at index {index} is already set to a different texture.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDepthAttachment(Identifier<RGTexture> texture, AccessFlags flags = AccessFlags.Write)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var id = UseTexture(texture, flags);
|
||||
if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid)
|
||||
{
|
||||
_pass.depthAccess = new TextureAccess(id, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Depth attachment is already set to a different texture.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRenderFunc<TPassData>(Action<TPassData, IRasterRenderContext> renderFunc)
|
||||
where TPassData : class, new()
|
||||
{
|
||||
((RasterRenderGraphPass<TPassData>)_pass).renderFunc = renderFunc;
|
||||
}
|
||||
|
||||
public void SetRenderFunc<TPassData>(Action<TPassData, IComputeRenderContext> renderFunc)
|
||||
where TPassData : class, new()
|
||||
{
|
||||
((ComputeRenderGraphPass<TPassData>)_pass).renderFunc = renderFunc;
|
||||
}
|
||||
|
||||
public void SetRenderFunc<TPassData>(Action<TPassData, IUnsafeRenderContext> renderFunc)
|
||||
where TPassData : class, new()
|
||||
{
|
||||
((UnsafeRenderGraphPass<TPassData>)_pass).renderFunc = renderFunc;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_pass.HasRenderFunc())
|
||||
{
|
||||
throw new InvalidOperationException("RenderGraphBuilder must be disposed after setting up the render function.");
|
||||
}
|
||||
|
||||
_graph = null!;
|
||||
_pass = null!;
|
||||
_resources = null!;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
154
Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs
Normal file
154
Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents cached compilation results for a render graph.
|
||||
/// This avoids recompiling the graph when the structure hasn't changed.
|
||||
/// </summary>
|
||||
internal sealed class CachedCompilation
|
||||
{
|
||||
// Compiled pass indices (indices into the _passes list)
|
||||
public readonly List<int> compiledPassIndices = new(64);
|
||||
|
||||
// Culling decisions for each pass
|
||||
public readonly List<bool> passCulledFlags = new(64);
|
||||
|
||||
// Physical resource aliasing mappings (logical index -> physical index)
|
||||
public readonly Dictionary<int, int> logicalToPhysical = new(128);
|
||||
|
||||
// Placed resource metadata
|
||||
public readonly List<PlacedResourceData> placedResources = new(32);
|
||||
|
||||
// Resource barriers
|
||||
public readonly List<ResourceBarrier> barriers = new(128);
|
||||
|
||||
// Resource state mappings (for barrier generation)
|
||||
public readonly Dictionary<int, ResourceState> resourceStates = new(128);
|
||||
|
||||
// Real gpu resource
|
||||
public readonly List<Handle<GPUResource>> backingResources = new(32);
|
||||
|
||||
// View state used for this compilation
|
||||
public ViewState viewState;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
compiledPassIndices.Clear();
|
||||
passCulledFlags.Clear();
|
||||
logicalToPhysical.Clear();
|
||||
placedResources.Clear();
|
||||
barriers.Clear();
|
||||
resourceStates.Clear();
|
||||
backingResources.Clear();
|
||||
viewState = default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placed resource data for caching.
|
||||
/// </summary>
|
||||
internal struct PlacedResourceData
|
||||
{
|
||||
public int index;
|
||||
public RenderGraphResourceType type;
|
||||
public ulong heapOffset;
|
||||
public ulong sizeInBytes;
|
||||
public int firstUsePass;
|
||||
public int lastUsePass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages compilation caching for render graphs.
|
||||
/// Stores compiled results and allows cache hits when graph structure is unchanged.
|
||||
/// </summary>
|
||||
internal sealed class RenderGraphCompilationCache
|
||||
{
|
||||
private readonly CachedCompilation _cached = new();
|
||||
private ulong _cachedHash;
|
||||
private bool _hasCachedData;
|
||||
|
||||
// Statistics
|
||||
public int CacheHits { get; private set; }
|
||||
public int CacheMisses { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve cached compilation results.
|
||||
/// </summary>
|
||||
public bool TryGetCached(ulong hash, [MaybeNullWhen(false)] out CachedCompilation result)
|
||||
{
|
||||
if (_hasCachedData && _cachedHash == hash)
|
||||
{
|
||||
result = _cached;
|
||||
CacheHits++;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
CacheMisses++;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores compilation results in the cache.
|
||||
/// </summary>
|
||||
public void Store(ulong hash, CachedCompilation data)
|
||||
{
|
||||
_cachedHash = hash;
|
||||
_hasCachedData = true;
|
||||
|
||||
// Deep copy the data
|
||||
_cached.Clear();
|
||||
|
||||
_cached.compiledPassIndices.AddRange(data.compiledPassIndices);
|
||||
_cached.passCulledFlags.AddRange(data.passCulledFlags);
|
||||
|
||||
foreach (var kvp in data.logicalToPhysical)
|
||||
{
|
||||
_cached.logicalToPhysical[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
_cached.placedResources.AddRange(data.placedResources);
|
||||
_cached.barriers.AddRange(data.barriers);
|
||||
|
||||
foreach (var kvp in data.resourceStates)
|
||||
{
|
||||
_cached.resourceStates[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
_cached.backingResources.AddRange(data.backingResources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the cache, forcing recompilation on next Compile().
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
_hasCachedData = false;
|
||||
_cachedHash = 0;
|
||||
_cached.Clear();
|
||||
}
|
||||
|
||||
public void UpdateBackingResource(int logicalIndex, Handle<GPUResource> resource)
|
||||
{
|
||||
if (logicalIndex < 0 || logicalIndex >= _cached.backingResources.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cached.backingResources[logicalIndex] = resource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets cache statistics for debugging.
|
||||
/// </summary>
|
||||
public (int hits, int misses, double hitRate) GetStatistics()
|
||||
{
|
||||
int total = CacheHits + CacheMisses;
|
||||
double hitRate = total > 0 ? (double)CacheHits / total : 0.0;
|
||||
return (CacheHits, CacheMisses, hitRate);
|
||||
}
|
||||
}
|
||||
198
Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs
Normal file
198
Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Contracts;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
public interface IRenderGraphContext
|
||||
{
|
||||
IResourceDatabase ResourceDatabase { get; }
|
||||
|
||||
Handle<GPUResource> GetActualResource(Identifier<RGResource> resource);
|
||||
Handle<Texture> GetActualTexture(Identifier<RGTexture> texture);
|
||||
Handle<GraphicsBuffer> GetActualBuffer(Identifier<RGBuffer> buffer);
|
||||
}
|
||||
|
||||
public interface IRasterRenderContext : IRenderGraphContext
|
||||
{
|
||||
int ActiveMeshIndexCount { get; }
|
||||
|
||||
void SetActiveMaterial(Handle<Material> material);
|
||||
void SetActiveMaterial(ref readonly Material material);
|
||||
void SetActiveMesh(Handle<Mesh> mesh);
|
||||
void SetActiveMesh(ref readonly Mesh mesh);
|
||||
void DispatchMesh(uint3 threadGroupCount);
|
||||
}
|
||||
|
||||
public interface IComputeRenderContext : IRenderGraphContext
|
||||
{
|
||||
void DispatchCompute(uint3 threadGroupCount);
|
||||
}
|
||||
|
||||
public interface IUnsafeRenderContext : IRasterRenderContext, IRenderGraphContext
|
||||
{
|
||||
ICommandBuffer CommandBuffer { get; }
|
||||
}
|
||||
|
||||
internal sealed class RenderGraphContext : IRasterRenderContext, IComputeRenderContext, IUnsafeRenderContext
|
||||
{
|
||||
private readonly IResourceDatabase _resourceDatabase;
|
||||
private readonly IPipelineLibrary _pipelineLibrary;
|
||||
private readonly IShaderCompiler _shaderCompiler;
|
||||
private readonly RenderGraphResourceRegistry _resources;
|
||||
|
||||
private ICommandBuffer _commandBuffer = null!;
|
||||
|
||||
private readonly TextureFormat[] _rtvFormats;
|
||||
private TextureFormat _dsvFormat;
|
||||
|
||||
private Handle<GraphicsBuffer> _activePerMaterialData;
|
||||
private Handle<GraphicsBuffer> _activePerMeshData;
|
||||
private int _activeMeshIndexCount;
|
||||
|
||||
public IResourceDatabase ResourceDatabase => _resourceDatabase;
|
||||
|
||||
public int ActiveMeshIndexCount => _activeMeshIndexCount;
|
||||
|
||||
public ICommandBuffer CommandBuffer => _commandBuffer;
|
||||
|
||||
internal RenderGraphContext(IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler, RenderGraphResourceRegistry resources)
|
||||
{
|
||||
_resourceDatabase = resourceDatabase;
|
||||
_pipelineLibrary = pipelineLibrary;
|
||||
_shaderCompiler = shaderCompiler;
|
||||
_resources = resources;
|
||||
|
||||
_rtvFormats = new TextureFormat[RHIUtility.MAX_RENDER_TARGETS];
|
||||
_dsvFormat = TextureFormat.Unknown;
|
||||
}
|
||||
|
||||
internal void SetCommandBuffer(ICommandBuffer commandBuffer)
|
||||
{
|
||||
_commandBuffer = commandBuffer;
|
||||
}
|
||||
|
||||
internal void SetRenderTargetFormats(ReadOnlySpan<TextureFormat> rtvFormats, TextureFormat dsvFormat)
|
||||
{
|
||||
for (int i = 0; i < RHIUtility.MAX_RENDER_TARGETS; i++)
|
||||
{
|
||||
_rtvFormats[i] = i < rtvFormats.Length ? rtvFormats[i] : TextureFormat.Unknown;
|
||||
}
|
||||
|
||||
_dsvFormat = dsvFormat;
|
||||
}
|
||||
|
||||
public Handle<GPUResource> GetActualResource(Identifier<RGResource> resource)
|
||||
{
|
||||
return _resources.GetResource(resource).backingResource;
|
||||
}
|
||||
|
||||
public Handle<Texture> GetActualTexture(Identifier<RGTexture> texture)
|
||||
{
|
||||
return _resources.GetResource(texture.AsResource()).backingResource.AsTexture();
|
||||
}
|
||||
|
||||
public Handle<GraphicsBuffer> GetActualBuffer(Identifier<RGBuffer> buffer)
|
||||
{
|
||||
return _resources.GetResource(buffer.AsResource()).backingResource.AsGraphicsBuffer();
|
||||
}
|
||||
|
||||
public void SetActiveMaterial(Handle<Material> material)
|
||||
{
|
||||
var r = _resourceDatabase.GetMaterialReference(material);
|
||||
if (r.IsFailure)
|
||||
{
|
||||
_activePerMaterialData = Handle<GraphicsBuffer>.Invalid;
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var mat = ref r.Value;
|
||||
SetActiveMaterial(in mat);
|
||||
}
|
||||
|
||||
public void SetActiveMaterial(ref readonly Material material)
|
||||
{
|
||||
var shaderResult = _resourceDatabase.GetShaderReference(material.Shader);
|
||||
if (shaderResult.IsFailure)
|
||||
{
|
||||
_activePerMaterialData = Handle<GraphicsBuffer>.Invalid;
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var shader = ref shaderResult.Value;
|
||||
ref readonly var pass = ref shader.GetPassReference(material.ActivePassIndex);
|
||||
|
||||
var passPipelineHash = new PassPipelineHash(_rtvFormats, _dsvFormat);
|
||||
var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex);
|
||||
|
||||
// Mask out the keywords that are not used in this pass.
|
||||
var variantMask = material._keywordMask & pass.KeywordIDs;
|
||||
var shaderVariantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask);
|
||||
var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(shaderVariantKey, materialPipeline, passPipelineHash);
|
||||
|
||||
if (!_pipelineLibrary.HasPipeline(pipelineKey))
|
||||
{
|
||||
var compiledCacheResult = _shaderCompiler.LoadCompiledCache(shaderVariantKey);
|
||||
if (compiledCacheResult.IsFailure)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation.");
|
||||
}
|
||||
|
||||
var psoDes = new GraphicsPSODescriptor
|
||||
{
|
||||
VariantKey = shaderVariantKey,
|
||||
PipelineOption = materialPipeline,
|
||||
|
||||
RtvFormats = _rtvFormats,
|
||||
DsvFormat = _dsvFormat,
|
||||
};
|
||||
|
||||
var compiled = compiledCacheResult.Value;
|
||||
_pipelineLibrary.CompilePSO(in psoDes, in compiled).GetValueOrThrow();
|
||||
}
|
||||
|
||||
_activePerMaterialData = material._cBufferCache.GpuResource;
|
||||
_commandBuffer.SetPipelineState(pipelineKey);
|
||||
}
|
||||
|
||||
public void SetActiveMesh(Handle<Mesh> mesh)
|
||||
{
|
||||
var r = _resourceDatabase.GetMeshReference(mesh);
|
||||
if (r.IsFailure)
|
||||
{
|
||||
_activePerMeshData = Handle<GraphicsBuffer>.Invalid;
|
||||
_activeMeshIndexCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var meshRef = ref r.Value;
|
||||
SetActiveMesh(in meshRef);
|
||||
}
|
||||
|
||||
public void SetActiveMesh(ref readonly Mesh mesh)
|
||||
{
|
||||
_activePerMeshData = mesh.ObjectDataBuffer;
|
||||
_activeMeshIndexCount = mesh.IndexCount;
|
||||
}
|
||||
|
||||
public unsafe void DispatchMesh(uint3 threadGroupCount)
|
||||
{
|
||||
// TODO: Global and view constants
|
||||
var data = new PushConstantsData
|
||||
{
|
||||
objectIndex = _resourceDatabase.GetBindlessIndex(_activePerMeshData.AsResource()),
|
||||
materialIndex = _resourceDatabase.GetBindlessIndex(_activePerMaterialData.AsResource()),
|
||||
};
|
||||
|
||||
var pushConstantSpan = new ReadOnlySpan<uint>(&data, sizeof(PushConstantsData) / sizeof(uint));
|
||||
_commandBuffer.SetGraphicsRoot32Constants(RootSignatureLayout.PUSH_CONSTANT_SLOT, pushConstantSpan);
|
||||
_commandBuffer.DispatchMesh(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z);
|
||||
}
|
||||
|
||||
public void DispatchCompute(uint3 threadGroupCount)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
52
Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs
Normal file
52
Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a native render pass that can contain multiple merged logical passes.
|
||||
/// Maps to D3D12 BeginRenderPass/EndRenderPass or Vulkan vkCmdBeginRenderPass/vkCmdEndRenderPass.
|
||||
/// </summary>
|
||||
internal sealed class NativeRenderPass
|
||||
{
|
||||
public int index;
|
||||
|
||||
/// <summary>
|
||||
/// Indices of logical passes merged into this native render pass.
|
||||
/// </summary>
|
||||
public readonly List<int> mergedPassIndices = new(4);
|
||||
|
||||
/// <summary>
|
||||
/// Color attachments shared across all merged passes.
|
||||
/// </summary>
|
||||
public RenderTargetInfo[] colorAttachments = new RenderTargetInfo[8];
|
||||
public int colorAttachmentCount;
|
||||
|
||||
/// <summary>
|
||||
/// Depth-stencil attachment (optional).
|
||||
/// </summary>
|
||||
public DepthStencilInfo depthAttachment;
|
||||
public bool hasDepthAttachment;
|
||||
|
||||
/// <summary>
|
||||
/// Range of logical passes included in this native pass.
|
||||
/// </summary>
|
||||
public int firstLogicalPass;
|
||||
public int lastLogicalPass;
|
||||
|
||||
/// <summary>
|
||||
/// Whether UAV writes are allowed during this render pass.
|
||||
/// </summary>
|
||||
public bool allowUAVWrites;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
index = -1;
|
||||
mergedPassIndices.Clear();
|
||||
colorAttachmentCount = 0;
|
||||
hasDepthAttachment = false;
|
||||
depthAttachment = default;
|
||||
firstLogicalPass = int.MaxValue;
|
||||
lastLogicalPass = -1;
|
||||
allowUAVWrites = false;
|
||||
}
|
||||
}
|
||||
172
Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs
Normal file
172
Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Ghost.Core;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Represents different types of render passes.
|
||||
/// </summary>
|
||||
public enum RenderPassType : byte
|
||||
{
|
||||
Raster,
|
||||
Compute,
|
||||
Unsafe
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Base class for render passes.
|
||||
/// Uses pooling to avoid allocations after the first frame.
|
||||
/// </summary>
|
||||
internal abstract class RenderGraphPassBase
|
||||
{
|
||||
public string name = string.Empty;
|
||||
public int index;
|
||||
public RenderPassType type;
|
||||
public bool allowCulling = true;
|
||||
public bool asyncCompute;
|
||||
|
||||
public TextureAccess depthAccess;
|
||||
public TextureAccess[] colorAccess = new TextureAccess[8];
|
||||
public int maxColorIndex = -1;
|
||||
|
||||
public List<Identifier<RGResource>> randomAccess = new(8);
|
||||
|
||||
// Resource dependencies
|
||||
public readonly List<Identifier<RGResource>>[] resourceReads = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
|
||||
public readonly List<Identifier<RGResource>>[] resourceWrites = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
|
||||
public readonly List<Identifier<RGResource>>[] resourceCreates = new List<Identifier<RGResource>>[(int)RenderGraphResourceType.Count];
|
||||
|
||||
// Buffer usage hints (maps buffer resource ID to hint)
|
||||
public readonly Dictionary<int, BufferHint> bufferHints = new(8);
|
||||
|
||||
// Execution state
|
||||
public bool culled;
|
||||
public bool hasSideEffects;
|
||||
|
||||
public RenderGraphPassBase()
|
||||
{
|
||||
for (int i = 0; i < (int)RenderGraphResourceType.Count; i++)
|
||||
{
|
||||
resourceReads[i] = new List<Identifier<RGResource>>(8);
|
||||
resourceWrites[i] = new List<Identifier<RGResource>>(4);
|
||||
resourceCreates[i] = new List<Identifier<RGResource>>(4);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void Execute(RenderGraphContext context);
|
||||
public abstract bool HasRenderFunc();
|
||||
public abstract int GetRenderFuncHashCode();
|
||||
|
||||
public virtual void Reset(RenderGraphObjectPool pool)
|
||||
{
|
||||
name = string.Empty;
|
||||
index = -1;
|
||||
type = RenderPassType.Raster;
|
||||
allowCulling = true;
|
||||
asyncCompute = false;
|
||||
|
||||
depthAccess = default;
|
||||
colorAccess.AsSpan().Clear();
|
||||
maxColorIndex = -1;
|
||||
|
||||
randomAccess.Clear();
|
||||
|
||||
for (var i = 0; i < (int)RenderGraphResourceType.Count; i++)
|
||||
{
|
||||
resourceReads[i].Clear();
|
||||
resourceWrites[i].Clear();
|
||||
resourceCreates[i].Clear();
|
||||
}
|
||||
|
||||
bufferHints.Clear();
|
||||
|
||||
culled = false;
|
||||
hasSideEffects = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class RenderGraphPass<TPassData, TRenderContext> : RenderGraphPassBase
|
||||
where TPassData : class, new()
|
||||
{
|
||||
public TPassData passData = null!;
|
||||
public Action<TPassData, TRenderContext>? renderFunc;
|
||||
|
||||
public void Init(int index, TPassData passData, string name, RenderPassType type)
|
||||
{
|
||||
this.index = index;
|
||||
this.passData = passData;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public sealed override bool HasRenderFunc()
|
||||
{
|
||||
return renderFunc != null;
|
||||
}
|
||||
|
||||
public override int GetRenderFuncHashCode()
|
||||
{
|
||||
if (renderFunc == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var methodHashCode = RuntimeHelpers.GetHashCode(renderFunc.Method);
|
||||
return renderFunc.Target == null ? methodHashCode : methodHashCode ^ RuntimeHelpers.GetHashCode(renderFunc.Target); // static deleget does not have target
|
||||
}
|
||||
|
||||
public override void Reset(RenderGraphObjectPool pool)
|
||||
{
|
||||
base.Reset(pool);
|
||||
pool.Return(passData);
|
||||
|
||||
passData = null!;
|
||||
renderFunc = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RasterRenderGraphPass<TPassData> : RenderGraphPass<TPassData, IRasterRenderContext>
|
||||
where TPassData : class, new()
|
||||
{
|
||||
public override void Execute(RenderGraphContext context)
|
||||
{
|
||||
renderFunc!(passData, context);
|
||||
}
|
||||
|
||||
public override void Reset(RenderGraphObjectPool pool)
|
||||
{
|
||||
base.Reset(pool);
|
||||
pool.Return(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ComputeRenderGraphPass<TPassData> : RenderGraphPass<TPassData, IComputeRenderContext>
|
||||
where TPassData : class, new()
|
||||
{
|
||||
public override void Execute(RenderGraphContext context)
|
||||
{
|
||||
renderFunc!(passData, context);
|
||||
}
|
||||
|
||||
public override void Reset(RenderGraphObjectPool pool)
|
||||
{
|
||||
base.Reset(pool);
|
||||
pool.Return(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UnsafeRenderGraphPass<TPassData> : RenderGraphPass<TPassData, IUnsafeRenderContext>
|
||||
where TPassData : class, new()
|
||||
{
|
||||
public override void Execute(RenderGraphContext context)
|
||||
{
|
||||
renderFunc!(passData, context);
|
||||
}
|
||||
|
||||
public override void Reset(RenderGraphObjectPool pool)
|
||||
{
|
||||
base.Reset(pool);
|
||||
pool.Return(this);
|
||||
}
|
||||
}
|
||||
311
Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs
Normal file
311
Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Buffer;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
/// <summary>
|
||||
/// Object pool for reusing allocated objects across frames.
|
||||
/// This is key to minimizing GC allocations after the first frame.
|
||||
/// </summary>
|
||||
internal sealed class RenderGraphObjectPool
|
||||
{
|
||||
private static readonly List<SharedObjectPoolBase> s_allocatedPools = new();
|
||||
|
||||
private class SharedObjectPoolBase
|
||||
{
|
||||
public SharedObjectPoolBase() { }
|
||||
public virtual void Clear() { }
|
||||
}
|
||||
|
||||
private class SharedObjectPool<T> : SharedObjectPoolBase where T : class, new()
|
||||
{
|
||||
private static readonly ObjectPool<T> s_pool = AllocatePool();
|
||||
|
||||
private static ObjectPool<T> AllocatePool()
|
||||
{
|
||||
var newPool = new ObjectPool<T>(() => new T(), null);
|
||||
// Storing instance to clear the static pool of the same type if needed
|
||||
s_allocatedPools.Add(new SharedObjectPool<T>());
|
||||
return newPool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the pool using SharedObjectPool instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override void Clear()
|
||||
{
|
||||
s_pool.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rent a new instance from the pool.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
// FIX: ObjectPool<T>.Rent() has a critical bug that it will put the newly created object into the pool directly and give out the same instance again.
|
||||
// This will cause multiple renters to get the same instance.
|
||||
public static T Rent() => s_pool.Rent();
|
||||
|
||||
/// <summary>
|
||||
/// Return an object to the pool.
|
||||
/// </summary>
|
||||
/// <param name="toRelease">instance to release.</param>
|
||||
public static void Return(T toRelease) => s_pool.Return(toRelease);
|
||||
}
|
||||
|
||||
public T Rent<T>()
|
||||
where T : class, new()
|
||||
{
|
||||
return SharedObjectPool<T>.Rent();
|
||||
}
|
||||
|
||||
public void Return<T>(T obj)
|
||||
where T : class, new()
|
||||
{
|
||||
SharedObjectPool<T>.Return(obj);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (var i = 0; i < s_allocatedPools.Count; i++)
|
||||
{
|
||||
s_allocatedPools[i].Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a resource in the render graph (texture or buffer).
|
||||
/// </summary>
|
||||
internal sealed class RenderGraphResource
|
||||
{
|
||||
public string name = string.Empty;
|
||||
|
||||
public int index;
|
||||
public RenderGraphResourceType type;
|
||||
|
||||
// Resource descriptors (only one is valid based on type)
|
||||
public RGTextureDesc rgTextureDesc;
|
||||
public BufferDesc bufferDesc;
|
||||
|
||||
// Resolved dimensions (computed from rgTextureDesc + ViewState for textures)
|
||||
public uint resolvedWidth;
|
||||
public uint resolvedHeight;
|
||||
|
||||
public bool isImported;
|
||||
public int firstUsePass = -1;
|
||||
public int lastUsePass = -1;
|
||||
public int producerPass = -1;
|
||||
public List<int> consumerPasses = new(4);
|
||||
public int refCount;
|
||||
|
||||
public Handle<GPUResource> backingResource = Handle<GPUResource>.Invalid;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
name = string.Empty;
|
||||
|
||||
type = RenderGraphResourceType.Texture;
|
||||
index = -1;
|
||||
rgTextureDesc = default;
|
||||
bufferDesc = default;
|
||||
resolvedWidth = 0;
|
||||
resolvedHeight = 0;
|
||||
isImported = false;
|
||||
firstUsePass = -1;
|
||||
lastUsePass = -1;
|
||||
producerPass = -1;
|
||||
consumerPasses.Clear();
|
||||
refCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registry for managing all resources in the render graph.
|
||||
/// Uses pooling to minimize allocations after the first frame.
|
||||
/// Uses a single unified list for both textures and buffers with global indexing.
|
||||
/// </summary>
|
||||
internal sealed class RenderGraphResourceRegistry
|
||||
{
|
||||
private readonly RenderGraphObjectPool _pool;
|
||||
private readonly List<RenderGraphResource> _resources;
|
||||
|
||||
internal IReadOnlyList<RenderGraphResource> Resources => _resources;
|
||||
|
||||
public RenderGraphResourceRegistry(RenderGraphObjectPool pool)
|
||||
{
|
||||
_pool = pool;
|
||||
_resources = new List<RenderGraphResource>(64);
|
||||
}
|
||||
|
||||
public int ResourceCount => _resources.Count;
|
||||
public int TextureResourceCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < _resources.Count; i++)
|
||||
{
|
||||
if (_resources[i].type == RenderGraphResourceType.Texture)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
public int BufferResourceCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < _resources.Count; i++)
|
||||
{
|
||||
if (_resources[i].type == RenderGraphResourceType.Buffer)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
// Return all resources to pool
|
||||
for (var i = 0; i < _resources.Count; i++)
|
||||
{
|
||||
_pool.Return(_resources[i]);
|
||||
}
|
||||
|
||||
_resources.Clear();
|
||||
}
|
||||
|
||||
public Identifier<RGTexture> ImportTexture(ref readonly TextureDesc desc, Handle<Texture> texture, string name)
|
||||
{
|
||||
var resource = _pool.Rent<RenderGraphResource>();
|
||||
resource.name = name;
|
||||
resource.type = RenderGraphResourceType.Texture;
|
||||
resource.index = _resources.Count;
|
||||
resource.rgTextureDesc = RGTextureDesc.FromTextureDesc(in desc);
|
||||
resource.isImported = true;
|
||||
resource.backingResource = texture.AsResource();
|
||||
resource.resolvedWidth = desc.Width;
|
||||
resource.resolvedHeight = desc.Height;
|
||||
|
||||
_resources.Add(resource);
|
||||
|
||||
return new Identifier<RGTexture>(resource.index);
|
||||
}
|
||||
|
||||
public Identifier<RGTexture> CreateTexture(ref readonly RGTextureDesc desc, string name)
|
||||
{
|
||||
var resource = _pool.Rent<RenderGraphResource>();
|
||||
resource.name = name;
|
||||
resource.type = RenderGraphResourceType.Texture;
|
||||
resource.index = _resources.Count;
|
||||
resource.rgTextureDesc = desc;
|
||||
resource.isImported = false;
|
||||
|
||||
_resources.Add(resource);
|
||||
|
||||
return new Identifier<RGTexture>(resource.index);
|
||||
}
|
||||
|
||||
public Identifier<RGBuffer> ImportBuffer(ref readonly BufferDesc desc, Handle<GraphicsBuffer> buffer, string name)
|
||||
{
|
||||
var resource = _pool.Rent<RenderGraphResource>();
|
||||
resource.name = name;
|
||||
resource.type = RenderGraphResourceType.Buffer;
|
||||
resource.index = _resources.Count;
|
||||
resource.bufferDesc = desc;
|
||||
resource.isImported = true;
|
||||
resource.backingResource = buffer.AsResource();
|
||||
|
||||
_resources.Add(resource);
|
||||
|
||||
return new Identifier<RGBuffer>(resource.index);
|
||||
}
|
||||
|
||||
public Identifier<RGBuffer> CreateBuffer(ref readonly BufferDesc desc, string name)
|
||||
{
|
||||
var resource = _pool.Rent<RenderGraphResource>();
|
||||
resource.name= name;
|
||||
resource.type = RenderGraphResourceType.Buffer;
|
||||
resource.index = _resources.Count;
|
||||
resource.bufferDesc = desc;
|
||||
resource.isImported = false;
|
||||
|
||||
_resources.Add(resource);
|
||||
|
||||
return new Identifier<RGBuffer>(resource.index);
|
||||
}
|
||||
|
||||
public RenderGraphResource GetResource(Identifier<RGResource> resource)
|
||||
{
|
||||
return _resources[resource.Value];
|
||||
}
|
||||
|
||||
public RenderGraphResource GetResource(Identifier<RGTexture> texture)
|
||||
{
|
||||
return _resources[texture.Value];
|
||||
}
|
||||
|
||||
public RenderGraphResource GetResource(Identifier<RGBuffer> buffer)
|
||||
{
|
||||
return _resources[buffer.Value];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets resource by global index. Use this when iterating over all resources.
|
||||
/// </summary>
|
||||
public RenderGraphResource GetResourceByIndex(int index)
|
||||
{
|
||||
return _resources[index];
|
||||
}
|
||||
|
||||
public void SetProducer(Identifier<RGResource> resourceID, int passIndex)
|
||||
{
|
||||
var resource = GetResource(resourceID);
|
||||
resource.producerPass = passIndex;
|
||||
if (resource.firstUsePass < 0)
|
||||
{
|
||||
resource.firstUsePass = passIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddConsumer(Identifier<RGResource> resourceID, int passIndex)
|
||||
{
|
||||
var resource = GetResource(resourceID);
|
||||
resource.consumerPasses.Add(passIndex);
|
||||
resource.lastUsePass = passIndex;
|
||||
if (resource.firstUsePass < 0)
|
||||
{
|
||||
resource.firstUsePass = passIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves texture sizes based on current view state.
|
||||
/// Must be called after all resources are created and before compilation.
|
||||
/// </summary>
|
||||
internal void ResolveTextureSizes(in ViewState viewState)
|
||||
{
|
||||
for (var i = 0; i < _resources.Count; i++)
|
||||
{
|
||||
var res = _resources[i];
|
||||
if (res.type != RenderGraphResourceType.Texture || res.isImported)
|
||||
continue;
|
||||
|
||||
var desc = res.rgTextureDesc;
|
||||
if (desc.sizeMode == RGTextureSizeMode.Absolute)
|
||||
{
|
||||
res.resolvedWidth = desc.width;
|
||||
res.resolvedHeight = desc.height;
|
||||
}
|
||||
else // Relative
|
||||
{
|
||||
res.resolvedWidth = (uint)(desc.scaleX * viewState.viewportWidth);
|
||||
res.resolvedHeight = (uint)(desc.scaleY * viewState.viewportHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
500
Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs
Normal file
500
Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs
Normal file
@@ -0,0 +1,500 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Graphics.RenderGraphModule;
|
||||
|
||||
internal enum RenderGraphResourceType : int
|
||||
{
|
||||
Texture,
|
||||
Buffer,
|
||||
// AccelerationStructure,
|
||||
Count
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how texture dimensions are determined.
|
||||
/// </summary>
|
||||
public enum RGTextureSizeMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixed pixel dimensions (width, height).
|
||||
/// </summary>
|
||||
Absolute,
|
||||
|
||||
/// <summary>
|
||||
/// Scale relative to view state (scaleX * viewportWidth, scaleY * viewportHeight).
|
||||
/// </summary>
|
||||
Relative
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View state information for resolving relative texture sizes.
|
||||
/// </summary>
|
||||
public struct ViewState : IEquatable<ViewState>
|
||||
{
|
||||
public uint viewportWidth;
|
||||
public uint viewportHeight;
|
||||
|
||||
public ViewState(uint width, uint height)
|
||||
{
|
||||
viewportWidth = width;
|
||||
viewportHeight = height;
|
||||
}
|
||||
|
||||
public readonly bool Equals(ViewState other)
|
||||
{
|
||||
return viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight;
|
||||
}
|
||||
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
return obj is ViewState other && Equals(other);
|
||||
}
|
||||
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
public static bool operator ==(ViewState left, ViewState right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ViewState left, ViewState right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render graph texture descriptor with support for relative sizing and clear operations.
|
||||
/// </summary>
|
||||
public struct RGTextureDesc : IEquatable<RGTextureDesc>
|
||||
{
|
||||
public RGTextureSizeMode sizeMode;
|
||||
|
||||
// Size specification (union-like - only one set is used based on sizeMode)
|
||||
public uint width; // For Absolute mode
|
||||
public uint height; // For Absolute mode
|
||||
public float scaleX; // For Relative mode
|
||||
public float scaleY; // For Relative mode
|
||||
|
||||
// Common texture properties
|
||||
public TextureFormat format;
|
||||
public TextureDimension dimension;
|
||||
public uint mipLevels;
|
||||
public uint slice;
|
||||
public TextureUsage usage;
|
||||
|
||||
public bool clearAtFirstUse;
|
||||
public bool discardAtLastUse;
|
||||
|
||||
// Clear operation support
|
||||
public Color128 clearColor;
|
||||
|
||||
public float clearDepth;
|
||||
public byte clearStencil;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture descriptor with absolute dimensions.
|
||||
/// </summary>
|
||||
public static RGTextureDesc Absolute(
|
||||
uint width,
|
||||
uint height,
|
||||
TextureFormat format,
|
||||
Color128 clearColor = default,
|
||||
bool clearAtFirstUse = true,
|
||||
bool discardAtLastUse = true,
|
||||
TextureDimension dimension = TextureDimension.Texture2D,
|
||||
uint mipLevels = 1,
|
||||
uint slice = 1,
|
||||
TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource)
|
||||
{
|
||||
return new RGTextureDesc
|
||||
{
|
||||
sizeMode = RGTextureSizeMode.Absolute,
|
||||
width = width,
|
||||
height = height,
|
||||
format = format,
|
||||
clearColor = clearColor,
|
||||
clearAtFirstUse = clearAtFirstUse,
|
||||
discardAtLastUse = discardAtLastUse,
|
||||
clearDepth = 1.0f,
|
||||
clearStencil = 0,
|
||||
dimension = dimension,
|
||||
mipLevels = mipLevels,
|
||||
slice = slice,
|
||||
usage = usage
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture descriptor with relative dimensions (uniform scale).
|
||||
/// </summary>
|
||||
public static RGTextureDesc Relative(
|
||||
float scale,
|
||||
TextureFormat format,
|
||||
Color128 clearColor = default,
|
||||
bool clearAtFirstUse = true,
|
||||
bool discardAtLastUse = true,
|
||||
TextureDimension dimension = TextureDimension.Texture2D,
|
||||
uint mipLevels = 1,
|
||||
uint slice = 1,
|
||||
TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource)
|
||||
{
|
||||
return new RGTextureDesc
|
||||
{
|
||||
sizeMode = RGTextureSizeMode.Relative,
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
format = format,
|
||||
clearColor = clearColor,
|
||||
clearAtFirstUse = clearAtFirstUse,
|
||||
discardAtLastUse = discardAtLastUse,
|
||||
clearDepth = 1.0f,
|
||||
clearStencil = 0,
|
||||
dimension = dimension,
|
||||
mipLevels = mipLevels,
|
||||
slice = slice,
|
||||
usage = usage
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture descriptor with relative dimensions (non-uniform scale).
|
||||
/// </summary>
|
||||
public static RGTextureDesc Relative(
|
||||
float scaleX,
|
||||
float scaleY,
|
||||
TextureFormat format,
|
||||
Color128 clearColor = default,
|
||||
bool clearAtFirstUse = true,
|
||||
bool discardAtLastUse = true,
|
||||
TextureDimension dimension = TextureDimension.Texture2D,
|
||||
uint mipLevels = 1,
|
||||
uint slice = 1,
|
||||
TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource)
|
||||
{
|
||||
return new RGTextureDesc
|
||||
{
|
||||
sizeMode = RGTextureSizeMode.Relative,
|
||||
scaleX = scaleX,
|
||||
scaleY = scaleY,
|
||||
format = format,
|
||||
clearColor = clearColor,
|
||||
clearAtFirstUse = clearAtFirstUse,
|
||||
discardAtLastUse = discardAtLastUse,
|
||||
clearDepth = 1.0f,
|
||||
clearStencil = 0,
|
||||
dimension = dimension,
|
||||
mipLevels = mipLevels,
|
||||
slice = slice,
|
||||
usage = usage
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a depth texture descriptor with relative dimensions.
|
||||
/// </summary>
|
||||
public static RGTextureDesc RelativeDepth(
|
||||
float scale,
|
||||
float clearDepth = 1.0f,
|
||||
byte clearStencil = 0,
|
||||
bool clearAtFirstUse = true,
|
||||
bool discardAtLastUse = true,
|
||||
TextureFormat format = TextureFormat.D32_Float)
|
||||
{
|
||||
return new RGTextureDesc
|
||||
{
|
||||
sizeMode = RGTextureSizeMode.Relative,
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
format = format,
|
||||
clearColor = default,
|
||||
clearDepth = clearDepth,
|
||||
clearStencil = clearStencil,
|
||||
clearAtFirstUse = clearAtFirstUse,
|
||||
discardAtLastUse = discardAtLastUse,
|
||||
dimension = TextureDimension.Texture2D,
|
||||
mipLevels = 1,
|
||||
slice = 1,
|
||||
usage = TextureUsage.DepthStencil | TextureUsage.ShaderResource
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an RGTextureDesc from an RHI TextureDesc (for imported textures).
|
||||
/// </summary>
|
||||
public static RGTextureDesc FromTextureDesc(in TextureDesc desc)
|
||||
{
|
||||
return new RGTextureDesc
|
||||
{
|
||||
sizeMode = RGTextureSizeMode.Absolute,
|
||||
width = desc.Width,
|
||||
height = desc.Height,
|
||||
format = desc.Format,
|
||||
clearColor = default,
|
||||
clearDepth = 1.0f,
|
||||
clearStencil = 0,
|
||||
clearAtFirstUse = false,
|
||||
discardAtLastUse = false,
|
||||
dimension = desc.Dimension,
|
||||
mipLevels = desc.MipLevels,
|
||||
slice = desc.Slice,
|
||||
usage = desc.Usage
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts to RHI TextureDesc using resolved dimensions.
|
||||
/// </summary>
|
||||
public readonly TextureDesc ToTextureDesc(uint resolvedWidth, uint resolvedHeight)
|
||||
{
|
||||
return new TextureDesc
|
||||
{
|
||||
Width = resolvedWidth,
|
||||
Height = resolvedHeight,
|
||||
Format = format,
|
||||
Dimension = dimension,
|
||||
MipLevels = mipLevels,
|
||||
Slice = slice,
|
||||
Usage = usage
|
||||
};
|
||||
}
|
||||
|
||||
public readonly bool Equals(RGTextureDesc other)
|
||||
{
|
||||
return sizeMode == other.sizeMode &&
|
||||
format == other.format &&
|
||||
dimension == other.dimension &&
|
||||
mipLevels == other.mipLevels &&
|
||||
slice == other.slice &&
|
||||
usage == other.usage &&
|
||||
clearAtFirstUse == other.clearAtFirstUse &&
|
||||
discardAtLastUse == other.discardAtLastUse &&
|
||||
(sizeMode == RGTextureSizeMode.Absolute
|
||||
? width == other.width && height == other.height
|
||||
: scaleX == other.scaleX && scaleY == other.scaleY);
|
||||
}
|
||||
|
||||
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
return obj is RGTextureDesc other && Equals(other);
|
||||
}
|
||||
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
if (sizeMode == RGTextureSizeMode.Absolute)
|
||||
{
|
||||
return HashCode.Combine(sizeMode, width, height, format, dimension, mipLevels, slice, usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return HashCode.Combine(sizeMode, scaleX, scaleY, format, dimension, mipLevels, slice, usage);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool operator ==(RGTextureDesc left, RGTextureDesc right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(RGTextureDesc left, RGTextureDesc right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
|
||||
public struct RGResource;
|
||||
public struct RGTexture;
|
||||
public struct RGBuffer;
|
||||
|
||||
public static class RGResourceExtensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Identifier<RGResource> AsResource(this Identifier<RGTexture> texture)
|
||||
{
|
||||
return new Identifier<RGResource>(texture.Value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Identifier<RGResource> AsResource(this Identifier<RGBuffer> buffer)
|
||||
{
|
||||
return new Identifier<RGResource>(buffer.Value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static Identifier<RGTexture> AsTexture(this Identifier<RGResource> resource)
|
||||
{
|
||||
return new Identifier<RGTexture>(resource.Value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static Identifier<RGBuffer> AsBuffer(this Identifier<RGResource> resource)
|
||||
{
|
||||
return new Identifier<RGBuffer>(resource.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hints for how a buffer will be used in a pass.
|
||||
/// Used to determine correct resource state transitions.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum BufferHint
|
||||
{
|
||||
/// <summary>
|
||||
/// No special usage - buffer will be used as shader resource (SRV) or UAV based on AccessFlags.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Buffer will be used as indirect argument buffer (ExecuteIndirect).
|
||||
/// Requires ResourceState.IndirectArgument.
|
||||
/// </summary>
|
||||
IndirectArgument = 1 << 0,
|
||||
}
|
||||
|
||||
internal readonly struct TextureAccess
|
||||
{
|
||||
public readonly Identifier<RGTexture> id;
|
||||
public readonly AccessFlags accessFlags;
|
||||
|
||||
public TextureAccess(Identifier<RGTexture> id, AccessFlags accessFlags)
|
||||
{
|
||||
this.id = id;
|
||||
this.accessFlags = accessFlags;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks buffer access information including usage hints.
|
||||
/// </summary>
|
||||
internal readonly struct BufferAccess
|
||||
{
|
||||
public readonly Identifier<RGBuffer> id;
|
||||
public readonly AccessFlags accessFlags;
|
||||
public readonly BufferHint hint;
|
||||
|
||||
public BufferAccess(Identifier<RGBuffer> id, AccessFlags accessFlags, BufferHint hint = BufferHint.None)
|
||||
{
|
||||
this.id = id;
|
||||
this.accessFlags = accessFlags;
|
||||
this.hint = hint;
|
||||
}
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// Descriptor for creating a texture resource.
|
||||
///// </summary>
|
||||
//public readonly struct TextureDescriptor : IEquatable<TextureDescriptor>
|
||||
//{
|
||||
// public readonly int width;
|
||||
// public readonly int height;
|
||||
// public readonly TextureFormat format;
|
||||
// public readonly string name;
|
||||
|
||||
// public TextureDescriptor(int width, int height, TextureFormat format, string name)
|
||||
// {
|
||||
// this.width = width;
|
||||
// this.height = height;
|
||||
// this.format = format;
|
||||
// this.name = name;
|
||||
// }
|
||||
|
||||
// public readonly bool Equals(TextureDescriptor other)
|
||||
// {
|
||||
// return width == other.width &&
|
||||
// height == other.height &&
|
||||
// format == other.format &&
|
||||
// name == other.name;
|
||||
// }
|
||||
|
||||
// public override readonly bool Equals(object? obj) => obj is TextureDescriptor other && Equals(other);
|
||||
// public override readonly int GetHashCode() => HashCode.Combine(width, height, format, name);
|
||||
|
||||
// public static bool operator ==(TextureDescriptor left, TextureDescriptor right)
|
||||
// {
|
||||
// return left.Equals(right);
|
||||
// }
|
||||
|
||||
// public static bool operator !=(TextureDescriptor left, TextureDescriptor right)
|
||||
// {
|
||||
// return !(left == right);
|
||||
// }
|
||||
//}
|
||||
|
||||
//public readonly struct BufferDescriptor : IEquatable<BufferDescriptor>
|
||||
//{
|
||||
// public readonly uint sizeInBytes;
|
||||
// public readonly uint stride;
|
||||
// public readonly BufferUsage usage;
|
||||
// public readonly string name;
|
||||
|
||||
// public BufferDescriptor(uint sizeInBytes, uint stride, BufferUsage usage, string name)
|
||||
// {
|
||||
// this.sizeInBytes = sizeInBytes;
|
||||
// this.stride = stride;
|
||||
// this.usage = usage;
|
||||
// this.name = name;
|
||||
// }
|
||||
|
||||
// public readonly bool Equals(BufferDescriptor other)
|
||||
// {
|
||||
// return sizeInBytes == other.sizeInBytes &&
|
||||
// stride == other.stride &&
|
||||
// usage == other.usage &&
|
||||
// name == other.name;
|
||||
// }
|
||||
|
||||
// public override readonly bool Equals(object? obj) => obj is BufferDescriptor other && Equals(other);
|
||||
// public override readonly int GetHashCode() => HashCode.Combine(sizeInBytes, name);
|
||||
|
||||
// public static bool operator ==(BufferDescriptor left, BufferDescriptor right)
|
||||
// {
|
||||
// return left.Equals(right);
|
||||
// }
|
||||
|
||||
// public static bool operator !=(BufferDescriptor left, BufferDescriptor right)
|
||||
// {
|
||||
// return !(left == right);
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for pass data that can be stored in the blackboard.
|
||||
/// </summary>
|
||||
public interface IPassData
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a render target attachment in a native render pass.
|
||||
/// </summary>
|
||||
internal struct RenderTargetInfo
|
||||
{
|
||||
public Identifier<RGTexture> texture;
|
||||
public AccessFlags access;
|
||||
public AttachmentLoadOp loadOp;
|
||||
public AttachmentStoreOp storeOp;
|
||||
public Color128 clearColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a depth-stencil attachment in a native render pass.
|
||||
/// </summary>
|
||||
internal struct DepthStencilInfo
|
||||
{
|
||||
public Identifier<RGTexture> texture;
|
||||
public AccessFlags access;
|
||||
public AttachmentLoadOp loadOp;
|
||||
public AttachmentStoreOp storeOp;
|
||||
public float clearDepth;
|
||||
public byte clearStencil;
|
||||
}
|
||||
Reference in New Issue
Block a user