Files
UnrealEngine/Engine/Source/Runtime/RHI/Private/PipelineFileCache.cpp
2025-05-18 13:04:45 +08:00

4531 lines
155 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
PipelineFileCache.cpp: Pipeline state cache implementation.
=============================================================================*/
#include "PipelineFileCache.h"
#include "Containers/List.h"
#include "Containers/Ticker.h"
#include "PipelineStateCache.h"
#include "HAL/IConsoleManager.h"
#include "Misc/EngineVersion.h"
#include "HAL/PlatformFile.h"
#include "Serialization/MemoryReader.h"
#include "Misc/CommandLine.h"
#include "Serialization/MemoryWriter.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "Misc/ScopeRWLock.h"
#include "Misc/Paths.h"
#include "Async/AsyncFileHandle.h"
#include "HAL/PlatformFileManager.h"
#include "Misc/FileHelper.h"
#include "ProfilingDebugging/CsvProfiler.h"
#include "RHIStrings.h"
#include "String/LexFromString.h"
#include "String/ParseTokens.h"
#include "Misc/ScopeExit.h"
#include <Algo/ForEach.h>
static FString JOURNAL_FILE_EXTENSION(TEXT(".jnl"));
// Loaded + New created
#if STATS // If STATS are not enabled RHI_API will DLLEXPORT on an empty line
RHI_API DEFINE_STAT(STAT_TotalGraphicsPipelineStateCount);
RHI_API DEFINE_STAT(STAT_TotalComputePipelineStateCount);
RHI_API DEFINE_STAT(STAT_TotalRayTracingPipelineStateCount);
#endif
// CSV category for PSO encounter and save events
CSV_DEFINE_CATEGORY(PSO, true);
// New Saved count
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Serialized Graphics Pipeline State Count"), STAT_SerializedGraphicsPipelineStateCount, STATGROUP_PipelineStateCache );
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Serialized Compute Pipeline State Count"), STAT_SerializedComputePipelineStateCount, STATGROUP_PipelineStateCache );
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Serialized RayTracing Pipeline State Count"), STAT_SerializedRayTracingPipelineStateCount, STATGROUP_PipelineStateCache);
// New created - Cache Miss count
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("New Graphics Pipeline State Count"), STAT_NewGraphicsPipelineStateCount, STATGROUP_PipelineStateCache );
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("New Compute Pipeline State Count"), STAT_NewComputePipelineStateCount, STATGROUP_PipelineStateCache );
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("New RayTracing Pipeline State Count"), STAT_NewRayTracingPipelineStateCount, STATGROUP_PipelineStateCache);
// Memory - Only track the file representation and new state cache stats
DECLARE_MEMORY_STAT(TEXT("New Cached PSO"), STAT_NewCachedPSOMemory, STATGROUP_PipelineStateCache);
DECLARE_MEMORY_STAT(TEXT("PSO Stat"), STAT_PSOStatMemory, STATGROUP_PipelineStateCache);
DECLARE_MEMORY_STAT(TEXT("File Cache"), STAT_FileCacheMemory, STATGROUP_PipelineStateCache);
void LexFromString(ETextureCreateFlags& OutValue, const FStringView& InString)
{
__underlying_type(ETextureCreateFlags) TmpFlags = static_cast<__underlying_type(ETextureCreateFlags)>(OutValue);
LexFromString(TmpFlags, InString);
OutValue = static_cast<ETextureCreateFlags>(TmpFlags);
}
enum class EPipelineCacheFileFormatVersions : uint32
{
FirstWorking = 7,
LibraryID = 9,
ShaderMetaData = 10,
SortedVertexDesc = 11,
TOCMagicGuard = 12,
PSOUsageMask = 13,
PSOBindCount = 14,
EOFMarker = 15,
EngineFlags = 16,
Subpass = 17,
PatchSizeReduction_NoDuplicatedGuid = 18,
AlphaToCoverage = 19,
AddingMeshShaders = 20,
RemovingTessellationShaders = 21,
LastUsedTime = 22,
MoreRenderTargetFlags = 23,
FragmentDensityAttachment = 24,
AddingDepthClipMode = 25,
BeforeStableCacheVersioning = 26,
RemovingLineAA = 27,
AddingDepthBounds = 28,
AddRTPSOShaderBindingLayout = 29,
};
const uint64 FPipelineCacheFileFormatMagic = 0x5049504543414348; // PIPECACH
const uint64 FPipelineCacheTOCFileFormatMagic = 0x544F435354415232; // TOCSTAR2
const uint64 FPipelineCacheEOFFileFormatMagic = 0x454F462D4D41524B; // EOF-MARK
const RHI_API uint32 FPipelineCacheFileFormatCurrentVersion = (uint32)EPipelineCacheFileFormatVersions::AddRTPSOShaderBindingLayout;
const int32 FPipelineCacheGraphicsDescPartsNum = 67; // parser will expect this number of parts in a description string
/**
* PipelineFileCache API access
**/
static TAutoConsoleVariable<int32> CVarPSOFileCacheEnabled(
TEXT("r.ShaderPipelineCache.Enabled"),
PIPELINE_CACHE_DEFAULT_ENABLED,
TEXT("1 Enables the PipelineFileCache, 0 disables it."),
ECVF_Default | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarPSOFileCacheLogPSO(
TEXT("r.ShaderPipelineCache.LogPSO"),
PIPELINE_CACHE_DEFAULT_ENABLED,
TEXT("1 Logs new PSO entries into the file cache and allows saving."),
ECVF_Default | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarPSOFileCacheReportPSO(
TEXT("r.ShaderPipelineCache.ReportPSO"),
PIPELINE_CACHE_DEFAULT_ENABLED,
TEXT("1 reports new PSO entries via a delegate, but does not record or modify any cache file. New PSOs are reported in bulk once per frame."),
ECVF_Default | ECVF_RenderThreadSafe
);
static int32 GPSOExcludePrecachePSOsInFileCache = 0;
static FAutoConsoleVariableRef CVarPSOFileCacheExcludePrecachePSO(
TEXT("r.ShaderPipelineCache.ExcludePrecachePSO"),
GPSOExcludePrecachePSOsInFileCache,
TEXT("1 excludes saving runtime-precached graphics PSOs in the file cache, 0 (default) includes them. Excluding precached PSOs requires PSO precaching to be enabled."),
ECVF_ReadOnly
);
static int32 GPSOFileCachePrintNewPSODescriptors = 0;
static FAutoConsoleVariableRef CVarPSOFileCachePrintNewPSODescriptors(
TEXT("r.ShaderPipelineCache.PrintNewPSODescriptors"),
GPSOFileCachePrintNewPSODescriptors,
TEXT("1 prints descriptions for all new PSO entries to the log/console while 0 does not. 2 prints additional details about graphics PSO. Defaults to 0."),
ECVF_Default
);
static TAutoConsoleVariable<int32> CVarPSOFileCacheSaveUserCache(
TEXT("r.ShaderPipelineCache.SaveUserCache"),
PIPELINE_CACHE_DEFAULT_ENABLED && UE_BUILD_SHIPPING,
TEXT("If > 0 then any missed PSOs will be saved to a writable user cache file for subsequent runs to load and avoid in-game hitches. Enabled by default on macOS only."),
ECVF_Default | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarPSOFileCacheUserCacheUnusedElementRetainDays(
TEXT("r.ShaderPipelineCache.UserCacheUnusedElementRetainDays"),
30,
TEXT("The amount of time in days to keep unused PSO entries in the cache."),
ECVF_Default
);
static TAutoConsoleVariable<int32> CVarPSOFileCacheUserCacheUnusedElementCheckPeriod(
TEXT("r.ShaderPipelineCache.UserCacheUnusedElementCheckPeriod"),
-1,
TEXT("The amount of time in days between running the garbage collection on unused PSOs in the user cache. Use a negative value to disable."),
ECVF_Default
);
static TAutoConsoleVariable<int32> CVarLazyLoadShadersWhenPSOCacheIsPresent(
TEXT("r.ShaderPipelineCache.LazyLoadShadersWhenPSOCacheIsPresent"),
0,
TEXT("Non-Zero: If we load a PSO cache, then lazy load from the shader code library. This assumes the PSO cache is more or less complete. This will only work on RHIs that support the library+Hash CreateShader API (GRHISupportsLazyShaderCodeLoading == true)."),
ECVF_RenderThreadSafe);
static TAutoConsoleVariable<int32> CVarClearOSPSOFileCache(
TEXT("r.ShaderPipelineCache.ClearOSCache"),
0,
TEXT("1 Enables the OS level clear after install, 0 disables it."),
ECVF_Default | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarAlwaysGeneratePOSSOFileCache(
TEXT("r.ShaderPipelineCache.AlwaysGenerateOSCache"),
1,
TEXT("1 generates the cache every run, 0 generates it only when it is missing."),
ECVF_Default | ECVF_RenderThreadSafe
);
FRWLock FPipelineFileCacheManager::FileCacheLock;
TMap<FString, TUniquePtr< class FPipelineCacheFile>> FPipelineFileCacheManager::FileCacheMap;
TMap<FGuid, FString> FPipelineFileCacheManager::GameGuidToCacheKey;
TMap<uint32, FPSOUsageData> FPipelineFileCacheManager::RunTimeToPSOUsage;
TMap<uint32, FPSOUsageData> FPipelineFileCacheManager::NewPSOUsage;
TMap<uint32, FPipelineStateStats*> FPipelineFileCacheManager::Stats;
TSet<FPipelineCacheFileFormatPSO> FPipelineFileCacheManager::NewPSOs;
TMpscQueue<FPipelineCacheFileFormatPSO> FPipelineFileCacheManager::NewPSOsToReport;
TSet<uint32> FPipelineFileCacheManager::NewPSOHashes;
uint32 FPipelineFileCacheManager::NumNewPSOs;
FString FPipelineFileCacheManager::UserCacheKey;
FPipelineFileCacheManager::PSOOrder FPipelineFileCacheManager::RequestedOrder = FPipelineFileCacheManager::PSOOrder::MostToLeastUsed;
bool FPipelineFileCacheManager::FileCacheEnabled = false;
FPipelineFileCacheManager::FPipelineStateLoggedEvent FPipelineFileCacheManager::PSOLoggedEvent;
uint64 FPipelineFileCacheManager::GameUsageMask = 0;
bool FPipelineFileCacheManager::GameUsageMaskSet = false;
bool FPipelineFileCacheManager::LogNewPSOsToConsoleAndCSV = true;
static int64 GetCurrentUnixTime()
{
return FDateTime::UtcNow().ToUnixTimestamp();
}
bool DefaultPSOMaskComparisonFunction(uint64 ReferenceMask, uint64 PSOMask)
{
return (ReferenceMask & PSOMask) == ReferenceMask;
}
FPSOMaskComparisonFn FPipelineFileCacheManager::MaskComparisonFn = DefaultPSOMaskComparisonFunction;
static inline bool IsReferenceMaskSet(uint64 ReferenceMask, uint64 PSOMask)
{
return (ReferenceMask & PSOMask) == ReferenceMask;
}
#if PLATFORM_WINDOWS
FRHIShader::~FRHIShader()
{
if (InUseByPSOCompilation > 0)
{
UE_LOG(LogRHI, Fatal, TEXT("FRHIShader with hash: %s and Frequency: %d still in use by PSO compilation when being destroyed"), *Hash.ToString(), Frequency);
}
}
void FRHIShader::SetInUseByPSOCompilation(bool bInUse)
{
if (bInUse)
{
FPlatformAtomics::InterlockedIncrement(&InUseByPSOCompilation);
}
else
{
check(InUseByPSOCompilation > 0);
FPlatformAtomics::InterlockedDecrement(&InUseByPSOCompilation);
}
}
#endif // PLATFORM_WINDOWS
void FRHIComputeShader::UpdateStats()
{
FPipelineStateStats::UpdateStats(Stats);
}
void FPipelineStateStats::UpdateStats(FPipelineStateStats* Stats)
{
if (Stats)
{
FPlatformAtomics::InterlockedExchange(&Stats->LastFrameUsed, GFrameCounter);
FPlatformAtomics::InterlockedIncrement(&Stats->TotalBindCount);
FPlatformAtomics::InterlockedCompareExchange(&Stats->FirstFrameUsed, GFrameCounter, -1);
}
}
struct FPipelineCacheFileFormatHeader
{
uint64 Magic; // Sanity check
uint32 Version; // File version must match engine version, otherwise we ignore
uint32 GameVersion; // Same as above but game specific code can invalidate
TEnumAsByte<EShaderPlatform> Platform; // The shader platform for all referenced PSOs.
FGuid Guid; // Guid to identify the file uniquely
uint64 TableOffset; // absolute file offset to TOC
int64 LastGCUnixTime; // Last time that the cache was scanned to remove out of date elements.
friend FArchive& operator<<(FArchive& Ar, FPipelineCacheFileFormatHeader& Info)
{
Ar << Info.Magic;
Ar << Info.Version;
Ar << Info.GameVersion;
Ar << Info.Platform;
Ar << Info.Guid;
Ar << Info.TableOffset;
if (Info.Version >= (uint32)EPipelineCacheFileFormatVersions::LastUsedTime)
{
Ar << Info.LastGCUnixTime;
}
return Ar;
}
};
FArchive& operator<<( FArchive& Ar, FPipelineStateStats& Info )
{
Ar << Info.FirstFrameUsed;
Ar << Info.LastFrameUsed;
Ar << Info.CreateCount;
Ar << Info.TotalBindCount;
Ar << Info.PSOHash;
return Ar;
}
/**
* PipelineFileCache MetaData Engine Flags
**/
const uint16 FPipelineCacheFlagInvalidPSO = 1 << 0;
struct FPipelineCacheFileFormatPSOMetaData
{
FPipelineCacheFileFormatPSOMetaData()
: FileOffset(0)
, UsageMask(0)
, LastUsedUnixTime(0)
, EngineFlags(0)
{
}
~FPipelineCacheFileFormatPSOMetaData()
{
}
uint64 FileOffset;
uint64 FileSize;
FGuid FileGuid;
FPipelineStateStats Stats;
TSet<FSHAHash> Shaders;
uint64 UsageMask;
int64 LastUsedUnixTime;
uint16 EngineFlags;
void AddShaders(const FPipelineCacheFileFormatPSO& NewEntry)
{
switch (NewEntry.Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
INC_DWORD_STAT(STAT_SerializedComputePipelineStateCount);
Shaders.Add(NewEntry.ComputeDesc.ComputeShader);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
INC_DWORD_STAT(STAT_SerializedGraphicsPipelineStateCount);
if (NewEntry.GraphicsDesc.VertexShader != FSHAHash())
Shaders.Add(NewEntry.GraphicsDesc.VertexShader);
if (NewEntry.GraphicsDesc.FragmentShader != FSHAHash())
Shaders.Add(NewEntry.GraphicsDesc.FragmentShader);
if (NewEntry.GraphicsDesc.GeometryShader != FSHAHash())
Shaders.Add(NewEntry.GraphicsDesc.GeometryShader);
if (NewEntry.GraphicsDesc.MeshShader != FSHAHash())
Shaders.Add(NewEntry.GraphicsDesc.MeshShader);
if (NewEntry.GraphicsDesc.AmplificationShader != FSHAHash())
Shaders.Add(NewEntry.GraphicsDesc.AmplificationShader);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
INC_DWORD_STAT(STAT_SerializedRayTracingPipelineStateCount);
Shaders.Add(NewEntry.RayTracingDesc.ShaderHash);
break;
}
default:
{
check(false);
break;
}
}
}
friend FArchive& operator<<(FArchive& Ar, FPipelineCacheFileFormatPSOMetaData& Info)
{
Ar << Info.FileOffset;
Ar << Info.FileSize;
// if FileGuid is zeroed out (a frequent case), don't write all 16 bytes of it
uint8 ArchiveFullGuid = 1;
if (Ar.GameNetVer() == (uint32)EPipelineCacheFileFormatVersions::PatchSizeReduction_NoDuplicatedGuid)
{
if (Ar.IsSaving())
{
ArchiveFullGuid = (Info.FileGuid != FGuid()) ? 1 : 0;
}
Ar << ArchiveFullGuid;
}
if (ArchiveFullGuid != 0)
{
Ar << Info.FileGuid;
}
Ar << Info.Stats;
if (Ar.GameNetVer() == (uint32)EPipelineCacheFileFormatVersions::LibraryID)
{
TSet<uint32> IDs;
Ar << IDs;
}
else if (Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::ShaderMetaData)
{
Ar << Info.Shaders;
}
if(Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::PSOUsageMask)
{
Ar << Info.UsageMask;
}
if(Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::EngineFlags)
{
Ar << Info.EngineFlags;
}
if (Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::LastUsedTime)
{
Ar << Info.LastUsedUnixTime;
}
return Ar;
}
};
RHI_API FArchive& operator<<(FArchive& Ar, FPipelineFileCacheRasterizerState& RasterizerStateInitializer)
{
Ar << RasterizerStateInitializer.DepthBias;
Ar << RasterizerStateInitializer.SlopeScaleDepthBias;
Ar << RasterizerStateInitializer.FillMode;
Ar << RasterizerStateInitializer.CullMode;
Ar << RasterizerStateInitializer.DepthClipMode;
Ar << RasterizerStateInitializer.bAllowMSAA;
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::RemovingLineAA)
{
bool bEnableLineAA = false;
Ar << bEnableLineAA;
}
return Ar;
}
FString FPipelineFileCacheRasterizerState::ToString() const
{
return FString::Printf(TEXT("<%f %f %u %u %u %u>")
, DepthBias
, SlopeScaleDepthBias
, uint32(FillMode)
, uint32(CullMode)
, uint32(DepthClipMode)
, uint32(!!bAllowMSAA)
);
}
void FPipelineFileCacheRasterizerState::FromString(const FStringView& Src)
{
constexpr int32 PartCount = 6;
TArray<FStringView, TInlineAllocator<PartCount>> Parts;
UE::String::ParseTokensMultiple(Src.TrimStartAndEnd(), {TEXT('\r'), TEXT('\n'), TEXT('\t'), TEXT('<'), TEXT('>'), TEXT(' ')},
[&Parts](FStringView Part) { if (!Part.IsEmpty()) { Parts.Add(Part); } });
check(Parts.Num() == PartCount && sizeof(FillMode) == 1 && sizeof(CullMode) == 1 && sizeof(DepthClipMode) == 1 && sizeof(bAllowMSAA) == 1); //not a very robust parser
const FStringView* PartIt = Parts.GetData();
LexFromString(DepthBias, *PartIt++);
LexFromString(SlopeScaleDepthBias, *PartIt++);
LexFromString((uint8&)FillMode, *PartIt++);
LexFromString((uint8&)CullMode, *PartIt++);
LexFromString((uint8&)DepthClipMode, *PartIt++);
LexFromString((uint8&)bAllowMSAA, *PartIt++);
check(Parts.GetData() + PartCount == PartIt);
}
FString FPipelineCacheFileFormatPSO::ComputeDescriptor::ToString() const
{
return ComputeShader.ToString();
}
void FPipelineCacheFileFormatPSO::ComputeDescriptor::AddToReadableString(TReadableStringBuilder& OutBuilder) const
{
OutBuilder << TEXT(" CS:");
OutBuilder << ComputeShader.ToString();
}
void FPipelineCacheFileFormatPSO::ComputeDescriptor::FromString(const FStringView& Src)
{
ComputeShader.FromString(Src.TrimStartAndEnd());
}
FString FPipelineCacheFileFormatPSO::ComputeDescriptor::HeaderLine()
{
return FString(TEXT("ComputeShader"));
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::ShadersToString() const
{
FString Result;
Result += FString::Printf(TEXT("%s,%s,%s,%s,%s")
, *VertexShader.ToString()
, *FragmentShader.ToString()
, *GeometryShader.ToString()
, *MeshShader.ToString()
, *AmplificationShader.ToString()
);
return Result;
}
void FPipelineCacheFileFormatPSO::GraphicsDescriptor::AddShadersToReadableString(TReadableStringBuilder& OutBuilder) const
{
if (VertexShader != FSHAHash())
{
OutBuilder << TEXT(" VS:");
OutBuilder << VertexShader;
}
if (MeshShader != FSHAHash())
{
OutBuilder << TEXT(" MS:");
OutBuilder << MeshShader;
}
if (GeometryShader != FSHAHash())
{
OutBuilder << TEXT(" GS:");
OutBuilder << GeometryShader;
}
if (AmplificationShader != FSHAHash())
{
OutBuilder << TEXT(" AS:");
OutBuilder << AmplificationShader;
}
if (FragmentShader != FSHAHash())
{
OutBuilder << TEXT(" PS:");
OutBuilder << FragmentShader;
}
}
void FPipelineCacheFileFormatPSO::GraphicsDescriptor::ShadersFromString(const FStringView& Src)
{
constexpr int32 PartCount = 5;
TArray<FStringView, TInlineAllocator<PartCount>> Parts;
UE::String::ParseTokens(Src.TrimStartAndEnd(), TEXT(','), [&Parts](FStringView Part) { Parts.Add(Part); });
check(Parts.Num() == PartCount); //not a very robust parser
const FStringView* PartIt = Parts.GetData();
VertexShader.FromString(*PartIt++);
FragmentShader.FromString(*PartIt++);
GeometryShader.FromString(*PartIt++);
MeshShader.FromString(*PartIt++);
AmplificationShader.FromString(*PartIt++);
check(Parts.GetData() + PartCount == PartIt);
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::ShaderHeaderLine()
{
return FString(TEXT("VertexShader,FragmentShader,GeometryShader,MeshShader,AmplificationShader"));
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::StateToString() const
{
FString Result;
Result += FString::Printf(TEXT("%s,%s,%s,")
, *BlendState.ToString()
, *RasterizerState.ToString()
, *DepthStencilState.ToString()
);
Result += FString::Printf(TEXT("%d,%d,%lld,")
, MSAASamples
, uint32(DepthStencilFormat)
, DepthStencilFlags
);
Result += FString::Printf(TEXT("%d,%d,%d,%d,%d,")
, uint32(DepthLoad)
, uint32(StencilLoad)
, uint32(DepthStore)
, uint32(StencilStore)
, uint32(PrimitiveType)
);
Result += FString::Printf(TEXT("%d,")
, RenderTargetsActive
);
for (int32 Index = 0; Index < MaxSimultaneousRenderTargets; Index++)
{
Result += FString::Printf(TEXT("%d,%lld,%d,%d,")
, uint32(RenderTargetFormats[Index])
, RenderTargetFlags[Index]
, 0/*Load*/
, 0/*Store*/
);
}
Result += FString::Printf(TEXT("%d,%d,")
, uint32(SubpassHint)
, uint32(SubpassIndex)
);
Result += FString::Printf(TEXT("%d,%d,")
, uint32(MultiViewCount)
, uint32(bHasFragmentDensityAttachment)
);
Result += FString::Printf(TEXT("%d,")
, uint32(bDepthBounds)
);
FVertexElement NullVE;
FMemory::Memzero(NullVE);
Result += FString::Printf(TEXT("%d,")
, VertexDescriptor.Num()
);
for (int32 Index = 0; Index < MaxVertexElementCount; Index++)
{
if (Index < VertexDescriptor.Num())
{
Result += FString::Printf(TEXT("%s,")
, *VertexDescriptor[Index].ToString()
);
}
else
{
Result += FString::Printf(TEXT("%s,")
, *NullVE.ToString()
);
}
}
return Result.Left(Result.Len() - 1); // remove trailing comma
}
void FPipelineCacheFileFormatPSO::GraphicsDescriptor::AddStateToReadableString(TReadableStringBuilder& OutBuilder) const
{
OutBuilder << TEXT(" BS:");
OutBuilder << BlendState.ToString();
OutBuilder << TEXT(" RS:");
OutBuilder << RasterizerState.ToString();
OutBuilder << TEXT(" DSS:");
OutBuilder << DepthStencilState.ToString();
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" NumMSAA:");
OutBuilder << MSAASamples;
OutBuilder << TEXT(" DSfmt:");
OutBuilder << uint32(DepthStencilFormat);
OutBuilder << TEXT(" DSflags:");
OutBuilder << uint64(DepthStencilFlags);
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" DL:");
OutBuilder << uint32(DepthLoad);
OutBuilder << TEXT(" SL:");
OutBuilder << uint32(StencilLoad);
OutBuilder << TEXT(" DS:");
OutBuilder << uint32(DepthStore);
OutBuilder << TEXT(" SS:");
OutBuilder << uint32(StencilStore);
OutBuilder << TEXT(" PT:");
OutBuilder << uint32(PrimitiveType);
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" RTA ");
OutBuilder << RenderTargetsActive;
OutBuilder << TEXT("\n");
if (RenderTargetsActive)
{
OutBuilder << TEXT(" ");
for (uint32 Index = 0; Index < RenderTargetsActive; Index++)
{
OutBuilder << TEXT(" RT");
OutBuilder << Index,
OutBuilder << TEXT(":fmt=");
OutBuilder << uint32(RenderTargetFormats[Index]);
OutBuilder << TEXT(" flg=");
OutBuilder << uint64(RenderTargetFlags[Index]);
}
OutBuilder << TEXT("\n");
}
OutBuilder << TEXT(" SuH:");
OutBuilder << uint32(SubpassHint);
OutBuilder << TEXT(" SuI:");
OutBuilder << uint32(SubpassIndex);
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" MVC:");
OutBuilder << MultiViewCount;
OutBuilder << TEXT(" HasFDM:");
OutBuilder << bHasFragmentDensityAttachment;
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" DB:");
OutBuilder << bDepthBounds;
OutBuilder << TEXT("\n");
OutBuilder << TEXT(" NumVE ");
OutBuilder << VertexDescriptor.Num();
OutBuilder << TEXT("\n");
for (int32 Index = 0; Index < VertexDescriptor.Num(); Index++)
{
OutBuilder << TEXT(" ");
OutBuilder << Index;
OutBuilder << TEXT(":");
OutBuilder << VertexDescriptor[Index].ToString();
}
}
bool FPipelineCacheFileFormatPSO::GraphicsDescriptor::StateFromString(const FStringView& Src)
{
static_assert(sizeof(EPixelFormat) == 1);
static_assert(sizeof(ERenderTargetLoadAction) == 1);
static_assert(sizeof(ERenderTargetStoreAction) == 1);
static_assert(sizeof(DepthLoad) == 1);
static_assert(sizeof(DepthStore) == 1);
static_assert(sizeof(StencilLoad) == 1);
static_assert(sizeof(StencilStore) == 1);
static_assert(sizeof(PrimitiveType) == 4);
constexpr int32 PartCount = FPipelineCacheGraphicsDescPartsNum;
TArray<FStringView, TInlineAllocator<PartCount>> Parts;
UE::String::ParseTokens(Src.TrimStartAndEnd(), TEXT(','), [&Parts](FStringView Part) { Parts.Add(Part); });
// check if we have expected number of parts
if (Parts.Num() != PartCount)
{
// instead of crashing let caller handle this case
return false;
}
const FStringView* PartIt = Parts.GetData();
const FStringView* PartEnd = PartIt + PartCount;
check(PartEnd - PartIt >= 3); //not a very robust parser
BlendState.FromString(*PartIt++);
RasterizerState.FromString(*PartIt++);
DepthStencilState.FromString(*PartIt++);
check(PartEnd - PartIt >= 3); //not a very robust parser
LexFromString(MSAASamples, *PartIt++);
LexFromString((uint32&)DepthStencilFormat, *PartIt++);
ETextureCreateFlags DSFlags;
LexFromString(DSFlags, *PartIt++);
DepthStencilFlags = ReduceDSFlags(DSFlags);
check(PartEnd - PartIt >= 5); //not a very robust parser
LexFromString((uint32&)DepthLoad, *PartIt++);
LexFromString((uint32&)StencilLoad, *PartIt++);
LexFromString((uint32&)DepthStore, *PartIt++);
LexFromString((uint32&)StencilStore, *PartIt++);
LexFromString((uint32&)PrimitiveType, *PartIt++);
check(PartEnd - PartIt >= 1); //not a very robust parser
LexFromString(RenderTargetsActive, *PartIt++);
for (int32 Index = 0; Index < MaxSimultaneousRenderTargets; Index++)
{
check(PartEnd - PartIt >= 4); //not a very robust parser
LexFromString((uint8&)(RenderTargetFormats[Index]), *PartIt++);
ETextureCreateFlags RTFlags;
LexFromString(RTFlags, *PartIt++);
// going forward, the flags will already be reduced when logging the PSOs to disk. However as of 2021-06-17 there are still old stable cache files in existence that have flags recorded as is
RenderTargetFlags[Index] = ReduceRTFlags(RTFlags);
uint8 Load, Store;
LexFromString(Load, *PartIt++);
LexFromString(Store, *PartIt++);
}
// parse sub-pass information
{
uint32 LocalSubpassHint = 0;
uint32 LocalSubpassIndex = 0;
check(PartEnd - PartIt >= 2);
LexFromString(LocalSubpassHint, *PartIt++);
LexFromString(LocalSubpassIndex, *PartIt++);
SubpassHint = LocalSubpassHint;
SubpassIndex = LocalSubpassIndex;
}
// parse multiview and FDM information
{
uint32 LocalMultiViewCount = 0;
uint32 LocalHasFDM = 0;
check(PartEnd - PartIt >= 2);
LexFromString(LocalMultiViewCount, *PartIt++);
LexFromString(LocalHasFDM, *PartIt++);
MultiViewCount = (uint8)LocalMultiViewCount;
bHasFragmentDensityAttachment = (bool)LocalHasFDM;
}
// parse depth bounds
{
uint32 DepthBounds = 0;
check(PartEnd - PartIt >= 1);
LexFromString(DepthBounds, *PartIt++);
bDepthBounds = (bool)DepthBounds;
}
check(PartEnd - PartIt >= 1); //not a very robust parser
int32 VertDescNum = 0;
LexFromString(VertDescNum, *PartIt++);
check(VertDescNum >= 0 && VertDescNum <= MaxVertexElementCount);
VertexDescriptor.Empty(VertDescNum);
VertexDescriptor.AddZeroed(VertDescNum);
check(PartEnd - PartIt == MaxVertexElementCount); //not a very robust parser
for (int32 Index = 0; Index < VertDescNum; Index++)
{
VertexDescriptor[Index].FromString(*PartIt++);
}
check(PartIt + MaxVertexElementCount == PartEnd + VertDescNum);
VertexDescriptor.Sort([](FVertexElement const& A, FVertexElement const& B)
{
if (A.StreamIndex < B.StreamIndex)
{
return true;
}
if (A.StreamIndex > B.StreamIndex)
{
return false;
}
if (A.Offset < B.Offset)
{
return true;
}
if (A.Offset > B.Offset)
{
return false;
}
if (A.AttributeIndex < B.AttributeIndex)
{
return true;
}
if (A.AttributeIndex > B.AttributeIndex)
{
return false;
}
return false;
});
return true;
}
ETextureCreateFlags FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceRTFlags(ETextureCreateFlags InFlags)
{
// We care about flags that influence RT formats (which is the only thing the underlying API cares about).
// In most RHIs, the format is only influenced by TexCreate_SRGB. D3D12 additionally uses TexCreate_Shared in its format selection logic.
return (InFlags & FGraphicsPipelineStateInitializer::RelevantRenderTargetFlagMask);
}
ETextureCreateFlags FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceDSFlags(ETextureCreateFlags InFlags)
{
return (InFlags & FGraphicsPipelineStateInitializer::RelevantDepthStencilFlagMask);
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::StateHeaderLine()
{
FString Result;
Result += FString::Printf(TEXT("%s,%s,%s,")
, TEXT("BlendState")
, TEXT("RasterizerState")
, TEXT("DepthStencilState")
);
Result += FString::Printf(TEXT("%s,%s,%s,")
, TEXT("MSAASamples")
, TEXT("DepthStencilFormat")
, TEXT("DepthStencilFlags")
);
Result += FString::Printf(TEXT("%s,%s,%s,%s,%s,")
, TEXT("DepthLoad")
, TEXT("StencilLoad")
, TEXT("DepthStore")
, TEXT("StencilStore")
, TEXT("PrimitiveType")
);
Result += FString::Printf(TEXT("%s,")
, TEXT("RenderTargetsActive")
);
for (int32 Index = 0; Index < MaxSimultaneousRenderTargets; Index++)
{
Result += FString::Printf(TEXT("%s%d,%s%d,%s%d,%s%d,")
, TEXT("RenderTargetFormats"), Index
, TEXT("RenderTargetFlags"), Index
, TEXT("RenderTargetsLoad"), Index
, TEXT("RenderTargetsStore"), Index
);
}
Result += FString::Printf(TEXT("%s,%s,")
, TEXT("SubpassHint")
, TEXT("SubpassIndex")
);
Result += FString::Printf(TEXT("%s,%s,")
, TEXT("MultiViewCount")
, TEXT("bHasFDMAttachment")
);
Result += FString::Printf(TEXT("%s,")
, TEXT("VertexDescriptorNum")
);
for (int32 Index = 0; Index < MaxVertexElementCount; Index++)
{
Result += FString::Printf(TEXT("%s%d,")
, TEXT("VertexDescriptor"), Index
);
}
return Result.Left(Result.Len() - 1); // remove trailing comma
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::ToString() const
{
return FString::Printf(TEXT("%s,%s"), *ShadersToString(), *StateToString());
}
void FPipelineCacheFileFormatPSO::GraphicsDescriptor::AddToReadableString(TReadableStringBuilder& OutBuilder) const
{
AddShadersToReadableString(OutBuilder);
OutBuilder << TEXT("\n");
AddStateToReadableString(OutBuilder);
OutBuilder << TEXT("\n");
}
bool FPipelineCacheFileFormatPSO::GraphicsDescriptor::FromString(const FStringView& Src)
{
constexpr int32 NumShaderParts = 5;
int32 StateOffset = 0;
for (int32 CommaCount = 0; CommaCount < NumShaderParts; ++CommaCount)
{
int32 CommaOffset = 0;
bool FoundComma = Src.RightChop(StateOffset).FindChar(TEXT(','), CommaOffset);
check(FoundComma);
StateOffset += CommaOffset + 1;
}
ShadersFromString(Src.Left(StateOffset - 1));
return StateFromString(Src.RightChop(StateOffset));
}
FString FPipelineCacheFileFormatPSO::GraphicsDescriptor::HeaderLine()
{
return FString::Printf(TEXT("%s,%s"), *ShaderHeaderLine(), *StateHeaderLine());
}
FString FPipelineCacheFileFormatPSO::CommonHeaderLine()
{
return TEXT("BindCount,UsageMask");
}
FString FPipelineCacheFileFormatPSO::CommonToString() const
{
uint64 Mask = 0;
int64 Count = 0;
#if PSO_COOKONLY_DATA
Mask = UsageMask;
Count = BindCount;
#endif
return FString::Printf(TEXT("\"%" INT64_FMT ",%" UINT64_FMT "\""), Count, Mask);
}
FString FPipelineCacheFileFormatPSO::ToStringReadable() const
{
TReadableStringBuilder Builder;
Builder << TEXT("PSO hash ");
Builder << GetTypeHash(*this);
#if PSO_COOKONLY_DATA
Builder << TEXT(" mask ");
Builder << UsageMask;
Builder << TEXT(" bindc ");
Builder << BindCount;
#endif
Builder << TEXT("\n");
if (Type == DescriptorType::Graphics)
{
GraphicsDesc.AddToReadableString(Builder);
}
else if (Type == DescriptorType::Compute)
{
ComputeDesc.AddToReadableString(Builder);
}
else if (Type == DescriptorType::RayTracing)
{
RayTracingDesc.AddToReadableString(Builder);
}
else
{
Builder << TEXT(" Unknown PSO type ");
Builder << static_cast<int32>(Type);
}
return FString(FStringView(Builder));
}
void FPipelineCacheFileFormatPSO::CommonFromString(const FStringView& Src)
{
#if PSO_COOKONLY_DATA
TArray<FStringView, TInlineAllocator<2>> Parts;
UE::String::ParseTokens(Src.TrimStartAndEnd(), TEXT(','), [&Parts](FStringView Part) { Parts.Add(Part); });
if (Parts.Num() == 1)
{
LexFromString(UsageMask, Parts[0]);
}
else if(Parts.Num() > 1)
{
LexFromString(BindCount, Parts[0]);
LexFromString(UsageMask, Parts[1]);
}
#endif
}
bool FPipelineCacheFileFormatPSO::Verify() const
{
if(Type == DescriptorType::Compute)
{
return ComputeDesc.ComputeShader != FSHAHash();
}
else if(Type == DescriptorType::Graphics)
{
if (GraphicsDesc.VertexShader == FSHAHash() && GraphicsDesc.MeshShader == FSHAHash())
{
// No vertex or mesh shader - no graphics - nothing else matters
return false;
}
#if PLATFORM_SUPPORTS_MESH_SHADERS
if (GraphicsDesc.MeshShader != FSHAHash())
{
// this check is also done in commandlets, which don't set RHI settings properly. Exempt them.
if (!IsRunningCommandlet() && !GRHISupportsMeshShadersTier0)
{
// do not allow precompilation of mesh shaders if runtime doesn't support them
return false;
}
if (GraphicsDesc.VertexShader != FSHAHash())
{
// Vertex shader and mesh shader are mutually exclusive
return false;
}
if (GraphicsDesc.VertexDescriptor.Num() > 0)
{
// mesh shader should not have descriptors
return false;
}
}
#endif
#if PLATFORM_SUPPORTS_GEOMETRY_SHADERS
// Is there anything to actually test here?
#endif
if( GraphicsDesc.RenderTargetsActive > MaxSimultaneousRenderTargets ||
GraphicsDesc.MSAASamples > 16 ||
(uint32)GraphicsDesc.PrimitiveType >= (uint32)EPrimitiveType::PT_Num ||
(uint32)GraphicsDesc.DepthStencilFormat >= (uint32)EPixelFormat::PF_MAX ||
(uint8)GraphicsDesc.DepthLoad >= (uint8)ERenderTargetLoadAction::Num ||
(uint8)GraphicsDesc.StencilLoad >= (uint8)ERenderTargetLoadAction::Num ||
(uint8)GraphicsDesc.DepthStore >= (uint8)ERenderTargetStoreAction::Num ||
(uint8)GraphicsDesc.StencilStore >= (uint8)ERenderTargetStoreAction::Num )
{
return false;
}
for(uint32 rt = 0;rt < GraphicsDesc.RenderTargetsActive;++rt)
{
if((uint32)GraphicsDesc.RenderTargetFormats[rt] >= (uint32)EPixelFormat::PF_MAX)
{
return false;
}
if( GraphicsDesc.BlendState.RenderTargets[rt].ColorBlendOp >= EBlendOperation::EBlendOperation_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].AlphaBlendOp >= EBlendOperation::EBlendOperation_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].ColorSrcBlend >= EBlendFactor::EBlendFactor_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].ColorDestBlend >= EBlendFactor::EBlendFactor_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].AlphaSrcBlend >= EBlendFactor::EBlendFactor_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].AlphaDestBlend >= EBlendFactor::EBlendFactor_Num ||
GraphicsDesc.BlendState.RenderTargets[rt].ColorWriteMask > 0xf)
{
return false;
}
}
if( (uint8)GraphicsDesc.RasterizerState.FillMode >= (uint8)ERasterizerFillMode::ERasterizerFillMode_Num ||
(uint8)GraphicsDesc.RasterizerState.CullMode >= (uint8)ERasterizerCullMode_Num)
{
return false;
}
if( (uint8)GraphicsDesc.DepthStencilState.DepthTest >= (uint8)ECompareFunction::ECompareFunction_Num ||
(uint8)GraphicsDesc.DepthStencilState.FrontFaceStencilTest >= (uint8)ECompareFunction::ECompareFunction_Num ||
(uint8)GraphicsDesc.DepthStencilState.BackFaceStencilTest >= (uint8)ECompareFunction::ECompareFunction_Num ||
(uint8)GraphicsDesc.DepthStencilState.FrontFaceStencilFailStencilOp >= (uint8)EStencilOp::EStencilOp_Num ||
(uint8)GraphicsDesc.DepthStencilState.FrontFaceDepthFailStencilOp >= (uint8)EStencilOp::EStencilOp_Num ||
(uint8)GraphicsDesc.DepthStencilState.FrontFacePassStencilOp >= (uint8)EStencilOp::EStencilOp_Num ||
(uint8)GraphicsDesc.DepthStencilState.BackFaceStencilFailStencilOp >= (uint8)EStencilOp::EStencilOp_Num ||
(uint8)GraphicsDesc.DepthStencilState.BackFaceDepthFailStencilOp >= (uint8)EStencilOp::EStencilOp_Num ||
(uint8)GraphicsDesc.DepthStencilState.BackFacePassStencilOp >= (uint8)EStencilOp::EStencilOp_Num)
{
return false;
}
uint32 ElementCount = (uint32)GraphicsDesc.VertexDescriptor.Num();
for (uint32 i = 0; i < ElementCount;++i)
{
if(GraphicsDesc.VertexDescriptor[i].Type >= EVertexElementType::VET_MAX)
{
return false;
}
}
return true;
}
else if (Type == DescriptorType::RayTracing)
{
return RayTracingDesc.ShaderHash != FSHAHash() &&
RayTracingDesc.Frequency >= SF_RayGen &&
RayTracingDesc.Frequency <= SF_RayCallable;
}
else
{
checkNoEntry();
}
return false;
}
/**
* FPipelineCacheFileFormatPSO
**/
/*friend*/ uint32 GetTypeHash(const FPipelineCacheFileFormatPSO &Key)
{
uint32 KeyHash = GetTypeHash(Key.Type);
switch(Key.Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
KeyHash ^= GetTypeHash(Key.ComputeDesc.ComputeShader);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RenderTargetsActive, sizeof(Key.GraphicsDesc.RenderTargetsActive), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.MSAASamples, sizeof(Key.GraphicsDesc.MSAASamples), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.PrimitiveType, sizeof(Key.GraphicsDesc.PrimitiveType), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.VertexShader.Hash, sizeof(Key.GraphicsDesc.VertexShader.Hash), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.FragmentShader.Hash, sizeof(Key.GraphicsDesc.FragmentShader.Hash), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.GeometryShader.Hash, sizeof(Key.GraphicsDesc.GeometryShader.Hash), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.MeshShader.Hash, sizeof(Key.GraphicsDesc.MeshShader.Hash), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.AmplificationShader.Hash, sizeof(Key.GraphicsDesc.AmplificationShader.Hash), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilFormat, sizeof(Key.GraphicsDesc.DepthStencilFormat), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilFlags, sizeof(Key.GraphicsDesc.DepthStencilFlags), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthLoad, sizeof(Key.GraphicsDesc.DepthLoad), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.StencilLoad, sizeof(Key.GraphicsDesc.StencilLoad), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStore, sizeof(Key.GraphicsDesc.DepthStore), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.StencilStore, sizeof(Key.GraphicsDesc.StencilStore), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.bUseIndependentRenderTargetBlendStates, sizeof(Key.GraphicsDesc.BlendState.bUseIndependentRenderTargetBlendStates), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.bUseAlphaToCoverage, sizeof(Key.GraphicsDesc.BlendState.bUseAlphaToCoverage), KeyHash);
for( uint32 i = 0; i < MaxSimultaneousRenderTargets; i++ )
{
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].ColorBlendOp, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].ColorBlendOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].ColorSrcBlend, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].ColorSrcBlend), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].ColorDestBlend, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].ColorDestBlend), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].ColorWriteMask, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].ColorWriteMask), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaBlendOp, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaBlendOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaSrcBlend, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaSrcBlend), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaDestBlend, sizeof(Key.GraphicsDesc.BlendState.RenderTargets[i].AlphaDestBlend), KeyHash);
}
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RenderTargetFormats, sizeof(Key.GraphicsDesc.RenderTargetFormats), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RenderTargetFlags, sizeof(Key.GraphicsDesc.RenderTargetFlags), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.SubpassHint, sizeof(Key.GraphicsDesc.SubpassHint), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.SubpassIndex, sizeof(Key.GraphicsDesc.SubpassIndex), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.MultiViewCount, sizeof(Key.GraphicsDesc.MultiViewCount), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.bHasFragmentDensityAttachment, sizeof(Key.GraphicsDesc.bHasFragmentDensityAttachment), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.bDepthBounds, sizeof(Key.GraphicsDesc.bDepthBounds), KeyHash);
for(auto const& Element : Key.GraphicsDesc.VertexDescriptor)
{
KeyHash = FCrc::MemCrc32(&Element, sizeof(FVertexElement), KeyHash);
}
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RasterizerState.DepthBias, sizeof(Key.GraphicsDesc.RasterizerState.DepthBias), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RasterizerState.SlopeScaleDepthBias, sizeof(Key.GraphicsDesc.RasterizerState.SlopeScaleDepthBias), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RasterizerState.FillMode, sizeof(Key.GraphicsDesc.RasterizerState.FillMode), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RasterizerState.CullMode, sizeof(Key.GraphicsDesc.RasterizerState.CullMode), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.RasterizerState.bAllowMSAA, sizeof(Key.GraphicsDesc.RasterizerState.bAllowMSAA), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.bEnableDepthWrite, sizeof(Key.GraphicsDesc.DepthStencilState.bEnableDepthWrite), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.DepthTest, sizeof(Key.GraphicsDesc.DepthStencilState.DepthTest), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.bEnableFrontFaceStencil, sizeof(Key.GraphicsDesc.DepthStencilState.bEnableFrontFaceStencil), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.FrontFaceStencilTest, sizeof(Key.GraphicsDesc.DepthStencilState.FrontFaceStencilTest), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.FrontFaceStencilFailStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.FrontFaceStencilFailStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.FrontFaceDepthFailStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.FrontFaceDepthFailStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.FrontFacePassStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.FrontFacePassStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.bEnableBackFaceStencil, sizeof(Key.GraphicsDesc.DepthStencilState.bEnableBackFaceStencil), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.BackFaceStencilTest, sizeof(Key.GraphicsDesc.DepthStencilState.BackFaceStencilTest), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.BackFaceStencilFailStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.BackFaceStencilFailStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.BackFaceDepthFailStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.BackFaceDepthFailStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.BackFacePassStencilOp, sizeof(Key.GraphicsDesc.DepthStencilState.BackFacePassStencilOp), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.StencilReadMask, sizeof(Key.GraphicsDesc.DepthStencilState.StencilReadMask), KeyHash);
KeyHash = FCrc::MemCrc32(&Key.GraphicsDesc.DepthStencilState.StencilWriteMask, sizeof(Key.GraphicsDesc.DepthStencilState.StencilWriteMask), KeyHash);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
KeyHash ^= GetTypeHash(Key.RayTracingDesc);
break;
}
default:
{
checkNoEntry();
}
}
return KeyHash;
}
/*friend*/ FArchive& operator<<( FArchive& Ar, FPipelineCacheFileFormatPSO& Info )
{
Ar << Info.Type;
#if PSO_COOKONLY_DATA
/* Ignore: Ar << Info.UsageMask; during serialization */
/* Ignore: Ar << Info.BindCoun during serialization*/
#endif
switch (Info.Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
Ar << Info.ComputeDesc.ComputeShader;
if (Ar.GameNetVer() == (uint32)EPipelineCacheFileFormatVersions::LibraryID)
{
uint32 ID = 0;
Ar << ID;
}
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
Ar << Info.GraphicsDesc.VertexShader;
Ar << Info.GraphicsDesc.FragmentShader;
Ar << Info.GraphicsDesc.GeometryShader;
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::RemovingTessellationShaders)
{
FSHAHash HullShader;
Ar << HullShader;
FSHAHash DomainShader;
Ar << DomainShader;
}
if (Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::AddingMeshShaders)
{
Ar << Info.GraphicsDesc.MeshShader;
Ar << Info.GraphicsDesc.AmplificationShader;
}
if (Ar.GameNetVer() == (uint32)EPipelineCacheFileFormatVersions::LibraryID)
{
for (uint32 i = 0; i < SF_Compute; i++)
{
uint32 ID = 0;
Ar << ID;
}
}
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::SortedVertexDesc)
{
check(Ar.IsLoading());
FVertexDeclarationElementList Elements;
Ar << Elements;
Elements.Sort([](FVertexElement const& A, FVertexElement const& B)
{
if (A.StreamIndex < B.StreamIndex)
{
return true;
}
if (A.StreamIndex > B.StreamIndex)
{
return false;
}
if (A.Offset < B.Offset)
{
return true;
}
if (A.Offset > B.Offset)
{
return false;
}
if (A.AttributeIndex < B.AttributeIndex)
{
return true;
}
if (A.AttributeIndex > B.AttributeIndex)
{
return false;
}
return false;
});
Info.GraphicsDesc.VertexDescriptor.AddZeroed(Elements.Num());
for (uint32 i = 0; i < (uint32)Elements.Num(); i++)
{
Info.GraphicsDesc.VertexDescriptor[i].StreamIndex = Elements[i].StreamIndex;
Info.GraphicsDesc.VertexDescriptor[i].Offset = Elements[i].Offset;
Info.GraphicsDesc.VertexDescriptor[i].Type = Elements[i].Type;
Info.GraphicsDesc.VertexDescriptor[i].AttributeIndex = Elements[i].AttributeIndex;
Info.GraphicsDesc.VertexDescriptor[i].Stride = Elements[i].Stride;
Info.GraphicsDesc.VertexDescriptor[i].bUseInstanceIndex = Elements[i].bUseInstanceIndex;
}
}
else
{
Ar << Info.GraphicsDesc.VertexDescriptor;
}
Ar << Info.GraphicsDesc.BlendState;
Ar << Info.GraphicsDesc.RasterizerState;
Ar << Info.GraphicsDesc.DepthStencilState;
for ( uint32 i = 0; i < MaxSimultaneousRenderTargets; i++ )
{
uint32 Format = (uint32)Info.GraphicsDesc.RenderTargetFormats[i];
Ar << Format;
Info.GraphicsDesc.RenderTargetFormats[i] = (EPixelFormat)Format;
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::MoreRenderTargetFlags)
{
uint32 RTFlags = 0;
Ar << RTFlags;
// going forward, the flags will already be reduced when logging the PSOs to disk. However as of 2021-06-17 there still exist cache files (e.g. user ones) that have flags recorded as is
Info.GraphicsDesc.RenderTargetFlags[i] = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceRTFlags(static_cast<ETextureCreateFlags>(RTFlags));
}
else
{
static_assert(sizeof(uint64) == sizeof(Info.GraphicsDesc.RenderTargetFlags[i]), "ETextureCreateFlags size changed, please change serialization");
uint64 RTFlags = static_cast<uint64>(FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceRTFlags(Info.GraphicsDesc.RenderTargetFlags[i]));
Ar << RTFlags;
Info.GraphicsDesc.RenderTargetFlags[i] = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceRTFlags(static_cast<ETextureCreateFlags>(RTFlags));
}
uint8 LoadStore = 0;
Ar << LoadStore;
Ar << LoadStore;
}
Ar << Info.GraphicsDesc.RenderTargetsActive;
Ar << Info.GraphicsDesc.MSAASamples;
uint32 PrimType = (uint32)Info.GraphicsDesc.PrimitiveType;
Ar << PrimType;
Info.GraphicsDesc.PrimitiveType = (EPrimitiveType)PrimType;
uint32 Format = (uint32)Info.GraphicsDesc.DepthStencilFormat;
Ar << Format;
Info.GraphicsDesc.DepthStencilFormat = (EPixelFormat)Format;
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::MoreRenderTargetFlags)
{
uint32 DepthStencilFlags = 0;
Ar << DepthStencilFlags;
Info.GraphicsDesc.DepthStencilFlags = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceDSFlags(static_cast<ETextureCreateFlags>(DepthStencilFlags));
}
else
{
static_assert(sizeof(uint64) == sizeof(Info.GraphicsDesc.DepthStencilFlags), "ETextureCreateFlags size changed, please change serialization");
uint64 DepthStencilFlags = static_cast<uint64>(FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceDSFlags(Info.GraphicsDesc.DepthStencilFlags));
Ar << DepthStencilFlags;
Info.GraphicsDesc.DepthStencilFlags = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceDSFlags(static_cast<ETextureCreateFlags>(DepthStencilFlags));
}
Ar << Info.GraphicsDesc.DepthLoad;
Ar << Info.GraphicsDesc.StencilLoad;
Ar << Info.GraphicsDesc.DepthStore;
Ar << Info.GraphicsDesc.StencilStore;
Ar << Info.GraphicsDesc.SubpassHint;
Ar << Info.GraphicsDesc.SubpassIndex;
if (Ar.GameNetVer() < (uint32)EPipelineCacheFileFormatVersions::FragmentDensityAttachment)
{
uint8 MultiViewCount = 0;
Ar << MultiViewCount;
bool bHasFragmentDensityAttachment = false;
Ar << bHasFragmentDensityAttachment;
}
else
{
Ar << Info.GraphicsDesc.MultiViewCount;
Ar << Info.GraphicsDesc.bHasFragmentDensityAttachment;
}
if (Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::AddingDepthBounds)
{
Ar << Info.GraphicsDesc.bDepthBounds;
}
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
Ar << Info.RayTracingDesc.ShaderHash;
// Not used, kept for binary format compatibility
Ar << Info.RayTracingDesc.DeprecatedMaxPayloadSizeInBytes;
uint32 Frequency = uint32(Info.RayTracingDesc.Frequency);
Ar << Frequency;
Info.RayTracingDesc.Frequency = EShaderFrequency(Frequency);
if (Ar.GameNetVer() >= (uint32)EPipelineCacheFileFormatVersions::AddRTPSOShaderBindingLayout)
{
// Serialize RHIShaderBindingLayout
Ar << Info.RayTracingDesc.ShaderBindingLayout;
}
break;
}
default:
{
checkNoEntry();
}
}
return Ar;
}
FPipelineCacheFileFormatPSO::FPipelineCacheFileFormatPSO()
#if PSO_COOKONLY_DATA
: UsageMask(0)
, BindCount(0)
#endif
{
}
/*static*/ bool FPipelineCacheFileFormatPSO::Init(FPipelineCacheFileFormatPSO& PSO, FRHIComputeShader const* Init)
{
check(Init);
PSO.Type = DescriptorType::Compute;
#if PSO_COOKONLY_DATA
PSO.UsageMask = 0;
PSO.BindCount = 0;
#endif
// Because of the cheat in the copy constructor - lets play this safe
FMemory::Memset(&PSO.ComputeDesc, 0, sizeof(ComputeDescriptor));
PSO.ComputeDesc.ComputeShader = Init->GetHash();
bool bOK = true;
#if !UE_BUILD_SHIPPING
bOK = PSO.Verify();
#endif
return bOK;
}
/*static*/ bool FPipelineCacheFileFormatPSO::Init(FPipelineCacheFileFormatPSO& PSO, FGraphicsPipelineStateInitializer const& Init)
{
bool bOK = true;
PSO.Type = DescriptorType::Graphics;
#if PSO_COOKONLY_DATA
PSO.UsageMask = 0;
PSO.BindCount = 0;
#endif
// Because of the cheat in the copy constructor - lets play this safe
FMemory::Memset(&PSO.GraphicsDesc, 0, sizeof(GraphicsDescriptor));
#if PLATFORM_SUPPORTS_MESH_SHADERS
checkf(Init.BoundShaderState.GetVertexShader() || Init.BoundShaderState.GetMeshShader(), TEXT("A graphics pipeline must always have either a vertex or a mesh shader"));
if (Init.BoundShaderState.GetVertexShader())
#else
checkf(Init.BoundShaderState.GetVertexShader(), TEXT("A graphics pipeline must always have a vertex shader"));
#endif
{
check (Init.BoundShaderState.VertexDeclarationRHI);
check (Init.BoundShaderState.VertexDeclarationRHI->IsValid());
{
bOK &= Init.BoundShaderState.VertexDeclarationRHI->GetInitializer(PSO.GraphicsDesc.VertexDescriptor);
check(bOK);
PSO.GraphicsDesc.VertexDescriptor.Sort([](FVertexElement const& A, FVertexElement const& B)
{
if (A.StreamIndex < B.StreamIndex)
{
return true;
}
if (A.StreamIndex > B.StreamIndex)
{
return false;
}
if (A.Offset < B.Offset)
{
return true;
}
if (A.Offset > B.Offset)
{
return false;
}
if (A.AttributeIndex < B.AttributeIndex)
{
return true;
}
if (A.AttributeIndex > B.AttributeIndex)
{
return false;
}
return false;
});
}
PSO.GraphicsDesc.VertexShader = Init.BoundShaderState.VertexShaderRHI->GetHash();
}
if (Init.BoundShaderState.GetMeshShader())
{
PSO.GraphicsDesc.MeshShader = Init.BoundShaderState.GetMeshShader()->GetHash();
}
if (Init.BoundShaderState.GetAmplificationShader())
{
PSO.GraphicsDesc.AmplificationShader = Init.BoundShaderState.GetAmplificationShader()->GetHash();
}
if (Init.BoundShaderState.PixelShaderRHI)
{
PSO.GraphicsDesc.FragmentShader = Init.BoundShaderState.PixelShaderRHI->GetHash();
}
if (Init.BoundShaderState.GetGeometryShader())
{
PSO.GraphicsDesc.GeometryShader = Init.BoundShaderState.GetGeometryShader()->GetHash();
}
check (Init.BlendState);
{
bOK &= Init.BlendState->GetInitializer(PSO.GraphicsDesc.BlendState);
check(bOK);
}
check (Init.RasterizerState);
{
FRasterizerStateInitializerRHI Temp;
bOK &= Init.RasterizerState->GetInitializer(Temp);
check(bOK);
PSO.GraphicsDesc.RasterizerState = Temp;
}
check (Init.DepthStencilState);
{
bOK &= Init.DepthStencilState->GetInitializer(PSO.GraphicsDesc.DepthStencilState);
check(bOK);
}
for (uint32 i = 0; i < MaxSimultaneousRenderTargets; i++)
{
PSO.GraphicsDesc.RenderTargetFormats[i] = (EPixelFormat)Init.RenderTargetFormats[i];
PSO.GraphicsDesc.RenderTargetFlags[i] = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceRTFlags(Init.RenderTargetFlags[i]);
}
PSO.GraphicsDesc.RenderTargetsActive = Init.RenderTargetsEnabled;
PSO.GraphicsDesc.MSAASamples = Init.NumSamples;
PSO.GraphicsDesc.DepthStencilFormat = Init.DepthStencilTargetFormat;
PSO.GraphicsDesc.DepthStencilFlags = FPipelineCacheFileFormatPSO::GraphicsDescriptor::ReduceDSFlags(Init.DepthStencilTargetFlag);
PSO.GraphicsDesc.DepthLoad = Init.DepthTargetLoadAction;
PSO.GraphicsDesc.StencilLoad = Init.StencilTargetLoadAction;
PSO.GraphicsDesc.DepthStore = Init.DepthTargetStoreAction;
PSO.GraphicsDesc.StencilStore = Init.StencilTargetStoreAction;
PSO.GraphicsDesc.PrimitiveType = Init.PrimitiveType;
PSO.GraphicsDesc.SubpassHint = (uint8)Init.SubpassHint;
PSO.GraphicsDesc.SubpassIndex = Init.SubpassIndex;
PSO.GraphicsDesc.MultiViewCount = (uint8)Init.MultiViewCount;
PSO.GraphicsDesc.bHasFragmentDensityAttachment = Init.bHasFragmentDensityAttachment;
PSO.GraphicsDesc.bDepthBounds = Init.bDepthBounds;
#if !UE_BUILD_SHIPPING
bOK = bOK && PSO.Verify();
#endif
return bOK;
}
FPipelineCacheFileFormatPSO::~FPipelineCacheFileFormatPSO()
{
}
bool FPipelineCacheFileFormatPSO::operator==(const FPipelineCacheFileFormatPSO& Other) const
{
bool bSame = true;
if (this != &Other)
{
bSame = Type == Other.Type;
#if PSO_COOKONLY_DATA
/* Ignore: [UsageMask == UsageMask] in this test. */
/* Ignore: [BindCount == BindCount] in this test. */
#endif
if(Type == Other.Type)
{
switch(Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
bSame = (FMemory::Memcmp(&ComputeDesc, &Other.ComputeDesc, sizeof(ComputeDescriptor)) == 0);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
bSame = GraphicsDesc.VertexDescriptor.Num() == Other.GraphicsDesc.VertexDescriptor.Num();
for (uint32 i = 0; i < (uint32)FMath::Min(GraphicsDesc.VertexDescriptor.Num(), Other.GraphicsDesc.VertexDescriptor.Num()); i++)
{
bSame &= (FMemory::Memcmp(&GraphicsDesc.VertexDescriptor[i], &Other.GraphicsDesc.VertexDescriptor[i], sizeof(FVertexElement)) == 0);
}
bSame &=
GraphicsDesc.PrimitiveType == Other.GraphicsDesc.PrimitiveType &&
GraphicsDesc.VertexShader == Other.GraphicsDesc.VertexShader &&
GraphicsDesc.FragmentShader == Other.GraphicsDesc.FragmentShader &&
GraphicsDesc.GeometryShader == Other.GraphicsDesc.GeometryShader &&
GraphicsDesc.MeshShader == Other.GraphicsDesc.MeshShader &&
GraphicsDesc.AmplificationShader == Other.GraphicsDesc.AmplificationShader &&
GraphicsDesc.RenderTargetsActive == Other.GraphicsDesc.RenderTargetsActive &&
GraphicsDesc.MSAASamples == Other.GraphicsDesc.MSAASamples && GraphicsDesc.DepthStencilFormat == Other.GraphicsDesc.DepthStencilFormat &&
GraphicsDesc.DepthStencilFlags == Other.GraphicsDesc.DepthStencilFlags && GraphicsDesc.DepthLoad == Other.GraphicsDesc.DepthLoad &&
GraphicsDesc.DepthStore == Other.GraphicsDesc.DepthStore && GraphicsDesc.StencilLoad == Other.GraphicsDesc.StencilLoad && GraphicsDesc.StencilStore == Other.GraphicsDesc.StencilStore &&
GraphicsDesc.SubpassHint == Other.GraphicsDesc.SubpassHint && GraphicsDesc.SubpassIndex == Other.GraphicsDesc.SubpassIndex &&
GraphicsDesc.MultiViewCount == Other.GraphicsDesc.MultiViewCount && GraphicsDesc.bHasFragmentDensityAttachment == Other.GraphicsDesc.bHasFragmentDensityAttachment &&
GraphicsDesc.bDepthBounds == Other.GraphicsDesc.bDepthBounds &&
FMemory::Memcmp(&GraphicsDesc.BlendState, &Other.GraphicsDesc.BlendState, sizeof(FBlendStateInitializerRHI)) == 0 &&
FMemory::Memcmp(&GraphicsDesc.RasterizerState, &Other.GraphicsDesc.RasterizerState, sizeof(FPipelineFileCacheRasterizerState)) == 0 &&
FMemory::Memcmp(&GraphicsDesc.DepthStencilState, &Other.GraphicsDesc.DepthStencilState, sizeof(FDepthStencilStateInitializerRHI)) == 0 &&
FMemory::Memcmp(&GraphicsDesc.RenderTargetFormats, &Other.GraphicsDesc.RenderTargetFormats, sizeof(GraphicsDesc.RenderTargetFormats)) == 0 &&
FMemory::Memcmp(&GraphicsDesc.RenderTargetFlags, &Other.GraphicsDesc.RenderTargetFlags, sizeof(GraphicsDesc.RenderTargetFlags)) == 0;
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
bSame &= RayTracingDesc == Other.RayTracingDesc;
break;
}
default:
{
check(false);
break;
}
}
}
}
return bSame;
}
FPipelineCacheFileFormatPSO::FPipelineCacheFileFormatPSO(const FPipelineCacheFileFormatPSO& Other)
: Type(Other.Type)
#if PSO_COOKONLY_DATA
, UsageMask(Other.UsageMask)
, BindCount(Other.BindCount)
#endif
{
switch(Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
FMemory::Memcpy(&ComputeDesc, &Other.ComputeDesc, sizeof(ComputeDescriptor));
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
FMemory::Memcpy(&GraphicsDesc, &Other.GraphicsDesc, sizeof(GraphicsDescriptor));
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
RayTracingDesc = Other.RayTracingDesc;
break;
}
default:
{
check(false);
break;
}
}
}
FPipelineCacheFileFormatPSO& FPipelineCacheFileFormatPSO::operator=(const FPipelineCacheFileFormatPSO& Other)
{
if(this != &Other)
{
Type = Other.Type;
#if PSO_COOKONLY_DATA
UsageMask = Other.UsageMask;
BindCount = Other.BindCount;
#endif
switch(Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
FMemory::Memcpy(&ComputeDesc, &Other.ComputeDesc, sizeof(ComputeDescriptor));
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
// If we implement a classic copy constructor without memcpy - remove memset in ::Init() function above
FMemory::Memcpy(&GraphicsDesc, &Other.GraphicsDesc, sizeof(GraphicsDescriptor));
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
RayTracingDesc = Other.RayTracingDesc;
break;
}
default:
{
check(false);
break;
}
}
}
return *this;
}
struct FPipelineCacheFileFormatTOC
{
FPipelineCacheFileFormatTOC()
: SortedOrder(FPipelineFileCacheManager::PSOOrder::MostToLeastUsed)
{}
FPipelineFileCacheManager::PSOOrder SortedOrder;
TMap<uint32, FPipelineCacheFileFormatPSOMetaData> MetaData;
void DumpToLog() const
{
for (TMap<uint32, FPipelineCacheFileFormatPSOMetaData>::TConstIterator It(MetaData); It; ++It)
{
const FPipelineCacheFileFormatPSOMetaData& dat = It.Value();
UE_LOG(LogRHI, VeryVerbose, TEXT("PSO hash %u - guid (%s), stats(FF %" INT64_FMT ", LF %" INT64_FMT ", bind %" INT64_FMT ")"), It.Key(), *dat.FileGuid.ToString(), dat.Stats.FirstFrameUsed, dat.Stats.LastFrameUsed, dat.Stats.TotalBindCount)
}
UE_LOG(LogRHI, VeryVerbose, TEXT("Total PSOs %d"), MetaData.Num());
}
friend FArchive& operator<<(FArchive& Ar, FPipelineCacheFileFormatTOC& Info)
{
// TOC is assumed to be at the end of the file
// If this changes then the EOF read check and write need to moved out of here
// if all entries are using the same GUID (which is the norm when saving a packaged cache with the "buildsc" command of the commandlet),
// do not save it with every entry, reducing the surface of changes (GUID is regenerated on each save even if entries are the same)
bool bAllEntriesUseSameGuid = true;
FGuid FirstEntryGuid;
if(Ar.IsLoading())
{
uint64 TOCMagic = 0;
Ar << TOCMagic;
if(FPipelineCacheTOCFileFormatMagic != TOCMagic)
{
Ar.SetError();
return Ar;
}
uint64 EOFMagic = 0;
const int64 FileSize = Ar.TotalSize();
const int64 FilePosition = Ar.Tell();
Ar.Seek(FileSize - sizeof(FPipelineCacheEOFFileFormatMagic));
Ar << EOFMagic;
Ar.Seek(FilePosition);
if(FPipelineCacheEOFFileFormatMagic != EOFMagic)
{
Ar.SetError();
return Ar;
}
}
else
{
uint64 TOCMagic = FPipelineCacheTOCFileFormatMagic;
Ar << TOCMagic;
// check if the whole file is using the same GUID
bool bGuidSet = false;
for (TMap<uint32, FPipelineCacheFileFormatPSOMetaData>::TConstIterator It(Info.MetaData); It; ++It)
{
if (bGuidSet)
{
if (It.Value().FileGuid != FirstEntryGuid)
{
bAllEntriesUseSameGuid = false;
break;
}
}
else
{
FirstEntryGuid = It.Value().FileGuid;
bGuidSet = true;
}
}
if (!bGuidSet)
{
bAllEntriesUseSameGuid = false; // no entries, so don't do save the guid at all
}
// if the whole file uses the same guids, zero out
if (bAllEntriesUseSameGuid)
{
for (TMap<uint32, FPipelineCacheFileFormatPSOMetaData>::TIterator It(Info.MetaData); It; ++It)
{
It.Value().FileGuid = FGuid();
}
}
}
uint8 AllEntriesUseSameGuid = bAllEntriesUseSameGuid ? 1 : 0;
Ar << AllEntriesUseSameGuid;
bAllEntriesUseSameGuid = AllEntriesUseSameGuid != 0;
if (bAllEntriesUseSameGuid)
{
Ar << FirstEntryGuid;
}
Ar << Info.SortedOrder;
Ar << Info.MetaData;
if(Ar.IsSaving())
{
uint64 EOFMagic = FPipelineCacheEOFFileFormatMagic;
Ar << EOFMagic;
}
else if (bAllEntriesUseSameGuid)
{
for (TMap<uint32, FPipelineCacheFileFormatPSOMetaData>::TIterator It(Info.MetaData); It; ++It)
{
It.Value().FileGuid = FirstEntryGuid;
}
}
return Ar;
}
};
static bool ShouldDeleteExistingUserCache()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("deleteuserpsocache")) || FParse::Param(FCommandLine::Get(), TEXT("logPSO"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Deleting user-writable PSO cache as requested on command line"));
}
return bCmdLineForce;
}
class FPipelineCacheFile
{
FString Name;
EShaderPlatform ShaderPlatform;
FName PlatformName;
uint64 TOCOffset;
FPipelineCacheFileFormatTOC TOC;
FGuid FileGuid;
FString FilePath;
TSharedPtr<IAsyncReadFileHandle, ESPMode::ThreadSafe> AsyncFileHandle;
FString RecordingFilename;
public:
enum class EStatus : uint8
{
Unknown,
BundledCache,
UserCacheOpened, // a user cache was successfully opened
NewUserCache, // user cache failed to open, started empty.
};
EStatus CacheStatus = EStatus::Unknown;
static uint32 GameVersion;
FPipelineCacheFile()
: TOCOffset(0)
, AsyncFileHandle(nullptr)
{
}
~FPipelineCacheFile()
{
DEC_MEMORY_STAT_BY(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
}
static bool OpenPipelineFileCache(const FString& FilePath, EShaderPlatform ShaderPlatform, FGuid& Guid, TSharedPtr<IAsyncReadFileHandle, ESPMode::ThreadSafe>& Handle, FPipelineCacheFileFormatTOC& Content, uint64& TOCOffsetOUT)
{
bool bSuccess = false;
FArchive* FileReader = IFileManager::Get().CreateFileReader(*FilePath);
if (FileReader)
{
FileReader->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
FPipelineCacheFileFormatHeader Header = {};
*FileReader << Header;
if (Header.Magic == FPipelineCacheFileFormatMagic && Header.Version == FPipelineCacheFileFormatCurrentVersion && Header.GameVersion == GameVersion && Header.Platform == ShaderPlatform)
{
check(Header.TableOffset > 0);
check(FileReader->TotalSize() > 0);
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile Header Game Version: %d"), Header.GameVersion);
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile Header Engine Data Version: %d"), Header.Version);
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile Header TOC Offset: %llu"), Header.TableOffset);
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile File Size: %lld Bytes"), FileReader->TotalSize());
if(Header.TableOffset < (uint64)FileReader->TotalSize())
{
FileReader->Seek(Header.TableOffset);
*FileReader << Content;
// FPipelineCacheFileFormatTOC archive read can set the FArchive to error on failure
bSuccess = !FileReader->IsError();
}
if(!bSuccess)
{
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile: %s is corrupt reading TOC"), *FilePath);
}
}
else
{
bool bMagicMatch = (Header.Magic == FPipelineCacheFileFormatMagic);
bool bVersionMatch = (Header.Version == FPipelineCacheFileFormatCurrentVersion);
bool bGameVersionMatch = (Header.GameVersion == GameVersion);
bool bSPMatch = (Header.Platform == ShaderPlatform);
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile: skipping %s (different %s%s%s%s)"), *FilePath,
bMagicMatch ? TEXT("") : TEXT(" magic"),
bVersionMatch ? TEXT("") : TEXT(" version"),
bGameVersionMatch ? TEXT("") : TEXT(" gameversion"),
bSPMatch ? TEXT("") : TEXT(" shaderplatform")
);
}
if(!FileReader->Close())
{
bSuccess = false;
}
delete FileReader;
FileReader = nullptr;
if(bSuccess)
{
Handle = MakeShareable(FPlatformFileManager::Get().GetPlatformFile().OpenAsyncRead(*FilePath));
if(Handle.IsValid())
{
UE_LOG(LogRHI, Log, TEXT("Opened FPipelineCacheFile: %s (GUID: %s) with %d entries."), *FilePath, *Header.Guid.ToString(), Content.MetaData.Num());
Guid = Header.Guid;
TOCOffsetOUT = Header.TableOffset;
}
else
{
UE_LOG(LogRHI, Log, TEXT("Failed to create async read file handle to FPipelineCacheFile: %s (GUID: %s)"), *FilePath, *Header.Guid.ToString());
bSuccess = false;
}
}
}
else
{
UE_LOG(LogRHI, Log, TEXT("Could not open FPipelineCacheFile: %s"), *FilePath);
}
return bSuccess;
}
void GarbageCollectUserCache(FString const& UserCacheFilePath, const TSet<FGuid>& KnownGuids)
{
UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile: GarbageCollectUserCache() Begin"));
ON_SCOPE_EXIT{ UE_LOG(LogRHI, Log, TEXT("FPipelineCacheFile: GarbageCollectUserCache() End")); };
int32 GCPeriodInDays = CVarPSOFileCacheUserCacheUnusedElementCheckPeriod.GetValueOnAnyThread();
if (GCPeriodInDays < 0)
{
UE_LOG(LogRHI, Log, TEXT("User cache GC is disabled"));
return;
}
FArchive* FileReader = IFileManager::Get().CreateFileReader(*UserCacheFilePath);
if (!FileReader)
{
UE_LOG(LogRHI, Log, TEXT("No user cache file found"));
return;
}
ON_SCOPE_EXIT
{
if (FileReader)
{
FileReader->Close();
delete FileReader;
}
};
FileReader->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
FPipelineCacheFileFormatHeader Header;
*FileReader << Header;
if (!(Header.Magic == FPipelineCacheFileFormatMagic && Header.Version == FPipelineCacheFileFormatCurrentVersion && Header.GameVersion == GameVersion && Header.Platform == ShaderPlatform))
{
UE_LOG(LogRHI, Error, TEXT("File has invalid or out of date header"));
return;
}
FTimespan GCPeriod = FTimespan::FromDays(GCPeriodInDays);
int64 NextGCTime = Header.LastGCUnixTime + GCPeriod.GetTotalSeconds();
const int64 UnixTime = GetCurrentUnixTime();
if (UnixTime < NextGCTime)
{
const FTimespan TimespanToNextGC = FTimespan::FromSeconds(NextGCTime - UnixTime);
const double DaysToNextGC = TimespanToNextGC.GetTotalDays();
UE_LOG(LogRHI, Log, TEXT("Next GC on user cache is in %0.3f days."), DaysToNextGC);
return;
}
FPipelineCacheFileFormatTOC Content;
if (Header.TableOffset < (uint64)FileReader->TotalSize())
{
FileReader->Seek(Header.TableOffset);
*FileReader << Content;
// FPipelineCacheFileFormatTOC archive read can set the FArchive to error on failure
if (FileReader->IsError())
{
UE_LOG(LogRHI, Log, TEXT("Failed to read TOC"));
return;
}
}
int64 StaleDays = CVarPSOFileCacheUserCacheUnusedElementRetainDays.GetValueOnAnyThread();
FTimespan StaleTimespan = FTimespan::FromDays(StaleDays);
int64 EvictionTime = UnixTime - (int64)StaleTimespan.GetTotalSeconds();
auto EntryShouldBeRemovedFromUserCache = [&Header, FileGuid=this->FileGuid, EvictionTime,&KnownGuids](const FPipelineCacheFileFormatPSOMetaData& MetaData)
{
// Remove the element if it is in the user cache and the time has elapsed, or if it was in a cache that no longer exists.
if (MetaData.FileGuid == Header.Guid)
{
return EvictionTime >= MetaData.LastUsedUnixTime;
}
else
{
// TODO: right now we do not have a way to supply known guids.
// we use only the expired date check for now.
// return !KnownGuids.Contains(MetaData.FileGuid);
return EvictionTime >= MetaData.LastUsedUnixTime;
}
};
int32 NumOutOfDateEntries = 0;
for (auto const& Entry : Content.MetaData)
{
if (EntryShouldBeRemovedFromUserCache(Entry.Value))
{
NumOutOfDateEntries++;
}
}
if (NumOutOfDateEntries == 0)
{
UE_LOG(LogRHI, Log, TEXT("No out of date entries."));
return;
}
if (NumOutOfDateEntries == Content.MetaData.Num())
{
FileReader->Close();
delete FileReader;
FileReader = nullptr;
if (IFileManager::Get().FileExists(*UserCacheFilePath))
{
IFileManager::Get().Delete(*UserCacheFilePath);
}
UE_LOG(LogRHI, Log, TEXT("All entries are out of date, recreating cache."));
return;
}
UE_LOG(LogRHI, Log, TEXT("%d/%d elements are out of date, performing GC."), NumOutOfDateEntries, Content.MetaData.Num());
TArray<uint8> Buffer;
FMemoryWriter MemoryWriter(Buffer);
MemoryWriter.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
FPipelineCacheFileFormatHeader NewHeader = Header;
check(NewHeader.Magic == FPipelineCacheFileFormatMagic);
check(NewHeader.Platform == ShaderPlatform);
NewHeader.Version = FPipelineCacheFileFormatCurrentVersion;
NewHeader.LastGCUnixTime = UnixTime;
NewHeader.TableOffset = 0; // Will overwrite with the correct offset after building the TOC
MemoryWriter << NewHeader;
FPipelineCacheFileFormatTOC NewTOC;
NewTOC.SortedOrder = Content.SortedOrder; // Removal maintains sort order of existing cache
for (auto const& Entry : Content.MetaData)
{
if (!EntryShouldBeRemovedFromUserCache(Entry.Value))
{
// Copy the meta data, and the FPipelineCacheFileFormatPSO if it exists in the user cache (the meta data can point into the game cache).
FPipelineCacheFileFormatPSOMetaData NewEntry = Entry.Value;
if (Entry.Value.FileGuid == Header.Guid && Entry.Value.FileSize > 0)
{
NewEntry.FileSize = Entry.Value.FileSize;
NewEntry.FileOffset = MemoryWriter.Tell();
// Copy from file to new memory writer
FPipelineCacheFileFormatPSO ExistingPSO;
FileReader->Seek(Entry.Value.FileOffset);
*FileReader << ExistingPSO;
MemoryWriter << ExistingPSO;
}
check(NewEntry.FileGuid != FGuid());
NewTOC.MetaData.Add(Entry.Key, NewEntry);
}
}
NewHeader.TableOffset = MemoryWriter.Tell();
MemoryWriter << NewTOC;
MemoryWriter.Seek(0);
MemoryWriter << NewHeader;
UE_LOG(LogRHI, Log, TEXT("Deleting existing cache file"));
int64 OriginalSize = FileReader->TotalSize();
FileReader->Close();
delete FileReader;
FileReader = nullptr;
if (IFileManager::Get().FileExists(*UserCacheFilePath))
{
IFileManager::Get().Delete(*UserCacheFilePath);
}
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*UserCacheFilePath);
if (!FileWriter)
{
UE_LOG(LogRHI, Log, TEXT("Unable to open new cache file for writing"));
return;
}
int64 NewSize = MemoryWriter.TotalSize();
FileWriter->Serialize(Buffer.GetData(), MemoryWriter.TotalSize());
FileWriter->Close();
delete FileWriter;
UE_LOG(LogRHI, Log, TEXT("Rewrote cache file. Old Size %lld, new size %lld (%lld byte reduction)"), OriginalSize, NewSize, OriginalSize - NewSize);
}
bool OpenPipelineFileCache(FString const& NameIn, EShaderPlatform Platform, FGuid& OutGameFileGuid)
{
check(CacheStatus == EStatus::Unknown);
OutGameFileGuid = FGuid();
TOC.SortedOrder = FPipelineFileCacheManager::PSOOrder::Default;
TOC.MetaData.Empty();
Name = NameIn;
ShaderPlatform = Platform;
PlatformName = LegacyShaderPlatformToShaderFormat(Platform);
FString GamePath = FPaths::ProjectContentDir() / TEXT("PipelineCaches") / ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()) / FString::Printf(TEXT("%s_%s.stable.upipelinecache"), *Name, *PlatformName.ToString());
static bool bCommandLineNotStable = FParse::Param(FCommandLine::Get(), TEXT("nostablepipelinecache"));
if (bCommandLineNotStable)
{
GamePath.Empty();
}
const bool bGameFileOk = OpenPipelineFileCache(GamePath, ShaderPlatform, FileGuid, AsyncFileHandle, TOC, TOCOffset);
if (bGameFileOk)
{
FilePath = GamePath;
OutGameFileGuid = FileGuid;
CacheStatus = EStatus::BundledCache;
}
if (bGameFileOk && GRHISupportsLazyShaderCodeLoading && CVarLazyLoadShadersWhenPSOCacheIsPresent.GetValueOnAnyThread())
{
UE_LOG(LogRHI, Log, TEXT("Lazy loading from the shader code library is enabled."));
GRHILazyShaderCodeLoading = true;
}
#if !UE_BUILD_SHIPPING
uint32 InvalidEntryCount = 0;
#endif
for (auto const& Entry : TOC.MetaData)
{
FPipelineStateStats* Stat = FPipelineFileCacheManager::Stats.FindRef(Entry.Key);
if (!Stat)
{
Stat = new FPipelineStateStats;
Stat->PSOHash = Entry.Key;
Stat->TotalBindCount = -1;
FPipelineFileCacheManager::Stats.Add(Entry.Key, Stat);
}
UE_CLOG(!!FPipelineFileCacheManager::NewPSOUsage.Find(Entry.Key), LogRHI, Warning, TEXT("loaded PSOFC %s contains entry (%u) previously marked as new "), *Name, (Entry.Key));
#if !UE_BUILD_SHIPPING
if((Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
++InvalidEntryCount;
}
#endif
}
#if !UE_BUILD_SHIPPING
if(InvalidEntryCount > 0)
{
UE_LOG(LogRHI, Warning, TEXT("Found %d / %d PSO entries marked as invalid."), InvalidEntryCount, TOC.MetaData.Num());
}
#endif
INC_MEMORY_STAT_BY(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
UE_LOG(LogRHI, VeryVerbose, TEXT("-- opened bundled %s cache:"), *NameIn);
TOC.DumpToLog();
UE_LOG(LogRHI, VeryVerbose, TEXT("-- end of dump (%s)"), *NameIn);
return bGameFileOk;
}
//
bool OpenUserPipelineFileCache(FString const& CacheNameIn, EShaderPlatform Platform)
{
bool bUserFileOk = false;
if (CacheStatus == EStatus::Unknown)
{
SET_DWORD_STAT(STAT_TotalGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_TotalComputePipelineStateCount, 0);
SET_DWORD_STAT(STAT_TotalRayTracingPipelineStateCount, 0);
SET_DWORD_STAT(STAT_SerializedGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_SerializedComputePipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewComputePipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewRayTracingPipelineStateCount, 0);
// one time attempt to open the user cache.
TOC.SortedOrder = FPipelineFileCacheManager::PSOOrder::Default;
TOC.MetaData.Empty();
Name = CacheNameIn;
ShaderPlatform = Platform;
PlatformName = LegacyShaderPlatformToShaderFormat(Platform);
FGuid UniqueFileGuid;
FPlatformMisc::CreateGuid(UniqueFileGuid); // not very unique on android, but won't matter much here
RecordingFilename = FString::Printf(TEXT("%s-CL-%u-"), *FEngineVersion::Current().GetBranchDescriptor(), FEngineVersion::Current().GetChangelist());
RecordingFilename += FString::Printf(TEXT("%s_%s_%s.rec.upipelinecache"), *Name, *PlatformName.ToString(), *UniqueFileGuid.ToString());
RecordingFilename = FPaths::ProjectSavedDir() / TEXT("CollectedPSOs") / RecordingFilename;
UE_LOG(LogRHI, Log, TEXT("Base name for record PSOs is %s"), *RecordingFilename);
FilePath = FPaths::ProjectSavedDir() / FString::Printf(TEXT("%s_%s.upipelinecache"), *Name, *PlatformName.ToString());
if (ShouldDeleteExistingUserCache())
{
UE_LOG(LogRHI, Log, TEXT("Deleting FPipelineCacheFile: %s"), *FilePath);
if (IFileManager::Get().FileExists(*FilePath))
{
IFileManager::Get().Delete(*FilePath);
}
}
FString JournalPath = FilePath + JOURNAL_FILE_EXTENSION;
bool const bJournalFileExists = IFileManager::Get().FileExists(*JournalPath);
if (bJournalFileExists || ShouldDeleteExistingUserCache())
{
UE_LOG(LogRHI, Log, TEXT("Deleting FPipelineCacheFile: %s"), *FilePath);
// If either of the above are true we need to dispose of this case as we consider it invalid
if (IFileManager::Get().FileExists(*FilePath))
{
IFileManager::Get().Delete(*FilePath);
}
if (bJournalFileExists)
{
IFileManager::Get().Delete(*JournalPath);
}
}
// TODO: we currently do not know the full set of valid PSOFC guids.
// KnownGuids is a placeholder for all possible PSO cache guids.
TSet<FGuid> KnownGuids;
GarbageCollectUserCache(FilePath, KnownGuids);
FPipelineCacheFileFormatTOC UserTOC;
bUserFileOk = OpenPipelineFileCache(FilePath, ShaderPlatform, FileGuid, AsyncFileHandle, TOC, TOCOffset);
CacheStatus = bUserFileOk ? EStatus::UserCacheOpened : EStatus::NewUserCache;
if (!bUserFileOk)
{
FileGuid = FGuid::NewGuid();
// Start the file again!
IFileManager::Get().Delete(*FilePath);
TOCOffset = 0;
}
#if !UE_BUILD_SHIPPING
uint32 InvalidEntryCount = 0;
#endif
for (auto const& Entry : TOC.MetaData)
{
FPipelineStateStats* Stat = FPipelineFileCacheManager::Stats.FindRef(Entry.Key);
if (!Stat)
{
Stat = new FPipelineStateStats;
Stat->PSOHash = Entry.Key;
Stat->TotalBindCount = -1;
FPipelineFileCacheManager::Stats.Add(Entry.Key, Stat);
}
#if !UE_BUILD_SHIPPING
if ((Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
++InvalidEntryCount;
}
#endif
}
#if !UE_BUILD_SHIPPING
if (InvalidEntryCount > 0)
{
UE_LOG(LogRHI, Warning, TEXT("Found %d / %d PSO entries marked as invalid."), InvalidEntryCount, TOC.MetaData.Num());
}
#endif
}
INC_MEMORY_STAT_BY(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
return bUserFileOk;
}
static void MergePSOUsageToMetaData(TMap<uint32, FPSOUsageData>& NewPSOUsage, TMap<uint32, FPipelineCacheFileFormatPSOMetaData>& MetaData, int64 CurrentUnixTime, bool bRemoveUpdatedentries = false)
{
for(auto It = NewPSOUsage.CreateIterator(); It; ++It)
{
auto& MaskEntry = *It;
//Don't use FindChecked as if new PSO was not bound - it might not be in the TOC.MetaData - they are not always added in every save mode - this is not an error
auto* PSOMetaData = MetaData.Find(MaskEntry.Key);
if(PSOMetaData != nullptr)
{
PSOMetaData->UsageMask |= MaskEntry.Value.UsageMask;
PSOMetaData->EngineFlags |= MaskEntry.Value.EngineFlags;
PSOMetaData->LastUsedUnixTime = CurrentUnixTime;
if(bRemoveUpdatedentries)
{
It.RemoveCurrent();
}
}
}
}
bool SavePipelineFileCache(FPipelineFileCacheManager::SaveMode Mode, TMap<uint32, FPipelineStateStats*> const& Stats, TSet<FPipelineCacheFileFormatPSO>& NewEntries, FPipelineFileCacheManager::PSOOrder Order, TMap<uint32, FPSOUsageData>& NewPSOUsage)
{
check(CacheStatus == EStatus::NewUserCache || CacheStatus == EStatus::UserCacheOpened);
// remove from stats because this operation will modify the content and re-set the stat at the end.
DEC_MEMORY_STAT_BY(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
QUICK_SCOPE_CYCLE_COUNTER(STAT_SavePipelineFileCache);
double StartTime = FPlatformTime::Seconds();
FString SaveFilePath = FilePath;
if (FPipelineFileCacheManager::SaveMode::BoundPSOsOnly == Mode)
{
SaveFilePath = GetRecordingFilename();
}
bool bFileWriteSuccess = false;
bool bPerformWrite = true;
if (FPipelineFileCacheManager::SaveMode::Incremental == Mode)
{
bPerformWrite = NewEntries.Num() || Order != TOC.SortedOrder || NewPSOUsage.Num();
bFileWriteSuccess = !bPerformWrite;
}
if (bPerformWrite)
{
uint32 NumNewEntries = 0;
int64 UnixTime = GetCurrentUnixTime();
FString JournalPath;
if (Mode != FPipelineFileCacheManager::SaveMode::BoundPSOsOnly)
{
JournalPath = SaveFilePath + JOURNAL_FILE_EXTENSION;
FArchive* JournalWriter = IFileManager::Get().CreateFileWriter(*JournalPath);
check(JournalWriter);
// Header
{
FPipelineCacheFileFormatHeader Header;
Header.Magic = FPipelineCacheFileFormatMagic;
Header.Version = FPipelineCacheFileFormatCurrentVersion;
Header.GameVersion = GameVersion;
Header.Platform = ShaderPlatform;
Header.Guid = FileGuid;
Header.TableOffset = 0;
Header.LastGCUnixTime = UnixTime;
*JournalWriter << Header;
}
check(!JournalWriter->IsError());
JournalWriter->Close();
delete JournalWriter;
bPerformWrite = IFileManager::Get().FileExists(*JournalPath);
}
if (bPerformWrite)
{
struct FMemoryReaderAndMemory
{
// Non-copyable
FMemoryReaderAndMemory(const FMemoryReaderAndMemory&) = delete;
FMemoryReaderAndMemory& operator=(const FMemoryReaderAndMemory&) = delete;
TArray<uint8> Bytes;
TUniquePtr<FMemoryReader> Reader;
explicit FMemoryReaderAndMemory(FPipelineCacheFile* PipelineFile)
{
if(PipelineFile)
{
int64 FileSize = IFileManager::Get().FileSize(*PipelineFile->FilePath);
if (FileSize > 0)
{
Bytes.SetNumUninitialized(FileSize);
if (PipelineFile->AsyncFileHandle.IsValid())
{
IAsyncReadRequest* Request = PipelineFile->AsyncFileHandle->ReadRequest(0, FileSize, AIOP_Normal, nullptr, Bytes.GetData());
Request->WaitCompletion();
delete Request;
UE_LOG(LogRHI, VeryVerbose, TEXT("1 Opening %s as guid %s, size %" INT64_FMT), *PipelineFile->FilePath, *PipelineFile->GetFileGuid().ToString(), FileSize);
}
else
{
bool bReadOK = FFileHelper::LoadFileToArray(Bytes, *PipelineFile->FilePath);
UE_LOG(LogRHI, VeryVerbose, TEXT("2 Opening %s as guid %s, size %" INT64_FMT), *PipelineFile->FilePath, *PipelineFile->GetFileGuid().ToString(), FileSize);
UE_CLOG(!bReadOK, LogRHI, Warning, TEXT("Failed to read %lld bytes from %s while re-saving the PipelineFileCache!"), FileSize, *PipelineFile->FilePath);
}
Reader = MakeUnique<FMemoryReader>(Bytes);
Reader->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
}
}
}
FMemoryReader* GetReader() { return Reader.Get(); }
};
TMap<FGuid, TUniquePtr<FMemoryReaderAndMemory>> GuidToFileCacheReader;
auto GetFileCacheReaderFromGuid = [&GuidToFileCacheReader](const FGuid& guid)
{
check(guid != FGuid());
if(!GuidToFileCacheReader.Contains(guid))
{
const FString& FoundKey = FPipelineFileCacheManager::GameGuidToCacheKey.FindRef(guid);
FPipelineCacheFile* FoundPipelineFile = FPipelineFileCacheManager::GetPipelineCacheFileFromKey(FoundKey);
GuidToFileCacheReader.Add(guid, MakeUnique<FMemoryReaderAndMemory>(FoundPipelineFile));
}
if (TUniquePtr<FMemoryReaderAndMemory>* Found = GuidToFileCacheReader.Find(guid))
{
return (*Found)->GetReader();
}
checkNoEntry();
return static_cast<FMemoryReader*>(nullptr);
};
// Assume caller has handled Platform specifc path + filename
TArray<uint8> SaveBytes;
FArchive* FileWriter;
bool bUseMemoryWriter = (Mode == FPipelineFileCacheManager::SaveMode::BoundPSOsOnly);
FString TempPath = SaveFilePath;
// Only use a file switcheroo on Apple platforms as they are the only ones tested so far.
// At least two other platforms MoveFile implementation looks broken when moving from a writable source file to a writeable destination.
// They only handle moves/renames between the read-only -> writeable directories/devices.
if ((PLATFORM_APPLE || PLATFORM_ANDROID) && Mode != FPipelineFileCacheManager::SaveMode::Incremental)
{
TempPath += TEXT(".tmp");
}
if (bUseMemoryWriter)
{
FileWriter = new FMemoryWriter(SaveBytes, true, false, FName(*SaveFilePath));
}
else
{
// parent directory creation is necessary because the deploy process from
// AndroidPlatform.Automation.cs destroys the parent directories and recreates them
IFileManager::Get().MakeDirectory(*FPaths::GetPath(TempPath), true);
FileWriter = IFileManager::Get().CreateFileWriter(*TempPath, FILEWRITE_Append);
}
if (FileWriter)
{
FileWriter->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
FileWriter->Seek(0);
// Header
FPipelineCacheFileFormatHeader Header;
{
Header.Magic = FPipelineCacheFileFormatMagic;
Header.Version = FPipelineCacheFileFormatCurrentVersion;
Header.GameVersion = GameVersion;
Header.Platform = ShaderPlatform;
Header.Guid = FileGuid;
Header.TableOffset = 0;
Header.LastGCUnixTime = UnixTime;
*FileWriter << Header;
TOCOffset = FMath::Max(TOCOffset, (uint64)FileWriter->Tell());
}
uint32 TotalEntries = 0;
uint32 ConsolidatedEntries = 0;
uint32 RemovedEntries = 0;
switch (Mode)
{
// This mode just writes new, used, entries to the end of the file and updates the TOC which will contain entries from the Game-Content file that are redundant.
// Any current tasks are unaffected as the prior offsets are still valid.
case FPipelineFileCacheManager::SaveMode::Incremental:
{
// PSO Descriptors
uint64 PSOOffset = TOCOffset;
FileWriter->Seek(PSOOffset);
// Add new entries
for(auto It = NewEntries.CreateIterator(); It; ++It)
{
FPipelineCacheFileFormatPSO& NewEntry = *It;
uint32 PSOHash = GetTypeHash(NewEntry);
bool bFound = FPipelineFileCacheManager::IsPSOEntryCached(NewEntry, nullptr);
if (bFound)
{
// this could happen if another PSOFC loads after the PSO was encountered, if desired we could remove things from newentries when a psofc is mounted..
UE_LOG(LogRHI, Display, TEXT("Incrementally saving new PSOs but entry (%u), is already cached.."), PSOHash);
// Not removing it as the cached item could be legit if co-owner is not always loaded.
}
FPipelineStateStats const* Stat = Stats.FindRef(PSOHash);
if (Stat && Stat->TotalBindCount > 0)
{
FPipelineCacheFileFormatPSOMetaData Meta;
Meta.Stats.PSOHash = PSOHash;
Meta.FileOffset = PSOOffset;
Meta.FileGuid = FileGuid;
Meta.AddShaders(NewEntry);
TArray<uint8> Bytes;
FMemoryWriter Wr(Bytes);
Wr.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
Wr << NewEntry;
FileWriter->Serialize(Bytes.GetData(), Wr.TotalSize());
Meta.FileSize = Wr.TotalSize();
check(Meta.FileGuid != FGuid());
TOC.MetaData.Add(PSOHash, Meta);
PSOOffset += Meta.FileSize;
check(PSOOffset == FileWriter->Tell());
NumNewEntries++;
It.RemoveCurrent();
UE_LOG(LogRHI, VeryVerbose, TEXT("Incremental save is appending new PSOs (%u)"), PSOHash);
}
}
// We're appending to the current user cache here, Our TOC is the total.
TotalEntries = TOC.MetaData.Num();
if(Order != FPipelineFileCacheManager::PSOOrder::Default)
{
SortMetaData(TOC.MetaData, Order);
TOC.SortedOrder = Order;
}
else
{
// Added new entries and not re-sorted - the sort order invalid - reset to default
TOC.SortedOrder = FPipelineFileCacheManager::PSOOrder::Default;
}
// Update TOC Metadata usage and clear relevant entries in NewPSOUsage as we are saving this file cache TOC
MergePSOUsageToMetaData(NewPSOUsage, TOC.MetaData, UnixTime, true);
Header.TableOffset = PSOOffset;
TOCOffset = PSOOffset;
FileWriter->Seek(Header.TableOffset);
// use a temp here because serializing can destroy our metadata guids.
FPipelineCacheFileFormatTOC TempTOC = TOC;
*FileWriter << TempTOC;
break;
}
// This mode actually saves to a separate file that records only PSOs that were bound.
// BoundPSOsOnly will record all those PSOs used in this run of the game.
case FPipelineFileCacheManager::SaveMode::BoundPSOsOnly:
{
FPipelineCacheFileFormatTOC TempTOC;
// Merge all of the existing PSO caches together, including this (user cache)
for (TPair<FString, TUniquePtr<class FPipelineCacheFile>>& PipelineCachePair : FPipelineFileCacheManager::FileCacheMap)
{
TempTOC.MetaData.Append(PipelineCachePair.Value->TOC.MetaData);
}
TMap<uint32, FPipelineCacheFileFormatPSO> PSOs;
Header.Guid = FGuid::NewGuid();
for (auto& Entry : NewEntries)
{
FPipelineCacheFileFormatPSOMetaData Meta;
Meta.Stats.PSOHash = GetTypeHash(Entry);
Meta.FileOffset = 0;
Meta.FileSize = 0;
Meta.FileGuid = Header.Guid;
Meta.AddShaders(Entry);
check(Meta.FileGuid != FGuid());
TempTOC.MetaData.Add(Meta.Stats.PSOHash, Meta);
PSOs.Add(Meta.Stats.PSOHash, Entry);
}
// Update TOC Metadata usage masks - don't clear NewPSOUsage as we are using a TempTOC
MergePSOUsageToMetaData(NewPSOUsage, TempTOC.MetaData, UnixTime);
for (auto& Pair : Stats)
{
auto* MetaPtr = TempTOC.MetaData.Find(Pair.Key);
if (MetaPtr)
{
auto& Meta = *MetaPtr;
check(Meta.Stats.PSOHash == Pair.Value->PSOHash);
Meta.Stats.CreateCount += Pair.Value->CreateCount;
if (Pair.Value->FirstFrameUsed > Meta.Stats.FirstFrameUsed)
{
Meta.Stats.FirstFrameUsed = Pair.Value->FirstFrameUsed;
}
if (Pair.Value->LastFrameUsed > Meta.Stats.LastFrameUsed)
{
Meta.Stats.LastFrameUsed = Pair.Value->LastFrameUsed;
}
Meta.Stats.TotalBindCount = (int64)FMath::Min((uint64)INT64_MAX, (uint64)FMath::Max(Meta.Stats.TotalBindCount, 0ll) + (uint64)FMath::Max(Pair.Value->TotalBindCount, 0ll));
}
}
for (auto It = TempTOC.MetaData.CreateIterator(); It; ++It)
{
FPipelineStateStats const* Stat = Stats.FindRef(It->Key);
bool bUsed = (Stat && (Stat->TotalBindCount > 0));
if (bUsed)
{
if (!PSOs.Contains(It->Key))
{
check(It->Value.FileSize > 0);
FMemoryReader* Reader = GetFileCacheReaderFromGuid(It->Value.FileGuid);
if (Reader)
{
UE_LOG(LogRHI, VeryVerbose, TEXT("reading PSO (%u) from guid %s, Off %" UINT64_FMT " // %" INT64_FMT), It->Key, *It->Value.FileGuid.ToString(), (It->Value.FileOffset), Reader->TotalSize());
check(It->Value.FileOffset < (uint32)Reader->TotalSize());
Reader->Seek(It->Value.FileOffset);
FPipelineCacheFileFormatPSO PSO;
(*Reader) << PSO;
PSOs.Add(It->Key, PSO);
}
else
{
FString GameGuids;
Algo::ForEach(FPipelineFileCacheManager::FileCacheMap, [&GameGuids](auto& MapPair) { GameGuids += FString::Printf(TEXT("[%s - %s]"), *MapPair.Value->Name, *MapPair.Value->FileGuid.ToString()); });
UE_LOG(LogRHI, Display, \
TEXT("Trying to reconcile from unknown file GUID: %s but bound log file is: %s user file is: %s and the currently known game files are: %s - this means you have stale entries in a local cache file or the relevant game content file is yet to be mounted."), \
*(It->Value.FileGuid.ToString()), *(Header.Guid.ToString()), *(FileGuid.ToString()), *(GameGuids));
RemovedEntries++;
It.RemoveCurrent();
}
}
}
else
{
RemovedEntries++;
It.RemoveCurrent();
}
}
TotalEntries = TempTOC.MetaData.Num();
SortMetaData(TempTOC.MetaData, Order);
TempTOC.SortedOrder = Order;
uint64 TempTOCOffset = (uint64)FileWriter->Tell();
uint64 PSOOffset = TempTOCOffset;
for (auto& Entry : TempTOC.MetaData)
{
FPipelineCacheFileFormatPSO& PSO = PSOs.FindChecked(Entry.Key);
FileWriter->Seek(PSOOffset);
Entry.Value.FileGuid = Header.Guid;
Entry.Value.FileOffset = PSOOffset;
int64 At = FileWriter->Tell();
(*FileWriter) << PSO;
Entry.Value.FileSize = FileWriter->Tell() - At;
PSOOffset += Entry.Value.FileSize;
check(PSOOffset == FileWriter->Tell());
NumNewEntries++;
}
Header.TableOffset = PSOOffset;
TempTOCOffset = PSOOffset;
FileWriter->Seek(Header.TableOffset);
*FileWriter << TempTOC;
break;
}
default:
{
check(false);
break;
}
}
// Overwrite the header now that we have the TOC location.
FileWriter->Seek(0);
*FileWriter << Header;
FileWriter->Flush();
bFileWriteSuccess = !FileWriter->IsError();
if(!FileWriter->Close())
{
bFileWriteSuccess = false;
}
if (bFileWriteSuccess && bUseMemoryWriter)
{
if (TotalEntries > 0)
{
bFileWriteSuccess = FFileHelper::SaveArrayToFile(SaveBytes, *TempPath);
}
else
{
delete FileWriter;
float ThisTimeMS = float(FPlatformTime::Seconds() - StartTime) * 1000.0f;
UE_LOG(LogRHI, Log, TEXT("FPipelineFileCacheManager skipping saving empty .upipelinecache (took %6.2fms): %s."), ThisTimeMS, *SaveFilePath);
return false;
}
}
if (bFileWriteSuccess)
{
delete FileWriter;
// As on POSIX only file moves on the same device are atomic
if ((SaveFilePath == TempPath) || IFileManager::Get().Move(*SaveFilePath, *TempPath, true, true, true, true))
{
float ThisTimeMS = float(FPlatformTime::Seconds() - StartTime) * 1000.0f;
TCHAR const* ModeName = nullptr;
switch (Mode)
{
default:
checkNoEntry();
case FPipelineFileCacheManager::SaveMode::Incremental:
ModeName = TEXT("Incremental");
break;
case FPipelineFileCacheManager::SaveMode::BoundPSOsOnly:
ModeName = TEXT("BoundPSOsOnly");
break;
}
UE_LOG(LogRHI, Log, TEXT("FPipelineFileCacheManager %s saved %u total, %u new, %u removed, %u cons .upipelinecache (took %6.2fms): %s."), ModeName, TotalEntries, NumNewEntries, RemovedEntries, ConsolidatedEntries, ThisTimeMS, *SaveFilePath);
if (JournalPath.Len())
{
IFileManager::Get().Delete(*JournalPath);
}
}
else
{
float ThisTimeMS = float(FPlatformTime::Seconds() - StartTime) * 1000.0f;
UE_LOG(LogRHI, Error, TEXT("Failed to move .upipelinecache from %s to %s (took %6.2fms)."), *TempPath, *SaveFilePath, ThisTimeMS);
}
}
else
{
delete FileWriter;
IFileManager::Get().Delete(*TempPath);
float ThisTimeMS = float(FPlatformTime::Seconds() - StartTime) * 1000.0f;
UE_LOG(LogRHI, Error, TEXT("Failed to write .upipelinecache, (took %6.2fms): %s."), ThisTimeMS, *SaveFilePath);
}
}
else
{
UE_LOG(LogRHI, Error, TEXT("Failed to open .upipelinecache for write: %s."), *SaveFilePath);
}
}
}
INC_MEMORY_STAT_BY(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
return bFileWriteSuccess;
}
bool IsPSOEntryCachedInternal(FPipelineCacheFileFormatPSO const& NewEntry, FPSOUsageData* EntryData = nullptr) const
{
uint32 PSOHash = GetTypeHash(NewEntry);
check(!EntryData || EntryData->PSOHash == PSOHash);
FPipelineCacheFileFormatPSOMetaData const * const Existing = TOC.MetaData.Find(PSOHash);
if(Existing != nullptr && EntryData != nullptr)
{
EntryData->UsageMask = Existing->UsageMask;
EntryData->EngineFlags = Existing->EngineFlags;
}
return Existing != nullptr;
}
bool IsBSSEquivalentPSOEntryCachedInternal(FPipelineCacheFileFormatPSO const& NewEntry) const
{
check(!IsPSOEntryCachedInternal(NewEntry)); // this routine should only be called after we have done the much faster test
bool bResult = false;
if (NewEntry.Type == FPipelineCacheFileFormatPSO::DescriptorType::Graphics)
{
// this is O(N) and potentially slow, measured timing is 10s of us.
TSet<FSHAHash> TempShaders;
TempShaders.Add(NewEntry.GraphicsDesc.VertexShader);
if (NewEntry.GraphicsDesc.FragmentShader != FSHAHash())
{
TempShaders.Add(NewEntry.GraphicsDesc.FragmentShader);
}
if (NewEntry.GraphicsDesc.GeometryShader != FSHAHash())
{
TempShaders.Add(NewEntry.GraphicsDesc.GeometryShader);
}
if (NewEntry.GraphicsDesc.MeshShader != FSHAHash())
{
TempShaders.Add(NewEntry.GraphicsDesc.MeshShader);
}
if (NewEntry.GraphicsDesc.AmplificationShader != FSHAHash())
{
TempShaders.Add(NewEntry.GraphicsDesc.AmplificationShader);
}
for (auto const& Hash : TOC.MetaData)
{
if (LegacyCompareEqual(TempShaders, Hash.Value.Shaders))
{
bResult = true;
break;
}
}
}
return bResult;
}
static void SortMetaData(TMap<uint32, FPipelineCacheFileFormatPSOMetaData>& MetaData, FPipelineFileCacheManager::PSOOrder Order)
{
// Only sorting metadata ordering - this should not affect PSO data offsets / lookups
switch(Order)
{
case FPipelineFileCacheManager::PSOOrder::FirstToLatestUsed:
{
MetaData.ValueSort([](const FPipelineCacheFileFormatPSOMetaData& A, const FPipelineCacheFileFormatPSOMetaData& B) {return A.Stats.FirstFrameUsed > B.Stats.FirstFrameUsed;});
break;
}
case FPipelineFileCacheManager::PSOOrder::MostToLeastUsed:
{
MetaData.ValueSort([](const FPipelineCacheFileFormatPSOMetaData& A, const FPipelineCacheFileFormatPSOMetaData& B) {return A.Stats.TotalBindCount > B.Stats.TotalBindCount;});
break;
}
case FPipelineFileCacheManager::PSOOrder::Default:
default:
{
// NOP - leave as is
break;
}
}
}
void GetOrderedPSOHashes(TArray<FPipelineCachePSOHeader>& PSOHashes, FPipelineFileCacheManager::PSOOrder Order, int64 MinBindCount, TSet<uint32> const& AlreadyCompiledHashes)
{
if(Order != TOC.SortedOrder)
{
SortMetaData(TOC.MetaData, Order);
TOC.SortedOrder = Order;
}
for (auto const& Hash : TOC.MetaData)
{
if( (Hash.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) == 0 &&
FPipelineFileCacheManager::MaskComparisonFn(FPipelineFileCacheManager::GameUsageMask, Hash.Value.UsageMask) &&
Hash.Value.Stats.TotalBindCount >= MinBindCount &&
!AlreadyCompiledHashes.Contains(Hash.Key))
{
FPipelineCachePSOHeader Header;
Header.Hash = Hash.Key;
Header.Shaders = Hash.Value.Shaders;
PSOHashes.Add(Header);
}
}
}
bool OnExternalReadCallback(FPipelineCacheFileFormatPSORead* Entry, double RemainingTime)
{
TSharedPtr<IAsyncReadRequest, ESPMode::ThreadSafe> LocalReadRequest = Entry->ReadRequest;
check(LocalReadRequest.IsValid());
if (RemainingTime < 0.0 && !LocalReadRequest->PollCompletion())
{
return false;
}
else if (RemainingTime >= 0.0 && !LocalReadRequest->WaitCompletion(RemainingTime))
{
return false;
}
Entry->bReadCompleted = 1;
return true;
}
void FetchPSODescriptors(TDoubleLinkedList<FPipelineCacheFileFormatPSORead*>& Batch)
{
for (TDoubleLinkedList<FPipelineCacheFileFormatPSORead*>::TIterator It(Batch.GetHead()); It; ++It)
{
FPipelineCacheFileFormatPSORead* Entry = *It;
FPipelineCacheFileFormatPSOMetaData const& Meta = TOC.MetaData.FindChecked(Entry->Hash);
if((Meta.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
// In reality we should not get to this case as GetOrderedPSOHashes() won't pass back PSOs that have this flag set
UE_LOG(LogRHI, Verbose, TEXT("Encountered a PSO entry %u marked invalid - ignoring"), Entry->Hash);
Entry->bValid = false;
continue;
}
if (Meta.FileGuid == FileGuid)
{
FPipelineCacheFileFormatPSOMetaData const* GameMeta = TOC.MetaData.Find(Entry->Hash);
if (GameMeta && ensure(AsyncFileHandle.IsValid()))
{
Entry->Data.SetNum(GameMeta->FileSize);
Entry->ParentFileHandle = AsyncFileHandle;
Entry->ReadRequest = MakeShareable(AsyncFileHandle->ReadRequest(GameMeta->FileOffset, GameMeta->FileSize, AIOP_Normal, nullptr, Entry->Data.GetData()));
}
else
{
UE_LOG(LogRHI, Verbose, TEXT("Encountered a PSO entry %u that has been removed from the cache file: %s "), Entry->Hash, *Meta.FileGuid.ToString());
Entry->bValid = false;
continue;
}
}
else
{
UE_LOG(LogRHI, Warning, TEXT("Encountered a PSO entry %u that references unknown file ID: %s"), Entry->Hash, *Meta.FileGuid.ToString());
Entry->bValid = false;
continue;
}
Entry->bValid = true;
FExternalReadCallback ExternalReadCallback = [this, Entry](double ReaminingTime)
{
return this->OnExternalReadCallback(Entry, ReaminingTime);
};
if (!Entry->Ar || !Entry->Ar->AttachExternalReadDependency(ExternalReadCallback))
{
ExternalReadCallback(0.0);
check(Entry->bReadCompleted);
}
}
}
FName GetPlatformName() const
{
return PlatformName;
}
const FString& GetRecordingFilename() const
{
return RecordingFilename;
}
const FString& GetCacheFilename() const
{
return Name;
}
const FGuid& GetFileGuid() const
{
return FileGuid;
}
const int32 GetTOCMetaDataSize() const
{
return TOC.MetaData.Num();
}
};
uint32 FPipelineCacheFile::GameVersion = 0;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool FPipelineFileCacheManager::IsBSSEquivalentPSOEntryCached(FPipelineCacheFileFormatPSO const& NewEntry)
{
check(!IsPSOEntryCached(NewEntry)); // this routine should only be called after we have done the much faster test
bool bFound = false;
for (auto MapIt = FileCacheMap.CreateIterator(); !bFound && MapIt; ++MapIt)
{
bFound = MapIt->Value->IsBSSEquivalentPSOEntryCachedInternal(NewEntry);
}
return bFound;
}
// note: when EntryData is supplied it also performs a state update, we may need to verify all occurrences are coherent...
bool FPipelineFileCacheManager::IsPSOEntryCached(FPipelineCacheFileFormatPSO const& NewEntry, FPSOUsageData* EntryData)
{
bool bFound = false;
for (auto MapIt = FileCacheMap.CreateIterator(); !bFound && MapIt; ++MapIt)
{
bFound = MapIt->Value->IsPSOEntryCachedInternal(NewEntry, EntryData);
}
return bFound;
}
bool FPipelineFileCacheManager::IsPipelineFileCacheEnabled()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("psocache"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing PSO cache from command line"));
}
return FileCacheEnabled && (bCmdLineForce || CVarPSOFileCacheEnabled.GetValueOnAnyThread() == 1);
}
bool FPipelineFileCacheManager::LogPSOtoFileCache()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("logpso"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing logging of PSOs from command line"));
}
return (bCmdLineForce || CVarPSOFileCacheLogPSO.GetValueOnAnyThread() == 1);
}
bool FPipelineFileCacheManager::ReportNewPSOs()
{
static bool bOnce = false;
static bool bCmdLineForce = false;
if (!bOnce)
{
bOnce = true;
bCmdLineForce = FParse::Param(FCommandLine::Get(), TEXT("reportpso"));
UE_CLOG(bCmdLineForce, LogRHI, Warning, TEXT("****************************** Forcing reporting of new PSOs from command line"));
}
return (bCmdLineForce || CVarPSOFileCacheReportPSO.GetValueOnAnyThread() == 1);
}
bool FPipelineFileCacheManager::LogPSODetails()
{
static bool bOnce = false;
static bool bCmdLineOption = false;
#if !UE_BUILD_SHIPPING
if (!bOnce)
{
bOnce = true;
bCmdLineOption = FParse::Param(FCommandLine::Get(), TEXT("logpsodetails"));
}
#endif
return bCmdLineOption;
}
void FPipelineFileCacheManager::Initialize(uint32 InGameVersion)
{
ClearOSPipelineCache();
// Make enabled explicit on a flag not the existence of "FileCache" object as we are using that behind a lock and in Open / Close operations
FileCacheEnabled = ShouldEnableFileCache();
FPipelineCacheFile::GameVersion = InGameVersion;
if (FPipelineCacheFile::GameVersion == 0)
{
// Defaulting the CL is fine though
FPipelineCacheFile::GameVersion = (uint32)FEngineVersion::Current().GetChangelist();
}
SET_MEMORY_STAT(STAT_NewCachedPSOMemory, 0);
SET_MEMORY_STAT(STAT_PSOStatMemory, 0);
}
bool FPipelineFileCacheManager::ShouldEnableFileCache()
{
if (!GRHISupportsPipelineFileCache)
{
return false;
}
#if PLATFORM_IOS
if (CVarAlwaysGeneratePOSSOFileCache.GetValueOnAnyThread() == 0)
{
struct stat FileInfo;
static FString PrivateWritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/");
FString Result = PrivateWritePathBase + FString([NSString stringWithFormat:@"/Caches/%@/com.apple.metal/functions.data", [NSBundle mainBundle].bundleIdentifier]);
FString Result2 = PrivateWritePathBase + FString([NSString stringWithFormat:@"/Caches/%@/com.apple.metal/usecache.txt", [NSBundle mainBundle].bundleIdentifier]);
if (stat(TCHAR_TO_UTF8(*Result), &FileInfo) != -1 && stat(TCHAR_TO_UTF8(*Result2), &FileInfo) != -1)
{
return false;
}
}
#endif
return true;
}
void FPipelineFileCacheManager::PreCompileComplete()
{
#if PLATFORM_IOS
// write out a file signifying we have completed a pre-compile of the PSO cache. Used on successive runs of the game to determine how much caching we need to still perform
static FString PrivateWritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/");
FString Result = PrivateWritePathBase + FString([NSString stringWithFormat:@"/Caches/%@/com.apple.metal/usecache.txt", [NSBundle mainBundle].bundleIdentifier]);
int32 Handle = open(TCHAR_TO_UTF8(*Result), O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
FString Version = FEngineVersion::Current().ToString();
write(Handle, TCHAR_TO_ANSI(*Version), Version.Len());
close(Handle);
#endif
}
void FPipelineFileCacheManager::ClearOSPipelineCache()
{
UE_LOG(LogTemp, Display, TEXT("Clearing the OS Cache"));
bool bCmdLineSkip = FParse::Param(FCommandLine::Get(), TEXT("skippsoclear"));
if (CVarClearOSPSOFileCache.GetValueOnAnyThread() > 0 && !bCmdLineSkip)
{
// clear the PSO cache on IOS if the executable is newer
#if PLATFORM_IOS
SCOPED_AUTORELEASE_POOL;
static FString ExecutablePath = FString([[NSBundle mainBundle] bundlePath]) + TEXT("/") + FPlatformProcess::ExecutableName();
struct stat FileInfo;
if(stat(TCHAR_TO_UTF8(*ExecutablePath), &FileInfo) != -1)
{
// TODO: add ability to only do this change on major release as opposed to minor release (e.g. 10.30 -> 10.40 (delete) vs 10.40 -> 10.40.1 (don't delete)), this is very much game specific, so need a way to have games be able to modify this
FTimespan ExecutableTime(0, 0, FileInfo.st_atime);
static FString PrivateWritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/");
FString Result = PrivateWritePathBase + FString([NSString stringWithFormat:@"/Caches/%@/com.apple.metal/functions.data", [NSBundle mainBundle].bundleIdentifier]);
if (stat(TCHAR_TO_UTF8(*Result), &FileInfo) != -1)
{
FTimespan DataTime(0, 0, FileInfo.st_atime);
if (ExecutableTime > DataTime)
{
UE_LOG(LogTemp, Display, TEXT("Clearing functions.data"));
unlink(TCHAR_TO_UTF8(*Result));
}
}
Result = PrivateWritePathBase + FString([NSString stringWithFormat:@"/Caches/%@/com.apple.metal/functions.maps", [NSBundle mainBundle].bundleIdentifier]);
if (stat(TCHAR_TO_UTF8(*Result), &FileInfo) != -1)
{
FTimespan MapsTime(0, 0, FileInfo.st_atime);
if (ExecutableTime > MapsTime)
{
UE_LOG(LogTemp, Display, TEXT("Clearing functions.maps"));
unlink(TCHAR_TO_UTF8(*Result));
}
}
}
#elif PLATFORM_MAC && (UE_BUILD_TEST || UE_BUILD_SHIPPING)
if (!FPlatformProcess::IsSandboxedApplication())
{
SCOPED_AUTORELEASE_POOL;
static FString ExecutablePath = FString([[NSBundle mainBundle] executablePath]);
struct stat FileInfo;
if (stat(TCHAR_TO_UTF8(*ExecutablePath), &FileInfo) != -1)
{
FTimespan ExecutableTime(0, 0, FileInfo.st_atime);
FString CacheDir = FString([NSString stringWithFormat:@"%@/../C/%@/com.apple.metal", NSTemporaryDirectory(), [NSBundle mainBundle].bundleIdentifier]);
TArray<FString> FoundFiles;
IPlatformFile::GetPlatformPhysical().FindFilesRecursively(FoundFiles, *CacheDir, TEXT(".data"));
// Find functions.data file in cache subfolders. If it's older than the executable, delete the whole cache.
bool bIsCacheOutdated = false;
for (FString& DataFile : FoundFiles)
{
if (FPaths::GetCleanFilename(DataFile) == TEXT("functions.data") && stat(TCHAR_TO_UTF8(*DataFile), &FileInfo) != -1)
{
FTimespan DataTime(0, 0, FileInfo.st_atime);
if (ExecutableTime > DataTime)
{
bIsCacheOutdated = true;
}
}
}
if (bIsCacheOutdated)
{
IPlatformFile::GetPlatformPhysical().DeleteDirectoryRecursively(*CacheDir);
}
}
}
#endif
}
}
int32 FPipelineFileCacheManager::GetTotalPSOCount(const FString& PSOCacheKey)
{
int32 TotalPSOs = 0;
if (IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
FPipelineCacheFile* Found = GetPipelineCacheFileFromKey(PSOCacheKey);
TotalPSOs = Found ? Found->GetTOCMetaDataSize() : 0;
}
return TotalPSOs;
}
uint64 FPipelineFileCacheManager::SetGameUsageMaskWithComparison(uint64 InGameUsageMask, FPSOMaskComparisonFn InComparisonFnPtr)
{
uint64 OldMask = 0;
if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
OldMask = FPipelineFileCacheManager::GameUsageMask;
FPipelineFileCacheManager::GameUsageMask = InGameUsageMask;
if(InComparisonFnPtr == nullptr)
{
InComparisonFnPtr = DefaultPSOMaskComparisonFunction;
}
FPipelineFileCacheManager::MaskComparisonFn = InComparisonFnPtr;
FPipelineFileCacheManager::GameUsageMaskSet = true;
}
return OldMask;
}
void FPipelineFileCacheManager::Shutdown()
{
if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
for (auto const& Pair : Stats)
{
delete Pair.Value;
}
Stats.Empty();
NewPSOs.Empty();
NewPSOHashes.Empty();
NumNewPSOs = 0;
FileCacheMap.Empty();
FileCacheEnabled = false;
SET_MEMORY_STAT(STAT_NewCachedPSOMemory, 0);
SET_MEMORY_STAT(STAT_PSOStatMemory, 0);
}
}
bool FPipelineFileCacheManager::HasPipelineFileCache(const FString & Key)
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
return FileCacheMap.Contains(Key);
}
bool FPipelineFileCacheManager::OpenPipelineFileCache(const FString& Key, const FString& CacheName, EShaderPlatform Platform, FGuid& OutGameFileGuid)
{
bool bOk = false;
OutGameFileGuid = FGuid();
if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
bool bFound = FileCacheMap.Contains(Key);
if(!bFound)
{
TUniquePtr<FPipelineCacheFile> NewFileCache = MakeUnique<FPipelineCacheFile>();
bOk = NewFileCache->OpenPipelineFileCache(CacheName, Platform, OutGameFileGuid);
if (!bOk)
{
NewFileCache = nullptr;
}
else
{
UE_LOG(LogRHI, Display, TEXT("FPipelineCacheFile[%s] opened %s, filename %s, guid %s. "), *Key, *CacheName, *NewFileCache->GetCacheFilename(), *NewFileCache->GetFileGuid().ToString());
FileCacheMap.Add(Key, MoveTemp(NewFileCache));
check(!GameGuidToCacheKey.Contains(OutGameFileGuid));
GameGuidToCacheKey.Add(OutGameFileGuid, Key);
}
}
}
return bOk;
}
bool FPipelineFileCacheManager::OpenUserPipelineFileCache(const FString& Key, const FString& CacheName, EShaderPlatform Platform, FGuid& OutGameFileGuid)
{
// close any existing cache and reset the user cache's PSO recording containers.
CloseUserPipelineFileCache();
bool bUserFileOpened = false;
if (IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
UserCacheKey = Key;
FPipelineCacheFile* FileCache = GetPipelineCacheFileFromKey(UserCacheKey);
if(ensure(!FileCache))
{
TUniquePtr<FPipelineCacheFile> NewFileCache = MakeUnique<FPipelineCacheFile>();
FileCache = NewFileCache.Get();
bUserFileOpened = NewFileCache->OpenUserPipelineFileCache(CacheName, Platform);
// we always add the user cache, even if we did not open a file
FileCacheMap.Add(UserCacheKey, MoveTemp(NewFileCache));
check(!GameGuidToCacheKey.Contains(FileCache->GetFileGuid()));
GameGuidToCacheKey.Add(FileCache->GetFileGuid(), UserCacheKey);
}
OutGameFileGuid = FileCache->GetFileGuid();
UE_LOG(LogRHI, Display, TEXT("FPipelineCacheFile User cache [key:%s] opened '%s'=%d, filename %s, guid %s. "), *UserCacheKey, *CacheName, (int)bUserFileOpened, *FileCache->GetCacheFilename(), *FileCache->GetFileGuid().ToString());
// User Cache now exists - these caches should be empty for this file otherwise will have false positives from any previous file caching - if not something has been caching when it should not be
check(NewPSOs.Num() == 0);
check(NewPSOHashes.Num() == 0);
check(RunTimeToPSOUsage.Num() == 0);
}
return bUserFileOpened;
}
void FPipelineFileCacheManager::CloseUserPipelineFileCache()
{
if (IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
if(GetPipelineCacheFileFromKey(UserCacheKey))
{
const FGuid& UserGuid = GetPipelineCacheFileFromKey(UserCacheKey)->GetFileGuid();
GameGuidToCacheKey.Remove(UserGuid);
FileCacheMap.Remove(UserCacheKey);
}
// Reset stats tracking for the next file.
for (auto const& Pair : Stats)
{
FPlatformAtomics::InterlockedExchange((int64*)&Pair.Value->TotalBindCount, -1);
FPlatformAtomics::InterlockedExchange((int64*)&Pair.Value->FirstFrameUsed, -1);
FPlatformAtomics::InterlockedExchange((int64*)&Pair.Value->LastFrameUsed, -1);
}
// Reset serialized counts
SET_DWORD_STAT(STAT_SerializedGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_SerializedComputePipelineStateCount, 0);
// Not tracking when there is no file clear other stats as well
SET_DWORD_STAT(STAT_TotalGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_TotalComputePipelineStateCount, 0);
SET_DWORD_STAT(STAT_TotalRayTracingPipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewGraphicsPipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewComputePipelineStateCount, 0);
SET_DWORD_STAT(STAT_NewRayTracingPipelineStateCount, 0);
// Clear Runtime hashes otherwise we can't start adding newPSO's for a newly opened file
RunTimeToPSOUsage.Empty();
NewPSOUsage.Empty();
NewPSOs.Empty();
NewPSOHashes.Empty();
NumNewPSOs = 0;
SET_MEMORY_STAT(STAT_NewCachedPSOMemory, 0);
}
}
bool FPipelineFileCacheManager::SavePipelineFileCache(SaveMode Mode)
{
bool bOk = false;
if (IsPipelineFileCacheEnabled() && LogPSOtoFileCache())
{
CSV_EVENT(PSO, TEXT("Saving PSO cache"));
FRWScopeLock Lock(FileCacheLock, SLT_Write);
FPipelineCacheFile* UserCache = GetPipelineCacheFileFromKey(UserCacheKey);
bOk = (UserCache != nullptr) && UserCache->SavePipelineFileCache(Mode, Stats, NewPSOs, RequestedOrder, NewPSOUsage);
// If successful clear new PSO's as they should have been saved out
// Leave everything else in-tact (e.g stats) for subsequent in place save operations
if (bOk)
{
NumNewPSOs = NewPSOs.Num();
SET_MEMORY_STAT(STAT_NewCachedPSOMemory, (NumNewPSOs * (sizeof(FPipelineCacheFileFormatPSO) + sizeof(uint32) + sizeof(uint32))));
}
}
return bOk;
}
void FPipelineFileCacheManager::RegisterPSOUsageDataUpdateForNextSave(FPSOUsageData& UsageData)
{
FPSOUsageData& CurrentEntry = NewPSOUsage.FindOrAdd(UsageData.PSOHash);
CurrentEntry.PSOHash = UsageData.PSOHash;
CurrentEntry.UsageMask |= UsageData.UsageMask;
CurrentEntry.EngineFlags |= UsageData.EngineFlags;
}
void FPipelineFileCacheManager::LogNewGraphicsPSOToConsoleAndCSV(FPipelineCacheFileFormatPSO& PSO, uint32 PSOHash, bool bWasPSOPrecached)
{
if (!LogNewPSOsToConsoleAndCSV)
{
return;
}
if (!bWasPSOPrecached)
{
CSV_EVENT(PSO, TEXT("Encountered new graphics PSO"));
UE_LOG(LogRHI, Display, TEXT("Encountered a new graphics PSO: %u"), PSOHash);
int32 LogDetailLevel = LogPSODetails() ? 2 : GPSOFileCachePrintNewPSODescriptors;
if (LogDetailLevel > 0)
{
UE_LOG(LogRHI, Display, TEXT("New Graphics PSO (%u)"), PSOHash);
if (LogDetailLevel > 1)
{
UE_LOG(LogRHI, Display, TEXT("%s"), *PSO.ToStringReadable());
}
}
}
else
{
UE_LOG(LogRHI, Verbose, TEXT("Encountered a new graphics PSO for the file cache but it was already precached at runtime: %u"), PSOHash);
}
}
void FPipelineFileCacheManager::LogNewComputePSOToConsoleAndCSV(FPipelineCacheFileFormatPSO& PSO, uint32 PSOHash, bool bWasPSOPrecached)
{
if (!LogNewPSOsToConsoleAndCSV)
{
return;
}
if (!bWasPSOPrecached)
{
CSV_EVENT(PSO, TEXT("Encountered new compute PSO"));
UE_LOG(LogRHI, Display, TEXT("Encountered a new compute PSO: %u"), PSOHash);
if (GPSOFileCachePrintNewPSODescriptors > 0)
{
UE_LOG(LogRHI, Display, TEXT("New compute PSO (%u) Description: %s"), PSOHash, *PSO.ComputeDesc.ComputeShader.ToString());
}
}
else
{
UE_LOG(LogRHI, Verbose, TEXT("Encountered a new compute PSO for the file cache but it was already precached at runtime: %u"), PSOHash);
}
}
void FPipelineFileCacheManager::LogNewRaytracingPSOToConsole(FPipelineCacheFileFormatPSO& PSO, uint32 PSOHash, bool bIsNonBlockingPSO)
{
// When non-blocking creation is used, encountering a non-cached RTPSO is not likely to cause a hitch and so the logging is not useful/actionable.
if (!LogNewPSOsToConsoleAndCSV || bIsNonBlockingPSO)
{
return;
}
UE_LOG(LogRHI, Display, TEXT("Encountered a new ray tracing PSO: %u"), PSOHash);
if (GPSOFileCachePrintNewPSODescriptors > 0)
{
UE_LOG(LogRHI, Display, TEXT("New ray tracing PSO (%u) Description: %s"), PSOHash, *PSO.RayTracingDesc.ToString());
}
}
void FPipelineFileCacheManager::BroadcastNewPSOsDelegate()
{
TArray<FPipelineCacheFileFormatPSO> PSOs;
{
FPipelineCacheFileFormatPSO PSO;
while (NewPSOsToReport.Dequeue(PSO))
{
PSOs.Emplace(MoveTemp(PSO));
}
}
if (!PSOs.IsEmpty() && ReportNewPSOs())
{
// It's not safe to touch UObjects-based delegates from the render thread.
ExecuteOnGameThread(TEXT("OnPipelineStateLoggedBroadcastGT"), [InPSOs = MoveTemp(PSOs)]()
{
if (PSOLoggedEvent.IsBound())
{
for (const FPipelineCacheFileFormatPSO& PSO : InPSOs)
{
PSOLoggedEvent.Broadcast(PSO);
}
}
});
}
}
void FPipelineFileCacheManager::CacheGraphicsPSO(uint32 RunTimeHash, FGraphicsPipelineStateInitializer const& Initializer, bool bWasPSOPrecached, FPipelineStateStats** OutStats)
{
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()))
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
Lock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr)
{
FPipelineCacheFileFormatPSO NewEntry;
bool bOK = FPipelineCacheFileFormatPSO::Init(NewEntry, Initializer);
check(bOK);
uint32 PSOHash = GetTypeHash(NewEntry);
FPSOUsageData CurrentUsageData(PSOHash, 0, 0);
if (!FPipelineFileCacheManager::IsPSOEntryCached(NewEntry, &CurrentUsageData))
{
bool bActuallyNewPSO = !NewPSOHashes.Contains(PSOHash);
if (Initializer.bFromPSOFileCache)
{
// FIXME: this is a workaround. Needs proper investigation
UE_LOG(LogRHI, Warning, TEXT("PSO from the cache was not found in the cache! PSOHash: %u"), PSOHash);
bActuallyNewPSO = false;
}
if (bActuallyNewPSO && IsOpenGLPlatform(GMaxRHIShaderPlatform)) // OpenGL is a BSS platform and so we don't report BSS matches as missing.
{
bActuallyNewPSO = !FPipelineFileCacheManager::IsBSSEquivalentPSOEntryCached(NewEntry);
}
if (bActuallyNewPSO)
{
LogNewGraphicsPSOToConsoleAndCSV(NewEntry, PSOHash, bWasPSOPrecached);
if (bWasPSOPrecached)
{
bActuallyNewPSO = !GPSOExcludePrecachePSOsInFileCache;
}
}
if (bActuallyNewPSO)
{
if (LogPSOtoFileCache())
{
NewPSOs.Add(NewEntry);
INC_MEMORY_STAT_BY(STAT_NewCachedPSOMemory, sizeof(FPipelineCacheFileFormatPSO) + sizeof(uint32) + sizeof(uint32));
}
NewPSOHashes.Add(PSOHash);
NumNewPSOs++;
INC_DWORD_STAT(STAT_NewGraphicsPipelineStateCount);
INC_DWORD_STAT(STAT_TotalGraphicsPipelineStateCount);
if (ReportNewPSOs())
{
NewPSOsToReport.Enqueue(MoveTemp(NewEntry));
}
}
}
// Only set if the file cache doesn't have this Mask for the PSO - avoid making more entries and unnessary file saves
if(!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, CurrentUsageData.UsageMask))
{
CurrentUsageData.UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
}
// Apply the existing file PSO Usage mask and current to our "fast" runtime check
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
}
else if(!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
PSOUsage->UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
}
}
// Optionally supply stats to share the lock scope
if (OutStats)
{
*OutStats = RegisterPSOStatsInternal(Lock, RunTimeHash);
}
}
}
void FPipelineFileCacheManager::CacheComputePSO(uint32 RunTimeHash, FRHIComputeShader const* Initializer, bool bWasPSOPrecached, FPipelineStateStats** OutStats)
{
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()))
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
{
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
Lock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr)
{
FPipelineCacheFileFormatPSO NewEntry;
bool bOK = FPipelineCacheFileFormatPSO::Init(NewEntry, Initializer);
check(bOK);
uint32 PSOHash = GetTypeHash(NewEntry);
FPSOUsageData CurrentUsageData(PSOHash, 0, 0);
if (!FPipelineFileCacheManager::IsPSOEntryCached(NewEntry, &CurrentUsageData))
{
bool bActuallyNewPSO = !NewPSOHashes.Contains(PSOHash);
if (bActuallyNewPSO)
{
LogNewComputePSOToConsoleAndCSV(NewEntry, PSOHash, bWasPSOPrecached);
if (bWasPSOPrecached)
{
bActuallyNewPSO = !GPSOExcludePrecachePSOsInFileCache;
}
}
if (bActuallyNewPSO)
{
if (LogPSOtoFileCache())
{
NewPSOs.Add(NewEntry);
INC_MEMORY_STAT_BY(STAT_NewCachedPSOMemory, sizeof(FPipelineCacheFileFormatPSO) + sizeof(uint32) + sizeof(uint32));
}
NewPSOHashes.Add(PSOHash);
NumNewPSOs++;
INC_DWORD_STAT(STAT_NewComputePipelineStateCount);
INC_DWORD_STAT(STAT_TotalComputePipelineStateCount);
if (ReportNewPSOs())
{
NewPSOsToReport.Enqueue(MoveTemp(NewEntry));
}
}
}
// Only set if the file cache doesn't have this Mask for the PSO - avoid making more entries and unnessary file saves
if(!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, CurrentUsageData.UsageMask))
{
CurrentUsageData.UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
}
// Apply the existing file PSO Usage mask and current to our "fast" runtime check
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
}
else if(!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
PSOUsage->UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
}
}
}
// Optionally supply stats to share the lock scope
if (OutStats)
{
*OutStats = RegisterPSOStatsInternal(Lock, RunTimeHash);
}
}
}
void FPipelineFileCacheManager::CacheRayTracingPSO(const FRayTracingPipelineStateInitializer& Initializer, ERayTracingPipelineCacheFlags Flags)
{
if (!IsPipelineFileCacheEnabled() || !(LogPSOtoFileCache() || ReportNewPSOs()))
{
return;
}
TArrayView<FRHIRayTracingShader*> ShaderTables[] =
{
Initializer.GetRayGenTable(),
Initializer.GetMissTable(),
Initializer.GetHitGroupTable(),
Initializer.GetCallableTable()
};
const bool bIsNonBlocking = !EnumHasAnyFlags(Flags, ERayTracingPipelineCacheFlags::NonBlocking);
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
for (TArrayView<FRHIRayTracingShader*>& Table : ShaderTables)
{
for (FRHIRayTracingShader* Shader : Table)
{
FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc Desc(Initializer, Shader);
uint32 RunTimeHash = GetTypeHash(Desc);
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if (PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
Lock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if (PSOUsage == nullptr)
{
FPipelineCacheFileFormatPSO NewEntry;
bool bOK = FPipelineCacheFileFormatPSO::Init(NewEntry, Desc);
check(bOK);
uint32 PSOHash = GetTypeHash(NewEntry);
FPSOUsageData CurrentUsageData(PSOHash, 0, 0);
if (!FPipelineFileCacheManager::IsPSOEntryCached(NewEntry, &CurrentUsageData))
{
LogNewRaytracingPSOToConsole(NewEntry, PSOHash, bIsNonBlocking);
if (LogPSOtoFileCache())
{
NewPSOs.Add(NewEntry);
INC_MEMORY_STAT_BY(STAT_NewCachedPSOMemory, sizeof(FPipelineCacheFileFormatPSO) + sizeof(uint32) + sizeof(uint32));
}
NumNewPSOs++;
INC_DWORD_STAT(STAT_NewRayTracingPipelineStateCount);
INC_DWORD_STAT(STAT_TotalRayTracingPipelineStateCount);
if (ReportNewPSOs())
{
NewPSOsToReport.Enqueue(MoveTemp(NewEntry));
}
}
// Only set if the file cache doesn't have this Mask for the PSO - avoid making more entries and unnessary file saves
if (!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, CurrentUsageData.UsageMask))
{
CurrentUsageData.UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
}
// Apply the existing file PSO Usage mask and current to our "fast" runtime check
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
// Immediately register usage of this ray tracing shader
FPipelineStateStats* Stat = Stats.FindRef(PSOHash);
if (Stat == nullptr)
{
Stat = new FPipelineStateStats;
Stat->FirstFrameUsed = 0;
Stat->LastFrameUsed = 0;
Stat->CreateCount = 1;
Stat->TotalBindCount = 1;
Stat->PSOHash = PSOHash;
Stats.Add(PSOHash, Stat);
INC_MEMORY_STAT_BY(STAT_PSOStatMemory, sizeof(FPipelineStateStats) + sizeof(uint32));
}
}
}
else if (!IsReferenceMaskSet(FPipelineFileCacheManager::GameUsageMask, PSOUsage->UsageMask))
{
PSOUsage->UsageMask |= FPipelineFileCacheManager::GameUsageMask;
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
}
}
}
}
void FPipelineFileCacheManager::RegisterPSOCompileFailure(uint32 RunTimeHash, FGraphicsPipelineStateInitializer const& Initializer)
{
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()) && Initializer.bFromPSOFileCache)
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineCacheFlagInvalidPSO, PSOUsage->EngineFlags))
{
Lock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if(PSOUsage == nullptr)
{
FPipelineCacheFileFormatPSO ShouldBeExistingEntry;
bool bOK = FPipelineCacheFileFormatPSO::Init(ShouldBeExistingEntry, Initializer);
check(bOK);
uint32 PSOHash = GetTypeHash(ShouldBeExistingEntry);
FPSOUsageData CurrentUsageData(PSOHash, 0, 0);
bool bCached = FPipelineFileCacheManager::IsPSOEntryCached(ShouldBeExistingEntry, &CurrentUsageData);
check(bCached); //bFromPSOFileCache was set but not in the cache something has gone wrong
{
CurrentUsageData.EngineFlags |= FPipelineCacheFlagInvalidPSO;
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
UE_LOG(LogRHI, Warning, TEXT("Graphics PSO (%u) compile failure registering to File Cache"), PSOHash);
}
}
else if(!IsReferenceMaskSet(FPipelineCacheFlagInvalidPSO, PSOUsage->EngineFlags))
{
PSOUsage->EngineFlags |= FPipelineCacheFlagInvalidPSO;
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
UE_LOG(LogRHI, Warning, TEXT("Graphics PSO (%u) compile failure registering to File Cache"), PSOUsage->PSOHash);
}
}
}
}
FPipelineStateStats* FPipelineFileCacheManager::RegisterPSOStats(uint32 RunTimeHash)
{
if(IsPipelineFileCacheEnabled() && LogPSOtoFileCache())
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
return RegisterPSOStatsInternal(Lock, RunTimeHash);
}
return nullptr;
}
FPipelineStateStats* FPipelineFileCacheManager::RegisterPSOStatsInternal(FRWScopeLock& Lock, uint32 RunTimeHash)
{
// May fail registration if the user cache has been closed
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
if (!PSOUsage)
{
UE_LOG(LogRHI, Display, TEXT("PSO (%u) missing from cache, please instead use the OutStats parameter in CacheGraphicsPSO/CacheComputePSO"), RunTimeHash);
return nullptr;
}
uint32 PSOHash = PSOUsage->PSOHash;
FPipelineStateStats* Stat = Stats.FindRef(PSOHash);
if (!Stat)
{
Lock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
Stat = Stats.FindRef(PSOHash);
if (!Stat)
{
Stat = new FPipelineStateStats;
Stat->PSOHash = PSOHash;
Stats.Add(PSOHash, Stat);
INC_MEMORY_STAT_BY(STAT_PSOStatMemory, sizeof(FPipelineStateStats) + sizeof(uint32));
}
}
Stat->CreateCount++;
return Stat;
}
void FPipelineFileCacheManager::GetOrderedPSOHashes(const FString& PSOCacheKey, TArray<FPipelineCachePSOHeader>& PSOHashes, PSOOrder Order, int64 MinBindCount, TSet<uint32> const& AlreadyCompiledHashes)
{
if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_Write);
RequestedOrder = Order;
FPipelineCacheFile* FileCache = GetPipelineCacheFileFromKey(PSOCacheKey);
if(FileCache)
{
FileCache->GetOrderedPSOHashes(PSOHashes, Order, MinBindCount, AlreadyCompiledHashes);
}
}
}
void FPipelineFileCacheManager::FetchPSODescriptors(const FString& PSOCacheKey, TDoubleLinkedList<FPipelineCacheFileFormatPSORead*>& Batch)
{
if(IsPipelineFileCacheEnabled())
{
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
FPipelineCacheFile* FileCache = GetPipelineCacheFileFromKey(PSOCacheKey);
if(FileCache)
{
FileCache->FetchPSODescriptors(Batch);
}
}
}
struct FPipelineCacheFileData
{
FPipelineCacheFileFormatHeader Header;
TMap<uint32, FPipelineCacheFileFormatPSO> PSOs;
FPipelineCacheFileFormatTOC TOC;
static FPipelineCacheFileData Open(FString const& FilePath)
{
FPipelineCacheFileData Data;
Data.Header.Magic = 0;
FArchive* FileAReader = IFileManager::Get().CreateFileReader(*FilePath);
if (FileAReader)
{
*FileAReader << Data.Header;
if (Data.Header.Magic == FPipelineCacheFileFormatMagic && Data.Header.Version >= (uint32)EPipelineCacheFileFormatVersions::FirstWorking)
{
FileAReader->SetGameNetVer(Data.Header.Version);
check(Data.Header.TableOffset > 0);
FileAReader->Seek(Data.Header.TableOffset);
*FileAReader << Data.TOC;
if (!FileAReader->IsError())
{
for (auto& Entry : Data.TOC.MetaData)
{
if ( (Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) == 0 &&
Entry.Value.FileGuid == Data.Header.Guid &&
Entry.Value.FileSize > sizeof(FPipelineCacheFileFormatPSO::DescriptorType))
{
FPipelineCacheFileFormatPSO PSO;
FileAReader->Seek(Entry.Value.FileOffset);
*FileAReader << PSO;
#if PSO_COOKONLY_DATA
// Tools get cook data populated into the PSO as the PSOs can be independant from Meta data
if(Data.Header.Version >= (uint32)EPipelineCacheFileFormatVersions::PSOUsageMask)
{
PSO.UsageMask = Entry.Value.UsageMask;
}
if(Data.Header.Version >= (uint32)EPipelineCacheFileFormatVersions::PSOBindCount)
{
PSO.BindCount = Entry.Value.Stats.TotalBindCount;
}
#endif
Data.PSOs.Add(Entry.Key, PSO);
}
}
}
if (FileAReader->IsError())
{
UE_LOG(LogRHI, Error, TEXT("Failed to read: %s."), *FilePath);
Data.Header.Magic = 0;
}
else
{
if (Data.Header.Version < (uint32)EPipelineCacheFileFormatVersions::ShaderMetaData)
{
for (auto& Entry : Data.TOC.MetaData)
{
FPipelineCacheFileFormatPSO& PSO = Data.PSOs.FindChecked(Entry.Key);
switch(PSO.Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
Entry.Value.Shaders.Add(PSO.ComputeDesc.ComputeShader);
break;
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
Entry.Value.Shaders.Add(PSO.GraphicsDesc.VertexShader);
if (PSO.GraphicsDesc.FragmentShader != FSHAHash())
{
Entry.Value.Shaders.Add(PSO.GraphicsDesc.FragmentShader);
}
if (PSO.GraphicsDesc.GeometryShader != FSHAHash())
{
Entry.Value.Shaders.Add(PSO.GraphicsDesc.GeometryShader);
}
if (PSO.GraphicsDesc.MeshShader != FSHAHash())
{
Entry.Value.Shaders.Add(PSO.GraphicsDesc.MeshShader);
}
if (PSO.GraphicsDesc.AmplificationShader != FSHAHash())
{
Entry.Value.Shaders.Add(PSO.GraphicsDesc.AmplificationShader);
}
break;
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
Entry.Value.Shaders.Add(PSO.RayTracingDesc.ShaderHash);
break;
default:
check(false);
break;
}
}
}
if (Data.Header.Version < (uint32)EPipelineCacheFileFormatVersions::SortedVertexDesc)
{
TMap<uint32, FPipelineCacheFileFormatPSOMetaData> MetaData;
TMap<uint32, FPipelineCacheFileFormatPSO> PSOs;
for (auto& Entry : Data.TOC.MetaData)
{
FPipelineCacheFileFormatPSO& PSO = Data.PSOs.FindChecked(Entry.Key);
PSOs.Add(GetTypeHash(PSO), PSO);
check(Entry.Value.FileGuid != FGuid());
MetaData.Add(GetTypeHash(PSO), Entry.Value);
}
Data.TOC.MetaData = MetaData;
Data.PSOs = PSOs;
}
Data.Header.Version = FPipelineCacheFileFormatCurrentVersion;
}
}
FileAReader->Close();
delete FileAReader;
}
else
{
UE_LOG(LogRHI, Error, TEXT("Failed to open: %s."), *FilePath);
}
return Data;
}
};
uint32 FPipelineFileCacheManager::NumPSOsLogged()
{
uint32 Result = 0;
if(IsPipelineFileCacheEnabled() && LogPSOtoFileCache())
{
// Only count PSOs that are both new and have at least one bind or have been marked invalid (compile failure) otherwise we can ignore them
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
// We now need to know if the number of usage masks changes - this number should be as least the same as before but could be conceptually more if an existing PSO has an extra usage mask applied
if(NewPSOUsage.Num() > 0)
{
for(auto& MaskEntry : NewPSOUsage)
{
FPipelineStateStats const* Stat = Stats.FindRef(MaskEntry.Key);
if ((Stat && Stat->TotalBindCount > 0) || (MaskEntry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
Result++;
}
}
}
if(Result == 0 && NumNewPSOs > 0)
{
// This can happen if the Mask was zero at some point
for (auto& PSO : NewPSOs)
{
FPipelineStateStats const* Stat = Stats.FindRef(GetTypeHash(PSO));
if (Stat && Stat->TotalBindCount > 0)
{
Result++;
}
}
}
}
return Result;
}
FPipelineFileCacheManager::FPipelineStateLoggedEvent& FPipelineFileCacheManager::OnPipelineStateLogged()
{
return PSOLoggedEvent;
}
bool FPipelineFileCacheManager::LoadPipelineFileCacheInto(FString const& Path, TSet<FPipelineCacheFileFormatPSO>& PSOs)
{
FPipelineCacheFileData A = FPipelineCacheFileData::Open(Path);
bool bAny = false;
for (const auto& Pair : A.PSOs)
{
PSOs.Add(Pair.Value);
bAny = true;
}
return bAny;
}
bool FPipelineFileCacheManager::SavePipelineFileCacheFrom(uint32 GameVersion, EShaderPlatform Platform, FString const& Path, const TSet<FPipelineCacheFileFormatPSO>& PSOs)
{
FPipelineCacheFileData Output;
Output.Header.Magic = FPipelineCacheFileFormatMagic;
Output.Header.Version = FPipelineCacheFileFormatCurrentVersion;
Output.Header.GameVersion = GameVersion;
Output.Header.Platform = Platform;
Output.Header.TableOffset = 0;
Output.Header.Guid = FGuid::NewGuid();
Output.TOC.MetaData.Reserve(PSOs.Num());
for (const FPipelineCacheFileFormatPSO& Item : PSOs)
{
FPipelineCacheFileFormatPSOMetaData Meta;
Meta.Stats.PSOHash = GetTypeHash(Item);
Meta.FileGuid = Output.Header.Guid;
Meta.FileSize = 0;
#if PSO_COOKONLY_DATA
Meta.UsageMask = Item.UsageMask;
Meta.Stats.TotalBindCount = Item.BindCount;
#endif
switch (Item.Type)
{
case FPipelineCacheFileFormatPSO::DescriptorType::Compute:
{
INC_DWORD_STAT(STAT_SerializedComputePipelineStateCount);
Meta.Shaders.Add(Item.ComputeDesc.ComputeShader);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::Graphics:
{
INC_DWORD_STAT(STAT_SerializedGraphicsPipelineStateCount);
if (Item.GraphicsDesc.VertexShader != FSHAHash())
Meta.Shaders.Add(Item.GraphicsDesc.VertexShader);
if (Item.GraphicsDesc.FragmentShader != FSHAHash())
Meta.Shaders.Add(Item.GraphicsDesc.FragmentShader);
if (Item.GraphicsDesc.GeometryShader != FSHAHash())
Meta.Shaders.Add(Item.GraphicsDesc.GeometryShader);
if (Item.GraphicsDesc.MeshShader != FSHAHash())
Meta.Shaders.Add(Item.GraphicsDesc.MeshShader);
if (Item.GraphicsDesc.AmplificationShader != FSHAHash())
Meta.Shaders.Add(Item.GraphicsDesc.AmplificationShader);
break;
}
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
{
INC_DWORD_STAT(STAT_SerializedRayTracingPipelineStateCount);
Meta.Shaders.Add(Item.RayTracingDesc.ShaderHash);
break;
}
default:
{
check(false);
break;
}
}
check(Meta.FileGuid != FGuid());
Output.TOC.MetaData.Add(Meta.Stats.PSOHash, Meta);
Output.PSOs.Add(Meta.Stats.PSOHash, Item);
}
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*Path);
if (!FileWriter)
{
return false;
}
FileWriter->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
*FileWriter << Output.Header;
uint64 PSOOffset = (uint64)FileWriter->Tell();
for (auto& Entry : Output.TOC.MetaData)
{
FPipelineCacheFileFormatPSO& PSO = Output.PSOs.FindChecked(Entry.Key);
uint32 PSOHash = Entry.Key;
Entry.Value.FileOffset = PSOOffset;
Entry.Value.FileGuid = Output.Header.Guid;
TArray<uint8> Bytes;
FMemoryWriter Wr(Bytes);
Wr.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
Wr << PSO;
FileWriter->Serialize(Bytes.GetData(), Wr.TotalSize());
Entry.Value.FileSize = Wr.TotalSize();
PSOOffset += Entry.Value.FileSize;
}
FileWriter->Seek(0);
Output.Header.TableOffset = PSOOffset;
*FileWriter << Output.Header;
FileWriter->Seek(PSOOffset);
*FileWriter << Output.TOC;
FileWriter->Flush();
bool bOK = !FileWriter->IsError();
FileWriter->Close();
delete FileWriter;
return bOK;
}
bool FPipelineFileCacheManager::MergePipelineFileCaches(FString const& PathA, FString const& PathB, FPipelineFileCacheManager::PSOOrder Order, FString const& OutputPath)
{
bool bOK = false;
FPipelineCacheFileData A = FPipelineCacheFileData::Open(PathA);
FPipelineCacheFileData B = FPipelineCacheFileData::Open(PathB);
if (A.Header.Magic == FPipelineCacheFileFormatMagic && B.Header.Magic == FPipelineCacheFileFormatMagic && A.Header.GameVersion == B.Header.GameVersion && A.Header.Platform == B.Header.Platform && A.Header.Version == FPipelineCacheFileFormatCurrentVersion && B.Header.Version == FPipelineCacheFileFormatCurrentVersion)
{
FPipelineCacheFileData Output;
Output.Header.Magic = FPipelineCacheFileFormatMagic;
Output.Header.Version = FPipelineCacheFileFormatCurrentVersion;
Output.Header.GameVersion = A.Header.GameVersion;
Output.Header.Platform = A.Header.Platform;
Output.Header.TableOffset = 0;
Output.Header.Guid = FGuid::NewGuid();
uint32 MergeCount = 0;
for (auto const& Entry : A.TOC.MetaData)
{
// Don't merge PSOs that have the invalid bit set
if((Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
continue;
}
Output.TOC.MetaData.Add(Entry.Key, Entry.Value);
}
for (auto const& Entry : B.TOC.MetaData)
{
// Don't merge PSOs that have the invalid bit set
if((Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
{
continue;
}
// Make sure these usage masks for the same PSOHash find their way in
auto* ExistingMetaEntry = Output.TOC.MetaData.Find(Entry.Key);
if(ExistingMetaEntry != nullptr)
{
ExistingMetaEntry->UsageMask |= Entry.Value.UsageMask;
ExistingMetaEntry->EngineFlags |= Entry.Value.EngineFlags;
++MergeCount;
}
else
{
Output.TOC.MetaData.Add(Entry.Key, Entry.Value);
}
}
FPipelineCacheFile::SortMetaData(Output.TOC.MetaData, Order);
Output.TOC.SortedOrder = Order;
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*OutputPath);
if (FileWriter)
{
FileWriter->SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
FileWriter->Seek(0);
*FileWriter << Output.Header;
uint64 PSOOffset = (uint64)FileWriter->Tell();
TSet<uint32> HashesToRemove;
for (auto& Entry : Output.TOC.MetaData)
{
FPipelineCacheFileFormatPSO PSO;
if (Entry.Value.FileGuid == A.Header.Guid)
{
PSO = A.PSOs.FindChecked(Entry.Key);
}
else if (Entry.Value.FileGuid == B.Header.Guid)
{
PSO = B.PSOs.FindChecked(Entry.Key);
}
else
{
HashesToRemove.Add(Entry.Key);
continue;
}
uint32 PSOHash = Entry.Key;
Entry.Value.FileOffset = PSOOffset;
Entry.Value.FileGuid = Output.Header.Guid;
TArray<uint8> Bytes;
FMemoryWriter Wr(Bytes);
Wr.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
Wr << PSO;
FileWriter->Serialize(Bytes.GetData(), Wr.TotalSize());
Entry.Value.FileSize = Wr.TotalSize();
PSOOffset += Entry.Value.FileSize;
}
for (uint32 Key : HashesToRemove)
{
Output.TOC.MetaData.Remove(Key);
}
FileWriter->Seek(0);
Output.Header.TableOffset = PSOOffset;
*FileWriter << Output.Header;
FileWriter->Seek(PSOOffset);
*FileWriter << Output.TOC;
FileWriter->Flush();
bOK = !FileWriter->IsError();
UE_CLOG(!bOK, LogRHI, Error, TEXT("Failed to write output file: %s."), *OutputPath);
FileWriter->Close();
delete FileWriter;
}
else
{
UE_LOG(LogRHI, Error, TEXT("Failed to open output file: %s."), *OutputPath);
}
}
else if (A.Header.GameVersion != B.Header.GameVersion)
{
UE_LOG(LogRHI, Error, TEXT("Incompatible game versions: %u vs. %u."), A.Header.GameVersion, B.Header.GameVersion);
}
else if (A.Header.Platform != B.Header.Platform)
{
UE_LOG(LogRHI, Error, TEXT("Incompatible shader platforms: %s vs. %s."), *LegacyShaderPlatformToShaderFormat(A.Header.Platform).ToString(), *LegacyShaderPlatformToShaderFormat(B.Header.Platform).ToString());
}
else if (A.Header.Version != B.Header.Version)
{
UE_LOG(LogRHI, Error, TEXT("Incompatible file versions: %u vs. %u."), A.Header.Version, B.Header.Version);
}
else
{
UE_LOG(LogRHI, Error, TEXT("Incompatible file headers: %" UINT64_FMT " vs. %" UINT64_FMT ": expected %" UINT64_FMT "."), A.Header.Magic, B.Header.Magic, FPipelineCacheFileFormatMagic);
}
return bOK;
}
FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::FPipelineFileCacheRayTracingDesc(const FRayTracingPipelineStateInitializer& Initializer, const FRHIRayTracingShader* ShaderRHI)
: ShaderHash(ShaderRHI->GetHash())
, Frequency(ShaderRHI->GetFrequency())
, ShaderBindingLayout(Initializer.ShaderBindingLayout ? *Initializer.ShaderBindingLayout : FRHIShaderBindingLayout())
{
}
FString FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::HeaderLine() const
{
FString Result(TEXT("RayTracingShader,DeprecatedMaxPayloadSizeInBytes,Frequency,ShaderBindingLayout-Hash,ShaderBindingLayout-Flags,ShaderBindingLayout-NumUBEntries"));
for (int32 Index = 0; Index < FRHIShaderBindingLayout::MaxUniformBufferEntries; Index++)
{
Result += FString::Printf(TEXT(",%s%d,%s%d,%s%d,%s%d,%s%d,%s%d")
, TEXT("ShaderBindingLayout-UBEntry-Name"), Index
, TEXT("ShaderBindingLayout-UBEntry-RegisterSpace"), Index
, TEXT("ShaderBindingLayout-UBEntry-CBVResourceIndex"), Index
, TEXT("ShaderBindingLayout-UBEntry-BaseSRVResourceIndex"), Index
, TEXT("ShaderBindingLayout-UBEntry-BaseUAVResourceIndex"), Index
, TEXT("ShaderBindingLayout-UBEntry-BaseSamplerResourceIndex"), Index
);
}
return Result;
}
FString FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::ToString() const
{
FString Result = FString::Printf(TEXT("%s,%d,%d,%d,%d,%d")
, *ShaderHash.ToString()
, DeprecatedMaxPayloadSizeInBytes
, uint32(Frequency)
, ShaderBindingLayout.GetHash()
, ShaderBindingLayout.GetFlags()
, ShaderBindingLayout.GetNumUniformBufferEntries()
);
for (uint32 Index = 0; Index < FRHIShaderBindingLayout::MaxUniformBufferEntries; Index++)
{
FRHIUniformBufferShaderBindingLayout Entry = Index < ShaderBindingLayout.GetNumUniformBufferEntries() ? ShaderBindingLayout.GetUniformBufferEntry(Index) : FRHIUniformBufferShaderBindingLayout();
Result += FString::Printf(TEXT(",%s,%d,%d,%d,%d,%d")
, Entry.LayoutName.Len() > 0 ? *Entry.LayoutName : TEXT("None")
, Entry.RegisterSpace
, Entry.CBVResourceIndex
, Entry.BaseSRVResourceIndex
, Entry.BaseUAVResourceIndex
, Entry.BaseSamplerResourceIndex
);
}
return Result;
}
void FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::AddToReadableString(TReadableStringBuilder& OutBuilder) const
{
// TODO: probably needs a better implementation once we get to this
switch (Frequency)
{
case SF_RayGen:
OutBuilder << TEXT(" RGS:");
break;
case SF_RayCallable:
OutBuilder << TEXT(" RCS:");
break;
case SF_RayHitGroup:
OutBuilder << TEXT(" RHGS:");
break;
case SF_RayMiss:
OutBuilder << TEXT(" RMS:");
break;
}
OutBuilder << ShaderHash.ToString();
OutBuilder << TEXT(" AHGI ");
}
void FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::FromString(const FString& Src)
{
TArray<FString> Parts;
Src.TrimStartAndEnd().ParseIntoArray(Parts, TEXT(","));
// make sure we have required number of parts
if (Parts.Num() != (6 + 6 * FRHIShaderBindingLayout::MaxUniformBufferEntries))
{
return;
}
ShaderHash.FromString(Parts[0]);
// Not used, but kept for back-compatibility
LexFromString(DeprecatedMaxPayloadSizeInBytes, Parts[1]);
uint32 Temp = 0;
LexFromString(Temp, Parts[2]);
Frequency = EShaderFrequency(Temp);
uint32 ShaderBiningLayoutHash;
LexFromString(ShaderBiningLayoutHash, Parts[3]);
LexFromString(Temp, Parts[4]);
EShaderBindingLayoutFlags ShaderBindingLayoutFlags = (EShaderBindingLayoutFlags) Temp;
uint32 ShaderBindingLayoutNumUniformBuffers;
LexFromString(ShaderBindingLayoutNumUniformBuffers, Parts[5]);
uint32 CurrentPartIndex = 6;
TArray<FRHIUniformBufferShaderBindingLayout> ShaderBindingLayoutUBEntries;
ShaderBindingLayoutUBEntries.SetNum(ShaderBindingLayoutNumUniformBuffers);
for (uint32 UBEntryIndex = 0; UBEntryIndex < ShaderBindingLayoutNumUniformBuffers; ++UBEntryIndex)
{
FRHIUniformBufferShaderBindingLayout& UBEntry = ShaderBindingLayoutUBEntries[UBEntryIndex];
UBEntry.LayoutName = Parts[CurrentPartIndex++];
LexFromString(Temp, Parts[CurrentPartIndex++]);
UBEntry.RegisterSpace = Temp;
LexFromString(Temp, Parts[CurrentPartIndex++]);
UBEntry.CBVResourceIndex = Temp;
LexFromString(Temp, Parts[CurrentPartIndex++]);
UBEntry.BaseSRVResourceIndex = Temp;
LexFromString(Temp, Parts[CurrentPartIndex++]);
UBEntry.BaseUAVResourceIndex = Temp;
LexFromString(Temp, Parts[CurrentPartIndex++]);
UBEntry.BaseSamplerResourceIndex = Temp;
}
ShaderBindingLayout = FRHIShaderBindingLayout(ShaderBindingLayoutFlags, ShaderBindingLayoutUBEntries);
check(ShaderBindingLayout.GetHash() == ShaderBiningLayoutHash);
}
bool FPipelineCacheFileFormatPSO::Init(FPipelineCacheFileFormatPSO& PSO, FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc const& Desc)
{
PSO.Type = DescriptorType::RayTracing;
#if PSO_COOKONLY_DATA
PSO.UsageMask = 0;
PSO.BindCount = 0;
#endif
PSO.RayTracingDesc = Desc;
return true;
}