FPGA上でVoodooグラフィックスと1990年代後半のゲーミングPCを再現する
Recreating Voodoo Graphics and a Late-1990s Gaming PC on an FPGA (nand2mario.github.io)
要約
筆者はFPGA上で3dfx Voodooグラフィックスチップ(SST-1)をSystemVerilogで実装し、z486 CPUと組み合わせることで、1990年代後半のDOSゲーミングPC環境「z486 XL」を構築しました。これにより、FPGA上で「Tomb Raider」などのVoodoo対応ゲームをオリジナルのレンダラーで動作させることが可能になりました。このプロジェクトは、当時のPCゲーミング体験を現代のハードウェアで再現し、その技術的側面を探求するものです。
全文翻訳
FPGAでVoodooグラフィックスと1990年代後半のゲーミングPCを再現する
この1ヶ月間、私はz486_MiSTerに機能を追加し、パフォーマンスを向上させることに費やしてきました。主に1990年代前半のゲームを扱っていましたが、数年先を見据えて、3Dグラフィックスカードの登場という別の変化を探求したいと思いました。
私に強い印象を残した最初のカードはVoodooでした。プレイしていたゲームは『Need for Speed II SE』でした。滑らかなテクスチャ、フォグ、そして全体のスピード感は、PCゲーミングの新世代を感じさせました。z486 CPUが存在する今、FPGAでそれを再現できるだろうか?
この寄り道の成果が、3dfx Voodoo Graphics、すなわちSST-1のSystemVerilog実装であるzSSTです。私のz486 CPUと周辺のPCハードウェアと組み合わせることで、これはz486 XLを形成します。これは、Xilinx KV260ボードのプログラマブルロジック上で動作するVoodooグラフィックスを備えたDOS PCです。『Tomb Raider』は、オリジナルの3dfxレンダラーで動作するようになりました。
zSSTは、Voodooの主要な機能のほとんどを実装しています。準備された三角形、テクスチャフィルタリングとミップマッピング、深度とアルファテスト、フォグ、ブレンディング、ディザリング、フレームバッファアクセス、バッファスワップです。固定小数点と浮動小数点両方のセットアップインターフェイスをサポートしています。ハードウェアでのゲームテストはまだ『Tomb Raider』に集中しています。より広範な互換性や後の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へ、クロックあたり1ピクセル
Voodoo Graphicsは三角形をピクセルに変換しますが、3Dの作業の多くはホストCPUに任せます。そのコマンドインターフェイスは驚くほどコンパクトです。5つの主要なコマンドレジスタがアクセラレータを駆動します。
| レジスタ | アクション |
|---|---|
triangleCMD | 準備された三角形のレンダリングを開始する。 |
ftriangleCMD | 浮動小数点セットアップインターフェイスを通じて三角形を開始する。 |
nopCMD | パイプラインをフラッシュする。オプションで統計カウンタをリセットする。 |
fastfillCMD | 色および/または深度データのクリップされた矩形をクリアする。 |
swapbufferCMD | 表示バッファを切り替える。即時または垂直リトレースに同期して行う。 |
どちらの三角形コマンドも同じレンダリングパイプラインを起動します。他のレジスタには座標、勾配、レンダリング状態が格納され、メモリマップド領域はテクスチャのアップロードと直接のフレームバッファアクセスを提供します。主要な描画プリミティブは、単に準備された三角形です。
ゲーム開発者にとって、Glideはよりフレンドリーなインターフェイスを提供します。
void grDrawTriangle(const GrVertex *a, const GrVertex *b, const GrVertex *c);
この呼び出しの前に、ホストCPUは3Dジオメトリを変換し、頂点ライティングを計算し、クリッピングし、画面に投影します。その後、Glideは画面空間の三角形とそのパラメータ勾配(表面全体で値を補間するために使用される増分)を準備し、レンダリングを開始するために三角形コマンドを書き込みます。GeForce 256のような後続のGPUとは異なり、SST-1にはハードウェア変換・ライティングエンジンはありません。
それでも、アクセラレータには多くの作業が残されています。ラスタライザは、三角形の内側にあるピクセル中心を見つけ、それらの色、深度、テクスチャ座標を補間します。テクスチャユニットはテクセルを取得・フィルタリングし、フレームバッファユニットは色を組み合わせ、可視性テストとフォグを適用し、既存の画像とブレンドして結果を書き込みます。
[画像: 三角形ラスタライザが、複数の異なるピクセルが同時に進行中のパイプラインに供給されている様子]
1つのピクセルが完了するまでにいくつかのステージを要しますが、新しいピクセルが継続的に流入できます。
オリジナルのカードは、この作業を2つのASICに分割しています。FBI(Frame Buffer Interface)と、通常TMU(テクスチャマッピングユニット)と呼ばれるTREXです。50 MHzのグラフィックスクロックで、公称ピーク性能は1クロックあたり1つのテクスチャ処理済み、深度テスト済みの出力ピクセルです。これは毎秒5000万ピクセルに相当します。
クロックあたり1ピクセルというのは、1ピクセルが1クロックで完了することを意味するのではありません。異なるステージが同時に異なるピクセルを処理できるということです。1つのピクセルがテクスチャ処理されている間に、前のピクセルはブレンディングされ、別のピクセルが出力される可能性があります。パイプラインがいっぱいになると、メモリが追いつく限り、理想的にはクロックあたり1ピクセルを受け入れて完了できます。
これが固定機能パイプラインの魅力です。ソフトウェアレンダラーはピクセルごとに多くの命令を実行しますが、専用ハードウェアはそれをピクセルの安定したストリーム全体でオーバーラップさせます。Voodooは、リッチなテクスチャ処理された3Dゲームを、流れるような30 FPS以上で実現しました。これがその人気の大きな理由の一つです。
ピクセルパイプラインの構築
x86 CPUと比較すると、算術パスは心地よく規則的です。ピクセルが補間されたパラメータからテクスチャ処理、色操作を経てフレームバッファに至るまでを追ってみましょう。まず、数値の表現方法から始めます。
浮動小数点インターフェイスの背後にある固定小数点
浮動小数点演算は、現代のGPUプログラミングの中心です。SST-1は興味深い移行期に位置しています。ソフトウェアは浮動小数点値を送信できますが、レンダリング機構の大部分は固定小数点(暗黙のスケールファクターを持つ整数)で動作します。
| セットアップ値 | 固定小数点レジスタ形式 |
|---|---|
| スクリーンXおよびY | 12.4 |
| 赤、緑、青、アルファ | 12.12 |
| 深度Z | 20.12 |
| テクスチャS/WおよびT/W | 14.18 |
| Wの逆数 | 2.30 |
ここで 12.4 は、小数点(2進数)の前に12ビット(符号を含む)、小数点以下に4ビットがあることを意味します。したがって、スクリーン座標10.5は整数168として格納されます。16を掛けてエンコードし、16で割って値を復元します。これらの小数ビットにより、ラスタライザはピクセル中心間の頂点を処理できます。
fvertex、fstart、および浮動小数点勾配レジスタは、IEEE単精度値を受け取ります。SST-1はそれらを内部の固定小数点表現に変換し、zSSTもその契約に従います。三角形が準備されると、スキャンラインに沿って進むことは、主に事前に計算された増分を各補間パラメータに加算することを意味します。ピクセルごとの作業の多くは、単純な整数加算になります。
1ピクセルあたり4テクセル
パースペクティブ補正されたテクスチャリングのために、TMUはS/W、T/W、および1/Wを補間し、その後、最初の2つを3番目の値で割ってテクスチャ座標を復元します。これにより、表面が遠ざかるにつれて床や壁のテクスチャがパースペクティブを保ちます。TMUはミップレベルも選択します。これは、ピクセルが表面の広い領域をカバーする場合に使用されるテクスチャの縮小版です。これにより、遠くのエイリアシングやちらつきが軽減されます。
バイリニアフィルタリングは、サンプリング位置の周りの4つの隣接するテクセル(テクスチャのピクセル)を組み合わせます。まず、上のペアを水平にブレンドし、次に下のペアをブレンドし、最後にそれら2つの結果の間で垂直にブレンドします。小数位置が重み付けを決定し、テクセル色間の急激なジャンプではなく、滑らかな遷移を生み出します。
[画像: 4つの隣接するテクセル中心と、2回の水平補間、それに続く1回の垂直補間]
4回のテクスチャ読み込みで1つのフィルタリング済みサンプルが得られます。
zSSTでは、4ステージのフロントエンドがパースペクティブと詳細度(level-of-detail)の計算をパイプライン化します。アドレス生成とキャッシュルックアップがテクセルを供給し、2つのレジスタ化されたデコードステージがそれらの格納されたフォーマットをフィルタリングおよびテクスチャ結合用の色に変換します。パレットベースおよびNCCエンコードされたテクスチャは異なるデコードルールを必要としますが、最終的には同じピクセルストリームに供給されます。
色、テスト、フォグ、ブレンディング
テクスチャとフレームバッファデータが利用可能になると、zSSTのFBIピクセルパスは6つのレジスタ化されたステージを使用します。
| ステージ | 主な作業 |
|---|---|
| F0 | ソースを選択し、クロマキーをチェックし、Z/W深度値を準備する。 |
| F1 | 色とアルファの結合関数を適用する。 |
| F2a | アルファ/深度をテストし、フォグ係数をルックアップする。 |
| F2b | フォグを適用する。 |
| F3 | 宛先色を再構築し、アルファブレンディングを実行する。 |
| F4 | フレームバッファ精度に変換し、ディザリングし、ライトマスクを適用する。 |
これらのステージ境界は、FPGAのクロックターゲットを満たすように選択されています。SST-1仕様書は操作を説明していますが、元のASICの正確なパイプラインレジスタは明らかにされていません。例えば、フォグのルックアップとフォグの適用を分離することで、長い算術パスが単一クロックに収まるのを防ぎつつ、クロックあたり1ピクセルを受け入れる能力を維持しています。
結果はバックバッファに書き込まれます。リトレース同期バッファスワップは、スキャンアウトの途中でバッファを切り替えることなく、完成した画像をディスプレイに表示します。オリジナルのVoodooは3D専用のアドオンであり、非アクティブ時には通常のVGAカードの出力をパススルーしていました。z486 XLは、FPGAシステム内でPCのVGA出力とzSSTのディスプレイ出力の間で同様の選択を行います。
難しい部分:メモリからの供給
zSSTのピクセルパイプラインは、少なくともz486のCPUパイプラインと比較すると、実装は比較的簡単であることがわかりました。それを供給し続けることは、はるかに困難であることが判明しました。バイリニアサンプルは、個別のアドレスから4つのテクセルを必要とします。深度テストされ、ブレンドされたピクセルも、既存の深度と色を必要とし、その後新しい値を書き込みます。これらのアクセスを一度に1つずつ実行すると、スループットはすぐに低下します。算術パイプラインよりもメモリシステムの設計、調整、デバッグに多くの時間を費やすことになりました。
オリジナルのカードがピクセルを供給する方法
[画像: Diamond Monster 3Dボードの写真。TMUとテクスチャRAMがFBIとフレームバッファRAMの上にある] [画像: 対応する回路図。TMUとFBIはそれぞれ4つのEDOチップに64ビットインターフェイスで接続されている。矢印はテクスチャ処理、フィルタリングされた色、PCIコマンド、RAMDACを通じたディスプレイ出力を示している]
ボードとその作業分担。両方のビューでチップの位置が一致しています。接続は模式図です。クリックすると拡大します。
写真提供: Konstantin Lanzet; 切り抜き: Pittigrilli, Wikimedia Commons。写真ライセンス: GFDL 1.2以降。回路図: nand2mario。
このDiamond Monster 3Dボードには、作業分担が視覚化されています。上部の3dfxチップがTMU、下部がFBIで、それぞれ右側に4つのEDO RAMチップがあります。上部のグループはテクスチャを保持し、下部のグループは色と深度/アルファバッファを保持します。
FBIとTMUはそれぞれ専用の64ビットメモリパスを持っています。テクスチャ側では、4ウェイインターリーブにより、バンクは独立したアドレスを読み取ることができ、バイリニアフィルタリングに必要な4つの隣接テクセルを並列に供給します。仕様書(p. 13)は、重複テクセルを格納することなく、ポイントサンプリングと同じスループットを約束しています。
しかし、2つの隣接テクセルが同じチップに着弾した場合はどうなるでしょうか?トリックは、画像を4つの大きな領域に分割するのではなく、テクセルを繰り返しパターン化された2次元パターンに分散させることです。各バンクを、偶数または奇数の列と行のすべての組み合わせに割り当てると、その理由が明らかになります。
[画像: 交互のA/B/C/Dバンクレイアウト。整列された2x2ウィンドウと境界を越えるウィンドウの両方に4つのバンクすべてが含まれており、バンクあたり1回の独立した読み取りが可能]
サンプルの移動はバンクの位置を変更しますが、その数は変更しません。座標は(列、行)です。バンクの文字は原理を示しており、物理的なSST-1チップ番号ではありません。
すべての2x2ウィンドウにはA、B、C、Dが含まれています。水平および垂直ブロック境界を越えるオレンジ色のウィンドウでさえそうです。連続する2つの列は反対のパリティを持ち、連続する2つの行も同様です。4つの組み合わせすべてが正確に1回発生するため、各バンクは競合なしに1つのテクセルを供給します。
テクスチャのエッジと小さなミップレベルは、もう少し注意が必要です。SST-1は2のべき乗のテクスチャ次元を使用するため、ラップアラウンドは2以上の次元で交互パターンを維持します。クランプされたエッジ、または幅または高さが1テクセルしかないミップレベルでは、一部のサンプルは同じテクセルを再利用します。中心的な洞察は変わりません。高速なバイリニアフィルタリングは、算術演算がすべての入力をまとめて受け取れるようにメモリを配置することに依存しています。
FBIは、色と深度/アルファメモリにも同様のアイデアを適用します。そのインターリーブパスは、レンダリングされたピクセルあたり1クロック、クリアの場合はピクセルあたり2クロックのピークをサポートします。隣接するピクセルを一緒に処理することで、スキャンライン全体で読み書きコストが分散されます。Fabien Sanglardの2ピクセル解説は、この動作の有用な再構築を提供していますが、正確なASICバンクスケジュールはプログラミングガイドには記載されていません。
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、およびディスプレイのスキャンアウトが、そのメモリを競合します。目標は同じです――ピクセルパイプラインに供給し続けること――しかし、それを達成する方法は変更する必要があります。
[画像: SST-1上の個別のFBIおよびTMU EDOパスと、KV260上の共有DDRおよび3つのAXIクライアントの比較]
オリジナルのSST-1は専用のテクスチャおよびフレームバッファバスを備えています。zSSTはレンダラーポートを共有し、バッファリングを使用してDDRレイテンシを許容します。
KV260の測定結果は、帯域幅だけでは不十分な理由を示しています。100 MHzの128ビットポートは、理論帯域幅1.6 GB/sを持っています。1つのリクエストが発行されている場合、4 KiBの読み取りは1,370 MiB/sに達しますが、64バイトの読み取りはわずか189 MiB/sしか達しません。最初のデータは通常、到着までに約280 ns(100 MHzで約28クロック)かかりますが、時折さらに長い待ち時間が発生します。
[画像: バースト長とともに測定されたDDR帯域幅は増加しますが、最初のデータまでの時間は約280ナノ秒にとどまります]
単一発行時のボード測定。長いバーストはレイテンシを償却しますが、小さなリクエストは並行処理を必要とします。
各小さな読み取りを待ってから次の読み取りを発行するレンダラーは、ほとんどの時間アイドル状態になります。zSSTは、それらの待ち時間をカバーするために、十分な独立したインフライトワークを必要とします。
キャッシュ、リプレイ、およびリオーダバッファ
パイプラインを供給し続けるには、DDRアクセスを減らし、それらを待つ時間を短縮する必要があります。最初のステップはキャッシュです。近くのスクリーンピクセルは、テクスチャの重複部分をサンプリングすることがよくあるため、最近取得されたテクセルはオンチップRAMから再利用できます。zSSTのテクスチャキャッシュは、64バイトラインで8 KiBを保持します。各フェッチは、後続のピクセルが要求する可能性が高い隣接テクセルも取り込みます。
キャッシュミスは依然として多くのクロックを要しますが、独立したテクスチャサンプルはそれを待つ必要はありません。zSSTは最大8つのキャッシュラインフェッチをインフライトで保持し、データが欠落しているサンプルをリプレイキューに配置して、データが到着したときに再試行します。その間、テクセルがすでにキャッシュされているサンプルは続行できます。プリフェッチは、将来の読み取りの開始を早めます。
これで、後続のキャッシュヒットが、前のミスよりも先に完了できます。64エントリのリオーダバッファ(ROB)は、それらの結果を収集し、元の順序でリリースします。この原則はCPUから馴染み深いものです。長い待ち時間の間に有用な作業を行い、結果を下流に渡す前に順序を復元します。
フレームバッファ側は、個別の4 KiBカラーおよび深度/アルファ読み取りキャッシュを使用し、ライトコンバイナは隣接する16ビット更新を128ビットリクエストにパックします。ここでは順序が重要です。ブレンディングまたは深度テストは、前のピクセルが変更したがまだDDRに書き込んでいない値が必要になる場合があります。フォワーディングは、保留中の値を直接供給します。フレームバッファの更新は順序通りに有効になり、完了した作業を必要とする状態変更はそのドレインを待ちます。メモリリクエストはオーバーラップできますが、後続のピクセルは依然として前のピクセルの効果を見る必要があります。
[画像: テクスチャとフレームバッファのリクエストが並列に進行し、タグで結合され、FBIピクセルステージとライトコンバイナを通じてリタイアする様子]
キャッシュは近くのデータを再利用し、キューはメモリリクエストをオーバーラップさせ、順序付けられたリタイアは結果を保持します。
FBIとTMUは、レンダラーの128ビットAXIポートであるHP2を共有します。PCはHP0を使用し、ディスプレイのスキャンアウトはHP3を使用します。これら3つは最終的にDDRを共有しますが、リクエストキューは個別に保たれます。
評価結果
レンダラーは、完全なPCとは別に測定しました。シミュレーションベンチマークは、zSSTのフロントエンドを通じてコマンドを送信し、TMU、FBI、共有アービタ、およびDDRタイミングモデルを実行します。読み取りデータは、少なくとも26クロック後に到着し、決定論的な変動と時折の長い遅延があります。書き込みもレート制限されています。フルレンダラーテストでは、32個のインフライト読み取りが許可されます。
100 MHzで、zSSTはテクスチャ処理された三角形で毎秒7850万ピクセル(MPix/s)、深度テストとブレンディングを伴う場合は毎秒7280万ピクセルに達します。Voodoo 1の公称値は、ネイティブ50 MHzで、同等の機能セットで43 MPix/sと37 MPix/sでした。これは直接比較ベンチマークではありません。三角形のワークロードが異なり、オリジナルの推定値にはフォグ、ミップマッピング、グーローシェーディングも含まれていました。同じテストを両方で実行できるVoodoo 1を持っていません。この比較は、おおよそのフィルレートの範囲を示しており、オリジナルのカードに対する測定された速度向上ではありません。
[画像: ネイティブスピードフィルレート比較:テクスチャ処理済みzSST 78.47対SST-1 43 MPix/s;テクスチャ処理済みで深度とブレンドありzSST 72.83対SST-1 37 MPix/s]
100 MHz zSSTシミュレーションと、公称50 MHz SST-1の比較。テストは類似の機能クラスをカバーしていますが、ワークロードは異なります。
高いフィルレートが自動的に高いゲームFPSに変換されるわけではありません。ボード上では、『Tomb Raider』レベル2は、約20秒間で237回の表示バッファスワップを生成しました。これは約12 FPS(エンジンFPSカウンターではなく、スワップからの測定)です。予備的な測定は、CPUボトルネックを示唆しています。CPUはまだゲームを実行し、ジオメトリを準備し、コマンドを送信する必要があります。共有DDRの競合も寄与している可能性があります。完全なマシンには、最適化すべきことがたくさん残っています。
Voodoo以外のゲームでは、現在の100 MHz z486 XLは、最高設定の『Doom』を38.5 FPS、『Quake 1.06』を8.1 FPSで実行します。これは、85 MHzのDE10-Nanoビルドと比較して約20%高速です(Doomで約23%、Quakeで19%)。UltraRAMの512 KiBライトバックL2キャッシュは、CPUがDDRをより有効に活用するのに役立ちます。
統合されたXCK26ビルドでは、zSSTは約29,500個のLUT、28,100個のフリップフロップ、14個のRAMB36ブロック、8個のRAMB18ブロック、および97個のDSPスライスを消費します。統合されたPCとグラフィックスデザインは、100 MHzでタイミングを満たします。
締めくくり
やりがいのある部分は、私がRTLで構築したハードウェアをオリジナルのGlideソフトウェアが駆動するのを見ることです。レンダリング演算が難しい部分になると予想していましたが、データ効率よく供給することがより多くの作業を必要としました。Voodooの慎重にインターリーブされたEDOとzSSTのキャッシュおよびキューは、非常に異なる制約の下で同じ問題を解決します。高速なピクセルパイプラインは、何か作業がある場合にのみ有用です。
zSSTとz486 XLの両方がオープンソースで利用可能です。すでにKV260をお持ちの場合は、z486 XL SDイメージに、独自のDOSディスクイメージを起動するために必要なLinuxサポートとアプリケーションが含まれています。
謝辞: Glideトレースと参照スクリーンショットについてはSpinalVoodooに、実装参照については86Boxに感謝します。Fabien SanglardのThe story of the 3dfx Voodoo1は、オリジナルのカードのメモリシステムに関する優れた入門書です。