1.游戏引擎随笔 0x36:UE5.x Nanite 源码解析之可编程光栅化(下)
2.什么是PSOS
3.超详细 | 灰狼优化算法原理及其实现(Matlab)
4.用matlab实现粒子群优化算法的可视化模拟,跪求源代码!!!!
5.UE4.27 PSO Caching
游戏引擎随笔 0x36:UE5.x Nanite 源码解析之可编程光栅化(下)
书接上回。0.5的源码
在展开正题之前,先做必要的铺垫,解释纳尼特(Nanite)技术方案中的Vertex Reuse Batch。纳尼特在软光栅路径实现机制中,将每个Cluster对应一组线程执行软光栅,每ThreadGroup有个线程。在光栅化三角形时访问三角形顶点数据,但顶点索引范围可能覆盖整个Cluster的个顶点,因此需要在光栅化前完成Cluster顶点变换。纳尼特将变换后的顶点存储于Local Shared Memory(LDS)中,进行组内线程同步,确保所有顶点变换完成,光栅化计算时直接访问LDS,实现软光栅高性能。
然而,在使用PDO(Masked)等像素可编程光栅化时,纳尼特遇到了性能问题。启用PDO或Mask时,可能需要读取Texture,根据读取的Texel决定像素光栅化深度或是否被Discard。读取纹理需计算uv坐标,而uv又需同时计算重心坐标,增加指令数量,降低寄存器使用效率,影响Active Warps数量,降低延迟隐藏能力,导致整体性能下降。复杂材质指令进一步加剧问题。
此外,当Cluster包含多种材质时,同一Cluster中的三角形被重复光栅化多次,尤其是材质仅覆盖少数三角形时,大量线程闲置,浪费GPU计算资源。
为解决这些问题,纳尼特引入基于GPU SIMT/SIMD的Vertex Reuse Batch技术。技术思路如下:将每个Material对应的三角形再次分为每个为一组的Batch,每Batch对应一组线程,每个ThreadGroup有个线程,正好对应一个GPU Warp。利用Wave指令共享所有线程中的分发封装平台源码变换后的顶点数据,无需LDS,减少寄存器数量,增加Warp占用率,提升整体性能。
Vertex Reuse Batch技术的启用条件由Shader中的NANITE_VERT_REUSE_BATCH宏控制。
预处理阶段,纳尼特在离线时构建Vertex Reuse Batch,核心逻辑在NaniteEncode.cpp中的BuildVertReuseBatches函数。通过遍历Material Range,统计唯一顶点数和三角形数,达到顶点去重和优化性能的目标。
最终,数据被写入FPackedCluster,根据材质数量选择直接或通过ClusterPageData存储Batch信息。Batch数据的Pack策略确保数据对齐和高效存储。
理解Vertex Reuse Batch后,再来回顾Rasterizer Binning的数据:RasterizerBinData和RasterizerBinHeaders。在启用Vertex Reuse Batch时,这两者包含的是Batch相关数据,Visible Index实际指的是Batch Index,而Triangle Range则对应Batch的三角形数量。
当Cluster不超过3个材质时,直接从FPackedCluster中的VertReuseBatchInfo成员读取每个材质对应的BatchCount。有了BatchCount,即可遍历所有Batch获取对应的三角形数量。在Binning阶段的ExportRasterizerBin函数中,根据启用Vertex Reuse Batch的条件调整BatchCount,表示一个Cluster对应一个Batch。
接下来,遍历所有Batch并将其对应的Cluster Index、Triangle Range依次写入到RasterizerBinData Buffer中。启用Vertex Reuse Batch时,通过DecodeVertReuseBatchInfo函数获取Batch对应的三角形数量。对于不超过3个材质的Cluster,DecodeVertReuseBatchInfo直接从Cluster的VertReuseBatchInfo中Unpack出Batch数据,否则从ClusterPageData中根据Batch Offset读取数据。
在Binning阶段的AllocateRasterizerBinCluster中,还会填充Indirect Argument Buffer,将当前Cluster的Batch Count累加,用于硬件光栅化Indirect Draw的Instance参数以及软件光栅化Indirect Dispatch的ThreadGroup参数。这标志着接下来的光栅化Pass中,每个Instance和ThreadGroup对应一个Batch,以Batch为光栅化基本单位。
终于来到了正题:光栅化。本文主要解析启用Vertex Reuse Batch时的软光栅源码,硬件光栅化与之差异不大,封面小程序源码此处略过。此外,本文重点解析启用Vertex Reuse Batch时的光栅化源码,对于未启用部分,除可编程光栅化外,与原有固定光栅化版本差异不大,不再详细解释。
CPU端针对硬/软光栅路径的Pass,分别遍历所有Raster Bin进行Indirect Draw/Dispatch。由于Binning阶段GPU中已准备好Draw/Dispatch参数,因此在Indirect Draw/Dispatch时只需设置每个Raster Bin对应的Argument Offset即可。
由于可编程光栅化与材质耦合,导致每个Raster Bin对应的Shader不同,因此每个Raster Bin都需要设置各自的PSO。对于不使用可编程光栅化的Nanite Cluster,即固定光栅化,为不降低原有性能,在Shader中通过两个宏隔绝可编程和固定光栅化的执行路径。
此外,Shader中还包括NANITE_VERT_REUSE_BATCH宏,实现软/硬光栅路径、Compute Pipeline、Graphics Pipeline、Mesh Shader、Primitive Shader与材质结合生成对应的Permutation。这部分代码冗长繁琐,不再详细列出讲解,建议自行阅读源码。
GPU端软光栅入口函数依旧是MicropolyRasterize,线程组数量则根据是否启用Vertex Reuse Batch决定。
首先判断是否使用Rasterizer Binning渲染标记,启用时根据VisibleIndex从Binning阶段生成的RasterizerBinHeaders和RasterizerBinData Buffer中获取对应的Cluster Index和光栅化三角形的起始范围。当启用Vertex Reuse Batch,这个范围是Batch而非Cluster对应的范围。
在软光栅中,每线程计算任务分为三步。第一步利用Wave指令共享所有线程中的Vertex Attribute,线程数设置为Warp的Size,目前为,每个Lane变换一个顶点,最多变换个顶点。由于三角形往往共用顶点,直接根据LaneID访问顶点可能重复,为确保每个Warp中的每个Lane处理唯一的顶点,需要去重并返回当前Lane需要处理的唯一顶点索引,通过DeduplicateVertIndexes函数实现。java汽车美容源码同时返回当前Lane对应的三角形顶点索引,用于三角形设置和光栅化步骤。
获得唯一顶点索引后,进行三角形设置。这里代码与之前基本一致,只是写成模板函数,将Sub Pixel放大倍数SubpixelSamples和是否背面剔除bBackFaceCull作为模板参数,通过使用HLSL 语法实现。
最后是光栅化三角形写入像素。在Virtual Shadow Map等支持Nanite的场景下,定义模板结构TNaniteWritePixel来实现不同应用环境下Nanite光栅化Pipeline的细微差异。
在ENABLE_EARLY_Z_TEST宏定义时,调用EarlyDepthTest函数提前剔除像素,减少后续重心坐标计算开销。当启用NANITE_PIXEL_PROGRAMMABLE宏时,可以使用此机制提前剔除像素。
最后重点解析前面提到的DeduplicateVertIndexes函数。
DeduplicateVertIndexes函数给每个Lane返回唯一的顶点索引,同时给当前Lane分配三角形顶点索引以及去重后的顶点数量。
首先通过DecodeTriangleIndices获取Cluster Local的三角形顶点索引,启用Cluster约束时获取所有Lane中最小的顶点索引,即顶点基索引。将当前三角形顶点索引(Cluster Local)减去顶点基索引,得到相对顶点基索引的局部顶点索引。
接下来生成顶点标志位集合。遍历三角形三个顶点,将局部顶点索引按顺序设置到对应位,表示哪些顶点已被使用。每个标志位是顶点的索引,并在已使用的顶点位置处设置为1。使用uint2数据类型,最多表示个顶点位。
考虑Cluster最多有个顶点,为何使用位uint2来保存Vertex Mask而非位?这是由于Nanite在Build时启用了约束机制(宏NANITE_USE_CONSTRAINED_CLUSTERS),该机制保证了Cluster中的三角形顶点索引与当前最大值之差必然小于(宏CONSTRAINED_CLUSTER_CACHE_SIZE),因此,生成的Triangle Batch第一个索引与当前最大值之差将不小于,并且每个Batch最多有个唯一顶点,顶点索引差的最大值为,仅需2个位数据即可。约束机制确保使用更少数据和计算。
将所有Lane所标记三个顶点的Vertex Mask进行位合并,得到当前Wave所有顶点位掩码。通过FindNthSetBit函数找出当前Lane对应的Mask索引,加上顶点基索引得到当前Lane对应的Cluster Local顶点索引。
接下来获取当前Lane对应的快应用saas源码三角形的Wave Local的三个顶点索引,用于后续通过Wave指令访问其他Lane中已经计算完成的顶点属性。通过MaskedBitCount函数根据Vertex Mask以及前面局部顶点索引通过前缀求和得到当前Lane对应的Vertex Wave Local Index。
最后统计Vertex Mask所有位,返回总计有效的顶点数量。
注意FindNthSetBit函数,实现Lane与顶点局部索引(减去顶点基索引)的映射,返回当前Lane对应的Vertex Mask中被设置为1的位索引。如果某位为0,则返回下一个位为1的索引。如果Mask中全部位都设置为1,则实际返回为Lane索引。通过二分法逐渐缩小寻找索引范围,不断更新所在位置,最后返回找到的位置索引。
最后,出于验证目的进行了Vertex Reuse Batch的性能测试。在材质包含WPO、PDO或Mask时关闭Vertex Reuse Batch功能,与开启功能做对比。测试场景为由每颗万个三角形的树木组成的森林,使用Nsight Graphics进行Profiling,得到GPU统计数据如下:
启用Vertex Reuse Batch后,软光栅总计耗时减少了1.毫秒。SM Warp总占用率有一定提升。SM内部工作量分布更加均匀,SM Launch的总Warp数量提升了一倍。长短板Stall略有增加,但由于完全消除了由于LDS同步导致的Barrier Stall,总体性能还是有很大幅度的提升。
至此,Nanite可编程光栅化源码解析讲解完毕。回顾整个解析过程,可以发现UE5团队并未使用什么高深的黑科技,而是依靠引擎开发者强悍的工程实现能力完成的,尤其是在充分利用GPU SIMT/SIMD机制榨干机能的同时,保证了功能与极限性能的实现。这种能力和精神,都很值得我们学习。
什么是PSOS
pSOS系统结构
pSOS是一个由标准软组件组成的,可剪裁的实时操作系统。其系统结构如图2.1所示
,它分为内核层、系统服务层、用户层。
1. 内核层
pSOS内核负责任务的管理与调度、任务间通信、内存管理、实时时钟管理、中断服
务;可以动态生成或删除任务、内存区、消息队列、信号灯等系统对象;实现了基于优
先级的、选择可抢占的任务调度算法,并提供了可选的时间片轮转调度。pSOS Kernel还
提供了任务建间通信机制及同步、互斥手段,如消息、信号灯、事件、异步信号等。
pSOS操作系统在Kernel层中将与具体硬件有关的操作放在一个模块中,对系统服务层
以上屏蔽了具体的硬件特性,从而使得pSOS很方便地从支持Intel x系列转到支持MC
XXX系列,并且在系统服务层上对不同应用系统不同用户提供标准的软组件如PNA+、
PHILE+等。
2. 系统服务层
pSOS系统服务层包括PNA+、PRPC+、PHILE+等组件。PNA+实现了完整的基于流的TCP
/IP协议集,并具有良好的实时性能,网络组件内中断屏蔽时间不大于内核模块中断屏蔽时
间。PRPC+提供了远程调用库,支持用户建立一个分布式应用系统。PHILE+提供了文件系
统管理和对块存储设备的管理。PREPC+提供了标准的C、C++库,支持用户使用C、C++语言
编写应用程序。
由于pSOS内核屏蔽了具体的硬件特性,因此,pSOS系统服务层的软组件是标准的、与
硬件无关的。这意味着pSOS各种版本,无论是对X系列还是MCXXX系列,其系统服务
层各组件是标准的、同一的,这减少了软件维护工作,增强了软件可移植性。
每个软组件都包含一系列的系统调用。对用户而言,这些系统调用就象一个个可重入
的C函数,然而它们却是用户进入pSOS内核的唯一手段。
3. 用户层
用户指的是用户编写的应用程序,它们是以任务的形式出现的。任务通过发系统调
用而进入pSOS内核,并为pSOS内核所管理和调度。
pSOS为用户还提供了一个集成式的开发环境(IDE)。pSOS_IDE可驻留于UNIX或DOS
环境下,它包括C和C++优化编译器、CPU和pSOS模拟仿真和DEBUG功能。
pSOS内核机制
§3.1 几个基本概念
3.1.1 任务
在实时操作系统中,任务是参与资源竞争(如CPU、Memory、I/O devices等)
的基本单位。pSOS为每个任务构造了一个虚拟的、隔离的环境,从而在概念上,一个任务
与另一个任务之间可以相互并行、独立地执行。任务与任务之间的切换、任务之间的通
信都是通过发系统调用(在有些情况下是通过ISR)进入pSOS Kernel,由pSOS Kernel完
成的。
pSOS系统中任务包括系统任务和用户任务两类。关于用户任务的划分并没有一个固
定的法则,但很明显,划分太多将导致任务间的切换过于频繁,系统开销太大,划分太少又
会导致实时性和并行性下降,从而影响系统的效率。一般说来,功能模块A与功能模块B是
分开为两个任务还是合为一个任务可以从是否具有时间相关性、优先性、逻辑特性和功
能耦合等几个方面考虑。
3.1.2 优先级
每个任务都有一个优先级。pSOS系统支持0~级优先级,0级最低,级最高。0级
专为IDLE任务所有,~级为系统所用。在运行时,任务(包括系统任务)的优先级
可以通过t_setpri系统调用改变。
3.1.3 任务状态
pSOS下任务具有三种可能状态并处于这三个状态之一。只有通过任务本身或其他任
务、ISR对pSOS内核所作的系统调用才能改变任务状态。从宏观角度看,一个多任务应用
通过一系列到pSOS的系统调用迫使pSOS内核改变受影响任务而从运行一个任务到运行另
一任务向前发展的。
对于pSOS kernel,任务在创建前或被删除后是不存在的。被创建的任务在能够运行
前必须被启动。一旦启动后,一个任务通常处于下面三个状态之一:
①Executing (Ready)就绪
②Running运行
③Blocked阻塞
就绪任务是未被阻塞可运行的,只等待高优先级任务释放CPU的任务。由于一个任务
只能由正运行的任务通过调用来被启动,而且任何时刻只能有一个正在运行的任务,所
以新任务总是从就绪态开始。
运行态任务是正在使用CPU的就绪任务, 系统只能有一个running任务。一般runni
ng任务是所有就绪任务中优先级最高的,但也有例外。
任务是由自身特定活动而变为阻塞的,通常是系统调用引起调用任务进入等待状态
的。所以任务不可能从ready态到blocked态,因为只有运行任务才能执行系统调用。
3.1.4 任务控制块
任务控制块TCB是pSOS内核建立并维护的一个系统数据结构,它包含了pSOS Kernel调
度与管理任务所需的一切信息,如任务名、优先级、剩余时间片数、当前寄存器状态等。
在有的RTOS中,任务的状态与任务TCB所处的队列是等同的。pSOS操作系统将二者分
为两个概念,例如任务处于阻塞状态,但它的TCB却处于消息等待队列、信号灯等待队列、
内存等待队列、超时队列之一。
pSOS启动时,将根据Configuration Table中的参数kc_ntask建立一个包含kc_ntask
个TCB块的TCB池,它表示最大并行任务数。在创建一个任务时,分配一个TCB给该任务,在
撤销一个任务时,该TCB将被收回。
3.1.5 对象、对象名及ID号
pSOS Kernel是一个面向对象的操作系统内核,pSOS系统中对象包括任务、memory
regions、memory partitions、消息队列和信号灯。
对象名由用户定义(4位ASCII字符),并且在该对象创建时作为系统调用obj_CREAT
E
的一个人口参数传给pSOS Kernel。pSOS Kernel反过来赋予该对象一个唯一的位ID号
。除obj_CREATE和obj_IDENT外,所有涉及对象的系统调用都要用到对象ID号。
创建对象的任务通过obj_CREATE就已经知道了该对象的ID号,其余任务可通过obj_
IDENT或通过全局变量(如果已经为该任务的ID号建立了一个全局变量的话)获取该对象
的ID号。对象ID号隐含了该对象控制块(如TCB、QCB)的位置信息,这一位置信息被pSO
S
Kernel用于对该对象的管理和操作,如挂起/解挂一个任务、删除一个消息队列等。
3.1.6 任务模式字Mode word.
每个任务带有一个mode word,用来改变调度决策或执行环境。主要有以下四个参
数
Preemption Enabled/Disabled.
Roundrobin Enabled/Disabled
Interupts Enabled/Disabled.
ASR Enabled/Disabled: 每个任务有一个通过as-catoh建立起来的异步信号服务例
程ASR。异步信号类似于软件中断。当ASR位为1时as-catch所指向的任务将会被改变执行
路径,先执行ASR,再返回原执行点。
§3.2 任务调度
3.2.1 影响动态调度效果的两个因素
pSOS采用优先级+时间片的调度方式。有两个因素将影响动态调度的效果:一是优先
级可变(通过t_setpri系统调用改变任务的优先级);二是任务模式字中的preemption
bit位和roundrobin bit位。preemption bit位决定不同优先级的任务是否可抢占,并和
roundrobin bit位一起决定任务的时间片轮转是否有效。
3.2.2 引起任务调度的原因及结果
pSOS系统中引起调度的原因有两条:
1. 在轮转方式下时间片到
2. pSOS系统调用引发任务调度。该系统调用可能是ISR发出的,也可能是某个任务发出的
pSOS任务调度的结果有两种:
1. 引起运行任务切换,这指的是
2. 不引起运行任务切换,这指的是
不论任务调度是否引发运行任务切换,都有可能引起一个或多个任务状态变迁。
3.2.3 运行任务的切换
一、何时切换
下面三种情况将引发运行任务切换:
1. 在时间片轮转方式下(此时任务模式字的roundrobin bit与preemption bit均为
enable),运行任务Task A的时间片用完,且Ready队列中有相同优先级的其它任务,则
Task A退出运行。
2. 在运行任务Task A的Mode word的preemption bit位为enable的前提下,若Task A发出
的某条相同调用引发一个优先级高于Task A的任务Task B从Block状态进入Reary状态,则
将Task B投入运行。
3. ISR使用I_RETURN系统调用,则ISR退出运行,pSOS Kernel选择Ready队列中优先级最高
的任务投入运行(这一任务并不一定是被ISR打断的前运行任务)。
二、如何切换
上述三类运行任务的切换,其具体的pSOS Kernel运作过程并非完全一样,但彼此之间
差别不大。为了简单起见,我们以
为例对切换过程作一简单叙述。这一过程可细分为4个步骤:
1. 任务A运行信息保存(_t_save proc far)
这一过程主要完成修改系统工作标志,保存切换点地址及运行信息、任务A栈调
整
栈
指针保存、栈切换、参数及返址入栈等一系列工作。
2.任务A入就绪队列(void t_in_chain)
这一过程将任务A的TCB块按优先级顺序插入就绪队列。
3.选择一个高优先级任务B(void t_choice( ))
按一定算法从就绪队列中选出最高优先级任务B的TCB块,并使运行指针指向它。
4.将任务B投入运行(_t_run proc far)
从系统栈切换到任务B栈,用任务B的TCB块中保存的信息恢复上次运行被打断的
地
,恢
复任务运行环境,于是任务B开始继续运行。
图3.1反映了典型任务切换过程中CPU控制权的转移、各堆栈活动生命期、任务活动
生命期等信息。图中
t1,t4为切换点 t2,t3为开/关中断
Tsch=t4-t1 // Tsch为任务切换时间
Tforbid=t3-t2 // Tforbid为中断禁止时间
它们是实时操作系统最重要的两个性能指标。
超详细 | 灰狼优化算法原理及其实现(Matlab)
在解决复杂问题的探索中,元启发式算法犹如一把锐利的工具,其中包括了进化算法的遗传算法(GA)和差分进化(DE),以及自然与人类行为的模拟,如GSA和CFO,IA和MEA,以及群体智能领域的PSO、ACO等。其中,灰狼优化算法(GWO),由Mirjalili在年提出,凭借其独特的狼群行为模拟,以其快速收敛和高精度的特点,在工程应用中独树一帜。 深入剖析GWO:GWO以狼群的狩猎行为为灵感,通过迭代更新位置来追寻最优解。它借助C随机权重,强化了全局搜索的广度,其流程包括种群初始化、最优解的设定和位置更新。在著名的CEC测试函数F上,GWO展示了卓越的性能。然而,对于多模态问题,GWO的表现有待优化,这需要我们对参数和位置更新机制进行改良。 想要更深入地了解GWO的实现细节,以及获取MATLAB源码,你可以通过关注KAU的云实验台微信公众号,回复"GWO"即可获取相关资源。这里不仅有详实的代码示例,还有针对GWO优化策略的深入探讨和改进思路。 在解决优化问题的道路上,我们将持续分享最新的研究进展和实用技巧,期待与你共同探索优化世界的奥秘。敬请期待后续的精彩内容更新。用matlab实现粒子群优化算法的可视化模拟,跪求源代码!!!!
给你一个地址,是Mathworks公司网站上的,全球Matlab使用者将自己的代码在这里分享,这是粒子群算法PSO工具箱地址
/matlabcentral/fileexchange/-particle-swarm-optimization-toolbox
看看使用说明,用一下demo就会了,在界面的右下方有平面粒子显示
在这里你还可以搜到很多源代码,希望对你有帮助
UE4. PSO Caching
UE4.引入的PSO Caching系统是一个全面的渲染状态缓存解决方案,相比之前的FShaderCache,PSO Caching不仅缓存了Shader代码和二进制ByteCode,还额外缓存了渲染状态信息。这使得缓存集涵盖了渲染所需的全部关键信息,如顶点声明、Primitive类型、RenderTarget像素格式等,向Vulkan的Pipeline Cache理念致敬。
PSO Caching通过存储Shader路径的SHA1 Hash作为唯一索引,而非直接保存Shader代码或路径,确保了高效且可靠的管理。Shader的真正内容由FShaderCodeLibrary负责。缓存过程涉及稳定ShaderKey的生成和PSO数据的收集与保存。
启用ShaderStableKeys是PSO Caching的第一步,这在执行Cook时生成稳定的ShaderKey。通过在ProjectName/Config/DefaultEngine.ini(或平台相关配置文件)中添加特定值来开启此功能。执行打包后,会生成稳定ShaderKey的目录,以及对应项目和引擎的文件。
运行时捕获PSO数据通过添加-logPSO参数或修改配置文件来实现。官方文档建议通过ProjectLauncher启动游戏,添加-logPSO参数并使用ADB连接手机来收集数据。对于无法连接的情况,可修改AndroidEngine.ini打包安卓版本至手机进行数据收集。
收集的PSO数据以*.scl.upipelinecache文件形式保存在手机的存储路径下。为了进一步处理这些数据,生成*.stablepc.csv文件,这一步骤结合了UE4和UE5的文档指导,通过创建文件夹和CMD脚本整合相关文件进行操作。
生成*.stable.upipelinecache文件是关键步骤。这涉及到将*.stablepc.csv文件放入本地Build/Android/PipelineCaches目录下,并进行打包。通过执行特定命令,引擎在Cook时使用stavlepc.csv创建PipelineCache,从而实现PSO缓存的生成。通过研究源码和编译日志,我们得出流程包括创建shk文件和执行相关代码片段,最终生成特定目录下的缓存文件。
验证PSO缓存成功启用后,打包的应用在启动时会显示日志信息,确认了缓存已激活。通过这种方式,UE4.实现了高效、全面的渲染状态缓存管理,显著优化了渲染性能。