PSO构建总流程

image.png

根据上面总流程图,我会分别一个个去讲解每一步的操作和需要额外处理的东西

开发环境

  • UE版本:4.27.2
  • VS版本:2019

1.打包

首先,需要打一个可以实机收集PSO缓存文件的包

2.实机收集

自动采集

这里主要说一下录取的方式方法, 一种方式就是引擎自带的自动采集指令

Config/DefaultEngine.ini
[ConsoleVariables]
r.ShaderPipelineCache.Enabled=1
r.ShaderPipelineCache.LogPSO=1
r.ShaderPipelineCache.SaveBoundPSOLog=1

上述采集指令配置了以后直接打包,然后开始跑项目即可,会自动采集并且保存缓存文件到本地,如果你们是普通小场景项目上面方式就已经足够了,可直接跳到 采集完成

手动采集

我自己做了一个PsoCacheGather的小工具插件,下面会大概说一下实现思路
image 3.png

我们小工具就如图所示,支持自动跑图录取和手动录取,因为我们是大世界,场景太大,如果让QA去全部跑一遍有点吃力,所以直接做了一个自动跑图收集,如果你们项目组是小副本或者小场景直接手动跑图录取或者拉一条Line,然后摄像机沿着跑即可,这个看自己需求

我们项目因为是大世界场景,测试发现长时间全部跑下来会有问题,可能是实时一直在保存太大太频繁,所以我改成了自动跑图完再保存一次,中间不会实时保存,测试没问题

[ConsoleVariables]
r.ShaderPipelineCache.Enabled=0

首先配置中关闭自动开启,我们在开始录取的时候代码中开启

bool UPSOCacheGatherHelper::EnableShaderPipelineCache(bool bEnable)
{
	UE_LOG(LogPSO, Display, TEXT("EnableShaderPipelineCache %s"), bEnable ? TEXT("true") : TEXT("false"));
	auto Var = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ShaderPipelineCache.Enabled"));
	if (Var)
	{
		Var->Set(bEnable ? 1 : 0);
	}
	return !!Var;
}

首先在一开始先开启总开关 r.ShaderPipelineCache.Enabled

bool UPSOCacheGatherHelper::EnableLogPSO(bool bEnable)
{
	auto Var = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ShaderPipelineCache.LogPSO"));
	if (Var)
	{
		Var->Set(bEnable ? 1 : 0);
	}
	return !!Var;
}
bool UPSOCacheGatherHelper::LoadShaderPipelineCache(const FString& Name)
{
	UE_LOG(LogPSO, Display, TEXT("Load Shader pipeline cache %s for platform"), FApp::GetProjectName());
	return FShaderPipelineCache::OpenPipelineFileCache(FApp::GetProjectName(), GMaxRHIShaderPlatform);
}

然后开始录制时先调用EnableLogPSO(true),再LoadShaderPipelineCache(),就会开始录制了

bool UPSOCacheGatherHelper::SavePipelineFileCache(EPSOCacheSaveMode Mode)
{
	return FShaderPipelineCache::SavePipelineFileCache((FPipelineFileCache::SaveMode)Mode);
}

开始录制后在内存中不会自动保存本地,在我们录制结束的时候手动调用SavePipelineFileCache(EPSOCacheSaveMode::BoundPSOsOnly)保存到本地,至此手动采集结束.

采集完成

当你手动收集完成或者自动收集完成以后可从手机磁盘目录 /SDCard/UE4Game/{ProjectName}/Saved/CollectedPSOs 下取得 后缀名为 .upipelinecache的文件

特别说明一下,采集文件可分开录取到多个文件中,这些文件在生成最终PSO缓存时会自动合并的

3.生成SHK

我自己在项目目录下建了一个Tools/PSOCache 目录,专门作为PSO相关的工作目录
image.png
之前采集到的.rec.upipelinecache文件放到对应文件夹下,然后我们就要生成SHK文件了,这个文件是在打包时Cook生成的,直接在DefaultEngine.ini中加上

[DevOptions.Shaders]
NeedsShaderStableKeys=true

然后打包项目,在 %ProjectPath%\Saved\Cooked%%a\Client\Metadata\PipelineCaches\目录下就会找到.SHK文件(4.27版本以上引擎是这个格式,之前是.scl.csv)

手动生成SHK文件

如果你的项目在生成APK时会COOK到所有文件,那你就忽略下面手动生成,直接跳到下一段

这一步按正常流程应该是打包时自动生成全部.shk文件,但是因为这个文件的信息录入跟cook的文件列表挂钩,我们的首包apk又不是打的完整的文件列表,是排除掉大部分文件的,所以会导致没法自动生成,我只能写了个工具去专门处理这种情况,通过测试,有两种方案可以达到这一步的效果

1.首包不做排除方案,通过引擎自带的配哪些打哪些进去以做到只打指定资源进首包,但是测试发现在我们项目中,因为存在软引用的情况,所以无法直接做到,需要修改引擎才行,所以目前暂用第二种方案

2.我通过工具修改ini文件,生成完shk以后再把ini还原的方式去生成,操作上直接执行 ProjectDir/Tools/PSOCache/CookFullAssert.bat 即可,等他执行完成,会在 当前目录下的 PipelineCaches/对应平台下生成上图中的两个SHK文件,名字什么的都不要动,然后上传到svn即可,这个SHK文件理论上只需要在资源有修改时才去重新生成即可

脚本中其实就是在ini里面加上这个NeedsShaderStableKeys和去掉排除的文件夹, 然后直接执行cook指令

%EnginePath% %ProjectPath%\Client.uproject -run=Cook  -TargetPlatform=%%a -fileopenlog -ddc=InstalledDerivedDataBackendGraph -unversioned -stdout -CrashForUAT -unattended -NoLogTimes  -UTF8Output -CookAll

最后把生成的两个SHK文件拷出来即可,完事以后会把ini置回,至于为什么要置回,后面使用时会讲到

4.导出合并

现在已经有了SHK文件和rec.upipelinecache文件,我们现在要做的就是把这些文件合并生成出最后的二进制PSO文件

ProjectDir/Tools/PSOCache/PSO_Expand.bat

call PublicParam.bat

for %%a in (%Platform%) do ( 
	if %%a==Android_ASTC %EnginePath% %ProjectPath%\Client.uproject -run=ShaderPipelineCacheTools expand "%ToolsPath%\rec.upipelinecache\%%a\*.rec.upipelinecache" "%ToolsPath%\PipelineCaches\%%a\*.shk" "%ToolsPath%\stablepc.csv\%%a\Client_GLSL_ES3_1_ANDROID.stablepc.csv"
	if %%a==IOS %EnginePath% %ProjectPath%\Client.uproject -run=ShaderPipelineCacheTools expand "%ToolsPath%\rec.upipelinecache\%%a\*.rec.upipelinecache" "%ToolsPath%\PipelineCaches\%%a\*.shk" "%ToolsPath%\stablepc.csv\%%a\Client_SF_METAL.stablepc.csv"
)

这个脚本就是合并导出所有rec.upipelinecache文件为一个.csv文件,这个stablepc.csv文件很重要,他就是最终PSO文件的源文件,这个可以当成保存文件上传到svn去,执行完这个批处理后会生成到 /stablepc.csv/对应平台/ *.stablepc.csv

ProjectDir/Tools/PSOCache/PSO_Build.bat


call PublicParam.bat

for %%a in (%Platform%) do ( 
	if %%a==Android_ASTC xcopy %ToolsPath%\stablepc.csv\%%a\Client_GLSL_ES3_1_ANDROID.stablepc.csv %ProjectPath%\Build\Android\PipelineCaches\Client_GLSL_ES3_1_ANDROID.stablepc.* /s /i /y
	if %%a==IOS xcopy %ToolsPath%\stablepc.csv\%%a\Client_SF_METAL.stablepc.csv %ProjectPath%\Build\IOS\PipelineCaches\Client_SF_METAL.stablepc.* /s /i /y
	
	if %%a==Android_ASTC call %EnginePath% %ProjectPath%\Client.uproject -run=ShaderPipelineCacheTools build "%ProjectPath%\Build\Android\PipelineCaches\*Client_GLSL_ES3_1_ANDROID.stablepc.csv" "%ToolsPath%\PipelineCaches\%%a\*.shk" "%ProjectPath%\Content\PipelineCaches\Android\Client_GLSL_ES3_1_ANDROID.stable.upipelinecache"
	if %%a==IOS call %EnginePath% %ProjectPath%\Client.uproject -run=ShaderPipelineCacheTools build "%ProjectPath%\Build\IOS\PipelineCaches\*Client_SF_METAL.stablepc.csv" "%ToolsPath%\PipelineCaches\%%a\*.shk" "%ProjectPath%\Content\PipelineCaches\IOS\Client_SF_METAL.stable.upipelinecache"
)

这个就是生成最终的.upipelinecache文件,这个文件是要打到包里去的,所以这个批处理就是把前面生成的.csv拷贝到ProjectDir/Build/对应平台/PipelineCaches 下,然后执行命令生成出.upipelinecache文件到ProjectDir/Content/PipelineCaches/ 下,然后可以把build下面的scv文件和Tools下面的都提交到svn上去

这里可以发现我们其实使用的就是最终content下的.upipelinecache文件,为什么还要保留中间文件.csv呢,其实这里就要说一下一个很重要也很鸡肋的东西,我们前面有说过NeedsShaderStableKeys=true加了这个以后就会自动生成SHK文件,其实这个.upipelinecache文件加了这个也会自动生成,前提是你要把.csv文件拷到build下面以后,打包的时候引擎会自动生成.upipelinecache并且打包,但是有个问题是引擎没考虑过会有热更的情况,如果走这个自动的流程,这个.upipelinecache文件是没法管理的,都是引擎那边处理死了,即使我通过指令手动生成到content下面,引擎有个恶心的地方是他会在打包的时候强制删除掉content下面的,所以这里想热更的话只能不走自动的流程,后面打包和热更步骤还会有说明

5.打包和热更

如果你不需要热更PSO文件,请忽略这一步,直接跳到运行时加载

通过前面生成出.upipelinecache文件以后,我们知道要把这个文件打包进平台包里去,以供游戏运行使用,可自由选择首包中要不要打这个文件,这个文件就是实际运行时会实时编译成一个缓存文件到手机本地去,然后在运行时需要新建PSO是他就会到本地寻找缓存并直接使用,以减少创建的消耗,因为大世界场景等资源会有热更的情况,当后续版本资源增加了,自然也要把这个upipelinecache文件热更到手机上去

  • 打包进首包 可以选择打到首包也可以选择不打,按道理不会影响,需要的话就只需要在ProjectSetting的Package选项配置一下PipelineCaches文件夹即可
    image.png
    这里有个小问题就是这个文件夹下是包含安卓和IOS的资源的,但是这个打包配置每个平台都是一份,所以只能全部打进去,好在这个文件一般来说都很小,所以可自己选择要不要打进首包

  • 热更打包 我这边打包工具是用的是HotPatcher,下面演示是基于这个插件的,现在项目热更是按照文件夹打到每个pak中去的,所以只需要选择额外增加一个pak放这个资源。或者直接放到之前的某个pak中去,无非就是选择配置非assert文件即可
    image.png
    这里是总的非Assert文件配置,可选择对应平台对应的资源,我这里是全部都配进去了,在这里配置应该是会打到default.pak中去
    image.png
    在每个chunk里面都有一个非assert的配置,这里演示的是比较正确的配法,直接AddExternFileToPak,把对应平台的这个文件打到对应平台的pak中即可

最后按路径总结下

按路径划分就两个重要的东西

  • 插件PsoCacheGather 这个主要负责PSO的实机采集任务
  • /ProjectDir/Tools/PSOCache 这个下面主要是批处理工具脚本,主要负责PSO二进制文件的生成工作,这里要重点说明下这些脚本执行前有一个重要的配置需要处理 PublicParam.bat
@echo off
set "EnginePath=XXX\Engine\Binaries\Win64\UE4Editor-Cmd.exe"

set "ProjectPath=XXXX"

set "ToolsPath={ProjectPath}\Tools\PSOCache"

REM Platform=Android_ASTC IOS
set Platform=Android_ASTC

运行时使用PSO

前面说完了生成和打包,运行时也有非常重要的处理,首先这个PSO的运行时编译缓存需要用到ShaderCode,那也就是说我们需要先处理ShaderCode

ShaderCode热更打包

image_10_L64PeH-I9w.png
新版的HotPatcher已经很方便的集成了,只需要这边开关开一下就会自动打包进pak里了

每个pak里面都会生成自己的ShaderLibrary,因为引擎启动时默认加载了首包中的ShaderCode,所以我们需要在游戏热更之后手动open一下所有的ShaderLibrary

在HotPatcher这个插件中有实现加载方法,UFlibPakHelper::LoadShaderbytecodeInDefaultDir ,只需要在热更过后的时机遍历加载每个Chunk即可,在没有自定义路径和名称的情况下,只需要传ChunkName就可以了,不用后缀名和路径,只需要Chunk名

编译PSO并加载

PSO加载之前必须已经操作完前面的ShaderCode加载

这里加载规则有一些特殊的情况,首先因为引擎起来的时候会自动去编译PSO,并且后面不会再去编译了,即使你手动去编译也无效,但是我们PSO热更的话,在引擎初始化的时候我们都还没开始热更,所以我们需要先关闭自动编译,然后在等热更完成以后动态的去编译并且加载缓存文件

  1. 关闭自动编译,修改ini,添加新的命令即可,具体添加哪些查看 [ini添加块段落]
  2. 因为手动编译时需要把自动编译的选项再打开,直接调用 EnableShaderPipelineCache(true),然后再去编译和加载PSO
  3. 还有一种特殊情况需要处理,就是当第一次安装包时,本地的Program Binary Cache文件是不存在的,在加载PSO缓存时,会先生成这个缓存到本地,那也就是说我们需要调用两次LoadShaderPipelineCache(),最好判断一下本地目录是否存在,每个平台的目录在下面两张图中,至于目录怎么获取,直接从LoadShaderPipelineCache往里面跟会有获取Cache路径的方法
  4. 前面说的把Cache文件编译到本地以后,还需要加载到内存中去,游戏中才能生效,所以还需要调用一次 UFlibShaderPipelineCacheHelper::LoadShaderPipelineCache(),只不过这一次要判断下FShaderPipelineCache::NumPrecompilesRemaining() ≤ 0 的时候再去load,防止还没编译完就加载了,这些操作其实建议在loading界面的时候去做,等这些操作完成后再关闭Loading界面

image_11_H3tHSIlMTc.png

安卓的Program Binary Cache文件所在目录

image_12_jvmBZx2stN.png

IOS下所在的目录

ini添加块段落

[ConsoleVariables]
r.ShaderPipelineCache.Enabled=0
r.ProgramBinaryCache.RestartAndroidAfterPrecompile=0

把这个添加到DefaultEngine.ini中就行了

结语

可以看到,PSO的整个生成加使用的流程即使在工具的帮助下依然是非常麻烦的,所以如果大家项目没有明显的Shader编译卡顿的情况,可以考虑不用使用这个东西,本文章基本从实用角度详细介绍了整个流程,网上看了不少文章只点出了部分细节,在实际接入中碰到不少坑,这里最主要记录了主要步骤和坑点,当然其中可能还缺少了一些细节,大家可参考最下方的其他文章,有什么问题可在下方评论留言,Enjoy it~😀

  • 资料参考

UE4之PSO缓存 - 可可西 - 博客园

UE4中使用PSO缓存优化_ue4 pso_Yuk丶的博客-CSDN博客

UE 项目优化:PSO Caching | 虚幻社区知识库 (ue5wiki.com)