// Copyright Epic Games, Inc. All Rights Reserved. #include "RenderGraph.h" #include "HAL/PlatformFileManager.h" #include "HAL/PlatformMisc.h" #include "Misc/FileHelper.h" #include "Misc/WildcardString.h" #include "Misc/OutputDeviceRedirector.h" #include "Runtime/Launch/Resources/Version.h" #include "BuildSettings.h" #include "Serialization/JsonTypes.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "GenericPlatform/GenericPlatformCrashContext.h" #include "GenericPlatform/GenericPlatformDriver.h" #include "RenderGraphUtils.h" #include "GlobalShader.h" #include "RHIValidation.h" #if RDG_DUMP_RESOURCES namespace { static TAutoConsoleVariable GDumpGPURootCVar( TEXT("r.DumpGPU.Root"), TEXT("*"), TEXT("Allows to filter the tree when using r.DumpGPU command, the pattern match is case sensitive."), ECVF_Default); static TAutoConsoleVariable GDumpTextureCVar( TEXT("r.DumpGPU.Texture"), 2, TEXT("Whether to dump textures.\n") TEXT(" 0: Ignores all textures\n") TEXT(" 1: Dump only textures' descriptors\n") TEXT(" 2: Dump textures' descriptors and binaries (default)"), ECVF_RenderThreadSafe); static TAutoConsoleVariable GDumpBufferCVar( TEXT("r.DumpGPU.Buffer"), 2, TEXT("Whether to dump buffer.\n") TEXT(" 0: Ignores all buffers\n") TEXT(" 1: Dump only buffers' descriptors\n") TEXT(" 2: Dump buffers' descriptors and binaries (default)"), ECVF_RenderThreadSafe); static TAutoConsoleVariable GDumpGPUPassParameters( TEXT("r.DumpGPU.PassParameters"), 1, TEXT("Whether to dump the pass parameters."), ECVF_RenderThreadSafe); static TAutoConsoleVariable GDumpGPUDraws( TEXT("r.DumpGPU.Draws"), 0, TEXT("Whether to dump resource after each individual draw call (disabled by default)."), ECVF_Default); static TAutoConsoleVariable GDumpExploreCVar( TEXT("r.DumpGPU.Explore"), 1, TEXT("Whether to open file explorer to where the GPU dump on completion (enabled by default)."), ECVF_Default); static TAutoConsoleVariable GDumpRenderingConsoleVariablesCVar( TEXT("r.DumpGPU.ConsoleVariables"), 1, TEXT("Whether to dump rendering console variables (enabled by default)."), ECVF_Default); static TAutoConsoleVariable GDumpTestEnableDiskWrite( TEXT("r.DumpGPU.Test.EnableDiskWrite"), 1, TEXT("Master switch whether any files should be written to disk, used for r.DumpGPU automation tests to not fill up workers' hard drive."), ECVF_Default); static TAutoConsoleVariable GDumpTestPrettifyResourceFileNames( TEXT("r.DumpGPU.Test.PrettifyResourceFileNames"), 0, TEXT("Whether the resource file names should include resource name. May increase the likelyness of running into Windows' filepath limit."), ECVF_RenderThreadSafe); static TAutoConsoleVariable GDumpGPUDirectoryCVar( TEXT("r.DumpGPU.Directory"), TEXT(""), TEXT("Directory to dump to."), ECVF_Default); // Although this cvar does not seams used in the C++ code base, it is dumped by DumpRenderingCVarsToCSV() and used by GPUDumpViewer.html. static TAutoConsoleVariable GDumpGPUVisualizeResource( TEXT("r.DumpGPU.Viewer.Visualize"), TEXT(""), TEXT("Name of RDG output resource to automatically open in the dump viewer."), ECVF_Default); class FDumpTextureCS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FDumpTextureCS); SHADER_USE_PARAMETER_STRUCT(FDumpTextureCS, FGlobalShader); static inline bool IsSupported(const FStaticShaderPlatform ShaderPlatform) { return RHISupportsComputeShaders(ShaderPlatform); } enum class ETextureType { Texture2DFloatNoMSAA, Texture2DUintNoMSAA, Texture2DDepthStencilNoMSAA, MAX }; class FTextureTypeDim : SHADER_PERMUTATION_ENUM_CLASS("DIM_TEXTURE_TYPE", ETextureType); using FPermutationDomain = TShaderPermutationDomain; static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return IsSupported(Parameters.Platform); } BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER_SRV(Texture2D, Texture) SHADER_PARAMETER_UAV(RWTexture2D, StagingOutput) END_SHADER_PARAMETER_STRUCT() }; IMPLEMENT_GLOBAL_SHADER(FDumpTextureCS, "/Engine/Private/Tools/DumpTexture.usf", "MainCS", SF_Compute); BEGIN_SHADER_PARAMETER_STRUCT(FDumpTexturePass, ) SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, Texture) RDG_TEXTURE_ACCESS_DYNAMIC(TextureAccess) END_SHADER_PARAMETER_STRUCT() BEGIN_SHADER_PARAMETER_STRUCT(FDumpBufferPass, ) RDG_BUFFER_ACCESS(Buffer, ERHIAccess::CopySrc) END_SHADER_PARAMETER_STRUCT() struct FRDGResourceDumpContext { static constexpr const TCHAR* kBaseDir = TEXT("Base/"); static constexpr const TCHAR* kPassesDir = TEXT("Passes/"); static constexpr const TCHAR* kResourcesDir = TEXT("Resources/"); static constexpr const TCHAR* kStructuresDir = TEXT("Structures/"); static constexpr const TCHAR* kStructuresMetadataDir = TEXT("StructuresMetadata/"); bool bEnableDiskWrite = false; FString DumpingDirectoryPath; FDateTime Time; FGenericPlatformMemoryConstants MemoryConstants; FGenericPlatformMemoryStats MemoryStats; int32 ResourcesDumpPasses = 0; int32 ResourcesDumpExecutedPasses = 0; int32 PassesCount = 0; TMap LastResourceVersion; TSet IsDumpedToDisk; // Pass being dumping individual draws const FRDGPass* DrawDumpingPass = nullptr; int32 DrawDumpCount = 0; bool bShowInExplore = false; bool IsDumpingFrame() const { check(IsInRenderingThread() || IsInParallelRenderingThread()); return !DumpingDirectoryPath.IsEmpty(); } FString GetDumpFullPath(const FString& DumpRelativeFileName) const { check(bEnableDiskWrite); return DumpingDirectoryPath / DumpRelativeFileName; } bool DumpStringToFile(FStringView OutputString, const FString& FileName, uint32 WriteFlags = FILEWRITE_None) { // Make it has if the write happened and was successful. if (!bEnableDiskWrite) { return true; } FString FullPath = GetDumpFullPath(FileName); return FFileHelper::SaveStringToFile( OutputString, *FullPath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), WriteFlags); } bool DumpJsonToFile(const TSharedPtr& JsonObject, const FString& FileName, uint32 WriteFlags = FILEWRITE_None) { FString OutputString; TSharedRef>> Writer = TJsonWriterFactory>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer); return DumpStringToFile(OutputString, FileName, WriteFlags); } bool DumpBinaryToFile(TArrayView ArrayView, const FString& FileName) { // Make it has if the write happened and was successful. if (!bEnableDiskWrite) { return true; } FString FullPath = GetDumpFullPath(FileName); return FFileHelper::SaveArrayToFile(ArrayView, *FullPath); } bool DumpBinaryToFile(const TArray64& Array, const FString& FileName) { // Make it has if the write happened and was successful. if (!bEnableDiskWrite) { return true; } FString FullPath = GetDumpFullPath(FileName); return FFileHelper::SaveArrayToFile(Array, *FullPath); } bool IsUnsafeToDumpResource(SIZE_T ResourceByteSize, float DumpMemoryMultiplier) const { uint64 AproximatedStagingMemoryRequired = uint64(double(ResourceByteSize) * DumpMemoryMultiplier); uint64 MaxMemoryAvailable = FMath::Min(MemoryStats.AvailablePhysical, MemoryStats.AvailableVirtual); return AproximatedStagingMemoryRequired > MaxMemoryAvailable; } template static uint64 PtrToUint(const T* Ptr) { return static_cast(reinterpret_cast(Ptr)); } template static FString PtrToString(const T* Ptr) { return FString::Printf(TEXT("%016x"), PtrToUint(Ptr)); } static FString GetUniqueResourceName(const FRDGResource* Resource) { if (GDumpTestPrettifyResourceFileNames.GetValueOnRenderThread()) { FString UniqueResourceName = FString::Printf(TEXT("%s.%016x"), Resource->Name, PtrToUint(Resource)); UniqueResourceName.ReplaceInline(TEXT("/"), TEXT("")); UniqueResourceName.ReplaceInline(TEXT("\\"), TEXT("")); return UniqueResourceName; } return PtrToString(Resource); } static FString GetUniqueSubResourceName(FRDGTextureSRVDesc SubResourceDesc) { check(SubResourceDesc.NumMipLevels == 1); FString UniqueResourceName = GetUniqueResourceName(SubResourceDesc.Texture); if (SubResourceDesc.Format == PF_X24_G8) { return FString::Printf(TEXT("%s.stencil"), *UniqueResourceName); } return FString::Printf(TEXT("%s.mip%d"), *UniqueResourceName, SubResourceDesc.MipLevel); } void ReleaseRHIResources(FRHICommandListImmediate& RHICmdList) { // Flush the RHI resource memory so the readback memory can be fully reused in the next resource dump. { RHICmdList.SubmitCommandsAndFlushGPU(); RHICmdList.BlockUntilGPUIdle(); FRHIResource::FlushPendingDeletes(RHICmdList); RHICmdList.FlushResources(); RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources); } } void UpdatePassProgress() { ResourcesDumpExecutedPasses++; if (ResourcesDumpExecutedPasses % 10 == 0) { UE_LOG(LogRendererCore, Display, TEXT("Dumped %d / %d resources"), ResourcesDumpExecutedPasses, ResourcesDumpPasses); } } void DumpRenderingCVarsToCSV() const { FString FileName = GetDumpFullPath(FString(FRDGResourceDumpContext::kBaseDir) / TEXT("ConsoleVariables.csv")); TUniquePtr Ar = TUniquePtr(IFileManager::Get().CreateFileWriter(*FileName)); auto OnConsoleVariable = [&Ar](const TCHAR* CVarName, IConsoleObject* ConsoleObj) { if (ConsoleObj->TestFlags(ECVF_Unregistered)) { return; } const IConsoleVariable* CVar = ConsoleObj->AsVariable(); if (!CVar) { return; } EConsoleVariableFlags CVarFlags = CVar->GetFlags(); const TCHAR* Type = nullptr; if (CVar->IsVariableBool()) { Type = TEXT("bool"); } else if (CVar->IsVariableInt()) { Type = TEXT("int32"); } else if (CVar->IsVariableFloat()) { Type = TEXT("float"); } else if (CVar->IsVariableString()) { Type = TEXT("FString"); } else { return; } const TCHAR* SetByName = TEXT(""); switch (CVarFlags & ECVF_SetByMask) { case ECVF_SetByConstructor: { SetByName = TEXT("Constructor"); break; } case ECVF_SetByScalability: { SetByName = TEXT("Scalability"); break; } case ECVF_SetByGameSetting: { SetByName = TEXT("GameSetting"); break; } case ECVF_SetByProjectSetting: { SetByName = TEXT("ProjectSetting"); break; } case ECVF_SetBySystemSettingsIni: { SetByName = TEXT("SystemSettingsIni"); break; } case ECVF_SetByDeviceProfile: { SetByName = TEXT("DeviceProfile"); break; } case ECVF_SetByConsoleVariablesIni: { SetByName = TEXT("ConsoleVariablesIni"); break; } case ECVF_SetByCommandline: { SetByName = TEXT("Commandline"); break; } case ECVF_SetByCode: { SetByName = TEXT("Code"); break; } case ECVF_SetByConsole: { SetByName = TEXT("Console"); break; } default: unimplemented(); } FString Value = CVar->GetString(); FString CSVLine = FString::Printf(TEXT("%s,%s,%s,%s\n"), CVarName, Type, SetByName, *Value); FStringView CSVLineView(CSVLine); auto Src = StringCast(CSVLineView.GetData(), CSVLineView.Len()); Ar->Serialize((ANSICHAR*)Src.Get(), Src.Length() * sizeof(ANSICHAR)); }; bool bSuccess = false; if (Ar) { { FString CSVLine = TEXT("CVar,Type,SetBy,Value\n"); FStringView CSVLineView(CSVLine); auto Src = StringCast(CSVLineView.GetData(), CSVLineView.Len()); Ar->Serialize((ANSICHAR*)Src.Get(), Src.Length() * sizeof(ANSICHAR)); } if (GDumpRenderingConsoleVariablesCVar.GetValueOnGameThread() != 0) { IConsoleManager::Get().ForEachConsoleObjectThatStartsWith(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), TEXT("r.")); IConsoleManager::Get().ForEachConsoleObjectThatStartsWith(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), TEXT("sg.")); IConsoleManager::Get().ForEachConsoleObjectThatContains(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), TEXT("rhi")); } else { IConsoleManager::Get().ForEachConsoleObjectThatStartsWith(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), TEXT("r.DumpGPU.")); } // Always explicitly close to catch errors from flush/close Ar->Close(); bSuccess = !Ar->IsError() && !Ar->IsCriticalError(); } if (bSuccess) { UE_LOG(LogRendererCore, Display, TEXT("DumpGPU dumped rendering cvars to %s."), *FileName); } else { UE_LOG(LogRendererCore, Error, TEXT("DumpGPU had a file error when dumping rendering cvars to %s."), *FileName); } } template bool IsDumped(const T* Ptr) const { return IsDumpedToDisk.Contains(static_cast(Ptr)); } template void SetDumped(const T* Ptr) { check(!IsDumped(Ptr)); if (IsDumpedToDisk.Num() % 1024 == 0) { IsDumpedToDisk.Reserve(IsDumpedToDisk.Num() + 1024); } IsDumpedToDisk.Add(static_cast(Ptr)); } void GetResourceDumpInfo( const FRDGPass* Pass, const FRDGResource* Resource, bool bIsOutputResource, bool* bOutDumpResourceInfos, bool* bOutDumpResourceBinary) { *bOutDumpResourceInfos = false; *bOutDumpResourceBinary = bIsOutputResource; //if (bIsOutputResource) //{ // *OutResourceVersionDumpName = GetResourceVersionDumpName(Pass, Resource); //} if (!LastResourceVersion.Contains(Resource)) { // First time we ever see this resource, so dump it's info to disk *bOutDumpResourceInfos = true; // If not an output, it might be a resource undumped by r.DumpGPU.Root or external texture so still dump it as v0. if (!bIsOutputResource) { *bOutDumpResourceBinary = true; } if (LastResourceVersion.Num() % 1024 == 0) { LastResourceVersion.Reserve(LastResourceVersion.Num() + 1024); } LastResourceVersion.Add(Resource, Pass); } else { LastResourceVersion[Resource] = Pass; } } FString ToJson(EPixelFormat Format) { FString PixelFormat = GPixelFormats[Format].Name; if (!PixelFormat.StartsWith(TEXT("PF_"))) { PixelFormat = FString::Printf(TEXT("PF_%s"), GPixelFormats[Format].Name); } return PixelFormat; } TSharedPtr ToJson(const FShaderParametersMetadata::FMember& Member) { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("Name"), Member.GetName()); JsonObject->SetStringField(TEXT("ShaderType"), Member.GetShaderType()); JsonObject->SetNumberField(TEXT("FileLine"), Member.GetFileLine()); JsonObject->SetNumberField(TEXT("Offset"), Member.GetOffset()); JsonObject->SetStringField(TEXT("BaseType"), GetUniformBufferBaseTypeString(Member.GetBaseType())); JsonObject->SetNumberField(TEXT("Precision"), Member.GetPrecision()); JsonObject->SetNumberField(TEXT("NumRows"), Member.GetNumRows()); JsonObject->SetNumberField(TEXT("NumColumns"), Member.GetNumColumns()); JsonObject->SetNumberField(TEXT("NumElements"), Member.GetNumElements()); JsonObject->SetStringField(TEXT("StructMetadata"), *PtrToString(Member.GetStructMetadata())); return JsonObject; } TSharedPtr ToJson(const FShaderParametersMetadata* Metadata) { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("StructTypeName"), Metadata->GetStructTypeName()); JsonObject->SetStringField(TEXT("ShaderVariableName"), Metadata->GetShaderVariableName()); JsonObject->SetStringField(TEXT("FileName"), Metadata->GetFileName()); JsonObject->SetNumberField(TEXT("FileLine"), Metadata->GetFileLine()); JsonObject->SetNumberField(TEXT("Size"), Metadata->GetSize()); //JsonObject->SetNumberField(TEXT("UseCase"), Metadata->GetUseCase()); { TArray> Members; for (const FShaderParametersMetadata::FMember& Member : Metadata->GetMembers()) { Members.Add(MakeShared(ToJson(Member))); } JsonObject->SetArrayField(TEXT("Members"), Members); } return JsonObject; } TSharedPtr ToJson(const TCHAR* Name, const FRDGTextureDesc& Desc) { FString PixelFormat = ToJson(Desc.Format); int32 ResourceByteSize = Desc.Extent.X * Desc.Extent.Y * Desc.Depth * Desc.ArraySize * Desc.NumSamples * GPixelFormats[Desc.Format].BlockBytes; TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("Name"), Name); JsonObject->SetNumberField(TEXT("ByteSize"), ResourceByteSize); JsonObject->SetStringField(TEXT("Desc"), TEXT("FRDGTextureDesc")); JsonObject->SetStringField(TEXT("Type"), GetTextureDimensionString(Desc.Dimension)); JsonObject->SetStringField(TEXT("Format"), *PixelFormat); JsonObject->SetNumberField(TEXT("ExtentX"), Desc.Extent.X); JsonObject->SetNumberField(TEXT("ExtentY"), Desc.Extent.Y); JsonObject->SetNumberField(TEXT("Depth"), Desc.Depth); JsonObject->SetNumberField(TEXT("ArraySize"), Desc.ArraySize); JsonObject->SetNumberField(TEXT("NumMips"), Desc.NumMips); JsonObject->SetNumberField(TEXT("NumSamples"), Desc.NumSamples); { TArray> FlagsNames; for (uint64 BitId = 0; BitId < uint64(8 * sizeof(ETextureCreateFlags)); BitId++) { ETextureCreateFlags Flag = ETextureCreateFlags(uint64(1) << BitId); if (EnumHasAnyFlags(Desc.Flags, Flag)) { FlagsNames.Add(MakeShareable(new FJsonValueString(GetTextureCreateFlagString(Flag)))); } } JsonObject->SetArrayField(TEXT("Flags"), FlagsNames); } return JsonObject; } TSharedPtr ToJson(const TCHAR* Name, const FRDGBufferDesc& Desc, int32 ResourceByteSize) { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("Name"), Name); JsonObject->SetNumberField(TEXT("ByteSize"), ResourceByteSize); JsonObject->SetStringField(TEXT("Desc"), TEXT("FRDGBufferDesc")); JsonObject->SetStringField(TEXT("Type"), GetBufferUnderlyingTypeName(Desc.UnderlyingType)); JsonObject->SetNumberField(TEXT("BytesPerElement"), Desc.BytesPerElement); JsonObject->SetNumberField(TEXT("NumElements"), Desc.NumElements); JsonObject->SetStringField(TEXT("Metadata"), *PtrToString(Desc.Metadata)); { TArray> UsageNames; for (uint64 BitId = 0; BitId < uint64(8 * sizeof(EBufferUsageFlags)); BitId++) { EBufferUsageFlags Flag = EBufferUsageFlags(uint64(1) << BitId); if (EnumHasAnyFlags(Desc.Usage, Flag)) { UsageNames.Add(MakeShareable(new FJsonValueString(GetBufferUsageFlagString(Flag)))); } } JsonObject->SetArrayField(TEXT("Usage"), UsageNames); } return JsonObject; } void Dump(const FShaderParametersMetadata* Metadata) { if (IsDumped(Metadata)) { return; } TSharedPtr JsonObject = ToJson(Metadata); FString JsonPath = kStructuresMetadataDir / PtrToString(Metadata) + TEXT(".json"); DumpJsonToFile(JsonObject, JsonPath); SetDumped(Metadata); // Dump dependencies Metadata->IterateStructureMetadataDependencies( [&](const FShaderParametersMetadata* Struct) { if (Struct) { Dump(Struct); } }); } struct FTextureSubresourceDumpDesc { FIntPoint SubResourceExtent = FIntPoint(0, 0); SIZE_T ByteSize = 0; bool bPreprocessForStaging = false; FDumpTextureCS::ETextureType DumpTextureType = FDumpTextureCS::ETextureType::MAX; EPixelFormat PreprocessedPixelFormat; bool IsDumpSupported() const { return ByteSize != SIZE_T(0); } }; FTextureSubresourceDumpDesc TranslateSubresourceDumpDesc(FRDGTextureSRVDesc SubresourceDesc) { const FRDGTextureDesc& Desc = SubresourceDesc.Texture->Desc; FTextureSubresourceDumpDesc SubresourceDumpDesc; SubresourceDumpDesc.PreprocessedPixelFormat = Desc.Format; bool bIsUnsupported = false; if (GPixelFormats[Desc.Format].BlockSizeX != 1 || GPixelFormats[Desc.Format].BlockSizeY != 1 || GPixelFormats[Desc.Format].BlockSizeZ != 1) { bIsUnsupported = true; } if (!bIsUnsupported && Desc.IsTexture2D() && !Desc.IsMultisample() && !Desc.IsTextureArray()) { SubresourceDumpDesc.SubResourceExtent.X = Desc.Extent.X >> SubresourceDesc.MipLevel; SubresourceDumpDesc.SubResourceExtent.Y = Desc.Extent.Y >> SubresourceDesc.MipLevel; if (IsUintFormat(Desc.Format) || IsSintFormat(Desc.Format)) { SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DUintNoMSAA; } else { SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DFloatNoMSAA; } if (SubresourceDesc.Format == PF_X24_G8) { SubresourceDumpDesc.PreprocessedPixelFormat = PF_R8_UINT; SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DDepthStencilNoMSAA; } else if (Desc.Format == PF_DepthStencil) { SubresourceDumpDesc.PreprocessedPixelFormat = PF_R32_FLOAT; SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DFloatNoMSAA; } else if (Desc.Format == PF_ShadowDepth) { SubresourceDumpDesc.PreprocessedPixelFormat = PF_R32_FLOAT; SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DFloatNoMSAA; } else if (Desc.Format == PF_D24) { SubresourceDumpDesc.PreprocessedPixelFormat = PF_R32_FLOAT; SubresourceDumpDesc.DumpTextureType = FDumpTextureCS::ETextureType::Texture2DFloatNoMSAA; } } SubresourceDumpDesc.ByteSize = SIZE_T(SubresourceDumpDesc.SubResourceExtent.X) * SIZE_T(SubresourceDumpDesc.SubResourceExtent.Y) * SIZE_T(GPixelFormats[SubresourceDumpDesc.PreprocessedPixelFormat].BlockBytes); // Whether the subresource need preprocessing pass before copy into staging. SubresourceDumpDesc.bPreprocessForStaging = SubresourceDumpDesc.PreprocessedPixelFormat != Desc.Format || SubresourceDesc.MipLevel != 0; return SubresourceDumpDesc; } void DumpTextureSubResource( FRHICommandListImmediate& RHICmdList, const TCHAR* TextureDebugName, FRHITexture* Texture, FRHIShaderResourceView* SubResourceSRV, const FTextureSubresourceDumpDesc& SubresourceDumpDesc, const FString& DumpFilePath) { check(IsInRenderingThread()); // Preprocess FTextureRHIRef StagingSrcTexture; if (SubresourceDumpDesc.bPreprocessForStaging) { { FRHIResourceCreateInfo CreateInfo(TEXT("DumpGPU.PreprocessTexture")); StagingSrcTexture = RHICreateTexture2D( SubresourceDumpDesc.SubResourceExtent.X, SubresourceDumpDesc.SubResourceExtent.Y, SubresourceDumpDesc.PreprocessedPixelFormat, /* NumMips = */ 1, /* NumSamples = */ 1, TexCreate_UAV | TexCreate_ShaderResource | TexCreate_HideInVisualizeTexture, CreateInfo); } FUnorderedAccessViewRHIRef StagingOutput = RHICreateUnorderedAccessView(StagingSrcTexture, /* MipLevel = */ 0); RHICmdList.Transition(FRHITransitionInfo(StagingSrcTexture, ERHIAccess::Unknown, ERHIAccess::UAVCompute)); FDumpTextureCS::FPermutationDomain PermutationVector; PermutationVector.Set(SubresourceDumpDesc.DumpTextureType); TShaderMapRef ComputeShader(GetGlobalShaderMap(GMaxRHIShaderPlatform), PermutationVector); FDumpTextureCS::FParameters ShaderParameters; ShaderParameters.Texture = SubResourceSRV; ShaderParameters.StagingOutput = StagingOutput; FComputeShaderUtils::Dispatch( RHICmdList, ComputeShader, ShaderParameters, FComputeShaderUtils::GetGroupCount(SubresourceDumpDesc.SubResourceExtent, 8)); RHICmdList.Transition(FRHITransitionInfo(StagingSrcTexture, ERHIAccess::UAVCompute, ERHIAccess::CopySrc)); } else { StagingSrcTexture = Texture; } // Copy the texture for CPU readback FTextureRHIRef StagingTexture; { FRHIResourceCreateInfo CreateInfo(TEXT("DumpGPU.StagingTexture")); StagingTexture = RHICreateTexture2D( SubresourceDumpDesc.SubResourceExtent.X, SubresourceDumpDesc.SubResourceExtent.Y, SubresourceDumpDesc.PreprocessedPixelFormat, /* NumMips = */ 1, /* NumSamples = */ 1, TexCreate_CPUReadback | TexCreate_HideInVisualizeTexture, CreateInfo); RHICmdList.Transition(FRHITransitionInfo(StagingTexture, ERHIAccess::Unknown, ERHIAccess::CopyDest)); // Ensure this copy call does not perform any transitions. We're handling them manually. FResolveParams ResolveParams; ResolveParams.SourceAccessFinal = ERHIAccess::Unknown; ResolveParams.DestAccessFinal = ERHIAccess::Unknown; // Transfer memory GPU -> CPU RHICmdList.CopyToResolveTarget(StagingSrcTexture, StagingTexture, ResolveParams); RHICmdList.Transition(FRHITransitionInfo(StagingTexture, ERHIAccess::CopyDest, ERHIAccess::CPURead)); } // Submit to GPU and wait for completion. static const FName FenceName(TEXT("DumpGPU.TextureFence")); FGPUFenceRHIRef Fence = RHICreateGPUFence(FenceName); { Fence->Clear(); RHICmdList.WriteGPUFence(Fence); RHICmdList.SubmitCommandsAndFlushGPU(); RHICmdList.BlockUntilGPUIdle(); } void* Content = nullptr; int32 RowPitchInPixels = 0; int32 ColumnPitchInPixels = 0; RHICmdList.MapStagingSurface(StagingTexture, Fence.GetReference(), Content, RowPitchInPixels, ColumnPitchInPixels); if (Content) { TArray64 Array; Array.SetNumUninitialized(SubresourceDumpDesc.ByteSize); SIZE_T BytePerPixel = SIZE_T(GPixelFormats[SubresourceDumpDesc.PreprocessedPixelFormat].BlockBytes); const uint8* SrcData = static_cast(Content); for (int32 y = 0; y < SubresourceDumpDesc.SubResourceExtent.Y; y++) { // Flip the data to be bottom left corner for the WebGL viewer. const uint8* SrcPos = SrcData + SIZE_T(SubresourceDumpDesc.SubResourceExtent.Y - 1 - y) * SIZE_T(RowPitchInPixels) * BytePerPixel; uint8* DstPos = (&Array[0]) + SIZE_T(y) * SIZE_T(SubresourceDumpDesc.SubResourceExtent.X) * BytePerPixel; FPlatformMemory::Memmove(DstPos, SrcPos, SIZE_T(SubresourceDumpDesc.SubResourceExtent.X) * BytePerPixel); } RHICmdList.UnmapStagingSurface(StagingTexture); DumpBinaryToFile(Array, DumpFilePath); } else { UE_LOG(LogRendererCore, Warning, TEXT("RHICmdList.MapStagingSurface() to dump texture %s failed."), TextureDebugName); } } void DumpDrawTextureSubResource( FRHICommandList& RHICmdList, FRDGTextureSRVDesc SubresourceDesc, ERHIAccess RHIAccessState) { check(IsInRenderingThread()); FRHICommandListImmediate& RHICmdListImmediate = FRHICommandListExecutor::GetImmediateCommandList(); check(&RHICmdListImmediate == &RHICmdList); const FString UniqueResourceSubResourceName = GetUniqueSubResourceName(SubresourceDesc); const FTextureSubresourceDumpDesc SubresourceDumpDesc = TranslateSubresourceDumpDesc(SubresourceDesc); if (!SubresourceDumpDesc.IsDumpSupported()) { return; } FRHITexture* RHITexture = SubresourceDesc.Texture->GetRHI(); FShaderResourceViewRHIRef SubResourceSRV; if (SubresourceDumpDesc.bPreprocessForStaging) { SubResourceSRV = RHICreateShaderResourceView(RHITexture, FRHITextureSRVCreateInfo(SubresourceDesc)); RHICmdListImmediate.Transition(FRHITransitionInfo(RHITexture, ERHIAccess::Unknown, ERHIAccess::SRVCompute)); } else { RHICmdListImmediate.Transition(FRHITransitionInfo(RHITexture, RHIAccessState, ERHIAccess::CopySrc)); } FString DumpFilePath = kResourcesDir / FString::Printf( TEXT("%s.v%016x.d%d.bin"), *UniqueResourceSubResourceName, PtrToUint(DrawDumpingPass), DrawDumpCount); DumpTextureSubResource( RHICmdListImmediate, SubresourceDesc.Texture->Name, RHITexture, SubResourceSRV, SubresourceDumpDesc, DumpFilePath); if (SubresourceDumpDesc.bPreprocessForStaging) { RHICmdListImmediate.Transition(FRHITransitionInfo(RHITexture, ERHIAccess::SRVCompute, RHIAccessState)); } else { RHICmdListImmediate.Transition(FRHITransitionInfo(RHITexture, ERHIAccess::CopySrc, RHIAccessState)); } SubResourceSRV = nullptr; ReleaseRHIResources(RHICmdListImmediate); } void AddDumpTextureSubResourcePass( FRDGBuilder& GraphBuilder, TArray>& InputResourceNames, TArray>& OutputResourceNames, const FRDGPass* Pass, FRDGTextureSRVDesc SubresourceDesc, bool bIsOutputResource) { int32 DumpTextureMode = GDumpTextureCVar.GetValueOnRenderThread(); if (DumpTextureMode == 0) { return; } const FRDGTextureDesc& Desc = SubresourceDesc.Texture->Desc; const FString UniqueResourceName = GetUniqueResourceName(SubresourceDesc.Texture); const FString UniqueResourceSubResourceName = GetUniqueSubResourceName(SubresourceDesc); const FTextureSubresourceDumpDesc SubresourceDumpDesc = TranslateSubresourceDumpDesc(SubresourceDesc); if (bIsOutputResource) { OutputResourceNames.AddUnique(MakeShareable(new FJsonValueString(UniqueResourceSubResourceName))); } else { InputResourceNames.AddUnique(MakeShareable(new FJsonValueString(UniqueResourceSubResourceName))); } bool bDumpResourceInfos; bool bDumpResourceBinary; GetResourceDumpInfo(Pass, SubresourceDesc.Texture, bIsOutputResource, &bDumpResourceInfos, &bDumpResourceBinary); // Dump the information of the texture to json file. if (bDumpResourceInfos) { TSharedPtr JsonObject = ToJson(SubresourceDesc.Texture->Name, Desc); FString ResourceInfoJsonPath = kResourcesDir / UniqueResourceName + TEXT(".json"); DumpJsonToFile(JsonObject, ResourceInfoJsonPath); } if (!SubresourceDumpDesc.IsDumpSupported()) { return; } // Early return if this resource shouldn't be dumped. if (!bDumpResourceBinary || DumpTextureMode != 2) { return; } FString DumpFilePath = kResourcesDir / FString::Printf(TEXT("%s.v%016x.bin"), *UniqueResourceSubResourceName, PtrToUint(bIsOutputResource ? Pass : nullptr)); // Verify there is enough available memory to dump the resource. if (IsUnsafeToDumpResource(SubresourceDumpDesc.ByteSize, 2.2f + (SubresourceDumpDesc.bPreprocessForStaging ? 1.0f : 0.0f))) { UE_LOG(LogRendererCore, Warning, TEXT("Not dumping %s because of insuficient memory available for staging texture."), *DumpFilePath); return; } // Dump the resource's binary to a .bin file. { FDumpTexturePass* PassParameters = GraphBuilder.AllocParameters(); if (SubresourceDumpDesc.bPreprocessForStaging) { if (!(SubresourceDesc.Texture->Desc.Flags & TexCreate_ShaderResource)) { UE_LOG(LogRendererCore, Warning, TEXT("Not dumping %s because requires copy to staging texture using compute, but is missing TexCreate_ShaderResource."), *UniqueResourceSubResourceName); return; } if (!FDumpTextureCS::IsSupported(GMaxRHIShaderPlatform)) { UE_LOG(LogRendererCore, Warning, TEXT("Not dumping %s because FDumpTextureCS compute shader is not supported."), *UniqueResourceSubResourceName); return; } PassParameters->Texture = GraphBuilder.CreateSRV(SubresourceDesc); } else { PassParameters->TextureAccess = FRDGTextureAccess(SubresourceDesc.Texture, ERHIAccess::CopySrc); } GraphBuilder.AddPass( RDG_EVENT_NAME("RDG DumpTexture(%s -> %s) %dx%d", SubresourceDesc.Texture->Name, *DumpFilePath, SubresourceDumpDesc.SubResourceExtent.X, SubresourceDumpDesc.SubResourceExtent.Y), PassParameters, (SubresourceDumpDesc.bPreprocessForStaging ? ERDGPassFlags::Compute : ERDGPassFlags::Copy) | ERDGPassFlags::NeverCull, [ PassParameters, this, DumpFilePath, SubresourceDesc, SubresourceDumpDesc] (FRHICommandListImmediate& RHICmdList) { this->DumpTextureSubResource( RHICmdList, SubresourceDesc.Texture->Name, SubresourceDumpDesc.bPreprocessForStaging ? nullptr : SubresourceDesc.Texture->GetRHI(), SubresourceDumpDesc.bPreprocessForStaging ? PassParameters->Texture->GetRHI() : nullptr, SubresourceDumpDesc, DumpFilePath); this->ReleaseRHIResources(RHICmdList); this->UpdatePassProgress(); }); ResourcesDumpPasses++; } } void AddDumpTexturePasses( FRDGBuilder& GraphBuilder, TArray>& InputResourceNames, TArray>& OutputResourceNames, const FRDGPass* Pass, FRDGTextureSRVDesc SubresourceRangeDesc, bool bIsOutputResource) { if (SubresourceRangeDesc.Format == PF_X24_G8) { AddDumpTextureSubResourcePass( GraphBuilder, InputResourceNames, OutputResourceNames, Pass, SubresourceRangeDesc, bIsOutputResource); } else { for (int32 MipLevel = SubresourceRangeDesc.MipLevel; MipLevel < (SubresourceRangeDesc.MipLevel + SubresourceRangeDesc.NumMipLevels); MipLevel++) { FRDGTextureSRVDesc SubresourceDesc = FRDGTextureSRVDesc::CreateForMipLevel(SubresourceRangeDesc.Texture, MipLevel); AddDumpTextureSubResourcePass( GraphBuilder, InputResourceNames, OutputResourceNames, Pass, SubresourceDesc, bIsOutputResource); } } } void AddDumpBufferPass( FRDGBuilder& GraphBuilder, TArray>& InputResourceNames, TArray>& OutputResourceNames, const FRDGPass* Pass, FRDGBuffer* Buffer, bool bIsOutputResource) { int32 DumpBufferMode = GDumpTextureCVar.GetValueOnRenderThread(); if (DumpBufferMode == 0) { return; } FString UniqueResourceName = GetUniqueResourceName(Buffer); if (bIsOutputResource) { OutputResourceNames.AddUnique(MakeShareable(new FJsonValueString(UniqueResourceName))); } else { InputResourceNames.AddUnique(MakeShareable(new FJsonValueString(UniqueResourceName))); } const FRDGBufferDesc& Desc = Buffer->Desc; const int32 ByteSize = Desc.GetTotalNumBytes(); bool bDumpResourceInfos; bool bDumpResourceBinary; GetResourceDumpInfo(Pass, Buffer, bIsOutputResource, &bDumpResourceInfos, &bDumpResourceBinary); // Dump the information of the buffer to json file. if (bDumpResourceInfos) { TSharedPtr JsonObject = ToJson(Buffer->Name, Desc, ByteSize); FString ResourceInfoJsonPath = kResourcesDir / UniqueResourceName + TEXT(".json"); DumpJsonToFile(JsonObject, ResourceInfoJsonPath); if (Desc.Metadata && DumpBufferMode == 2) { Dump(Desc.Metadata); } } if (Desc.UnderlyingType == FRDGBufferDesc::EUnderlyingType::AccelerationStructure) { return; } // Dump the resource's binary to a .bin file. if (bDumpResourceBinary && DumpBufferMode == 2) { FString DumpFilePath = kResourcesDir / FString::Printf(TEXT("%s.v%016x.bin"), *UniqueResourceName, PtrToUint(bIsOutputResource ? Pass : nullptr)); if (IsUnsafeToDumpResource(ByteSize, 1.2f)) { UE_LOG(LogRendererCore, Warning, TEXT("Not dumping %s because of insuficient memory available for staging buffer."), *DumpFilePath); return; } FDumpBufferPass* PassParameters = GraphBuilder.AllocParameters(); PassParameters->Buffer = Buffer; GraphBuilder.AddPass( RDG_EVENT_NAME("RDG DumpBuffer(%s -> %s)", Buffer->Name, *DumpFilePath), PassParameters, ERDGPassFlags::Readback, [this, DumpFilePath, Buffer, ByteSize](FRHICommandListImmediate& RHICmdList) { check(IsInRenderingThread()); FStagingBufferRHIRef StagingBuffer = RHICreateStagingBuffer(); // Transfer memory GPU -> CPU RHICmdList.CopyToStagingBuffer(Buffer->GetRHI(), StagingBuffer, 0, ByteSize); // Submit to GPU and wait for completion. static const FName FenceName(TEXT("DumpGPU.BufferFence")); FGPUFenceRHIRef Fence = RHICreateGPUFence(FenceName); { Fence->Clear(); RHICmdList.WriteGPUFence(Fence); RHICmdList.SubmitCommandsAndFlushGPU(); RHICmdList.BlockUntilGPUIdle(); } void* Content = RHICmdList.LockStagingBuffer(StagingBuffer, Fence.GetReference(), 0, ByteSize); if (Content) { TArrayView ArrayView(reinterpret_cast(Content), ByteSize); DumpBinaryToFile(ArrayView, DumpFilePath); RHICmdList.UnlockStagingBuffer(StagingBuffer); } else { UE_LOG(LogRendererCore, Warning, TEXT("RHICmdList.LockStagingBuffer() to dump buffer %s failed."), Buffer->Name); } StagingBuffer = nullptr; Fence = nullptr; this->ReleaseRHIResources(RHICmdList); this->UpdatePassProgress(); }); ResourcesDumpPasses++; } } // Look whether the pass matches matches r.DumpGPU.Root bool IsDumpingPass(const FRDGPass* Pass) { FString RootWildcardString = GDumpGPURootCVar.GetValueOnRenderThread(); FWildcardString WildcardFilter(RootWildcardString); bool bDumpPass = (RootWildcardString == TEXT("*")); if (!bDumpPass) { bDumpPass = WildcardFilter.IsMatch(Pass->GetEventName().GetTCHAR()); } #if RDG_GPU_SCOPES if (!bDumpPass) { const FRDGEventScope* ParentScope = Pass->GetGPUScopes().Event; while (ParentScope) { bDumpPass = bDumpPass || WildcardFilter.IsMatch(ParentScope->Name.GetTCHAR()); ParentScope = ParentScope->ParentScope; } } #endif return bDumpPass; } }; bool bIsDumpingFrame_GameThread = false; FRDGResourceDumpContext GRDGResourceDumpContext; } bool IsDumpingRDGResources() { return GRDGResourceDumpContext.IsDumpingFrame(); } FString FRDGBuilder::BeginResourceDump(const TArray& Args) { check(IsInGameThread()); if (bIsDumpingFrame_GameThread) { return FString(); } FRDGResourceDumpContext NewResourceDumpContext; NewResourceDumpContext.Time = FDateTime::Now(); { FString MasterDirectoryPath = GDumpGPUDirectoryCVar.GetValueOnGameThread(); if (MasterDirectoryPath.IsEmpty()) { MasterDirectoryPath = FPaths::ProjectSavedDir() / TEXT("GPUDumps/"); } NewResourceDumpContext.DumpingDirectoryPath = MasterDirectoryPath / FApp::GetProjectName() + TEXT("-") + FPlatformProperties::PlatformName() + TEXT("-") + NewResourceDumpContext.Time.ToString() + TEXT("/"); } NewResourceDumpContext.bEnableDiskWrite = GDumpTestEnableDiskWrite.GetValueOnGameThread() != 0; NewResourceDumpContext.bShowInExplore = NewResourceDumpContext.bEnableDiskWrite && GDumpExploreCVar.GetValueOnGameThread() != 0; NewResourceDumpContext.MemoryConstants = FPlatformMemory::GetConstants(); NewResourceDumpContext.MemoryStats = FPlatformMemory::GetStats(); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (NewResourceDumpContext.bEnableDiskWrite) { if (!PlatformFile.DirectoryExists(*NewResourceDumpContext.DumpingDirectoryPath)) { PlatformFile.CreateDirectoryTree(*NewResourceDumpContext.DumpingDirectoryPath); } PlatformFile.CreateDirectoryTree(*(NewResourceDumpContext.DumpingDirectoryPath / FRDGResourceDumpContext::kBaseDir)); PlatformFile.CreateDirectoryTree(*(NewResourceDumpContext.DumpingDirectoryPath / FRDGResourceDumpContext::kResourcesDir)); NewResourceDumpContext.DumpStringToFile(TEXT(""), FString(FRDGResourceDumpContext::kBaseDir) / TEXT("PassDrawCounts.json")); } // Output informations { const TCHAR* BranchName = BuildSettings::GetBranchName(); const TCHAR* BuildDate = BuildSettings::GetBuildDate(); const TCHAR* BuildVersion = BuildSettings::GetBuildVersion(); FString BuildConfiguration = LexToString(FApp::GetBuildConfiguration()); FString BuildTarget = LexToString(FApp::GetBuildTargetType()); FGPUDriverInfo GPUDriverInfo = FPlatformMisc::GetGPUDriverInfo(GRHIAdapterName); TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("Project"), FApp::GetProjectName()); JsonObject->SetNumberField(TEXT("EngineMajorVersion"), ENGINE_MAJOR_VERSION); JsonObject->SetNumberField(TEXT("EngineMinorVersion"), ENGINE_MINOR_VERSION); JsonObject->SetNumberField(TEXT("EnginePatchVersion"), ENGINE_PATCH_VERSION); JsonObject->SetStringField(TEXT("BuildBranch"), BranchName ? BranchName : TEXT("")); JsonObject->SetStringField(TEXT("BuildDate"), BuildDate ? BuildDate : TEXT("")); JsonObject->SetStringField(TEXT("BuildVersion"), BuildVersion ? BuildVersion : TEXT("")); JsonObject->SetStringField(TEXT("BuildTarget"), BuildTarget); JsonObject->SetStringField(TEXT("BuildConfiguration"), BuildConfiguration); JsonObject->SetNumberField(TEXT("Build64Bits"), (PLATFORM_64BITS ? 1 : 0)); JsonObject->SetStringField(TEXT("Platform"), FPlatformProperties::IniPlatformName()); JsonObject->SetStringField(TEXT("DeviceName"), FPlatformProcess::ComputerName()); JsonObject->SetStringField(TEXT("CPUVendor"), FPlatformMisc::GetCPUVendor()); JsonObject->SetStringField(TEXT("CPUBrand"), FPlatformMisc::GetCPUBrand()); JsonObject->SetNumberField(TEXT("CPUNumberOfCores"), FPlatformMisc::NumberOfCores()); JsonObject->SetNumberField(TEXT("CPUNumberOfCoresIncludingHyperthreads"), FPlatformMisc::NumberOfCoresIncludingHyperthreads()); JsonObject->SetStringField(TEXT("GPUVendor"), RHIVendorIdToString()); JsonObject->SetStringField(TEXT("GPUDeviceDescription"), GPUDriverInfo.DeviceDescription); JsonObject->SetStringField(TEXT("GPUDriverUserVersion"), GPUDriverInfo.UserDriverVersion); JsonObject->SetStringField(TEXT("GPUDriverInternalVersion"), GPUDriverInfo.GetUnifiedDriverVersion()); JsonObject->SetStringField(TEXT("GPUDriverDate"), GPUDriverInfo.DriverDate); JsonObject->SetNumberField(TEXT("MemoryTotalPhysical"), NewResourceDumpContext.MemoryConstants.TotalPhysical); JsonObject->SetNumberField(TEXT("MemoryPageSize"), NewResourceDumpContext.MemoryConstants.PageSize); JsonObject->SetStringField(TEXT("RHI"), GDynamicRHI->GetName()); JsonObject->SetStringField(TEXT("RHIMaxFeatureLevel"), LexToString(GMaxRHIFeatureLevel)); JsonObject->SetStringField(TEXT("DumpTime"), NewResourceDumpContext.Time.ToString()); NewResourceDumpContext.DumpJsonToFile(JsonObject, FString(FRDGResourceDumpContext::kBaseDir) / TEXT("Infos.json")); } // Dump the rendering cvars if (NewResourceDumpContext.bEnableDiskWrite) { NewResourceDumpContext.DumpRenderingCVarsToCSV(); } // Copy the viewer if (NewResourceDumpContext.bEnableDiskWrite) { const TCHAR* OpenGPUDumpViewerWindowsName = TEXT("OpenGPUDumpViewer.bat"); const TCHAR* ViewerHTML = TEXT("GPUDumpViewer.html"); FString DumpGPUViewerSourcePath = FPaths::EngineDir() + FString(TEXT("Extras")) / TEXT("GPUDumpViewer"); PlatformFile.CopyFile(*(NewResourceDumpContext.DumpingDirectoryPath / ViewerHTML), *(DumpGPUViewerSourcePath / ViewerHTML)); PlatformFile.CopyFile(*(NewResourceDumpContext.DumpingDirectoryPath / OpenGPUDumpViewerWindowsName), *(DumpGPUViewerSourcePath / OpenGPUDumpViewerWindowsName)); } ENQUEUE_RENDER_COMMAND(FStartGPUDump)( [NewResourceDumpContext](FRHICommandListImmediate& ImmediateRHICmdList) { check(IsInRenderingThread()); GRDGResourceDumpContext = NewResourceDumpContext; ImmediateRHICmdList.SubmitCommandsAndFlushGPU(); // Disable the validation for BUF_SourceCopy so that all buffers can be copied into staging buffer for CPU readback. #if ENABLE_RHI_VALIDATION GRHIValidateBufferSourceCopy = false; #endif }); bIsDumpingFrame_GameThread = true; if (NewResourceDumpContext.bEnableDiskWrite) { return NewResourceDumpContext.DumpingDirectoryPath; } return FString(); } void FRDGBuilder::EndResourceDump() { check(IsInGameThread()); if (!bIsDumpingFrame_GameThread) { return; } // Wait all rendering commands are completed to finish with GRDGResourceDumpContext. { UE_LOG(LogRendererCore, Display, TEXT("Stalling game thread until render thread finishes to dump resources")); ENQUEUE_RENDER_COMMAND(FEndGPUDump)( [](FRHICommandListImmediate& ImmediateRHICmdList) { ImmediateRHICmdList.SubmitCommandsAndFlushGPU(); #if ENABLE_RHI_VALIDATION GRHIValidateBufferSourceCopy = true; #endif }); FlushRenderingCommands(); } // Log information about the dump. FString AbsDumpingDirectoryPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*GRDGResourceDumpContext.DumpingDirectoryPath); { UE_LOG(LogRendererCore, Display, TEXT("Dumped %d resources to %s"), GRDGResourceDumpContext.ResourcesDumpPasses, *AbsDumpingDirectoryPath); } // Dump the log into the dump directory. if (GRDGResourceDumpContext.bEnableDiskWrite) { if (GLog) { GLog->FlushThreadedLogs(); GLog->Flush(); } FGenericCrashContext::DumpLog(GRDGResourceDumpContext.DumpingDirectoryPath / FRDGResourceDumpContext::kBaseDir); } #if PLATFORM_DESKTOP if (GRDGResourceDumpContext.bShowInExplore) { FPlatformProcess::ExploreFolder(*AbsDumpingDirectoryPath); } #endif GRDGResourceDumpContext = FRDGResourceDumpContext(); bIsDumpingFrame_GameThread = false; } void FRDGBuilder::DumpResourcePassOutputs(const FRDGPass* Pass) { if (bInDebugPassScope) { return; } if (!GRDGResourceDumpContext.IsDumpingFrame()) { return; } check(IsInRenderingThread()); if (!GRDGResourceDumpContext.IsDumpingPass(Pass)) { return; } bInDebugPassScope = true; TArray> InputResourceNames; TArray> OutputResourceNames; Pass->GetParameters().Enumerate([&](FRDGParameter Parameter) { switch (Parameter.GetType()) { case UBMT_RDG_TEXTURE: { if (FRDGTextureRef Texture = Parameter.GetAsTexture()) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::Create(Texture); GRDGResourceDumpContext.AddDumpTexturePasses(*this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, /* bIsOutputResource = */ false); } } break; case UBMT_RDG_TEXTURE_SRV: { if (FRDGTextureSRVRef SRV = Parameter.GetAsTextureSRV()) { if (SRV->Desc.MetaData == ERHITextureMetaDataAccess::None) { GRDGResourceDumpContext.AddDumpTexturePasses(*this, InputResourceNames, OutputResourceNames, Pass, SRV->Desc, /* bIsOutputResource = */ false); } else { UE_LOG(LogRendererCore, Warning, TEXT("Dumping texture %s's meta data unsupported"), SRV->Desc.Texture->Name); } } } break; case UBMT_RDG_TEXTURE_UAV: { if (FRDGTextureUAVRef UAV = Parameter.GetAsTextureUAV()) { if (UAV->Desc.MetaData == ERHITextureMetaDataAccess::None) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateForMipLevel(UAV->Desc.Texture, UAV->Desc.MipLevel); GRDGResourceDumpContext.AddDumpTextureSubResourcePass(*this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, /* bIsOutputResource = */ true); } else { UE_LOG(LogRendererCore, Warning, TEXT("Dumping texture %s's meta data unsupported"), UAV->Desc.Texture->Name); } } } break; case UBMT_RDG_TEXTURE_ACCESS: { if (FRDGTextureAccess TextureAccess = Parameter.GetAsTextureAccess()) { bool bIsOutputResource = ( TextureAccess.GetAccess() == ERHIAccess::UAVCompute || TextureAccess.GetAccess() == ERHIAccess::UAVGraphics || TextureAccess.GetAccess() == ERHIAccess::RTV); FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::Create(TextureAccess); GRDGResourceDumpContext.AddDumpTexturePasses(*this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, bIsOutputResource); } } break; case UBMT_RDG_TEXTURE_ACCESS_ARRAY: { const FRDGTextureAccessArray& TextureAccessArray = Parameter.GetAsTextureAccessArray(); for (FRDGTextureAccess TextureAccess : TextureAccessArray) { bool bIsOutputResource = ( TextureAccess.GetAccess() == ERHIAccess::UAVCompute || TextureAccess.GetAccess() == ERHIAccess::UAVGraphics || TextureAccess.GetAccess() == ERHIAccess::RTV); FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::Create(TextureAccess); GRDGResourceDumpContext.AddDumpTexturePasses(*this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, bIsOutputResource); } } break; case UBMT_RDG_BUFFER_SRV: { if (FRDGBufferSRVRef SRV = Parameter.GetAsBufferSRV()) { FRDGBufferRef Buffer = SRV->Desc.Buffer; GRDGResourceDumpContext.AddDumpBufferPass(*this, InputResourceNames, OutputResourceNames, Pass, Buffer, /* bIsOutputResource = */ false); } } break; case UBMT_RDG_BUFFER_UAV: { if (FRDGBufferUAVRef UAV = Parameter.GetAsBufferUAV()) { FRDGBufferRef Buffer = UAV->Desc.Buffer; GRDGResourceDumpContext.AddDumpBufferPass(*this, InputResourceNames, OutputResourceNames, Pass, Buffer, /* bIsOutputResource = */ true); } } break; case UBMT_RDG_BUFFER_ACCESS: { if (FRDGBufferAccess BufferAccess = Parameter.GetAsBufferAccess()) { bool bIsOutputResource = ( BufferAccess.GetAccess() == ERHIAccess::UAVCompute || BufferAccess.GetAccess() == ERHIAccess::UAVGraphics); GRDGResourceDumpContext.AddDumpBufferPass(*this, InputResourceNames, OutputResourceNames, Pass, BufferAccess, bIsOutputResource); } } break; case UBMT_RDG_BUFFER_ACCESS_ARRAY: { const FRDGBufferAccessArray& BufferAccessArray = Parameter.GetAsBufferAccessArray(); for (FRDGBufferAccess BufferAccess : BufferAccessArray) { bool bIsOutputResource = ( BufferAccess.GetAccess() == ERHIAccess::UAVCompute || BufferAccess.GetAccess() == ERHIAccess::UAVGraphics); GRDGResourceDumpContext.AddDumpBufferPass(*this, InputResourceNames, OutputResourceNames, Pass, BufferAccess, bIsOutputResource); } } break; case UBMT_RENDER_TARGET_BINDING_SLOTS: { const FRenderTargetBindingSlots& RenderTargets = Parameter.GetAsRenderTargetBindingSlots(); RenderTargets.Enumerate([&](FRenderTargetBinding RenderTarget) { FRDGTextureRef Texture = RenderTarget.GetTexture(); FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateForMipLevel(Texture, RenderTarget.GetMipIndex()); GRDGResourceDumpContext.AddDumpTextureSubResourcePass(*this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, /* bIsOutputResource = */ true); }); const FDepthStencilBinding& DepthStencil = RenderTargets.DepthStencil; if (FRDGTextureRef Texture = DepthStencil.GetTexture()) { FExclusiveDepthStencil DepthStencilAccess = DepthStencil.GetDepthStencilAccess(); if (DepthStencilAccess.IsUsingDepth()) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateForMipLevel(Texture, 0); GRDGResourceDumpContext.AddDumpTextureSubResourcePass( *this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, /* bIsOutputResource = */ DepthStencilAccess.IsDepthWrite()); } if (DepthStencilAccess.IsUsingStencil()) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateWithPixelFormat(Texture, PF_X24_G8); GRDGResourceDumpContext.AddDumpTextureSubResourcePass( *this, InputResourceNames, OutputResourceNames, Pass, TextureSubResource, /* bIsOutputResource = */ DepthStencilAccess.IsStencilWrite()); } } } break; } }); // Dump the pass informations { TArray> ParentEventScopeNames; #if RDG_GPU_SCOPES { const FRDGEventScope* ParentScope = Pass->GetGPUScopes().Event; while (ParentScope) { ParentEventScopeNames.Add(MakeShareable(new FJsonValueString(ParentScope->Name.GetTCHAR()))); ParentScope = ParentScope->ParentScope; } } #endif { ParentEventScopeNames.Add(MakeShareable(new FJsonValueString(FString::Printf(TEXT("Frame %llu"), GFrameCounterRenderThread)))); } TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("EventName"), Pass->GetEventName().GetTCHAR()); JsonObject->SetStringField(TEXT("ParametersName"), Pass->GetParameters().GetLayout().GetDebugName()); JsonObject->SetStringField(TEXT("Parameters"), FRDGResourceDumpContext::PtrToString(Pass->GetParameters().GetContents())); JsonObject->SetStringField(TEXT("ParametersMetadata"), FRDGResourceDumpContext::PtrToString(Pass->GetParameters().GetMetadata())); JsonObject->SetStringField(TEXT("Pointer"), FString::Printf(TEXT("%016x"), FRDGResourceDumpContext::PtrToUint(Pass))); JsonObject->SetNumberField(TEXT("Id"), GRDGResourceDumpContext.PassesCount); JsonObject->SetArrayField(TEXT("ParentEventScopes"), ParentEventScopeNames); JsonObject->SetArrayField(TEXT("InputResources"), InputResourceNames); JsonObject->SetArrayField(TEXT("OutputResources"), OutputResourceNames); GRDGResourceDumpContext.DumpJsonToFile(JsonObject, FString(FRDGResourceDumpContext::kBaseDir) / TEXT("Passes.json"), FILEWRITE_Append); } // Dump the pass' parameters if (GDumpGPUPassParameters.GetValueOnRenderThread() != 0) { int32 PassParametersByteSize = 0; { const FShaderParametersMetadata* Metadata = Pass->GetParameters().GetMetadata(); if (Metadata) { GRDGResourceDumpContext.Dump(Metadata); PassParametersByteSize = Metadata->GetSize(); } } if (PassParametersByteSize == 0 && Pass->GetParameters().GetLayoutPtr()) { PassParametersByteSize = Pass->GetParameters().GetLayout().ConstantBufferSize; } const uint8* PassParametersContent = Pass->GetParameters().GetContents(); if (PassParametersContent && !GRDGResourceDumpContext.IsDumped(PassParametersContent)) { TArrayView ArrayView(PassParametersContent, PassParametersByteSize); FString DumpFilePath = FRDGResourceDumpContext::kStructuresDir / FRDGResourceDumpContext::PtrToString(PassParametersContent) + TEXT(".bin"); GRDGResourceDumpContext.DumpBinaryToFile(ArrayView, DumpFilePath); GRDGResourceDumpContext.SetDumped(PassParametersContent); } } GRDGResourceDumpContext.PassesCount++; bInDebugPassScope = false; } #if RDG_DUMP_RESOURCES_AT_EACH_DRAW void FRDGBuilder::BeginPassDump(const FRDGPass* Pass) { if (!GRDGResourceDumpContext.IsDumpingFrame()) { return; } if (!GDumpGPUDraws.GetValueOnRenderThread()) { return; } if (!EnumHasAnyFlags(Pass->GetFlags(), ERDGPassFlags::Raster)) { return; } if (!IsInRenderingThread()) { UE_LOG(LogRendererCore, Warning, TEXT("Couldn't start dumping draw's resources for pass %s because not in the rendering thread"), Pass->GetEventName().GetTCHAR()); return; } check(GRDGResourceDumpContext.DrawDumpingPass == nullptr); if (GRDGResourceDumpContext.IsDumpingPass(Pass)) { GRDGResourceDumpContext.DrawDumpingPass = Pass; GRDGResourceDumpContext.DrawDumpCount = 0; } } // static void FRDGBuilder::DumpDraw(const FRDGEventName& DrawEventName) { if (!GRDGResourceDumpContext.IsDumpingFrame()) { return; } if (!IsInRenderingThread()) { UE_LOG(LogRendererCore, Warning, TEXT("Couldn't dump draw because not in the rendering thread")); return; } if (!GRDGResourceDumpContext.DrawDumpingPass) { return; } const FRDGPass* Pass = GRDGResourceDumpContext.DrawDumpingPass; FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList(); if (EnumHasAnyFlags(Pass->GetFlags(), ERDGPassFlags::Raster)) { RHICmdList.EndRenderPass(); } Pass->GetParameters().Enumerate([&](FRDGParameter Parameter) { switch (Parameter.GetType()) { case UBMT_RENDER_TARGET_BINDING_SLOTS: { const FRenderTargetBindingSlots& RenderTargets = Parameter.GetAsRenderTargetBindingSlots(); RenderTargets.Enumerate([&](FRenderTargetBinding RenderTarget) { FRDGTextureRef Texture = RenderTarget.GetTexture(); FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateForMipLevel(Texture, RenderTarget.GetMipIndex()); GRDGResourceDumpContext.DumpDrawTextureSubResource( RHICmdList, TextureSubResource, ERHIAccess::RTV); }); const FDepthStencilBinding& DepthStencil = RenderTargets.DepthStencil; if (FRDGTextureRef Texture = DepthStencil.GetTexture()) { FExclusiveDepthStencil DepthStencilAccess = DepthStencil.GetDepthStencilAccess(); if (DepthStencilAccess.IsDepthWrite()) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateForMipLevel(Texture, 0); GRDGResourceDumpContext.DumpDrawTextureSubResource( RHICmdList, TextureSubResource, ERHIAccess::RTV); } if (DepthStencilAccess.IsStencilWrite()) { FRDGTextureSRVDesc TextureSubResource = FRDGTextureSRVDesc::CreateWithPixelFormat(Texture, PF_X24_G8); GRDGResourceDumpContext.DumpDrawTextureSubResource( RHICmdList, TextureSubResource, ERHIAccess::RTV); } } } break; } }); if (EnumHasAnyFlags(Pass->GetFlags(), ERDGPassFlags::Raster)) { RHICmdList.BeginRenderPass(Pass->GetParameters().GetRenderPassInfo(), Pass->GetName()); } // Dump the draw even name { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("DrawName"), DrawEventName.GetTCHAR()); FString DumpFilePath = FRDGResourceDumpContext::kPassesDir / FString::Printf(TEXT("Pass.%016x.Draws.json"), FRDGResourceDumpContext::PtrToUint(Pass)); GRDGResourceDumpContext.DumpJsonToFile(JsonObject, DumpFilePath, FILEWRITE_Append); } GRDGResourceDumpContext.DrawDumpCount++; if (GRDGResourceDumpContext.DrawDumpCount % 10 == 0) { UE_LOG(LogRendererCore, Display, TEXT("Dumped %d draws' resources"), GRDGResourceDumpContext.DrawDumpCount); return; } } void FRDGBuilder::EndPassDump(const FRDGPass* Pass) { if (!GRDGResourceDumpContext.IsDumpingFrame()) { return; } if (!IsInRenderingThread()) { return; } if (!GRDGResourceDumpContext.DrawDumpingPass) { return; } check(Pass == GRDGResourceDumpContext.DrawDumpingPass); // Output how many draw has been dump for this pass. if (GRDGResourceDumpContext.DrawDumpCount > 0) { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("EventName"), Pass->GetEventName().GetTCHAR()); JsonObject->SetStringField(TEXT("Pointer"), FString::Printf(TEXT("%016x"), FRDGResourceDumpContext::PtrToUint(Pass))); JsonObject->SetNumberField(TEXT("DrawCount"), GRDGResourceDumpContext.DrawDumpCount); GRDGResourceDumpContext.DumpJsonToFile(JsonObject, FString(FRDGResourceDumpContext::kBaseDir) / TEXT("PassDrawCounts.json"), FILEWRITE_Append); UE_LOG(LogRendererCore, Display, TEXT("Completed dump of %d draws for pass: %s"), GRDGResourceDumpContext.DrawDumpCount, Pass->GetEventName().GetTCHAR()); } GRDGResourceDumpContext.DrawDumpingPass = nullptr; GRDGResourceDumpContext.DrawDumpCount = 0; } // static bool FRDGBuilder::IsDumpingDraws() { if (!GRDGResourceDumpContext.IsDumpingFrame()) { return false; } return GDumpGPUDraws.GetValueOnRenderThread() != 0; } #endif // RDG_DUMP_RESOURCES_AT_EACH_DRAW #else //! RDG_DUMP_RESOURCES bool IsDumpingRDGResources() { return false; } #endif //! RDG_DUMP_RESOURCES