首页›行业资讯›在 FPGA 上重现 Voodoo 显卡与 1990 年代末游戏 PC
← 返回行业资讯
社区精选

在 FPGA 上重现 Voodoo 显卡与 1990 年代末游戏 PC

Hacker News ·
项目分享在 FPGA 上重现 Voodoo 显卡及 90 年代末游戏 PC。
本文为原文全文的中文翻译(约 5511 字),原文配图已同步至站内;机器翻译仅供快速阅读,细节请以原文为准。

在 FPGA 上重现 Voodoo Graphics 与 1990 年代末游戏 PC

过去一个月我一直在为 z486_MiSTer 添加功能并提升性能,主要测试的是 1990 年代前半期的游戏。把目光投向几年之后,引出了我想探索的另一个变化:3D 显卡的到来。

第一个给我留下深刻印象的是 Voodoo。当时的游戏是《极品飞车 II SE》。平滑的纹理、雾效,以及整体的速度感,让人感觉像是新一代的 PC 游戏体验。既然现在有了 z486 CPU,我能否在 FPGA 上重现它?

这次偏题探索的成果是 zSST,一个用 SystemVerilog 实现的 3dfx Voodoo Graphics(即 SST-1)。结合我的 z486 CPU 和周边 PC 硬件,它构成了 z486 XL:一台带 Voodoo 显卡的 DOS PC,运行在 Xilinx KV260 板卡的可编程逻辑中。《古墓丽影》现在可以用其原始的 3dfx 渲染器运行了。

zSST 实现了 Voodoo 的大部分核心特性:预处理三角形、纹理过滤与 mipmapping、深度测试与 alpha 测试、雾效、混合、抖动、帧缓冲访问以及缓冲区交换。它同时支持定点与浮点两种 setup 接口。硬件游戏测试仍集中在《古墓丽影》上;更广泛的兼容性和后续 Voodoo 世代是日后的工作。

CPU 和渲染器在 KV260 上以 100 MHz 运行。该板卡拥有足够的逻辑资源、DSP 模块、片上存储器和 DDR 带宽来承载这一组合设计。DE10-Nano 没有空间容纳这一图形扩展。KV260 使用其板载 DDR;无需额外添加外部 SDRAM 模块。

从编程模型出发

幸运的是,有大量材料可供参考。3dfx 于 1999 年发布了 Glide 源代码,早于 NVIDIA 在 2000 年 12 月收购其核心图形资产。现存的 Glide 源代码和 SST-1 规范解释了软件如何准备三角形、配置像素管线以及管理纹理和帧缓冲。

该规范是行为层面的目标,而非电路图。它说明了软件写入寄存器时应该发生什么,但留下了许多实现选择空间。86Box 为复杂的渲染行为提供了有用的参考。早期 MAME 的 Voodoo 工作也是这一保存历史的一部分。SpinalVoodoo 提供了特别有用的 Glide 跟踪记录和参考截图用于测试。

从三角形到 3D,每时钟一个像素

Voodoo Graphics 将三角形转换为像素,将大量 3D 工作留给主机 CPU。其命令接口出奇地紧凑:五个主命令寄存器即可驱动加速器。

两个三角形命令都会启动相同的渲染管线。其他寄存器保存坐标、梯度与渲染状态,而内存映射区域则提供纹理上传与直接帧缓冲访问。主要的绘制图元只是一个准备好的三角形。

对游戏开发者而言,Glide 提供了更友好的接口:

void grDrawTriangle(const GrVertex *a, const GrVertex *b, const GrVertex *c); 在此调用之前,主机 CPU 会完成 3D 几何变换、顶点光照计算、裁剪并投影到屏幕。随后 Glide 准备屏幕空间三角形及其参数梯度——用于在三角形表面插值的增量——并写入三角形命令以启动渲染。与后来诸如 GeForce 256 等 GPU 不同,SST-1 没有硬件变换与光照引擎。

这仍为加速器留下了大量工作。光栅化器找出哪些像素中心位于三角形内部,并对其颜色、深度与纹理坐标进行插值。纹理单元获取并过滤纹素;帧缓冲单元组合颜色、应用可见性测试与雾效、与现有图像混合,并写入结果。

原始显卡将这项工作分配给两颗 ASIC:FBI(Frame Buffer Interface,帧缓冲接口)与 TREX(纹理映射单元,通常称为 TMU)。在 50 MHz 图形时钟下,标称峰值为每时钟一个带纹理、带深度测试的输出像素:每秒 5000 万像素。

每时钟一个像素并不意味着一个像素在一个时钟内完成。它意味着不同阶段可以同时处理不同的像素:当一个像素正在进行纹理化时,较早的一个像素可以进行混合,另一个可以写出。一旦流水线填满,理想情况下它可以在每个时钟接收并完成一个像素,前提是内存能够跟上。

这正是固定功能流水线的吸引力所在。软件渲染器为每个像素执行大量指令;专用硬件则在稳定的像素流中重叠这些工作。Voodoo 以流畅的 30 FPS 或更高帧率让纹理丰富的 3D 游戏栩栩如生——这是它如此受欢迎的重要原因。

构建像素流水线

与 x86 CPU 相比,算术路径非常规整。让我们跟随一个像素,从它的插值参数经过纹理化和颜色运算一直到帧缓冲,首先从数字的表示方式开始。

浮点接口背后的定点运算

浮点运算在现代 GPU 编程中处于核心地位。SST-1 处于一个有趣的过渡点:软件可以提交浮点值,但渲染机制主要采用定点运算——即带有隐含比例因子的整数。

这里的 12.4 表示二进制小数点前有十二位(包括符号位),以及四位小数位。因此屏幕坐标 10.5 存储为整数 168:乘以 16 进行编码,除以 16 恢复原值。这些小数位让光栅化器能够处理像素中心之间的顶点。

fvertex、fstart 和浮点梯度寄存器接受 IEEE 单精度值。SST-1 将其转换为内部定点表示,zSST 遵循这一约定。一旦三角形准备就绪,沿扫描线推进主要意味着对每个插值参数加上一个预计算的增量。大部分逐像素工作变成了简单的整数加法。

一个像素对应四个纹素

对于透视校正纹理映射,TMU 对 S/W、T/W 和 1/W 进行插值,然后将前两者除以第三者以恢复纹理坐标。这使得地板或墙壁纹理在表面远去时保持透视效果。TMU 还会选择 mip 层级:为覆盖表面较大面积的像素选用更小版本的纹理。这减少了远处的混叠和闪烁。

随后双线性过滤会围绕采样位置合并四个相邻纹素——即纹理的像素。首先水平混合顶部一对,然后混合底部一对,最后在这两个结果之间进行垂直混合。小数位置决定权重,从而在纹素颜色之间产生平滑过渡,而非从一个纹素到下一个纹素的突变。

在 zSST 中,一个四级前端对透视和细节层次计算进行流水线处理。地址生成和缓存查找提供纹素,两个寄存解码级将其存储格式转换为颜色,用于过滤和纹理组合。基于调色板的纹理和 NCC 编码的纹理需要不同的解码规则,但最终馈入相同的像素流。

颜色、测试、雾和混合

一旦纹理和帧缓冲数据可用,zSST 的 FBI 像素路径使用六个寄存级:

这些级边界的选择是为了满足 FPGA 的时钟目标。SST-1 规范描述了操作,但未揭示原始 ASIC 的确切流水线寄存器。例如,将雾查找与雾应用分离,可将长算术路径排除在单个时钟之外,同时保留每时钟接受一个像素的能力。

结果写入后缓冲。随后进行与回扫同步的缓冲交换,在不于扫描输出中途切换缓冲的情况下显示完成图像。原始 Voodoo 是仅 3D 的附加卡,在不活动时将普通 VGA 卡的输出直通。z486 XL 在 FPGA 系统内部在 PC 的 VGA 输出和 zSST 的显示输出之间做出类似选择。

难点:从内存为其提供数据

事实证明,zSST 像素流水线的实现相对直接,至少与 z486 的 CPU 流水线相比是如此。但要让流水线持续获得数据供给,则困难得多。一次双线性采样需要从不同地址读取四个纹素。一个经过深度测试和混合的像素还需要读取已有的深度和颜色值,然后写回新的值。逐次执行这些访问会迅速破坏吞吐量。最终,我在内存系统的设计、调优和调试上花费的时间比算术流水线还多。

原版显卡如何提供像素

分工在这块 Diamond Monster 3D 上清晰可见。上方那颗 3dfx 芯片是 TMU,下方那颗是 FBI,各自右侧配有四颗 EDO RAM 芯片。上方一组存放纹理;下方一组存放颜色和深度/alpha 缓冲。

FBI 和 TMU 各自拥有专用的 64 位内存通路。在纹理侧,四路交织让各存储体可以读取独立地址,并行提供双线性滤波所需的四个相邻纹素。规格书(第 13 页)承诺与点采样相同的吞吐量,且无需存储重复纹素。

但如果两个相邻纹素落在同一颗芯片中呢?诀窍在于以重复的二维模式分布纹素,而不是将图像分割成四个大区域。为偶/奇列与偶/奇行的每种组合分配一个存储体,原因就变得显而易见了:

每个2×2窗口都包含A、B、C和D——即使是跨越水平和垂直块边界的橙色窗口也是如此。两个连续列具有相反的奇偶性,两个连续行也是如此。所有四种组合恰好各出现一次,因此每个bank提供一个texel而不会产生冲突。

纹理边缘和较小的mip层级需要稍多注意。SST-1使用2的幂次纹理尺寸,因此对于二维或以上的尺寸,环绕会保持交替模式。在钳制边缘处,或在仅一个texel宽或高的mip层级中,某些采样会复用同一个texel。核心洞见依然成立:快速双线性过滤依赖于内存排列方式,使运算单元同时接收到所有输入。

FBI对颜色和深度/alpha内存应用了类似思路。其交错路径支持每时钟一个渲染像素的峰值,清除操作时每时钟两个像素。同时处理相邻像素将读/写开销分摊到一条扫描线上。Fabien Sanglard的双像素解释对此行为提供了有用的重建,尽管确切的ASIC bank调度在编程指南中并未记载。

在50 MHz下,每条64位路径的理论带宽为400 MB/s:总计800 MB/s,但分别用于不同的任务。TMU不能借用空闲的FBI带宽,反之亦然。这些专用总线和精心安排的存储体让我想起我在SNESTang等项目中探索过的NES和SNES时代的设计:要最大化内存利用率,就需要围绕每个值何时何地被需要来精心设计。

FPGA SoC上有哪些变化

Voodoo的内存布局解释了它如何让流水线保持忙碌,但我不能简单地将该设计移植到KV260上。该板卡拥有更大的内存带宽,但两个渲染单元都没有连接专用的EDO内存。相反,FPGA通过Zynq处理系统的AXI端口访问共享DDR。Linux、FPGA PC和显示扫描输出都在竞争这块内存。目标是一样的——让像素流水线持续获得数据——但实现方式必须改变。

我们的KV260测量结果表明了为什么仅靠带宽是不够的。一个100 MHz的128位端口理论带宽为1.6 GB/s。在只有一个未完成请求的情况下,4 KiB读取达到1,370 MiB/s,但64字节读取仅达到189 MiB/s。首个数据通常需要约280 ns到达——在100 MHz下大约28个时钟周期——偶尔会有更长的等待。

如果渲染器在发出下一次小读取之前等待每次读取完成,它将大部分时间处于空闲状态。zSST需要足够的独立工作在途来覆盖这些等待时间。

缓存、重放与重排序缓冲区

要让流水线持续运转,既需要减少DDR访问次数,也需要减少等待DDR访问的时间。第一步是缓存。相邻的屏幕像素通常会采样纹理中相互重叠的部分,因此最近获取的纹素可以从片上RAM中复用。zSST的纹理缓存容量为8 KiB,采用64字节的行;每次取数还会一并载入相邻纹素,以供后续像素很可能使用。

缓存未命中仍然需要许多时钟周期,但独立的纹理采样无需等待它。zSST最多保持八个缓存行取数未完成,使用重放队列将缺少数据的采样暂存,并在数据到达时重试。与此同时,纹素已在缓存中的采样可以继续推进。预取则能提前启动未来的读取。

这样一来,较晚的缓存命中可能先于较早的未命中完成。一个64项的重排序缓冲区(ROB)会收集这些结果,并按原始顺序释放它们。这一原理在CPU中很常见:在漫长等待期间做有用的工作,然后在将结果传递到下游之前恢复顺序。

帧缓冲部分使用独立的 4 KiB 颜色缓存和深度/alpha 读取缓存,而写合并器将相邻的 16 位更新打包为 128 位请求。此处顺序至关重要:混合或深度测试可能需要某个先前像素已修改但尚未写入 DDR 的值。前递(forwarding)直接提供待处理的值。帧缓冲更新按顺序生效,需要已完成工作的状态变更会等待其排空。内存请求可以重叠,但后续像素仍必须看到先前像素的效果。

FBI 和 TMU 共享渲染器的 128 位 AXI 端口 HP2。PC 使用 HP0,显示扫描输出使用 HP3,三者的请求队列彼此独立,尽管最终都共享 DDR。

评估结果

我将渲染器与完整 PC 分开测量。仿真基准通过 zSST 前端发送命令,并测试 TMU、FBI、共享仲裁器以及 DDR 时序模型。读取数据在至少 26 个时钟周期后到达,具有确定性变化和偶发的更长延迟;写入也受到速率限制。完整渲染器测试允许 32 个未完成读取。

在100 MHz下,zSST处理带纹理三角形时达到每秒7850万像素(MPix/s),开启深度测试与混合时则为72.8 MPix/s。Voodoo 1在其原生50 MHz下的公开估算值,在可比功能集下为43和37 MPix/s。这并非对等基准测试:三角形工作负载不同,且原始估算值还包含雾效、mipmapping和Gouraud着色。我没有Voodoo 1来在两者上运行相同测试。该对比展示的是近似填充率范围,而非相对原卡的实测加速比。

高填充率并不会自动转化为高游戏FPS。在板上,Tomb Raider第二关在大约20秒内产生了237次显示的缓冲区交换——约每秒12次,这是从交换次数而非引擎FPS计数器测得的。初步测量指向CPU瓶颈:它仍需运行游戏、准备几何数据并提交命令。共享DDR争用也可能有所贡献。整机中仍有大量可优化空间。

阅读原文 →
来源:Hacker News

本页摘要由 AI 辅助整理,经编辑审核后发布;AI 仅用于提炼与分类,原文均附来源链接。