シリーズ: AI とともにコードを学ぶ

dxcompiler.dllを見つけられなかったテスト

堅いDLL依存関係を優雅で観測可能なフォールバックに変える。

DLLなしで起動する 読み込み時依存関係 → 観測可能なフォールバック 0xC0000135 STATUS_DLL_NOT_FOUND LoadLibrary DXCが存在する → 完全なSPIR-V / DXIL D3DCompiler → 縮小版(DXILのみ) なし → マゼンタのプレースホルダー + WARN 71% → 89% テスト通過
ランタイムは起動する。機能はオプトインだ。失敗モードは大きく可視化される。

1つのオプショナルなDLLが欠けているだけでクラッシュするランタイムは、静かに自身をある種のマシンだけに閉じ込めてしまっている。真のポータビリティとは、どこでも起動し、大きな音を立てて劣化することだ——そしてそれこそがパートナーがデプロイする対象なのだ。

土曜日の朝、CIダッシュボード、8つのテストが赤。8つすべてが同じWindowsエラーコードで失敗していた。0xc0000135。Windowsネイティブ開発に時間を費やしたことがある人なら、これを見ただけでピンと来るだろう。STATUS_DLL_NOT_FOUND。プロセスが必要なDLLを読み込もうとしたが、そのDLLがシステム上に存在しなかった。

問題のDLLはdxcompiler.dll、Microsoft DirectX Shader Compilerのランタイムだった。CIマシンにはそれがなかった。本番のユーザーマシンにもないかもしれない。開発者の整った環境の外側であればどこでも、ランタイムはdxcompiler.dllが存在するものと静かに仮定し、それが存在しないと激しくクラッシュしていた。

土曜日が終わる頃には、3つの問題が修正され、テストスイートの通過率は71%から89%に上がっていた。この投稿は、その一つひとつについて書くものだ。

そもそもなぜこの依存関係が「堅い」ものになっていたのか

シェーダークロスコンパイル経路は、MicrosoftのDXCコンパイラを使ってHLSLシェーダーソースを取り込み、SPIR-VまたはDXILバイトコードを生成する。エンジンが出荷するシェーダーのほとんどは事前コンパイル済みだ。ランタイム内のいくつかのコードパスは実行時に新しいシェーダーをコンパイルできる。開発中のシェーダー派生パターンのホットリロード、エディタでの動的マテリアル編集、そしていくつかのデバッグ経路である。

元の実装はシェーダークロスのソース内で#pragma comment(lib, "dxcompiler.lib")ディレクティブを使ってdxcompiler.libをリンクしていた。これは堅い読み込み時依存関係を生んでいた。Windowsのローダーはプロセス起動時に読み込み時依存関係を解決する。DLLが欠けていれば、プロセスはmain()にすら到達しない。ランタイムの実行ファイルは、そのDLLがシステム上になければ起動すらできなかった。

これは、DXC SDKをフルインストールしている開発者にとっては正しい答えだ。CIランナーやユーザーマシン、あるいはDXCが存在しないその他の環境にとっては間違った答えだ。ランタイムは起動すべきだ。ホットリロード機能は「利用不可」と報告すべきだ。それ以外のすべては動き続けるべきだ。

修正はどのようなものだったか

3つの変更、それぞれが小さく、それぞれが的確だった。

シェーダークロスのソースを、読み込み時のDLL読み込みから実行時読み込みへ移行した。 #pragma comment(lib, ...)ディレクティブを、明示的なLoadLibraryGetProcAddress呼び出しに置き換えた。DLLはプロセス起動時ではなく、初回使用時に検索される。LoadLibraryが失敗すれば、そのコードパスは明確なエラーを返す。

DXCが欠けている場合のフォールバック経路を追加した。 dxcompiler.dllが存在しない場合、シェーダークロスのサブシステムは2つの経路のいずれかにフォールバックする。より古いD3DCompilerが利用可能であれば、機能を縮小して(SPIR-V出力なし、DXILのみ)それにフォールバックする。D3DCompilerさえも欠けている場合は、けたたましいマゼンタ一色のフラグメントシェーダーを生成するプレースホルダーのSPIR-Vブロブにフォールバックする。このプレースホルダーは、シェーダーコンパイラが一切利用できない環境でもランタイムがエンドツーエンドで動作し続けることを保証する。視覚的な合図により、プレースホルダーモードで動作していることが一目瞭然になる。

優雅な失敗経路はログとテレメトリを出力する。 すべてのフォールバックはWARNINGレベルで、フォールバックした具体的なサブシステムと理由とともにログに記録される。テレメトリはこのフォールバックを運用可視性のために記録する。DXCなしでローカル実行している開発者は、コンソールで警告を見て、本物のシェーダーコンパイルが必要ならDXCをインストールすべきだと分かる。すべてが事前コンパイル済みだという前提でランタイムを実行しているユーザーには、警告は一切表示されない。事前コンパイル済みのシェーダーはDXCなしでも問題なく動くからだ。

これはこの依存関係にとって正しい形だ。ランタイムはポータブルになった。機能は何がインストールされているかに応じたオプトインになった。失敗モードは観測可能になった。

同じ監査から見つかった隣接する2つのバグ

シェーダー読み込みのコードのふたを開けたついでに、隣接する形で失敗していた他の2つのテストも同じパスで修正した。

オーディオバスエフェクトのABI不一致。 raku_audio_bus_add_effectraku_audio_bus_remove_effectのC API関数は(handle, struct*)というシグネチャで書かれていたが、テストスイートはオーディオAPIの残り部分と同様に(handle, handle)で呼び出していた。テストがセグフォルトしていたのは、構造体ポインタの参照外しが、2つ目のハンドルのビット列がたまたま似ていたアドレスにあった何かを読んでいたからだ。ABIの不一致である。

修正は、C APIをオーディオモジュールの残りの部分に合わせて(handle, handle)に変更することだった。オーディオバスにおけるエフェクトチェーンはランタイムが追跡する第一級のオブジェクトであるため、これがAPIにとって正しい形だ。構造体ポインタ版は、リファクタリングの残りの部分を生き延びられなかった以前のAPI設計の名残だった。テストスイートは正しく、実装が古びていたのだ。

メモリリークテストのスタブが本物の実装を上書きしていた。 アセットストリーミングのサブシステムには、ストリーミングされたアセットの参照カウントを検証するメモリリークテストがある。このテストが失敗していたのは、テストフィクスチャにAssetStreamingManagerのスタブ実装が残されたままになっており、それがランタイムDLLの本物の実装を上書きしていたからだ。テストはスタブを実行していたのであって、本物のコードではなかった。スタブにはメモリリークがあった。本物の実装にはなかった。テストがリークがあると言っていたのは正しかった。それが誰のリークかについては間違っていた。

修正は、テストフィクスチャからスタブを取り除き、テストがきれいにリンクできるよう本物の実装にRAKU_STREAMING_APIエクスポートを追加することだった。正しくリンクされると、テストは本物のコードに対して通過した。

3つの修正が合わさって何を解放したか

dxcompilerの修正で8つのテストがグリーンになった。オーディオバスの修正で2つ。メモリリークの修正で3つ。通過するテストの合計は39/55から49/55へと上昇した。89%という数字は祝うべき節目として正しいが、より深い節目は、ランタイムがDXCなしで動くようになった、つまりまだ想定していない環境でも動くようになるということだ。

学んだこと

3つある。

堅い読み込み時依存関係はポータビリティの欠陥である。 ランタイムが、普遍的には存在しないDLLや共有オブジェクトに対して読み込み時依存関係を持っている場所はどこであれ、ランタイムは自らをそのDLLを含む環境に制限してしまっている。その制約を意図的に設けるのは構わない。それを偶然に作ってしまうのはまずい。「ランタイムが読み込み時に何を要求しているか」の監査は実施する価値がある。

0xc0000135は最もよくあるWindowsのDLLエラーコードであり、メッセージは何も明かさない。 エラーは何らかのDLLが欠けていることを教えてくれる。どのDLLかは教えてくれない。それを突き止めるための診断ツール(Process MonitorDependencies.exe、新しいWindows ETWトレーシング)はどれも機能するが、そのすべてが、失敗が起きる前にそれらの存在を知り、セットアップしておくことを要求する。ランタイムは今、優雅なフォールバックが発動したときに、欠けていたDLLの名前を明示した明確なエラーメッセージを出力するようになった。未来の自分はきっと感謝するだろう。

テストと実装の間のABI不一致は、たいていテスト側が正しい。 テストはコードベースの残りの部分が公開しているAPIに対して書かれていた。テストが実装と食い違っていたとき、テストの方が正しかった。これは通常の直感——「テストを直せ」——とは逆だ。正しい直感は、より広いAPI表面を見て、どちらの側が外れ値なのかを問うことである。

パートナーとビルダーがここから汲み取るべきこと

開発者のマシンと本番環境が異なる環境(ほぼすべてのデプロイがそうだ)へのデプロイを検討していて、パートナーシップのためにエンジンを評価しているなら、チームに堅い読み込み時依存関係について尋ねてみるとよい。正しい答えは短いリストで、すべてに正当な理由がある。間違った答えは長いリストで、その半分をチーム自身が忘れている。

あなたがWindowsネイティブの開発者で、普遍的には存在しないDLLに対してまだLoadLibraryGetProcAddressを使っていないなら、これはその優しい後押しだ。パターンは小さい。ポータビリティの見返りは大きい。

あなたがコーディングエージェントにWindowsコードを書かせているAIラボなら、エージェントが書いたコードで注意すべきパターンは、本来は実行時読み込みであるべき#pragma comment(lib, ...)ディレクティブと、コードベースの残りの部分からずれていくABIシグネチャだ。どちらも静的解析でフラグを立てられる。どちらもエージェントのレビューチェックリストに入れておくべきだ。

週末終わりのまとめ。ランタイムはDXCを持たないマシンでもきれいに起動する。2日間で11のテストがグリーンになった。次の監査もカレンダーに入っている。

さあ、開発に戻ろう。

あなたのハードウェアが存在する場所で動くランタイム

RakuAIは、スマートグラスと乱雑な現実世界のために作られた空間ランタイムだ——ポータブルで、依存関係が欠けても優雅に振る舞い、フォールバック時には観測可能である。あらゆる場所へのデプロイに何が必要かを見てほしい。

← すべての記事