FPGA系统架构设计
| 对比维度 | 状态机流水线(固定功能) | 指令式设计(可编程) |
|---|---|---|
| 架构思路 | 每个渲染步骤做成状态机/流水线模块,数据按流程走 | 定义指令集,硬件执行指令驱动渲染 |
| 灵活性 | 低(功能固定,难扩展) | 高(算法可通过程序/微码修改) |
| 性能 | 高(专用硬件加速,时钟周期可预测) | 中(需要调度,性能受指令开销影响) |
| 资源利用 | 紧凑(LUT/DSP/BRAM 利用高效) | 占用较大(需指令解码器、寄存器文件、控制器) |
| 开发难度 | 中(需要模块化设计,但不涉及 ISA) | 高(需要设计 ISA、指令调度、编译/汇编工具链) |
考虑到要在ZYNQ7020实现,资源有限,小bram+DDR3指令式速度不敢想象,同时其实也不需要太复杂的渲染功能,所以我选择了状态机流水线的设计思路。
内存存取设计
在内存设计方面存在不少难点,根本的原因在于片上的bram比较小,而把数据存在DDR中,每次读取又需要十几个周期的延迟,会造成渲染帧率的降低。具体而言,渲染器需要存取以下几类数据:
Z-buffer
对于图形的消隐,考虑到效率现代最常用的算法是Zbuffer算法,也就是开辟一个屏幕空间大小的buffer,深度浅的覆盖深度深的,最终完成深度测试。对于非常紧张的bram,很难开一个全局的缓冲区,为此需要把画面分成一个一个的tile,如64x64,对于每一个tile单独进行渲染。
纹理
在纹理映射步骤中,对于纹理的访问是一个随机读取,而随机读取对于DDR又是非常不友好的,所以最好的方式是依次顺序读取,把小三角形(实际操作可能是矩形)纹理读进bram。对于每一个tile而言,也不能把所有纹理全部加载进来,而只能把正在渲染的三角形面加载进来。在渲染过程中,再配合流水线加载,可以把ddr造成的延迟消除掉。
顶点数据
纯计算方案
每次算出的所有顶点数据全部 顺序交给下一步tile的纹理映射器,会有大量浪费,因为每个tile只会用到一部分顶点数据,其他的顶点数据白算了。
纯不计算方案
顶点数据算出来就分配给不同的tile。
顶点数据的访问也是一个随机读取,很搞笑的是,我认为数据量最少的顶点数据居然也放不进bram,也只能设计ddr存取流程。
在这里有一个很有意思的权衡,顶点数据如果是顺序存储的,那么可以顺序读取,但是渲染器需要按照三角形的顶点索引来访问顶点数据,这样就会变成随机读取。如果把顶点数据按照三角形的顶点索引顺序存储,那么就可以顺序读取了,但是这样会造成顶点数据的冗余存储。
同时,怎么把三角形分配到各个tile上也是一个问题,可以把三角形的包围盒和tile进行相交测试,分配到相交的tile上,但是怎么存?可以为每个tile维护一个桶,把相交的三角形放进去,这样在渲染tile时就是顺序读取了,但是在写的时候又是随机写入。你可能说很简单,每个tile维护一个小的bram桶就行了,但是bram桶开多大呢?如果开得太小,如果所有面都到一个tile,可能会溢出;如果满就写一次,满就写一次,又成了随机读取。如果开得太大,又浪费bram资源。统一缓冲区,再分配,倒是一个选择…
也可以把三角形顺序存储,然后每个tile维护一个索引列表,存储相交的三角形的索引,这样在渲染tile时就是随机读取了。
不管怎么样,总有一个随机读取。
折中方案
也许可以浪费一点计算资源,来做一些重复计算,同时减少内存访问?比如说,把顶点分成几个大片(注意是按照乘以MVP矩阵之前的大片,是为了可预测顺序),把每个大片放到ddr中,渲染时再从bram中读取大片数据,ddr顺序读大片。
我愿称之为双tile方案