这次抽象,我几乎全盘否定了之前的抽象。
本来,RHI的抽象已经基本完成了,可以开心的写基础的光照阴影这些功能了。
但是,在QQ群里无意间看到大佬们聊起来bindless, 然后去查了查资料,发现bindless性能又好,抽象又好做,于是果断入bindless的坑。
在bindless抽象中,会把所有用到的材质参数,纹理全部放加载到显存,然后使用一个ID索引来引用相关的资源。
随着重构的进行,我发现之前的设计思路有很多问题,这都是因为我对GPU资源管理的不熟悉所致。
当我们向GPU上传纹理时或做更新资源时,由于GPU离CPU较远,所以Vulkan总是倾向于我们调用批量接口。
对!就像Nvidia说的那样,Batch! Batch! Batch!,但不仅仅局限于DrawCall, 转换内存布局,上传资源都需要Batch。
那么,此前做的所有抽象基本完全无用了。
在之前的抽象中,我都是基于同步的思路来做的,比如我new一个图片就需要等待他上传成功才能返回,不然结构体中的一些数据没有办法进行初始化。
经过仔细思考后,我认为此前提到的方案1其实是更好的选择。
数据冗余的问题也不难解决,只要我们把图形API层需要用到的数据下沉到图形API层的内部代码中,然后在RHI层的结构中做一个代理函数,通过gpu_handle来获取相关属性并返回即可。在实际应用中,由于width,height,format等属性属于低频访问,无论怎么封装,其实都不能成为性能瓶颈。
这样,新RHI层的数据结构只需要持有相应gpu_handle即可,这个结构有释放资源的责任。
比较微妙的是,由于bindless的存在,我们shader中的参数,如纹理,材质参数也都是通过id来索引的。
这样我们便直接把图形API层的资源管理透明掉了,RHI的数据结构可以直接和shader进行匹配,而不用关心图形API层是如何管理资源的。
透明掉图形API层之后,我们便拥有了更大的实现自由,比如在new rhi::texture2d时,图形API层只需要把上传到GPU时所需要的数据保存下来,然后分配一个gpu_handle, 函数就可以立即返回了。
在业务逻辑调用DrawCall之前,把需要用到的资源上传到GPU即可。
在实践中,由于DrawCall也是通过写入CommandBuffer中最终提交才异步执行的,因此我们只需要保证在CommandBuffer中,上传资源相关的GPU命令先于相关的DrawCall命令之前执行就可以了。
在Batch的指导思想下,其实使用gpu_handle的封装方式,能获得更好的Cache Friendly。因为所有图形API层的数据结构总是在渲染时,被批量访问。
而且,由于gpu_handle并不是指针,如果有需要还可以实现一些池的优化技术,如内存整理等(随着分配释放的进行,池中可能会有很多空位长时间用不到,这时出于内存和Cache Friendly的考虑,需要整理一个池中的元素的分布)。
最后,这次重构代码在此。
ps. 由于一些原因,可能未来很长一段时间内,这个引擎的Thread都需要换出了,先在这里保存一下上下文,以便后面可以继续换入 😀