挑战 Z80 的极限:在 ZX Spectrum 上实现实时 3D 渲染

ZX Spectrum 48K 是 20 世纪 80 年代家用计算的主力机型,但它从未被设计用于 3D 图形。凭借 Z80 CPU 和仅有的 48KB RAM,硬件限制极其巨大。然而,开发者 Thanassis (ttsiodras) 最近的一个项目证明了,通过巧妙的数学捷径和激进的汇编优化,实时 3D 点云渲染不仅是可能的,而且流畅得令人惊讶。

这个项目是“无用折腾”的典范——这种工程实践旨在将硬件推向绝对极限,以探索其可能性。通过将一个 3D 点云渲染器从 ATmega328P 移植到 Z80,作者展示了在复古硅片上,高级 C 代码与手写汇编之间的巨大差异。

性能差距:C 语言 vs. 汇编

该项目最引人注目的发现之一是,与手写汇编相比,Z80 C 编译器的效率极低。当使用 C 语言实现 3D 投影循环时,渲染器的帧率约为 6.2 FPS。通过将核心逻辑改写为 Z80 汇编,作者成功将其提升至 14.0 FPS

这种提速是通过以下几种底层优化实现的:

  • 寄存器管理: 作者对 Z80 寄存器集的利用效率远高于编译器。
  • 倒数查找表: 除法在 Z80 上是一项昂贵的运算。作者使用倒数查找表将昂贵的除法替换为乘法。
  • 基于页面的查找: 为了进一步加速内存访问,作者实现了“基于页面”的查找,将表偏移量的高字节加载到 H 寄存器,将索引加载到 L 寄存器,以便从 (HL) 读取数据。

对于追求更高性能的用户,作者尝试了预计算分支。通过预先计算整个路径和屏幕内存写入——本质上是将渲染器变成一个播放系统——帧率飙升至 40 FPS。这个版本预先计算了目标像素的视频 RAM 位置和像素偏移量,从而将内层循环简化为简单的内存读写。

极简投影的数学原理

为了在 3.5MHz 的处理器上实现可行的 3D 渲染,作者完全避开了复杂的旋转矩阵和浮点运算。相反,该项目使用了一种简化的投影模型,即相机围绕物体运行,而不是物体旋转。

投影方程

核心运行时逻辑依赖于三个简单的方程:

wxnew = X' - mcos
y = 96 - Z' / wxnew
x = 128 + (Y' + msin) / wxnew

在此模型中,96128 代表 Spectrum 的 256x192 屏幕中心。为了避免浮点运算,所有源数据在构建流水线中通过因子 $S = 8960$ 进行预缩放,并转换为整数。

为速度进行预处理

性能的提升不仅体现在运行时循环中,通过 points_gen.py 在构建流水线中也实现了提升:

  1. 轴向交换: 存储顺序被更改为 $[X, Z, Y]$。这使得 CPU 可以先计算深度和屏幕-Y 坐标,从而能够跳过任何落在屏幕垂直边界之外的点位的屏幕-X 计算。
  2. 定点数空间: 坐标被转换到“屏幕就绪”空间,以最大限度地减少运行时计算。
  3. 非对称缩放: 相机轨道的正弦和余弦值采用了不同的缩放比例。mcos(控制相机深度)使用适度的偏移,而 msin(控制水平摆动)的缩放比例显著更高,以确保除法后仍有意义的像素移动。

社区工程见解

该项目引发了关于 8 位开发本质的讨论。一个反复出现的主题是工具在生产力方面的作用。正如社区成员 @flohofwoe 所指出的,80 年代开发的缓慢感并不一定源于汇编语言本身,而是由于缺乏现代工具。

"使用一个好的宏汇编器,你已经比机器码更接近于像 C 这样的一种高级语言了……通过使用模拟器进行快速的 'edit-compile-debug' 循环……你几乎可以获得与在 'proper' 高级编程语言中工作时相同的生产力。"

此外,社区还分享了 Z80 优化技术建议,例如避免使用 IXIY 寄存器(它们速度较慢)以及利用 256 字节对齐的表,通过仅操作寄存器对的低字节来简化索引操作。

结论

从 C 语言的 6.2 FPS 到预计算汇编的 40 FPS,这段历程凸显了复古计算的一个基本真理:硬件是极限,但软件是杠杆。通过将计算负担从运行时转移到构建流水线,并利用 Z80 架构的特定特性,一台 1982 年的机器仍然可以用其能力让我们感到惊喜。

Sources