UE 中具有 PSO Caching 机制,全称 Pipeline State Object Caching,用于预先记录和构建出运行时所使用的材质依赖的 Shader 信息,当项目首次使用这些 Shader 时,该列表可以加速 Shader 的加载 / 编译过程。PSO Caching 会把渲染状态、顶点声明、Primitive 类型、RenderTarget 像素格式等数据保存到文件中,提升 Shader 的加载效率。
本篇文章主要介绍 PSO Caching 的启用及构建流程,并会分析 PSO Cache 在引擎中的加载流程以及实现热更 PSO 方式、错误处理等,PSO Caching 的原理有时间再进行详细分析。
PSO Caching 的官方文档:PSO Caching。
PSO Cache 构建流程概览:
PSO Caching 的部署和使用大致分为以下几个步骤:
- 为项目开启 PSO Cahche 和 ShaderStableKeys,打包后可以从
Metadata/PipelineCaches
目录下获得ShaderStableInfo*.scl.csv
- 添加
logPSO
参数启动游戏,用于在运行时记录 PSO 数据(*.scl.upipelinecache
) - 通过
ShaderStableInfo*.scl.csv
和*.scl.upipelinecache
生成*.stablepc.csv
- 再次执行 Cook,通过
*.stablepc.csv
生成upipelinecache
文件,打至包内; - 启动游戏,引擎自动加载
*.stable.upipelinecache
,编译 Shader 时使用 PSO Caching
本篇文章的内容顺序也遵循着几个步骤。
启用 ShaderStableKeys
首先需要为项目开启ShaderStableKeys
,在执行 Cook 时生成稳定的 ShaderKey,作为记录 Shader 的凭据。
在DefaultEngine.ini
(或平台相关如 AndroidEngine.ini)中添加以下值:
1 | [DevOptions.Shaders] |
添加之后再执行打包(Cook),会创建以下目录:
1 | Saved/Cooked/PLATFORM_NAME/PROJECT_NAME/Metadata/PipelineCaches |
并且会在该目录下生成两个文件(分别对应项目、引擎):
1 | ShaderStableInfo-PROJECT_NAME-GLSL_ES3_1_ANDROID.scl.csv |
Cook 过程中会有以下 log,表明生成了这两个文件:
1 | LogCook: Display: Saved scl.csv D:/PSOExample/Saved/Cooked/Android_ASTC/PSOExample/Metadata/PipelineCaches/ShaderStableInfo-Global-GLSL_ES3_1_ANDROID.scl.csv for platform Android_ASTC |
可以使用它们通过 -run=ShaderPipelineCacheTools
这个 Commandlet 来生成*.stablepc.csv
。
*.scl.csv
文件的内容:
运行时捕获 PSO 数据
启动游戏时加入 -logPSO
参数或者在 DefaultEngine.ini
中加入以下配置:
1 | [ConsoleVariables] |
也可以在 Devices Profile
中设置:
这两个参数在以下代码中使用:
1 | bool FPipelineFileCache::IsPipelineFileCacheEnabled() |
运行游戏时会有 log:
1 | LogConfig: Applying CVar settings from Section [ConsoleVariables] File [../../../FGame/Saved/Config/Android/Engine.ini] |
并会在 Saved/CoolectedPSOs
中创建以下文件:
生成 *.stablepc.csv
使用以下 commandlet:
1 | Engine/Binaries/Win64/UE4Editor-Cmd.exe |
以上命令会在引擎的 Binaries/Win64 下生成 Client_GLSL_ES3_1_ANDROID.stablepc.csv
文件,注意一定要匹配 {PROJECTNAME}_{SHADER_FORMART_NAME}.stablepc.csv
这个命名规则。
Android 的命名为:Client_GLSL_ES3_1_ANDROID.stablepc.csv
IOS 的命名为:Client_SF_METAL.stablepc.csv
生成时具有以下 Log:
1 | D:\PSOCache>"C:\Program Files\Epic Games\UE_4.25\Engine\Binaries\Win64\UE4Editor-Cmd.exe" "D:\PSOExample\PSOExample.uproject" -run=ShaderPipelineCacheTools expand D:/PSOCache/*.rec.upipelinecache D:/PSOCache/*.scl.csv D:/PSOCache/PSOExample_GLSL_ES3_1_ANDROID.stablepc.csv |
最终 PSO 所需要的所有文件:
1 | D:\PSOCache>tree /a /f |
我把测试工程生成的文件备份了一份:PSOCache.7z,可以查看每个文件中的内容。
生成 *.stable.upipelinecache
把生成的 *stablepc.csv
放到 Build/Android/PipelineCaches
目录下,注意 Build/PLATFORM
这个 Platform 是编译平台,不是 Cook 的资源平台,Android 的包就是 Android
而不是 Android_ASTC
等。
之后重新打包即可。
引擎在 Cook 时通过 stavlepc.csv
创建 PipelineCache 的代码:
1 | void UCookOnTheFlyServer::CreatePipelineCache(const ITargetPlatform* TargetPlatform, const FString& LibraryName) |
实际使用 stablepc.csv
的地方就是用它来执行 ShaderPipelineCacheTools
这个 commandlet 生成 upipelinecache 文件并打至包内。
ShaderPipelineCacheToolsCommandlet 的执行命令为:
1 | Engine\Binaries\Win64\UE4Editor-Cmd.exe |
生成 *.stable.upipelinecache
文件的包内路径为Content\PipelineCaches\Android
:
1 | D:\PSOExample\Saved\Cooked\Android_ASTC\PSOExample\Content\PipelineCaches>tree /a /f |
因为它是位于 Content 下并会打包进 pak 的文件,我们也可以对其进行热更。
当安装了包含 upipelinecache
的包,在运行时就会有以下 log:
1 | LogShaderLibrary: Display: Using ../../../PSOExample/Content/ShaderArchive-PSOExample-GLSL_ES3_1_ANDROID.ushaderbytecode for material shader code. Total 3053 unique shaders. |
PSO Cache 的加载与热更
与ShaderCode
类似,引擎在启动时也是会自动加载 PSO Cache 的,在 FEngineLoop
中通过调用 FPipelineCacheFile::OpenPipelineFileCache
读取 *.stable.upipelinecache
的。
在 PreInitPreStartupScreen
中加载 PSO 的代码:
1 | int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine) |
FShaderPipelineCache::OpenPipelineFileCache
有两个重载版本:
1 | bool FShaderPipelineCache::OpenPipelineFileCache(EShaderPlatform Platform) |
引擎启动的时候默认读取的就是 OpenPipelineFileCache(FApp::GetProjectName(), Platform)
,也就是PSOExample_GLSL_ES3_1_ANDROID.stable.upipelinecache
,Platform 参数可以通过传递全局对象GMaxRHIShaderPlatform
来获取当前运行的平台。
UE 也提供了一个 console
命令可以指定加载stable.upipelinecache
:r.ShaderPipelineCache.Open
,还有几个其他控制 PSO 的 console 命令:
1 | static FAutoConsoleCommand LoadPipelineCacheCmd( |
这样只需要在热更包中包含最新的 *.stable.upipelinecache
,之后调用OpenPipelineFileCache
加载最新的 PSO Cache 即可,可以与 ShaderCode 的热更流程保持一致。
生成新的 PSO Cache 需要关键的两种数据:
- 运行时捕获的 PSO 数据(upipelinecache)
- ShaderStableInfo(位于 Metadata 目录下)
因为 ShaderCode 是可以热更的,而 ShaderStableInfo
可以通过 Cook 最新的工程获得,所以 PSO Cache 也是可以通过热更 Shader 并不断地捕获最新的 PSO 数据进行迭代更新的。
准备有时间给 HotPacther 中增加 PSO Caching 的热更功能,这样也可以把 PSO Caching 的部署和打包集成至自动化地热更流程,先挖个坑。
因为当开启了 r.ShaderPipelineCache.Enabled=1
,在引擎启动时就会自动加载项目的 PSO Cache,而引擎中做了限制,只能够加载一次,后续调用OpenPipelineFileCache
的都不会被加载:
1 | bool FPipelineFileCache::OpenPipelineFileCache(FString const& Name, EShaderPlatform Platform, FGuid& OutGameFileGuid) |
当引擎默认加载执行之后 FileCache
就不为 nullptr
了,后续所有的加载调用都会直接返回 false
,解决办法就是,让引擎启动时不自动加载 PSO Cache,等到运行时热更之后由我们手动加载,翻了下代码,可以从这个IsPipelineFileCacheEnabled
检测中做:
1 | bool FPipelineFileCache::IsPipelineFileCacheEnabled() |
它的返回值依赖了两个值:FileCacheEnabled
以及CVarPSOFileCacheEnabled
。
FileCacheEnabled
在 FPipelineFileCache::Initialize
中被赋值,IOS 之外的平台总是 true
,IOS 则依赖于FPipelineFileCache::ShouldEnableFileCache
的结果。
CVarPSOFileCacheEnabled
是一个控制台变量,用来控制 r.ShaderPipelineCache.Enabled
的值:
1 | static TAutoConsoleVariable<int32> CVarPSOFileCacheEnabled( |
我们需要做的有三步:
- 引擎默认启动时
CVarPSOFileCacheEnabled
的值为 false - 运行时手动修改
CVarPSOFileCacheEnabled
值,开启 PSO Cache - 加载 PSO Cache
具体实现流程:
- 在
DefaultEngine.ini
中将r.ShaderPipelineCache.Enabled=0
并打包
1 | [ConsoleVariables] |
然后写两个函数在运行时开启和加载 PSO:
1 |
|
这样即可实现 PSO Cache 的延迟加载,手动加载时机在热更之后加载即可。
延迟采集和存储
因为采集和存储 PSO Cache 具有额外的性能消耗,所以可以把采集和存储 PSO 数据关闭,根据需求在运行时再开启。
在 DefaultEngine.ini
中关闭 LogPSO
和SaveBoundPSOLog
,打基础包时就不会自动采集和自动存储了:
1 | [ConsoleVariables] |
然后在运行时开启:
1 | UENUM(BlueprintType) |
开启 SaveBoundPSOLog
后会自动存储采集的 PSO 数据,可以不开启自动存储,在运行时通过调用 FShaderPipelineCache::SavePipelineFileCache
手动存储。
错误处理
Cook 没有生成.scl.csv
注意,一定要为项目开启ShaderStableKeys
,不然不会生成.scl.csv 文件。
运行时没有生成 upipelinecache 文件
请严格按照 运行时捕获 PSO 数据 中的步骤执行。
- 确认是否开启
r.ShaderPipelineCache.Enabled
(DefaultEngine.ini 或 DeviceProfile)。 - 在 ue4commandline.txt 中添加
-logPSO
参数。
Bad PSO
如果使用公版引擎,上述流程就是完整的流程,但是有时项目需要修改引擎支持一些渲染特性,如添加 Multi-subpasshint 支持:
1 | struct RHI_API FPipelineCacheFileFormatPSO |
这处变动需要同时修改 GraphicsDescriptor::StateToString()
与GraphicsDescriptor::StateFromString()
两个函数,加入 multi-subpasshint 的序列化支持。
但是,修改之后使用 -run=ShaderPipelineCacheTools
生成 *.stablepc.csv
时有以下报错的 Log:
1 | LogShaderPipelineCacheTools: Expanding matched 1 files: D:\PipelineCaches\*.rec.upipelinecache |
这是因为 PSO 数据从 String 的可逆性验证失败了:
1 | bool CheckPSOStringInveribility(const FPipelineCacheFileFormatPSO& Item) |
关键部分在于 DupItem.GraphicsDesc.FromString(StringRep);
这行代码中 GraphicsDesc
的数据没有恢复成功。
经过调试发现,引擎中具有记录 PipelineCacheGraphicsDesc 的字符串中可被解析元素的数量,也就是生成的 *.stablepc.csv
中第二列中数据的数量,公版引擎中默认是 63 个,使用 FPipelineCacheGraphicsDescPartsNum
记录:
1 | const int32 FPipelineCacheGraphicsDescPartsNum = 63; // parser will expect this number of parts in a description string |
生成的 *.stablepc.csv
中各项状态和数据如下:
刚好是 63 个。需要注意的是,在 GraphicsDescriptor::StateFromString
对数据的数量做了检测,FromString 的数据数量要与 FPipelineCacheGraphicsDescPartsNum
的值一致:
1 | bool FPipelineCacheFileFormatPSO::GraphicsDescriptor::StateFromString(const FStringView& Src) |
因为我们增加了 multi-subpasshint 支持,把 SubpassHint
从uint8
改成了 uint8[8]
,增加了 7 个数据,所以与之对应的FPipelineCacheGraphicsDescPartsNum
也要加 7,改为 70,上面 StateFromString
验证才能够通过。
修改之后再通过 -run=ShaderPipelineCacheTools
生成 *.stablepc.csv
就没有 Bas PSO
的错误了。
使用与配置
可以通过 FShaderPipelineCache
的函数在运行时控制构建 PSO 数据:
1 | /** Pauses precompilation. */ |
官方建议的做法是在加载屏幕时等待 PSO 构建完毕,再把 LoadingScreen 隐藏:
1 | if(FShaderPipelineCache::NumPrecompilesRemaining() > 0) |
也可以在打开 UI、过场动画、暂停菜单时构建,通过以下三个函数组合处理:
1 | // 暂停 PSO 缓存编译 |
也可以使用配置在游戏启动时自动构建,修改 DefaultEngine.ini
中[ConsoleVariables]
的配置,它们否时定义在 Runtime\RenderCore\Private\ShaderPipelineCache.cpp
中的ConsoleVariable
,可以根据自己的需要在运行时或配置文件中进行修改:
PSO 引擎默认配置:
1 | [ConsoleVariables] |
我修改的配置:
1 | [ConsoleVariables] |
开启了启动时自动构建 PSO 数据,会有以下 log:
1 | LogRHI: Base name for record PSOs is ../../../FGame/Saved/CollectedPSOs/++UE4+Release-4.25-CL-0-FGame_SF_METAL_8F3222B7964FE2A89C849E90E0000736.rec.upipelinecache |
也可以在 DefaultGameUserSettings.ini
中设置 PSO 的SortOrder
:
1 | [ShaderPipelineCache.CacheFile] |