Files
GhostEngine/Ghost.RenderGraph.Concept/RenderGraph.cs
Misaki 676f8bb74c Add render graph proof of concept and refactor graphics
Implemented a transient render graph system as a proof of concept, including resource aliasing, pass culling, and typed pass data. Added new project `Ghost.RenderGraph.Concept` targeting `.NET 10.0`.

Refactored graphics-related components:
- Simplified resource state transitions in `RenderingContext`.
- Improved resize handling in `GraphicsTestWindow`.
- Updated `D3D12GraphicsEngine` to streamline frame rendering.
- Enhanced `D3D12ResourceDatabase` and `D3D12SwapChain` for better resource management.

Added detailed documentation:
- `ALIASING.md` explains resource aliasing techniques.
- `API_DESIGN.md` outlines the render graph API design.

Updated solution to include the new render graph project.
2025-12-01 22:31:17 +09:00

416 lines
15 KiB
C#

namespace Ghost.RenderGraph.Concept;
public class RenderGraph
{
private int _resourceIdCounter = 0;
private int _passCounter = 0;
private readonly List<RenderGraphResourceHandle> _resources = new();
private readonly List<RenderGraphPass> _passes = new();
// Use List instead of Dictionary since resource IDs are sequential (0, 1, 2, ...)
private readonly List<ResourceLifetime> _resourceLifetimes = new();
private readonly List<ResourceState> _currentResourceStates = new();
private readonly List<int> _resourceToAllocationMap = new();
private readonly Dictionary<int, RenderGraphResourceHandle?> _allocationActiveResource = new();
private readonly RenderGraphBlackboard _blackboard = new();
private readonly ResourceAllocator _allocator = new();
public RenderGraphTextureHandle ImportTexture(string name, TextureDescriptor descriptor)
{
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, name, descriptor, isImported: true);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1); // -1 means no allocation
Console.WriteLine($"[RG] Import Texture: '{name}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
return handle;
}
public RenderGraphBufferHandle ImportBuffer(string name, BufferDescriptor descriptor)
{
var handle = new RenderGraphBufferHandle(_resourceIdCounter++, name, descriptor, isImported: true);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Import Buffer: '{name}' ({descriptor.SizeInBytes} bytes)");
return handle;
}
internal RenderGraphTextureHandle CreateTransientTexture(TextureDescriptor descriptor)
{
var handle = new RenderGraphTextureHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Create Transient Texture: '{descriptor.DebugName}' ({descriptor.Width}x{descriptor.Height}, {descriptor.Format})");
return handle;
}
internal RenderGraphBufferHandle CreateTransientBuffer(BufferDescriptor descriptor)
{
var handle = new RenderGraphBufferHandle(_resourceIdCounter++, descriptor.DebugName, descriptor, isImported: false);
_resources.Add(handle);
_resourceLifetimes.Add(new ResourceLifetime(handle));
_currentResourceStates.Add(ResourceState.Undefined);
_resourceToAllocationMap.Add(-1);
Console.WriteLine($"[RG] Create Transient Buffer: '{descriptor.DebugName}' ({descriptor.SizeInBytes} bytes)");
return handle;
}
public RenderGraphBlackboard Blackboard => _blackboard;
public RenderGraphTextureHandle CreateTexture(TextureDescriptor descriptor)
{
return CreateTransientTexture(descriptor);
}
public RenderGraphBufferHandle CreateBuffer(BufferDescriptor descriptor)
{
return CreateTransientBuffer(descriptor);
}
public RenderGraphPassBuilder<TPassData> AddRenderPass<TPassData>(string name, out TPassData passData)
where TPassData : class, new()
{
var builder = new RenderGraphPassBuilder<TPassData>(this, name, _passCounter);
passData = builder.PassData;
return builder;
}
internal void CommitPass<TPassData>(RenderGraphPassBuilder<TPassData> builder, string name)
where TPassData : class, new()
{
if (builder.RenderFunc == null)
{
throw new InvalidOperationException($"Pass '{name}' has no render function set. Call SetRenderFunc() on the builder.");
}
var pass = new RenderGraphPass<TPassData>(
name,
_passCounter++,
builder.PassData,
builder.RenderFunc,
builder.ResourceAccesses.ToList(),
builder.AllowCulling);
_passes.Add(pass);
foreach (var (handle, state) in pass.ResourceAccesses)
{
_resourceLifetimes[handle.Id].AddUsage(state, pass.Index);
}
Console.WriteLine($"[RG] Add Pass: '{name}' (Index: {pass.Index})");
foreach (var (handle, state) in pass.ResourceAccesses)
{
Console.WriteLine($" - {state}: '{handle.Name}'");
}
}
public void Compile()
{
Console.WriteLine("\n[RG] ========== COMPILING RENDER GRAPH ==========");
BuildDependencies();
CullUnusedPasses();
AnalyzeResourceLifetimes();
AllocatePhysicalResources();
}
private void AllocatePhysicalResources()
{
// Pass as IReadOnlyList since it's now a List
_allocator.AllocateResources(_resourceLifetimes, _passes);
// Build mapping from virtual resource to physical allocation
foreach (var allocation in _allocator.Allocations)
{
foreach (var resource in allocation.AliasedResources)
{
_resourceToAllocationMap[resource.Id] = allocation.AllocationId;
}
}
}
private void BuildDependencies()
{
Console.WriteLine("\n[RG] Building pass dependencies...");
for (int i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
var writtenResources = pass.ResourceAccesses
.Where(access => IsWriteState(access.state))
.Select(access => access.handle.Id)
.ToHashSet();
for (int j = 0; j < i; j++)
{
var previousPass = _passes[j];
var hasReadAfterWrite = previousPass.ResourceAccesses
.Where(access => IsWriteState(access.state))
.Any(access => pass.ResourceAccesses.Any(
current => current.handle.Id == access.handle.Id && IsReadState(current.state)));
var hasWriteAfterRead = pass.ResourceAccesses
.Where(access => IsWriteState(access.state))
.Any(access => previousPass.ResourceAccesses.Any(
prev => prev.handle.Id == access.handle.Id && IsReadState(prev.state)));
var hasWriteAfterWrite = previousPass.ResourceAccesses
.Where(access => IsWriteState(access.state))
.Any(access => writtenResources.Contains(access.handle.Id));
if (hasReadAfterWrite || hasWriteAfterRead || hasWriteAfterWrite)
{
if (!pass.Dependencies.Contains(j))
{
pass.Dependencies.Add(j);
Console.WriteLine($" Pass '{pass.Name}' depends on '{previousPass.Name}'");
}
}
}
}
}
private void CullUnusedPasses()
{
Console.WriteLine("\n[RG] Culling unused passes...");
// Mark passes that contribute to imported resources or don't allow culling
foreach (var pass in _passes)
{
foreach (var (handle, _) in pass.ResourceAccesses)
{
if (handle.IsImported)
{
pass.RefCount++;
}
}
// Mark passes that don't allow culling (synchronization, debug, etc.)
if (!pass.AllowCulling)
{
pass.RefCount++;
Console.WriteLine($" Pass '{pass.Name}' marked as non-cullable");
}
}
// Propagate reference counts through dependencies
bool changed = true;
while (changed)
{
changed = false;
foreach (var pass in _passes)
{
if (pass.RefCount > 0)
{
foreach (var depIndex in pass.Dependencies)
{
var depPass = _passes[depIndex];
if (depPass.RefCount == 0)
{
depPass.RefCount++;
changed = true;
}
}
}
}
}
var culledPasses = _passes.Where(p => p.RefCount == 0 && p.AllowCulling).ToList();
if (culledPasses.Count != 0)
{
foreach (var pass in culledPasses)
{
Console.WriteLine($" Culled unused pass: '{pass.Name}'");
}
}
else
{
Console.WriteLine(" No passes culled.");
}
}
private void AnalyzeResourceLifetimes()
{
Console.WriteLine("\n[RG] Resource lifetimes:");
foreach (var lifetime in _resourceLifetimes)
{
if (lifetime.FirstUse == int.MaxValue)
{
Console.WriteLine($" '{lifetime.Handle.Name}': UNUSED");
}
else
{
var passNames = _passes
.Where(p => p.Index >= lifetime.FirstUse && p.Index <= lifetime.LastUse && p.RefCount > 0)
.Select(p => p.Name);
Console.WriteLine($" '{lifetime.Handle.Name}': [{lifetime.FirstUse}..{lifetime.LastUse}] ({string.Join(", ", passNames)})");
}
}
}
public void Execute()
{
Console.WriteLine("\n[RG] ========== EXECUTING RENDER GRAPH ==========\n");
var commandBuffer = new SimulatedCommandBuffer();
foreach (var pass in _passes.Where(p => p.RefCount > 0).OrderBy(p => p.Index))
{
Console.WriteLine($"[PASS {pass.Index}] Executing: '{pass.Name}'");
var lifetime = _resourceLifetimes
.Where(lt => lt.FirstUse == pass.Index)
.ToList();
foreach (var lt in lifetime)
{
if (!lt.Handle.IsImported)
{
CreateResource(lt.Handle);
}
}
InsertBarriers(pass, commandBuffer);
commandBuffer.BeginRenderPass(pass.Name);
pass.Execute(commandBuffer);
commandBuffer.EndRenderPass();
var endLifetime = _resourceLifetimes
.Where(lt => lt.LastUse == pass.Index && !lt.Handle.IsImported)
.ToList();
foreach (var lt in endLifetime)
{
DestroyResource(lt.Handle);
}
Console.WriteLine();
}
Console.WriteLine("[RG] ========== EXECUTION COMPLETE ==========\n");
}
private void CreateResource(RenderGraphResourceHandle handle)
{
var allocation = _allocator.GetAllocation(handle);
if (allocation != null)
{
if (handle is RenderGraphTextureHandle textureHandle)
{
var desc = textureHandle.Descriptor;
Console.WriteLine($" [CREATE] Texture '{handle.Name}' using '{allocation.DebugName}' " +
$"({desc.Width}x{desc.Height}, {desc.Format}, offset: {allocation.OffsetInBytes})");
}
else if (handle is RenderGraphBufferHandle bufferHandle)
{
var desc = bufferHandle.Descriptor;
Console.WriteLine($" [CREATE] Buffer '{handle.Name}' using '{allocation.DebugName}' " +
$"({desc.SizeInBytes} bytes, offset: {allocation.OffsetInBytes})");
}
// Note: We do NOT set _allocationActiveResource here
// That happens in InsertBarriers when the resource is first accessed
}
else
{
if (handle is RenderGraphTextureHandle textureHandle)
{
var desc = textureHandle.Descriptor;
Console.WriteLine($" [CREATE] Texture '{handle.Name}' ({desc.Width}x{desc.Height}, {desc.Format})");
}
else if (handle is RenderGraphBufferHandle bufferHandle)
{
var desc = bufferHandle.Descriptor;
Console.WriteLine($" [CREATE] Buffer '{handle.Name}' ({desc.SizeInBytes} bytes)");
}
}
_currentResourceStates[handle.Id] = ResourceState.Undefined;
}
private void DestroyResource(RenderGraphResourceHandle handle)
{
Console.WriteLine($" [DESTROY] Resource '{handle.Name}'");
_currentResourceStates[handle.Id] = ResourceState.Undefined;
// Note: We intentionally DO NOT clear _allocationActiveResource here
// The allocation remains "owned" by this resource until another resource aliases it
// This allows us to track aliasing barriers correctly
}
private void InsertBarriers(RenderGraphPass pass, ICommandBuffer commandBuffer)
{
foreach (var (handle, targetState) in pass.ResourceAccesses)
{
// Check if this resource shares a physical allocation
var allocation = _allocator.GetAllocation(handle);
if (allocation != null)
{
// Check what resource is currently active on this allocation
if (_allocationActiveResource.TryGetValue(allocation.AllocationId, out var activeResource))
{
// If a different resource is currently active on this allocation, insert aliasing barrier
if (activeResource != null && activeResource.Id != handle.Id)
{
commandBuffer.AliasingBarrier(activeResource.Name, handle.Name, allocation.DebugName);
// Clear state for the old resource since it's being aliased away
_currentResourceStates[activeResource.Id] = ResourceState.Undefined;
}
}
// Update the active resource for this allocation
_allocationActiveResource[allocation.AllocationId] = handle;
}
var currentState = _currentResourceStates[handle.Id];
if (currentState != targetState)
{
commandBuffer.ResourceBarrier(handle.Name, currentState, targetState);
_currentResourceStates[handle.Id] = targetState;
}
}
}
private static bool IsWriteState(ResourceState state)
{
return state.HasFlag(ResourceState.RenderTarget) ||
state.HasFlag(ResourceState.DepthWrite) ||
state.HasFlag(ResourceState.UnorderedAccess) ||
state.HasFlag(ResourceState.CopyDest);
}
private static bool IsReadState(ResourceState state)
{
return state.HasFlag(ResourceState.ShaderResource) ||
state.HasFlag(ResourceState.DepthRead) ||
state.HasFlag(ResourceState.CopySource);
}
public void Reset()
{
_passes.Clear();
_resources.Clear();
_resourceLifetimes.Clear();
_currentResourceStates.Clear();
_resourceToAllocationMap.Clear();
_allocationActiveResource.Clear();
_blackboard.Clear();
_passCounter = 0;
_resourceIdCounter = 0;
Console.WriteLine("[RG] Render graph reset.");
}
}