using Ghost.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.RenderGraphModule;
///
/// Builds native render passes by merging compatible consecutive raster passes.
/// Optimizes for tile-based deferred rendering (TBDR) GPUs by minimizing load/store operations.
///
internal sealed class RenderGraphNativePassBuilder
{
private readonly RenderGraphObjectPool _objectPool;
private readonly RenderGraphResourceRegistry _resources;
public RenderGraphNativePassBuilder(RenderGraphObjectPool objectPool, RenderGraphResourceRegistry resources)
{
_objectPool = objectPool;
_resources = resources;
}
///
/// Builds native render passes by merging compatible consecutive raster passes.
/// Uses conservative merging: only merge passes with identical attachments and no barriers between them.
///
public void BuildNativeRenderPasses(
List compiledPasses,
List nativePasses,
List compiledBarriers)
{
// Clear previous native passes
for (var i = 0; i < nativePasses.Count; i++)
{
_objectPool.Return(nativePasses[i]);
}
nativePasses.Clear();
NativeRenderPass? currentNativePass = null;
for (var i = 0; i < compiledPasses.Count; i++)
{
var pass = compiledPasses[i];
// Only raster passes can be merged into native render passes
// Compute passes break the current native render pass
if (pass.type != RenderPassType.Raster)
{
// Close current native pass if open
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
currentNativePass = null;
}
continue; // Compute/Unsafe passes execute outside native render passes
}
// Check if we can merge with current native pass
if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i, compiledPasses, compiledBarriers))
{
// Merge into existing native pass
currentNativePass.mergedPassIndices.Add(i);
currentNativePass.lastLogicalPass = i;
}
else
{
// Start new native pass
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
}
currentNativePass = CreateNativePass(pass, i);
}
}
// Add final native pass
if (currentNativePass != null)
{
nativePasses.Add(currentNativePass);
}
// Infer load/store operations for all native passes
for (var i = 0; i < nativePasses.Count; i++)
{
InferLoadStoreOps(nativePasses[i]);
}
}
///
/// Creates a new native render pass from a logical pass.
///
private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex)
{
var nativePass = _objectPool.Rent();
nativePass.Reset();
nativePass.index = 0; // Will be set by caller
nativePass.mergedPassIndices.Add(passIndex);
nativePass.firstLogicalPass = passIndex;
nativePass.lastLogicalPass = passIndex;
nativePass.allowUAVWrites = pass.randomAccess.Count > 0;
// Copy color attachments
nativePass.colorAttachmentCount = pass.maxColorIndex + 1;
for (var i = 0; i <= pass.maxColorIndex; i++)
{
var access = pass.colorAccess[i];
nativePass.colorAttachments[i] = new RenderTargetInfo
{
texture = access.id,
access = access.accessFlags
};
}
// Copy depth attachment
if (!pass.depthAccess.id.IsInvalid)
{
nativePass.hasDepthAttachment = true;
nativePass.depthAttachment = new DepthStencilInfo
{
texture = pass.depthAccess.id,
access = pass.depthAccess.accessFlags
};
}
return nativePass;
}
///
/// Checks if a logical pass can be merged into an existing native render pass.
/// Conservative merging: only merge if attachments match and no barriers needed.
///
private bool CanMergePasses(
NativeRenderPass nativePass,
RenderGraphPassBase pass,
int passIndex,
List compiledPasses,
List compiledBarriers)
{
// Don't merge if UAVs are involved (conservative)
if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites)
{
return false;
}
// Check if attachment configuration matches
if (!AttachmentsMatch(nativePass, pass))
{
return false;
}
// Check if barriers are needed between last merged pass and this pass
if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex, compiledPasses, compiledBarriers))
{
return false;
}
return true;
}
///
/// Checks if the attachment configuration of a pass matches the native pass.
///
private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass)
{
// Check color attachment count
if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1)
{
return false;
}
// Check each color attachment
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{
if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id)
{
return false;
}
}
// Check depth attachment
if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid)
{
return false;
}
if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id)
{
return false;
}
return true;
}
///
/// Checks if any barriers are required between two passes that would prevent merging.
/// Only barriers affecting render targets prevent merging; SRV barriers are fine.
///
private bool RequiresBarrierBetweenPasses(
int passA,
int passB,
List compiledPasses,
List compiledBarriers)
{
var laterPass = compiledPasses[passB];
// Build a set of render target resource IDs (color + depth)
var renderTargets = new HashSet>();
for (var i = 0; i <= laterPass.maxColorIndex; i++)
{
if (!laterPass.colorAccess[i].id.IsInvalid)
{
renderTargets.Add(laterPass.colorAccess[i].id.AsResource());
}
}
if (!laterPass.depthAccess.id.IsInvalid)
{
renderTargets.Add(laterPass.depthAccess.id.AsResource());
}
// Check if any compiled barriers for passB affect render targets
for (var i = 0; i < compiledBarriers.Count; i++)
{
if (compiledBarriers[i].PassIndex == passB)
{
// Only prevent merge if barrier affects a render target
if (renderTargets.Contains(compiledBarriers[i].Resource))
{
return true; // Barrier affects render target, cannot merge
}
}
if (compiledBarriers[i].PassIndex > passB)
{
break; // No more barriers for this pass
}
}
return false;
}
///
/// Infers optimal load/store operations for all attachments in a native render pass.
/// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs).
///
private void InferLoadStoreOps(NativeRenderPass nativePass)
{
// Infer load/store ops for color attachments
for (var i = 0; i < nativePass.colorAttachmentCount; i++)
{
ref var attachment = ref nativePass.colorAttachments[i];
var resource = _resources.GetResource(attachment.texture);
var flags = attachment.access;
// ===== LOAD OP INFERENCE =====
// 1. First use
if (resource.firstUsePass == nativePass.firstLogicalPass)
{
// Clear at first use
if (resource.rgTextureDesc.clearAtFirstUse)
{
attachment.loadOp = AttachmentLoadOp.Clear;
attachment.clearColor = resource.rgTextureDesc.clearColor;
}
else
{
attachment.loadOp = AttachmentLoadOp.DontCare;
}
}
// 2. Discard flag: DontCare for performance
else if (flags.HasFlag(AccessFlags.Discard))
{
attachment.loadOp = AttachmentLoadOp.DontCare;
}
// 3. Read flag: Must preserve existing contents
else if (flags.HasFlag(AccessFlags.Read))
{
attachment.loadOp = AttachmentLoadOp.Load;
}
// 4. Continuation from previous pass
else
{
attachment.loadOp = AttachmentLoadOp.Load;
}
// ===== STORE OP INFERENCE =====
// Last use: No one needs it after this native pass
if (resource.lastUsePass == nativePass.lastLogicalPass)
{
if (resource.rgTextureDesc.discardAtLastUse)
{
attachment.storeOp = AttachmentStoreOp.DontCare;
}
else
{
attachment.storeOp = AttachmentStoreOp.Store;
}
}
// Intermediate: Store for future passes
else
{
attachment.storeOp = AttachmentStoreOp.Store;
}
}
// Infer load/store ops for depth attachment
if (nativePass.hasDepthAttachment)
{
ref var attachment = ref nativePass.depthAttachment;
var resource = _resources.GetResource(attachment.texture);
var flags = attachment.access;
// ===== LOAD OP INFERENCE =====
// 1. First Use
if (resource.firstUsePass == nativePass.firstLogicalPass)
{
// Clear at first use
if (resource.rgTextureDesc.clearAtFirstUse)
{
attachment.loadOp = AttachmentLoadOp.Clear;
attachment.clearDepth = resource.rgTextureDesc.clearDepth;
attachment.clearStencil = resource.rgTextureDesc.clearStencil;
}
else
{
attachment.loadOp = AttachmentLoadOp.DontCare;
}
}
// 2. Discard flag: DontCare for performance
else if (flags.HasFlag(AccessFlags.Discard))
{
attachment.loadOp = AttachmentLoadOp.DontCare;
}
// 3. Read flag: Must preserve existing contents
else if (flags.HasFlag(AccessFlags.Read))
{
attachment.loadOp = AttachmentLoadOp.Load;
}
// 4. Continuation from previous pass
else
{
attachment.loadOp = AttachmentLoadOp.Load;
}
// ===== STORE OP INFERENCE =====
// Depth is commonly discarded (depth-only passes, intermediate depth)
if (resource.lastUsePass == nativePass.lastLogicalPass)
{
if (resource.rgTextureDesc.discardAtLastUse)
{
attachment.storeOp = AttachmentStoreOp.DontCare;
}
else
{
attachment.storeOp = AttachmentStoreOp.Store;
}
}
else
{
attachment.storeOp = AttachmentStoreOp.Store;
}
}
}
}