3705 lines
135 KiB
C++
3705 lines
135 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
RHIValidation.cpp: Public RHI Validation layer definitions.
|
|
=============================================================================*/
|
|
|
|
#include "RHIValidation.h"
|
|
#include "RHIValidationContext.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "RHIValidationTransientResourceAllocator.h"
|
|
#include "HAL/PlatformStackWalk.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Misc/OutputDeviceRedirector.h"
|
|
#include "RHIContext.h"
|
|
#include "RHIStrings.h"
|
|
#include "RHIUniformBufferUtilities.h"
|
|
#include "Algo/BinarySearch.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "RHIResources.h"
|
|
|
|
#if ENABLE_RHI_VALIDATION
|
|
|
|
bool GRHIValidationEnabled = false;
|
|
|
|
bool GRHIValidateBufferSourceCopy = true;
|
|
|
|
bool GRHIValidationPrintHumanReadableCallStack = false;
|
|
|
|
// Define the number of stack frames to capture
|
|
const int32 NumStackFrames = 30;
|
|
const uint32 IgnoreStackCount = 2; // Ignore the call to the function itself and the log
|
|
|
|
// When set to 1, callstack for each uniform buffer allocation will be tracked
|
|
// (slow and leaks memory, but can be handy to find the location where an invalid
|
|
// allocation has been made)
|
|
#define CAPTURE_UNIFORMBUFFER_ALLOCATION_BACKTRACES 0
|
|
|
|
// When set to 1, logs resource transitions on all unnamed resources, useful for
|
|
// tracking down missing barriers when "-RHIValidationLog" cannot be used.
|
|
// Don't leave this enabled. Log backtraces are leaked.
|
|
#define LOG_UNNAMED_RESOURCES 0
|
|
|
|
namespace RHIValidation
|
|
{
|
|
int32 GBreakOnTransitionError = 1;
|
|
FAutoConsoleVariableRef CVarBreakOnTransitionError(
|
|
TEXT("r.RHIValidation.DebugBreak.Transitions"),
|
|
GBreakOnTransitionError,
|
|
TEXT("Controls whether the debugger should break when a validation error is encountered.\n")
|
|
TEXT(" 0: disabled;\n")
|
|
TEXT(" 1: break in the debugger if a validation error is encountered."),
|
|
ECVF_RenderThreadSafe);
|
|
|
|
// Returns an array of resource names parsed from the "-RHIValidationLog" command line switch.
|
|
// RHI validation logging is automatically enabled for resources whose debug names match those in this list.
|
|
// Multiple values are comma separated, e.g. -RHIValidationLog="SceneDepthZ,GBufferA"
|
|
// Use the additional -RHIValidationLogStack arg to enable printing the resolved symbols of the callstack in the log
|
|
static TArray<FString> const& GetAutoLogResourceNames()
|
|
{
|
|
struct FInit
|
|
{
|
|
TArray<FString> Strings;
|
|
|
|
FInit()
|
|
{
|
|
FString ResourceNames;
|
|
if (FParse::Value(FCommandLine::Get(), TEXT("-RHIValidationLog="), ResourceNames, false))
|
|
{
|
|
FString Left, Right;
|
|
while (ResourceNames.Split(TEXT(","), &Left, &Right))
|
|
{
|
|
Left.TrimStartAndEndInline();
|
|
Strings.Add(Left);
|
|
ResourceNames = Right;
|
|
}
|
|
|
|
ResourceNames.TrimStartAndEndInline();
|
|
Strings.Add(ResourceNames);
|
|
}
|
|
|
|
GRHIValidationPrintHumanReadableCallStack = FParse::Param(FCommandLine::Get(), TEXT("RHIValidationLogStack"));
|
|
}
|
|
} static Init;
|
|
|
|
return Init.Strings;
|
|
}
|
|
|
|
FBufferResource::FBufferResource(const FRHIBufferCreateDesc& CreateDesc)
|
|
{
|
|
InitBarrierTracking(CreateDesc);
|
|
}
|
|
|
|
void FBufferResource::InitBarrierTracking(const FRHIBufferCreateDesc& CreateDesc)
|
|
{
|
|
FResource::InitBarrierTracking(1, 1, 1, CreateDesc.InitialState, CreateDesc.DebugName);
|
|
}
|
|
|
|
FTextureResource::FTextureResource(FRHITextureCreateDesc const& CreateDesc)
|
|
: FTextureResource()
|
|
{
|
|
InitBarrierTracking(CreateDesc);
|
|
}
|
|
|
|
void FTextureResource::InitBarrierTracking(FRHITextureCreateDesc const& CreateDesc)
|
|
{
|
|
InitBarrierTracking(
|
|
CreateDesc.NumMips,
|
|
CreateDesc.ArraySize * (CreateDesc.IsTextureCube() ? 6 : 1),
|
|
CreateDesc.Format,
|
|
CreateDesc.Flags,
|
|
CreateDesc.InitialState,
|
|
CreateDesc.DebugName);
|
|
}
|
|
|
|
int32 FTextureResource::GetNumPlanesFromFormat(EPixelFormat Format)
|
|
{
|
|
int32 NumPlanes = 1;
|
|
|
|
// @todo: htile tracking
|
|
if (IsStencilFormat(Format))
|
|
{
|
|
NumPlanes = 2; // Depth + Stencil
|
|
}
|
|
else
|
|
{
|
|
NumPlanes = 1; // Depth only
|
|
}
|
|
|
|
return NumPlanes;
|
|
}
|
|
|
|
void FTextureResource::InitBarrierTracking(int32 InNumMips, int32 InNumArraySlices, EPixelFormat PixelFormat, ETextureCreateFlags /*Flags*/, ERHIAccess InResourceState, const TCHAR* InDebugName)
|
|
{
|
|
FResource* Resource = GetTrackerResource();
|
|
if (!Resource)
|
|
return;
|
|
|
|
Resource->InitBarrierTracking(InNumMips, InNumArraySlices, GetNumPlanesFromFormat(PixelFormat), InResourceState, InDebugName);
|
|
}
|
|
|
|
void FTextureResource::CheckValidationLayout(int32 InNumMips, int32 InNumArraySlices, EPixelFormat PixelFormat)
|
|
{
|
|
FResource* Resource = GetTrackerResource();
|
|
check(Resource);
|
|
|
|
check(Resource->NumMips == InNumMips);
|
|
check(Resource->NumArraySlices == InNumArraySlices);
|
|
check(Resource->NumPlanes == GetNumPlanesFromFormat(PixelFormat));
|
|
}
|
|
|
|
FResourceIdentity FTextureResource::GetViewIdentity(uint32 InMipIndex, uint32 InNumMips, uint32 InArraySlice, uint32 InNumArraySlices, uint32 InPlaneIndex, uint32 InNumPlanes)
|
|
{
|
|
FResource* Resource = GetTrackerResource();
|
|
|
|
checkSlow((InMipIndex + InNumMips) <= Resource->NumMips);
|
|
checkSlow((InArraySlice + InNumArraySlices) <= Resource->NumArraySlices);
|
|
checkSlow((InPlaneIndex + InNumPlanes) <= Resource->NumPlanes);
|
|
|
|
if (InNumMips == 0)
|
|
{
|
|
InNumMips = Resource->NumMips;
|
|
}
|
|
if (InNumArraySlices == 0)
|
|
{
|
|
InNumArraySlices = Resource->NumArraySlices;
|
|
}
|
|
if (InNumPlanes == 0)
|
|
{
|
|
InNumPlanes = Resource->NumPlanes;
|
|
}
|
|
|
|
FResourceIdentity Identity;
|
|
Identity.Resource = Resource;
|
|
Identity.SubresourceRange.MipIndex = InMipIndex;
|
|
Identity.SubresourceRange.NumMips = InNumMips;
|
|
Identity.SubresourceRange.ArraySlice = InArraySlice;
|
|
Identity.SubresourceRange.NumArraySlices = InNumArraySlices;
|
|
Identity.SubresourceRange.PlaneIndex = InPlaneIndex;
|
|
Identity.SubresourceRange.NumPlanes = InNumPlanes;
|
|
return Identity;
|
|
}
|
|
|
|
FResourceIdentity FTextureResource::GetTransitionIdentity(const FRHITransitionInfo& Info)
|
|
{
|
|
FResource* Resource = GetTrackerResource();
|
|
|
|
FResourceIdentity Identity;
|
|
Identity.Resource = Resource;
|
|
|
|
if (Info.IsAllMips())
|
|
{
|
|
Identity.SubresourceRange.MipIndex = 0;
|
|
Identity.SubresourceRange.NumMips = Resource->NumMips;
|
|
}
|
|
else
|
|
{
|
|
check(Info.MipIndex < uint32(Resource->NumMips));
|
|
Identity.SubresourceRange.MipIndex = Info.MipIndex;
|
|
Identity.SubresourceRange.NumMips = 1;
|
|
}
|
|
|
|
if (Info.IsAllArraySlices())
|
|
{
|
|
Identity.SubresourceRange.ArraySlice = 0;
|
|
Identity.SubresourceRange.NumArraySlices = Resource->NumArraySlices;
|
|
}
|
|
else
|
|
{
|
|
check(Info.ArraySlice < uint32(Resource->NumArraySlices));
|
|
Identity.SubresourceRange.ArraySlice = Info.ArraySlice;
|
|
Identity.SubresourceRange.NumArraySlices = 1;
|
|
}
|
|
|
|
if (Info.IsAllPlaneSlices())
|
|
{
|
|
Identity.SubresourceRange.PlaneIndex = 0;
|
|
Identity.SubresourceRange.NumPlanes = Resource->NumPlanes;
|
|
}
|
|
else
|
|
{
|
|
check(Info.PlaneSlice < uint32(Resource->NumPlanes));
|
|
Identity.SubresourceRange.PlaneIndex = Info.PlaneSlice;
|
|
Identity.SubresourceRange.NumPlanes = 1;
|
|
}
|
|
|
|
return Identity;
|
|
}
|
|
|
|
RHI_API FViewIdentity::FViewIdentity(FRHIViewableResource* InResource, FRHIViewDesc const& InViewDesc)
|
|
{
|
|
if (InViewDesc.IsBuffer())
|
|
{
|
|
FRHIBuffer* Buffer = static_cast<FRHIBuffer*>(InResource);
|
|
Resource = Buffer;
|
|
|
|
if (InViewDesc.IsUAV())
|
|
{
|
|
auto const Info = InViewDesc.Buffer.UAV.GetViewInfo(Buffer);
|
|
if (ensureMsgf(!Info.bNullView, TEXT("Attempt to use a null buffer UAV.")))
|
|
{
|
|
SubresourceRange = Resource->GetWholeResourceRange();
|
|
Stride = Info.StrideInBytes;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto const Info = InViewDesc.Buffer.SRV.GetViewInfo(Buffer);
|
|
if (ensureMsgf(!Info.bNullView, TEXT("Attempt to use a null buffer SRV.")))
|
|
{
|
|
SubresourceRange = Resource->GetWholeResourceRange();
|
|
Stride = Info.StrideInBytes;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FRHITexture* Texture = static_cast<FRHITexture*>(InResource);
|
|
Resource = Texture->GetTrackerResource();
|
|
|
|
auto GetPlaneIndex = [](ERHITexturePlane Plane)
|
|
{
|
|
switch (Plane)
|
|
{
|
|
default: checkNoEntry(); [[fallthrough]];
|
|
case ERHITexturePlane::Primary:
|
|
case ERHITexturePlane::PrimaryCompressed:
|
|
case ERHITexturePlane::Depth:
|
|
return EResourcePlane::Common;
|
|
|
|
case ERHITexturePlane::Stencil:
|
|
return EResourcePlane::Stencil;
|
|
|
|
case ERHITexturePlane::HTile:
|
|
return EResourcePlane::Htile;
|
|
|
|
case ERHITexturePlane::FMask:
|
|
return EResourcePlane::Cmask;
|
|
|
|
case ERHITexturePlane::CMask:
|
|
return EResourcePlane::Fmask;
|
|
}
|
|
};
|
|
|
|
if (InViewDesc.IsUAV())
|
|
{
|
|
auto const Info = InViewDesc.Texture.UAV.GetViewInfo(Texture);
|
|
|
|
SubresourceRange.MipIndex = Info.MipLevel;
|
|
SubresourceRange.NumMips = 1;
|
|
SubresourceRange.ArraySlice = Info.ArrayRange.First;
|
|
SubresourceRange.NumArraySlices = Info.ArrayRange.Num;
|
|
SubresourceRange.PlaneIndex = uint32(GetPlaneIndex(Info.Plane));
|
|
SubresourceRange.NumPlanes = 1;
|
|
|
|
Stride = GPixelFormats[Info.Format].BlockBytes;
|
|
}
|
|
else
|
|
{
|
|
auto const Info = InViewDesc.Texture.SRV.GetViewInfo(Texture);
|
|
|
|
SubresourceRange.MipIndex = Info.MipRange.First;
|
|
SubresourceRange.NumMips = Info.MipRange.Num;
|
|
SubresourceRange.ArraySlice = Info.ArrayRange.First;
|
|
SubresourceRange.NumArraySlices = Info.ArrayRange.Num;
|
|
SubresourceRange.PlaneIndex = uint32(GetPlaneIndex(Info.Plane));
|
|
SubresourceRange.NumPlanes = 1;
|
|
|
|
Stride = GPixelFormats[Info.Format].BlockBytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTracker::FUAVTracker::DrawOrDispatch(FTracker* BarrierTracker, const FState& RequiredState)
|
|
{
|
|
// The barrier tracking expects us to call Assert() only once per unique resource.
|
|
// However, multiple UAVs may be bound, all referencing the same resource.
|
|
// Find the unique resources to ensure we only do the tracking once per resource.
|
|
TArray<FResourceIdentity, TInlineAllocator<FRHIGlobals::MinGuaranteedSimultaneousUAVs>> UniqueIdentities;
|
|
|
|
for (int32 UAVIndex = 0; UAVIndex < UAVs.Num(); ++UAVIndex)
|
|
{
|
|
if (UAVs[UAVIndex])
|
|
{
|
|
const FResourceIdentity& Identity = UAVs[UAVIndex]->GetViewIdentity();
|
|
|
|
// Check if we've already seen this resource.
|
|
bool bFound = false;
|
|
for (int32 Index = 0; !bFound && Index < UniqueIdentities.Num(); ++Index)
|
|
{
|
|
bFound = UniqueIdentities[Index] == Identity;
|
|
}
|
|
|
|
if (!bFound)
|
|
{
|
|
check(UniqueIdentities.Num() < GRHIGlobals.MaxSimultaneousUAVs);
|
|
UniqueIdentities.Add(Identity);
|
|
|
|
// Assert unique resources have the required state.
|
|
BarrierTracker->AddOp(FOperation::Assert(Identity, RequiredState));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FRayTracingPipelineState::FRayTracingPipelineState(const FRayTracingPipelineStateInitializer& Initializer)
|
|
{
|
|
HitGroupShaders = Initializer.GetHitGroupTable();
|
|
MissShaders = Initializer.GetMissTable();
|
|
CallableShaders = Initializer.GetCallableTable();
|
|
}
|
|
|
|
FRHIRayTracingShader* FRayTracingPipelineState::GetShader(ERayTracingBindingType BindingType, uint32 Index) const
|
|
{
|
|
switch (BindingType)
|
|
{
|
|
case ERayTracingBindingType::HitGroup:
|
|
return HitGroupShaders[Index];
|
|
case ERayTracingBindingType::CallableShader:
|
|
return CallableShaders[Index];
|
|
case ERayTracingBindingType::MissShader:
|
|
return MissShaders[Index];
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
FShaderBindingTable::FShaderBindingTable(const FRayTracingShaderBindingTableInitializer& InInitializer)
|
|
{
|
|
LifeTime = InInitializer.Lifetime;
|
|
ShaderBindingMode = InInitializer.ShaderBindingMode;
|
|
HitGroupIndexingMode = InInitializer.HitGroupIndexingMode;
|
|
}
|
|
|
|
void FShaderBindingTable::Clear()
|
|
{
|
|
WorkerData[0].SRVs.Empty();
|
|
WorkerData[0].UAVs.Empty();
|
|
|
|
bIsDirty = true;
|
|
}
|
|
|
|
static void CollectShaderBindingTableResources(FRayTracingPipelineState* RayTracingPipelineState, FShaderBindingTable* ShaderTable, const FRayTracingLocalShaderBindings& LocalShaderBinding, ERayTracingBindingType BindingType, uint32 WorkerIndex)
|
|
{
|
|
FRHIRayTracingShader* Shader = RayTracingPipelineState->GetShader(BindingType, LocalShaderBinding.ShaderIndexInPipeline);
|
|
ensure(Shader);
|
|
|
|
struct FResourceBinder
|
|
{
|
|
FResourceBinder(FShaderBindingTable* InShaderTable, FRHIRayTracingShader* InShader, uint32 InWorkerIndex, uint32 InRecordIndex) :
|
|
ShaderBindingTable(InShaderTable), RHIShader(InShader), WorkerIndex(InWorkerIndex), RecordIndex(InRecordIndex)
|
|
{
|
|
}
|
|
|
|
void SetUAV(FRHIUnorderedAccessView* UAV, uint8 Index)
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateUnorderedAccessView(RHIShader, Index, UAV);
|
|
}
|
|
ShaderBindingTable->AddUAV(UAV, Index, WorkerIndex);
|
|
}
|
|
|
|
void SetSRV(FRHIShaderResourceView* SRV, uint8 Index)
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateShaderResourceView(RHIShader, Index, SRV);
|
|
}
|
|
ShaderBindingTable->AddSRV(SRV->GetViewIdentity(), WorkerIndex);
|
|
}
|
|
|
|
void SetTexture(FRHITexture* Texture, uint8 Index)
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateShaderResourceView(RHIShader, Index, Texture);
|
|
}
|
|
ShaderBindingTable->AddSRV(Texture->GetWholeResourceIdentitySRV(), WorkerIndex);
|
|
}
|
|
|
|
void SetResourceCollection(FRHIResourceCollection* ResourceCollection, uint8 Index)
|
|
{
|
|
for (const FRHIResourceCollectionMember& Member : ResourceCollection->Members)
|
|
{
|
|
switch (Member.Type)
|
|
{
|
|
case FRHIResourceCollectionMember::EType::Texture:
|
|
if (FRHITexture* Texture = static_cast<FRHITexture*>(Member.Resource))
|
|
{
|
|
ShaderBindingTable->AddSRV(Texture->GetWholeResourceIdentitySRV(), WorkerIndex);
|
|
}
|
|
break;
|
|
case FRHIResourceCollectionMember::EType::TextureReference:
|
|
if (FRHITextureReference* Texture = static_cast<FRHITextureReference*>(Member.Resource))
|
|
{
|
|
ShaderBindingTable->AddSRV(Texture->GetWholeResourceIdentitySRV(), WorkerIndex);
|
|
}
|
|
break;
|
|
case FRHIResourceCollectionMember::EType::ShaderResourceView:
|
|
if (FRHIShaderResourceView* SRV = static_cast<FRHIShaderResourceView*>(Member.Resource))
|
|
{
|
|
ShaderBindingTable->AddSRV(SRV->GetViewIdentity(), WorkerIndex);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetSampler(FRHISamplerState* RHISampler, uint8 Index)
|
|
{
|
|
// nothing to validate
|
|
}
|
|
|
|
FShaderBindingTable* ShaderBindingTable;
|
|
FRHIRayTracingShader* RHIShader;
|
|
uint32 WorkerIndex;
|
|
uint32 RecordIndex;
|
|
};
|
|
FResourceBinder Binder(ShaderTable, Shader, WorkerIndex, LocalShaderBinding.RecordIndex);
|
|
|
|
// Don't need to perform the state validation now because they can still change before the actual ray dispatch
|
|
RHIValidation::FTracker* ValidationTracker = nullptr;
|
|
|
|
// Use rhi core function to find all the used RHI resources in the uniform buffers
|
|
uint32 DirtyUniformBuffers = ~(0u);
|
|
UE::RHI::Private::SetUniformBufferResourcesFromTables(Binder, *Shader, DirtyUniformBuffers, LocalShaderBinding.UniformBuffers, ValidationTracker);
|
|
}
|
|
|
|
void FShaderBindingTable::SetBindingsOnShaderBindingTable(FRayTracingPipelineState* RayTracingPipelineState, uint32 NumBindings, const FRayTracingLocalShaderBindings* Bindings, ERayTracingBindingType BindingType)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(RHIValidation-SetBindingsOnShaderBindingTable);
|
|
|
|
// Disable tracking for persistent SBTs until per record tracking is implemented otherwise
|
|
// it might end up with dangling SVR/UAV pointers
|
|
if (LifeTime != ERayTracingShaderBindingTableLifetime::Persistent)
|
|
{
|
|
FGraphEventArray TaskList;
|
|
|
|
const uint32 NumWorkerThreads = FTaskGraphInterface::Get().GetNumWorkerThreads();
|
|
const uint32 MaxTasks = FApp::ShouldUseThreadingForPerformance() ? FMath::Min<uint32>(NumWorkerThreads, MaxBindingWorkers) : 1;
|
|
|
|
struct FTaskContext
|
|
{
|
|
uint32 WorkerIndex = 0;
|
|
};
|
|
|
|
TArray<FTaskContext, TInlineAllocator<MaxBindingWorkers>> TaskContexts;
|
|
for (uint32 WorkerIndex = 0; WorkerIndex < MaxTasks; ++WorkerIndex)
|
|
{
|
|
TaskContexts.Add(FTaskContext{ WorkerIndex });
|
|
}
|
|
|
|
auto BindingTask = [this, RayTracingPipelineState, Bindings, BindingType](const FTaskContext& Context, int32 CurrentIndex)
|
|
{
|
|
const FRayTracingLocalShaderBindings& Binding = Bindings[CurrentIndex];
|
|
|
|
// only collect shader binding data if RTPSO & hit group indexing mode
|
|
bool bValidBinding = (Binding.BindingType == ERayTracingLocalShaderBindingType::Persistent || Binding.BindingType == ERayTracingLocalShaderBindingType::Validation);
|
|
if (HitGroupIndexingMode == ERayTracingHitGroupIndexingMode::Allow && bValidBinding)
|
|
{
|
|
if (EnumHasAnyFlags(ShaderBindingMode, ERayTracingShaderBindingMode::RTPSO))
|
|
{
|
|
CollectShaderBindingTableResources(RayTracingPipelineState, this, Binding, BindingType, Context.WorkerIndex);
|
|
|
|
// Also add SRV view requirement for all index and vertex buffers used in the SBT
|
|
const FRayTracingGeometryInitializer& BLASInitializer = Binding.Geometry->GetInitializer();
|
|
if (BLASInitializer.IndexBuffer)
|
|
{
|
|
AddSRV(BLASInitializer.IndexBuffer->GetWholeResourceIdentity(), Context.WorkerIndex);
|
|
}
|
|
for (int32 SegmentIndex = 0; SegmentIndex < BLASInitializer.Segments.Num(); ++SegmentIndex)
|
|
{
|
|
AddSRV(BLASInitializer.Segments[SegmentIndex].VertexBuffer->GetWholeResourceIdentity(), Context.WorkerIndex);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const int32 ItemsPerTask = 1024;
|
|
ParallelForWithExistingTaskContext(TEXT("SetRayTracingBindings"), MakeArrayView(TaskContexts), NumBindings, ItemsPerTask, BindingTask);
|
|
}
|
|
|
|
// Mark dirty
|
|
bIsDirty = true;
|
|
}
|
|
|
|
void FShaderBindingTable::Commit()
|
|
{
|
|
// Merge all data from worker threads into the main set
|
|
for (uint32 WorkerIndex = 1; WorkerIndex < MaxBindingWorkers; ++WorkerIndex)
|
|
{
|
|
for (const FResourceIdentity& ResourceIdentity : WorkerData[WorkerIndex].SRVs)
|
|
{
|
|
AddSRV(ResourceIdentity, 0);
|
|
}
|
|
|
|
for (const FUAVBinding& UAVBinding : WorkerData[WorkerIndex].UAVs)
|
|
{
|
|
AddUAV(UAVBinding.UAV, UAVBinding.Slot, 0);
|
|
}
|
|
|
|
WorkerData[WorkerIndex].SRVs.Empty();
|
|
WorkerData[WorkerIndex].UAVs.Empty();
|
|
}
|
|
|
|
bIsDirty = false;
|
|
}
|
|
|
|
void FShaderBindingTable::ValidateStateForDispatch(RHIValidation::FTracker* Tracker) const
|
|
{
|
|
ensureMsgf(!bIsDirty, TEXT("RayTracing bindings have not been committed. You must call CommitRayTracingBindings first."));
|
|
|
|
// Validate all used SRVs
|
|
for (const FResourceIdentity& SRV : WorkerData[0].SRVs)
|
|
{
|
|
Tracker->Assert(SRV, ERHIAccess::SRVCompute);
|
|
}
|
|
|
|
// Validate all used UAVs
|
|
for (const FUAVBinding& UAVBinding : WorkerData[0].UAVs)
|
|
{
|
|
Tracker->AssertUAV(UAVBinding.UAV, ERHIAccess::UAVCompute, UAVBinding.Slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
TSet<uint32> FValidationRHI::SeenFailureHashes;
|
|
FCriticalSection FValidationRHI::SeenFailureHashesMutex;
|
|
|
|
FValidationRHI::FValidationRHI(FDynamicRHI* InRHI)
|
|
: RHI(InRHI)
|
|
{
|
|
check(RHI);
|
|
UE_LOG(LogRHI, Log, TEXT("FValidationRHI on, intercepting %s RHI!"), InRHI && InRHI->GetName() ? InRHI->GetName() : TEXT("<NULL>"));
|
|
GRHIValidationEnabled = true;
|
|
SeenFailureHashes.Reserve(256);
|
|
}
|
|
|
|
FValidationRHI::~FValidationRHI()
|
|
{
|
|
GRHIValidationEnabled = false;
|
|
}
|
|
|
|
IRHITransientResourceAllocator* FValidationRHI::RHICreateTransientResourceAllocator()
|
|
{
|
|
// Wrap around validation allocator
|
|
if (IRHITransientResourceAllocator* RHIAllocator = RHI->RHICreateTransientResourceAllocator())
|
|
{
|
|
return new FValidationTransientResourceAllocator(RHIAllocator);
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
IRHICommandContext* FValidationRHI::RHIGetDefaultContext()
|
|
{
|
|
IRHICommandContext* LowLevelContext = RHI->RHIGetDefaultContext();
|
|
IRHICommandContext* HighLevelContext = static_cast<IRHICommandContext*>(&LowLevelContext->GetHighestLevelContext());
|
|
|
|
if (LowLevelContext == HighLevelContext)
|
|
{
|
|
FValidationContext* ValidationContext = new FValidationContext(FValidationContext::EType::Default);
|
|
ValidationContext->LinkToContext(LowLevelContext);
|
|
HighLevelContext = ValidationContext;
|
|
}
|
|
|
|
return HighLevelContext;
|
|
}
|
|
|
|
struct FValidationCommandList : public IRHIPlatformCommandList
|
|
{
|
|
ERHIPipeline Pipeline;
|
|
TRHIPipelineArray<IRHIPlatformCommandList*> InnerCommandLists;
|
|
TArray<RHIValidation::FOperation> CompletedOpList;
|
|
};
|
|
|
|
IRHIComputeContext* FValidationRHI::RHIGetCommandContext(ERHIPipeline Pipeline, FRHIGPUMask GPUMask)
|
|
{
|
|
IRHIComputeContext* InnerContext = RHI->RHIGetCommandContext(Pipeline, GPUMask);
|
|
check(InnerContext);
|
|
|
|
switch (Pipeline)
|
|
{
|
|
case ERHIPipeline::Graphics:
|
|
{
|
|
FValidationContext* OuterContext = new FValidationContext(FValidationContext::EType::Parallel);
|
|
OuterContext->LinkToContext(static_cast<IRHICommandContext*>(InnerContext));
|
|
return OuterContext;
|
|
}
|
|
|
|
case ERHIPipeline::AsyncCompute:
|
|
{
|
|
FValidationComputeContext* OuterContext = new FValidationComputeContext(FValidationComputeContext::EType::Parallel);
|
|
OuterContext->LinkToContext(InnerContext);
|
|
return OuterContext;
|
|
}
|
|
|
|
default:
|
|
checkNoEntry();
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
void FValidationRHI::RHIFinalizeContext(FRHIFinalizeContextArgs&& Args, TRHIPipelineArray<IRHIPlatformCommandList*>& Output)
|
|
{
|
|
FRHIFinalizeContextArgs FinalArgs;
|
|
|
|
TRHIPipelineArray<IRHIPlatformCommandList*> FinalizedCommandLists { InPlace, nullptr };
|
|
TRHIPipelineArray<FValidationCommandList*> OuterCommandLists { InPlace, nullptr };
|
|
|
|
// Re-combine the args so that the validation matches a normal call to RHIFinalizeContext
|
|
for(IRHIComputeContext* Context : Args.Contexts)
|
|
{
|
|
IRHIComputeContext& InnerContext = Context->GetLowestLevelContext();
|
|
|
|
FValidationCommandList* OuterCommandList = new FValidationCommandList();
|
|
|
|
// RHIFinalizeContext makes the context available to other threads, so finalize the tracker beforehand.
|
|
OuterCommandList->CompletedOpList = InnerContext.Tracker->Finalize();
|
|
OuterCommandList->Pipeline = Context->GetPipeline();
|
|
OuterCommandLists[OuterCommandList->Pipeline] = OuterCommandList;
|
|
|
|
FinalArgs.Contexts.Add(&InnerContext);
|
|
}
|
|
FinalArgs.UploadContext = Args.UploadContext;
|
|
|
|
RHI->RHIFinalizeContext(MoveTemp(FinalArgs), FinalizedCommandLists);
|
|
|
|
for(IRHIComputeContext* Context : Args.Contexts)
|
|
{
|
|
FValidationCommandList* ValidationCmdList = OuterCommandLists[Context->GetPipeline()];
|
|
switch (ValidationCmdList->Pipeline)
|
|
{
|
|
case ERHIPipeline::Graphics:
|
|
if (static_cast<FValidationContext*>(Context)->Type == FValidationContext::EType::Parallel)
|
|
delete Context;
|
|
break;
|
|
|
|
case ERHIPipeline::AsyncCompute:
|
|
if (static_cast<FValidationComputeContext*>(Context)->Type == FValidationComputeContext::EType::Parallel)
|
|
delete Context;
|
|
break;
|
|
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
|
|
ValidationCmdList->InnerCommandLists = FinalizedCommandLists[ValidationCmdList->Pipeline];
|
|
Output[ValidationCmdList->Pipeline] = ValidationCmdList;
|
|
}
|
|
}
|
|
|
|
IRHIComputeContext* FValidationRHI::RHIGetParallelCommandContext(FRHIParallelRenderPassInfo const& ParallelRenderPass, FRHIGPUMask GPUMask)
|
|
{
|
|
// If a platform has a ChildWait or ParentWait it is expected that they will override RHIGetParallelCommandContext
|
|
// otherwise, we need to manually call RHIGetCommandContext and RHIBeginRenderPass seperately, because the default implementation
|
|
// calls one after another potentially crasing in GetHightestLevelContext (FRHICommandList_RecursiveHazardous)
|
|
// TODO: Remove this after implementing RHISetupParallelPass
|
|
if (GRHIParallelRHIExecuteChildWait || GRHIParallelRHIExecuteParentWait)
|
|
{
|
|
IRHIComputeContext* InnerContext = RHI->RHIGetParallelCommandContext(ParallelRenderPass, GPUMask);
|
|
check(InnerContext);
|
|
|
|
FValidationContext* OuterContext = new FValidationContext(FValidationContext::EType::Parallel);
|
|
OuterContext->LinkToContext(static_cast<IRHICommandContext*>(InnerContext));
|
|
|
|
// Parallel contexts are always inside a renderpass
|
|
OuterContext->State.bInsideBeginRenderPass = true;
|
|
OuterContext->State.RenderPassInfo = ParallelRenderPass;
|
|
if (ParallelRenderPass.PassName)
|
|
{
|
|
OuterContext->State.RenderPassName = ParallelRenderPass.PassName;
|
|
}
|
|
|
|
return OuterContext;
|
|
}
|
|
else
|
|
{
|
|
IRHICommandContext* Context = static_cast<IRHICommandContext*>(RHIGetCommandContext(ERHIPipeline::Graphics, GPUMask));
|
|
Context->RHIBeginRenderPass(ParallelRenderPass, ParallelRenderPass.PassName);
|
|
return Context;
|
|
}
|
|
}
|
|
|
|
void FValidationRHI::RHICloseTranslateChain(FRHIFinalizeContextArgs&& Args, TRHIPipelineArray<IRHIPlatformCommandList*>& Output, bool bShouldFinalize)
|
|
{
|
|
// If we aren't finalizing the context we need to finalize the tracking
|
|
if(!bShouldFinalize)
|
|
{
|
|
for(IRHIComputeContext* Context : Args.Contexts)
|
|
{
|
|
if(Context)
|
|
{
|
|
check(Context->GetPipeline() == ERHIPipeline::Graphics);
|
|
|
|
IRHIComputeContext& InnerContext = Context->GetLowestLevelContext();
|
|
|
|
FValidationCommandList* OuterCommandList = new FValidationCommandList();
|
|
OuterCommandList->CompletedOpList = InnerContext.Tracker->Finalize();
|
|
OuterCommandList->Pipeline = ERHIPipeline::Graphics;
|
|
|
|
Output[ERHIPipeline::Graphics] = OuterCommandList;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
FDynamicRHI::RHICloseTranslateChain(MoveTemp(Args), Output, bShouldFinalize);
|
|
}
|
|
|
|
IRHIPlatformCommandList* FValidationRHI::RHIFinalizeParallelContext(IRHIComputeContext* Context)
|
|
{
|
|
check(Context->GetPipeline() == ERHIPipeline::Graphics);
|
|
|
|
IRHIComputeContext& InnerContext = Context->GetLowestLevelContext();
|
|
|
|
FValidationCommandList* OuterCommandList = new FValidationCommandList();
|
|
OuterCommandList->CompletedOpList = InnerContext.Tracker->Finalize();
|
|
OuterCommandList->Pipeline = ERHIPipeline::Graphics;
|
|
|
|
IRHIPlatformCommandList* InnerCommandList = RHI->RHIFinalizeParallelContext(&InnerContext);
|
|
OuterCommandList->InnerCommandLists[ERHIPipeline::Graphics] = InnerCommandList;
|
|
|
|
check(static_cast<FValidationContext*>(Context)->Type == FValidationContext::EType::Parallel);
|
|
delete Context;
|
|
|
|
return OuterCommandList;
|
|
}
|
|
|
|
void FValidationRHI::RHISubmitCommandLists(FRHISubmitCommandListsArgs&& Args)
|
|
{
|
|
FDynamicRHI::FRHISubmitCommandListsArgs InnerArgs;
|
|
InnerArgs.CommandLists.Reserve(Args.CommandLists.Num());
|
|
|
|
for (IRHIPlatformCommandList* CmdList : Args.CommandLists)
|
|
{
|
|
FValidationCommandList* OuterCommandList = static_cast<FValidationCommandList*>(CmdList);
|
|
#if WITH_RHI_BREADCRUMBS
|
|
OuterCommandList->CompletedOpList.Insert(RHIValidation::FOperation::SetBreadcrumbRange(CmdList->BreadcrumbRange), 0);
|
|
#endif
|
|
|
|
// Replay or queue any barrier operations to validate resource barrier usage.
|
|
RHIValidation::FTracker::SubmitValidationOps(OuterCommandList->Pipeline, MoveTemp(OuterCommandList->CompletedOpList));
|
|
|
|
for(IRHIPlatformCommandList* InnerCmdList : OuterCommandList->InnerCommandLists)
|
|
{
|
|
if(!InnerCmdList)
|
|
{
|
|
continue;
|
|
}
|
|
#if WITH_RHI_BREADCRUMBS
|
|
// Forward the breadcrumb range and allocators
|
|
InnerCmdList->BreadcrumbAllocators = MoveTemp(CmdList->BreadcrumbAllocators);
|
|
InnerCmdList->BreadcrumbRange = CmdList->BreadcrumbRange;
|
|
#endif
|
|
InnerArgs.CommandLists.Add(InnerCmdList);
|
|
}
|
|
|
|
delete OuterCommandList;
|
|
}
|
|
|
|
RHI->RHISubmitCommandLists(MoveTemp(InnerArgs));
|
|
}
|
|
|
|
void FValidationRHI::ValidatePipeline(const FGraphicsPipelineStateInitializer& PSOInitializer)
|
|
{
|
|
{
|
|
// Verify depth/stencil access/usage
|
|
bool bHasDepth = IsDepthOrStencilFormat(PSOInitializer.DepthStencilTargetFormat);
|
|
bool bHasStencil = IsStencilFormat(PSOInitializer.DepthStencilTargetFormat);
|
|
const FDepthStencilStateInitializerRHI& Initializer = DepthStencilStates.FindChecked(PSOInitializer.DepthStencilState);
|
|
if (bHasDepth)
|
|
{
|
|
if (!bHasStencil)
|
|
{
|
|
RHI_VALIDATION_CHECK(!Initializer.bEnableFrontFaceStencil
|
|
&& Initializer.FrontFaceStencilTest == CF_Always
|
|
&& Initializer.FrontFaceStencilFailStencilOp == SO_Keep
|
|
&& Initializer.FrontFaceDepthFailStencilOp == SO_Keep
|
|
&& Initializer.FrontFacePassStencilOp == SO_Keep
|
|
&& !Initializer.bEnableBackFaceStencil
|
|
&& Initializer.BackFaceStencilTest == CF_Always
|
|
&& Initializer.BackFaceStencilFailStencilOp == SO_Keep
|
|
&& Initializer.BackFaceDepthFailStencilOp == SO_Keep
|
|
&& Initializer.BackFacePassStencilOp == SO_Keep, TEXT("No stencil render target set, yet PSO wants to use stencil operations!"));
|
|
/*
|
|
RHI_VALIDATION_CHECK(PSOInitializer.StencilTargetLoadAction == ERenderTargetLoadAction::ENoAction,
|
|
TEXT("No stencil target set, yet PSO wants to load from it!"));
|
|
RHI_VALIDATION_CHECK(PSOInitializer.StencilTargetStoreAction == ERenderTargetStoreAction::ENoAction,
|
|
TEXT("No stencil target set, yet PSO wants to store into it!"));
|
|
*/
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RHI_VALIDATION_CHECK(!Initializer.bEnableDepthWrite && Initializer.DepthTest == CF_Always, TEXT("No depth render target set, yet PSO wants to use depth operations!"));
|
|
RHI_VALIDATION_CHECK(PSOInitializer.DepthTargetLoadAction == ERenderTargetLoadAction::ENoAction
|
|
&& PSOInitializer.StencilTargetLoadAction == ERenderTargetLoadAction::ENoAction,
|
|
TEXT("No depth/stencil target set, yet PSO wants to load from it!"));
|
|
RHI_VALIDATION_CHECK(PSOInitializer.DepthTargetStoreAction == ERenderTargetStoreAction::ENoAction
|
|
&& PSOInitializer.StencilTargetStoreAction == ERenderTargetStoreAction::ENoAction,
|
|
TEXT("No depth/stencil target set, yet PSO wants to store into it!"));
|
|
}
|
|
}
|
|
}
|
|
|
|
void FValidationRHI::RHICreateTransition(FRHITransition* Transition, const FRHITransitionCreateInfo& CreateInfo)
|
|
{
|
|
using namespace RHIValidation;
|
|
|
|
const ERHIPipeline SrcPipelines = CreateInfo.SrcPipelines;
|
|
const ERHIPipeline DstPipelines = CreateInfo.DstPipelines;
|
|
|
|
TArray<FFence*> Fences;
|
|
|
|
if (SrcPipelines != DstPipelines)
|
|
{
|
|
for (ERHIPipeline SrcPipe : MakeFlagsRange(SrcPipelines))
|
|
{
|
|
for (ERHIPipeline DstPipe : MakeFlagsRange(DstPipelines))
|
|
{
|
|
if (SrcPipe == DstPipe)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FFence* Fence = new FFence;
|
|
Fence->SrcPipe = SrcPipe;
|
|
Fence->DstPipe = DstPipe;
|
|
Fences.Add(Fence);
|
|
}
|
|
}
|
|
}
|
|
|
|
TRHIPipelineArray<TArray<FOperation>> SignalOps, WaitOps;
|
|
|
|
TArray<FOperation> AliasingOps, AliasingOverlapOps, BeginOps, EndOps;
|
|
AliasingOverlapOps.Reserve(CreateInfo.AliasingInfos.Num());
|
|
AliasingOps .Reserve(CreateInfo.AliasingInfos.Num());
|
|
BeginOps .Reserve(CreateInfo.TransitionInfos.Num());
|
|
EndOps .Reserve(CreateInfo.TransitionInfos.Num());
|
|
|
|
for (FFence* Fence : Fences)
|
|
{
|
|
WaitOps[Fence->DstPipe].Emplace(FOperation::Wait(Fence));
|
|
}
|
|
|
|
// Take a backtrace of this transition creation if any of the resources it contains have logging enabled.
|
|
bool bDoTrace = false;
|
|
|
|
for (const FRHITransientAliasingInfo& Info : CreateInfo.AliasingInfos)
|
|
{
|
|
if (!Info.Resource)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FResource* Resource = nullptr;
|
|
|
|
if (Info.Type == FRHITransientAliasingInfo::EType::Texture)
|
|
{
|
|
Resource = Info.Texture->GetTrackerResource();
|
|
}
|
|
else
|
|
{
|
|
Resource = Info.Buffer;
|
|
}
|
|
|
|
bDoTrace |= (Resource->LoggingMode != RHIValidation::ELoggingMode::None);
|
|
|
|
if (Info.IsAcquire())
|
|
{
|
|
checkf(Resource->TransientState.bTransient, TEXT("Acquiring resource %s which is not transient. Only transient resources can be acquired."), Resource->GetDebugName());
|
|
|
|
AliasingOps.Emplace(FOperation::AcquireTransientResource(Resource, nullptr));
|
|
|
|
for (const FRHITransientAliasingOverlap& Overlap : Info.Overlaps)
|
|
{
|
|
FResource* ResourceBefore = nullptr;
|
|
|
|
if (Overlap.Type == FRHITransientAliasingOverlap::EType::Texture)
|
|
{
|
|
ResourceBefore = Overlap.Texture->GetTrackerResource();
|
|
}
|
|
else
|
|
{
|
|
ResourceBefore = Overlap.Buffer;
|
|
}
|
|
|
|
checkf(ResourceBefore, TEXT("Null resource provided as an aliasing overlap of %s"), Resource->GetDebugName());
|
|
|
|
AliasingOverlapOps.Emplace(FOperation::AliasingOverlap(ResourceBefore, Resource, nullptr));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 Index = 0; Index < CreateInfo.TransitionInfos.Num(); ++Index)
|
|
{
|
|
const FRHITransitionInfo& Info = CreateInfo.TransitionInfos[Index];
|
|
if (!Info.Resource)
|
|
continue;
|
|
|
|
RHI_VALIDATION_CHECK(Info.AccessAfter != ERHIAccess::Unknown || SrcPipelines == DstPipelines && DstPipelines != ERHIPipeline::All, TEXT("Cannot use Unknown after state when transitioning between pipelines."));
|
|
|
|
checkf(Info.Type != FRHITransitionInfo::EType::Unknown, TEXT("FRHITransitionInfo::Type cannot be Unknown when creating a resource transition."));
|
|
|
|
if (const FRHICommitResourceInfo* CommitInfo = Info.CommitInfo.GetPtrOrNull())
|
|
{
|
|
if (Info.Type == FRHITransitionInfo::EType::Buffer)
|
|
{
|
|
const FRHIBuffer* Buffer = Info.Buffer;
|
|
const EBufferUsageFlags BufferUsage = Buffer->GetUsage();
|
|
const uint32 BufferSize = Buffer->GetSize();
|
|
RHI_VALIDATION_CHECK(EnumHasAllFlags(BufferUsage, BUF_ReservedResource), TEXT("Commit transitions can only be used with reserved resources."));
|
|
RHI_VALIDATION_CHECK(CommitInfo->SizeInBytes <= BufferSize, TEXT("Buffer commit size request must not be larger than the size of the buffer itself, as virtual memory allocation cannot be resized."));
|
|
}
|
|
else
|
|
{
|
|
RHI_VALIDATION_CHECK(false, TEXT("Reserved resource commit is only supported for buffers"));
|
|
}
|
|
}
|
|
|
|
FResourceIdentity Identity;
|
|
|
|
switch (Info.Type)
|
|
{
|
|
default: checkNoEntry(); // fall through
|
|
case FRHITransitionInfo::EType::Texture:
|
|
Identity = Info.Texture->GetTransitionIdentity(Info);
|
|
break;
|
|
|
|
case FRHITransitionInfo::EType::Buffer:
|
|
Identity = Info.Buffer->GetWholeResourceIdentity();
|
|
break;
|
|
|
|
case FRHITransitionInfo::EType::UAV:
|
|
Identity = Info.UAV->GetViewIdentity();
|
|
break;
|
|
|
|
case FRHITransitionInfo::EType::BVH:
|
|
Identity = Info.BVH->GetWholeResourceIdentity();
|
|
break;
|
|
}
|
|
|
|
bDoTrace |= (Identity.Resource->LoggingMode != RHIValidation::ELoggingMode::None);
|
|
|
|
FState PreviousState = FState(Info.AccessBefore, SrcPipelines);
|
|
FState NextState = FState(Info.AccessAfter, DstPipelines);
|
|
|
|
BeginOps.Emplace(FOperation::BeginTransitionResource(Identity, PreviousState, NextState, Info.Flags, CreateInfo.Flags, nullptr));
|
|
EndOps.Emplace(FOperation::EndTransitionResource(Identity, PreviousState, NextState, Info.Flags, nullptr));
|
|
}
|
|
|
|
if (bDoTrace)
|
|
{
|
|
void* Backtrace = CaptureBacktrace();
|
|
|
|
for (FOperation& Op : AliasingOps)
|
|
{
|
|
switch (Op.Type)
|
|
{
|
|
case EOpType::AcquireTransient:
|
|
Op.Data_AcquireTransient.CreateBacktrace = Backtrace;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (FOperation& Op : AliasingOverlapOps) { Op.Data_AliasingOverlap.CreateBacktrace = Backtrace; }
|
|
for (FOperation& Op : BeginOps ) { Op.Data_BeginTransition.CreateBacktrace = Backtrace; }
|
|
for (FOperation& Op : EndOps ) { Op.Data_EndTransition .CreateBacktrace = Backtrace; }
|
|
}
|
|
|
|
for (FFence* Fence : Fences)
|
|
{
|
|
SignalOps[Fence->SrcPipe].Emplace(FOperation::Signal(Fence));
|
|
}
|
|
|
|
Transition->PendingSignals = MoveTemp(SignalOps);
|
|
Transition->PendingWaits = MoveTemp(WaitOps);
|
|
Transition->PendingAliases = MoveTemp(AliasingOps);
|
|
Transition->PendingAliasingOverlaps = MoveTemp(AliasingOverlapOps);
|
|
Transition->PendingOperationsBegin = MoveTemp(BeginOps);
|
|
Transition->PendingOperationsEnd = MoveTemp(EndOps);
|
|
|
|
return RHI->RHICreateTransition(Transition, CreateInfo);
|
|
}
|
|
|
|
namespace RHIValidation
|
|
{
|
|
static inline FString GetReasonString_LockBufferInsideRenderPass(FResource* Buffer)
|
|
{
|
|
const TCHAR* DebugName = Buffer->GetDebugName();
|
|
return FString::Printf(TEXT("Locking non-volatile buffers for writing inside a render pass is not allowed. Resource: \"%s\" (0x%p)."), DebugName ? DebugName : TEXT("Unnamed"), Buffer);
|
|
}
|
|
}
|
|
|
|
void FValidationRHI::LockBufferValidate(class FRHICommandListBase& RHICmdList, FRHIBuffer* Buffer, EResourceLockMode LockMode)
|
|
{
|
|
using namespace RHIValidation;
|
|
|
|
check(GRHISupportsMultithreadedResources || RHICmdList.IsImmediate());
|
|
check(LockMode != RLM_WriteOnly_NoOverwrite || GRHIGlobals.SupportsMapWriteNoOverwrite)
|
|
|
|
if (RHICmdList.IsGraphics() && !EnumHasAnyFlags(Buffer->GetUsage(), BUF_Volatile) && LockMode == RLM_WriteOnly)
|
|
{
|
|
bool bIsInsideRenderPass;
|
|
if (RHICmdList.IsTopOfPipe())
|
|
{
|
|
bIsInsideRenderPass = RHICmdList.IsInsideRenderPass();
|
|
}
|
|
else
|
|
{
|
|
FValidationContext& Ctx = static_cast<FValidationContext&>(RHICmdList.GetContext());
|
|
bIsInsideRenderPass = Ctx.State.bInsideBeginRenderPass;
|
|
}
|
|
RHI_VALIDATION_CHECK(!bIsInsideRenderPass, *GetReasonString_LockBufferInsideRenderPass(Buffer));
|
|
}
|
|
}
|
|
|
|
void* FValidationRHI::RHILockBuffer(class FRHICommandListBase& RHICmdList, FRHIBuffer* Buffer, uint32 Offset, uint32 SizeRHI, EResourceLockMode LockMode)
|
|
{
|
|
LockBufferValidate(RHICmdList, Buffer, LockMode);
|
|
|
|
return RHI->RHILockBuffer(RHICmdList, Buffer, Offset, SizeRHI, LockMode);
|
|
}
|
|
|
|
void* FValidationRHI::RHILockBufferMGPU(class FRHICommandListBase& RHICmdList, FRHIBuffer* Buffer, uint32 GPUIndex, uint32 Offset, uint32 SizeRHI, EResourceLockMode LockMode)
|
|
{
|
|
LockBufferValidate(RHICmdList, Buffer, LockMode);
|
|
|
|
return RHI->RHILockBufferMGPU(RHICmdList, Buffer, GPUIndex, Offset, SizeRHI, LockMode);
|
|
}
|
|
|
|
static void ValidateViewForBufferType(const FRHIViewDesc::FBuffer::FViewInfo& ViewInfo, FRHIBuffer* Buffer)
|
|
{
|
|
if (ViewInfo.BufferType == FRHIViewDesc::EBufferType::Typed)
|
|
{
|
|
const uint64 MaxViewDimensionForTypedBuffer = GRHIGlobals.MaxViewDimensionForTypedBuffer;
|
|
RHI_VALIDATION_CHECK(uint64(ViewInfo.NumElements) <= MaxViewDimensionForTypedBuffer, *FString::Printf(TEXT("Creating a View with Buffer Type = %s , BuferName(Pointer) = %s(%p) with Number of elements = %d which is greater than the Max Number of elements for this Type: %llu"), FRHIViewDesc::GetBufferTypeString(ViewInfo.BufferType), *Buffer->GetName().ToString(), Buffer, ViewInfo.NumElements, MaxViewDimensionForTypedBuffer));
|
|
}
|
|
else
|
|
{
|
|
const uint64 MaxViewSizeBytesForNonTypedBuffer = GRHIGlobals.MaxViewSizeBytesForNonTypedBuffer;
|
|
RHI_VALIDATION_CHECK(uint64(ViewInfo.SizeInBytes) <= MaxViewSizeBytesForNonTypedBuffer, *FString::Printf(TEXT("Creating a View with Buffer Type = %s , BuferName(Pointer) = %s(%p) with Size = %d which is greater than the Max Size for this Type: %llu"), FRHIViewDesc::GetBufferTypeString(ViewInfo.BufferType), *Buffer->GetName().ToString(), Buffer, ViewInfo.SizeInBytes, MaxViewSizeBytesForNonTypedBuffer));
|
|
}
|
|
}
|
|
|
|
FShaderResourceViewRHIRef FValidationRHI::RHICreateShaderResourceView(class FRHICommandListBase& RHICmdList, FRHIViewableResource* Resource, FRHIViewDesc const& ViewDesc)
|
|
{
|
|
if (ViewDesc.IsBuffer())
|
|
{
|
|
FRHIBuffer* Buffer = static_cast<FRHIBuffer*>(Resource);
|
|
const FRHIViewDesc::FBufferSRV::FViewInfo Info = ViewDesc.Buffer.SRV.GetViewInfo(Buffer);
|
|
ValidateViewForBufferType(Info, Buffer);
|
|
}
|
|
return RHI->RHICreateShaderResourceView(RHICmdList, Resource, ViewDesc);
|
|
}
|
|
|
|
FUnorderedAccessViewRHIRef FValidationRHI::RHICreateUnorderedAccessView(class FRHICommandListBase& RHICmdList, FRHIViewableResource* Resource, FRHIViewDesc const& ViewDesc)
|
|
{
|
|
if (ViewDesc.IsBuffer())
|
|
{
|
|
FRHIBuffer* Buffer = static_cast<FRHIBuffer*>(Resource);
|
|
const FRHIViewDesc::FBufferUAV::FViewInfo Info = ViewDesc.Buffer.UAV.GetViewInfo(Buffer);
|
|
ValidateViewForBufferType(Info, Buffer);
|
|
}
|
|
return RHI->RHICreateUnorderedAccessView(RHICmdList, Resource, ViewDesc);
|
|
}
|
|
|
|
FRHILockTextureResult FValidationRHI::RHILockTexture(FRHICommandListImmediate& RHICmdList, const FRHILockTextureArgs& Arguments)
|
|
{
|
|
FRHITexture* Texture = Arguments.Texture;
|
|
const FRHITextureDesc& Desc = Arguments.Texture->GetDesc();
|
|
|
|
RHI_VALIDATION_CHECK(Arguments.MipIndex < Desc.NumMips, TEXT("Out of bounds MipIndex"));
|
|
|
|
switch (Desc.Dimension)
|
|
{
|
|
case ETextureDimension::Texture2D:
|
|
RHI_VALIDATION_CHECK(Arguments.ArrayIndex == 0, TEXT("Texture2D locks do not support array indexing"));
|
|
RHI_VALIDATION_CHECK(Arguments.FaceIndex == 0, TEXT("Texture2D locks do not support face indexing"));
|
|
break;
|
|
case ETextureDimension::Texture2DArray:
|
|
RHI_VALIDATION_CHECK(Arguments.ArrayIndex < Desc.ArraySize, TEXT("Texture2DArray lock out of bounds ArrayIndex"));
|
|
RHI_VALIDATION_CHECK(Arguments.FaceIndex == 0, TEXT("Texture2DArray locks do not support face indexing"));
|
|
break;
|
|
case ETextureDimension::Texture3D:
|
|
RHI_VALIDATION_CHECK(false, TEXT("Texture3D locks have not been fully tested"));
|
|
RHI_VALIDATION_CHECK(Arguments.FaceIndex == 0, TEXT("Texture3D locks do not support face indexing"));
|
|
break;
|
|
case ETextureDimension::TextureCube:
|
|
RHI_VALIDATION_CHECK(Arguments.ArrayIndex == 0, TEXT("TextureCube locks do not support array indexing"));
|
|
RHI_VALIDATION_CHECK(Arguments.FaceIndex < 6, TEXT("TextureCube lock out of bounds FaceIndex"));
|
|
break;
|
|
case ETextureDimension::TextureCubeArray:
|
|
RHI_VALIDATION_CHECK(Arguments.ArrayIndex < Desc.ArraySize, TEXT("Out of bounds ArrayIndex"));
|
|
RHI_VALIDATION_CHECK(Arguments.FaceIndex < 6, TEXT("TextureCubeArray lock out of bounds Face Index"));
|
|
break;
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
|
|
return RHI->RHILockTexture(RHICmdList, Arguments);
|
|
}
|
|
|
|
void FValidationRHI::RHIUnlockTexture(FRHICommandListImmediate& RHICmdList, const FRHILockTextureArgs& Arguments)
|
|
{
|
|
return RHI->RHIUnlockTexture(RHICmdList, Arguments);
|
|
}
|
|
|
|
class FRHIValidationQueueScope
|
|
{
|
|
RHIValidation::FOpQueueState* Prev;
|
|
|
|
public:
|
|
FRHIValidationQueueScope(RHIValidation::FOpQueueState& Queue)
|
|
: Prev(ActiveQueue)
|
|
{
|
|
ActiveQueue = &Queue;
|
|
}
|
|
|
|
~FRHIValidationQueueScope()
|
|
{
|
|
ActiveQueue = Prev;
|
|
}
|
|
|
|
thread_local static RHIValidation::FOpQueueState* ActiveQueue;
|
|
};
|
|
|
|
thread_local RHIValidation::FOpQueueState* FRHIValidationQueueScope::ActiveQueue = nullptr;
|
|
|
|
static FString GetBreadcrumbPath()
|
|
{
|
|
#if WITH_RHI_BREADCRUMBS
|
|
RHIValidation::FOpQueueState* Queue = FRHIValidationQueueScope::ActiveQueue;
|
|
if (Queue && Queue->Breadcrumbs.Current)
|
|
{
|
|
return Queue->Breadcrumbs.Current->GetFullPath();
|
|
}
|
|
|
|
return {};
|
|
|
|
#else
|
|
|
|
return TEXT("<breadcrumbs not enabled>");
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
// FlushType: Thread safe
|
|
void FValidationRHI::RHIBindDebugLabelName(FRHICommandListBase& RHICmdList, FRHITexture* Texture, const TCHAR* Name)
|
|
{
|
|
RHICmdList.EnqueueLambda([Texture, Name = FString(Name)](FRHICommandListBase& RHICmdList)
|
|
{
|
|
((FValidationContext&)RHICmdList.GetComputeContext()).Tracker->Rename(Texture->GetTrackerResource(), *Name);
|
|
});
|
|
|
|
RHI->RHIBindDebugLabelName(RHICmdList, Texture, Name);
|
|
}
|
|
|
|
void FValidationRHI::RHIBindDebugLabelName(FRHICommandListBase& RHICmdList, FRHIBuffer* Buffer, const TCHAR* Name)
|
|
{
|
|
RHICmdList.EnqueueLambda([Buffer, Name = FString(Name)](FRHICommandListBase& RHICmdList)
|
|
{
|
|
((FValidationContext&)RHICmdList.GetComputeContext()).Tracker->Rename(Buffer, *Name);
|
|
});
|
|
|
|
RHI->RHIBindDebugLabelName(RHICmdList, Buffer, Name);
|
|
}
|
|
|
|
void FValidationRHI::RHIBindDebugLabelName(FRHICommandListBase& RHICmdList, FRHIUnorderedAccessView* UnorderedAccessViewRHI, const TCHAR* Name)
|
|
{
|
|
RHIValidation::FResource* Resource = UnorderedAccessViewRHI->GetViewIdentity().Resource;
|
|
RHICmdList.EnqueueLambda([Resource, Name = FString(Name)](FRHICommandListBase& RHICmdList)
|
|
{
|
|
((FValidationContext&)RHICmdList.GetComputeContext()).Tracker->Rename(Resource, *Name);
|
|
});
|
|
|
|
RHI->RHIBindDebugLabelName(RHICmdList, UnorderedAccessViewRHI, Name);
|
|
}
|
|
|
|
void FValidationRHI::ReportValidationFailure(const TCHAR* InMessage)
|
|
{
|
|
// Report failures only once per session, since many of them will happen repeatedly. This is similar to what ensure() does, but
|
|
// ensure() looks at the source location to determine if it's seen the error before. We want to look at the actual message, since
|
|
// all failures of a given kind will come from the same place, but (hopefully) the error message contains the name of the resource
|
|
// and a description of the state, so it should be unique for each failure.
|
|
uint32 Hash = FCrc::StrCrc32<TCHAR>(InMessage);
|
|
|
|
SeenFailureHashesMutex.Lock();
|
|
bool bIsAlreadyInSet;
|
|
SeenFailureHashes.Add(Hash, &bIsAlreadyInSet);
|
|
SeenFailureHashesMutex.Unlock();
|
|
|
|
if (bIsAlreadyInSet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FString Message;
|
|
FString BreadcrumbPath = GetBreadcrumbPath();
|
|
if (!BreadcrumbPath.IsEmpty())
|
|
{
|
|
Message = FString::Printf(
|
|
TEXT("%s")
|
|
TEXT("Breadcrumbs: %s\n")
|
|
TEXT("--------------------------------------------------------------------\n"),
|
|
InMessage, *BreadcrumbPath);
|
|
}
|
|
else
|
|
{
|
|
Message = InMessage;
|
|
}
|
|
|
|
UE_LOG(LogRHI, Error, TEXT("%s"), *Message);
|
|
|
|
if (FPlatformMisc::IsDebuggerPresent() && RHIValidation::GBreakOnTransitionError)
|
|
{
|
|
// Print the message again using the debug output function, because UE_LOG doesn't always reach
|
|
// the VS output window before the breakpoint is triggered, despite the log flush call below.
|
|
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("%s\n"), *Message);
|
|
GLog->Flush();
|
|
PLATFORM_BREAK();
|
|
}
|
|
}
|
|
|
|
static void ValidateBoundUniformBuffers(FRHIShader* Shader, const RHIValidation::FStaticUniformBuffers& StaticUniformBuffers, const RHIValidation::FStageBoundUniformBuffers& BoundUniformBuffers)
|
|
{
|
|
const TCHAR* FreqName = GetShaderFrequencyString(Shader->GetFrequency(), false);
|
|
const TArray<uint32>& LayoutHashes = Shader->GetShaderResourceTable().ResourceTableLayoutHashes;
|
|
|
|
const TArray<FUniformBufferStaticSlot>& StaticSlots = Shader->GetStaticSlots();
|
|
if (LayoutHashes.Num() != StaticSlots.Num())
|
|
{
|
|
RHI_VALIDATION_CHECK(false, *FString::Printf(TEXT("Shader %s(%s): The number of layout hashes (%d) is different from the number of static slots (%d)."), Shader->GetShaderName(), FreqName, LayoutHashes.Num(), StaticSlots.Num()));
|
|
return;
|
|
}
|
|
|
|
for (int32 BindIndex = 0; BindIndex < LayoutHashes.Num(); ++BindIndex)
|
|
{
|
|
uint32 ExpectedLayoutHash = LayoutHashes[BindIndex];
|
|
if (ExpectedLayoutHash == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FRHIUniformBuffer* BoundBuffer = nullptr;
|
|
bool bIsStatic = false;
|
|
|
|
const FUniformBufferStaticSlot StaticSlot = StaticSlots[BindIndex];
|
|
if (IsUniformBufferStaticSlotValid(StaticSlot) && StaticSlot < StaticUniformBuffers.Bindings.Num())
|
|
{
|
|
BoundBuffer = StaticUniformBuffers.Bindings[StaticSlot];
|
|
if (BoundBuffer)
|
|
{
|
|
bIsStatic = true;
|
|
}
|
|
}
|
|
|
|
if (BoundBuffer == nullptr && BindIndex < BoundUniformBuffers.Buffers.Num())
|
|
{
|
|
BoundBuffer = BoundUniformBuffers.Buffers[BindIndex];
|
|
}
|
|
|
|
if (BoundBuffer != nullptr)
|
|
{
|
|
const FRHIUniformBufferLayout& Layout = BoundBuffer->GetLayout();
|
|
uint32 UniformBufferHash = Layout.GetHash();
|
|
RHI_VALIDATION_CHECK(UniformBufferHash == ExpectedLayoutHash, *FString::Printf(TEXT("Shader %s(%s): Invalid layout hash %u for uniform buffer \"%s\" at bind index %d (static: %s). Expecting a buffer called \"%s\", hash %u.)"),
|
|
Shader->GetShaderName(), FreqName, UniformBufferHash, *Layout.GetDebugName(), BindIndex, bIsStatic ? TEXT("yes") : TEXT("no"), *Shader->GetUniformBufferName(BindIndex), ExpectedLayoutHash));
|
|
}
|
|
else
|
|
{
|
|
RHI_VALIDATION_CHECK(false, *FString::Printf(TEXT("Shader %s(%s): missing uniform buffer \"%s\" at index %d."),
|
|
Shader->GetShaderName(), FreqName , *Shader->GetUniformBufferName(BindIndex), BindIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
FValidationComputeContext::FValidationComputeContext(EType InType)
|
|
: Type(InType)
|
|
{
|
|
State.Reset();
|
|
Tracker = &State.TrackerInstance;
|
|
}
|
|
|
|
void FValidationComputeContext::ValidateDispatch()
|
|
{
|
|
if (State.BoundShader == nullptr)
|
|
{
|
|
RHI_VALIDATION_CHECK(false, TEXT("A compute PSO has to be set before dispatching a compute shader."));
|
|
return;
|
|
}
|
|
|
|
ValidateBoundUniformBuffers(State.BoundShader, State.StaticUniformBuffers, State.BoundUniformBuffers);
|
|
}
|
|
|
|
void FValidationComputeContext::FState::Reset()
|
|
{
|
|
ComputePassName.Reset();
|
|
BoundShader = nullptr;
|
|
TrackerInstance.ResetAllUAVState();
|
|
StaticUniformBuffers.Reset();
|
|
BoundUniformBuffers.Reset();
|
|
}
|
|
|
|
FValidationContext::FValidationContext(EType InType)
|
|
: Type(InType)
|
|
{
|
|
State.Reset();
|
|
Tracker = &State.TrackerInstance;
|
|
}
|
|
|
|
void FValidationRHI::RHIEndFrame_RenderThread(FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
RenderThreadFrameID++;
|
|
RHI->RHIEndFrame_RenderThread(RHICmdList);
|
|
}
|
|
|
|
void FValidationRHI::RHIEndFrame(const FRHIEndFrameArgs& Args)
|
|
{
|
|
RHIThreadFrameID++;
|
|
RHI->RHIEndFrame(Args);
|
|
}
|
|
|
|
namespace RHIValidation
|
|
{
|
|
static inline FString GetReasonString_SourceCopyFlagMissing(FRHIBuffer* Buffer)
|
|
{
|
|
return FString::Printf(TEXT("Buffers used as copy source need to be created with BUF_SourceCopy! Resource: \"%s\" (0x%p)."),
|
|
(Buffer->GetName().GetStringLength() > 0) ? *Buffer->GetName().ToString() : TEXT("Unnamed"), Buffer);
|
|
}
|
|
}
|
|
|
|
void FValidationContext::RHICopyToStagingBuffer(FRHIBuffer* SourceBufferRHI, FRHIStagingBuffer* DestinationStagingBufferRHI, uint32 InOffset, uint32 InNumBytes)
|
|
{
|
|
using namespace RHIValidation;
|
|
Tracker->Assert(SourceBufferRHI->GetWholeResourceIdentity(), ERHIAccess::CopySrc);
|
|
if (GRHIValidateBufferSourceCopy)
|
|
{
|
|
RHI_VALIDATION_CHECK(EnumHasAnyFlags(SourceBufferRHI->GetUsage(), BUF_SourceCopy), *GetReasonString_SourceCopyFlagMissing(SourceBufferRHI));
|
|
}
|
|
RHIContext->RHICopyToStagingBuffer(SourceBufferRHI, DestinationStagingBufferRHI, InOffset, InNumBytes);
|
|
}
|
|
|
|
void FValidationComputeContext::RHICopyToStagingBuffer(FRHIBuffer* SourceBufferRHI, FRHIStagingBuffer* DestinationStagingBufferRHI, uint32 InOffset, uint32 InNumBytes)
|
|
{
|
|
using namespace RHIValidation;
|
|
Tracker->Assert(SourceBufferRHI->GetWholeResourceIdentity(), ERHIAccess::CopySrc);
|
|
if (GRHIValidateBufferSourceCopy)
|
|
{
|
|
RHI_VALIDATION_CHECK(EnumHasAnyFlags(SourceBufferRHI->GetUsage(), BUF_SourceCopy), *GetReasonString_SourceCopyFlagMissing(SourceBufferRHI));
|
|
}
|
|
RHIContext->RHICopyToStagingBuffer(SourceBufferRHI, DestinationStagingBufferRHI, InOffset, InNumBytes);
|
|
}
|
|
|
|
void FValidationContext::ValidateDispatch()
|
|
{
|
|
if (State.BoundShaders[SF_Compute] == nullptr)
|
|
{
|
|
RHI_VALIDATION_CHECK(false, TEXT("A compute PSO has to be set before dispatching a compute shader."));
|
|
return;
|
|
}
|
|
|
|
ValidateBoundUniformBuffers(State.BoundShaders[SF_Compute], State.StaticUniformBuffers, State.BoundUniformBuffers.Get(SF_Compute));
|
|
}
|
|
|
|
void FValidationContext::ValidateDrawing()
|
|
{
|
|
if (!State.bGfxPSOSet)
|
|
{
|
|
RHI_VALIDATION_CHECK(false, TEXT("A graphics PSO has to be set in order to be able to draw!"));
|
|
return;
|
|
}
|
|
|
|
for (int32 FrequencyIndex = 0; FrequencyIndex < SF_NumFrequencies; ++FrequencyIndex)
|
|
{
|
|
EShaderFrequency Frequency = (EShaderFrequency)FrequencyIndex;
|
|
if (IsValidGraphicsFrequency(Frequency) && State.BoundShaders[Frequency])
|
|
{
|
|
ValidateBoundUniformBuffers(State.BoundShaders[Frequency], State.StaticUniformBuffers, State.BoundUniformBuffers.Get(Frequency));
|
|
}
|
|
}
|
|
}
|
|
|
|
void FValidationContext::FState::Reset()
|
|
{
|
|
bInsideBeginRenderPass = false;
|
|
bGfxPSOSet = false;
|
|
RenderPassName.Reset();
|
|
PreviousRenderPassName.Reset();
|
|
ComputePassName.Reset();
|
|
FMemory::Memset(BoundShaders, 0);
|
|
TrackerInstance.ResetAllUAVState();
|
|
StaticUniformBuffers.Reset();
|
|
BoundUniformBuffers.Reset();
|
|
}
|
|
|
|
namespace RHIValidation
|
|
{
|
|
void FStaticUniformBuffers::Reset()
|
|
{
|
|
Bindings.Reset();
|
|
check(!bInSetPipelineStateCall);
|
|
}
|
|
|
|
void FStaticUniformBuffers::ValidateSetShaderUniformBuffer(FRHIUniformBuffer* UniformBuffer)
|
|
{
|
|
check(UniformBuffer);
|
|
UniformBuffer->ValidateLifeTime();
|
|
|
|
// Skip validating global uniform buffers that are set internally by the RHI as part of the pipeline state.
|
|
if (bInSetPipelineStateCall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FRHIUniformBufferLayout& Layout = UniformBuffer->GetLayout();
|
|
|
|
checkf(EnumHasAnyFlags(Layout.BindingFlags, EUniformBufferBindingFlags::Shader), TEXT("Uniform buffer '%s' does not have the 'Shader' binding flag."), *Layout.GetDebugName());
|
|
|
|
if (Layout.StaticSlot < Bindings.Num())
|
|
{
|
|
check(Layout.BindingFlags == EUniformBufferBindingFlags::StaticAndShader);
|
|
|
|
ensureMsgf(Bindings[Layout.StaticSlot] == nullptr,
|
|
TEXT("Uniform buffer '%s' was bound statically and is now being bound on a specific RHI shader. Only one binding model should be used at a time."),
|
|
*Layout.GetDebugName());
|
|
}
|
|
}
|
|
|
|
FStageBoundUniformBuffers::FStageBoundUniformBuffers()
|
|
{
|
|
Buffers.Reserve(32);
|
|
}
|
|
|
|
void FStageBoundUniformBuffers::Reset()
|
|
{
|
|
Buffers.SetNum(0);
|
|
}
|
|
|
|
void FStageBoundUniformBuffers::Bind(uint32 Index, FRHIUniformBuffer* UniformBuffer)
|
|
{
|
|
if (Index >= (uint32)Buffers.Num())
|
|
{
|
|
Buffers.AddZeroed(Index + 1 - Buffers.Num());
|
|
}
|
|
|
|
Buffers[Index] = UniformBuffer;
|
|
}
|
|
|
|
void FBoundUniformBuffers::Reset()
|
|
{
|
|
for (FStageBoundUniformBuffers& Stage : StageBindings)
|
|
{
|
|
Stage.Reset();
|
|
}
|
|
}
|
|
|
|
ERHIAccess DecayResourceAccess(ERHIAccess AccessMask, ERHIAccess RequiredAccess, bool bAllowUAVOverlap)
|
|
{
|
|
using T = __underlying_type(ERHIAccess);
|
|
checkf(RequiredAccess == ERHIAccess::SRVGraphics || (T(RequiredAccess) & (T(RequiredAccess) - 1)) == 0, TEXT("Only one required access bit may be set at once."));
|
|
|
|
if (EnumHasAnyFlags(RequiredAccess, ERHIAccess::UAVMask | ERHIAccess::BVHWrite))
|
|
{
|
|
// UAV writes decay to no allowed resource access when overlaps are disabled. A barrier is always required after the dispatch/draw.
|
|
// Otherwise keep the same accessmask and don't touch or decay the state
|
|
return !bAllowUAVOverlap ? ERHIAccess::None : AccessMask;
|
|
}
|
|
|
|
// Handle DSV modes
|
|
if (EnumHasAnyFlags(RequiredAccess, ERHIAccess::DSVWrite))
|
|
{
|
|
constexpr ERHIAccess CompatibleStates =
|
|
ERHIAccess::DSVRead |
|
|
ERHIAccess::DSVWrite;
|
|
|
|
return AccessMask & CompatibleStates;
|
|
}
|
|
if (EnumHasAnyFlags(RequiredAccess, ERHIAccess::DSVRead))
|
|
{
|
|
constexpr ERHIAccess CompatibleStates =
|
|
ERHIAccess::DSVRead |
|
|
ERHIAccess::DSVWrite |
|
|
ERHIAccess::SRVGraphics |
|
|
ERHIAccess::SRVCompute |
|
|
ERHIAccess::CopySrc;
|
|
|
|
return AccessMask & CompatibleStates;
|
|
}
|
|
|
|
if (EnumHasAnyFlags(RequiredAccess, ERHIAccess::WritableMask))
|
|
{
|
|
// Decay to only 1 allowed state for all other writable states.
|
|
return RequiredAccess;
|
|
}
|
|
|
|
// Else, the state is readable. All readable states are compatible.
|
|
return AccessMask;
|
|
}
|
|
|
|
#define BARRIER_TRACKER_LOG_PREFIX_REASON(ReasonString) TEXT("RHI validation failed: " ReasonString ":\n\n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT(" RHI Resource Transition Validation Error \n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT("\n\n")
|
|
|
|
// Warning: this prefix expects a string argument for the resource name, make sure you add it.
|
|
#define BARRIER_TRACKER_LOG_PREFIX_RESNAME TEXT("RHI validation failed for resource: %s:\n\n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT(" RHI Resource Transition Validation Error \n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT("\n\n")
|
|
|
|
#define BARRIER_TRACKER_LOG_SUFFIX TEXT("\n\n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT("\n\n")
|
|
|
|
#define BARRIER_TRACKER_LOG_ENABLE_TRANSITION_BACKTRACE \
|
|
TEXT(" --- Enable barrier logging for this resource to see a callstack backtrace for the RHIBeginTransitions() call ") \
|
|
TEXT("which has not been completed. Use -RHIValidationLog=X,Y,Z to enable backtrace logging for individual resources.\n\n")
|
|
|
|
static inline FString GetResourceDebugName(FResource const* Resource, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
const TCHAR* DebugName = Resource->GetDebugName();
|
|
if (!DebugName)
|
|
{
|
|
DebugName = TEXT("Unnamed");
|
|
}
|
|
|
|
if (SubresourceIndex.IsWholeResource())
|
|
{
|
|
return FString::Printf(
|
|
TEXT("\"%s\" (0x%p) (Whole Resource)"),
|
|
DebugName,
|
|
Resource);
|
|
}
|
|
else
|
|
{
|
|
return FString::Printf(
|
|
TEXT("\"%s\" (0x%p) (Mip %d, Slice %d, Plane %d)"),
|
|
DebugName,
|
|
Resource,
|
|
SubresourceIndex.MipIndex,
|
|
SubresourceIndex.ArraySlice,
|
|
SubresourceIndex.PlaneIndex);
|
|
}
|
|
}
|
|
|
|
static inline FString GetReasonString_MissingBarrier(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState,
|
|
const FState& RequiredState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to access resource %s from a hardware unit it is not currently accessible from. A resource transition is required.\n\n")
|
|
TEXT(" --- Allowed access states for this resource are: %s\n")
|
|
TEXT(" --- Required access states are: %s\n")
|
|
TEXT(" --- Allowed pipelines for this resource are: %s\n")
|
|
TEXT(" --- Required pipelines are: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*GetRHIAccessName(RequiredState.Access),
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIPipelineName(RequiredState.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_IncorrectSetTrackedAccess(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState,
|
|
const FState& TrackedState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to assign resource %s a tracked access that does not match its validation tracked access.\n\n")
|
|
TEXT(" --- Actual access states: %s\n")
|
|
TEXT(" --- Actual pipelines: %s\n")
|
|
TEXT(" --- Assigned access states: %s\n")
|
|
TEXT(" --- Assigned pipelines: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIAccessName(TrackedState.Access),
|
|
*GetRHIPipelineName(TrackedState.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_IncorrectGetTrackedAccess(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState,
|
|
const FState& TrackedState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to resolve ERHIAccess::Unknown for resource %s but its tracked access that does match its validation tracked access.\n\n")
|
|
TEXT(" --- Validation actual access states: %s\n")
|
|
TEXT(" --- Validation actual pipelines: %s\n")
|
|
TEXT(" --- Tracked access states: %s\n")
|
|
TEXT(" --- Tracked pipelines: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIAccessName(TrackedState.Access),
|
|
*GetRHIPipelineName(TrackedState.Pipelines));
|
|
}
|
|
|
|
static FString ResolveAndFormatCallstack (uint64* Trace)
|
|
{
|
|
FString Callstack;
|
|
ANSICHAR Buffer[1024];
|
|
|
|
for (uint32 Idx = IgnoreStackCount; Idx < NumStackFrames; Idx++)
|
|
{
|
|
Buffer[0] = '\0'; // Clear the buffer
|
|
const ANSICHAR* TrimmedBuffer = Buffer;
|
|
|
|
// Resolve the program counter to a human-readable string
|
|
const bool bFoundSymbol = FPlatformStackWalk::ProgramCounterToHumanReadableString(
|
|
Idx,
|
|
Trace[Idx],
|
|
Buffer,
|
|
sizeof(Buffer)
|
|
);
|
|
|
|
if (bFoundSymbol)
|
|
{
|
|
// Find the "!" delimiter to trim the module and address
|
|
const ANSICHAR* BufferAfterModuleAndAddress = FCStringAnsi::Strstr(Buffer, "!");
|
|
if (BufferAfterModuleAndAddress)
|
|
{
|
|
TrimmedBuffer = BufferAfterModuleAndAddress + 1;
|
|
}
|
|
}
|
|
|
|
if (FCStringAnsi::Strstr(Buffer, "UnknownFunction") == nullptr)
|
|
{
|
|
// Append the trimmed symbol information to the call stack string
|
|
Callstack += FString(TrimmedBuffer) + TEXT("\r\n");
|
|
}
|
|
}
|
|
|
|
return Callstack;
|
|
}
|
|
|
|
|
|
static inline FString GetReasonString_BeginBacktrace(void* CreateTrace, void* BeginTrace)
|
|
{
|
|
if (CreateTrace || BeginTrace)
|
|
{
|
|
if (GRHIValidationPrintHumanReadableCallStack)
|
|
{
|
|
return FString::Printf(
|
|
TEXT(" --- Callstack backtraces for the transition which has not been completed:\n")
|
|
TEXT(" RHICreateTransition: %s\n")
|
|
TEXT(" RHIBeginTransitions: %s\n"),
|
|
*ResolveAndFormatCallstack((uint64*)CreateTrace),
|
|
*ResolveAndFormatCallstack((uint64*)BeginTrace));
|
|
|
|
}
|
|
else
|
|
{
|
|
return FString::Printf(
|
|
TEXT(" --- Callstack backtraces for the transition which has not been completed (resolve in the Watch window):\n")
|
|
TEXT(" RHICreateTransition: (void**)0x%p,32\n")
|
|
TEXT(" RHIBeginTransitions: (void**)0x%p,32\n"),
|
|
CreateTrace,
|
|
BeginTrace);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return BARRIER_TRACKER_LOG_ENABLE_TRANSITION_BACKTRACE;
|
|
}
|
|
}
|
|
|
|
static inline FString GetReasonString_Backtrace(const TCHAR* OperationPrefix, const TCHAR* TracePrefix, void* Trace)
|
|
{
|
|
if (Trace)
|
|
{
|
|
if (GRHIValidationPrintHumanReadableCallStack)
|
|
{
|
|
return FString::Printf(
|
|
TEXT(" --- Callstack backtrace for %s operation:\n")
|
|
TEXT(" %s: %s\n"),
|
|
OperationPrefix,
|
|
TracePrefix,
|
|
*ResolveAndFormatCallstack((uint64*)Trace));
|
|
}
|
|
else
|
|
{
|
|
return FString::Printf(
|
|
TEXT(" --- Callstack backtrace for %s operation (resolve in the Watch window):\n")
|
|
TEXT(" %s: (void**)0x%p,32\n"),
|
|
OperationPrefix,
|
|
TracePrefix,
|
|
Trace);
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return FString(BARRIER_TRACKER_LOG_ENABLE_TRANSITION_BACKTRACE);
|
|
}
|
|
}
|
|
|
|
static inline FString GetReasonString_DuplicateBackTrace(void* PreviousTrace, void* CurrentTrace)
|
|
{
|
|
if (PreviousTrace || CurrentTrace)
|
|
{
|
|
return
|
|
GetReasonString_Backtrace(TEXT("previous"), TEXT("RHICreateTransition"), PreviousTrace) +
|
|
GetReasonString_Backtrace(TEXT("current"), TEXT("RHICreateTransition"), CurrentTrace);
|
|
}
|
|
else
|
|
{
|
|
return BARRIER_TRACKER_LOG_ENABLE_TRANSITION_BACKTRACE;
|
|
}
|
|
}
|
|
|
|
static inline FString GetReasonString_AccessDuringTransition(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& PendingState,
|
|
const FState& AttemptedState,
|
|
void* CreateTrace, void* BeginTrace)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to access resource %s whilst an asynchronous resource transition is in progress. A call to RHIEndTransitions() must be made before the resource can be accessed again.\n\n")
|
|
TEXT(" --- Pending access states for this resource are: %s\n")
|
|
TEXT(" --- Attempted access states are: %s\n")
|
|
TEXT(" --- Pending pipelines for this resource are: %s\n")
|
|
TEXT(" --- Attempted pipelines are: %s\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(PendingState.Access),
|
|
*GetRHIAccessName(AttemptedState.Access),
|
|
*GetRHIPipelineName(PendingState.Pipelines),
|
|
*GetRHIPipelineName(AttemptedState.Pipelines),
|
|
*GetReasonString_BeginBacktrace(CreateTrace, BeginTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_TransitionWithoutAcquire(FResource* Resource)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted a resource transition for transient resource %s without acquiring it. Transient resources must be acquired before any transitions are begun and discarded after all transitions are complete.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName);
|
|
}
|
|
|
|
static inline FString GetReasonString_AcquireNonTransient(FResource* Resource)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to acquire non-transient resource %s. Only transient resources may be acquired with the transient aliasing API.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName);
|
|
}
|
|
|
|
static inline FString GetReasonString_DiscardNonTransient(FResource* Resource)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to discard non-transient resource %s. Only transient resources may be discarded with the transient aliasing API.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName);
|
|
}
|
|
|
|
static inline FString GetReasonString_AliasingOverlapNonDiscarded(FResource* ResourceBefore, FResource* ResourceAfter, void* CreateTrace)
|
|
{
|
|
FString DebugNameBefore = GetResourceDebugName(ResourceBefore, {});
|
|
FString DebugNameAfter = GetResourceDebugName(ResourceAfter, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to overlap resource %s (before) with resource %s (after), but %s (before) has not been discarded.\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugNameAfter,
|
|
*DebugNameBefore,
|
|
*DebugNameAfter,
|
|
*DebugNameBefore,
|
|
*GetReasonString_Backtrace(TEXT("acquire"), TEXT("RHICreateTransition"), CreateTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_AliasingOverlapNonTransient(FResource* ResourceBefore, FResource* ResourceAfter)
|
|
{
|
|
FString DebugNameBefore = GetResourceDebugName(ResourceBefore, {});
|
|
FString DebugNameAfter = GetResourceDebugName(ResourceAfter, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to overlap non-transient resource %s when acquiring resource %s. Only transient resources may be used in an aliasing overlap operation.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugNameBefore,
|
|
*DebugNameBefore,
|
|
*DebugNameAfter);
|
|
}
|
|
|
|
static inline FString GetReasonString_DuplicateAcquireTransient(FResource* Resource, void* PreviousAcquireTrace, void* CurrentAcquireTrace)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Mismatched acquire of transient resource %s. A transient resource may only be acquired once in its lifetime.\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetReasonString_DuplicateBackTrace(PreviousAcquireTrace, CurrentAcquireTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_DiscardWithoutAcquireTransient(FResource* Resource, void* DiscardTrace)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to discard transient resource %s, but it was never acquired.\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetReasonString_Backtrace(TEXT("discard"), TEXT("RHICreateTransition"), DiscardTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_AlreadyDiscarded(FResource* Resource, void* DiscardTrace)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, {});
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to transition transient resource %s to ERHIAccess::Discard, but it has already been discarded.\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetReasonString_Backtrace(TEXT("discard"), TEXT("RHICreateTransition"), DiscardTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_DuplicateBeginTransition(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& PendingState,
|
|
const FState& TargetState,
|
|
void* CreateTrace, void* BeginTrace)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to begin a resource transition for resource %s whilst a previous asynchronous resource transition is already in progress. A call to RHIEndTransitions() must be made before the resource can be transitioned again.\n\n")
|
|
TEXT(" --- Pending access states for this resource are: %s\n")
|
|
TEXT(" --- Attempted access states for the duplicate transition are: %s\n")
|
|
TEXT(" --- Pending pipelines for this resource are: %s\n")
|
|
TEXT(" --- Attempted pipelines for the duplicate transition are: %s\n")
|
|
TEXT("%s")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(PendingState.Access),
|
|
*GetRHIAccessName(TargetState.Access),
|
|
*GetRHIPipelineName(PendingState.Pipelines),
|
|
*GetRHIPipelineName(TargetState.Pipelines),
|
|
*GetReasonString_BeginBacktrace(CreateTrace, BeginTrace));
|
|
}
|
|
|
|
static inline FString GetReasonString_WrongPipeline(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& ActualCurrentState,
|
|
const FState& CurrentStateFromRHI)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to begin a resource transition for resource %s on the wrong pipeline(s) (\"%s\"). The resource is currently accessible on the \"%s\" pipeline(s).\n\n")
|
|
TEXT(" --- Current access states for this resource are: %s\n")
|
|
TEXT(" --- Attempted access states are: %s\n\n")
|
|
TEXT(" --- Ensure that resource transitions are issued on the correct pipeline.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIPipelineName(CurrentStateFromRHI.Pipelines),
|
|
*GetRHIPipelineName(ActualCurrentState.Pipelines),
|
|
*GetRHIAccessName(ActualCurrentState.Access),
|
|
*GetRHIAccessName(CurrentStateFromRHI.Access));
|
|
}
|
|
|
|
static inline FString GetReasonString_IncorrectFencing(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
ERHIPipeline SrcPipelineSkipped,
|
|
ERHIPipeline DstPipeline)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
FString SrcPipelineName = *GetRHIPipelineName(SrcPipelineSkipped);
|
|
FString DstPipelineName = *GetRHIPipelineName(DstPipeline);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attemped to begin a resource transition for resource %s on the %s pipeline but skipping the transition on the %s pipeline (which is allowed with the NoFence flag), however no external\n")
|
|
TEXT("fence was issued between these two pipelines between this begin transition and the last end transition call on the %s pipeline. You must insert a manual fence from '%s' to '%s'.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*DstPipelineName,
|
|
*SrcPipelineName,
|
|
*SrcPipelineName,
|
|
*SrcPipelineName,
|
|
*DstPipelineName);
|
|
}
|
|
|
|
static inline FString GetReasonString_IncorrectPreviousExplicitState(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState,
|
|
const FState& CurrentStateFromRHI)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("The explicit previous state \"%s\" does not match the tracked current state \"%s\" for the resource %s.\n")
|
|
TEXT(" --- Allowed pipelines for this resource are: %s\n")
|
|
TEXT(" --- Previous pipelines passed as part of the resource transition were: %s\n\n")
|
|
TEXT(" --- The best solution is to correct the explicit previous state passed for the resource in the call to RHICreateTransition().\n")
|
|
TEXT(" --- Alternatively, use ERHIAccess::Unknown if the actual previous state cannot be determined. Unknown previous resource states have a performance impact so should be avoided if possible.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentStateFromRHI.Access),
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*DebugName,
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIPipelineName(CurrentStateFromRHI.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_IncorrectPreviousTrackedState(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState,
|
|
ERHIPipeline PipelineFromRHI)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("The tracked previous state \"%s\" does not match the tracked current state \"%s\" for the resource %s.\n")
|
|
TEXT(" --- Allowed pipelines for this resource are: %s\n")
|
|
TEXT(" --- Previous pipelines passed as part of the resource transition were: %s\n\n")
|
|
TEXT(" --- The previous state was pulled from the last call to RHICmdList.SetTrackedAccess due to the use of ERHIAccess::Unknown. If this doesn't match the expected state, be sure to update the \n")
|
|
TEXT(" --- tracked state after using manual low - level transitions. It is highly recommended to coalesce all subresources into the same state before relying on tracked previous states with \n")
|
|
TEXT(" --- ERHIAccess::Unknown. RHICmdList.SetTrackedAccess applies to whole resources.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*GetRHIAccessName(Resource->GetTrackedState().Access),
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*DebugName,
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIPipelineName(PipelineFromRHI));
|
|
}
|
|
|
|
static inline FString GetReasonString_MismatchedEndTransition(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& TargetState,
|
|
const FState& TargetStateFromRHI)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("The expected target state \"%s\" on pipe \"%s\" in end transition does not match the tracked target state \"%s\" on pipe \"%s\" for the resource %s.\n")
|
|
TEXT(" --- The call to EndTransition() is mismatched with the another BeginTransition() with different states.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*GetRHIAccessName(TargetStateFromRHI.Access),
|
|
*GetRHIPipelineName(TargetState.Pipelines),
|
|
*GetRHIAccessName(TargetState.Access),
|
|
*GetRHIPipelineName(TargetStateFromRHI.Pipelines),
|
|
*DebugName);
|
|
}
|
|
|
|
static inline FString GetReasonString_UnnecessaryTransition(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to begin a resource transition for the resource %s to the \"%s\" state on the \"%s\" pipe, but the resource is already in this state. The resource transition is unnecessary.\n")
|
|
TEXT(" --- This is not fatal, but does have an effect on CPU and GPU performance. Consider refactoring rendering code to avoid unnecessary resource transitions.\n")
|
|
TEXT(" --- RenderGraph (RDG) is capable of handling resource transitions automatically.\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*GetRHIPipelineName(CurrentState.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_MismatchedAllUAVsOverlapCall(bool bAllow)
|
|
{
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_REASON("UAV overlap mismatch")
|
|
TEXT("Mismatched call to %sUAVOverlap.\n\n")
|
|
TEXT(" --- Ensure all calls to RHICmdList.BeginUAVOverlap() are paired with a call to RHICmdList.EndUAVOverlap().\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
bAllow ? TEXT("Begin") : TEXT("End")
|
|
);
|
|
}
|
|
|
|
static inline FString GetReasonString_MismatchedExplicitUAVOverlapCall(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
bool bAllow)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_REASON("UAV overlap mismatch")
|
|
TEXT("Mismatched call to %sUAVOverlap(FRHIUnorderedAccessView*) for the resource %s.\n\n")
|
|
TEXT(" --- Ensure all calls to RHICmdList.BeginUAVOverlap() are paired with a call to RHICmdList.EndUAVOverlap().\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
bAllow ? TEXT("Begin") : TEXT("End"),
|
|
*DebugName
|
|
);
|
|
}
|
|
|
|
static inline FString GetReasonString_UAVOverlap(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& CurrentState, const FState& RequiredState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to access resource %s which was previously used with overlapping UAV access, but has not been transitioned since UAV overlap was disabled. A resource transition is required.\n\n")
|
|
TEXT(" --- Allowed access states for this resource are: %s\n")
|
|
TEXT(" --- Required access states are: %s\n")
|
|
TEXT(" --- Allowed pipelines for this resource are: %s\n")
|
|
TEXT(" --- Required pipelines are: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(CurrentState.Access),
|
|
*GetRHIAccessName(RequiredState.Access),
|
|
*GetRHIPipelineName(CurrentState.Pipelines),
|
|
*GetRHIPipelineName(RequiredState.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_IgnoreAfterStateAllPipes(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& PendingState,
|
|
const FState& TargetState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to begin a resource transition for resource %s on All pipes. Transition with EResourceTransitionFlags::IgnoreAfterState on All pipes are not supported.\n\n")
|
|
TEXT(" --- Pending access states for this resource are: %s\n")
|
|
TEXT(" --- Attempted access states for the current transition are: %s\n")
|
|
TEXT(" --- Pending pipelines for this resource are: %s\n")
|
|
TEXT(" --- Attempted pipelines for the current transition are: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(PendingState.Access),
|
|
*GetRHIAccessName(TargetState.Access),
|
|
*GetRHIPipelineName(PendingState.Pipelines),
|
|
*GetRHIPipelineName(TargetState.Pipelines));
|
|
}
|
|
|
|
static inline FString GetReasonString_MismatchedIgnoreAfterState(
|
|
FResource* Resource, FSubresourceIndex const& SubresourceIndex,
|
|
const FState& PendingState,
|
|
const FState& TargetState)
|
|
{
|
|
FString DebugName = GetResourceDebugName(Resource, SubresourceIndex);
|
|
return FString::Printf(
|
|
BARRIER_TRACKER_LOG_PREFIX_RESNAME
|
|
TEXT("Attempted to begin a resource transition for resource %s whilst not having the flag EResourceTransitionFlags::IgnoreAfterState matching the previous transiton. Transition with EResourceTransitionFlags::IgnoreAfterState always needs to be done in pairs.\n\n")
|
|
TEXT(" --- Pending access states for this resource are: %s\n")
|
|
TEXT(" --- Attempted access states for the current transition are: %s\n")
|
|
TEXT(" --- Pending pipelines for this resource are: %s\n")
|
|
TEXT(" --- Attempted pipelines for the current transition are: %s\n")
|
|
BARRIER_TRACKER_LOG_SUFFIX,
|
|
*DebugName,
|
|
*DebugName,
|
|
*GetRHIAccessName(PendingState.Access),
|
|
*GetRHIAccessName(TargetState.Access),
|
|
*GetRHIPipelineName(PendingState.Pipelines),
|
|
*GetRHIPipelineName(TargetState.Pipelines));
|
|
}
|
|
|
|
static inline void* Log(FResource* Resource, FSubresourceIndex const& SubresourceIndex, void* CreateTrace, const TCHAR* TracePrefix, const TCHAR* Type, const TCHAR* LogStr)
|
|
{
|
|
void* Trace = CaptureBacktrace();
|
|
|
|
FString BreadcrumbMessage = GetBreadcrumbPath();
|
|
|
|
if (CreateTrace)
|
|
{
|
|
if (GRHIValidationPrintHumanReadableCallStack)
|
|
{
|
|
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("\n%s: Type: %s, %s, \nCreateTrace: %s\n, %sTrace: %s\n, %s\n"),
|
|
*GetResourceDebugName(Resource, SubresourceIndex),
|
|
Type,
|
|
LogStr,
|
|
*ResolveAndFormatCallstack((uint64*)CreateTrace),
|
|
TracePrefix,
|
|
*ResolveAndFormatCallstack((uint64*)Trace),
|
|
*BreadcrumbMessage);
|
|
}
|
|
else
|
|
{
|
|
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("\n%s: Type: %s, %s, CreateTrace: 0x%p, %sTrace: 0x%p, %s\n"),
|
|
*GetResourceDebugName(Resource, SubresourceIndex),
|
|
Type,
|
|
LogStr,
|
|
CreateTrace,
|
|
TracePrefix,
|
|
Trace,
|
|
*BreadcrumbMessage);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (GRHIValidationPrintHumanReadableCallStack)
|
|
{
|
|
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("\n%s: Type: %s, %s, \nTrace: %s\n, %s\n"),
|
|
*GetResourceDebugName(Resource, SubresourceIndex),
|
|
Type,
|
|
LogStr,
|
|
*ResolveAndFormatCallstack((uint64*)Trace),
|
|
*BreadcrumbMessage);
|
|
}
|
|
else
|
|
{
|
|
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("\n%s: Type: %s, %s, Trace: 0x%p, %s\n"),
|
|
*GetResourceDebugName(Resource, SubresourceIndex),
|
|
Type,
|
|
LogStr,
|
|
Trace,
|
|
*BreadcrumbMessage);
|
|
}
|
|
}
|
|
|
|
return Trace;
|
|
}
|
|
|
|
void FTransientState::Acquire(FResource* Resource, void* CreateTrace, ERHIPipeline ExecutingPipeline)
|
|
{
|
|
RHI_VALIDATION_CHECK(bTransient, *GetReasonString_AcquireNonTransient(Resource));
|
|
RHI_VALIDATION_CHECK(Status == EStatus::None, *GetReasonString_DuplicateAcquireTransient(Resource, AcquireBacktrace, CreateTrace));
|
|
Status = EStatus::Acquired;
|
|
|
|
if (!AcquireBacktrace)
|
|
{
|
|
AcquireBacktrace = CreateTrace;
|
|
}
|
|
|
|
NumAcquiredSubresources = Resource->GetNumSubresources() * GetRHIPipelineCount();
|
|
|
|
if (Resource->LoggingMode != ELoggingMode::None)
|
|
{
|
|
Log(Resource, {}, CreateTrace, TEXT("Acquire"), TEXT("Acquire"), *FString::Printf(TEXT("Transient Acquire, Executing Pipeline : %s"), *GetRHIPipelineName(ExecutingPipeline)));
|
|
}
|
|
}
|
|
|
|
void FTransientState::Discard(FResource* Resource, void* CreateTrace, ERHIPipeline DiscardPipelines, ERHIPipeline ExecutingPipeline)
|
|
{
|
|
RHI_VALIDATION_CHECK(bTransient, *GetReasonString_DiscardNonTransient(Resource));
|
|
RHI_VALIDATION_CHECK(Status != EStatus::None, *GetReasonString_DiscardWithoutAcquireTransient(Resource, CreateTrace));
|
|
RHI_VALIDATION_CHECK(Status != EStatus::Discarded, *GetReasonString_AlreadyDiscarded(Resource, CreateTrace));
|
|
|
|
// When discarding from all pipes, each pipe will call Discard separately. Otherwise it's just one call.
|
|
const uint32 NumDerefs = DiscardPipelines == ERHIPipeline::All ? 1 : 2;
|
|
|
|
NumAcquiredSubresources -= NumDerefs;
|
|
|
|
if (NumAcquiredSubresources == 0)
|
|
{
|
|
Status = EStatus::Discarded;
|
|
|
|
if (Resource->LoggingMode != ELoggingMode::None)
|
|
{
|
|
Log(Resource, {}, CreateTrace, TEXT("Discard"), TEXT("Discard"), *FString::Printf(TEXT("Transient Discard, Executing Pipeline : %s"), *GetRHIPipelineName(ExecutingPipeline)) );
|
|
}
|
|
}
|
|
}
|
|
|
|
void FTransientState::AliasingOverlap(FResource* ResourceBefore, FResource* ResourceAfter, void* CreateTrace)
|
|
{
|
|
FTransientState& TransientStateBefore = ResourceBefore->TransientState;
|
|
FTransientState& TransientStateAfter = ResourceAfter->TransientState;
|
|
|
|
// Acquire should validate whether ResourceAfter is transient. We assume it is here.
|
|
RHI_VALIDATION_CHECK(TransientStateBefore.bTransient, *GetReasonString_AliasingOverlapNonTransient(ResourceBefore, ResourceAfter));
|
|
RHI_VALIDATION_CHECK(TransientStateBefore.IsDiscarded(), *GetReasonString_AliasingOverlapNonDiscarded(ResourceBefore, ResourceAfter, CreateTrace));
|
|
|
|
if (ResourceBefore->LoggingMode != ELoggingMode::None)
|
|
{
|
|
Log(ResourceBefore, {}, CreateTrace, TEXT("AliasingOverlap"), TEXT("AliasingOverlap"), TEXT("Aliasing Overlap (Before)"));
|
|
}
|
|
|
|
if (ResourceAfter->LoggingMode != ELoggingMode::None)
|
|
{
|
|
Log(ResourceAfter, {}, CreateTrace, TEXT("AliasingOverlap"), TEXT("AliasingOverlap"), TEXT("Aliasing Overlap (After)"));
|
|
}
|
|
}
|
|
|
|
void FResource::SetDebugName(const TCHAR* Name, const TCHAR* Suffix)
|
|
{
|
|
DebugName = Suffix
|
|
? FString::Printf(TEXT("%s%s"), Name, Suffix)
|
|
: Name;
|
|
|
|
if (LoggingMode != ELoggingMode::Manual)
|
|
{
|
|
// Automatically enable/disable barrier logging if the resource name
|
|
// does/doesn't match one in the AutoLogResourceNames array.
|
|
if (Name)
|
|
{
|
|
for (FString const& Str : GetAutoLogResourceNames())
|
|
{
|
|
if (FCString::Stricmp(Name, *Str) == 0)
|
|
{
|
|
LoggingMode = ELoggingMode::Automatic;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
LoggingMode = ELoggingMode::None;
|
|
}
|
|
}
|
|
|
|
void FResource::InitTransient(const TCHAR* InDebugName)
|
|
{
|
|
check(TransientState.bTransient && TransientState.Status != FTransientState::EStatus::Acquired);
|
|
TransientState.Status = FTransientState::EStatus::None;
|
|
DebugName = InDebugName;
|
|
|
|
for (ERHIPipeline Pipeline : MakeFlagsRange(ERHIPipeline::All))
|
|
{
|
|
auto& State = WholeResourceState.States[Pipeline];
|
|
|
|
State.Current.Access = ERHIAccess::Discard;
|
|
State.Current.Pipelines = Pipeline;
|
|
State.Previous = State.Current;
|
|
}
|
|
SubresourceStates.Reset();
|
|
}
|
|
|
|
void FResource::InitBarrierTracking(int32 InNumMips, int32 InNumArraySlices, int32 InNumPlanes, ERHIAccess InResourceState, const TCHAR* InDebugName)
|
|
{
|
|
checkSlow(InNumMips > 0 && InNumArraySlices > 0 && InNumPlanes > 0);
|
|
check(InResourceState != ERHIAccess::Unknown);
|
|
|
|
NumMips = InNumMips;
|
|
NumArraySlices = InNumArraySlices;
|
|
NumPlanes = InNumPlanes;
|
|
TransientState = FTransientState(InResourceState);
|
|
TrackedState = FState(InResourceState, ERHIPipeline::None);
|
|
|
|
for (ERHIPipeline Pipeline : MakeFlagsRange(ERHIPipeline::All))
|
|
{
|
|
auto& State = WholeResourceState.States[Pipeline];
|
|
|
|
State.Current.Access = InResourceState;
|
|
State.Current.Pipelines = Pipeline;
|
|
State.Previous = State.Current;
|
|
}
|
|
|
|
if (InDebugName != nullptr)
|
|
{
|
|
SetDebugName(InDebugName);
|
|
}
|
|
}
|
|
|
|
void FSubresourceState::BeginTransition(FResource* Resource, FSubresourceIndex const& SubresourceIndex, const FState& CurrentStateFromRHI, const FState& InTargetState, EResourceTransitionFlags NewFlags, ERHITransitionCreateFlags CreateFlags, ERHIPipeline ExecutingPipeline, const TRHIPipelineArray<uint64>& PipelineMaxAwaitedFenceValues, void* CreateTrace)
|
|
{
|
|
FPipelineState& State = States[ExecutingPipeline];
|
|
|
|
FState TargetState = InTargetState;
|
|
|
|
if (TargetState.Access == ERHIAccess::Unknown)
|
|
{
|
|
TargetState.Access = Resource->GetTrackedState().Access;
|
|
}
|
|
|
|
void* BeginTrace = nullptr;
|
|
if (Resource->LoggingMode != ELoggingMode::None
|
|
#if LOG_UNNAMED_RESOURCES
|
|
|| Resource->GetDebugName() == nullptr
|
|
#endif
|
|
)
|
|
{
|
|
const TCHAR* PulledFromTracked = TEXT("");
|
|
|
|
if (InTargetState.Access == ERHIAccess::Unknown)
|
|
{
|
|
PulledFromTracked = TEXT(" (Pulled From SetTrackedAccess)");
|
|
}
|
|
|
|
BeginTrace = Log(Resource, SubresourceIndex, CreateTrace, TEXT("Begin"), TEXT("BeginTransition"), *FString::Printf(TEXT("Current: (%s) -> Before(%s) New: (%s)%s, Flags: %s, Executing Pipeline: %s"),
|
|
*State.Current.ToString(),
|
|
*CurrentStateFromRHI.ToString(),
|
|
*TargetState.ToString(),
|
|
PulledFromTracked,
|
|
*GetResourceTransitionFlagsName(NewFlags),
|
|
*GetRHIPipelineName(ExecutingPipeline)
|
|
));
|
|
}
|
|
|
|
if (CurrentStateFromRHI.Access == ERHIAccess::Unknown)
|
|
{
|
|
RHI_VALIDATION_CHECK(Resource->GetTrackedState().Access == State.Previous.Access, *GetReasonString_IncorrectGetTrackedAccess(Resource, SubresourceIndex, State.Previous, Resource->GetTrackedState()));
|
|
}
|
|
|
|
if (Resource->TransientState.bTransient)
|
|
{
|
|
RHI_VALIDATION_CHECK(Resource->TransientState.IsAcquired(), *GetReasonString_TransitionWithoutAcquire(Resource));
|
|
|
|
if (EnumHasAnyFlags(TargetState.Access, ERHIAccess::Discard))
|
|
{
|
|
Resource->TransientState.Discard(Resource, CreateTrace, CurrentStateFromRHI.Pipelines, ExecutingPipeline);
|
|
}
|
|
}
|
|
|
|
// If we are collapsing multiple pipes to one pipe (only allowed when not fencing), check that the other pipes were fenced prior to this call.
|
|
if (EnumHasAnyFlags(CreateFlags, ERHITransitionCreateFlags::NoFence))
|
|
{
|
|
for (ERHIPipeline AlreadyFencedPipeline : MakeFlagsRange(State.Previous.Pipelines & ~CurrentStateFromRHI.Pipelines))
|
|
{
|
|
// The max awaited fence value should be higher than the last transitioned fence value, otherwise a fence was not issued.
|
|
RHI_VALIDATION_CHECK(LastTransitionFences[AlreadyFencedPipeline] < PipelineMaxAwaitedFenceValues[AlreadyFencedPipeline], *GetReasonString_IncorrectFencing(Resource, SubresourceIndex, AlreadyFencedPipeline, ExecutingPipeline));
|
|
}
|
|
}
|
|
|
|
// Check we're not already transitioning
|
|
RHI_VALIDATION_CHECK(!State.bTransitioning, *GetReasonString_DuplicateBeginTransition(Resource, SubresourceIndex, State.Current, TargetState, State.CreateTransitionBacktrace, BeginTrace));
|
|
|
|
// Validate the explicit previous state from the RHI matches what we expect...
|
|
{
|
|
// Check for the correct pipeline
|
|
RHI_VALIDATION_CHECK(EnumHasAllFlags(CurrentStateFromRHI.Pipelines, ExecutingPipeline), *GetReasonString_WrongPipeline(Resource, SubresourceIndex, State.Current, TargetState));
|
|
|
|
const auto HasMatchingPipelines = [CreateFlags, Resource] (ERHIPipeline PreviousFromState, ERHIPipeline PreviousFromRHI)
|
|
{
|
|
if (PreviousFromState == PreviousFromRHI)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// We allow collapsing pipes from All -> Single only if the flag is explicitly provided.
|
|
if (EnumHasAnyFlags(CreateFlags, ERHITransitionCreateFlags::AllowDecayPipelines))
|
|
{
|
|
return EnumHasAnyFlags(PreviousFromState, PreviousFromRHI) && PreviousFromState == Resource->GetTrackedState().Pipelines;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
bool bHasMatchingPipelines = true;
|
|
|
|
// We do not check pipelines for IgnoreAfterState since we do not replicate those transition on all pipelines.
|
|
if (EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState) == false)
|
|
{
|
|
bHasMatchingPipelines = HasMatchingPipelines(State.Previous.Pipelines, CurrentStateFromRHI.Pipelines);
|
|
}
|
|
|
|
if (CurrentStateFromRHI.Access == ERHIAccess::Unknown)
|
|
{
|
|
RHI_VALIDATION_CHECK(Resource->TrackedState.Access == State.Previous.Access && bHasMatchingPipelines,
|
|
*GetReasonString_IncorrectPreviousTrackedState(Resource, SubresourceIndex, State.Previous, CurrentStateFromRHI.Pipelines));
|
|
}
|
|
else
|
|
{
|
|
// Check the current RHI state passed in matches the tracked state for the resource.
|
|
RHI_VALIDATION_CHECK(CurrentStateFromRHI.Access == State.Previous.Access && bHasMatchingPipelines,
|
|
*GetReasonString_IncorrectPreviousExplicitState(Resource, SubresourceIndex, State.Previous, CurrentStateFromRHI));
|
|
}
|
|
}
|
|
|
|
const bool bIgnoreAfterStateAllPipes = ((EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState)) && (CurrentStateFromRHI.Pipelines == ERHIPipeline::All));
|
|
RHI_VALIDATION_CHECK(bIgnoreAfterStateAllPipes == false,
|
|
*GetReasonString_IgnoreAfterStateAllPipes(Resource, SubresourceIndex, State.Current, TargetState));
|
|
|
|
const bool bRegularTransitionWhileIgnoring = (State.bIgnoringAfterState && (EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState) == false));
|
|
|
|
RHI_VALIDATION_CHECK(bRegularTransitionWhileIgnoring == false,
|
|
*GetReasonString_MismatchedIgnoreAfterState(Resource, SubresourceIndex, State.Current, TargetState));
|
|
|
|
State.bTransitioning = true;
|
|
State.Flags = NewFlags;
|
|
State.BeginTransitionBacktrace = BeginTrace;
|
|
// Update the tracked state once all pipes have begun.
|
|
State.Previous = TargetState;
|
|
State.Current = TargetState;
|
|
State.CreateTransitionBacktrace = CreateTrace;
|
|
State.bUsedWithAllUAVsOverlap = false;
|
|
State.bUsedWithExplicitUAVsOverlap = false;
|
|
|
|
// Do not replicate the state for EResourceTransitionFlags::IgnoreAfterState transition
|
|
if (EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState) == false)
|
|
{
|
|
// Replicate the state to other pipes that are not part of the begin pipe mask.
|
|
for (ERHIPipeline OtherPipeline : MakeFlagsRange(ERHIPipeline::All & ~CurrentStateFromRHI.Pipelines))
|
|
{
|
|
States[OtherPipeline] = State;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSubresourceState::EndTransition(FResource* Resource, FSubresourceIndex const& SubresourceIndex, const FState& CurrentStateFromRHI, const FState& InTargetState, EResourceTransitionFlags NewFlags, ERHIPipeline ExecutingPipeline, uint64 ExecutingPipelineFenceValue, void* CreateTrace)
|
|
{
|
|
FPipelineState& State = States[ExecutingPipeline];
|
|
|
|
FState TargetState = InTargetState;
|
|
|
|
if (TargetState.Access == ERHIAccess::Unknown)
|
|
{
|
|
TargetState.Access = Resource->GetTrackedState().Access;
|
|
}
|
|
|
|
if (Resource->LoggingMode != ELoggingMode::None
|
|
#if LOG_UNNAMED_RESOURCES
|
|
|| Resource->GetDebugName() == nullptr
|
|
#endif
|
|
)
|
|
{
|
|
const TCHAR* PulledFromTracked = TEXT("");
|
|
|
|
if (InTargetState.Access == ERHIAccess::Unknown)
|
|
{
|
|
PulledFromTracked = TEXT(" (Pulled From SetTrackedAccess)");
|
|
}
|
|
|
|
Log(Resource, SubresourceIndex, CreateTrace, TEXT("End"), TEXT("EndTransition"), *FString::Printf(TEXT("Access: %s%s, Pipeline: %s, Executing Pipeline: %s"),
|
|
*GetRHIAccessName(TargetState.Access),
|
|
PulledFromTracked,
|
|
*GetRHIPipelineName(TargetState.Pipelines),
|
|
*GetRHIPipelineName(ExecutingPipeline)
|
|
));
|
|
}
|
|
|
|
// Set bIgnoringAfterState to true in case of EResourceTransitionFlags::IgnoreAfterState so on the next transition we can check that EResourceTransitionFlags::IgnoreAfterState is used as well
|
|
if (EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState))
|
|
{
|
|
State.bIgnoringAfterState = !State.bIgnoringAfterState;
|
|
}
|
|
|
|
// Check that we aren't ending a transition that never began.
|
|
RHI_VALIDATION_CHECK(State.bTransitioning, TEXT("Unsolicited resource end transition call."));
|
|
State.bTransitioning = false;
|
|
State.BeginTransitionBacktrace = nullptr;
|
|
|
|
// Check that the end matches the begin.
|
|
RHI_VALIDATION_CHECK(TargetState == State.Current, *GetReasonString_MismatchedEndTransition(Resource, SubresourceIndex, State.Current, TargetState));
|
|
|
|
// Do not replicate the state for EResourceTransitionFlags::IgnoreAfterState transitions
|
|
if (EnumHasAnyFlags(NewFlags, EResourceTransitionFlags::IgnoreAfterState) == false)
|
|
{
|
|
// Replicate the state to other pipes that are not part of the end pipe mask.
|
|
for (ERHIPipeline OtherPipeline : MakeFlagsRange(ERHIPipeline::All))
|
|
{
|
|
if (!EnumHasAnyFlags(TargetState.Pipelines, OtherPipeline))
|
|
{
|
|
States[OtherPipeline] = State;
|
|
}
|
|
}
|
|
}
|
|
|
|
LastTransitionFences[ExecutingPipeline] = ExecutingPipelineFenceValue;
|
|
}
|
|
|
|
void FSubresourceState::Assert(FResource* Resource, FSubresourceIndex const& SubresourceIndex, const FState& RequiredState, bool bAllowAllUAVsOverlap)
|
|
{
|
|
if (Resource->LoggingMode != ELoggingMode::None
|
|
#if LOG_UNNAMED_RESOURCES
|
|
|| Resource->GetDebugName() == nullptr
|
|
#endif
|
|
)
|
|
{
|
|
Log(Resource, SubresourceIndex, nullptr, nullptr, TEXT("Assert"), *FString::Printf(TEXT("Access: %s, Pipeline: %s"),
|
|
*GetRHIAccessName(RequiredState.Access),
|
|
*GetRHIPipelineName(RequiredState.Pipelines)));
|
|
}
|
|
|
|
FPipelineState& State = States[RequiredState.Pipelines];
|
|
|
|
// Check we're not trying to access the resource whilst a pending resource transition is in progress.
|
|
RHI_VALIDATION_CHECK(!State.bTransitioning, *GetReasonString_AccessDuringTransition(Resource, SubresourceIndex, State.Current, RequiredState, State.CreateTransitionBacktrace, State.BeginTransitionBacktrace));
|
|
|
|
// If UAV overlaps are now disabled, ensure the resource has been transitioned if it was previously used in UAV overlap state.
|
|
RHI_VALIDATION_CHECK((bAllowAllUAVsOverlap || !State.bUsedWithAllUAVsOverlap) && (State.bExplicitAllowUAVOverlap || !State.bUsedWithExplicitUAVsOverlap), *GetReasonString_UAVOverlap(Resource, SubresourceIndex, State.Current, RequiredState));
|
|
|
|
// Ensure the resource is in the required state for this operation
|
|
RHI_VALIDATION_CHECK(EnumHasAllFlags(State.Current.Access, RequiredState.Access) && EnumHasAllFlags(State.Current.Pipelines, RequiredState.Pipelines), *GetReasonString_MissingBarrier(Resource, SubresourceIndex, State.Current, RequiredState));
|
|
|
|
State.Previous = State.Current;
|
|
|
|
if (EnumHasAnyFlags(RequiredState.Access, ERHIAccess::UAVMask | ERHIAccess::BVHWrite))
|
|
{
|
|
if (bAllowAllUAVsOverlap) { State.bUsedWithAllUAVsOverlap = true; }
|
|
if (State.bExplicitAllowUAVOverlap) { State.bUsedWithExplicitUAVsOverlap = true; }
|
|
}
|
|
|
|
// Disable all non-compatible access types
|
|
State.Current.Access = DecayResourceAccess(State.Current.Access, RequiredState.Access, bAllowAllUAVsOverlap || State.bExplicitAllowUAVOverlap);
|
|
}
|
|
|
|
void FSubresourceState::AssertTracked(FResource* Resource, FSubresourceIndex const& SubresourceIndex, const FState& RequiredState, ERHIPipeline ExecutingPipeline)
|
|
{
|
|
if (Resource->LoggingMode != ELoggingMode::None
|
|
#if LOG_UNNAMED_RESOURCES
|
|
|| Resource->GetDebugName() == nullptr
|
|
#endif
|
|
)
|
|
{
|
|
Log(Resource, SubresourceIndex, nullptr, nullptr, TEXT("AssertTracked"), *FString::Printf(TEXT("Access: %s, Pipelines %s"), *GetRHIAccessName(RequiredState.Access), *GetRHIPipelineName(RequiredState.Pipelines)));
|
|
}
|
|
|
|
for (ERHIPipeline Pipeline : MakeFlagsRange(RequiredState.Pipelines))
|
|
{
|
|
FPipelineState& State = States[Pipeline];
|
|
|
|
// Check we're not trying to access the resource whilst a pending resource transition is in progress (can only do this on the executing pipeline).
|
|
if (State.Current.Pipelines == ExecutingPipeline)
|
|
{
|
|
RHI_VALIDATION_CHECK(!State.bTransitioning, *GetReasonString_AccessDuringTransition(Resource, SubresourceIndex, State.Current, RequiredState, State.CreateTransitionBacktrace, State.BeginTransitionBacktrace));
|
|
}
|
|
|
|
// Ensure the resource is in the required state for this operation (ignore the Discard state which always resets).
|
|
RHI_VALIDATION_CHECK(State.Current.Access == ERHIAccess::Discard || State.Current == RequiredState, *GetReasonString_IncorrectSetTrackedAccess(Resource, SubresourceIndex, State.Current, RequiredState));
|
|
}
|
|
}
|
|
|
|
void FSubresourceState::SpecificUAVOverlap(FResource* Resource, FSubresourceIndex const& SubresourceIndex, ERHIPipeline Pipeline, bool bAllow)
|
|
{
|
|
if (Resource->LoggingMode != ELoggingMode::None
|
|
#if LOG_UNNAMED_RESOURCES
|
|
|| Resource->GetDebugName() == nullptr
|
|
#endif
|
|
)
|
|
{
|
|
Log(Resource, SubresourceIndex, nullptr, nullptr, TEXT("UAVOverlap"), *FString::Printf(TEXT("Allow: %s"), bAllow ? TEXT("True") : TEXT("False")));
|
|
}
|
|
|
|
FPipelineState& State = States[Pipeline];
|
|
RHI_VALIDATION_CHECK(State.bExplicitAllowUAVOverlap != bAllow, *GetReasonString_MismatchedExplicitUAVOverlapCall(Resource, SubresourceIndex, bAllow));
|
|
State.bExplicitAllowUAVOverlap = bAllow;
|
|
}
|
|
|
|
inline void FResource::EnumerateSubresources(FSubresourceRange const& SubresourceRange, TFunctionRef<void(FSubresourceState&, FSubresourceIndex const&)> Callback, bool bBeginTransition)
|
|
{
|
|
bool bWholeResource = SubresourceRange.IsWholeResource(*this);
|
|
if (bWholeResource && SubresourceStates.Num() == 0)
|
|
{
|
|
Callback(WholeResourceState, FSubresourceIndex());
|
|
}
|
|
else
|
|
{
|
|
if (SubresourceStates.Num() == 0)
|
|
{
|
|
const uint32 NumSubresources = NumMips * NumArraySlices * NumPlanes;
|
|
SubresourceStates.Reserve(NumSubresources);
|
|
|
|
// Copy the whole resource state into all the subresource slots
|
|
for (uint32 Index = 0; Index < NumSubresources; ++Index)
|
|
{
|
|
SubresourceStates.Add(WholeResourceState);
|
|
}
|
|
}
|
|
|
|
if (SubresourceStates.Num() != 0)
|
|
{
|
|
uint32 LastMip = SubresourceRange.MipIndex + SubresourceRange.NumMips;
|
|
uint32 LastArraySlice = SubresourceRange.ArraySlice + SubresourceRange.NumArraySlices;
|
|
uint32 LastPlaneIndex = SubresourceRange.PlaneIndex + SubresourceRange.NumPlanes;
|
|
|
|
for (uint32 PlaneIndex = SubresourceRange.PlaneIndex; PlaneIndex < LastPlaneIndex; ++PlaneIndex)
|
|
{
|
|
for (uint32 MipIndex = SubresourceRange.MipIndex; MipIndex < LastMip; ++MipIndex)
|
|
{
|
|
for (uint32 ArraySlice = SubresourceRange.ArraySlice; ArraySlice < LastArraySlice; ++ArraySlice)
|
|
{
|
|
uint32 SubresourceIndex = PlaneIndex + (MipIndex + ArraySlice * NumMips) * NumPlanes;
|
|
Callback(SubresourceStates[SubresourceIndex], FSubresourceIndex(MipIndex, ArraySlice, PlaneIndex));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bWholeResource && bBeginTransition && SubresourceStates.Num() != 0)
|
|
{
|
|
// Switch back to whole resource state tracking on begin transitions
|
|
WholeResourceState = SubresourceStates[0];
|
|
SubresourceStates.Reset();
|
|
}
|
|
}
|
|
|
|
#if WITH_RHI_BREADCRUMBS
|
|
bool IsInRange(FRHIBreadcrumbRange const& Range, FRHIBreadcrumbNode* const Target, ERHIPipeline Pipeline)
|
|
{
|
|
for (FRHIBreadcrumbNode* Current : Range.Enumerate(Pipeline))
|
|
{
|
|
if (Current == Target)
|
|
return true;
|
|
}
|
|
|
|
// Include all parent nodes above Last
|
|
for (FRHIBreadcrumbNode* Current = Range.Last; Current; Current = Current->GetParent())
|
|
{
|
|
if (Current == Target)
|
|
return true;
|
|
}
|
|
|
|
// Include all parent nodes above First
|
|
for (FRHIBreadcrumbNode* Current = Range.First; Current; Current = Current->GetParent())
|
|
{
|
|
if (Current == Target)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int32 CountLevels(FRHIBreadcrumbNode* Node)
|
|
{
|
|
auto Recurse = [](auto const& Recurse, FRHIBreadcrumbNode* Current) -> int32
|
|
{
|
|
check(Current != FRHIBreadcrumbNode::Sentinel);
|
|
return Current
|
|
? Recurse(Recurse, Current->GetParent()) + 1
|
|
: 0;
|
|
};
|
|
return Recurse(Recurse, Node) - 1;
|
|
}
|
|
|
|
void LogNode(FRHIBreadcrumbNode* Node, bool bBegin, ERHIPipeline Pipeline)
|
|
{
|
|
static bool bOutputBreadcrumbLog = FParse::Param(FCommandLine::Get(), TEXT("RHIValidationBreadcrumbLog"));
|
|
if (bOutputBreadcrumbLog)
|
|
{
|
|
int32 Levels = CountLevels(Node);
|
|
FString Output = TEXT("");
|
|
for (int32 Index = 0; Index < Levels; ++Index)
|
|
{
|
|
Output += TEXT("\t");
|
|
}
|
|
FRHIBreadcrumb::FBuffer Buffer;
|
|
const TCHAR* Str = Node->GetTCHAR(Buffer);
|
|
Output += Str;
|
|
UE_LOG(LogRHI, Display, TEXT(" ## BC (0x%016p, 0x%08x) [%12s] [%s]: %s")
|
|
, Node
|
|
, Node->ID
|
|
, *GetRHIPipelineName(Pipeline)
|
|
, bBegin ? TEXT("BEGIN") : TEXT(" END ")
|
|
, *Output
|
|
);
|
|
}
|
|
}
|
|
#endif // WITH_RHI_BREADCRUMBS
|
|
|
|
bool FOperation::Replay(FOpQueueState& Queue) const
|
|
{
|
|
switch (Type)
|
|
{
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
|
|
#if WITH_RHI_BREADCRUMBS
|
|
case EOpType::BeginBreadcrumbGPU:
|
|
{
|
|
FRHIBreadcrumbNode* Node = Data_Breadcrumb.Breadcrumb;
|
|
|
|
check(Node && Node != FRHIBreadcrumbNode::Sentinel);
|
|
check(Node->GetParent() != FRHIBreadcrumbNode::Sentinel);
|
|
check(Node->GetParent() == Queue.Breadcrumbs.Current);
|
|
check(GRHICommandList.Bypass() || IsInRange(Queue.Breadcrumbs.Range, Node, Queue.Pipeline));
|
|
check(EnumHasAllFlags(static_cast<ERHIPipeline>(Node->BeginPipes.load()), Queue.Pipeline));
|
|
|
|
LogNode(Node, true, Queue.Pipeline);
|
|
|
|
Queue.Breadcrumbs.Current = Node;
|
|
}
|
|
break;
|
|
|
|
case EOpType::EndBreadcrumbGPU:
|
|
{
|
|
FRHIBreadcrumbNode* Node = Data_Breadcrumb.Breadcrumb;
|
|
|
|
check(Node && Node != FRHIBreadcrumbNode::Sentinel);
|
|
check(Node->GetParent() != FRHIBreadcrumbNode::Sentinel);
|
|
check(Node == Queue.Breadcrumbs.Current);
|
|
check(GRHICommandList.Bypass() || IsInRange(Queue.Breadcrumbs.Range, Node, Queue.Pipeline));
|
|
check(EnumHasAllFlags(static_cast<ERHIPipeline>(Node->EndPipes.load()), Queue.Pipeline));
|
|
|
|
LogNode(Node, false, Queue.Pipeline);
|
|
|
|
Queue.Breadcrumbs.Current = Node->GetParent();
|
|
}
|
|
break;
|
|
|
|
case EOpType::SetBreadcrumbRange:
|
|
{
|
|
Queue.Breadcrumbs.Range = Data_BreadcrumbRange.Range;
|
|
check(!Queue.Breadcrumbs.Range.First == !Queue.Breadcrumbs.Range.Last);
|
|
|
|
TSet<FRHIBreadcrumbAllocator*> AllAllocators;
|
|
for (FRHIBreadcrumbNode* Node : Queue.Breadcrumbs.Range.Enumerate(Queue.Pipeline))
|
|
{
|
|
AllAllocators.Add(Node->Allocator);
|
|
|
|
// Check current node and all parents are valid
|
|
for (FRHIBreadcrumbNode* Other = Node; Other; Other = Other->GetParent())
|
|
{
|
|
check(Other != FRHIBreadcrumbNode::Sentinel);
|
|
check(Other->GetParent() != FRHIBreadcrumbNode::Sentinel);
|
|
}
|
|
}
|
|
|
|
// Check for circular references in the allocator parent pointers
|
|
for (FRHIBreadcrumbAllocator* Allocator : AllAllocators)
|
|
{
|
|
auto Recurse = [](FRHIBreadcrumbAllocator* Current, auto& Recurse) -> void
|
|
{
|
|
checkf(!Current->bVisited, TEXT("Circular reference detected in breadcrumb allocators."));
|
|
Current->bVisited = true;
|
|
|
|
for (auto const& Parent : Current->GetParents())
|
|
{
|
|
Recurse(&Parent.Get(), Recurse);
|
|
}
|
|
|
|
Current->bVisited = false;
|
|
};
|
|
Recurse(Allocator, Recurse);
|
|
}
|
|
}
|
|
break;
|
|
#endif // WITH_RHI_BREADCRUMBS
|
|
|
|
case EOpType::Rename:
|
|
Data_Rename.Resource->SetDebugName(Data_Rename.DebugName, Data_Rename.Suffix);
|
|
delete[] Data_Rename.DebugName;
|
|
Data_Rename.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::BeginTransition:
|
|
Data_BeginTransition.Identity.Resource->EnumerateSubresources(Data_BeginTransition.Identity.SubresourceRange, [this, &Queue](FSubresourceState& State, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
State.BeginTransition(
|
|
Data_BeginTransition.Identity.Resource,
|
|
SubresourceIndex,
|
|
Data_BeginTransition.PreviousState,
|
|
Data_BeginTransition.NextState,
|
|
Data_BeginTransition.Flags,
|
|
Data_BeginTransition.CreateFlags,
|
|
Queue.Pipeline,
|
|
Queue.MaxAwaitedFenceValues,
|
|
Data_BeginTransition.CreateBacktrace);
|
|
|
|
}, true);
|
|
Data_BeginTransition.Identity.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::EndTransition:
|
|
Data_EndTransition.Identity.Resource->EnumerateSubresources(Data_EndTransition.Identity.SubresourceRange, [this, &Queue](FSubresourceState& State, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
State.EndTransition(
|
|
Data_EndTransition.Identity.Resource,
|
|
SubresourceIndex,
|
|
Data_EndTransition.PreviousState,
|
|
Data_EndTransition.NextState,
|
|
Data_EndTransition.Flags,
|
|
Queue.Pipeline,
|
|
Queue.FenceValue,
|
|
Data_EndTransition.CreateBacktrace);
|
|
});
|
|
Data_EndTransition.Identity.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::AliasingOverlap:
|
|
FTransientState::AliasingOverlap(Data_AliasingOverlap.ResourceBefore, Data_AliasingOverlap.ResourceAfter, Data_AliasingOverlap.CreateBacktrace);
|
|
Data_AliasingOverlap.ResourceBefore->ReleaseOpRef();
|
|
Data_AliasingOverlap.ResourceAfter->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::SetTrackedAccess:
|
|
{
|
|
Data_Assert.Identity.Resource->EnumerateSubresources(Data_SetTrackedAccess.Resource->GetWholeResourceRange(), [this, &Queue](FSubresourceState& State, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
State.AssertTracked(
|
|
Data_SetTrackedAccess.Resource,
|
|
SubresourceIndex,
|
|
Data_SetTrackedAccess.State,
|
|
Queue.Pipeline);
|
|
});
|
|
FResource* Resource = Data_SetTrackedAccess.Resource;
|
|
if (Resource->LoggingMode != ELoggingMode::None)
|
|
{
|
|
Log(Resource, {}, nullptr, TEXT("SetTrackedAccess"), TEXT("SetTrackedAccess"), *FString::Printf(TEXT("Access: (%s), Pipelines: (%s), Executing Pipeline: (%s)"), *GetRHIAccessName(Data_SetTrackedAccess.State.Access), *GetRHIPipelineName(Data_SetTrackedAccess.State.Pipelines), *GetRHIPipelineName(Queue.Pipeline)) );
|
|
}
|
|
Resource->TrackedState = Data_SetTrackedAccess.State;
|
|
Resource->ReleaseOpRef();
|
|
break;
|
|
}
|
|
case EOpType::AcquireTransient:
|
|
Data_AcquireTransient.Resource->TransientState.Acquire(Data_AcquireTransient.Resource, Data_AcquireTransient.CreateBacktrace, Queue.Pipeline);
|
|
Data_AcquireTransient.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::InitTransient:
|
|
Data_InitTransient.Resource->InitTransient(Data_InitTransient.DebugName);
|
|
delete[] Data_InitTransient.DebugName;
|
|
Data_InitTransient.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::Assert:
|
|
Data_Assert.Identity.Resource->EnumerateSubresources(Data_Assert.Identity.SubresourceRange, [this, &Queue](FSubresourceState& State, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
State.Assert(
|
|
Data_Assert.Identity.Resource,
|
|
SubresourceIndex,
|
|
Data_Assert.RequiredState,
|
|
Queue.bAllowAllUAVsOverlap);
|
|
});
|
|
Data_Assert.Identity.Resource->ReleaseOpRef();
|
|
break;
|
|
|
|
case EOpType::Signal:
|
|
check(Data_Signal.Fence->SrcPipe == Queue.Pipeline);
|
|
Data_Signal.Fence->bSignaled = true;
|
|
Data_Signal.Fence->FenceValue = ++Queue.FenceValue;
|
|
break;
|
|
|
|
case EOpType::Wait:
|
|
{
|
|
FFence* Fence = Data_Wait.Fence;
|
|
check(Fence->DstPipe == Queue.Pipeline);
|
|
if (!Fence->bSignaled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Queue.MaxAwaitedFenceValues[Fence->SrcPipe] = FMath::Max(Fence->FenceValue, Queue.MaxAwaitedFenceValues[Fence->SrcPipe]);
|
|
|
|
// The fence has been completed. Free it now.
|
|
delete Fence;
|
|
}
|
|
break;
|
|
|
|
case EOpType::AllUAVsOverlap:
|
|
RHI_VALIDATION_CHECK(Queue.bAllowAllUAVsOverlap != Data_AllUAVsOverlap.bAllow, *GetReasonString_MismatchedAllUAVsOverlapCall(Data_AllUAVsOverlap.bAllow));
|
|
Queue.bAllowAllUAVsOverlap = Data_AllUAVsOverlap.bAllow;
|
|
break;
|
|
|
|
case EOpType::SpecificUAVOverlap:
|
|
Data_SpecificUAVOverlap.Identity.Resource->EnumerateSubresources(Data_SpecificUAVOverlap.Identity.SubresourceRange, [this, &Queue](FSubresourceState& State, FSubresourceIndex const& SubresourceIndex)
|
|
{
|
|
State.SpecificUAVOverlap(
|
|
Data_SpecificUAVOverlap.Identity.Resource,
|
|
SubresourceIndex,
|
|
Queue.Pipeline,
|
|
Data_SpecificUAVOverlap.bAllow);
|
|
});
|
|
Data_SpecificUAVOverlap.Identity.Resource->ReleaseOpRef();
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FTracker::AddOp(const RHIValidation::FOperation& Op)
|
|
{
|
|
if (GRHICommandList.Bypass() && CurrentList.IsEmpty())
|
|
{
|
|
if (Op.Replay(GetQueue(Pipeline)))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
CurrentList.Add(Op);
|
|
}
|
|
|
|
void FOpQueueState::AppendOps(FValidationCommandList* CommandList)
|
|
{
|
|
Ops.Emplace(MoveTemp(CommandList->CompletedOpList));
|
|
}
|
|
|
|
bool FOpQueueState::Execute()
|
|
{
|
|
if (!Ops.Num())
|
|
return false;
|
|
|
|
bool bProgressMade = false;
|
|
FRHIValidationQueueScope Scope(*this);
|
|
|
|
while (Ops.Num())
|
|
{
|
|
for (FOpsList& List = Ops[0]; List.ReplayPos < List.Num(); ++List.ReplayPos)
|
|
{
|
|
if (!List[List.ReplayPos].Replay(*this))
|
|
{
|
|
// Queue is blocked
|
|
return bProgressMade;
|
|
}
|
|
|
|
bProgressMade = true;
|
|
}
|
|
|
|
Ops.RemoveAt(0);
|
|
}
|
|
|
|
return bProgressMade;
|
|
}
|
|
|
|
void FTracker::SubmitValidationOps(ERHIPipeline Pipeline, TArray<RHIValidation::FOperation>&& Ops)
|
|
{
|
|
GetQueue(Pipeline).Ops.Emplace(MoveTemp(Ops));
|
|
|
|
// Keep executing until no more progress is made,
|
|
// (i.e. until queues are empty or blocked on fences).
|
|
bool bProgressMade;
|
|
do
|
|
{
|
|
bProgressMade = false;
|
|
for (FOpQueueState& CurrentQueue : OpQueues)
|
|
{
|
|
bProgressMade |= CurrentQueue.Execute();
|
|
}
|
|
|
|
} while (bProgressMade);
|
|
}
|
|
|
|
|
|
void FUniformBufferResource::InitLifetimeTracking(uint64 FrameID, const void* Contents, EUniformBufferUsage Usage)
|
|
{
|
|
AllocatedFrameID = FrameID;
|
|
UniformBufferUsage = Usage;
|
|
bContainsNullContents = Contents == nullptr;
|
|
|
|
#if CAPTURE_UNIFORMBUFFER_ALLOCATION_BACKTRACES
|
|
AllocatedCallstack = (UniformBufferUsage != UniformBuffer_MultiFrame) ? RHIValidation::CaptureBacktrace() : nullptr;
|
|
#else
|
|
AllocatedCallstack = nullptr;
|
|
#endif
|
|
}
|
|
|
|
void FUniformBufferResource::UpdateAllocation(uint64 FrameID)
|
|
{
|
|
AllocatedFrameID = FrameID;
|
|
bContainsNullContents = false;
|
|
|
|
#if CAPTURE_UNIFORMBUFFER_ALLOCATION_BACKTRACES
|
|
AllocatedCallstack = (UniformBufferUsage != UniformBuffer_MultiFrame) ? RHIValidation::CaptureBacktrace() : nullptr;
|
|
#else
|
|
AllocatedCallstack = nullptr;
|
|
#endif
|
|
}
|
|
|
|
void FUniformBufferResource::ValidateLifeTime()
|
|
{
|
|
FValidationRHI* ValidateRHI = (FValidationRHI*)GDynamicRHI;
|
|
|
|
RHI_VALIDATION_CHECK(bContainsNullContents == false, TEXT("Uniform buffer created with null contents is now being bound for rendering on an RHI context. The contents must first be updated."));
|
|
|
|
if (UniformBufferUsage != UniformBuffer_MultiFrame && AllocatedFrameID < ValidateRHI->RHIThreadFrameID)
|
|
{
|
|
FString ErrorMessage = TEXT("Non MultiFrame Uniform buffer has been allocated in a previous frame. The data could have been deleted already!");
|
|
if (AllocatedCallstack != nullptr)
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("\nAllocation callstack: (void**)0x%p,32"), AllocatedCallstack);
|
|
}
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
|
|
FOpQueueState FTracker::OpQueues[int32(ERHIPipeline::Num)]
|
|
{
|
|
ERHIPipeline::Graphics,
|
|
ERHIPipeline::AsyncCompute
|
|
};
|
|
|
|
FOpQueueState& FTracker::GetQueue(ERHIPipeline Pipeline)
|
|
{
|
|
uint32 Index;
|
|
switch (Pipeline)
|
|
{
|
|
default: checkNoEntry(); [[fallthrough]];
|
|
case ERHIPipeline::Graphics:
|
|
Index = 0;
|
|
break;
|
|
|
|
case ERHIPipeline::AsyncCompute:
|
|
Index = 1;
|
|
break;
|
|
}
|
|
|
|
return OpQueues[Index];
|
|
}
|
|
|
|
void* CaptureBacktrace()
|
|
{
|
|
// Back traces will leak. Don't leave this turned on.
|
|
uint64* Backtrace = new uint64[NumStackFrames];
|
|
FPlatformStackWalk::CaptureStackBackTrace(Backtrace, NumStackFrames);
|
|
|
|
return Backtrace;
|
|
}
|
|
|
|
bool ValidateDimension(EShaderCodeResourceBindingType Type, FRHIViewDesc::EDimension Dimension, ERHITexturePlane TexturePlane, bool SRV)
|
|
{
|
|
// Ignore invalid types
|
|
if (Type == EShaderCodeResourceBindingType::Invalid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (IsResourceBindingTypeSRV(Type) != SRV)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::RWStructuredBuffer || Type == EShaderCodeResourceBindingType::StructuredBuffer)
|
|
{
|
|
return TexturePlane == ERHITexturePlane::HTile;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::RWByteAddressBuffer || Type == EShaderCodeResourceBindingType::ByteAddressBuffer)
|
|
{
|
|
return TexturePlane == ERHITexturePlane::CMask;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::RWBuffer || Type == EShaderCodeResourceBindingType::Buffer)
|
|
{
|
|
return TexturePlane == ERHITexturePlane::PrimaryCompressed || TexturePlane == ERHITexturePlane::CMask;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture2D || Type == EShaderCodeResourceBindingType::RWTexture2D || Type == EShaderCodeResourceBindingType::Texture2DMS)
|
|
{
|
|
return Dimension == FRHIViewDesc::EDimension::Texture2D;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture2DArray || Type == EShaderCodeResourceBindingType::RWTexture2DArray)
|
|
{
|
|
return Dimension == FRHIViewDesc::EDimension::Texture2DArray || Dimension == FRHIViewDesc::EDimension::TextureCube;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture3D || Type == EShaderCodeResourceBindingType::RWTexture3D)
|
|
{
|
|
return Dimension == FRHIViewDesc::EDimension::Texture3D;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::TextureCube || Type == EShaderCodeResourceBindingType::RWTextureCube)
|
|
{
|
|
return Dimension == FRHIViewDesc::EDimension::TextureCube;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::TextureCubeArray)
|
|
{
|
|
return Dimension == FRHIViewDesc::EDimension::TextureCubeArray;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ValidateDimension(EShaderCodeResourceBindingType Type, ETextureDimension Dimension, bool SRV)
|
|
{
|
|
// Ignore invalid types
|
|
if (Type == EShaderCodeResourceBindingType::Invalid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture2D || Type == EShaderCodeResourceBindingType::RWTexture2D || Type == EShaderCodeResourceBindingType::Texture2DMS)
|
|
{
|
|
return Dimension == ETextureDimension::Texture2D;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture2DArray || Type == EShaderCodeResourceBindingType::RWTexture2DArray)
|
|
{
|
|
return Dimension == ETextureDimension::Texture2DArray || Dimension == ETextureDimension::TextureCube;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::Texture3D || Type == EShaderCodeResourceBindingType::RWTexture3D)
|
|
{
|
|
return Dimension == ETextureDimension::Texture3D;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::TextureCube || Type == EShaderCodeResourceBindingType::RWTextureCube)
|
|
{
|
|
return Dimension == ETextureDimension::TextureCube;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::TextureCubeArray)
|
|
{
|
|
return Dimension == ETextureDimension::TextureCubeArray;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ValidateBuffer(EShaderCodeResourceBindingType Type, FRHIViewDesc::EBufferType BufferType, bool SRV)
|
|
{
|
|
// Ignore invalid types
|
|
if (Type == EShaderCodeResourceBindingType::Invalid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (IsResourceBindingTypeSRV(Type) != SRV)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Type == EShaderCodeResourceBindingType::ByteAddressBuffer || Type == EShaderCodeResourceBindingType::RWByteAddressBuffer)
|
|
{
|
|
return BufferType == FRHIViewDesc::EBufferType::Raw;
|
|
}
|
|
else if (Type == EShaderCodeResourceBindingType::StructuredBuffer || Type == EShaderCodeResourceBindingType::RWStructuredBuffer)
|
|
{
|
|
return BufferType == FRHIViewDesc::EBufferType::Structured || BufferType == FRHIViewDesc::EBufferType::AccelerationStructure;
|
|
}
|
|
else if (Type == EShaderCodeResourceBindingType::Buffer || Type == EShaderCodeResourceBindingType::RWBuffer)
|
|
{
|
|
return BufferType == FRHIViewDesc::EBufferType::Typed;
|
|
}
|
|
else if (Type == EShaderCodeResourceBindingType::RaytracingAccelerationStructure)
|
|
{
|
|
return BufferType == FRHIViewDesc::EBufferType::AccelerationStructure;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Validates that the SRV is conform to what the shader expects */
|
|
void ValidateShaderResourceView(const FRHIShader* RHIShaderBase, uint32 BindIndex, const FRHIShaderResourceView* SRV)
|
|
{
|
|
#if RHI_INCLUDE_SHADER_DEBUG_DATA
|
|
if (SRV)
|
|
{
|
|
auto const ViewIdentity = SRV->GetViewIdentity();
|
|
|
|
static const auto GetSRVName = [](const FRHIShaderResourceView* SRV, auto& ViewIdentity) -> FString
|
|
{
|
|
FString SRVName;
|
|
if (ViewIdentity.Resource)
|
|
{
|
|
SRVName = ViewIdentity.Resource->GetDebugName();
|
|
}
|
|
if (SRVName.IsEmpty())
|
|
{
|
|
SRVName = SRV->GetOwnerName().ToString();
|
|
}
|
|
|
|
return SRVName;
|
|
};
|
|
|
|
// DebugStrideValidationData is supposed to be already sorted
|
|
static const auto ShaderCodeValidationStridePredicate = [](const FShaderCodeValidationStride& lhs, const FShaderCodeValidationStride& rhs) -> bool { return lhs.BindPoint < rhs.BindPoint; };
|
|
FShaderCodeValidationStride SRVValidationStride = { BindIndex , ViewIdentity.Stride };
|
|
|
|
int32 FoundIndex = Algo::BinarySearch(RHIShaderBase->DebugStrideValidationData, SRVValidationStride, ShaderCodeValidationStridePredicate);
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
FString SRVName = GetSRVName(SRV, ViewIdentity);
|
|
uint16 ExpectedStride = RHIShaderBase->DebugStrideValidationData[FoundIndex].Stride;
|
|
if (ExpectedStride != SRVValidationStride.Stride && SRV->GetDesc().Buffer.SRV.BufferType != FRHIViewDesc::EBufferType::AccelerationStructure)
|
|
{
|
|
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Buffer stride for \"%s\" must match structure size declared in the shader"), RHIShaderBase->GetShaderName(), *SRVName);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL size: %d, Buffer Size: %d"), BindIndex, ExpectedStride, SRVValidationStride.Stride);
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
|
|
// Validate Type
|
|
if (!RHIShaderBase->DebugSRVTypeValidationData.Num())
|
|
return;
|
|
|
|
static const auto ShaderCodeValidationTypePredicate = [](const FShaderCodeValidationType& lhs, const FShaderCodeValidationType& rhs) -> bool { return lhs.BindPoint < rhs.BindPoint; };
|
|
|
|
FShaderCodeValidationType SRVValidationType = { BindIndex , EShaderCodeResourceBindingType::Invalid };
|
|
FoundIndex = Algo::BinarySearch(RHIShaderBase->DebugSRVTypeValidationData, SRVValidationType, ShaderCodeValidationTypePredicate);
|
|
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
EShaderCodeResourceBindingType ExpectedType = RHIShaderBase->DebugSRVTypeValidationData[FoundIndex].Type;
|
|
|
|
if (SRV->IsTexture())
|
|
{
|
|
if (!ValidateDimension(ExpectedType, SRV->GetDesc().Texture.SRV.Dimension, SRV->GetDesc().Texture.SRV.Plane, true))
|
|
{
|
|
FString SRVName = GetSRVName(SRV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Dimension for SRV \"%s\" must match type declared in the shader"), RHIShaderBase->GetShaderName(), *SRVName);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL Type: %s, Actual Dimension: %s"),
|
|
BindIndex,
|
|
GetShaderCodeResourceBindingTypeName(ExpectedType),
|
|
FRHIViewDesc::GetTextureDimensionString(SRV->GetDesc().Texture.SRV.Dimension));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
else if (SRV->IsBuffer())
|
|
{
|
|
if (!ValidateBuffer(ExpectedType, SRV->GetDesc().Buffer.SRV.BufferType, true))
|
|
{
|
|
FString SRVName = GetSRVName(SRV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Buffer type for SRV \"%s\" must match buffer type declared in the shader"), RHIShaderBase->GetShaderName(), *SRVName);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL Type: %s, Actual Type: %s"),
|
|
BindIndex,
|
|
GetShaderCodeResourceBindingTypeName(ExpectedType),
|
|
FRHIViewDesc::GetBufferTypeString(SRV->GetDesc().Buffer.SRV.BufferType));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString SRVName = GetSRVName(SRV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: No bind point found for SRV \"%s\" possible UAV/SRV mismatch"), RHIShaderBase->GetShaderName(), *SRVName);
|
|
if (SRV->IsTexture())
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, Type: %s"),
|
|
BindIndex,
|
|
FRHIViewDesc::GetTextureDimensionString(SRV->GetDesc().Texture.SRV.Dimension));
|
|
}
|
|
else
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, Type: %s"),
|
|
BindIndex,
|
|
FRHIViewDesc::GetBufferTypeString(SRV->GetDesc().Buffer.SRV.BufferType));
|
|
}
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/** Validates that the SRV is conform to what the shader expects */
|
|
void ValidateShaderResourceView(const FRHIShader* RHIShaderBase, uint32 BindIndex, const FRHITexture* Texture)
|
|
{
|
|
#if RHI_INCLUDE_SHADER_DEBUG_DATA
|
|
if (Texture)
|
|
{
|
|
// Validate Type
|
|
if (!RHIShaderBase->DebugSRVTypeValidationData.Num())
|
|
return;
|
|
|
|
static const auto ShaderCodeValidationTypePredicate = [](const FShaderCodeValidationType& lhs, const FShaderCodeValidationType& rhs) -> bool { return lhs.BindPoint < rhs.BindPoint; };
|
|
|
|
FShaderCodeValidationType SRVValidationType = { BindIndex , EShaderCodeResourceBindingType::Invalid };
|
|
int32 FoundIndex = Algo::BinarySearch(RHIShaderBase->DebugSRVTypeValidationData, SRVValidationType, ShaderCodeValidationTypePredicate);
|
|
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
EShaderCodeResourceBindingType ExpectedType = RHIShaderBase->DebugSRVTypeValidationData[FoundIndex].Type;
|
|
|
|
if (!ValidateDimension(ExpectedType, Texture->GetDesc().Dimension, true))
|
|
{
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Dimension for Texture %s at BindIndex \"%d\" must match type declared in the shader"),
|
|
RHIShaderBase->GetShaderName(),
|
|
*Texture->GetName().ToString(),
|
|
BindIndex);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL Type: %s, Actual Dimension: %s"),
|
|
BindIndex,
|
|
GetShaderCodeResourceBindingTypeName(ExpectedType),
|
|
GetTextureDimensionString(Texture->GetDesc().Dimension));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: No bind point found at BindIndex \"%d\" possible UAV/SRV mismatch"), RHIShaderBase->GetShaderName(), BindIndex);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, Type: %s"),
|
|
BindIndex,
|
|
GetTextureDimensionString(Texture->GetDesc().Dimension));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/** Validates that the UAV is conform to what the shader expects */
|
|
void ValidateUnorderedAccessView(const FRHIShader* RHIShaderBase, uint32 BindIndex, const FRHIUnorderedAccessView* UAV)
|
|
{
|
|
#if RHI_INCLUDE_SHADER_DEBUG_DATA
|
|
if (UAV)
|
|
{
|
|
auto const ViewIdentity = UAV->GetViewIdentity();
|
|
|
|
static const auto GetUAVName = [](const FRHIUnorderedAccessView* UAV, auto& ViewIdentity) -> FString
|
|
{
|
|
FString UAVName;
|
|
if (ViewIdentity.Resource)
|
|
{
|
|
UAVName = ViewIdentity.Resource->GetDebugName();
|
|
}
|
|
if (UAVName.IsEmpty())
|
|
{
|
|
UAVName = UAV->GetOwnerName().ToString();
|
|
}
|
|
|
|
return UAVName;
|
|
};
|
|
|
|
// Validate Type
|
|
if (!RHIShaderBase->DebugUAVTypeValidationData.Num())
|
|
return;
|
|
|
|
static const auto ShaderCodeValidationTypePredicate = [](const FShaderCodeValidationType& lhs, const FShaderCodeValidationType& rhs) -> bool { return lhs.BindPoint < rhs.BindPoint; };
|
|
|
|
FShaderCodeValidationType SRVValidationType = { BindIndex , EShaderCodeResourceBindingType::Invalid };
|
|
int32 FoundIndex = Algo::BinarySearch(RHIShaderBase->DebugUAVTypeValidationData, SRVValidationType, ShaderCodeValidationTypePredicate);
|
|
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
EShaderCodeResourceBindingType ExpectedType = RHIShaderBase->DebugUAVTypeValidationData[FoundIndex].Type;
|
|
|
|
if (UAV->IsTexture())
|
|
{
|
|
if (!ValidateDimension(ExpectedType, UAV->GetDesc().Texture.UAV.Dimension, UAV->GetDesc().Texture.UAV.Plane, false))
|
|
{
|
|
FString UAVName = GetUAVName(UAV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Dimension for UAV \"%s\" must match type declared in the shader"), RHIShaderBase->GetShaderName(), *UAVName);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL Type: %s, Actual Dimension: %s"),
|
|
BindIndex,
|
|
GetShaderCodeResourceBindingTypeName(ExpectedType),
|
|
FRHIViewDesc::GetTextureDimensionString(UAV->GetDesc().Texture.SRV.Dimension));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
else if (UAV->IsBuffer())
|
|
{
|
|
if (!ValidateBuffer(ExpectedType, UAV->GetDesc().Buffer.UAV.BufferType, false))
|
|
{
|
|
FString UAVName = GetUAVName(UAV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Buffer type for UAV \"%s\" must match buffer type declared in the shader"), RHIShaderBase->GetShaderName(), *UAVName);
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL Type: %s, Actual Type: %s"),
|
|
BindIndex,
|
|
GetShaderCodeResourceBindingTypeName(ExpectedType),
|
|
FRHIViewDesc::GetBufferTypeString(UAV->GetDesc().Buffer.UAV.BufferType));
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString UAVName = GetUAVName(UAV, ViewIdentity);
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: No bind point found for UAV \"%s\" possible UAV/SRV mismatch"), RHIShaderBase->GetShaderName(), *UAVName);
|
|
|
|
if (UAV->IsTexture())
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, Type: %s"),
|
|
BindIndex,
|
|
FRHIViewDesc::GetTextureDimensionString(UAV->GetDesc().Texture.SRV.Dimension));
|
|
}
|
|
else
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, Type: %s"),
|
|
BindIndex,
|
|
FRHIViewDesc::GetBufferTypeString(UAV->GetDesc().Buffer.SRV.BufferType));
|
|
}
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/** Validates that the Uniform conforms to what the shader expects */
|
|
void ValidateUniformBuffer(const FRHIShader* RHIShaderBase, uint32 BindIndex, FRHIUniformBuffer* UB)
|
|
{
|
|
if (!UB)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FRHIUniformBufferLayout& Layout = UB->GetLayout();
|
|
|
|
const TArray<uint32>& LayoutHashes = RHIShaderBase->GetShaderResourceTable().ResourceTableLayoutHashes;
|
|
if (BindIndex >= (uint32)LayoutHashes.Num())
|
|
{
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Invalid bind index %u for uniform buffer \"%s\" (UB table size: %d)"), RHIShaderBase->GetShaderName(), BindIndex, *Layout.GetDebugName(), LayoutHashes.Num());
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
return;
|
|
}
|
|
|
|
uint32 ShaderTableHash = LayoutHashes[BindIndex];
|
|
uint32 UniformBufferHash = Layout.GetHash();
|
|
if (ShaderTableHash != 0 && UniformBufferHash != ShaderTableHash)
|
|
{
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Invalid layout hash %u for uniform buffer \"%s\" at bind index %u, expecting %u"), RHIShaderBase->GetShaderName(), UniformBufferHash, *Layout.GetDebugName(), BindIndex, ShaderTableHash);
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
|
|
#if RHI_INCLUDE_SHADER_DEBUG_DATA
|
|
{
|
|
// Validate Type
|
|
static const auto ShaderCodeValidationUBSizePredicate = [](const FShaderCodeValidationUBSize& lhs, const FShaderCodeValidationUBSize& rhs) -> bool { return lhs.BindPoint < rhs.BindPoint; };
|
|
|
|
FShaderCodeValidationUBSize SRVValidationSize = { BindIndex , 0 };
|
|
int32 FoundIndex = Algo::BinarySearch(RHIShaderBase->DebugUBSizeValidationData, SRVValidationSize, ShaderCodeValidationUBSizePredicate);
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
uint32_t Size = RHIShaderBase->DebugUBSizeValidationData[FoundIndex].Size;
|
|
|
|
if(Size > 0 && Size > UB->GetSize())
|
|
{
|
|
FString ErrorMessage = FString::Printf(TEXT("Shader %s: Uniform buffer \"%s\" has unexpected size"), RHIShaderBase->GetShaderName(), *Layout.GetDebugName());
|
|
ErrorMessage += FString::Printf(TEXT("\nBind point: %d, HLSL size: %d, Actual size: %d"), BindIndex, Size, UB->GetSize());
|
|
RHI_VALIDATION_CHECK(false, *ErrorMessage);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Validation Transient Resource Allocator
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#define TRANSIENT_RESOURCE_LOG_PREFIX_REASON(ReasonString) TEXT("RHI validation failed: " ReasonString "\n\n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT(" RHI Transient Resource Allocation Validation Error \n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT("\n")
|
|
|
|
#define TRANSIENT_RESOURCE_LOG_SUFFIX TEXT("\n")\
|
|
TEXT("--------------------------------------------------------------------\n")\
|
|
TEXT("\n")
|
|
|
|
FValidationTransientResourceAllocator::~FValidationTransientResourceAllocator()
|
|
{
|
|
checkf(!RHIAllocator, TEXT("Release was not called on FRHITransientResourceAllocator."));
|
|
}
|
|
|
|
void FValidationTransientResourceAllocator::SetCreateMode(ERHITransientResourceCreateMode InCreateMode)
|
|
{
|
|
// Validation intentionally doesn't pass through the create mode. It's always inline.
|
|
}
|
|
|
|
FRHITransientTexture* FValidationTransientResourceAllocator::CreateTexture(const FRHITextureCreateInfo& InCreateInfo, const TCHAR* InDebugName, const FRHITransientAllocationFences& Fences)
|
|
{
|
|
check(FRHITextureCreateInfo::CheckValidity(InCreateInfo, InDebugName));
|
|
|
|
FRHITransientTexture* TransientTexture = RHIAllocator->CreateTexture(InCreateInfo, InDebugName, Fences);
|
|
|
|
if (!TransientTexture)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FRHITexture* RHITexture = TransientTexture->GetRHI();
|
|
|
|
checkf(!AllocatedResourceMap.Contains(RHITexture), TEXT("Platform RHI returned an FRHITexture (0x%p) which was already in use by another transient texture resource on this allocator (0x%p)."), RHITexture, this);
|
|
AllocatedResourceMap.Add(RHITexture, { InDebugName, FAllocatedResourceData::EType::Texture });
|
|
|
|
RHIValidation::FResource* Resource = RHITexture->GetTrackerResource();
|
|
check(Resource);
|
|
|
|
if (!Resource->IsBarrierTrackingInitialized())
|
|
{
|
|
RHITexture->InitBarrierTracking(InCreateInfo.NumMips, InCreateInfo.ArraySize * (InCreateInfo.IsTextureCube() ? 6 : 1), InCreateInfo.Format, InCreateInfo.Flags, ERHIAccess::Discard, InDebugName);
|
|
}
|
|
else
|
|
{
|
|
// The existing resource returned by the platform RHI should have the layout we expect.
|
|
RHITexture->CheckValidationLayout(InCreateInfo.NumMips, InCreateInfo.ArraySize * (InCreateInfo.IsTextureCube() ? 6 : 1), InCreateInfo.Format);
|
|
|
|
// @todo dev-pr debug names are global properties of resources. It seems wrong to require the graphics pipe here. Decouple this.
|
|
// @todo we should validate the resource was in the Discard state rather than forcing it
|
|
PendingPipelineOps[ERHIPipeline::Graphics].Emplace(RHIValidation::FOperation::InitTransient(Resource, InDebugName));
|
|
}
|
|
|
|
return TransientTexture;
|
|
}
|
|
|
|
FRHITransientBuffer* FValidationTransientResourceAllocator::CreateBuffer(const FRHIBufferCreateInfo& InCreateInfo, const TCHAR* InDebugName, const FRHITransientAllocationFences& Fences)
|
|
{
|
|
FRHITransientBuffer* TransientBuffer = RHIAllocator->CreateBuffer(InCreateInfo, InDebugName, Fences);
|
|
|
|
if (!TransientBuffer)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FRHIBuffer* RHIBuffer = TransientBuffer->GetRHI();
|
|
|
|
checkf(!AllocatedResourceMap.Contains(RHIBuffer), TEXT("Platform RHI returned an FRHIBuffer (0x%p) which was already in use by another transient buffer resource on this allocator (0x%p)."), RHIBuffer, this);
|
|
AllocatedResourceMap.Add(RHIBuffer, { InDebugName, FAllocatedResourceData::EType::Buffer });
|
|
|
|
if (!RHIBuffer->IsBarrierTrackingInitialized())
|
|
{
|
|
const FRHIBufferCreateDesc CreateDesc =
|
|
FRHIBufferCreateDesc::Create(InDebugName, InCreateInfo)
|
|
.SetInitialState(ERHIAccess::Discard);
|
|
|
|
RHIBuffer->InitBarrierTracking(CreateDesc);
|
|
}
|
|
else
|
|
{
|
|
// @todo dev-pr debug names are global properties of resources. It seems wrong to require the graphics pipe here. Decouple this.
|
|
// @todo we should validate the resource was in the Discard state rather than forcing it
|
|
PendingPipelineOps[ERHIPipeline::Graphics].Emplace(RHIValidation::FOperation::InitTransient(RHIBuffer, InDebugName));
|
|
}
|
|
|
|
return TransientBuffer;
|
|
}
|
|
|
|
void FValidationTransientResourceAllocator::DeallocateMemory(FRHITransientTexture* InTransientTexture, const FRHITransientAllocationFences& Fences)
|
|
{
|
|
check(InTransientTexture);
|
|
|
|
RHIAllocator->DeallocateMemory(InTransientTexture, Fences);
|
|
|
|
checkf(AllocatedResourceMap.Contains(InTransientTexture->GetRHI()), TEXT("DeallocateMemory called on texture %s, but it is not marked as allocated."), InTransientTexture->GetName());
|
|
AllocatedResourceMap.Remove(InTransientTexture->GetRHI());
|
|
}
|
|
|
|
void FValidationTransientResourceAllocator::DeallocateMemory(FRHITransientBuffer* InTransientBuffer, const FRHITransientAllocationFences& Fences)
|
|
{
|
|
check(InTransientBuffer);
|
|
|
|
RHIAllocator->DeallocateMemory(InTransientBuffer, Fences);
|
|
|
|
checkf(AllocatedResourceMap.Contains(InTransientBuffer->GetRHI()), TEXT("DeallocateMemory called on buffer %s, but it is not marked as allocated."), InTransientBuffer->GetName());
|
|
AllocatedResourceMap.Remove(InTransientBuffer->GetRHI());
|
|
}
|
|
|
|
void FValidationTransientResourceAllocator::Flush(FRHICommandListImmediate& RHICmdList, FRHITransientAllocationStats* OutHeapStats)
|
|
{
|
|
// Insert pending ops into context trackers
|
|
for (ERHIPipeline Pipeline : MakeFlagsRange(ERHIPipeline::All))
|
|
{
|
|
if (PendingPipelineOps[Pipeline].Num())
|
|
{
|
|
FRHICommandListScopedPipeline Scope(RHICmdList, Pipeline);
|
|
RHICmdList.EnqueueLambda([Pipeline, PendingOps = MoveTemp(PendingPipelineOps[Pipeline])](FRHICommandListImmediate& InRHICmdList)
|
|
{
|
|
IRHIComputeContext& Context = InRHICmdList.GetComputeContext().GetLowestLevelContext();
|
|
Context.Tracker->AddOps(PendingOps);
|
|
});
|
|
}
|
|
}
|
|
|
|
RHIAllocator->Flush(RHICmdList, OutHeapStats);
|
|
}
|
|
|
|
void FValidationTransientResourceAllocator::Release(FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
RHIAllocator->Release(RHICmdList);
|
|
RHIAllocator = nullptr;
|
|
delete this;
|
|
}
|
|
|
|
void ValidateShaderParameters(FRHIShader* RHIShader, RHIValidation::FTracker* Tracker, RHIValidation::FStaticUniformBuffers& StaticUniformBuffers, RHIValidation::FStageBoundUniformBuffers& BoundUniformBuffers, TConstArrayView<FRHIShaderParameterResource> InParameters, ERHIAccess InRequiredAccess, RHIValidation::EUAVMode InRequiredUAVMode)
|
|
{
|
|
for (const FRHIShaderParameterResource& Parameter : InParameters)
|
|
{
|
|
switch (Parameter.Type)
|
|
{
|
|
case FRHIShaderParameterResource::EType::Texture:
|
|
if (FRHITexture* Texture = static_cast<FRHITexture*>(Parameter.Resource))
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateShaderResourceView(RHIShader, Parameter.Index, Texture);
|
|
}
|
|
Tracker->Assert(Texture->GetWholeResourceIdentitySRV(), InRequiredAccess);
|
|
}
|
|
break;
|
|
case FRHIShaderParameterResource::EType::ResourceView:
|
|
if (FRHIShaderResourceView* SRV = static_cast<FRHIShaderResourceView*>(Parameter.Resource))
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateShaderResourceView(RHIShader, Parameter.Index, SRV);
|
|
}
|
|
Tracker->Assert(SRV->GetViewIdentity(), InRequiredAccess);
|
|
}
|
|
break;
|
|
case FRHIShaderParameterResource::EType::UnorderedAccessView:
|
|
if (FRHIUnorderedAccessView* UAV = static_cast<FRHIUnorderedAccessView*>(Parameter.Resource))
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateUnorderedAccessView(RHIShader, Parameter.Index, UAV);
|
|
}
|
|
Tracker->AssertUAV(static_cast<FRHIUnorderedAccessView*>(Parameter.Resource), InRequiredUAVMode, Parameter.Index);
|
|
}
|
|
break;
|
|
case FRHIShaderParameterResource::EType::Sampler:
|
|
// No validation
|
|
break;
|
|
case FRHIShaderParameterResource::EType::UniformBuffer:
|
|
if (FRHIUniformBuffer* UniformBuffer = static_cast<FRHIUniformBuffer*>(Parameter.Resource))
|
|
{
|
|
if (GRHIValidationEnabled)
|
|
{
|
|
RHIValidation::ValidateUniformBuffer(RHIShader, Parameter.Index, UniformBuffer);
|
|
}
|
|
|
|
BoundUniformBuffers.Bind(Parameter.Index, UniformBuffer);
|
|
StaticUniformBuffers.ValidateSetShaderUniformBuffer(UniformBuffer);
|
|
}
|
|
break;
|
|
case FRHIShaderParameterResource::EType::ResourceCollection:
|
|
if (const FRHIResourceCollection* ResourceCollection = static_cast<const FRHIResourceCollection*>(Parameter.Resource))
|
|
{
|
|
for (const FRHIResourceCollectionMember& Member : ResourceCollection->Members)
|
|
{
|
|
switch (Member.Type)
|
|
{
|
|
case FRHIResourceCollectionMember::EType::Texture:
|
|
if (FRHITexture* Texture = static_cast<FRHITexture*>(Member.Resource))
|
|
{
|
|
Tracker->Assert(Texture->GetWholeResourceIdentitySRV(), InRequiredAccess);
|
|
}
|
|
break;
|
|
case FRHIResourceCollectionMember::EType::TextureReference:
|
|
if (FRHITextureReference* Texture = static_cast<FRHITextureReference*>(Member.Resource))
|
|
{
|
|
Tracker->Assert(Texture->GetWholeResourceIdentitySRV(), InRequiredAccess);
|
|
}
|
|
break;
|
|
case FRHIResourceCollectionMember::EType::ShaderResourceView:
|
|
if (FRHIShaderResourceView* SRV = static_cast<FRHIShaderResourceView*>(Member.Resource))
|
|
{
|
|
Tracker->Assert(SRV->GetViewIdentity(), InRequiredAccess);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
checkf(false, TEXT("Unhandled resource type?"));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // ENABLE_RHI_VALIDATION
|