Build_PSO

今天我们看看Build PSO具体是什么含义。Expand PSO得到了一系列"需求清单",利用这些清单,跑一个工具,就能够得到 .stable.upipelinecache 了。 .stable.upipelinecache 就是最终成品的PSO缓存,随包体分发给玩家。后文或接下来我的文章里,提到"PSO产物",都是指 .stable.upipelinecache 。这个"跑工具得到PSO产物"的过程,就是 Build PSO。工具命令行如下,其中最后一个参数是输出。
UE4Editor-Cmd.exe MyGame.uproject -run=ShaderPipelineCacheTools build C:/PSOCaching/*.stablepc.csv C:/PSOCaching/*.scl.csv MyGame_SF_METAL.stable.upipelinecache
提问:UE4默认的构建过程(Build、Cook、Stage、Package、Archive)中,就自带了 Build PSO的过程吗?
回答:不会默认自带。因此,需要自己在构建过程中加。但这并不是说非得在"Build、Cook、Stage、Package、Archive" 的过程中添加,Build PSO 本身可以在任意一台机器上,不一定是要在打包机上,只要将PSO的产物收集起来,最终构建机上拉到,那就可以了。所以上图 "在构建时" Build PSO,其实是宽泛的说法,你可以在构建apk之前准备好,就可以了。
假设非要在 "Build、Cook、Stage、Package、Archive" 的过程中 Build PSO,最晚可以在哪一个步骤中做呢?理论上,Build PSO的产物 .stable.upipelinecache 只要能进到最终apk / ipa / zip里,就好了,所以应该是 Stage 之前做好,会比较好,重点是最终进到apk(发行包体)中。
提问:为什么PSO要按照shader hash来收集,然后展开,然后再合并(逆展开)?
回答:原因是"多次run"方便合成最终PSO产物。破除不稳定(前面的文章有论述)。
另外有人说,是因为RHI独立性。我们一起看看,RHI独立性的解释如下:
RHI 是渲染硬件接口,如 DX12、Vulkan、Metal。PSO是一个包含了着色器、混合状态、深度模板状态等所有渲染状态集合的"巨型对象"。创建PSO是一个非常耗时的操作,而不同图形API创建PSO所需的具体参数和最终的二进制格式完全不同。这意味着:
-
一个在Vulkan API下编译好的PSO二进制块,不能直接拿到DX12上使用。
-
即使是同一个渲染效果(比如同一个材质),针对Vulkan和DX12也需要创建两个完全不同的PSO对象。
因此,UE4设计,在构建机上利用各台不同平台不同设备收集起来的"需求清单",然后再逐个平台逐个RHI产生PSO产物。
有人说,PSO之所以要"收集、展开、合并",是因为RHI独立性,我自己理解------不是的。因为哪怕RHI本身可混搭(设想PC、Android的RHI互通,不分彼此),不也得要"收集、展开、合并"吗,因为这一套主要解决"稳定"与否的问题,而不是为了兼容"RHI不独立"的问题。
稳定的PSO(.stable.upipelinecache)
提问:在 .stable.upipelinecache 中,其中是不是不存在ShaderHash呢?我想应该存在,因为Shader二进制代码并不直接由PSO对象包含,PSO对象始终要通过ShaderHash去找到Shader二进制?
回答:PSO确实需要通过某种方式找到正确的Shader二进制码。但在最终分发给玩家的 .stable.upipelinecache文件中,用于定位Shader的标识符,已经从易变的 ShaderHash转换为了稳定的逻辑密钥。
| 工作阶段与文件 | 核心标识符 | 特性与目的 |
|---|---|---|
**运行时记录 (.rec.upipelinecache)** |
**ShaderHash(着色器哈希值)** | 不稳定。此哈希值基于某次特定构建中编译出的着色器字节码生成。只要着色器代码有丝毫变动(甚至只是注释或编译器版本变化),哈希值就会改变,导致缓存失效。 |
**稳定PSO清单 (.stablepc.csv)** |
**稳定密钥(Stable Keys)** | 稳定 。这是一组由材质路径、着色器类型、顶点工厂类型、画质等级等逻辑属性构成的唯一组合。 它描述的是渲染需求(例如"使用材质A的顶点着色器"),而不关心这次构建产生的具体哈希值是什么。 |
**最终PSO缓存 (.stable.upipelinecache)** |
**稳定密钥(Stable Keys)** | 稳定 。这是 .stable.upipelinecache文件内部实际使用的索引方式。它使得缓存文件能够跨越不同的游戏构建版本而保持有效。 |
从"易变"到"稳定"
这个转换是如何发生的?关键在于PSO缓存生成流程中的 expand(展开) 和 **build(构建)** 这两个步骤。
-
收集与记录(产生不稳定的Hash) :您通过带
-logPSO参数运行游戏,收集到的.rec.upipelinecache文件里,记录的都是基于当时那次构建的、易变的ShaderHash。 -
展开(Translate:Hash → 稳定密钥) :使用
ShaderPipelineCacheTools工具的expand命令。这个命令就像一个"翻译官",它拿着一本"字典"(即项目打包时生成的.shk或.scl.csv文件),将.rec.upipelinecache记录中的每一个ShaderHash,"翻译"成对应的稳定密钥。 这一步生成了.stablepc.csv文件,此时的PSO需求清单已经与具体的ShaderHash解耦了。 -
构建(Build:稳定密钥 → 最终缓存) :在项目最终发布打包时,引擎执行
build操作。它读取上一步生成的稳定需求清单(.stablepc.csv),并根据当前项目最新的Shader代码 (通过再次查阅"字典".shk找到其最新Hash),为清单上的每一个稳定密钥组合编译出最终的PSO二进制码,生成.stable.upipelinecache文件。 至此,最终的缓存文件内部关联的就是稳定密钥。
即分发给玩家的 .stable.upipelinecache文件中------已经被更优的稳定密钥机制所替代。这正是UE4的Bundle PSO Cache系统能够实现跨版本稳定性的关键所在。
认识.stable.upipelinecache
.stable.upipelinecache 是一个二进制文件,在我项目中大概是 18M,它包含:
| 内容类别 | 具体说明 |
|---|---|
| 包含的核心数据 | 预编译的PSO二进制对象:这是文件的主体,包含了针对目标平台和图形API(如Vulkan、DX12)优化后的、可直接供GPU驱动使用的管线状态对象二进制数据。 |
| 包含的标识信息 | 稳定密钥(Stable Keys)的映射:文件内部通过一套稳定的标识符(例如材质路径、顶点工厂类型、着色器类型等)来索引对应的PSO,而不是依赖于易变的Shader哈希值。这确保了缓存能在不同的游戏构建版本间保持有效。 |
| 不包含的内容 | Shader源码或字节码 :PSO缓存文件本身不包含 任何着色器的源代码或编译后的中间字节码。真正的Shader代码由独立的Shader代码库(FShaderCodeLibrary) 管理。 Shader路径 :文件中也不直接存储Shader的资产路径,而是使用路径的哈希值(如SHA1)作为索引。 贴图资源本身,以及贴图的路径。 |
知识巩固:在ue4中,在Bundle PSO Cache(打包缓存)流程中,进包体的 .stable.upipelinecache中,会包含PSO对象本身吗?回答:包含。
平台差异性
提问:在UE4中,我可以利用安卓平台跑出来的.rec.upipelinecache ,去expand pso,然后build出PC平台的PSO吗?
直接的回答是:不可以 。这是因为PSO缓存从生成到使用的整个流程都与特定的目标平台和Shader格式紧密绑定。
下面的表格清晰地对比了安卓和PC平台在PSO缓存关键要素上的差异,这正是不支持混用的根本原因:
| 关键要素 | 安卓平台 | PC平台 (例如 DirectX 12) | 为何不能混用 |
|---|---|---|---|
| 目标平台/Shader格式 | 通常为 SF_VULKAN_ES31_ANDROID或 GLSL_ES3_1_ANDROID |
通常为 PCD3D_SM5(DirectX 11) 或 PCD3D_SM6(DirectX 12) |
不同平台的图形API和Shader语言完全不同,编译出的Shader字节码也完全不同。 |
| 顶点工厂与渲染状态 | 针对移动端GPU架构和特性进行优化。 | 针对桌面端GPU架构和特性设计。 | 即使材质逻辑相同,为不同平台优化的渲染路径、精度要求和可用功能也可能存在差异。 |
| Shader字节码 | 编译生成适用于移动端GPU的字节码(如SPIR-V)。 | 编译生成适用于桌面端GPU的字节码(如DXBC/DXIL)。 | Shader字节码的哈希值是PSO记录的基石,跨平台的字节码完全不同,哈希值也毫无关联。 |
虽然我们在流程中使用了"稳定密钥"(Stable Key)这一与具体Shader字节码解耦的概念,但整个PSO缓存机制的核心在于,稳定密钥最终必须在目标平台上映射到正确的、为该平台专门编译的Shader二进制代码上 。
-
平台特定的"字典" :在项目打包(Cook)时,UE4会为每个目标平台 生成其专属的稳定着色器密钥文件(如
.shk或.scl.csv)。这个文件就像是该平台的"专属字典",它建立了稳定密钥(如"材质A的顶点着色器")与该平台本次构建中产生的特定Shader字节码哈希之间的映射关系 。 -
Expand 过程的匹配 :
Expand操作的本质,是使用平台特定的"字典"(.shk文件)去"翻译"运行时记录(.rec.upipelinecache文件)。这个过程会检查记录中的Shader哈希是否能在"字典"里找到对应项。由于安卓的Shader哈希在PC的"字典"里根本不存在,这个匹配会失败,导致无法生成有效的.stablepc.csv文件 。 -
Build 过程的最终编译 :即便生成了
.stablepc.csv,最后的Build步骤也需要使用目标平台最新的"字典"(.shk文件)和该平台的Shader代码库,来编译出最终可供图形API直接使用的PSO二进制块(.stable.upipelinecache)。用安卓的输入不可能编译出PC可用的PSO 。
提问:在PSO的生产中,可以在WINDOWS构建机上,为iOS(metal)平台运行 expand PSO、Build PSO的流程吗?
回答:在我所在的项目里,确实是这么做的。
自动去重
提问:在ue4的pso中,如果同一平台同一rhi同一组shader稳定密钥,收集了不同构建版本(例如昨天、今天构建的app)app的.rec.upipelinecache,那么build pso(合并、逆展开)时会怎么做?
回答:当您为同一组稳定密钥 收集了不同构建版本(如昨日和今日)的PSO使用记录(.rec.upipelinecache文件)时,最终的Build(合并/逆展开)过程会基于稳定密钥进行去重与合并 ,最终生成一个唯一且最优 的稳定PSO缓存文件(.stable.upipelinecache)。
这套流程的核心价值在于,它将PSO需求的收集 与PSO的最终编译这两个步骤解耦了。
-
**需求收集(收集.rec.upipelinecache)** 可以跨越多个开发版本,持续进行,力求覆盖所有游戏内容。只要材质的核心逻辑没变(稳定密钥不变),之前辛苦收集的记录就持续有效。
-
**最终编译(Build生成.stable.upipelinecache)** 总是在项目打包发布时,基于当前最准确的Shader代码进行一次性的、干净的编译,确保分发给玩家的是最优结果。
BuildPSO时不再需要.rec.upipelinecache
提问: 我发现,Engine\Binaries\Win64\UE4Editor-Cmd.exe YourProject.uproject -run=ShaderPipelineCacheTools build "Path/To/Your/StableCache.stablepc.csv" "Path/To/GlobalShaderStableInfo.shk" "Path/To/ProjectShaderStableInfo.shk" "Output/Path/For/Final/PSO/Cache.stable.upipelinecache"
这个指令里并没有传入.rec.upipelinecache ,难道是因为不需要.rec.upipelinecache吗?
回答:在 build命令中,确实不需要 也不应该 传入 .rec.upipelinecache文件。因为Expand PSO的时候,已经将 .rec.upipelinecache 中携带的必要信息,给转换为了 .stablepc.csv 了。这些必要的信息就是 PSO记录。
| 步骤 | 核心输入文件 | 核心输出文件 | 该步骤的核心作用 |
|---|---|---|---|
| **1. Expand (展开)** | • 运行时记录 (.rec.upipelinecache) • 稳定密钥字典 (.shk) |
稳定的PSO清单 (.stablepc.csv) |
"翻译":将易变的、与特定构建绑定的PSO记录,转换为稳定的、与Shader逻辑对应的需求清单。 |
| **2. Build (构建/逆展开)** | • 稳定的PSO清单 (.stablepc.csv) • 稳定密钥字典 (.shk) |
最终的二进制PSO缓存 (.stable.upipelinecache) |
"编译":根据稳定的需求清单,利用当前最新的Shader代码,编译出可供图形API直接使用的最终PSO二进制文件。 |
并且, .rec.upipelinecache不包含真实的PSO对象,也不包含Shader二进制码、也不包含贴图,它什么都不包含,唯独包含"运行时遇到的PSO记录信息",即需求清单,所以它在Build PSO中没有利用价值,可以舍弃掉。
提问:在UE4游戏运行时中,游戏runtime应该不再需要读取 .scl.csv,而是直接使用 .stable.upipelinecache 就可以找到用到的PSO,因为其中的PSO Key是稳定的Key,而不是Shader Hash。我的理解对吗?
回答:是的。
引申学习:不可避免的运行时Shader编译
提问:在ue4中,在有Bundle PSO Cache(打包缓存)的情况下,首次进入游戏,能大大缓解编PSO的卡顿。但首次进入游戏,依然需要编Shader,是吗?
回答:即使已经预先准备好了Bundle PSO Cache,玩家首次进入游戏时,着色器仍然需要一个编译过程。不过,Bundle PSO Cache的存在极大地优化了这个过程,将其从可能导致游戏卡顿的"实时编译"转变为了更平滑的"预编译"。
为了帮助您更清晰地理解整个流程,下面的表格对比了有无Bundle PSO Cache时首次运行游戏的差异。
| 特性 | 有 Bundle PSO Cache | 无 Bundle PSO Cache |
|---|---|---|
| 编译核心 | 预编译PSO 。核心工作是根据缓存文件,提前创建完整的管线状态对象。 | 实时编译PSO。在每次绘制调用时,临时编译所需的PSO。 |
| 编译时机 | 集中在游戏首次启动的加载界面。 | 分散在游戏过程中,每当遇到新的画面组合时。 |
| 用户体验 | 加载时间可能较长,但进入游戏后卡顿极少。 | 加载快,但游玩过程中会频繁出现卡顿。 |
| 技术本质 | 将运行时(in-game)的编译开销转移到了加载时(loading)。 | 无法避免运行时的编译开销。 |
为什么有了PSO缓存还需要编译呢?这需要理解PSO和Shader的关系:
-
PSO是什么:管线状态对象是一个"渲染配方",它不仅仅包含着色器代码的索引,还包含了混合模式、深度测试、光栅化状态等所有渲染所需的设置组合。在现代图形API(如Vulkan、DX12)下,创建这个PSO对象是一个非常耗时的操作。Bundle PSO Cache 会:① 节省PSO创建的时间,② 将游玩时shader编译的开销,转移到 加载时,避免游玩时卡顿。此处提到的 游玩时 (例如局内)、加载时(例如Loading界面),都属于游戏运行时。
-
为何Shader仍需编译 :虽然PSO"配方"是现成的,但"食材"------即着色器代码------仍需在玩家设备上完成最终加工。游戏打包时附带的着色器是一种中间代码 (如DXBC、SPIR-V),它需要在运行时由玩家电脑上的显卡驱动程序 编译成当前GPU专属的机器码。Bundle PSO Cache解决了PSO创建的耗时问题,但并没有绕过着色器中间码到机器码的最终编译步骤。值得注意的是,显卡驱动程序自己也会缓存这次编译的结果(驱动缓存)。这意味着,第二次及以后启动游戏时,连着色器的最终编译时间也大大缩短了,加载速度会显著提升。
总结
-
Bundle PSO Cache 主要解决的是 "状态组合"的编译耗时问题,通过预创建PSO避免了游戏中的卡顿。
-
首次进入游戏时的编译 ,包含了将着色器中间代码转换为特定GPU机器码的过程。
因此,Bundle PSO Cache并没有消除首次加载时的等待,但它成功地将可能破坏游戏体验的运行时卡顿 ,转化为了集中的、可预测的加载界面等待,这对于提升玩家体验至关重要。
提问:在UE4的PSO中,不包含Shader二进制码,也不会包含贴图资源本身。既然不包含,是否意味着,即使PSO 缓存命中,是不是也得等待贴图的加载?
回答:在UE4的PSO缓存机制中,PSO缓存既不包含Shader二进制码,也绝对不包含任何贴图资源本身 。PSO缓存命中确实可以避免着色器编译引起的卡顿,但模型的最终渲染完成,还需要等待其所需的贴图资源加载完毕。一个优化的策略是:优先确保关键模型的PSO已预编译,同时积极启动其所需贴图的异步流送加载。这样可以让CPU(PSO工作)和IO(贴图工作)并行不悖,最大化利用加载时间,减少玩家进入可玩状态后的等待。