使用DX12手撸了一个GPU Driven Pipeline,前前后后大概花了一个月的时间,效率有点低,先总结一下为什么效率不高。

1.图形API不熟悉,很多东西虽然概念上理解了,但在实际编写的时候你会很困惑,为了实现这个功能,该怎么使用这些API。因此我需要花费精力去熟悉DX12,主要的学习途径就是MSDN以及各种示例代码,龙书可以做为入门书籍,对dx12有一个大概的了解。

2.调式困难,引擎开发最常遇到的两个问题崩溃和效果不对,崩溃往往都是和内存相关的问题。一旦崩溃很难定位,使用DX12开发功能是一个非常繁琐的过程,比如你要在shader中使用一个buffer,你首先要创建一个buffer,然后可能还需要一个上传的buffer,然后还要定义根参数,根参数要和shader的寄存器匹配,还要正确的设置根实参。其中一个步骤出错,哪怕是一个参数设置错误,都可能会导致崩溃。因此在开发的时候我采用小步快跑的原则,一口气写太多功能,一旦出错很难定位。当你开开心心的写了一堆代码,然后一运行,我靠什么都没画出来,到底问题出现在哪里?这时候如果没有Frame debug你会束手无策。有的时候我们需要判断shader中的参数是否正确传入或者计算的结果是否正确,我很希望能够debug shader,但是很遗憾我没有找到可以debug dx12 shader的工具,dx11下倒是可以调试。为了解决shader中的疑问,有的时候我会直接读取buffer中的内容做判断,甚至向buffer中写一些结果做为调试的手段。如果能够找到一个可以在dx12下调试shader的工具,我工作的效率会提高很多。

3.粗心和坑,在开发的过程中有几个问题印象深刻,我在这里列举出来避免以后再犯。

  • 为了偷懒,很多基础的代码我是从其他demo中拷贝过来的,在初始化的时候我忘记了调用OnResize()方法,导致没有创建RT和DS资源,结果程序崩溃。
  • 在设置根实参的时候用错了API接口,SetGraphicsRootShaderResourceView和SetComputeRootSignature分别对应渲染管线和计算管线。
  • cs中传递贴图不能使用根描述符需要使用根描述表,我尝试了很多次都是这样的结果,不知道是我使用的问题,还是潜规则。

  • C++代码中的结构体会做内存对齐,shader中的结构体虽然和C++中声明的结构一致,但是实际分配的内存大小可能并不一致,这个一定要很小心。

  • 并行开发经验不足,在写cs的时候遇到一些问题,比如不要在同一次cs调用中,多次修改一个buffer中的值,然后后面的代码又要读取这个值,串行开发是没有问题的,读取的肯定是最后一次设置的值,但是并行开发,我们无法保证读取的是最后一次设置的值,同理我们也无法在同一次cs中又读又写同一个buffer。

ok,言归正传,让我们开启GPU Driven Pipeline之旅,第一站为什么要使用GPU Driven Pipeline?

cpu和gpu发展的路线是不一致的,cpu的计算核心比较少,控制核心比较强大,它更适合做复杂的逻辑运算,虽然也有多核多线程,但是和gpu的并行计算相比还是弱了很多。gpu中的每一个处理器都是按照SIMD32执行指令的,也就是32个线程同时执行同一个指令,CPU端的SIMD通常是4个向量同时执行同一指令。当今游戏cpu端逐渐成为性能瓶颈,我们希望GPU能够缓解CPU端的压力,而且如果逻辑符合并行计算的要求,放到GPU端做也是非常适合的。

在渲染端,CPU主要的性能消耗在场景管理及图形API的调用。

如果场景中物件非常多,由其是使用基础模块拼接的方式搭建场景,那么相机剔除将会消耗大量的CPU性能。另外如果为了执行遮挡剔除在CPU端执行软光栅化,也会消耗大量的CPU性能。而场景管理算法(四/八叉树)本身其实消耗的性能并不多。

游戏最常见的优化手段就是合并批次,合并批次的目的是为了减少Drawcall的调用,Drawcall为什么会消耗CPU性能呢,因为每一次Drawcall的背后是一系列的操作,比如设置顶点和索引缓冲区,设置贴图和常量缓存区,设置shader,设置Blend等等,说白了就是图形API的调用,也可以说是渲染状态的切换。每一个API调用,CPU端都会做很多工作,比如顶点缓存区的设置,cpu端需要监测顶点格式和输入流是否匹配,创建一个临时的buffer为VS做准备,这些细节与图形API和显卡驱动的设计息息相关,对于上层开发的我们要牢记每一个API调用都会消耗cpu时间。理想情况下如果能够一个Drawcall渲染整个场景那么这部分的CPU消耗将会降到最低。

基于以上的分析,我们重新划分cpu和gpu端的工作。

  1. cpu端使用四/八叉树对场景进行粗剔除,虽然在技术上完全可以把这部分工作放到gpu上做,但是我并不推荐这么做,原因有二,第一场景管理并不是只有渲染使用,物理碰撞检测也会使用,因此渲染模块和物理模块有可能复用这部分的逻辑。第二场景管理一般都是递归的方式去遍历树,gpu并行计算不支持递归,因此我们需要把算法改成非递归版本,写一个非递归版本的八叉树也不难,但是保证并行计算的效率,这个我就没有经验了,但是想想限制应该还是挺多的。
  2. cpu端将场景物件按照PSO进行排序
  3. cpu端为每一个PSO中的所有物体合并顶点和索引缓存区,合并Instance缓存区。
  4. gpu端做相机剔除和遮挡剔除。
  5. gpu端设置Drawcall参数并调用Drawcall。

GDC中提到将物体划分成更细粒度的Cluster ,我在做GPU Driven Pipeline的时候一直在思考为什么要划分成Cluster,目前来说还没有找到一个必须要使用Cluster 的理由。使用Cluster 可能更多的是为了更细粒度的做裁剪和遮挡剔除,用cs的消耗换渲染管线的性能,cs和渲染管线配合多线程从而提高性能。当然因为目前我只是做了一个简单的技术测试,可能当真正做一个复杂场景的时候就会发现新的理由去使用Cluster。目前为止我没有使用Cluster。

让我们先把焦点放到如何减少Drawcall,通常有两种技术合并批次,但无论使用哪种技术都无法绕过切换shader,切换Blend等一些渲染状态的切换。当然你可以在shader中使用动态控制写一个超级大的shader支持所有效果,但是真的会这么做么?目前我们只能尽量减少 shader的数量,另外PBR渲染会大大减少shader的数量,因此很适合与GPU Driven Pipeline配合使用。

第一种是将许多mesh合并成一个大的mesh,这样做的缺点是浪费内存并且降低被相机裁剪的概率。

第二种是使用instance技术。

DX12的Drawcall API只有两个,一个是DrawInstanced,另一个是DrawIndexedInstanced,都是支持instance技术,一个不使用索引缓存,一个使用索引缓存。

使用索引的好处是减少VB的数量,因为共面的点可以用索引进行表示。

我们先创建一个UAV存储Instace Data,也就是每一个物体特有的属性比如坐标变换矩阵,AABB包围盒等。为什么是UAV不是SRV,因为我们需要在CS中对其进行剔除,然后在VS中读取这个Buffer,所以需要创建UAV。

如果我们使用相同的顶点格式,我们可以使用一个SRV存储所有物体的顶点信息,然后再创建一个SRV存储所有物体的索引信息。这样我们就可以不用再调用SetVB和SetIB函数。试想一下如果有n个不同种类的模型我们就需要调用n次SetVB和SetIB。GDC文章中推荐这么做,但是这里有一个疑问,只要合并了VB和IB,相同的PSO其实只需要设置一次即可,而且如果自己管理VB和IB还需要设置根实参,SetVB和SetIB比SetView浪费很多性能么?还是有其他的理由需要我们自己去管理VB和IB?

如果我们自己管理VB和IB,那么我们就只能使用DrawInstanced这个方法进行渲染了,而且我们需要告诉VS当前的顶点在索引buffer和顶点buffer中的位置,我们利用SV_VertexID和SV_InstanceID进行查找,而且我们还需要自己管理VertexStart,IndexStart,InstanceIdOffset。代码如下:

VertexOut VS(VertexIn vin)
{VertexOut vout;uint indexdOfIndex = IndexStart + vin.VertexId;uint indexOfVertex = indexDataBuffer[indexdOfIndex].VertexIndex;VertexData verData = vertexDataBuffer[indexOfVertex + VertexStart];float4 posW = mul(float4(verData.LocalPositon, 1.0f), instanceDataBuffer[vin.InstanceId + InstanceIdOffset].world);vout.PosH = mul(posW, gViewProj);vout.Color = verData.VertexColor;return vout;
}

ok,我们现在抛弃了VB和IB并且使用instance技术减少Drawcall。假设同一个PSO下有10种不同的模型,那么我们就需要10次Drawcall,这10次Drawcall都不需要切换渲染状态,因此性能还是很高的。

我们的思路是使用CS进行剔除,然后将剔除的结果传递给渲染管线执行渲染,我们绝对不可以将CS的剔除结果回传给CPU然后再调用渲染API,这个思路是错误的。因此我们需要使用间接绘制技术,在DX12中就是ExecuteIndirect接口。

我们需要把设置根实参和Darwcall的函数参数写入到一个command buffer中,然后调用ExecuteIndirect执行,这样整个过程都是在GPU端进行,没有回传到cpu端。

我们现在有3个Buffer分别存储VB,IB以及instance data buffer,现在我们还需要一个buffer用来存储间接绘制的command。另外我们还需要一个flag buffer来标志哪个物体被剔除了,还需要另外一个instance data buffer来保存实际被渲染的物体。

我们的思路是通过cs来剔除物体,如果物体没有通过测试,那么flag buffer中对应的flag就是0,通过测试的就是1,然后对flag buffer进行前缀和操作,这样就可以得到一个只包含通过测试的instance buffer了。

相机剔除的方法是在cs中直接转换到ndc空间,然后进行剔除。在cpu端我们不会这么做,因为这样做的乘法次数太多,但是在GPU中是可以的,第一并行计算,这对GPU来说小菜一碟,第二遮挡剔除也需要将其转换到ndc空间。

遮挡剔除使用hi-z算法,首先将遮挡物的深度渲染到一张纹理中,然后对这个纹理进行降采样,取深度值最大的值做下层mipmap的值。这里我们需要操作子资源,设置每一层的mipmap。

GPU Driven Pipeline的思路并不复杂,复杂的是自己用DX12写一个GPU Driven Pipeline,其实也就是对DX12熟悉的过程,当然使用DX11,Vulkan,Opengl也可以实现,只是针对不同的API进行微调。我没有很详细的介绍DX12的使用方法,如果你真的对这部分技术感兴趣,还是自己动手写一个理解的深刻。

总结一下疑问:

  1. 为什么GDC中要使用Cluster
  2. 为什么要自己管理VB和IB

后续,这里我一直没有提贴图,因为贴图也会影响批次的合并,为了配合GPU Driven Pipeline需要使用虚拟贴图技术,这个是我下一步要实现的功能。

DX12之手撸GPU Driven Pipeline相关推荐

  1. 【手撸RPC框架】SpringBoot+Netty4实现RPC框架

    [手撸RPC框架]SpringBoot+Netty4实现RPC框架 线程模型1:传统阻塞 I/O 服务模型 模型特点: 采用阻塞IO模式获取输入的数据 每个链接都需要独立的线程完成数据的输入,业务处理 ...

  2. 手撸一款简单高效的线程池(五)

    在之前的内容中,我们给大家介绍了 C++实现线程池过程中的一些常用线优化方案,并分析了不同机制使用时的利弊.这一篇,是线程池系列的最后一章.我们会介绍一下 CGraph 中的 threadpool 如 ...

  3. 在用安全框架前,我想先让你手撸一个登陆认证

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 转自:RudeCrab, 链接:blog.csdn.net ...

  4. 清华大一Python作业太难上热榜!只上3节课,手撸AI算法,网友:离本科毕设只差一篇万字论文...

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 金磊 发自 凹非寺 量子位 报道 | 公众号 QbitAI 太难了! ...

  5. 如何手撸一个较为完整的RPC框架

    [文章作者/来源]一个没有追求的技术人/https://sourl.cn/sJ4Brp 缘 起 最近在公司分享了手撸RPC,因此做一个总结. 概 念 篇 RPC 是什么? RPC 称远程过程调用(Re ...

  6. 10分钟手撸极简版ORM框架!

    最近很多小伙伴对ORM框架的实现很感兴趣,不少读者在冰河的微信上问:冰河,你知道ORM框架是如何实现的吗?比如像MyBatis和Hibernte这种ORM框架,它们是如何实现的呢? 为了能够让小伙伴们 ...

  7. mysql 原生 添加数据_手撸Mysql原生语句--增删改查

    mysql数据库的增删改查有以下的几种的情况, 1.DDL语句 数据库定义语言: 数据库.表.视图.索引.存储过程,例如CREATE DROP ALTER SHOW 2.DML语句 数据库操纵语言: ...

  8. .Net Core手撸一个基于Token的权限认证

    说明 权限认证是确定用户身份的过程.可确定用户是否有访问资源的权力 今天给大家分享一下类似JWT这种基于token的鉴权机制 基于token的鉴权机制,它不需要在服务端去保留用户的认证信息或者会话信息 ...

  9. .NET手撸绘制TypeScript类图——下篇

    .NET手撸绘制TypeScript类图--下篇 在上篇的文章中,我们介绍了如何使用 .NET解析 TypeScript,这篇将介绍如何使用代码将类图渲染出来. 类型定义渲染 不出意外,我们继续使用  ...

  10. .NET手撸绘制TypeScript类图——上篇

    .NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化, TypeScript越来越流行,前端的设计也越来复杂,而 类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方 ...

最新文章

  1. 我们错了 - One of us is wrong
  2. android开发环境搭建(for 驱动开发人员)
  3. 【opencv】8.获取鼠标动作(滑轮滚动,左键按下,右键按下,鼠标移动)并进行相应处理
  4. 数据库:SQLServer数据库备份方式介绍
  5. 前端学习(2949):创建webpack搭建项目
  6. CSharp for Jupyter Notebook
  7. mysql 错误代码:1293
  8. 计算机课程设计设计方案怎么写,(学生)计算机绘图课程设计方案.doc
  9. FPGA 实现SVPWM调制
  10. 2023南京工业大学计算机考研信息汇总
  11. Python编程基础
  12. 微信小程序前期申请企业认证、后期提审发布流程
  13. 以字符串为例,谈谈Python到底要学到什么程度
  14. A股全自动化交易——从零到实盘20(完结)
  15. 双千兆网口路由器方案开发板香橙派R1 Plus LTS连接USB无线网卡测试说明(OpenWRT 系统)
  16. 常见城市城市名称中英文json
  17. JVM调优-配置参数
  18. 《Dreamweaver CS6 完全自学教程》笔记 第十二章:框架的应用
  19. 对于神经网络的边缘计算以及嵌入式等应用
  20. 思博伦仪表模拟DHCP动态地址获取

热门文章

  1. c#图片转ico自制小工具
  2. [渝粤教育] 长安大学 液压传动 参考 资料
  3. 趋势linux版本杀毒软件,万万没想到,微软 Linux 版杀软来了
  4. android 微信浮窗实现_Android仿微信文章悬浮窗效果的实现代码
  5. 台式计算机提示内存不足怎么办,一招解决电脑提示内存不足-电脑内存不足怎么办...
  6. LaTeX 页眉设置
  7. CAD突然没有对话框了?只能命令行输入内容??(FILEDIA=0?CMDECHO=0?)
  8. 破解极验验证码之模拟登录B站
  9. 使用脚注添加网页类参考文献(使用word)
  10. pulseaudio,gmediarender