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
