Z80の限界に挑む:ZX Spectrumでのリアルタイム3Dレンダリング
1980年代のホームコンピューティングの定番であったZX Spectrum 48Kは、3Dグラフィックス向けに設計されたものではありませんでした。Z80 CPUとわずか48KBのRAMという、極めて厳しいハードウェア制約があります。しかし、開発者のThanassis (ttsiodras) による最近のプロジェクトは、巧妙な数学的ショートカットと積極的なアセンブリ最適化を用いることで、リアルタイムの3Dポイントレンダリングが単に可能であるだけでなく、驚くほど滑らかに動作することを示しています。
このプロジェクトは、「無益な試行錯誤(useless tinkering)」のマスタークラスと言えます。これは、何が可能であるかを確認するために、ハードウェアを絶対的な限界まで押し上げるエンジニアリングの類です。ATmega328PからZ80へ3Dポイントレンダラーを移植することで、著者は、ハイレベルなC言語コードと、ヴィンテージ・シリコン上での手書きアセンブリの間の決定的な違いを浮き彫りにしています。
パフォーマンス・ギャップ:C言語 vs. アセンブリ
このプロジェクトの最も驚くべき発見の一つは、手書きのアセンブリと比較した際のZ80 Cコンパイラの非効率性です。3D投影ループをC言語で実装した場合、レンダラーは約6.2 FPSを達成しました。コア・ロジックをZ80アセンブリで書き直すことで、著者は14.0 FPSへと跳ね上がらせることに成功しました。
このスピードアップは、いくつかの低レベルな最適化によって達成されました:
- レジスタ管理: 著者は、コンパイラが実行できるよりもはるかに効率的にZ80のレジスタセットを活用しました。
- 逆数ルックアップテーブル: Z80において、除算はコストの高い操作です。著者は、高価な除算を、逆数のルックアップテーブルを用いた乗算に置き換えました。
- ページベースのルックアップ: メモリ・アクセスをさらに加速させるため、著者は「ページベース」のルックアップを実装しました。テーブル・オフセットの上位バイトを
Hレジスタに、インデックスをLレジスタにロードすることで、(HL)から読み取ります。
さらなるパフォーマンスを求める人々のために、著者は事前計算された分岐(precomputed branch)を試みました。パス全体と画面メモリへの書き込みを事前計算することで(実質的にレンダラーを再生システムに変えることで)、フレームレートは40 FPSまで急上昇しました。このバージョンでは、ターゲットとなるピクセルのビデオRAM上の位置とピクセル・オフセットを事前計算し、内側のループを単純なメモリ読み書きのみに簡略化しています。
ミニマリスト投影の数学
3.5MHzのプロセッサで3Dレンダリングを実用的なものにするため、著者は複雑な回転行列や浮動小数点演算を完全に回避しました。その代わりに、このプロジェクトでは、オブジェクトが回転するのではなく、カメラがオブジェクトの周囲を回るという簡略化された投影モデルを使用しています。
投影方程式
コアとなる実行時ロジックは、3つの単純な方程式に基づいています:
wxnew = X' - mcos
y = 96 - Z' / wxnew
x = 128 + (Y' + msin) / wxnew
このモデルでは、96と128はSpectrumの256x192画面の中心を表します。浮動小数点演算を避けるため、すべてのソース・データはビルド・パイプラインにおいて、係数 $S = 8960$ で事前スケールされ、整数に変換されます。
高速化のための前処理
パフォーマンスは実行時ループだけでなく、points_gen.pyを介したビルド・パイプラインでも向上させられます:
- 軸の入れ替え: ストレージ順序を$[X, Z, Y]$に変更します。これにより、CPUが深度と画面上のY座標を最初に計算できるようになり、画面の垂直方向の範囲外に外れるポイントについては、画面上のX座標の計算をスキップすることが可能になります。
- 固定小数点空間: 実行時の計算を最小限にするため、座標を「画面準備完了」な空間へと変換します。
- 非対称スケーリング: カメラの軌道のためのサイン・コサイン値は、異なるスケールで調整されます。
mcos(カメラの深度を制御)は控えめなシフトを使用しますが、msin(水平方向の振れを制御)は、除算後の意味のあるピクセル移動を確保するために、大幅に高いスケールで調整されます。
コミュニティからのエンジニアリング・インサイト
このプロジェクトは、8ビット開発の性質に関する議論を巻き起こしました。繰り返し現れたテーマは、生産性におけるツールの役割です。コミュニティ・メンバーの@flohofwoeが指摘したように、80年代の開発は、必ずしもアセンブリ言語そのものが原因で遅かったのではなく、むしろ現代的なツールの欠のけが原因でした。
"Good macro assembler を用いることで、マシンコードに比べれば、すでにC言語のようなハイレベルな言語にかなり近い状態になります... エミュレータを用いた迅速な 'edit-compile-debug' ループがあれば... ほぼ同等の生産性を、'proper' なハイレベル・プログラミング言語で作業するのと同様に得ることができます。"
さらに、Z80最適化のためのテクニカル・チップスとして、速度の遅いIXやIYレジスタの使用を避け、256バイト整列されたテーブルを利用することで、レジスタ・ペアの低位バイトのみを操作することでインデックス作成を簡式化するという手法が共有されました。
結論
C言語での6.2 FPSから、事前計算されたアセンブリによる40 FPSへの道のりは、レトロ・コンピューティングの根本的な真実を浮き彫りにしています。ハードウェアが限界であり、ソフトウェアがレバー(梃子)であるということです。計算の負担を実行時ではなくビルド・パイプラインへと移し、Z80アーキテクチャの特有の癖を最大限に活用することで、1982年製のマシンでも、その能力で私たちを驚かせ続けてくれます。