You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
FPipelineCacheFileFormatPSO::Init() used to assume that a vertex declaration is present in the pipeline initializer. However, this may not be the case when mesh shaders are used. #rb Jason.Nadro #preflight 6262b821d558dfdec38ee839 [CL 19865181 by Yuriy ODonnell in ue5-main branch]
4198 lines
146 KiB
C++
4198 lines
146 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
PipelineFileCache.cpp: Pipeline state cache implementation.
|
|
=============================================================================*/
|
|
|
|
#include "PipelineFileCache.h"
|
|
#include "PipelineStateCache.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/EngineVersion.h"
|
|
#include "Serialization/Archive.h"
|
|
#include "Serialization/MemoryReader.h"
|
|
#include "Serialization/MemoryWriter.h"
|
|
#include "RHI.h"
|
|
#include "RHIResources.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 "String/LexFromString.h"
|
|
#include "String/ParseTokens.h"
|
|
#include "Misc/ScopeExit.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,
|
|
};
|
|
|
|
const uint64 FPipelineCacheFileFormatMagic = 0x5049504543414348; // PIPECACH
|
|
const uint64 FPipelineCacheTOCFileFormatMagic = 0x544F435354415232; // TOCSTAR2
|
|
const uint64 FPipelineCacheEOFFileFormatMagic = 0x454F462D4D41524B; // EOF-MARK
|
|
const RHI_API uint32 FPipelineCacheFileFormatCurrentVersion = (uint32)EPipelineCacheFileFormatVersions::AddingDepthClipMode;
|
|
const int32 FPipelineCacheGraphicsDescPartsNum = 66; // 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."),
|
|
ECVF_Default | ECVF_RenderThreadSafe
|
|
);
|
|
|
|
static int32 GPSOFileCachePrintNewPSODescriptors = UE_BUILD_SHIPPING ? 0 : 1;
|
|
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 the PSO. Defaults to 0 in *Shipping* builds, otherwise 1."),
|
|
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 FPipelineFileCache::FileCacheLock;
|
|
FPipelineCacheFile* FPipelineFileCache::FileCache = nullptr;
|
|
TMap<uint32, FPSOUsageData> FPipelineFileCache::RunTimeToPSOUsage;
|
|
TMap<uint32, FPSOUsageData> FPipelineFileCache::NewPSOUsage;
|
|
TMap<uint32, FPipelineStateStats*> FPipelineFileCache::Stats;
|
|
TSet<FPipelineCacheFileFormatPSO> FPipelineFileCache::NewPSOs;
|
|
TSet<uint32> FPipelineFileCache::NewPSOHashes;
|
|
uint32 FPipelineFileCache::NumNewPSOs;
|
|
FPipelineFileCache::PSOOrder FPipelineFileCache::RequestedOrder = FPipelineFileCache::PSOOrder::MostToLeastUsed;
|
|
bool FPipelineFileCache::FileCacheEnabled = false;
|
|
FPipelineFileCache::FPipelineStateLoggedEvent FPipelineFileCache::PSOLoggedEvent;
|
|
uint64 FPipelineFileCache::GameUsageMask = 0;
|
|
|
|
static int64 GetCurrentUnixTime()
|
|
{
|
|
return FDateTime::UtcNow().ToUnixTimestamp();
|
|
}
|
|
|
|
bool DefaultPSOMaskComparisonFunction(uint64 ReferenceMask, uint64 PSOMask)
|
|
{
|
|
return (ReferenceMask & PSOMask) == ReferenceMask;
|
|
}
|
|
FPSOMaskComparisonFn FPipelineFileCache::MaskComparisonFn = DefaultPSOMaskComparisonFunction;
|
|
|
|
static inline bool IsReferenceMaskSet(uint64 ReferenceMask, uint64 PSOMask)
|
|
{
|
|
return (ReferenceMask & PSOMask) == ReferenceMask;
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
|
|
FString FPipelineFileCacheRasterizerState::ToString() const
|
|
{
|
|
return FString::Printf(TEXT("<%f %f %u %u %u %u>")
|
|
, DepthBias
|
|
, SlopeScaleDepthBias
|
|
, uint32(FillMode)
|
|
, uint32(CullMode)
|
|
, uint32(DepthClipMode)
|
|
, uint32(!!bAllowMSAA)
|
|
, uint32(!!bEnableLineAA)
|
|
);
|
|
}
|
|
|
|
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(bAllowMSAA) == 1 && sizeof(bEnableLineAA) == 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++);
|
|
LexFromString((uint8&)bEnableLineAA, *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 (FragmentShader != FSHAHash())
|
|
{
|
|
OutBuilder << TEXT(" PS:");
|
|
OutBuilder << FragmentShader;
|
|
}
|
|
if (GeometryShader != FSHAHash())
|
|
{
|
|
OutBuilder << TEXT(" GS:");
|
|
OutBuilder << GeometryShader;
|
|
}
|
|
}
|
|
|
|
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)
|
|
);
|
|
|
|
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(" MSAA:");
|
|
OutBuilder << MSAASamples;
|
|
OutBuilder << TEXT(" DSfmt:");
|
|
OutBuilder << uint32(DepthStencilFormat);
|
|
OutBuilder << TEXT(" DSflags:");
|
|
OutBuilder << uint32(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 << uint32(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(" 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)
|
|
{
|
|
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 && sizeof(EPixelFormat) == sizeof(uint32)); //not a very robust parser
|
|
LexFromString(MSAASamples, *PartIt++);
|
|
LexFromString((uint32&)DepthStencilFormat, *PartIt++);
|
|
LexFromString(DepthStencilFlags, *PartIt++);
|
|
|
|
check(PartEnd - PartIt >= 5 && sizeof(DepthLoad) == 1 && sizeof(StencilLoad) == 1 && sizeof(DepthStore) == 1 && sizeof(StencilStore) == 1 && sizeof(PrimitiveType) == 4); //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 && sizeof(ERenderTargetLoadAction) == 1 && sizeof(ERenderTargetStoreAction) == 1 && sizeof(EPixelFormat) == sizeof(uint32)); //not a very robust parser
|
|
LexFromString((uint32&)(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;
|
|
}
|
|
|
|
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 & (TexCreate_SRGB | TexCreate_Shared));
|
|
}
|
|
|
|
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("\"%d,%llu\""), Count, Mask);
|
|
}
|
|
|
|
FString FPipelineCacheFileFormatPSO::ToStringReadable()
|
|
{
|
|
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.VertexShader != FSHAHash() && GraphicsDesc.MeshShader != FSHAHash())
|
|
{
|
|
// Vertex shader and mesh shader are mutually exclusive
|
|
return false;
|
|
}
|
|
|
|
if (GraphicsDesc.MeshShader != FSHAHash() && GraphicsDesc.VertexDescriptor.Num() > 0)
|
|
{
|
|
// mesh shader should not have descriptors
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
if(GraphicsDesc.PrimitiveType >= PT_1_ControlPointPatchList && GraphicsDesc.PrimitiveType <= PT_32_ControlPointPatchList)
|
|
{
|
|
// Define says we don't support tessellation - can't draw patches - not a valid PSO for target platform
|
|
return false;
|
|
}
|
|
|
|
#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)
|
|
{
|
|
if(FPlatformAtomics::AtomicRead((volatile int32*)&Key.Hash) == 0)
|
|
{
|
|
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);
|
|
|
|
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.RasterizerState.bEnableLineAA, sizeof(Key.GraphicsDesc.RasterizerState.bEnableLineAA), 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();
|
|
}
|
|
}
|
|
FPlatformAtomics::InterlockedCompareExchange((volatile int32*)&Key.Hash, KeyHash, 0);
|
|
}
|
|
return FPlatformAtomics::AtomicRead((volatile int32*)&Key.Hash);
|
|
}
|
|
|
|
/*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>(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 = static_cast<ETextureCreateFlags>(DepthStencilFlags);
|
|
}
|
|
else
|
|
{
|
|
static_assert(sizeof(uint64) == sizeof(Info.GraphicsDesc.DepthStencilFlags), "ETextureCreateFlags size changed, please change serialization");
|
|
Ar << Info.GraphicsDesc.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;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FPipelineCacheFileFormatPSO::DescriptorType::RayTracing:
|
|
{
|
|
Ar << Info.RayTracingDesc.ShaderHash;
|
|
Ar << Info.RayTracingDesc.MaxPayloadSizeInBytes;
|
|
|
|
uint32 Frequency = uint32(Info.RayTracingDesc.Frequency);
|
|
Ar << Frequency;
|
|
Info.RayTracingDesc.Frequency = EShaderFrequency(Frequency);
|
|
|
|
Ar << Info.RayTracingDesc.bAllowHitGroupIndexing;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
checkNoEntry();
|
|
}
|
|
}
|
|
return Ar;
|
|
}
|
|
|
|
FPipelineCacheFileFormatPSO::FPipelineCacheFileFormatPSO()
|
|
: Hash(0)
|
|
#if PSO_COOKONLY_DATA
|
|
, UsageMask(0)
|
|
, BindCount(0)
|
|
#endif
|
|
{
|
|
}
|
|
|
|
/*static*/ bool FPipelineCacheFileFormatPSO::Init(FPipelineCacheFileFormatPSO& PSO, FRHIComputeShader const* Init)
|
|
{
|
|
check(Init);
|
|
|
|
PSO.Hash = 0;
|
|
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.Hash = 0;
|
|
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 = 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;
|
|
|
|
#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;
|
|
/* Ignore: [Hash == Other.Hash] in this test. */
|
|
#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 &&
|
|
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)
|
|
, Hash(Other.Hash)
|
|
#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;
|
|
Hash = Other.Hash;
|
|
#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(FPipelineFileCache::PSOOrder::MostToLeastUsed)
|
|
{}
|
|
|
|
FPipelineFileCache::PSOOrder SortedOrder;
|
|
TMap<uint32, FPipelineCacheFileFormatPSOMetaData> MetaData;
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
class FPipelineCacheFile
|
|
{
|
|
public:
|
|
static uint32 GameVersion;
|
|
|
|
FPipelineCacheFile()
|
|
: TOCOffset(0)
|
|
, UserFileGuid(FGuid::NewGuid())
|
|
, UserAsyncFileHandle(nullptr)
|
|
, GameAsyncFileHandle(nullptr)
|
|
{
|
|
}
|
|
|
|
bool OpenPipelineFileCache(FString const& FilePath, FGuid& Guid, TSharedPtr<IAsyncReadFileHandle, ESPMode::ThreadSafe>& Handle, FPipelineCacheFileFormatTOC& Content)
|
|
{
|
|
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;
|
|
TOCOffset = 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)
|
|
{
|
|
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, GameFileGuid=this->GameFileGuid, EvictionTime](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
|
|
{
|
|
return MetaData.FileGuid != GameFileGuid;
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
|
|
bool ShouldLoadUserCache()
|
|
{
|
|
return FPipelineFileCache::LogPSOtoFileCache() && (CVarPSOFileCacheSaveUserCache.GetValueOnAnyThread() > 0);
|
|
}
|
|
|
|
bool OpenPipelineFileCache(FString const& FileName, EShaderPlatform Platform, FGuid& OutGameFileGuid)
|
|
{
|
|
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);
|
|
|
|
OutGameFileGuid = FGuid();
|
|
TOC.SortedOrder = FPipelineFileCache::PSOOrder::Default;
|
|
TOC.MetaData.Empty();
|
|
|
|
Name = FileName;
|
|
|
|
ShaderPlatform = Platform;
|
|
PlatformName = LegacyShaderPlatformToShaderFormat(Platform);
|
|
|
|
|
|
FString GamePathStable = FPaths::ProjectContentDir() / TEXT("PipelineCaches") / ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()) / FString::Printf(TEXT("%s_%s.stable.upipelinecache"), *FileName, *PlatformName.ToString());
|
|
FString GamePath = FPaths::ProjectContentDir() / TEXT("PipelineCaches") / ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()) / FString::Printf(TEXT("%s_%s.upipelinecache"), *FileName, *PlatformName.ToString());
|
|
|
|
static bool bCommandLineNotStable = FParse::Param(FCommandLine::Get(), TEXT("nostablepipelinecache"));
|
|
if (!bCommandLineNotStable)
|
|
{
|
|
GamePath = GamePathStable;
|
|
}
|
|
|
|
FString FilePath = FPaths::ProjectSavedDir() / FString::Printf(TEXT("%s_%s.upipelinecache"), *FileName, *PlatformName.ToString());
|
|
|
|
RecordingFilename = FString::Printf(TEXT("%s-CL-%u-"), *FEngineVersion::Current().GetBranchDescriptor(), FEngineVersion::Current().GetChangelist());
|
|
|
|
FGuid UniqueFileGuid;
|
|
FPlatformMisc::CreateGuid(UniqueFileGuid); // not very unique on android, but won't matter much here
|
|
RecordingFilename += FString::Printf(TEXT("%s_%s_%s.rec.upipelinecache"), *FileName, *PlatformName.ToString(), *UniqueFileGuid.ToString());
|
|
RecordingFilename = FPaths::ProjectSavedDir() / TEXT("CollectedPSOs") / RecordingFilename;
|
|
|
|
UE_LOG(LogRHI, Log, TEXT("Base name for record PSOs is %s"), *RecordingFilename);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
const bool bGameFileOk = OpenPipelineFileCache(GamePath, GameFileGuid, GameAsyncFileHandle, GameTOC);
|
|
|
|
if (bGameFileOk)
|
|
{
|
|
OutGameFileGuid = GameFileGuid;
|
|
}
|
|
|
|
if (bGameFileOk && GRHISupportsLazyShaderCodeLoading && CVarLazyLoadShadersWhenPSOCacheIsPresent.GetValueOnAnyThread())
|
|
{
|
|
UE_LOG(LogRHI, Log, TEXT("Lazy loading from the shader code library is enabled."));
|
|
GRHILazyShaderCodeLoading = true;
|
|
}
|
|
|
|
bool bUserFileOk = false;
|
|
|
|
if (ShouldLoadUserCache())
|
|
{
|
|
GarbageCollectUserCache(FilePath);
|
|
|
|
FPipelineCacheFileFormatTOC UserTOC;
|
|
bUserFileOk = OpenPipelineFileCache(FilePath, UserFileGuid, UserAsyncFileHandle, UserTOC);
|
|
if (!bUserFileOk)
|
|
{
|
|
// Start the file again!
|
|
IFileManager::Get().Delete(*FilePath);
|
|
TOCOffset = 0;
|
|
}
|
|
else
|
|
{
|
|
for (auto const& Entry : UserTOC.MetaData)
|
|
{
|
|
// We want this entry that references the game version not the one from the Game TOC as that doesn't have ongoing UsageMasks bind counts etc...
|
|
auto* MetaPtr = TOC.MetaData.Find(Entry.Key);
|
|
if ((Entry.Value.FileGuid == UserFileGuid || Entry.Value.FileGuid == GameFileGuid) && (!MetaPtr || (MetaPtr->FileGuid != UserFileGuid && MetaPtr->FileGuid != GameFileGuid)))
|
|
{
|
|
TOC.MetaData.Add(Entry.Key, Entry.Value);
|
|
}
|
|
}
|
|
|
|
for (auto const& Entry : GameTOC.MetaData)
|
|
{
|
|
// If its there - don't overwrite - we'll lose mutable user cache meta data unless an old entry
|
|
auto* MetaPtr = TOC.MetaData.Find(Entry.Key);
|
|
if (!MetaPtr || (MetaPtr->FileGuid != UserFileGuid && MetaPtr->FileGuid != GameFileGuid))
|
|
{
|
|
TOC.MetaData.Add(Entry.Key, Entry.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bUserFileOk)
|
|
{
|
|
TOC = GameTOC;
|
|
}
|
|
|
|
uint32 InvalidEntryCount = 0;
|
|
|
|
for (auto const& Entry : TOC.MetaData)
|
|
{
|
|
FPipelineStateStats* Stat = FPipelineFileCache::Stats.FindRef(Entry.Key);
|
|
if (!Stat)
|
|
{
|
|
Stat = new FPipelineStateStats;
|
|
Stat->PSOHash = Entry.Key;
|
|
Stat->TotalBindCount = -1;
|
|
FPipelineFileCache::Stats.Add(Entry.Key, Stat);
|
|
}
|
|
#if !UE_BUILD_SHIPPING
|
|
if((Entry.Value.EngineFlags & FPipelineCacheFlagInvalidPSO) != 0)
|
|
{
|
|
++InvalidEntryCount;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if(InvalidEntryCount > 0)
|
|
{
|
|
UE_LOG(LogRHI, Warning, TEXT("Found %d / %d PSO entries marked as invalid."), InvalidEntryCount, TOC.MetaData.Num());
|
|
}
|
|
|
|
SET_MEMORY_STAT(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
|
|
|
|
return bGameFileOk || bUserFileOk;
|
|
}
|
|
|
|
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(FString const& FilePath, FPipelineFileCache::SaveMode Mode, TMap<uint32, FPipelineStateStats*> const& Stats, TSet<FPipelineCacheFileFormatPSO>& NewEntries, FPipelineFileCache::PSOOrder Order, TMap<uint32, FPSOUsageData>& NewPSOUsage)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_SavePipelineFileCache);
|
|
double StartTime = FPlatformTime::Seconds();
|
|
FString SaveFilePath = FilePath;
|
|
|
|
if (FPipelineFileCache::SaveMode::BoundPSOsOnly == Mode)
|
|
{
|
|
SaveFilePath = GetRecordingFilename();
|
|
}
|
|
|
|
bool bFileWriteSuccess = false;
|
|
bool bPerformWrite = true;
|
|
if (FPipelineFileCache::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 != FPipelineFileCache::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 = UserFileGuid;
|
|
Header.TableOffset = 0;
|
|
Header.LastGCUnixTime = UnixTime;
|
|
|
|
*JournalWriter << Header;
|
|
}
|
|
|
|
check(!JournalWriter->IsError());
|
|
JournalWriter->Close();
|
|
delete JournalWriter;
|
|
bPerformWrite = IFileManager::Get().FileExists(*JournalPath);
|
|
}
|
|
if (bPerformWrite)
|
|
{
|
|
FString GamePathStable = FPaths::ProjectContentDir() / TEXT("PipelineCaches") / ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()) / FString::Printf(TEXT("%s_%s.stable.upipelinecache"), *Name, *PlatformName.ToString());
|
|
FString GamePath = FPaths::ProjectContentDir() / TEXT("PipelineCaches") / ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()) / FString::Printf(TEXT("%s_%s.upipelinecache"), *Name, *PlatformName.ToString());
|
|
static bool bCommandLineNotStable = FParse::Param(FCommandLine::Get(), TEXT("nostablepipelinecache"));
|
|
if (!bCommandLineNotStable)
|
|
{
|
|
GamePath = GamePathStable;
|
|
}
|
|
|
|
int64 GameFileSize = IFileManager::Get().FileSize(*GamePath);
|
|
TArray<uint8> GameFileBytes;
|
|
int64 FileSize = IFileManager::Get().FileSize(*FilePath);
|
|
TArray<uint8> UserFileBytes;
|
|
if(FPipelineFileCache::SaveMode::Incremental != Mode)
|
|
{
|
|
if (GameFileSize > 0)
|
|
{
|
|
if (GameAsyncFileHandle.IsValid())
|
|
{
|
|
GameFileBytes.AddZeroed(GameFileSize);
|
|
IAsyncReadRequest* Request = GameAsyncFileHandle->ReadRequest(0, GameFileSize, AIOP_Normal, nullptr, GameFileBytes.GetData());
|
|
Request->WaitCompletion();
|
|
delete Request;
|
|
// Can't report errors here because the AsyncIO requests have no such mechanism.
|
|
}
|
|
else
|
|
{
|
|
bool bReadOK = FFileHelper::LoadFileToArray(GameFileBytes, *GamePath);
|
|
UE_CLOG(!bReadOK, LogRHI, Warning, TEXT("Failed to read %lld bytes from %s while re-saving the PipelineFileCache!"), GameFileSize, *GamePath);
|
|
}
|
|
}
|
|
|
|
if (FileSize > 0)
|
|
{
|
|
if (UserAsyncFileHandle.IsValid())
|
|
{
|
|
UserFileBytes.AddZeroed(FileSize);
|
|
IAsyncReadRequest* Request = UserAsyncFileHandle->ReadRequest(0, FileSize, AIOP_Normal, nullptr, UserFileBytes.GetData());
|
|
Request->WaitCompletion();
|
|
delete Request;
|
|
// Can't report errors here because the AsyncIO requests have no such mechanism.
|
|
}
|
|
else
|
|
{
|
|
bool bReadOK = FFileHelper::LoadFileToArray(UserFileBytes, *FilePath);
|
|
UE_CLOG(!bReadOK, LogRHI, Warning, TEXT("Failed to read %lld bytes from %s while re-saving the PipelineFileCache!"), FileSize, *FilePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assume caller has handled Platform specifc path + filename
|
|
TArray<uint8> SaveBytes;
|
|
FArchive* FileWriter;
|
|
bool bUseMemoryWriter = (Mode == FPipelineFileCache::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 != FPipelineFileCache::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 = UserFileGuid;
|
|
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.
|
|
case FPipelineFileCache::SaveMode::Incremental:
|
|
{
|
|
// PSO Descriptors
|
|
uint64 PSOOffset = TOCOffset;
|
|
|
|
FileWriter->Seek(PSOOffset);
|
|
|
|
// Add new entries
|
|
TotalEntries = NewEntries.Num();
|
|
for(auto It = NewEntries.CreateIterator(); It; ++It)
|
|
{
|
|
FPipelineCacheFileFormatPSO& NewEntry = *It;
|
|
check(!IsPSOEntryCached(NewEntry));
|
|
|
|
uint32 PSOHash = GetTypeHash(NewEntry);
|
|
|
|
FPipelineStateStats const* Stat = Stats.FindRef(PSOHash);
|
|
if (Stat && Stat->TotalBindCount > 0)
|
|
{
|
|
FPipelineCacheFileFormatPSOMetaData Meta;
|
|
Meta.Stats.PSOHash = PSOHash;
|
|
Meta.FileOffset = PSOOffset;
|
|
Meta.FileGuid = UserFileGuid;
|
|
Meta.AddShaders(NewEntry);
|
|
|
|
TArray<uint8> Bytes;
|
|
FMemoryWriter Wr(Bytes);
|
|
Wr.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
|
|
Wr << NewEntry;
|
|
|
|
FileWriter->Serialize(Bytes.GetData(), Wr.TotalSize());
|
|
|
|
Meta.FileSize = Wr.TotalSize();
|
|
|
|
TOC.MetaData.Add(PSOHash, Meta);
|
|
PSOOffset += Meta.FileSize;
|
|
|
|
check(PSOOffset == FileWriter->Tell());
|
|
|
|
NumNewEntries++;
|
|
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
if(Order != FPipelineFileCache::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 = FPipelineFileCache::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);
|
|
*FileWriter << TOC;
|
|
break;
|
|
}
|
|
// These modes actually save 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 FPipelineFileCache::SaveMode::BoundPSOsOnly:
|
|
{
|
|
FMemoryReader UserFileBytesReader(UserFileBytes);
|
|
FMemoryReader GameFileBytesReader(GameFileBytes);
|
|
UserFileBytesReader.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
|
|
GameFileBytesReader.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
|
|
|
|
FPipelineCacheFileFormatTOC TempTOC = TOC;
|
|
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);
|
|
|
|
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);
|
|
if (It->Value.FileGuid == UserFileGuid)
|
|
{
|
|
check(It->Value.FileOffset < (uint32)UserFileBytes.Num());
|
|
UserFileBytesReader.Seek(It->Value.FileOffset);
|
|
|
|
FPipelineCacheFileFormatPSO PSO;
|
|
UserFileBytesReader << PSO;
|
|
|
|
PSOs.Add(It->Key, PSO);
|
|
}
|
|
else if (It->Value.FileGuid == GameFileGuid)
|
|
{
|
|
check(It->Value.FileOffset < (uint32)GameFileBytes.Num());
|
|
GameFileBytesReader.Seek(It->Value.FileOffset);
|
|
|
|
FPipelineCacheFileFormatPSO PSO;
|
|
GameFileBytesReader << PSO;
|
|
|
|
PSOs.Add(It->Key, PSO);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogRHI, Verbose, TEXT("Trying to reconcile from unknown file GUID: %s but bound log file is: %s user file is: %s and game file is: %s - this means you have stale entries in a local cache file or the game content file is filled with bogus entries whose FileGUID doesn't match."), *(It->Value.FileGuid.ToString()), *(Header.Guid.ToString()), *(UserFileGuid.ToString()), *(GameFileGuid.ToString()));
|
|
|
|
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;
|
|
}
|
|
// This mode should store all the PSOs that this device binds that weren't in a game-content cache.
|
|
// It will store the meta-data for all the PSOs that are ever bound, but it will omit PSO descriptors for entries that were cached in the game-content file
|
|
// This way the user builds up a log of uncaught entries but doesn't have to replicate the entire game-content file.
|
|
case FPipelineFileCache::SaveMode::SortedBoundPSOs:
|
|
{
|
|
TMap<uint32, FPipelineCacheFileFormatPSO> PSOs;
|
|
for (auto& Entry : TOC.MetaData)
|
|
{
|
|
FPipelineCacheFileFormatPSO PSO;
|
|
uint8* Bytes = nullptr;
|
|
check(Entry.Value.FileSize > 0);
|
|
if (Entry.Value.FileGuid == UserFileGuid)
|
|
{
|
|
Bytes = &UserFileBytes[Entry.Value.FileOffset];
|
|
|
|
TArray<uint8> PSOData(Bytes, Entry.Value.FileSize);
|
|
FMemoryReader Ar(PSOData);
|
|
Ar.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
|
|
Ar << PSO;
|
|
PSOs.Add(Entry.Key, PSO);
|
|
}
|
|
else if (Entry.Value.FileGuid != GameFileGuid)
|
|
{
|
|
UE_LOG(LogRHI, Verbose, TEXT("Trying to reconcile from unknown file GUID: %s but user file is: %s and game file is: %s - this means you have stale entries in a local cache file that reference a previous version of the game content cache."), *(Entry.Value.FileGuid.ToString()), *(UserFileGuid.ToString()), *(GameFileGuid.ToString()));
|
|
}
|
|
}
|
|
|
|
for (auto& Entry : NewEntries)
|
|
{
|
|
FPipelineCacheFileFormatPSOMetaData Meta;
|
|
Meta.Stats.PSOHash = GetTypeHash(Entry);
|
|
Meta.FileOffset = 0;
|
|
Meta.FileSize = 0;
|
|
Meta.FileGuid = UserFileGuid;
|
|
Meta.AddShaders(Entry);
|
|
|
|
TOC.MetaData.Add(Meta.Stats.PSOHash, Meta);
|
|
PSOs.Add(Meta.Stats.PSOHash, Entry);
|
|
}
|
|
|
|
// Update TOC Metadata usage and clear updated entries in NewPSOUsage as file TOC is getting updated
|
|
MergePSOUsageToMetaData(NewPSOUsage, TOC.MetaData, UnixTime, true);
|
|
|
|
FPipelineCacheFileFormatTOC TempTOC = TOC;
|
|
// Update PSO usage stats for new and old
|
|
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)
|
|
{
|
|
// If the entry doesn't belong to the game content or user local cache then remove it as it is invalid
|
|
// Anything that has never been compiled (BindCount < 0) is invalid and can be removed
|
|
// Or, if the BindCount is >= 0 and the same as in the GameTOC we have never seen it and we don't need to store it in the game cache
|
|
FPipelineCacheFileFormatPSOMetaData* GameData = GameTOC.MetaData.Find(It->Key);
|
|
if ((It->Value.FileGuid != UserFileGuid && It->Value.FileGuid != GameFileGuid)
|
|
|| It->Value.Stats.TotalBindCount < 0
|
|
|| (GameData && (It->Value.Stats.TotalBindCount == GameData->Stats.TotalBindCount)))
|
|
{
|
|
RemovedEntries++;
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
TotalEntries = TempTOC.MetaData.Num();
|
|
|
|
SortMetaData(TempTOC.MetaData, Order);
|
|
TOC.SortedOrder = TempTOC.SortedOrder = Order;
|
|
|
|
TOCOffset = (uint64)FileWriter->Tell();
|
|
|
|
uint64 PSOOffset = TOCOffset;
|
|
|
|
for (auto& Entry : TempTOC.MetaData)
|
|
{
|
|
// When saved in this mode the user local file only stores the PSO descriptor for entries that weren't in the game-content cache
|
|
// We don't need to store the PSO data for entries that come from the game cache
|
|
// We do store the meta-data for all PSOs that this device has ever seen and that are valid with the current game-content and user cache.
|
|
auto& CurrentMeta = TOC.MetaData.FindChecked(Entry.Key);
|
|
if (CurrentMeta.FileGuid == UserFileGuid)
|
|
{
|
|
CurrentMeta.FileOffset = PSOOffset;
|
|
Entry.Value.FileOffset = PSOOffset;
|
|
|
|
FPipelineCacheFileFormatPSO& PSO = PSOs.FindChecked(Entry.Key);
|
|
|
|
FileWriter->Seek(PSOOffset);
|
|
|
|
TArray<uint8> Bytes;
|
|
FMemoryWriter Wr(Bytes);
|
|
Wr.SetGameNetVer(FPipelineCacheFileFormatCurrentVersion);
|
|
Wr << PSO;
|
|
|
|
NewEntries.Remove(PSO);
|
|
|
|
FileWriter->Serialize(Bytes.GetData(), Wr.TotalSize());
|
|
|
|
CurrentMeta.FileSize = Wr.TotalSize();
|
|
Entry.Value.FileSize = Wr.TotalSize();
|
|
|
|
PSOOffset += Entry.Value.FileSize;
|
|
check(PSOOffset == FileWriter->Tell());
|
|
|
|
NumNewEntries++;
|
|
}
|
|
}
|
|
|
|
Header.TableOffset = PSOOffset;
|
|
TOCOffset = 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("FPipelineFileCache 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)
|
|
{
|
|
case FPipelineFileCache::SaveMode::Incremental:
|
|
ModeName = TEXT("Incremental");
|
|
break;
|
|
case FPipelineFileCache::SaveMode::BoundPSOsOnly:
|
|
ModeName = TEXT("BoundPSOsOnly");
|
|
break;
|
|
case FPipelineFileCache::SaveMode::SortedBoundPSOs:
|
|
default:
|
|
ModeName = TEXT("SortedBoundPSOs");
|
|
break;
|
|
}
|
|
UE_LOG(LogRHI, Log, TEXT("FPipelineFileCache %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);
|
|
}
|
|
}
|
|
}
|
|
|
|
SET_MEMORY_STAT(STAT_FileCacheMemory, TOC.MetaData.GetAllocatedSize());
|
|
|
|
return bFileWriteSuccess;
|
|
}
|
|
|
|
bool IsPSOEntryCached(FPipelineCacheFileFormatPSO const& NewEntry, FPSOUsageData* EntryData = nullptr) const
|
|
{
|
|
uint32 PSOHash = GetTypeHash(NewEntry);
|
|
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 IsBSSEquivalentPSOEntryCached(FPipelineCacheFileFormatPSO const& NewEntry) const
|
|
{
|
|
check(!IsPSOEntryCached(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, FPipelineFileCache::PSOOrder Order)
|
|
{
|
|
// Only sorting metadata ordering - this should not affect PSO data offsets / lookups
|
|
switch(Order)
|
|
{
|
|
case FPipelineFileCache::PSOOrder::FirstToLatestUsed:
|
|
{
|
|
MetaData.ValueSort([](const FPipelineCacheFileFormatPSOMetaData& A, const FPipelineCacheFileFormatPSOMetaData& B) {return A.Stats.FirstFrameUsed > B.Stats.FirstFrameUsed;});
|
|
break;
|
|
}
|
|
case FPipelineFileCache::PSOOrder::MostToLeastUsed:
|
|
{
|
|
MetaData.ValueSort([](const FPipelineCacheFileFormatPSOMetaData& A, const FPipelineCacheFileFormatPSOMetaData& B) {return A.Stats.TotalBindCount > B.Stats.TotalBindCount;});
|
|
break;
|
|
}
|
|
case FPipelineFileCache::PSOOrder::Default:
|
|
default:
|
|
{
|
|
// NOP - leave as is
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GetOrderedPSOHashes(TArray<FPipelineCachePSOHeader>& PSOHashes, FPipelineFileCache::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 &&
|
|
FPipelineFileCache::MaskComparisonFn(FPipelineFileCache::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 == GameFileGuid)
|
|
{
|
|
FPipelineCacheFileFormatPSOMetaData const* GameMeta = GameTOC.MetaData.Find(Entry->Hash);
|
|
if (GameMeta && GameAsyncFileHandle.IsValid())
|
|
{
|
|
Entry->Data.SetNum(GameMeta->FileSize);
|
|
Entry->ParentFileHandle = GameAsyncFileHandle;
|
|
Entry->ReadRequest = MakeShareable(GameAsyncFileHandle->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 game-content file: %s or no game-content file"), Entry->Hash, *Meta.FileGuid.ToString());
|
|
Entry->bValid = false;
|
|
continue;
|
|
}
|
|
}
|
|
else if (Meta.FileGuid == UserFileGuid)
|
|
{
|
|
if(UserAsyncFileHandle.IsValid())
|
|
{
|
|
Entry->Data.SetNum(Meta.FileSize);
|
|
Entry->ParentFileHandle = UserAsyncFileHandle;
|
|
Entry->ReadRequest = MakeShareable(UserAsyncFileHandle->ReadRequest(Meta.FileOffset, Meta.FileSize, AIOP_Normal, nullptr, Entry->Data.GetData()));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogRHI, Verbose, TEXT("Encountered a PSO entry %u that references user content file ID: %s but async handle not valid"), Entry->Hash, *Meta.FileGuid.ToString());
|
|
Entry->bValid = false;
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogRHI, Verbose, 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;
|
|
}
|
|
|
|
private:
|
|
FString Name;
|
|
EShaderPlatform ShaderPlatform;
|
|
FName PlatformName;
|
|
uint64 TOCOffset;
|
|
FPipelineCacheFileFormatTOC GameTOC; // < The game TOC is kept around separately to handle cases where a fast-saved user cache tries to load removed entries from the game file.
|
|
FPipelineCacheFileFormatTOC TOC;
|
|
FGuid UserFileGuid;
|
|
FGuid GameFileGuid;
|
|
TSharedPtr<IAsyncReadFileHandle, ESPMode::ThreadSafe> UserAsyncFileHandle;
|
|
TSharedPtr<IAsyncReadFileHandle, ESPMode::ThreadSafe> GameAsyncFileHandle;
|
|
|
|
FString RecordingFilename;
|
|
};
|
|
uint32 FPipelineCacheFile::GameVersion = 0;
|
|
|
|
bool FPipelineFileCache::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 FPipelineFileCache::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 FPipelineFileCache::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 FPipelineFileCache::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 FPipelineFileCache::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 FPipelineFileCache::ShouldEnableFileCache()
|
|
{
|
|
#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 GRHISupportsPipelineFileCache;
|
|
}
|
|
|
|
void FPipelineFileCache::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 FPipelineFileCache::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)
|
|
{
|
|
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)
|
|
{
|
|
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
|
|
}
|
|
}
|
|
|
|
uint64 FPipelineFileCache::SetGameUsageMaskWithComparison(uint64 InGameUsageMask, FPSOMaskComparisonFn InComparisonFnPtr)
|
|
{
|
|
uint64 OldMask = 0;
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
|
|
OldMask = FPipelineFileCache::GameUsageMask;
|
|
FPipelineFileCache::GameUsageMask = InGameUsageMask;
|
|
|
|
if(InComparisonFnPtr == nullptr)
|
|
{
|
|
InComparisonFnPtr = DefaultPSOMaskComparisonFunction;
|
|
}
|
|
|
|
FPipelineFileCache::MaskComparisonFn = InComparisonFnPtr;
|
|
}
|
|
|
|
return OldMask;
|
|
}
|
|
|
|
void FPipelineFileCache::Shutdown()
|
|
{
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
for (auto const& Pair : Stats)
|
|
{
|
|
delete Pair.Value;
|
|
}
|
|
Stats.Empty();
|
|
NewPSOs.Empty();
|
|
NewPSOHashes.Empty();
|
|
NumNewPSOs = 0;
|
|
|
|
FileCacheEnabled = false;
|
|
|
|
SET_MEMORY_STAT(STAT_NewCachedPSOMemory, 0);
|
|
SET_MEMORY_STAT(STAT_PSOStatMemory, 0);
|
|
}
|
|
}
|
|
|
|
bool FPipelineFileCache::OpenPipelineFileCache(FString const& Name, EShaderPlatform Platform, FGuid& OutGameFileGuid)
|
|
{
|
|
bool bOk = false;
|
|
OutGameFileGuid = FGuid();
|
|
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
|
|
if(FileCache == nullptr)
|
|
{
|
|
FileCache = new FPipelineCacheFile();
|
|
|
|
bOk = FileCache->OpenPipelineFileCache(Name, Platform, OutGameFileGuid);
|
|
|
|
if (!bOk && !LogPSOtoFileCache()) // don't delete the FileCache if we couldn't open and we are trying to log it as that leads to no file cache at all, NOTE: we might need to also check to see if there is a cache file on the system at all here
|
|
{
|
|
delete FileCache;
|
|
FileCache = nullptr;
|
|
}
|
|
// File 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 bOk;
|
|
}
|
|
|
|
bool FPipelineFileCache::SavePipelineFileCache(FString const& Name, SaveMode Mode)
|
|
{
|
|
bool bOk = false;
|
|
|
|
if(IsPipelineFileCacheEnabled() && LogPSOtoFileCache())
|
|
{
|
|
CSV_EVENT(PSO, TEXT("Saving PSO cache"));
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
|
|
if(FileCache)
|
|
{
|
|
FName PlatformName = FileCache->GetPlatformName();
|
|
FString Path = FPaths::ProjectSavedDir() / FString::Printf(TEXT("%s_%s.upipelinecache"), *Name, *PlatformName.ToString());
|
|
bOk = FileCache->SavePipelineFileCache(Path, 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 FPipelineFileCache::ClosePipelineFileCache()
|
|
{
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
|
|
if(FileCache)
|
|
{
|
|
delete FileCache;
|
|
FileCache = nullptr;
|
|
|
|
// 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);
|
|
SET_MEMORY_STAT(STAT_FileCacheMemory, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPipelineFileCache::RegisterPSOUsageDataUpdateForNextSave(FPSOUsageData& UsageData)
|
|
{
|
|
FPSOUsageData& CurrentEntry = NewPSOUsage.FindOrAdd(UsageData.PSOHash);
|
|
CurrentEntry.PSOHash = UsageData.PSOHash;
|
|
CurrentEntry.UsageMask |= UsageData.UsageMask;
|
|
CurrentEntry.EngineFlags |= UsageData.EngineFlags;
|
|
}
|
|
|
|
void FPipelineFileCache::CacheGraphicsPSO(uint32 RunTimeHash, FGraphicsPipelineStateInitializer const& Initializer)
|
|
{
|
|
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()))
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
if(FileCache)
|
|
{
|
|
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
|
|
if(PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineFileCache::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 (!FileCache->IsPSOEntryCached(NewEntry, &CurrentUsageData))
|
|
{
|
|
bool bActuallyNewPSO = !NewPSOHashes.Contains(PSOHash);
|
|
if (bActuallyNewPSO && IsOpenGLPlatform(GMaxRHIShaderPlatform)) // OpenGL is a BSS platform and so we don't report BSS matches as missing.
|
|
{
|
|
bActuallyNewPSO = !FileCache->IsBSSEquivalentPSOEntryCached(NewEntry);
|
|
}
|
|
if (bActuallyNewPSO)
|
|
{
|
|
CSV_EVENT(PSO, TEXT("Encountered new graphics PSO"));
|
|
UE_LOG(LogRHI, Display, TEXT("Encountered a new graphics PSO: %u"), PSOHash);
|
|
if (GPSOFileCachePrintNewPSODescriptors > 0)
|
|
{
|
|
UE_LOG(LogRHI, Display, TEXT("New Graphics PSO (%u)"), PSOHash);
|
|
if (LogPSODetails() || GPSOFileCachePrintNewPSODescriptors > 1)
|
|
{
|
|
UE_LOG(LogRHI, Display, TEXT("%s"), *NewEntry.ToStringReadable());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogRHI, Display, TEXT("%s"), *NewEntry.GraphicsDesc.ToString());
|
|
}
|
|
}
|
|
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() && PSOLoggedEvent.IsBound())
|
|
{
|
|
PSOLoggedEvent.Broadcast(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(FPipelineFileCache::GameUsageMask, CurrentUsageData.UsageMask))
|
|
{
|
|
CurrentUsageData.UsageMask |= FPipelineFileCache::GameUsageMask;
|
|
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
|
|
}
|
|
|
|
// Apply the existing file PSO Usage mask and current to our "fast" runtime check
|
|
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
|
|
}
|
|
else if(!IsReferenceMaskSet(FPipelineFileCache::GameUsageMask, PSOUsage->UsageMask))
|
|
{
|
|
PSOUsage->UsageMask |= FPipelineFileCache::GameUsageMask;
|
|
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPipelineFileCache::CacheComputePSO(uint32 RunTimeHash, FRHIComputeShader const* Initializer)
|
|
{
|
|
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()))
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
if(FileCache)
|
|
{
|
|
FPSOUsageData* PSOUsage = RunTimeToPSOUsage.Find(RunTimeHash);
|
|
if(PSOUsage == nullptr || !IsReferenceMaskSet(FPipelineFileCache::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 (!FileCache->IsPSOEntryCached(NewEntry, &CurrentUsageData))
|
|
{
|
|
bool bActuallyNewPSO = !NewPSOHashes.Contains(PSOHash);
|
|
if (bActuallyNewPSO)
|
|
{
|
|
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, *NewEntry.ComputeDesc.ComputeShader.ToString());
|
|
}
|
|
|
|
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() && PSOLoggedEvent.IsBound())
|
|
{
|
|
PSOLoggedEvent.Broadcast(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(FPipelineFileCache::GameUsageMask, CurrentUsageData.UsageMask))
|
|
{
|
|
CurrentUsageData.UsageMask |= FPipelineFileCache::GameUsageMask;
|
|
RegisterPSOUsageDataUpdateForNextSave(CurrentUsageData);
|
|
}
|
|
|
|
// Apply the existing file PSO Usage mask and current to our "fast" runtime check
|
|
RunTimeToPSOUsage.Add(RunTimeHash, CurrentUsageData);
|
|
}
|
|
else if(!IsReferenceMaskSet(FPipelineFileCache::GameUsageMask, PSOUsage->UsageMask))
|
|
{
|
|
PSOUsage->UsageMask |= FPipelineFileCache::GameUsageMask;
|
|
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPipelineFileCache::CacheRayTracingPSO(const FRayTracingPipelineStateInitializer& Initializer)
|
|
{
|
|
if (!IsPipelineFileCacheEnabled() || !(LogPSOtoFileCache() || ReportNewPSOs()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArrayView<FRHIRayTracingShader*> ShaderTables[] =
|
|
{
|
|
Initializer.GetRayGenTable(),
|
|
Initializer.GetMissTable(),
|
|
Initializer.GetHitGroupTable(),
|
|
Initializer.GetCallableTable()
|
|
};
|
|
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
if (!FileCache)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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(FPipelineFileCache::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 (!FileCache->IsPSOEntryCached(NewEntry, &CurrentUsageData))
|
|
{
|
|
//CSV_EVENT(PSO, TEXT("Encountered new ray tracing PSO"));
|
|
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, *NewEntry.RayTracingDesc.ToString());
|
|
}
|
|
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() && PSOLoggedEvent.IsBound())
|
|
{
|
|
PSOLoggedEvent.Broadcast(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(FPipelineFileCache::GameUsageMask, CurrentUsageData.UsageMask))
|
|
{
|
|
CurrentUsageData.UsageMask |= FPipelineFileCache::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(FPipelineFileCache::GameUsageMask, PSOUsage->UsageMask))
|
|
{
|
|
PSOUsage->UsageMask |= FPipelineFileCache::GameUsageMask;
|
|
RegisterPSOUsageDataUpdateForNextSave(*PSOUsage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPipelineFileCache::RegisterPSOCompileFailure(uint32 RunTimeHash, FGraphicsPipelineStateInitializer const& Initializer)
|
|
{
|
|
if(IsPipelineFileCacheEnabled() && (LogPSOtoFileCache() || ReportNewPSOs()) && Initializer.bFromPSOFileCache)
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
if(FileCache)
|
|
{
|
|
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 = FileCache->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* FPipelineFileCache::RegisterPSOStats(uint32 RunTimeHash)
|
|
{
|
|
FPipelineStateStats* Stat = nullptr;
|
|
if(IsPipelineFileCacheEnabled() && LogPSOtoFileCache())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
if(FileCache)
|
|
{
|
|
uint32 PSOHash = RunTimeToPSOUsage.FindChecked(RunTimeHash).PSOHash;
|
|
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 FPipelineFileCache::GetOrderedPSOHashes(TArray<FPipelineCachePSOHeader>& PSOHashes, PSOOrder Order, int64 MinBindCount, TSet<uint32> const& AlreadyCompiledHashes)
|
|
{
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_Write);
|
|
|
|
RequestedOrder = Order;
|
|
|
|
if(FileCache)
|
|
{
|
|
FileCache->GetOrderedPSOHashes(PSOHashes, Order, MinBindCount, AlreadyCompiledHashes);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPipelineFileCache::FetchPSODescriptors(TDoubleLinkedList<FPipelineCacheFileFormatPSORead*>& Batch)
|
|
{
|
|
if(IsPipelineFileCacheEnabled())
|
|
{
|
|
FRWScopeLock Lock(FileCacheLock, SLT_ReadOnly);
|
|
|
|
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);
|
|
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 FPipelineFileCache::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;
|
|
}
|
|
|
|
FPipelineFileCache::FPipelineStateLoggedEvent& FPipelineFileCache::OnPipelineStateLogged()
|
|
{
|
|
return PSOLoggedEvent;
|
|
}
|
|
|
|
bool FPipelineFileCache::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 FPipelineFileCache::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;
|
|
}
|
|
}
|
|
|
|
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 FPipelineFileCache::MergePipelineFileCaches(FString const& PathA, FString const& PathB, FPipelineFileCache::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: %u vs. %u: expected %u."), A.Header.Magic, B.Header.Magic, FPipelineCacheFileFormatMagic);
|
|
}
|
|
return bOK;
|
|
}
|
|
|
|
FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::FPipelineFileCacheRayTracingDesc(const FRayTracingPipelineStateInitializer& Initializer, const FRHIRayTracingShader* ShaderRHI)
|
|
: ShaderHash(ShaderRHI->GetHash())
|
|
, MaxPayloadSizeInBytes(Initializer.MaxPayloadSizeInBytes)
|
|
, Frequency(ShaderRHI->GetFrequency())
|
|
, bAllowHitGroupIndexing(Initializer.bAllowHitGroupIndexing)
|
|
{
|
|
}
|
|
|
|
FString FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::HeaderLine() const
|
|
{
|
|
return FString(TEXT("RayTracingShader,MaxPayloadSizeInBytes,Frequency,bAllowHitGroupIndexing"));
|
|
}
|
|
|
|
FString FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::ToString() const
|
|
{
|
|
return FString::Printf(TEXT("%s,%d,%d,%d")
|
|
, *ShaderHash.ToString()
|
|
, MaxPayloadSizeInBytes
|
|
, uint32(Frequency)
|
|
, uint32(bAllowHitGroupIndexing)
|
|
);
|
|
}
|
|
|
|
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(" MPSIB ");
|
|
OutBuilder << MaxPayloadSizeInBytes;
|
|
OutBuilder << TEXT(" AHGI ");
|
|
OutBuilder << bAllowHitGroupIndexing;
|
|
}
|
|
|
|
void FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc::FromString(const FString& Src)
|
|
{
|
|
TArray<FString> Parts;
|
|
Src.TrimStartAndEnd().ParseIntoArray(Parts, TEXT(","));
|
|
|
|
ShaderHash.FromString(Parts[0]);
|
|
|
|
LexFromString(MaxPayloadSizeInBytes, Parts[1]);
|
|
|
|
{
|
|
uint32 Temp = 0;
|
|
LexFromString(Temp, Parts[2]);
|
|
Frequency = EShaderFrequency(Temp);
|
|
}
|
|
|
|
{
|
|
uint32 Temp = 0;
|
|
LexFromString(Temp, Parts[3]);
|
|
bAllowHitGroupIndexing = Temp != 0;
|
|
}
|
|
}
|
|
|
|
bool FPipelineCacheFileFormatPSO::Init(FPipelineCacheFileFormatPSO& PSO, FPipelineCacheFileFormatPSO::FPipelineFileCacheRayTracingDesc const& Desc)
|
|
{
|
|
PSO.Hash = 0;
|
|
PSO.Type = DescriptorType::RayTracing;
|
|
|
|
#if PSO_COOKONLY_DATA
|
|
PSO.UsageMask = 0;
|
|
PSO.BindCount = 0;
|
|
#endif
|
|
|
|
PSO.RayTracingDesc = Desc;
|
|
|
|
return true;
|
|
}
|
|
|