材质变体 PSO学习笔记

学习笔记

参考各路知乎大佬文章

首先是对变体的基本认知

概括就是变体是指根据引擎中上层编写(UnityShaderLab/UE连连看)中的各种defines情况,根据不同平台编译成的底层shader,OpenGL-glsl/DX(9-11)-dxbc DX12-dxil/Vulkan-spirv,是打到游戏包里的

在引擎开发编辑模式下,Unity/UE用户层写的是HLSL,根据引擎选择的目标平台,编译底层shader的流程也有区别。项目打包出来到目标平台上,是不会用开发时的HLSL再在目标平台上实时编译成底层shader的,是在游戏打包时,将目标平台的所有shader变体(glsl/dxbc/spirv)生成好打包进去

并且由于压缩,变体数对于游戏包体的影响可能不是很大

DX9-DX11 dxbc是字节码,GPU上跑的机器码还需要进一步转换成二进制码

DX12(2014年推出) dxil,GPU仍需要转成二进制码,但是多了很重要的PSO Cache流程

OpenGL glsl不需要离线编译,直接交给GPU驱动编译成机器码。OpenGL 4.1以上可以通过glGetProgramBinary回读这个二进制码,省去之后的编译工作

OpenGL ES 3.0(2012年推出)以上也可以通过glGetProgramBinary回读厂商的机器码

Metal(2014年推出) AIR字节码,交给GPU驱动编译成机器码

最后GPU上跑的不是glsl/dxbc/spirv GPU上跑的是机器码

现在的各种游戏,第一次进入游戏时总会有一个编译着色器的流程,这一步是在做什么?Unity和UE项目做的事是不太一样的,后面讲到PSO时再提及

首先看看手机OpenGL ES流程

以OpenGL ES3.0为例,不同的硬件厂商的GPU其机器码标准是不一样的,所以第一次进游戏时,需要把hlsl编译成当前手机硬件的GPU机器码,并且可以通过glGetProgramBinary回读厂商的机器码将编译好的机器码存在本地磁盘,下次进游戏时直接从磁盘读取编译好的产物

那么有没有办法不在游戏第一次打开时,不想等这么久的着色器编译,可以快速开始游戏呢?有的,随着厂商的发展,有抽象出一种中间格式的语言,这种语言机器友好,编译非常快速,并且具备跨机器运行的能力

及以下三家及对应的中间语言,如果以后的游戏是用DX12/Vulkan/Metal开发的,游戏打包时就可以将shader编译成对应的中间语言,但是这样做同样有问题,那就是这种中间格式的大小比shader源码大很多

然后看看PC DX(9-11)/12的流程

DX(9-11)的dxbc传递给显卡生成机器码

DX12 dxil取代了dxbc

由于dxbc和dxil是互不相通的,所以游戏为了支持不支持DX12的老电脑,只能将shader源码打到游戏包中,实际根据用户电脑是否支持DX12,再编译成dxbc/dxil,最后再是dxil生成PSO Cache,所以黑猴耗时长的部分在源码到dxbc/dxil这一步

PSO Cache(Pipeline State Object Cache渲染管线状态对象缓存)

Metal和Vulkan中有和PSO对应的概念,但并不叫PSO,只是经常用PSO替代称呼,OpenGL/ES没有PSO概念

要了解PSO是什么,先回忆一下GPU流水线。绘制一个物体的整个流程(pipeline),除开shader,其中的还有很多状态设置,比如是否进行透明度混合,混合方式是什么等等。

PSO Cache做的事就是把整个pipeline生成的机器码存下来

这里的PSO包括了shader和应用层设置渲染状态的代码

PSO和硬件是强绑定的,不同显卡/显卡驱动生成的PSO缓存也是不能通用的

OK有了上述认知,我们现在知道了现代API(DX12/Metal/Vulkan)提供了在应用层cache PSO的功能,针对不同的平台,引擎应用层会做相应的处理,那么接下来就可以看一下应用层的游戏引擎对应PSO Cache的相关流程了。

https://zhuanlan.zhihu.com/p/572503905

Unity

先看Unity,Unity6(2024.10.17发布)之前的版本是没有PSO Cache的功能的

老版本Unity Unity - Manual: Shader loading

是把加载的场景或资源所有的材质变体都加载到CPU中的,并且有一个可自定义大小的CPU空间存所有的变体,首次加载时,创建PSO的流程还是要走,可能会出现卡顿。创建过一次之后,会缓存该变体。当没有任何物体引用到某变体时从CPU和GPU中清掉。

为了提高效率,方案是变体WarmUp和变体收集文件ShaderVariantCollection的组合拳。

可以看出老版本的Unity,是无法省掉创建PSO的开销的,所以项目的重点会在于减少项目变体,剔除掉不用的变体,以及尽可能跑全变体收集文件上。

Unity6+ 对应UE的Bundle PSO Cache 当前只支持(DX12/Metal/Vulkan)

Unity - Scripting API: GraphicsStateCollection

新增PSO工作流,主要的功能在GraphicsStateCollection对象

流程还是跑游戏,根据目标平台缓存本地PSO Cache文件,因为开发期中材质变体可能会经常变动,所以跑游戏更新cache的思路是和原来的变体收集文件是一样的

cache的结果同样可以查看包含的变体,以及修改每个变体关联的渲染状态

PSO Cache也需要WarmUp,有同步和异步俩种方法执行

UE

UE中的PSO类型,这里主要关心的是Graphics PSO

UE4

Bundle PSO Cache

首先shader会在打包时编译成字节码,这些字节码有三种保存形式

1、在项目设置中,ShareMaterialShaderCode开关勾选才能走PSO Cache流程,如果没有勾选,字节码会打包附带于每个材质变体自身上,这样影响包体大小,虽然热更只需考虑增量,但这个方案大体量一些的项目基本都不会用

2、勾选,存成ushaderbytecode,UE维护一个ShaderCodeLibrary归档这些字节码,除Metal语言外的所有语言都使用该ShaderCodeLibrary

3、勾选,存成Native(metallib/metalmap),Metal原生ShaderCodeLibrary

后续没走PSO Cache的流程,创建PSO时就读取对应的字节码,然后二次编译PSO

PSO Cache文件有俩种文件类型:

.upipelinecache类型文件,这种是运行游戏时记录的 其中不会直接保存shader代码(无论是源码或者编译好的机器码),也不保存shader路径,保存的是shader路径的SHA hash作为索引

.spc(Stable PSO cache)类型文件 稳定的缓存信息

存储预计多个版本中不会改变的信息,如材质名称,顶点工厂名称,着色器类型等的描述称为stable key,UE5 UE4.27用.shk/UE4老版本用.scl.csv文件表示

Bundle PSO Cache的流程

https://dev.epicgames.com/documentation/en-us/unreal-engine/optimizing-rendering-with-pso-caches-in-unreal-engine?application_version=5.4

https://zhuanlan.zhihu.com/p/681319390

总体流程就是

1、打包时Cook一遍工程,扫使用到的所有材质变体,将编译成的平台无关的字节码存到shaderCodeLibrary,生成.shk文件

2、手机上跑游戏收集PSO,存到.upipelinecache文件中

增量收集

3、根据.shk文件和.upipelinecache文件,用ShaderPipelineCacheTools命令行生成.spc文件,然后将该.spc文件放到项目中再打包,spc文件会转换成upipelinecache文件打进包中,UE会整理成对应平台的PSOList

4、再次启动游戏,自动加载upipelinecache,编译shader时使用PSO Caching,收集的是对应GPU上编译成的机器码

5、重复流程

6、项目材质有重大改变时,可能需要重新记录Cache信息,因为老的没用到的PSO如果更新后根本没用到就纯浪费了

以上是项目打包相关的相关流程,接下来看一下手机跑游戏时PSO编译的流程

首先是三个关键流程

UE Graphics PSO缓存的信息包括

其中BoundShaderStateInput(BSS)包括

根据平台的不同,上诉信息可能只有部分作为PSO提交,其余走FallBack设置

之前提到OpenGL本身没有PSO机制,但是UE这套PSO Cache的流程,也将OpenGL的渲染状态抽象为PSO,起作用是PSO中的一部分信息BoundShaderState

UE虽然也提供了后台异步编译的功能,但是手游基本都会关闭此功能,而是在第一次加载游戏时全部一次性编译完

Usage机制

默认引擎会加载PSOList中的所有PSO,UsageMask可以添加筛选机制

LRU机制

生成的PSO可以缓存在内存中,OpenGL和Vulkan提供了LRU机制,可以限制加载到内存中的PSO数量,Metal没有该机制

UE5+

多了一套PSO Precache流程 UE5.3首次出现,5.4默认开启

https://dev.epicgames.com/documentation/en-us/unreal-engine/pso-precaching-for-unreal-engine?application_version=5.4

https://zhuanlan.zhihu.com/p/679832250

这是一套相对自动收集PSO Cache的方案,在Loading后就开始走收集流程,并在后台线程上异步编译

目前仅适用于D3D12 手游项目制作和Precache这套暂时无缘

如何控制项目材质变体的数量

UE变体数太多会导致什么问题

如果变体数很多,影响游戏包体大小,首次运行游戏时编译PSOCache耗时会比较长,全量编译PSO低端机可能会OOM,并且垃圾一点的手机编的也慢,加上发热等,影响玩家第一次的游玩体验。Metal编译生成的MemoryCache也会很大,而且随着游戏版本持续运营,又一直在出新效果玩法,对后续的膨胀问题就很难把控。还有图形驱动的升级会清掉PSO缓存,IOS升系统等导致得重新编译一次,又影响体验。

是时候回忆一下UE的材质系统了

VertexFactory

材质面板中勾选Usage后,UE会编译相应VertexFactory的shader变体

https://zhuanlan.zhihu.com/p/707759496

FShader持有ShaderCode在FShaderMapResource中的索引

FShaderType

FShaderType是FShader的元类,负责桥接FShader与对应的usf文件,FShader对应的FShaderType用using指定

当使用IMPLEMENT_MATERIAL_SHADER_TYPE时,就会为FShader构造一个相应的FShaderType,将FShader、Shader入口函数名,ShaderFrequency桥接起来,同时将FShaderType注册到一个全局列表中。编译Shader时会使用到这个全局列表

FMaterial/FMaterialResource

FMaterialShaderMap

FMaterialShaderMap中存储着材质在特定QualityLevel + ShaderPlatform下编译出的所有shader数据

其父类FShaderMapBase中的几个重要数据

FShaderMapResourceCode

FShaderMapResourceCode中存储的是编译后的shader代码,通过FShader存储的ShaderIndex索引

FShaderMapResource

FShaderMapResource负责创建和存储多个RHI端的shader,其子类有FShaderMapResource_SharedCode和FShaderMapResource_InlineCode,对应不同获取ShaderCode的方式,SharedCode就是前文所说,如果项目设置勾选了ShareMaterialShaderCode,保存在.uasset中的代码会统一放在.ushaderbytecode文件中,运行时创建一个FShaderCodeLibrary管理

FShaderMapContent

FMaterialShaderMap持有一个FShaderMapContent的引用,FShaderMapContent存有特定VertexFactoryType和ShaderType设置下对应的FShader实例

整体的流程可以分为俩个大的步骤,编译流程和绘制流程

首先看编译流程

https://zhuanlan.zhihu.com/p/85340922

https://zhuanlan.zhihu.com/p/707759496

材质编辑器中连的蓝图节点可以理解为只是HLSL生成过程中的一种输入,具体Pass用到什么shader,还得根据shader主干文件(如移动端BasePass的MobileBasePassVertexShader.usf MobileBasePassPixelShader.usf),VertexFactory,Common文件等生成最终的HLSL,然后再根据对应图形API将HLSL编译成对应shaderCode

其中FHLSLMaterialTranslator MaterialTemplate.usf模版的填充,自定义材质节点的一些使用之前也提过这里就不再提了

编译流程,我们需要关心的大的步骤就是

UMaterial->FMaterial/FMaterialResource->FMaterialShaderMap

编译好的ShaderCode是保存在FShaderMapResource中的

不同VertexFactoryType ShaderType对应ShaderMap的生成逻辑在

FMaterialShaderMap::Compile()

FMaterial::GetDependentShaderAndVFTypes()中

https://zhuanlan.zhihu.com/p/467788335

然后是绘制流程

谈及变体主要涉及的是MeshMaterialShader(MaterialShader的子类),那么就需要回忆下Mesh Draw Pipeline的流程

https://dev.epicgames.com/documentation/en-us/unreal-engine/mesh-drawing-pipeline?application_version=4.27

MeshBatch的Cache和Dynamic生成流程,后续的MeshPassProcessor和MeshDrawCommand生成流程之前讲过,这里就不再重述了。

mesh如何知道自己对应的vertexFactory就在生成MeshBatch流程中完成

FMeshBatchElement包含的是一个基本的绘制需要的信息

MeshDrawCommand包含了一次drawCall所需的全部信息,渲染信息的收集绑定是在MeshPassProcessor中完成的

渲染所需相关的数据由MeshPassProcessor收集

渲染时shader的获取,关注

XXMeshProcessor::Process中的GetXXPassShaders如

其中根据RenderPass创建特定FShader对应的FShaderType实例,最后用TryGetShaders方法获取FShader实例

FMaterial::TryGetShaders中,先获取FMaterial中的FShaderMapContent,然后用FShaderMapContent::GetShader通过ShaderType template实例字符串索引对应的FShader实例

而FShader持有ShaderCode在FShaderMapResource中的索引

后续提交给RHI Thread找对应的硬件编译过的机器码或者PSO Cache绘制即可

要更细的话,其实还有一个游戏加载时的流程

https://zhuanlan.zhihu.com/p/681306302

OK 在有了以上内容的认知之后,我们就可以来看一下UE项目中有哪些地方可以优化变体和PSO Cache了

可以从正反俩角度出发分析

首先正向分析,项目中那些地方会影响产生的变体

https://zhuanlan.zhihu.com/p/681316533

1、静态材质开关

包含连连看中的staticSwitchParameter和.usf中项目自己加的#ifdef

设A为主材质(无论有多少个静态开关),BC为A的材质实例,如果BC的开关override情况是相同的,那么BC会有俩个shaderMap,对应的俩个shaderCode内容是一样的,经过ShaderCodeLibrary相同结果剔除机制,进包后是一个shaderCode。

这时D也是A的材质实例,E是C的材质实例,DE的开关override情况相同且与BC不同,那么DE也是俩个shaderMap,俩相同内容的shaderCode,进包后也是一个shaderCode。如果FE开关override没改动,那么FEC是同一套shaderMap。

2、材质Usage 注意这里的Usage和PSO Cache那个UsageMask不是一个概念

如前文所说,材质Usage的设置主要影响VertexFactory组合

项目中的主材质,尤其是通用主材质,AutoUsage开关都应该关闭,然后根据美术实际的使用情况,酌情考虑开关勾选以及是否需要拆分主材质

3、PSO UsageMask

做更细致的UsageMask拆分

然后是反向的分析

项目打包流程的.shk .spc文件都是很好的参考用于分析项目实际用到的变体情况,当然由于这俩是二进制文件,所以还得转成可阅读的文本文件

正向分析看不到实际用到的ShaderType情况和项目中图程侧的一些管线上的自定义修改。从.shk .spc反向分析shaderType,VFType,QulityLevel等条目还是很有必要的

相关推荐
武子康4 分钟前
大数据-212 数据挖掘 机器学习理论 - 无监督学习算法 KMeans 基本原理 簇内误差平方和
大数据·人工智能·学习·算法·机器学习·数据挖掘
CXDNW13 分钟前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0
使者大牙13 分钟前
【大语言模型学习笔记】第一篇:LLM大规模语言模型介绍
笔记·学习·语言模型
ssf-yasuo26 分钟前
SPIRE: Semantic Prompt-Driven Image Restoration 论文阅读笔记
论文阅读·笔记·prompt
As977_36 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
ajsbxi38 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
Rattenking39 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
TeYiToKu1 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
dsywws1 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
道法自然04021 小时前
Ethernet 系列(8)-- 基础学习::ARP
网络·学习·智能路由器