材质变体 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等条目还是很有必要的
相关文章:
材质变体 PSO学习笔记
学习笔记 参考各路知乎大佬文章 首先是对变体的基本认知 概括就是变体是指根据引擎中上层编写(UnityShaderLab/UE连连看)中的各种defines情况,根据不同平台编译成的底层shader,OpenGL-glsl/DX(9-11)-dxbc DX12-dxil/Vulkan-spirv,是打到游…...
2024年【烟花爆竹储存】考试及烟花爆竹储存复审模拟考试
题库来源:安全生产模拟考试一点通公众号小程序 烟花爆竹储存考试参考答案及烟花爆竹储存考试试题解析是安全生产模拟考试一点通题库老师及烟花爆竹储存操作证已考过的学员汇总,相对有效帮助烟花爆竹储存复审模拟考试学员顺利通过考试。 1、【单选题】( …...
文件夹操作
文件夹操作 opendir closedir readdir write(fd,buf,strlen(buf)); return 0; } 作用 : 打开目录 opendir 所有头文件 : #include <sys/types.h> #include <dirent.h> 函数 : DIR *opendir(const char *name); 参数: name :目…...
如何制作一台自己想要的无人机?无人机改装调试技术详解
制作一台符合个人需求的无人机并对其进行改装调试,是一个既具挑战性又充满乐趣的过程。以下是从设计、选购材料、组装、调试到改装的详细步骤: 一、明确需求与设计 1. 明确用途与性能要求: 确定无人机的使用目的,如航拍、比赛、…...
Linux -- 进程间通信、初识匿名管道
目录 进程间通信 什么是进程间通信 进程间通信的一般规律 前言: 管道 代码预准备: 如何创建管道 -- pipe 函数 参数: 返回值: wait 函数 参数: 验证管道的运行: 源文件 test.c : m…...
网站的SSL证书快到期了怎么办?怎么续签?
网站的SSL证书即将到期时,需要续签一个新的证书以保持网站的安全性和信任度。以下是续签SSL证书的一般步骤: 1. 选择证书提供商 如果您之前使用的是免费证书,您可以选择继续使用同一提供商的免费证书服务进行续签。如果您需要更高级别的证书…...
解決爬蟲代理連接的方法
爬蟲在運行過程中常常會遇到代理連接的問題,這可能導致數據抓取的效率降低甚至失敗。 常見的代理連接問題 代理IP失效:這是最常見的問題之一。有些代理IP可能在使用一段時間後失效,導致連接失敗。 連接超時:由於網路不穩定或代…...
Prometheus 监控Harbor
你好!今天分享的是基于Prometheus监控harbor服务。 在之前的文章中分别介绍了harbor基于离线安装的高可用汲取设计和部署。那么,如果我们的harbor服务主机或者harbor服务及组件出现异常,我们该如何快速处理呢? Harbor v2.2及以上…...
SQL 干货 | SQL 半连接
大多数数据库开发人员和管理员都熟悉标准的内、外、左和右连接类型。虽然可以使用 ANSI SQL 编写这些连接类型,但还有一些连接类型是基于关系代数运算符的,在 SQL 中没有语法表示。今天我们将学习一种这样的连接类型:半连接(Semi …...
洛谷 P1226:【模板】快速幂
【题目来源】https://www.luogu.com.cn/problem/P1226【题目描述】 给你三个整数 a,b,p,求 a^b mod p。【输入格式】 输入只有一行三个整数,分别代表 a,b,p。【输出格式】 输出一行一个字符串 a^b mod ps&a…...
nginx常规操作
Linux下查找Nginx配置文件位置 1、查看Nginx进程 ps -aux | grep nginx 圈出的就是Nginx的二进制文件 2、测试Nginx配置文件 /usr/sbin/nginx -t 可以看到nginx配置文件位置 3、nginx的使用(启动、重启、关闭) 首先利用配置文件启动nginx。 nginx -c /usr/local/nginx/conf…...
Docker镜像不能访问
Get "https://registry-1.docker.io/v2/": dial tcp 192.168.10.194:443: connect: connection refused Idea推送镜像至Harbor私服,报以上错误,Docker镜像地址不能访问,更新Harbor服务器Docker镜像地址,重启Docker服务…...
TCP simultaneous open测试
源代码 /*************************************************************************> File Name: common.h> Author: hsz> Brief:> Created Time: 2024年10月23日 星期三 09时47分51秒**********************************************************************…...
Spring 配置文件动态读取pom.xml中的属性
需求: 配置文件中的 spring.profiles.active${env}需要打包时动态绑定。 一、方案: 在pom.xml文件中配置启用占位符替换 <profiles><!-- 本地开发 --><profile><id>dev</id><properties><env>dev</env>…...
Konva 组,层级
代码: <template><div class"rect"><div class"header"> <!-- <el-button type"primary" click"show">展示</el-button>--> <!-- <el-button type"success&quo…...
vue图片加载失败的图片
1.vue图片加载失败的图片 这个问题发生在测试环境和开发本地,线上环境是可以的,测试环境估计被第三方屏蔽了 2.图片有,却加载不出来 <template v-slot:imageUrlsSlots"{ row }"><div class"flexRow rowCenter"&…...
终止,半成收入来自海外,收入可持续性被质疑
芬尼科技终止原因如下:芬尼科技4年期间经历了两次IPO失败,公司半成收入来自海外,然而公司泳池收入面临欧洲地区冲突冲击及德国新节能措施影响。交易所质疑其收入是否具有可持续性。 作者:Eric 来源:IPO魔女 9月25日&a…...
日常记录,使用springboot,vue2,easyexcel使实现字段的匹配导入
目前的需求是数据库字段固定,而excel的字段不固定,需要实现excel导入到一个数据库内。 首先是前端的字段匹配,显示数据库字段和表头字段 读取表头字段: 我这里实现的是监听器导入,需要新建一个listen类。 读Excel …...
Unable to open nested entry ‘********.jar‘ 问题解决
今天把现网版本的task的jar拖回来然后用7-zip打开拖了一个jar进去替换mysql-connector-java-5.1.47.jar 为 mysql-connector-java-5.1.27.jar 启动微服务的时候就报错下面的 Exception in thread "main" java.lang.IllegalStateException: Failed to get nested ar…...
反编译华为-研究功耗联网监控日志
摘要 待机功耗中联网目前已知的盲点:App自己都不知道的push类型的被动联网、app下载场景所需时长、组播联网、路由器打醒AP。 竞品 策略 华为 灭屏使用handler定时检测(若灭屏30分钟内则周期1分钟,否则为2分钟),检…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...
