材质变体 PSO学习笔记

news/2024/10/23 13:56:51/

学习笔记

参考各路知乎大佬文章

首先是对变体的基本认知

概括就是变体是指根据引擎中上层编写(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等条目还是很有必要的


http://www.ppmy.cn/news/1541345.html

相关文章

案例分享-优秀蓝色系UI界面赏析

蓝色UI设计界面要提升舒适度,关键在于色彩搭配与对比度。选择柔和的蓝色调作为主色,搭配浅灰或白色作为辅助色,能营造清新、宁静的氛围。同时,确保文字与背景之间有足够的对比度,避免视觉疲劳,提升阅读体验…

攻防世界web引导模式 框架梳理

view_source 根据提示,没法右键,想办法右键查看源代码f12,找到flag robots 这个协议:表示网络爬虫可以访问哪些路径和不能访问哪些路径 之后查看路径得到flag backup 根据提示知道要找备份文件,搜索备份文件后缀…

嵌入式入门学习——8基于Protues仿真Arduino+SSD1306液晶显示数字时钟

0 系列文章入口 嵌入式入门学习——0快速入门,Let‘s Do It! SSD1306 1 Protues查找SSD1306器件并放置在画布,画好电气连接(这里VCC和GND画反了,后面仿真出错我才看见,要是现实硬件估计就烧毁了&#xf…

搞错了,再来!谷歌利用AI重新推出全新的Google Shopping

近年来,随着电子商务的迅猛发展,消费者对个性化和便捷购物体验的需求愈发高涨。谷歌,作为互联网巨头之一,一直在不断探索和创新,它一直在应对这样一个事实:越来越多的消费者首先访问零售商的网站&#xff0…

你了解的spring框架有哪些

列举一些重要的Spring模块? Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IOC 依赖注入功能。**Spring Aspects ** : 该模块为与AspectJ的集成提供支持。Spring AOP :提供了面向方面的编程实现。…

【Docker】Dockerfile 镜像实战

目录 一、构建SSH镜像 二、构建Systemctl镜像 三、nginx镜像 四、tomcat 镜像 五、mysql镜像 一、构建SSH镜像 mkdir /opt/sshd cd /opt/sshdvim Dockerfile #第一行必须指明基于的基础镜像 FROM centos:7 #作者信息 MAINTAINER this is ssh image <hmj> #镜像的操…

仅一行代码,使LLaMA3在知识编辑任务上表现暴涨35%!您确定不来试试嘛?

引言 LLMs常因错误/过时知识产生幻觉&#xff0c;而基于新知识微调耗时且易过拟合、引入额外的知识库或参数模块又会带来不断增加的存储空间压力。因此&#xff0c;基于“Locate-then-Edit”的知识编辑&#xff08;如ROME&#xff09;被提出&#xff0c;用“少时间成本、零空间…

破解数字化转型的挑战:应对物联网与微服务架构实施中的难点与解决方案

数字化转型已经成为现代企业提升竞争力、优化运营效率和提高客户体验的必经之路。然而&#xff0c;在转型过程中&#xff0c;企业往往面临技术、组织和管理等多方面的挑战。随着物联网&#xff08;IoT&#xff09;和微服务架构&#xff08;MSA&#xff09; 的普及&#xff0c;这…