UE中具有PSO Cache机制,全称Pipeline State Object Caching,用于预先记录和构建出运行时所使用的材质依赖的Shader信息,当项目首次使用这些Shader时,该列表可以加速Shader的加载/编译过程。PSO Cache会把渲染状态、顶点声明、Primitive类型、RenderTarget像素格式等数据保存到文件中,提升Shader的加载效率。
本篇文章主要介绍PSO Cache的启用及构建流程,并会分析PSO Cache在引擎中的加载流程以及实现热更PSO方式、错误处理等,PSO Cache的原理有时间再进行详细分析。
PSO Cache的官方文档:PSO Cache。
PSO Cache构建流程概览:
PSO Cache的部署和使用大致分为以下几个步骤:
- 为项目开启PSO Cahche和ShaderStableKeys,打包后可以从
Metadata/PipelineCaches
目录下获得ShaderStableInfo*.scl.csv
- 添加
logPSO
参数启动游戏,用于在运行时记录PSO数据(*.rec.upipelinecache
) - 通过
ShaderStableInfo*.scl.csv
和*.rec.upipelinecache
生成*.stablepc.csv
- 再次执行Cook,通过
*.stablepc.csv
生成upipelinecache
文件,打至包内; - 启动游戏,引擎自动加载
*.stable.upipelinecache
,编译Shader时使用PSO Cache
本篇文章的内容顺序也遵循着几个步骤。
启用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
中创建以下文件:
注意,默认只会采集当前的MaterialQualityLevel,如果想要采集其他质量的,需要切换后重新跑。
生成*.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 Cache的热更功能,这样也可以把PSO Cache的部署和打包集成至自动化地热更流程,先挖个坑。
因为当开启了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缓存编译 |
引擎中有多个CVars可用于控制PSO的捕获、加载等等配置,详细介绍详见:UE Console Variables and Command,在其中搜索
r.shaderpipelinecache
即可看到所有支持的CVars。
也可以使用配置在游戏启动时自动构建,修改DefaultEngine.ini
中[ConsoleVariables]
的配置,它们否时定义在Runtime\RenderCore\Private\ShaderPipelineCache.cpp
中的ConsoleVariable
,可以根据自己的需要在运行时或配置文件中进行修改:
开启了启动时自动构建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] |
Profiling
可以使用Unreal Ingishts查看shader link的耗时:
以及通过stat pipelinestatecache
,可以查看PSO的加载和生成情况。
注意
ShaderLibrary要求
使用PSO的前提要求是必须要把Shader的序列化方式改成share shader library,如果是inline shader的形式,是无法使用Pre-Compiled的PSO的。运行时采集是可以的,但无法利用到下次打的包里。
scl.csv to shk
4.27将StableExtension从
scl.csv
改成了shk
。ShaderCodeLibrary.cpp#L65