- Mach engineの実験的グラフィックスAPI sysgpu は Direct3D 12 向けのシェーダー出力を必要としており、Microsoft DXC を静的かつクロスプラットフォームで使いやすい形に再ビルドした
- Direct3D 12 の DXIL は、Microsoft の LLVM/Clang 3.7 フォークが出力する 後処理済み LLVM bitcode に近く、仕様よりも「実際のコンパイラ出力」に依存する構造になっている
- WebGPU 系ランタイムは通常、WGSL を HLSL に変換した後、FXC または DXC で DXBC/DXIL を生成するが、DXC の配布負担のため古くて遅い FXC がデフォルトになりやすい
- 既存の DXC は静的リンクが難しく、
dxil.dll という 独自の署名/検証バイナリ が Windows・x86 Linux 向けにしか配布されていないため、macOS や Arm Linux の CI ではオフライン DirectX シェーダーコンパイルができない
mach-dxcompiler は CMake ビルドを Zig の build.zig に書き直し、DLL 依存を除去して静的 dxcompiler ライブラリと dxc CLI を提供するが、MSVC ABI・musl テスト・SPIR-V 出力・SM6.7 対応には制限が残っている
Mach が DXC を再ビルドした理由
- Mach engine は Zig で sysgpu という実験的グラフィックス API を開発しており、Metal、Vulkan、Direct3D、OpenGL バックエンドのサポートを目指している
- Direct3D 12 バックエンドでは、シェーダープログラムを Direct3D 12 が利用できる形式にコンパイルする必要がある
- その過程で、Microsoft の DirectX シェーダーコンパイラ DXC がゲーム開発者にとって複雑で不便な配布体験を生んでいることが明らかになった
FXC から DXC へ移行した DirectX シェーダーコンパイル
- DirectX グラフィックス API はシェーディング言語として HLSL を使用する
- Direct3D 11 以前の HLSL コンパイラは FXC、すなわち effects compiler と呼ばれていた
- FXC はゲーム開発者の間で、遅くコード生成品質も良くないコンパイラとして知られている
- シェーダーコンパイル速度と実行性能の両面で不利になりうる
- Direct3D 12 と Shader Model 6.0 で Microsoft は、Windows OS に含まれていた FXC を正式に deprecated とし、LLVM/Clang v3.7 ベースのフォークである DXC を導入した
- DXC は Microsoft/DirectXShaderCompiler として公開されており、Microsoft は事前ビルド済みバイナリも配布している
- Microsoft の LLVM フォークでは、HLSL 関連の変更が
// HLSL Change Start、// HLSL Change End というコメントで示されている
Direct3D ドライバが利用する DXBC と DXIL
- GPU メーカーごとにハードウェア構造や要件が異なるため、HLSL が最終的に実行されるネイティブバイナリも Intel、NVIDIA、AMD の GPU ごとに異なる
- Microsoft は Direct3D や HLSL のようなフロントエンド API を提供し、Intel・AMD・NVIDIA などの IHV がドライバを実装して、それをハードウェア ISA に近い形へ接続する
- DirectX 9〜11 では、ドライバは DXBC を利用する
- ゲーム開発者は
fxc.exe CLI または d3dcompiler API で HLSL を DXBC にコンパイルする
- ドライバは DXBC を実際の GPU で実行されるバイナリに変換する
- DXBC は Microsoft と GPU ドライバメーカーの間で使われていた非公開の独自形式だった
- DirectX 12 と Shader Model 6.0 以降では、DXIL が DirectX 12 ドライバメーカーの利用する公式形式になった
- DXIL は LLVM 3.7 のコード生成と最適化パスの後の bitcode 形式に、小さなカスタムコンテナ/ラッパーを加えたものに近い
- DXIL の文書は独立した仕様というより、Microsoft の LLVM 3.7 フォークが HLSL 変更と最適化後に実際に出力する bitcode に依存している
消えた DXIR 計画と LLVM アップストリーム移行
- Microsoft は DirectX 12 と Shader Model 6.0 の時点で、高水準・非最適化 IR である DXIR を作り、DXC がそれを最適化済み DXIL に落とし込む構想を持っていた
- 2021 年には DXIR の可能性を示唆していた表現が削除された
- 2019 年には Microsoft の担当者が、DXIR から DXIL への lowering 過程はほとんど文書化されておらず、DXIR は公式形式ではなく CodeGen 後の最初の LLVM IR に近いと説明している
- 2023 年には Microsoft の担当者が、DXC の LLVM フォークは LLVM のコード生成レイヤーやインフラの相当部分を削除または壊していると述べた
- DXC に DXBC 生成を追加するには壊れた LLVM 機能を復旧する必要があり、DXC ではそれを扱うつもりはないとしている
- Microsoft は 2022 年 3 月から、HLSL コンパイル対応を LLVM/Clang 本流へアップストリームする作業を提案し、進めている
- この移行計画には、現代の LLVM/Clang に レガシー LLVM v3.7 bitcode writing のサポートを再追加する作業も含まれる
WebGPU とゲームエンジンにおける DXC 配布負担
- Metal、Direct3D 12、Vulkan のような現代的グラフィックス API を統合しようとするグラフィックス抽象化レイヤには、統一シェーディング言語も必要になる
- 現在の WebGPU 実装には、長期的には DXIL を直接出力する経路を目指しているものもあるが、実際にはほとんどそうしていない
- 一般的な WebGPU の流れは次の通り
- WGSL テキスト言語をランタイム時に HLSL へ変換する
- HLSL を HLSL コンパイラで DXBC または DXIL にコンパイルする
- 最適化済み DXBC/DXIL をグラフィックスドライバに渡し、ドライバがベンダー別の中間表現やマシンコードに変換する
- Vulkan/SPIR-V でも、ドライバが SPIR-V をネイティブバイナリへコンパイルする構造になっている
- 一部のドライバは SPIR-V が最適化済みだと仮定できる場合があるが、モバイル・デスクトップ GPU によって異なる
- Valve の Fossilize は、GPU とドライババージョンの組み合わせごとに、ドライバがコンパイルした実際のバイナリキャッシュを保持する
- DXIL は常に最適化パス後の LLVM bitcode である一方、SPIR-V は最適化済みの場合もそうでない場合もある
- Apple Metal だけが、実際の対象ハードウェアのネイティブバイナリ形式へ直接コンパイルする API をサポートしている
dxcompiler.dll と dxil.dll が生む選択肢
- WebGPU ランタイムは WGSL→HLSL→DXIL 変換をランタイム時に行うため、新しい DXC と古い FXC のどちらかを選ぶ必要がある
- Bevy のドキュメントでは、FXC は古く遅く保守されていないが、追加の DLL 配布は不要とされている
- 逆に DXC は新しく高速で保守されているが、アプリケーションと一緒に
dxcompiler.dll と dxil.dll を配布しなければならない
- この選択の問題は Bevy だけでなく、
wgpu の Rust ユーザーや Dawn WebGPU ユーザーにも影響する
- 結果として、多くのソフトウェアで古く遅く保守されていない FXC がデフォルトになってしまう
DXC の静的リンクが難しい理由
- Microsoft の LLVM フォークは静的リンクをサポートしていない
- CMake ファイルで
SHARED を STATIC に変えると、およそ 15 個の静的ライブラリが生成されるが、単一ライブラリよりリンク体験は悪い
- CMake の
OBJECT ライブラリを使おうとすると、Microsoft の HLSL 変更のため、論理的依存関係とは別に暗黙の相互依存が表面化する
- DXC の COM インターフェース実装の一部は、
dxcompiler.dll と dxil.dll を動的ライブラリとして読み込み、自分自身を呼び出すように設計されている
- 単にビルド設定を変えるだけでは、静的 DXC を作るのは難しい
独自 dxil.dll とシェーダー署名
dxil.dll は DirectXShaderCompiler をソースからビルドしても生成されないが、GitHub リリースでは Windows x86/Arm と Linux x86 向けに配布されている
- D3D12 Shader Cache API specification によれば、D3D12 は署名済みシェーダーしか受け付けず、ランタイム最適化やパッチが行われた場合はシェーダーを再検証し、再署名する必要がある
- Shader Model 6.8 preview リリースでは
dxil.dll/libdxil.so は提供されていない
- そのコンパイラが生成した SM6.8 向け DXIL は最終版ではなく、検証できない
- 開発者モードではないマシンでの配布や実行はサポートされない
dxil.dll がないと、シェーダーは署名/検証されない
- 署名/検証されていないシェーダーは、Windows マシンが Developer Mode でない場合は実行できない
オフラインコンパイルとプラットフォーム制約
- Mach は必要なときに重い DXC 依存の配布を避けつつ、オフラインシェーダーコンパイル を行いたいと考えている
- Microsoft は
dxil.dll を Windows x86/Arm と Linux x86 向けにしか配布していない
- Linux aarch64 バイナリと macOS バイナリは提供されていない
- そのため、macOS で Windows 向けクロスプラットフォームゲームビルドを作成したり、Arm Linux CI パイプラインでオフライン DirectX シェーダーコンパイルを行ったりできない
- 独自の署名バイナリを実行するには、Windows または x86_64 Linux マシンが必要になる
mach-dxcompiler が変更した点
- 既存の CMake ビルドシステム約 10.5k 行を Zig の
build.zig に書き直した
- 利用者が主に必要とする 2 つの部分、
dxcompiler.dll ライブラリと dxc.exe オフラインコンパイル/テストバイナリだけをビルド対象にした
- その結果、約 1k 行の
build.zig ロジック に整理された
- DXC が
dxcompiler.dll と dxil.dll の存在を前提にする構造を修正するため、Microsoft のコードベースをフォークした
- DLL エントリポイントをシミュレートする
- DLL 由来のコンパイラバージョン情報出力機能を無効化する
- 動的ライブラリ関数ポインタの読み込みをエミュレートする
mach-dxcompiler は dxil.dll に依存しない形 で構成されている
- macOS マシン上で独自
dxil.dll なしに HLSL シェーダーをコンパイルし、通常の Windows マシンで実行される DXIL bytecode と byte-for-byte で同一のファイルを作成できる
成果物と使い方
- リリース には静的
dxcompiler ライブラリと dxc CLI の事前ビルド済みバイナリが含まれている
- 独自
dxil.dll への依存はない
- CI パイプラインでビルドされる対象は次の通り
- macOS: Apple Silicon aarch64 と Intel x86_64
- Linux: musl と glibc、aarch64 と x86_64
- Windows: x86_64 と aarch64、MinGW/GNU ABI を含む
- ライブラリは既存の COM API の代替として 小さな C API を公開している
- Zig のゲーム開発者はリポジトリの Zig API を利用でき、
src/main.zig のテストで使用例を確認できる
- 基本的には事前ビルド済みバイナリをダウンロードして使う
- ソースビルドは mach-dxcompiler リポジトリ で
zig と git だけで行え、指定された Zig バージョンが必要となる
git clone https://github.com/hexops/mach-dxcompiler
cd mach-dxcompiler/
zig build -Dfrom_source -Dtarget=aarch64-macos
zig build -Dfrom_source -Dtarget=x86_64-windows-gnu
zig build -Dfrom_source -Dtarget=x86_64-linux-gnu
現在の制限と保守条件
- Windows の MSVC ABI バイナリは、C バインディングの小さなバグのため現在はビルドできない
- Linux musl バイナリはビルドされるが、まだテストされていない
- Mach engine は HLSL ではなく Zig 自体をシェーディング言語として使う計画のため、SPIR-V 出力サポートはビルドしておらず、追加予定もない
- 最近リリースされた SM6.7 対応アップデートの予定は現在ない
- LLVM の CMake ビルドシステムの一部はまだ完全には移植されておらず、
generated-include/ に関連する詳細が残っている
- このプロジェクトは Mach の問題を解決するために存在しており、現在は 1 人で issue を処理する体制になっている
- より良い方法が見つかれば、プロジェクトは deprecated になる可能性がある
1件のコメント
Hacker Newsのコメント
クロス3D APIのシェーダーコンパイルの足元がどれほど混沌としているかをよく整理した記事。
D3DとMicrosoftに焦点を当てているが、他の3D APIも大してましではない。たとえばLinuxホストではMetalシェーダーをクロスコンパイルできず、macOSと比較的新しいWindowsでしかできない。
MachチームがZigをクロス3D APIシェーダーコンパイラとして使う構想を、「クロスコンパイル・ツールチェーンとしてのZig」と同じくらい滑らかにできるなら、1995年ごろ以降のコンピュータグラフィックスで最大級の出来事になり得る。
これはGodotにも関係がある。
「任意にした理由は、Direct3D 12対応が現在、DirectX Shader Compilerのプロプライエタリなdxil.dllライブラリをGodotと一緒に配布することに依存しており、プロプライエタリソフトウェアの配布はGodotプロジェクトの使命に反するため」だという。
https://godotengine.org/article/dev-snapshot-godot-4-3-dev-3...
自分のGPU、Wi-Fi、Bluetoothに動いてほしいので、デフォルトでチェックを入れておいてほしい。
「アプリケーションと一緒に追加の.dllを配布する必要はない」というくだりについては、すでに多くのビデオゲームがBink、SpeedTree、PhysXのようなプロプライエタリなミドルウェアをそのように配布している。
Steam、GOG、Epicのような大半のランチャーもそれぞれの.DLLを要求するし、多くのゲームがD3D11On12を使っている。インストール済みファイル一覧にdxil.dllが含まれている発売済みゲームも多い。
だから率直に気になるのだが、DLLをもう1つ配布することの何が問題なのか?ここでコード署名をリバースエンジニアリングして再実装した作業は見事だし、とりわけdxil.dllの出力とビット単位で同一という点はすごい。だが自分なら、とんでもなく怠け者なので、もっと簡単な道であるDLL配布を選んでいただろう。
たとえばMachエンジンは、Zigコードを対象プラットフォーム全体のシェーダーへコンパイルするコンパイラを作る際にこれを使えるし、エンドユーザーは追加設定や依存関係なしに、Machの中核機能の中でそのまま使える。
AAAゲームでも、また別のゲームやソフトウェアでも、制御の及ばないところで壊れ得る追加依存関係が増える。以前勤めていた会社では、あるミドルウェアをDLLとライブラリの形でしか受け取れずリンクしなければならなかったため、Visual Studioのアップグレードにより多くの労力を費やし、新バージョンも受け取る必要があった。そのミドルウェア会社が自社側のアップグレードを済ませていなかったせいで、こちらが新しいVS互換性のQAまで行うことになった。
もちろんソースコードがあるからといって更新が摩擦なく進むわけではないが、摩擦ははるかに少なくなり、他者を待つ必要もなくなる。最近のVisual StudioはバイナリC++ライブラリの後方互換性を試みているように見えるが、長期的に依存できる性質のものではないと思う。
さらに、これらはすべてコードが同じプラットフォームとターゲットにとどまるという前提の話だ。どこかの時点で別のプラットフォームをホストやターゲットとして扱いたくなるかもしれず、ソースコードがなければ非常に難しいか不可能になり得る。DXILのようなプラットフォーム特化のものでは大きな問題に見えないかもしれないが、記事でもDXILのバイナリBlob性のため、MicrosoftがDLLを提供した特定のWindowsおよびLinuxアーキテクチャ以外ではシェーダーの事前コンパイルが不可能だったと述べている。
DXIL.dll が行う「署名」[1]は、結局のところ改変された MD5 にすぎないのか?
1: https://github.com/hexops/DirectXShaderCompiler/blob/4190bb0...
そこまで注意深く読んだ人なら、基本的なハッシュかそれに近いもののはずだと分かっただろうし、最悪でも誰かがアセンブリをリバースエンジニアリングすれば済む程度だった
そこまで苦労した後なら、Microsoft を公然と批判してもいい。特に、関心のある人なら誰でも掘り下げて見つけられるオープンソースコードの話ならなおさらで、見つけてくれた msk には感謝
https://github.com/baldurk/renderdoc/blob/4a620bb5a16b4de4e2...
Microsoft 側のこの引用[0]は気に入っている。DXC の LLVM フォークは LLVM のコード生成層とインフラを大幅に削除または壊してしまっているため、DXC で DXBC 生成をサポートするには、壊れた LLVM 機能を修正・復元する大規模な作業が必要だという
問題の規模が大きく、チームのリソースも限られているため、新しい DXC コンパイラでこの問題を解決することはなく、将来的に Clang で DXBC 生成をサポートできる可能性はあるものの、当面は DXIL と SPIR-V の生成サポートに集中するため、数年は着手が難しいという。やらないことを明確に伝えてくれるのは新鮮だ
[0] https://github.com/microsoft/DirectXShaderCompiler/issues/57...
Mach エコシステムはぜひ見てみることを勧める。特に mach-sysgpu は WebGPU の完全な再実装で、その大部分を 17 歳の Ali Chraghi が書いている
SDL 側では、SDL3 に入れるために既存のものとは異なる SDL_gpu という形のシェーダー言語を作っている。ゲーム向け 3D グラフィックスを扱う際のクロスプラットフォームな方法になり得るので、しばらく注目している
違いがあるとすれば、SDL_gpu はまだ初期段階の作業中で、WebGPU にはすでに良い公開実装が 2 つあることくらいだ
あまり面倒でない方法は、HLSL/GLSL → SPIR-V ↔ DXIL のような形か、シェーダーを直接 SPIR-V で書くことだろう
Wine の vkd3d には DXIL → SPIR-V 変換器があるようで、高水準シェーディング言語の変換器よりもはるかに単純な中間言語なので、より堅牢かもしれない
ただ、この LLVM の怪物ではなく、GCC や Clang なしでコンパイルできる、純粋で単純な C99 ベースの HLSL → DXIL コンパイラ があるのか気になる
質問に答えると、ない。HLSL から DXIL への変換は事実上 Microsoft が握っていて、そこから抜け出そうとする取り組みはほとんどなかった
Zig 自体をシェーディング言語として使うのは格好いい。Zig は真の単一言語だ。ビルドシステムでもあり、シェーディング言語でもある!