UE4:4.27版本PSO变化

前言

最近项目升级了4.27版本,发现PSO部分似乎又有了什么改动,于是决定去看一下ReleaseNotes,对比一下相对4.26版本的变化。

ReleaseNotes

其中与PSO相关的几点如下:

安卓

BUG FIX

  • 修复了在PSO缓存处理期间,如果传入的PSO在PSO缓存加载开始之前已经实例化则可能触发GLES断言的问题

Rendering

新特性

  • 实现了可选的非阻塞光线跟踪PSO编译模式

BUG FIX

  • 修复了在CPU核数较低的计算机上光线跟踪PSO编译任务中可能出现的死锁

Materials

新特性

  • 包含用于PSO缓存的 stable shader keys 的文件不再是文本(scl.csv),而是二进制文件(.shk)
  • PSO缓存收集过程中的“expand”步骤的性能有了很大提高

BUG FIX

  • 将PSO Cache的过滤规则改的宽松了,为了避免一些问题(?另外这条在RealeaseNotes里出现两次?)

下文主要关注Materials部分的变化

PSO部署和使用的基本流程

首先回顾一下4.26版本下我们生成和使用PSO的流程:

  1. 打开项目的NeedsShaderStableKeys设置,在DefaultEngine.ini和相关平台的Engine.ini中添加
1
2
[DevOptions.Shaders]
NeedsShaderStableKeys=true
  1. 打包,cook阶段生成对应的两个.scl.csv文件(4.26版本)
  2. 打开DefaultEngine中PSO的相关Log设置确认成功启用PSO
1
2
3
4
[ConsoleVariables]
r.ShaderPipelineCache.Enabled=1
r.ShaderPipelineCache.LogPSO=1
r.ShaderPipelineCache.SaveBoundPSOLog=1

游戏启动后会有Log可以确认:

1
2
3
LogConfig: Setting CVar [[r.ShaderPipelineCache.Enabled:1]]
LogConfig: Setting CVar [[r.ShaderPipelineCache.LogPSO:1]]
LogConfig: Setting CVar [[r.ShaderPipelineCache.SaveBoundPSOLog:1]]
  1. 正常进行游戏流程,发现Saved文件夹中出现CoolectedPSOs文件夹,生成.rec.upipelinecache文件
  2. 使用UE4Editor-Cmd.exe和相关命令生成.stablepc.csv文件,参考:
1
2
3
4
5
6
Engine/Binaries/Win64/UE4Editor-Cmd.exe
D:/Client/Client.uproject
-run=ShaderPipelineCacheTools expand
D:/PSOCache/*.rec.upipelinecache
D:/PSOCache/*.scl.csv
D:/PSOCache/Client_GLSL_ES3_1_ANDROID.stablepc.csv
  1. 到这一步我们应该有四个文件:
    • .scl.csv 两个 打包时生成的文件
    • .rec.upipelinecache 运行时收集的文件
    • .stablepc.csv 第5步生成的文件
  2. UE4Editor-Cmd.exe命令生成.stable.upipelinecache文件,文件路径在,参考:
1
2
3
4
5
6
7
Engine\Binaries\Win64\UE4Editor-Cmd.exe
D:\TestProject\TestProject.uproject
-run=ShaderPipelineCacheTools build
"D:\TestProject/Build/Android/PipelineCaches/*TestProject_GLSL_ES3_1_ANDROID.stablepc.csv"
"D:\TestProject/Saved/Cooked/Android_ASTC/TestProject/Metadata/PipelineCaches/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv"
"D:\TestProject/Saved/Cooked/Android_ASTC/TestProject/Metadata/PipelineCaches/ShaderStableInfo-TestProject-GLSL_ES3_1_ANDROID.scl.csv"
"D:\TestProject/Saved/Cooked/Android_ASTC/TestProject/Content/PipelineCaches/Android/TestProject_GLSL_ES3_1_ANDROID.stable.upipelinecache"
  1. 将上一步生成的.stable.upipelinecache文件放在项目的Saved\Cooked对应平台的Content的PipelineCaches目录下,比如:
    D:\TestProject\Saved\Cooked\Android_ASTC\TestProject\Content\PipelineCaches
  2. 再次打包,这次打包就有PSO文件了,启动游戏时查看log进行确认:
1
2
3
4
5
6
7
8
9
10
11
LogShaderLibrary: Display: Using ../../../TestProject/Content/ShaderArchive-TestProject-GLSL_ES3_1_ANDROID.ushaderbytecode for material shader code. Total 3053 unique shaders.
LogShaderLibrary: Display: Cooked Context: Using Shared Shader Library TestProject
LogRHI: Display: Opened pipeline cache after state change and enqueued 0 of 0 tasks for precompile.
LogRHI: Base name for record PSOs is ../../../TestProject/Saved/CollectedPSOs/++UE4+Release-4.25-CL-13942748-TestProject_GLSL_ES3_1_ANDROID_00087B4B08D905BBC5A827F40CA03A0C.rec.upipelinecache
LogRHI: FPipelineCacheFile Header Game Version: 13942748
LogRHI: FPipelineCacheFile Header Engine Data Version: 17
LogRHI: FPipelineCacheFile Header TOC Offset: 38155
LogRHI: FPipelineCacheFile File Size: 51011 Bytes
LogRHI: Opened FPipelineCacheFile: ../../../TestProject/Content/PipelineCaches/Android/TestProject_GLSL_ES3_1_ANDROID.stable.upipelinecache (GUID: 00000000000000000000000000000000) with 102 entries.
LogRHI: Scanning Binary program cache, using Shader Pipeline Cache version 6988202F47BA858F3F0DE483D7DB0606
LogRHI: AndroidEGL:SwapBuffers eglGetCompositorTimingANDROID EGL_COMPOSITE_DEADLINE_ANDROID=2718926192606265, EGL_COMPOSITE_INTERVAL_ANDROID=16559027, EGL_COMPOSITE_TO_PRESENT_LATENCY_ANDROID=14559027

以上流程在4.27中,按照版本信息中显示的,变化最大的是第二步生成的.scl.csv文件变成了二进制.shk文件

SCL.CSV文件

20220612203906
文件里包含了材质类名与Shader类型、特性级别、目标平台、顶点工厂类型和Hash相关信息
在4.27版本中,生成的文件为.shk:
20220612205018

文件的生成

4.26中源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ShaderCodeLibrary.cpp
// 忽略了部分无关代码
bool Finalize(FString OutputDir, bool bNativeFormat, FString& OutSCLCSVPath)
{
// ...

// Shader library
if (bSuccess && StableMap.Num() > 0)
{
// Write to a intermediate file
// 这里生成了.scl.csv的文件路径
FString IntermediateFormatPath = GetStableInfoArchiveFilename(FPaths::ProjectSavedDir() / TEXT("Shaders") / FormatName.ToString(), LibraryName, FormatName);

// Write directly to the file
{
TUniquePtr<FArchive> IntermediateFormatAr(IFileManager::Get().CreateFileWriter(*IntermediateFormatPath));

// 这一行是在打表,生成了csv文件的表头
FString HeaderText = FStableShaderKeyAndValue::HeaderLine();
HeaderText += TCHAR('\n');
auto HeaderSrc = StringCast<ANSICHAR>(*HeaderText, HeaderText.Len());
// 写入文件第一行
IntermediateFormatAr->Serialize((ANSICHAR*)HeaderSrc.Get(), HeaderSrc.Length() * sizeof(ANSICHAR));

TAnsiStringBuilder<512> LineBuffer;

// TSet<FStableShaderKeyAndValue> StableMap;
for (const FStableShaderKeyAndValue& Item : StableMap)
{
LineBuffer.Reset();
// 将FStableShaderKeyAndValue的相关信息写入LineBuffer
Item.AppendString(LineBuffer);
LineBuffer << '\n';
// 将一个FStableShaderKeyAndValue的信息写入文件
IntermediateFormatAr->Serialize(const_cast<ANSICHAR*>(LineBuffer.ToString()), LineBuffer.Len() * sizeof(ANSICHAR));
}
}

// ...

}
}

可以看出在4.26版本中是直接将每一个FStableShaderKeyAndValue的信息直接转成文本写入到文本文件中的
再看一下4.27版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool Finalize(FString OutputDir, FString& OutSCLCSVPath)
{
// ...

// Shader library
if (bSuccess && StableMap.Num() > 0)
{
// Write to a intermediate file
FString IntermediateFormatPath = GetStableInfoArchiveFilename(FPaths::ProjectSavedDir() / TEXT("Shaders") / FormatName.ToString(), LibraryName, FormatName);

// Write directly to the file
{
// PipelineCacheUtilities 用这里的方法去保存文件了
if (!UE::PipelineCacheUtilities::SaveStableKeysFile(IntermediateFormatPath, StableMap))
{
UE_LOG(LogShaderLibrary, Error, TEXT("Could not save stable map to file '%s'"), *IntermediateFormatPath);
}

// ...
}

// ...
}

return bSuccess;
}

那么去看一下UE::PipelineCacheUtilities::SaveStableKeysFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// PipelineCacheUtilities.h
/**
* Saves stable shader keys file (using a proprietary format). Stable key is a way to identify a shader independently of its output hash
*
* @param Filename filename (with path if needed)
* @param Values values to be saved
* @return true if successful
*/
RENDERCORE_API bool SaveStableKeysFile(const FStringView& Filename, const TSet<FStableShaderKeyAndValue>& Values);

// ...

// PipelineCacheUtilities.cpp
bool UE::PipelineCacheUtilities::SaveStableKeysFile(const FStringView& Filename, const TSet<FStableShaderKeyAndValue>& Values)
{
TUniquePtr<FArchive> FileArchiveInner(IFileManager::Get().CreateFileWriter(*FString(Filename.Len(), Filename.GetData())));
if (!FileArchiveInner)
{
return false;
}
TUniquePtr<FArchive> Archive(new FNameAsStringIndexProxyArchive(*FileArchiveInner.Get()));

// 处理文件头,已经不是表头了,只取了TSet的Num
UE::PipelineCacheUtilities::Private::FStableKeysSerializedHeader Header;
Header.NumEntries = Values.Num();

*Archive << Header;

// go through all the hashes and index them
// 这里要拿所有的ShaderHash和对应的Index
TArray<FSHAHash> Hashes;
TMap<FSHAHash, int32> HashToIndex;
// 填充HashToIndex的Map的方法(感觉lambda流行了起来……
auto IndexHash = [&Hashes, &HashToIndex](const FSHAHash& Hash)
{
if (HashToIndex.Find(Hash) == nullptr)
{
Hashes.Add(Hash);
HashToIndex.Add(Hash, Hashes.Num() - 1);
}
};

for (const FStableShaderKeyAndValue& Item : Values)
{
// PipelineHash和OutputHash都需要,在原来的csv文件中也是两个
IndexHash(Item.PipelineHash);
IndexHash(Item.OutputHash);
}

int32 NumHashes = Hashes.Num();
*Archive << NumHashes;
for (int32 IdxHash = 0; IdxHash < NumHashes; ++IdxHash)
{
*Archive << Hashes[IdxHash];
}

// save the rest of the properties
for (const FStableShaderKeyAndValue& ConstItem : Values)
{
// serialization unfortunately needs non-const and this is easier than const-casting every field
FStableShaderKeyAndValue& Item = const_cast<FStableShaderKeyAndValue&>(ConstItem);

int8 CompactNamesNum = static_cast<int8>(Item.ClassNameAndObjectPath.ObjectClassAndPath.Num());
ensure(Item.ClassNameAndObjectPath.ObjectClassAndPath.Num() < 256);
*Archive << CompactNamesNum;
for (int Idx = 0; Idx < (int)CompactNamesNum; ++Idx)
{
*Archive << Item.ClassNameAndObjectPath.ObjectClassAndPath[Idx];
}

*Archive << Item.ShaderType;
*Archive << Item.ShaderClass;
*Archive << Item.MaterialDomain;
*Archive << Item.FeatureLevel;
*Archive << Item.QualityLevel;
*Archive << Item.TargetFrequency;
*Archive << Item.TargetPlatform;
*Archive << Item.VFType;
*Archive << Item.PermutationId;

uint64 PipelineHashIdx = static_cast<uint64>(*HashToIndex.Find(Item.PipelineHash)) ;
WriteVarUIntToArchive(*Archive, PipelineHashIdx);
uint64 OutputHashIdx = static_cast<uint64>(*HashToIndex.Find(Item.OutputHash));
WriteVarUIntToArchive(*Archive, OutputHashIdx);
}

return true;
}

当然,在保存的文件格式变化之后,读取的方法相应的也变了,namespace PipelineCacheUtilities只有两个行数,一个是LoadStableKeysFile,一个是SaveStableKeysFile
从csv文件变成二进制文件后直接打开文件查看信息是不能了,这么改的目的应该就是更新日志里提到的对PSO build的expand过程的性能提升,可以看流程中的第5条的命令
在ShaderPipelineCacheToolsCommandlet.cpp文件中相关的有变化的方法:

4.26 4.27
LoadStableSCL LoadStableShaderKeys
LoadStableSCLs LoadStableShaderKeysMultiple
If you can find the old .scl.csv file If you can find the old %s file
GeneratePermuations GeneratePermutations(他们甚至改了单词的拼写错误
ExpandPSOSC ExpandPSOSC(函数体中关于文件类型的修改,这里就是关于更新日志中关于expand性能提升的部分)

总结

在Material方面:

  1. 4.26 使用csv文件作为ShaderAssetInfo文件
    4.27 使用.shk的二进制文件作为ShaderAssetInfo文件
  2. 4.27 对应新增了.shk文件的读写方法
  3. 4.27 将PSOExpand中的一个for循环优化为ParallelFor提升性能

UE4:4.27版本PSO变化
http://muchenhen.com/posts/2274/
作者
木尘痕
发布于
2022年6月12日
许可协议