DX12描述符管理

最近一直在研究图形学方面的知识,学得头疼,恰好现在的Demo需要一个描述符管理系统,因此转向DX12的学习,换换脑子。

对于资源描述符,描述符堆,描述符表,根参数,根描述符等这些基本概念,我就不介绍了,网上一抓一大把。我先先谈谈为什么DX12要引入描述符的概念,先看一下msdn的说明,以下加粗字体,来源于msdn。

Directx12 绑定背后的主要设计决策之一是将其与其他管理任务分离。 这对应用提出了一些要求,以管理某些潜在的危险。

D3D12 绑定模型的主要优点是,它使应用能够经常更改纹理绑定,而不会产生巨大的 CPU 性能成本。 其他优点是,着色器可以访问大量资源,着色器不需要预先知道将绑定多少资源,并且无论硬件或应用的内容流如何,都可以使用统一的资源绑定模型。

为了提高性能,绑定模型不需要系统跟踪应用请求 GPU 使用的绑定,并且绑定和多线程命令列表之间存在干净的集成。

说实话这一段话说的云里雾里的,我用我自己的理解来尝试解释一下上面这段话。首先如何理解“D3D12 绑定模型的主要优点是,它使应用能够经常更改纹理绑定,而不会产生巨大的 CPU 性能成本”这句话?

DX11是没有资源描述符以及根参数这些概念的,DX12启用这个技术是一个全新的绑定模型(如何将资源绑定到shader中)。这么对比吧,DX12的绑定模型就好比C的结构体,里面只有数据没有逻辑,而DX11的绑定模型是C++的class,即有数据又有处理函数。

为什么DX11的绑定模型会浪费更多的cpu时间呢?我个人觉得DX12的多线程,新的资源绑定模型以及内存管理,这三个是一体式设计,因为这三个地方的重构可以大大减少cpu及gpu的异步通信时间。因为这三个模块的解耦,让底层的操作系统以及gpu驱动程序的设计更加简单。正如msdn中说:

内存驻留管理与绑定分离

DX11会检测shader绑定的资源是否驻留在显存中,以免发生非法访问,而现在这个工作需要交给上层来保证,因此DX12的描述符仅仅只有数据的指针以及数据的描述,并不需要更多的底层代码进行跟踪。

对象生命周期管理与绑定分离

在释放任何资源(如纹理)之前,应用程序必须确保 GPU 已完成对该资源的引用。 这意味着在应用程序可以安全释放资源之前,GPU 必须完成该引用该资源的命令列表的执行。

驱动程序资源状态跟踪与绑定分离

系统不再检查资源绑定以了解何时发生了资源转换。 对于许多 GPU 和驱动程序来说,一个常见的例子是必须知道shader从RTV过渡到SRV。应用程序本身现在必须确定系统可能关心的任何资源转换。

CPU GPU 映射内存同步与绑定分离

系统不再检查资源绑定以了解present是否需要延迟,应用程序现在有责任同步 CPU 和 GPU 内存访问。 为了解决这个问题,系统为应用程序提供了相关机制,以请求 CPU 线程休眠,直到工作完成。 也可以进行轮询,但可能会降低效率。

以上四点简单概括就是DX12设计的更轻量级,DX12为我们上层应用提供了更大的灵活性,可以根据不同的硬件进行特殊处理,以换取高性能。当然它全新的解耦设计,从设计上就为我们提供了高效的异步通信。不过话又说回来,如果DX12用的不好,可能性能还不如DX11。

目前我对DX12的理解仅停留在大体上是这么回事,如何高效的使用DX12还需要真刀真枪的实战。接下来我会从功能性,正确性以及高效性上对DX12描述符管理进行需求整理。

1.对于上层应用来说描述符管理系统类似内存池,上层应用可以通过描述符管理系统申请1-n个描述符,而无需担心描述符堆是怎么分配描述符的。上层应用需要保存描述符的句柄,等到不使用的时候手动进行free,类似c++的new和delete操作。

2.描述符的管理需要支持多线程,因为DX12编程环境很多情况下是在多线程下进行的。

3. 因为描述符堆的切换是很费时的操作,我们需要一个ring buffer来存储每一帧需要的描述符我们暂且称这个ring buffer为gpu descriptor heap。另外我们创建的资源比如贴图资源,我们也需要为它分配一个描述符,这个描述符的生命周期和资源是一致的,对于资源分配的描述符我们暂且叫他cpu descriptor heap。每一次在渲染之前我们会通过CopyDescriptorsSimple将cpu descriptor拷贝到gpu descriptor heap。因为gpu descriptor有些描述符是每帧共享的,有些是每帧更新的,因此gpu descriptor heap将会被分成两部分,分别是gpu static descriptor heap以及gpu dynamic descriptor heap。

这里我直接参考了一个开源引擎的代码,接下来我会分析该引擎的实现:

首先我们需要一个描述符分配器,这个分配器支持变长描述符的申请以及回收整理。

我们定义两个有序map,一个存储空闲块的偏移量,一个存储空闲块的尺寸,这两个map相互引用,这样可以高效的进行插入,删除和合并操作,因为使用的是有序map,因此时间复杂度是log(n),代码如下:

因为空闲的Block的size可能相同,因此使用multimap存储Block,而offset是唯一的所以使用map存储。我们暂且称这两个map为offset map和size map。

Allocation

申请一块空间很简单:

1.我们根据size map找到第一个能够分配的Block。

2.删除原来的Block,申请一块新的Block,新Block的size等于剩余的size。

如下图我们申请一块20bytes的内存空间:

代码如下:

Deallocation

释放稍微复杂一点,因为我们需要合并空闲的Block。我们可以根据释放的offset找到next block以及pre block,销毁有四种情况:

1.释放的Block前后没有相邻的空闲Block,因此不需要合并

2.前一个Block相邻,合并两者

3.后一个Block相邻,合并后者

4.前后都相邻,合并三者

如下图我们释放了size 8的Block在56的位置:

描述符分配策略

首先我们需要了解的是N卡中有很多显卡推荐每一个Frame最好只设置一次描述符堆,因此我们需要一个ring buffer来存储当前帧需要的描述符。根据需求我将描述符堆分成三种:

1.cpu 描述符堆,这些描述符不是shader visible。也就是说这些描述符只是cpu中的缓存,它用来存储资源描述符,比如由create*view创建出来的描述符。

2.gpu 静态描述符堆,这些描述符只设置一次,不会改变,比如shader中的全局变量,它是shader visible会被传入gpu中。

3.gpu 动态描述符,这些描述符是shader渲染需要的描述符,它是shader visible会被传入gpu中。

在每帧渲染之前,会调用ID3D12Device::CopyDescriptors or ID3D12Device::CopyDescriptorsSimple将cpu堆中的描述符拷贝到gpu堆中。

关于cpu中的描述符堆,我们可以使用上面介绍的变长分配器存储描述符。申请和释放不需要验证gpu是否还在使用资源,因为这些描述符根本没有传入gpu。我们可以根据资源的生命周期,来管理描述符的生命周期。

关于gpu中的静态描述符堆,我们设置好后,就会长驻留显存,因此只申请不释放。

关于gpu中的动态描述符堆,这个比较复杂。首先这个描述符堆,不能只有一份,如果只有一份的话,cpu就会需要gpu的确认才能继续添加描述符。比如第n帧填充了描述符堆,gpu去执行渲染,我们需要确定gpu渲染完成,第n+1帧才能覆盖描述符堆,这就造成了cpu的等待。

我们可以申请一个大的描述符堆,将其分成n份,第一份存储静态描述符,n-1份为动态描述符区域。比如第n帧我们使用第一份,然后第n+1帧使用第二份,依次类推。我们预计第n帧的时候,第一帧已经完成渲染,这样就可以形成工作流,避免了阻塞。当然极端情况下如果还未完成我们只能进行等待。

这个概念其实就是龙书中的Frame resource。这个方案的缺点是每一份中会有很多重复的描述符,但是这是空间换时间的代价。另外我们每一帧是不是都要重新刷新描述符堆,这也是个问题。因为描述符堆是根据shader改变的,如果我们实现为每一个shader对应描述符堆中的一个位置。

如果shader中的资源没有改变,我们就不需要重新copy这些描述符了,这是后期的优化,不过这个要和材质系统一起设计,这里只是提一下。

描述符堆系统只负责单纯的申请和释放描述符(但它保证线程安全),至于分配策略应该放在上层逻辑去处理。

完。

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值