.rec.upipelinecache存放的位置
很多教程、日常口头表达中,"PSO" 或 "PSO文件"可能会是指.rec.upipelinecache、.scl.csv、.stable.upipelinecache、.shk、.stablepc.csv 。因此我们就避开 "PSO" 或 "PSO文件" 这样笼统的称呼。
在"介绍一"里面提到的"游戏厂商录制PSO记录"的环节里,PSO记录文件会产生到哪里?PSO运行记录文件存放路径为 Saved/CollectedPSOs/*.rec.upipelinecache,如下图所示。同时,它们是二进制的。

.rec.upipelinecache包含的内容
.rec.upipelinecache 包含与不包含以下这些内容:
1、不包含 Shader二进制码本身。.rec.upipelinecache只记录引用标识(哈希),并不包含着色器的实际二进制代码;
2、包含 Shader字节码的哈希值。这是 .rec.upipelinecache文件中的核心标识符。它记录的是在某一次特定的游戏构建版本中,实际用到的PSO所对应的着色器字节码的哈希值。
3、不必包含所有游戏情景下遇到的所有PSO记录。对于有的游戏来说也做不到遍历全部情景。一个 .rec.upipelinecache 只包含这一次运行游戏时对应的PSO记录。
4、**不包含 PSO对象本身。**它不包含已编译好的、可直接供GPU驱动使用的PSO二进制对象 。它记录的是一条条"PSO使用记录",而非PSO实体。
5、不包含 稳定密钥与Shader哈希的映射关系。因为建立"稳定密钥"(如材质路径、顶点工厂类型)与"着色器哈希"映射关系的文件是 .shk 文件(Shader Stable Info)或 .scl.csv,它是在项目打包(Cook)时生成的 。.rec.upipelinecache文件本身只是一个运行时记录,它不包含这种映射字典。
因此你会发现,.rec.upipelinecache 文件都并不大,很多都是1M以下的。
Expand PSO的意义在于求得稳定Key
提问:为什么需要Expand PSO?原理上,不能直接利用rec.upipelinecache直接当成PSO缓存吗?
回答:不行,不能直接利用rec.upipelinecache来作为PSO缓存。 rec.upipelinecache文件包含的是"瞬时"信息,它与项目构建时特定的Shader二进制码严格绑定,无法在项目重新打包后直接使用(为何Shader容易变动 请见后文"容易变动的Shader哈希"小节)。而Expand PSO这一步,正是将这些"瞬时"记录转换成一个与Shader逻辑描述绑定的、"稳定"的缓存文件的关键过程。
下面这个表格清晰地展示了两类缓存文件的根本区别。
| 特性对比 | 记录缓存 (Recorded Cache) .rec.upipelinecache |
稳定缓存 (Stable Cache) .stablepc.csv或 .spc |
|---|---|---|
| Shader标识符 | Shader字节码的SHA哈希值 | 材质名、顶点工厂名等**稳定键(Stable Key)** |
| 与构建的关系 | 强依赖于本次构建的Shader字节码 | 关联于Shader的高级逻辑描述,与具体字节码解耦 |
| 版本兼容性 | 极差。Shader代码稍有改动,哈希值就变,整个缓存失效 | 极强。只要材质网络结构不变,即使Shader重新编译也能匹配 |
| 主要用途 | 在同一构建版本下,记录本次运行遇到的PSO列表 | 作为稳定资产随项目打包,供所有玩家在游戏首次启动时预编译PSO |
Expand PSO 过程的核心是转换与关联。它利用项目打包(Cook)时生成的稳定着色器键 (Stable Shader Key, .shk 文件、或 .scl.csv) 作为"翻译字典"。这个过程(也称为"扩展")将运行时记录的PSO列表(.rec.upipelinecache)中的、基于易变的SHA哈希的引用,转换为基于稳定的材质名称等逻辑信息的描述。
这样做的巨大价值在于:
-
实现缓存的持久化 :生成出的
.stablepc.csv或.spc文件不再依赖于某一次特定的编译结果。只要材质的高层逻辑(如节点连接)没有发生颠覆性改变,即使项目代码变动、Shader重新编译,这个稳定缓存文件在下次打包时依然有效,无需重新收集。 -
支持大规模协作与分发:开发团队可以系统性地进行测试,收集全面的PSO数据,生成一个高覆盖率的稳定缓存文件,并将其打包进最终发行的游戏中。确保所有玩家在首次进入游戏时,就能在加载界面编译大部分PSO,从而极大减少游戏过程中的卡顿。
稳定的PSO和不稳定的PSO
什么是稳定PSO?什么是不稳定的PSO?这得从.rec.upipelinecache的格式入手去了解。我们解析出 .rec.upipelinecache (它本身是二进制不可直接读),看到其格式如下:

红框处 Vertex shader, Pixel shader存储的是哈希值,不稳定。为何Shader容易变动 请见后文"容易变动的Shader哈希"小节。Shader Hash的计算对于不同的平台计算方法也不一样,比如vulkan是用SPIR-V中间码来计算hash值的,GLSL是用文本源码来计算哈希的。
不稳定会怎么样?
在构建机上,假设就保持这些不稳定的hash,喂给UE4,当UE4将若干个.rec.upipeline 都整合成为 "打进app里的发行版 PSO Cache "(包含了PSO对象的upipelinecache)时,会傻眼------"我构建机上压根没有 6D4049F688... 的Shader啊,它早就消失在某次构建的历史长河中了!"
稳定会怎么样?
在构建机上,假设用稳定的Key,喂给UE4,当UE4将若干个.rec.upipeline 展开后得到的稳定Key,都整合成为 "打进app里的发行版 PSO Cache "(包含了PSO对象的upipelinecache)时,会方便找到需要的Shader**------"我当前要生产某PSO,它的VS、PS是某某名字,找得到;该PSO的BlendState, RasterizerState..PrimitiveType 为某某值,某某要素为某某值,都是明确的,因此知道如何生产出此PSO对象!"**
总结: .rec.upipelinecache是不稳定的PSO,因为它其中的Shader由哈希来表达,因此不稳定,不能被任意版本app所使用到。将它expand PSO得到稳定key,用这个稳定的key去创建得到任意版本都能理解的 .stable.upipelinecache ,这个.stable.upipelinecache 就是稳定的PSO缓存了。
拖堂小知识 >
容易变动的Shader哈希
引言------在UE4中,.ush, .usf 是Shader的源码,它们编译出来、进入app包体里给Runtime使用的是 Shader二进制码。Shader二进制码中任何一部分变动,其整体哈希就会变动。
提问:在UE4中,Shader字节码的SHA哈希值 是非常容易变动的,这是为什么?如果构建机上没有任何新的代码、资源更新,直接再次构建得到APP,Shader字节码对应的哈希值会变动吗?此哈希值取决于什么因素?
回答:在UE4开发中,Shader字节码的SHA哈希值确实非常容易变动,这主要是因为其生成机制依赖于一个包含大量可变因素的复杂依赖网络。
SHA哈希值的决定因素
Shader字节码的SHA哈希值并非由单一因素决定,而是由一长串相互关联的输入共同计算得出的。下表整理了这些关键的决定因素:
| 类别 | 具体因素 |
|---|---|
| Shader源码 | • 引擎内置的.usf等源文件的内容 • 材质蓝图节点生成的HLSL代码 |
| 编译环境与配置 | • 目标平台 (如Windows、Android) • 渲染质量等级 (Quality Level)和功能级别 (Feature Level) • 与材质搭配的顶点工厂 (Vertex Factory)类型 • 各种宏定义 (Macros)和静态开关(Static Switches)的状态 |
| 关联资源与代码 | • 材质引用的自定义HLSL代码 • 材质中引用的纹理资源 的哈希值 • 材质引用的参数集合(Parameter Collections)的ID |
| 引擎与工具链 | • 引擎版本 和Shader编译器的版本 • 编译时使用的第三方库(如HLSLCC)的版本 |
这个依赖网络极其庞大和敏感。例如,仅仅修改一个渲染质量等级的设置,或者引擎更新了某个.usf文件,都可能导致大量相关联的Shader重新编译并生成全新的哈希值。
无更新构建时的哈希稳定性
如果你的构建机上确实没有任何新的代码、资源更新,并且保证了编译环境的一致性,那么再次构建得到的APP,其Shader字节码的哈希值理论上应该是保持不变的。
但这有一个重要前提:构建环境的绝对纯净和一致。这意味着:
-
代码和资源:确保没有未提交的更改,构建过程完全从干净的仓库拉取。
-
引擎和工具链:使用完全相同的引擎版本和编译工具,避免任何自动更新。
-
缓存机制 :UE4的**派生数据缓存(DDC)** 工作正常。DDC会缓存编译结果,如果输入参数的哈希值相同,就会直接使用缓存,从而避免重新编译并保证输出一致性。
在实践中,由于构建环境中的隐形变动(如系统库的微小更新、环境变量差异等),要实现100%的确定性构建并保证哈希完全不变,具有一定挑战性。
拖堂小知识 <
参考:
https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/PSOCaching/GatheringPSOData/