系列: 與 AI 一起學寫程式

找不到 dxcompiler.dll 的那個測試

把硬性的 DLL 相依變成優雅、可觀測的後備路徑。

沒有這個 DLL 也能啟動 載入期相依 → 可觀測的後備路徑 0xC0000135 STATUS_DLL_NOT_FOUND LoadLibrary 有 DXC → 完整 SPIR-V / DXIL D3DCompiler → 精簡(僅 DXIL) 都沒有 → 洋紅色佔位 + WARN 測試通過率 71% → 89%
執行環境照常啟動。能力依安裝情況啟用。失效模式響亮而可見。

一個因為缺了某個選用 DLL 就當機的執行環境,已經悄悄把自己侷限在一種機器上。真正的可攜性意味著在任何地方都能啟動、並且大聲地降級——而那才是合作夥伴會部署的東西。

週六早上,CI 儀表板,八個測試亮紅燈。八個全部以同一個 Windows 錯誤代碼失敗:0xc0000135。在 Windows 原生開發上花過時間的人一眼就認得它。STATUS_DLL_NOT_FOUND。程序試圖載入它需要的 DLL,而那個 DLL 不在系統上。

問題中的 DLL 是 dxcompiler.dll,Microsoft DirectX 著色器編譯器的執行期程式庫。CI 機器上沒有它。正式環境的使用者機器上也可能沒有。在開發者配備齊全的工作站之外的任何地方,執行環境一直在默默假設 dxcompiler.dll 存在,並在它不存在時硬性當機。

到週六結束時,三個問題修好了,測試套件的通過率從 71% 升到 89%。這篇文章要講的就是每一個問題。

為什麼這個相依一開始是硬性的

著色器交叉編譯路徑使用 Microsoft 的 DXC 編譯器,把 HLSL 著色器原始碼編譯成 SPIR-V 或 DXIL 位元組碼。引擎出貨的著色器大多是預先編譯的。執行環境中有少數程式碼路徑能在執行期編譯新著色器:開發期間的著色器排列組合熱重載、編輯器中的動態材質編輯,以及幾條除錯路徑。

原始實作透過著色器交叉編譯原始碼中的 #pragma comment(lib, "dxcompiler.lib") 指示詞連結 dxcompiler.lib。這產生了一個硬性的載入期相依。Windows 載入器在程序啟動時解析載入期相依;如果 DLL 缺失,程序根本走不到 main()。系統上沒有這個 DLL,執行環境的可執行檔就無法啟動。

對安裝了完整 DXC SDK 的開發者來說,這是正確答案。但對 CI 執行器、使用者機器,或任何沒有 DXC 的地方來說,這是錯誤答案。執行環境應該要能啟動。熱重載功能應該回報「不可用」。其他一切應該繼續運作。

修復長什麼樣

三個變更,每個都很小,每個都很精準。

著色器交叉編譯原始碼從載入期改為執行期 DLL 載入。#pragma comment(lib, ...) 指示詞換成明確的 LoadLibraryGetProcAddress 呼叫。DLL 在首次使用時才查找,而不是在程序啟動時。如果 LoadLibrary 失敗,該程式碼路徑回傳明確的錯誤。

為 DXC 缺失的情況加上後備路徑。dxcompiler.dll 不存在時,著色器交叉編譯子系統退回兩條路徑之一。如果較舊的 D3DCompiler 可用,就退回它並提供精簡功能(沒有 SPIR-V 輸出,只有 DXIL)。如果連 D3DCompiler 都缺失,就退回一個佔位的 SPIR-V 資料塊,它會產生一個醒目的、只輸出洋紅色的片段著色器。這個佔位方案確保執行環境在沒有任何著色器編譯器的環境中也能端到端運作;視覺訊號則讓「你正在佔位模式下執行」一目了然。

優雅失敗路徑會記錄並發出遙測。每次後備都會以 WARNING 層級記錄,指明是哪個子系統退回、以及原因。遙測會記錄後備事件供維運觀測。在本機執行、沒裝 DXC 的開發者會在主控台看到警告,知道若需要真正的著色器編譯就該安裝 DXC。假設一切都已預先編譯的使用者執行時則完全不會看到警告,因為預先編譯的著色器不需要 DXC 也能正常運作。

這才是這個相依該有的形狀。執行環境可攜。能力依安裝情況啟用。失效模式可觀測。

同一次稽核順帶挖出的兩個相鄰臭蟲

趁著我掀開著色器載入程式碼的引擎蓋,另外兩個測試也以相鄰的方式失敗,我在同一輪順手修了。

音訊匯流排效果的 ABI 不匹配。C API 函式 raku_audio_bus_add_effectraku_audio_bus_remove_effect 是以 (handle, struct*) 簽章寫的,但測試套件用 (handle, handle) 呼叫它們,因為音訊 API 的其餘部分都是這樣。測試發生記憶體區段錯誤,因為結構指標解參考讀到的是第二個控制代碼位元恰好對應到的位址上的任意內容。ABI 不匹配。

修復方式是把 C API 改成與音訊模組其餘部分一致:(handle, handle)。這才是這個 API 該有的形狀,因為音訊匯流排中的效果鏈是執行環境追蹤的一級物件。結構指標版本是較早期 API 設計的殘留,那個設計並沒有在後續的重構中存活下來。測試套件是對的;實作已經過時。

記憶體洩漏測試的樁覆蓋了真實實作。素材串流子系統有一個記憶體洩漏測試,用來檢驗串流素材的參考計數。測試失敗,是因為測試夾具裡留下了一個 AssetStreamingManager 的樁實作,覆蓋了來自執行環境 DLL 的真實實作。測試檢驗的是樁,不是真實程式碼。樁有記憶體洩漏。真實實作沒有。測試說有洩漏是對的;但它搞錯了是誰的洩漏。

修復方式是把樁從測試夾具中移除,並在真實實作上加上 RAKU_STREAMING_API 匯出,讓測試能乾淨地連結它。連結正確之後,測試對真實程式碼通過了。

三個修復合起來解鎖了什麼

dxcompiler 修復讓八個測試轉綠。音訊匯流排修復讓兩個轉綠。記憶體洩漏修復讓三個轉綠。通過的測試總數從 39/55 爬到 49/55。89% 這個數字是值得慶祝的里程碑,但更深層的里程碑是執行環境現在不需要 DXC 也能運作,這代表它將能在我尚未預料到的環境中運作。

我學到的事

三件事。

硬性的載入期相依是一種可攜性缺陷。只要你的執行環境對某個並非到處都有的 DLL 或共享物件存在載入期相依,執行環境就把自己限制在包含那個 DLL 的環境裡了。刻意做出這個限制沒問題。意外做出這個限制就糟了。「我們的執行環境在載入期需要什麼」這項稽核值得一跑。

0xc0000135 是最常見的 Windows DLL 錯誤代碼,而錯誤訊息什麼都沒透露。這個錯誤告訴你有個 DLL 缺失。它不告訴你是哪一個。用來查明是哪一個的診斷工具(Process MonitorDependencies.exe、新的 Windows ETW 追蹤)都有效,但它們全都要求你知道它們存在、並在失敗發生之前就先設置好。執行環境現在會在優雅後備觸發時發出明確的錯誤訊息,點名缺失的 DLL。未來的我會感激的。

測試與實作之間的 ABI 不匹配,通常是測試那邊才是對的。測試是照著程式庫其餘部分暴露的 API 寫的。當測試與實作不一致時,是測試對。這和「把測試修掉」的常見直覺正好相反。正確的直覺是綜觀更廣的 API 表面,問哪一邊才是異數。

合作夥伴與開發者該從中學到什麼

如果你正在評估一個引擎作為合作對象,而你要部署的環境和開發者的機器不同(幾乎每一種部署都是如此),問問這個團隊他們的硬性載入期相依有哪些。正確答案是一份簡短的清單,每一項都有理由。錯誤答案是一份很長的清單,其中一半團隊自己都忘了。

如果你是 Windows 原生開發者,而你還沒有對任何並非到處都有的 DLL 使用 LoadLibraryGetProcAddress,這就是那個溫和的提醒。這個模式很小。可攜性的收益很大。

如果你是 AI 實驗室、你的程式設計代理會寫 Windows 程式碼,那麼在代理撰寫的程式碼中該留意的模式,是那些本該改成執行期載入的 #pragma comment(lib, ...) 指示詞,以及偏離程式庫其餘部分的 ABI 簽章。兩者都能被靜態分析標記出來。兩者都應該列入代理的審查檢查清單。

週末收尾。執行環境在沒有 DXC 的機器上乾淨啟動。兩天內十一個測試轉綠。下一次稽核已排上行事曆。

回去繼續打造。

一個在你的硬體所在之處運行的執行環境

RakuAI 是為智慧眼鏡與混亂的真實世界打造的空間執行環境——可攜、在相依缺失時優雅降級、在後備時可觀測。看看要在任何地方部署需要什麼。

← 所有文章