
2026/03/22 22:24
FPGAで構築する3DFX Voodoo(モダンRTLツール使用)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
チームはクラシックなVoodoo 1グラフィックスアクセラレータをSpinalHDLで再実装し、設計をGitHubに公開しました。
Voodoo 1はソフトウェア変換やライティングなしで三角形を描画する固定機能GPUです。その430の設定レジスタは、タイミング(FIFO、FIFO+ストール、直接、またはフロートエイリアス)に注意して書き込む必要があります。SpinalHDLの
RegIf抽象化を拡張し、各レジスタのカテゴリ・アドレス・型・リセット値・フロートエイリアスフラグを含めることで、チームはこれらフィールド用の制御ロジックを自動生成しました。
デバッグにはconetraceというネットリスト認識トレースを使用し、ピクセルがラスター化器、テクスチャユニット、カラーコンバイン、およびフレームバッファステージを通過する様子を追跡しました。このワークフローは微妙な精度の欠陥を発見しました:
Wの早期量子化、ミップマップ境界近くでの不正確なパースペクティブテクスコーディングラウンド、ブレンド係数計算においてダイタ―減算カラーではなく拡張された宛先カラーを使用していた点です。これら小さなエラーを修正することで、かつて半透明オーバーレイやテキストに現れていたランダムな透明度アーティファクトが排除されました。
SpinalHDLの採用により散在したレジスタロジックは単一の高水準記述へと凝縮され、conetraceは手動波形検査から自動ツールへの再構築を移行させることで、1人のエンジニアがデバッグできるようにしました。これらの最新RTLツールの組み合わせは、レガシーGPU設計をより少ないバグ、明確な意図、および容易な保守で再活性化できることを示しており、レトロゲーミング愛好家、教育者、開発者全員に恩恵をもたらします。
本文
目次
- 見た目より難しい固定機能チップ
- レジスタ書き込みがすべて同じ振る舞いをしない理由
- Voodoo の四つのレジスタ挙動
- SpinalHDL でレジスタ意味論をエンコードする方法
- 波形スクロールではなく実行クエリでデバッグ
- 実際に変わった現代 RTL ツール
この Screamer 2 のフレームは、元の 3dfx カードやエミュレーターによって作られたものではなく、私が SpinalHDL で書いた Voodoo 1 の FPGA 再実装です。GitHub で公開しています。
驚きだったのは単に動くという事実だけではありませんでした。こうした設計を「一人で」記述・シミュレーション・デバッグできるようになったことです。ツールがアーキテクチャを直接表現し、適切な抽象レベルで実行を検査できると、この難解なチップも扱いやすくなるのです。
Voodoo 1 は古いですが単純ではありません。変換・ライティングハードウェアやプログラマブルシェーダがないため、グラフィック動作は全てシリコンに固定されています:Gouraud シェーディング用の勾配、テクスチャサンプリング、ミップマッピング、バイリニア・トリリニアフィルタリング、アルファクリッピング、クリッピング、深度比較、フォグなど。現代 GPU は多くの複雑さを柔軟なプログラマブルユニットに集中させますが、Voodoo では多数のハードウェア固定レンダリング動作に集約されています。
このプロジェクトを実感したバグの一つは、フレームバッファの危険性のように見えたものでした。部分的に透明なテキストやオーバーレイピクセルが不思議に透明になり、ほとんどのフレームは正常でした。本当の問題は、一つの壊れたサブシステムではなく、いくつかの小さなハードウェア精度ミスマッチが正しい順序で重なることでした。このバグはプロジェクト全体を要約する良い例になりました。重要なのは「三角形を描画できるようにする」ではなく、Voodoo の正確な挙動と一致させて誤ったピクセルが出現しないようにすることでした。
この投稿はその実現を可能にした二つの抽象化について述べます。第一は SpinalHDL で Voodoo のレジスタ意味論を表現した方法です。第二は Conetrace を使った深いグラフィックパイプラインのデバッグ手法です。
見た目より難しい固定機能チップ
一見、Voodoo は非常に控えめに見えるかもしれません。メモリマップ型アクセラレータで、一つの仕事しかありません:三角形をできるだけ速くレンダリングすることです。後続のアクセラレータとは異なり、変換とライティングは行わず、その重い 3D 計算はホスト CPU が担います。
これによりハードウェアが実際より単純に聞こえるかもしれません。たとえ一つの三角形であっても、補間された色、テクスチャサンプリング、ミップレベル選択、バイリニア・トリリニアフィルタリング、アルファクリッピング、深度比較、クリッピング、フォグなどが必要です。これらの操作はすべて現代的にプログラム可能ではなく、シリコンに組み込まれています。
ここが中心的な対比です。現代 GPU では複雑さは柔軟性から来ます。一方、オリジナル Voodoo は直接固定機能ハードウェアに多くのレンダリング動作をエンコードすることで複雑さを生み出します。
レジスタ書き込みがすべて同じ振る舞いをしない理由
レジスタインターフェースはこれを明確に示しています。Voodoo では
triangleCmd または ftriangleCmd に書き込むと三角形が起動します。他のレジスタはその三角形をどのようにラスタライズするかを記述します:使用する勾配、テクスチャサンプリング方法、実行すべきテストなど。
Voodoo は深くパイプライン化されているため、ピクセルのレンダリングは一連のステージ—勾配進展、テクスチャサンプリング、色合成、深度バッファとの比較など—を経ます。パイプライニングは作業単位をステージに分割し、複数ピクセルを同時にフライトできるようにします。これがチップのスループットをソフトウェアでは実現不可能なレベルに達する方法です。
図 1: 重要なのはどのレジスタ書き込みが即座に適用され、どれがフライト中の作業と共に動くべきか、またパイプラインが空になるまで待つべきかを決定することです。
パイプライニングはレジスタモデルに問題をもたらします。三角形 A がまだパイプライン内を移動している間に CPU が三角形 B を設定し始めると、レンダリング設定が早すぎて変更されると、三角形 A の後半ピクセルが三角形 B 用の状態を参照する可能性があります。結果は微妙な破損:一部のピクセルが誤ったテクスチャモードやブレンディングモード、深度動作で描画されることです。
回避策は二つあります。1) レジスタ書き込みをパイプラインがドレインするまで待たせる、または 2) 書き込みをフライト中の作業と同期して進め、各三角形が属する状態を見えるようにする。言い換えれば、Voodoo のレジスタ書き込みは単なる設定更新ではなく、機械のタイミング契約の一部です。
Voodoo の四つのレジスタ挙動
私のモデルでは、Voodoo レジスタを以下の四種類に分類します:
| 種類 | 振る舞い |
|---|---|
| FIFO | 順序通りキューイングして適用 |
| FIFO + Stall | キューイングはするがパイプラインが空になるまで適用しない |
| Direct | 即座に適用 |
| Float | 変換後、固定小数点レジスタへ書き込み |
重要なのはこれらのカテゴリがアーキテクチャ的であり、単なるソフトウェアインターフェースではないことです。レジスタタイプは状態が即座に変更できるか、フライト中の作業と共に動くべきか、あるいは機械が静止するまで待つべきかを示します。
図 2: これらカテゴリが存在する理由――新しい状態が古い作業に流れ込むことを防ぐためです。
この区別は HDL に直接モデル化すると自然なものになります。
SpinalHDL でレジスタ意味論をエンコード
Voodoo は多くのレジスタにわたる 430 の設定フィールドを持ち、各レジスタが上記カテゴリのいずれかに属します。Verilog のような従来 HDL では、この違いは複数箇所(レジスタ宣言、バス制御ロジック、パイプライン側処理、外部ドキュメント)に散らばります。
SpinalHDL は
RegIf という便利な抽象化を提供します。これを使うとレジスタを自然に宣言し、周辺の制御ロジックを多く自動生成できます。私はそれを拡張して、Voodoo 固有の意味論(FIFO 動作、同期書き込み、float アリース)を直接レジスタ宣言でエンコードできるようにしました。
val startR = busif .newRegAtWithCategory(0x020, "startR", RegisterCategory.fifoNoSync) .field(AFix.SQ(24 bits, 12 bits), AccessType.WO, 255 << 12, "Starting red value") .withFloatAlias() .asOutput()
この宣言は勾配歩行器で使用される開始 R 値を定義します。1 回の場所でアドレス、名前、カテゴリ、データ型、アクセスモード、リセット値、および浮動小数点エイリアスの存在を指定しています。設計内では
startR は通常の信号として登場します。
float エイリアスは特に有用です。オリジナル Voodoo には固定小数点レジスタより 128 アドレス上に浮動小数点書き込みを受け取り、変換後に固定小数点レジスタへ格納する別のレジスタがあります。この挙動はレジスタインターフェース自体の一部であるため、ロジックを分散させるよりもここで表現する方が理にかなっています。
レジスタメタデータが明示的になると、
RegIf はヘッダーや SystemRDL など他形式へマップを書き出すことも可能です。私の場合はさらにそのメタデータを使って Voodoo のレジスタ意味論をエミュレートする PciFifo コンポーネントを駆動させました。FIFO スタイルの書き込みはキューイングされ、同期書き込みはパイプラインが空になるまで停止し、float アリースは浮動小数点から固定小数点への変換後、元アドレスへ再書き込みされてキューに入ります。
ここで得られる利点はコード行数の削減だけではありません。レジスタのアーキテクチャ的意味が一箇所に集約されることで、単なるドキュメントではなく「機械がどのように振る舞うか」の実行可能な記述になります。
波形スクロールではなく実行クエリでデバッグ
設計を記述することは RTL 作業の半分です。デバッグはもう一方です。
本当に私をこのワークフローに納得させたバグは、透明オーバーレイとテキストで現れました。ほぼ全体は正しいように見えましたが、小さなピクセルクラスタが不思議に消えてしまいました。出力色ブレンディングは既存フレームバッファ値を読み取るため、最初の仮説はメモリ順序バグでした:古い読み取り、読込/書き込み競合、あるいは新しいフラッシュキャッシュが古いデータを返すケースです。
図 3: ハードウェア(左)対参照(右)。症状はフレームバッファ危険性のように見えました:数ピクセルだけが失われ、残りは正しく表示される。
この仮説は十分に妥当であったため、書き込み優先度を変更し、真に直接キャッシュレス経路を追加し、代替バッファ読み取りを比較しました。アーティファクトはほぼ動かず、その結果が転換点でした。フレームバッファ危険性のように見えたものですが、証拠はその説明と合致しませんでした。
ここで Netlist‑aware トレース(Conetrace)が従来の波形ビューアよりも遥かに役立ちました。大量の信号を手作業で時系列に合わせて確認する代わりに、失敗したピクセルをラスタライザ、TMU、カラーコンビネーションロジック、最後にフレームバッファ出力まで段階的に追跡できました。対象ピクセルの終端までトレースできたことで、キャッシュ仮説は崩れました:誤りはフレームバッファ経路が説明できるよりも前から存在していました。
Terminal $ conetrace rv path core_1.rasterizer_1.o core_1.writeColor.i_fromPipeline --track 52410001core_1.rasterizer_1.o @ cycle 5241000 payload={x: 396, y: 189, W: 1.972412, S: 124.492, T: 57.031}2-> core_1.tmu_1.io_output @ cycle 5241001 payload={S': 63.469, T': 13.984, lod: 0, texel: 0x58}3-> core_1.fbAccess.read @ cycle 5241002 payload={dst565: 0x4A29}4-> core_1.colorCombine_1.o @ cycle 5241002 payload={src: 0x56C9, dst_blend: 0x4A29, out: 0x39E7}5-> core_1.writeColor.i_fromPipeline @ cycle 5241003 payload={final565: 0x39E7}
注釈
-
RasterizerSame フラグメントは両パスに入り、微小な W 精度損失が既に存在。
ref:{x: 396, y: 189, W: 1.972427, S: 124.492, T: 57.031} -
First divergencePerspective ラウンドとピクセルごとの LOD は TMU パスで既に差異。
ref:{S': 63.492, T': 14.031, lod: 1, texel: 0x6B} -
Framebuffer readDestination 色は正確に一致し、キャッシュ理論を除外。
ref:{dst565: 0x4A29} -
Second divergenceThe 参照ブレンドパスは実際にデイタリングされた destination color を使用。
ref:{src: 0x5A8C, dst_blend: 0x49E7, out: 0x4A69} -
Visible symptom RTL は最終書き込みで参照よりかなり暗くなる。
ref:{final565: 0x4A69}
実際の問題は一つの壊れたブロックではなく、可視化されるまでにしか見えない小さなハードウェア精度ミスマッチの積み重ねでした。
- 第一の問題は浮動小数点三角形
が TMU パスで早期に量子化されていたことです。W - 第二は視差テクスチャ座標のラウンドとピクセルごとの LOD 調整がミップ境界近くで少しずれたことです。
- 第三はブレンディング:拡張 destination color をブレンド係数計算に使用したものの、実際の Voodoo はデイタリングされた destination color を想定していました。
それぞれの挙動は単独ではほぼ正しいものでしたが、正確なクラスのテクスチャ付きブレンディングプリミティブで組み合わせると目に見える誤ったピクセルを生成しました。これがバグがランダムに見えた理由です:フレーム全体は正常であり、失敗パスも状態空間の狭い角度だけでしか正しくありませんでした。
修正は最初に妥当だと感じた仮説を放棄し、機械を段階ごとに一致させることでした。広い
W、S、T アキュムレータを保持し、視差ラウンドと LOD 数学を修正し、デイタリングされた destination color をブレンド係数計算へ入力しました。これで「メモリ順序バグ」が消え、実際には存在しなかったことが明らかになりました。
従来の波形ビューアは関与するすべての信号を表示できますが、多くの再構築はエンジニアに委ねます。Netlist‑aware クエリツールはその再構築の一部をツール自身に移します。Voodoo のような設計では、この違いが「妥当な理論」と「実際の説明」の間のギャップになります。
実際に変わった現代 RTL ツール
Voodoo 1 が単純でない理由は古さだけではありません。非常に特定の方法で難しいのです:その挙動はシリコンに固定されているため、複雑さの多くは制御パス、レジスタ意味論、パイプラインタイミングにあります。
私が実感した現代 RTL ツールの変化は、設計自体の複雑さではなく、その複雑さを一度に頭に収める必要があった量です。
SpinalHDL はアーキテクチャ的意図をソース内で直接エンコードでき、宣言、バスロジック、ドキュメントへの散在を排除します。Conetrace は原始波形よりも設計構造に近い単位で実行を検査できます。
この組み合わせが、Voodoo の FPGA 再実装を一人で扱えるようにしました。機械は依然として複雑ですが、適切な抽象化があればその複雑さの多くが表現可能・クエリ可能・管理しやすいものになります。