Web 高階著色語言(WHLSL) - 為WebGPU設計的Web圖形著色語言
原文作者:Myles Maxfield @Litherum
譯者:UC 國際研發 Jothy
寫在最前:歡迎你來到“UC國際技術”公眾號,我們將為大家提供與客戶端、服務端、演算法、測試、資料、前端等相關的高質量技術文章,不限於原創與翻譯。
本文介紹了一種新的 Web 圖形著色語言:Web 高階著色語言(WHLSL,發音為 “whistle”)。 這種語言受 HLSL 的啟發,HLSL 是圖形應用開發人員用的主要著色語言。 它擴充套件了 Web 平臺的 HLSL,使其安全可靠。 它易於閱讀和編寫,使用了正式技術而可以很好地指定。
背景
在過去的幾十年中,3D 圖形已經發生了重大變化,程式員用來編寫 3D 應用的 API 也發生了相應的變化。五年前,最先進的圖形應用使用 OpenGL 來執行渲染。然而,在過去幾年中,3D 圖形行業正朝著更新,更低級別的圖形框架轉變,這種框架更符合真實硬體的行為。 2014 年,Apple 建立了 Metal 框架,讓 iOS 和 macOS 應用可以充分利用 GPU。 2015 年,微軟建立了 Direct3D 12,這是 Direct3D 的一個重大更新,它允許控制檯級的渲染和計算效率。
2016 年,Khronos Group 釋出了 Vulkan API,主要用於 Android,具有類似的優勢。
就像 WebGL 將 OpenGL 引入 Web 一樣,Web 社群正在尋求將這種型別的新型低階 3D 圖形 API 引入平臺。去年,Apple 在 W3C 內部建立了 WebGPU 社群組,以使新的 3D 圖形 API 標準化,該 API 提供了原生 API 的優勢,但也適用於 Web 環境。這個新的 Web API 可以在 Metal,Direct3D 和 Vulkan 之上實現。所有主要的瀏覽器廠商都參與併為該標準化工作做出貢獻。
這些現代 3D 圖形 API 中的每一個都使用著色器,WebGPU 也不例外。著色器是利用 GPU 專用架構的程式。特別是,在重型並行數值處理中,GPU 要優於 CPU。為了利用這兩種架構,現代 3D 應用使用混合設計,使用 CPU 和 GPU 來完成不同的任務。通過利用每個架構的最佳特性,現代圖形 API 為開發人員提供了一個強大的框架,可以建立複雜,豐富,快速的 3D 應用程式。專為 Metal 設計的應用使用 Metal Shading Language,為 Direct3D 12 設計的應用使用 HLSL,為 Vulkan 設計的應用使用 SPIR-V 或 GLSL。
語言要求
就像它的原生同行一樣,WebGPU 也需要一種著色器語言。這種語言需要滿足幾個要求,以適合 Web 平臺。
它需要是安全的。無論應用做什麼,著色器必須只能從網頁的域中讀取或寫入資料。如果沒有這種保證,惡意網站可以執行著色器,從螢幕的其他部分讀取畫素,甚至是本機應用。
它需要明確指定語言規範。語言規範必須明確是否每個可能的字串都是有效的程式。與所有其他 Web 格式一樣,必須精確指定 Web 的著色語言以保證瀏覽器之間的互操作性。
它也需要明確指定編譯規範,以便它可以用作編譯目標。許多渲染團隊使用內部自定義語言編寫著色器,然後交叉編譯為必要的語言。出於這個原因,語言應該有一組相當少的明確的語法和型別檢查規則,編譯器編寫者在發出這種語言時可以參考這些規則。
它需要翻譯成 Metal Shading Language,HLSL(或 DXIL)和 SPIR-V。這是因為 WebGPU 被設計為能同時在 Metal,Direct3D 12 和 Vulkan 之上工作,因此著色器需要能夠以以上每個 API 都可以接受的形式表示。
它需要具有高效性。開發人員首先想要使用 GPU 的終極原因是效能。編譯器本身需要快速執行,編譯器生成的程式需要在真正的 GPU 上高效執行。
它需要使用 WebGPU API 進行演變。 WebGPU 功能(如繫結模型和曲面細分模型)與著色語言深度互動。儘管使用獨立於 API 開發的語言是可行的,但在同一論壇中使用 WebGPU API 和著色語言可確保共享目標,並使開發更加簡化。
它需要易於開發者閱讀和編寫。這包括兩個部分:首先,GPU 程式設計師和 CPU 程式設計師都應該熟悉這種語言。 GPU 程式設計師是重要的使用者,因為他們有編寫著色器的經驗。 CPU 程式設計師很重要,因為 GPU 越來越多地用於渲染之外的目的,包括機器學習,計算機視覺和神經網路。對於他們來說,語言應該與熟悉的程式語言概念和語法相容。
第二部分是語言應該是人類可讀的。 Web 的文化是任何人都可以用文字編輯器和瀏覽器開始編寫網頁。內容的民主化是 Web 最大的優勢之一。這種文化創造了一個豐富的工具和審查員生態系統,修補者可以通過 View-Source 調查任何網頁的工作方式。使用單一規範的人類可讀語言將極大地幫助社群採用 WebGPU API。
當今網路上使用的所有主要語言都是人類可讀的,但有一個例外。 WebAssembly 社群組希望解析位元組碼比解析文字語言更有效。但事實證明並非如此; Asm.js 是 JavaScript 原始碼,在許多用例中仍然比 WebAssembly 快。
類似地,使用諸如 WebAssembly 之類的位元組碼格式並不能避免瀏覽器對原始碼進行優化的需要。每個主要瀏覽器在執行之前都會在位元組碼上執行優化。不幸的是,追求更簡單的編譯器的願望從未結束。
社群小組正在積極討論這種人類可讀的語言是否應該是 API 本身接受的語言,但該小組同意著色器編寫的語言應該易於讀寫。
一種新語言?真的嗎?
雖然有許多現有語言,但沒有一種語言設計時考慮到 Web 和現代圖形應用程式,並且沒有一種語言符合上面列出的要求。在我們描述 WHLSL 之前,讓我們看看一些現有的語言。
Metal Shading Language 與 C++ 非常相似,這意味著它具有位轉換和原始指標的所有功能。它非常強大; 甚至可以為 CPU 和 GPU 編譯相同的原始碼。將現有的 CPU 端程式碼移植到 Metal Shading Language 非常容易。不幸的是,所有這些能力都有一些缺點。例如,在 Metal Shading Language 中,你可以編寫一個著色器,將指標轉換為整數,新增 17,將其強制轉換回指標,然後取消引用它。這是一個安全問題,因為它意味著著色器可以訪問恰好位於應用程式地址空間中的任何資源,這與 Web 的安全模型相反。從理論上講,可以指定一個沒有原始指標的 Metal Shading Language,但指標對於 C 和 C++ 語言來說是如此基礎,結果將完全陌生。 C++ 也嚴重依賴於未定義的行為,因此任何完全指定 C++ 眾多功能的努力都不太可能成功。
HLSL 是便攜 Direct3D 著色器的受支援語言。它是目前世界上最流行的實時著色語言,因此是圖形程式設計師最熟悉的語言。有多種實現,但沒有正式的規範,因此很難建立一致的,可互操作的實現。儘管如此,鑑於 HLSL 無處不在,在 WHLSL 的設計中儘可能採用其語法是很有價值的。
GLSL 是 WebGL 使用的語言,並被 WebGL 用於 Web 平臺。但是,由於 GLSL 編譯器不相容,達到跨瀏覽器的互操作性極其困難。由於仍然存在長期的安全性和可移植性錯誤,GLSL 仍處於調研中。此外,GLSL 到年紀了。它的侷限性在於它沒有類似指標的物件,或者具有可變長度陣列的能力。它的輸入和輸出是具有硬編碼名稱的全域性變數。
SPIR-V 被設計為開發人員將使用的實際著色語言的低階通用中間格式。人們不寫作 SPIR-V; 它們使用人類可讀的語言,然後使用工具將其轉換為 SPIR-V 位元組碼。
在 Web 上採用 SPIR-V 存在一些挑戰。首先,SPIR-V 不是以安全性作為第一原則編寫的,並且不清楚是否可以對其進行修改以滿足 Web 的安全性要求。Fork SPIR-V 語言意味著開發人員必須重新編譯著色器,可能還是被迫重寫他們的原始碼。此外,瀏覽器仍然無法信任傳入的位元組碼,並且需要驗證程式以確保它們沒有做任何不安全的事情。由於 Windows 和 macOS/iOS 不支援 Vulkan,因此傳入的 SPIR-V 仍需要翻譯/編譯成另一種語言。奇怪的是,這意味著在這兩個平臺上,起點和終點都是人類可讀的,但介於兩者之間的位被混淆而沒有任何好處。
其次,SPIR-V 包含 50 多個可選功能,它們的實現是選擇性支援的,因此使用 SPIR-V 的著色器作者不知道它們的著色器是否可以在 WebGPU 實現上工作。這與 Web 的一次寫入執行特性相反。
第三,許多圖形應用程式(如 Babylon.js)需要在執行時動態修改著色器。使用位元組碼格式意味著這些應用程式必須包含用 JavaScript 編寫的編譯器,該編譯器在瀏覽器中執行以從動態建立的著色器生成位元組碼。這將顯著增加這些網站的膨脹,並將導致更差的效能。
儘管 JavaScript 是 Web 的規範語言,但它的屬性使其成為著色語言的不良候選者。它的優勢之一是它的靈活性,但這種動態導致許多條件和不同的控制流程,而 GPU 不能有效地執行。它也是垃圾收集的,這是一個絕對不適合 GPU 硬體的程式。
WebAssembly 是另一種熟悉的可能性,但它也不能很好地對映到 GPU 的體系結構。例如,WebAssembly 假設一個動態大小的堆,但 GPU 程式可以訪問多個動態大小的緩衝區。沒有重新編譯,沒有一種高效能的方法可以在兩個模型之間進行對映。
因此,在對相應語言進行相當詳盡的搜尋之後,我們找不到足以滿足專案要求的語言。因此,社群小組正在製作一種新語言。建立一門新語言是一項艱鉅的任務,但我們認為有機會製作一些使用現代程式語言設計原則並滿足我們要求的新東西。
WHLSL
WHLSL 是一種適合 Web 平臺的新著色語言。它由 W3C 的 WebGPU 社群組開發,該組正在研究規範,編譯器和 CPU 埠直譯器以彰顯它的正確性。
該語言基於 HLSL,但簡化並擴充套件了它。我們真的希望現有的 HLSL 著色器能作為 WHLSL 著色器執行。由於 WHLSL 是一種功能強大且富有表現力的著色語言,因此一些 HLSL 著色器需要調整才行,因此,WHLSL 可以保證上述安全性和其他好處。
例如,以下是 Microsoft 的 DirectX-Graphics-Samples 儲存庫中的示例頂點著色器。 它可以作為 WHLSL 著色器而無需任何更改:
VSParticleDrawOut output; output.pos = g_bufPosVelo[input.id].pos.xyz; float mag = g_bufPosVelo[input.id].velo.w / 9; output.color = lerp(float4(1.0f, 0.1f, 0.1f, 1.0f), input.color, mag); return output;複製程式碼
這是關聯的畫素著色器,它作為完全未修改的 WHLSL 著色器執行:
float intensity = 0.5f - length(float2(0.5f, 0.5f) - input.tex); intensity = clamp(intensity, 0.0f, 0.5f) * 2.0f; return float4(input.color.xyz, intensity);複製程式碼
基礎
我們來談談語言本身。
就像在 HLSL 中一樣,原始資料型別是 bool,int,uint,float 和 half。不支援 Double 型別,因為它們在 Metal 中不存在,並且軟體模擬太慢。 Bool 沒有特定的位表示,因此不能出現在著色器輸入 / 輸出或資源中。 SPIR-V 中存在同樣的限制,我們希望能夠在生成的 SPIR-V 程式碼中使用 OpTypeBool。 WHLSL 還包括較小的整數型別的 char,uchar,short 和 ushort,可以直接在 Metal Shading Language 中使用,可以在 SPIR-V 中通過在 OpTypeFloat 中指定 16 來指定,並且可以在 HLSL 中進行模擬。這些型別的模擬比 double 型別的模擬更快,因為型別更小並且它們的位表示不那麼複雜。
WHLSL 不提供 C 風格的隱式轉換。我們發現隱式轉換是著色器中常見的錯誤來源,並且迫使程式設計師明確轉換髮生的位置,這消除了這種經常令人沮喪和神祕的錯誤。這是一種類似於 Swift 等語言的方法。此外,缺少隱式轉換使規範和編譯器變得簡單。
就像在 HLSL 中一樣,WHLSL 有向量型別和矩陣型別,例如 float4 和 int3x4。我們選擇保持標準庫簡單,而不是新增一堆 “x1” 單元素向量和矩陣,因為單元素向量已經可以表示為標量,單元素矩陣已經可以表示為向量。這與消除隱式轉換的願望一致,並且要求 float1 和 float 之間的顯式轉換,float 是麻煩且不必要的冗長的。
因此,以下是著色器的有效片段:
int a = 7; a += 3; float3 b = float3(float(a) * 5, 6, 7); float3 c = b.xxy; float3 d = b * c;複製程式碼
我之前提到過,不允許隱式轉換,但你可能已經注意到在上面的程式碼片段中,5 並未寫為 5.0。這是因為文字表示為可與其他數字型別統一的特殊型別。當編譯器看到上面的程式碼時,它知道乘法運算子要求引數型別相同,第一個引數顯然是浮點數。所以,當編譯器看到 float(a)* 5 時,它說 “好吧,我知道第一個引數是一個浮點數,這意味著我必須使用(浮點數,浮點數)過載,所以讓我們用第二個引數統一 5,因此 5 變為浮點數。“即使兩個引數都是文字,這也有效,因為文字有一個首選型別。因此,5 * 5 將獲得(int,int)過載,5u * 5u 將獲得(uint,uint)過載,5.0 * 5.0 將獲得(float,float)過載。
WHLSL 和 C 之間的一個區別是 WHLSL 在其宣告站點對所有未初始化的變數進行零初始化。這可以防止跨作業系統和驅動程式的不可移植行為——甚至更糟糕的是,在著色器開始執行之前讀取頁面的任何值。這也意味著 WHLSL 中的所有可構造型別都具有零值。
列舉
因為列舉不會產生任何執行時成本並且非常有用,所以 WHLSL 本身支援它們。
enum Weekday { Monday, Tuesday, Wednesday, Thursday, PizzaDay }複製程式碼
列舉的基礎型別預設為 int,但你可以覆蓋型別,例如,列舉 Weekday:uint。 類似地,列舉值可以具有基礎值,例如 Tuesday = 72. 因為列舉已經定義了型別和值,因此它們可以在緩衝區中使用,並且它們可以在基礎型別和列舉型別之間進行轉換。 當你想在程式碼中引用一個值時,你可以像在 C++ 中使用列舉一樣直接使用 Weekday.PizzaDay。 這意味著列舉值不會汙染全域性名稱空間,獨立列舉的值也不會發生衝突。
結構
WHLSL 中的結構與 HLSL 和 C 類似。
struct Foo { int x; float y; }複製程式碼
設計簡單,它們可以避免繼承,虛擬方法和訪問控制。 擁有結構的 “私有” 成員是不可能的。 由於結構體沒有訪問控制,因此結構體不需要具有成員函式。 自由函式可以看到每個結構的每個成員。
陣列
與其他著色語言一樣,陣列是通過值傳遞和返回函式的值型別(也稱為 “copy-in copy-out”,類似於常規標量)。 使用以下語法可以建立一個:
int[3] x;複製程式碼
就像任何變數宣告一樣,這將零填充陣列的內容,因此是 O(n) 操作。 我們希望將括號放在型別後面而不是變數名後面,原因有兩個:
-
將所有型別資訊放在一個地方使得解析器更簡單(避免順時針 / 螺旋規則)
-
在單個語句中宣告多個變數時避免歧義(例如 int [10] x,y;)
我們確保語言安全的一個關鍵方法是對每個陣列訪問執行邊界檢查。 我們通過多種方式使這種潛在的昂貴操作變得高效。 陣列索引是 uint,它將檢查減少到單個比較。 陣列沒有稀疏實現,並且包含一個在編譯時可用的長度成員,使訪問成本接近於零。
陣列是值型別,而 WHLSL 使用另外兩種型別實現引用語義:安全指標和陣列引用。
安全指標 第一個是安全指標。某些形式的引用語義(行為指標允許)幾乎用於每種 CPU 端程式語言。在 WHLSL 中包含指標將使開發人員更容易將現有的 CPU 端程式碼遷移到 GPU,從而可以輕鬆移植諸如機器學習,計算機視覺和訊號處理應用之類的東西。
為了滿足安全要求,WHLSL 使用安全指標,保證指向有效或無效的指標。與 C 一樣,你可以使用&運算子建立指向左值的指標,並可以使用 * 運算子取消引用。與 C 不同,你不能通過指標索引 - 如果它是一個數組。您不能將其轉換為標量值,也不能使用特定的位模式表示。因此,它不能存在於緩衝區中或作為著色器輸入/輸出。
就像在 OpenCL 和 Metal Shading Language 中一樣,GPU 具有不同的堆,或者可以存值的地址空間。 WHLSL 有 4 種不同的堆:裝置,常量,執行緒組和執行緒。所有引用型別都必須使用它們指向的地址空間進行標記。
裝置地址空間對應於裝置上的大部分記憶體。該儲存器是可讀寫的,對應於 Direct3D 中的無序訪問檢視和 Metal Shading Language 中的裝置儲存器。常量地址空間對應於儲存器的只讀區域,通常針對廣播到每個執行緒的資料進行優化。因此,寫入存在於常量地址空間中的左值是編譯錯誤。最後,執行緒組地址空間對應於可讀寫的記憶體區域,該區域線上程組中的每個執行緒之間共享。它只能用於計算著色器。
預設情況下,值存在於執行緒地址空間中:
int i = 4; thread int* j = &i; *j = 7; // i is now 7複製程式碼
因為所有變數都是零初始化的,所以指標是空初始化的。 因此,以下內容有效:
thread int* i;複製程式碼
嘗試取消引用此指標將導致陷阱或鉗位,如稍後所述。
陣列引用
陣列引用類似於指標,但它們可以與下標運算子一起使用,以訪問陣列引用中的多個元素。 雖然陣列的長度在編譯時是已知的,並且必須在型別宣告中宣告,但陣列引用的長度僅在執行時已知。 就像指標一樣,它們必須與地址空間相關聯,並且它們可能是 nullptr。 就像陣列一樣,它們使用 uint 進行索引以進行單比較邊界檢查,並且它們不能是稀疏的。
它們對應於 SPIR-V 中的 OpTypeRuntimeArray 型別以及 HLSL 中的 Buffer,RWBuffer,StructuredBuffer 或 RWStructuredBuffer 之一。 在 Metal 中,它表示為指標和長度的元組。 就像陣列訪問一樣,所有操作都是根據陣列引用的長度進行檢查的。 緩衝區通過陣列引用或指標傳遞到 API 的入口點。
你可以使用 @ 運算子從左值進行陣列引用:
int i = 4; thread int[] j = @i; j[0] = 7; // i is 7 // j.length is 1複製程式碼
正如你所料,在指標 j 上使用 @ 會建立一個指向與 j 相同的陣列引用:
int i = 4; thread int* j = &i; thread int[] k = @j; k[0] = 7; // i is 7 // k.length is 1複製程式碼
在陣列上使用 @ 使陣列引用指向該陣列:
int[3] i = int[3](4, 5, 6); thread int[] j = @i; j[1] = 7; // i[1] is 7 // j.length is 3複製程式碼
函式
函式看起來與 C 的函式非常相似。 例如,這是標準庫中的一個函式:
float4 lit(float n_dot_l, float n_dot_h, float m) { float ambient = 1; float diffuse = max(0, n_dot_l); float specular = n_dot_l < 0 || n_dot_h < 0 ? 0 : n_dot_h * m; float4 result; result.x = ambient; result.y = diffuse; result.z = specular; result.w = 1; return result; }複製程式碼
此示例顯示了類似 WHLSL 函式與 C 的相似之處:函式宣告和呼叫(例如,對於 max())具有相似的語法,引數和引數按順序成對匹配,並且支援三元表示式。
操作符和操作符過載 但是,這裡也有其他事情發生。 當編譯器看到 n_dot_h * m 時,它本質上不知道如何執行該乘法。 相反,編譯器會將其轉換為對 operator() 的呼叫。 然後,通過標準函式過載決策演算法選擇特定運算子執行。 這很重要,因為這意味著你可以編寫自己的 operator*() 函式,並教 WHLSL 如何將你自己的型別相乘。
這甚至適用於像 ++ 這樣的操作。 雖然前後增量有不同的行為,但它們都被過載到同一個函式:operator++()。 以下是標準庫中的示例:
int operator++(int value) { return value + 1; }複製程式碼
將呼叫此操作符以進行預增量和後增量,並且編譯器足夠智慧以對結果執行正確的操作。 這解決了 C++ 執行到這些運算子不同的地方的問題,並使用額外的偽 int 引數進行區分。 對於後遞增,編譯器將發出程式碼以將值儲存到匿名變數,呼叫 operator++(),賦值結果,並使用儲存的值進行進一步處理。
整個語言都使用了操作符過載。 這就是實現向量和矩陣乘法的方式。 這是陣列索引的方式。 這是混合運算子的工作方式。 運算子過載提供了功能和簡單性; 核心語言不必直接瞭解每個操作,因為它們是由過載的運算子實現的。
生成屬性
但是,WHLSL 並不僅僅停留在運算子的超載上。 前面的例子包括 b.xxy,其中 b 是 float3。 這是一個表示式,意思是 “製作一個 3 元素向量,其中前兩個元素具有與 bx 相同的值,第三個元素具有相同的值”,所以它有點像向量的成員,除了它不是 ' 實際上與任何儲存相關聯; 相反,它是在訪問期間計算的。 這些 “混合操作符” 存在於每種實時著色語言中,WHLSL 也不例外。 它們的支援方式是將它們標記為生成的屬性,就像在 Swift 中一樣。
Getters
標準庫包含以下形式的許多功能:
float3 operator.xxy(float3 v) { float3 result; result.x = v.x; result.y = v.x; result.z = v.y; return result; }複製程式碼
當編譯器看到對不存在的成員的屬性訪問時,它可以呼叫傳遞物件作為第一個引數的運算子。 通俗地說,我們稱之為 getter。
Setters
同樣的方法甚至適用於 setter:
float4 operator.xyz=(float4 v, float3 c) { float4 result = v; result.x = c.x; result.y = c.y; result.z = c.z; return result; }複製程式碼
使用 setter 非常自然:
float4 a = float4(1, 2, 3, 4); a.xyz = float3(7, 8, 9);複製程式碼
setter 的實現使用新資料建立物件的副本。 當編譯器看到對生成的屬性的賦值時,它會呼叫 setter 並將結果賦給原始變數。
Anders
getter 和 setter 的泛化是 ander,它與指標一起使用。 它作為效能優化存在,因此 setter 不必建立物件的副本。 這是一個例子:
thread float* operator.r(thread Foo* value) { return &value->x; }複製程式碼
Anders 比 getter 或 setter 更強大,因為編譯器可以使用 anders 來實現讀取或賦值。 當通過 ander 從生成的屬性讀取時,編譯器呼叫 ander 然後取消引用結果。 寫入時,編譯器呼叫 ander,取消引用結果,並把結果賦值給它。 任何使用者定義的型別都可以包含 getter,setter,anders 和 indexer 的任意組合; 如果相同型別具有 ander 以及 getter 或 setter,編譯器將更喜歡使用 ander。
Indexers
但是矩陣怎麼樣? 在大多數實時著色語言中,不會使用與其列或行對應的成員訪問矩陣。 相反,它們是使用陣列語法訪問的,例如 myMatrix 的 3。 向量型別通常也有這種語法。 那怎麼辦? 更多運算子超載!
float operator[](float2 v, uint index) { switch (index) { case 0: return v.x; case 1: return v.y; default: /* trap or clamp, more on this below */ } } float2 operator[]=(float2 v, uint index, float a) { switch (index) { case 0: v.x = a; break; case 1: v.y = a; break; default: /* trap or clamp, more on this below */ } return v; }複製程式碼
如你所見,索引也使用運算子,因此可能會過載。 向量也獲得這些 “索引器”,因此 myVector.x 和 myVector [0] 是彼此的同義詞。
標準庫
我們基於描述 HLSL 標準庫的 Microsoft Docs 設計了標準庫。 WHLSL 標準庫主要包括數學運算,它既可以處理標量值,也可以處理向量和矩陣的元素。 定義了您期望的所有標準運算子,包括邏輯運算和按位運算,如 operator*() 和 operator<<()。 在適用的情況下,為向量和矩陣定義所有混合運算子,getter 和 setter。
WHLSL 的設計原則之一是保持語言本身很小,以便儘可能在標準庫中定義。 當然,並非標準庫中的所有函式都可以用 WHLSL 表示(如 bool 運算子 *(float,float)),但幾乎所有函式都在 WHLSL 中實現。 例如,此函式是標準庫的一部分:
float smoothstep(float edge0, float edge1, float x) { float t = clamp((x - edge0) / (edge1 - edge0), 0, 1); return t * t * (3 - 2 * t); }複製程式碼
由於標準庫旨在儘可能匹配 HLSL,因此其中的大多數函式已直接存在於 HLSL 中。因此,對 HLSL 的 WHLSL 標準庫的彙編將選擇忽略這些函式,而是使用內建版本。例如,對於所有向量/矩陣索引器都會發生這種情況 - GPU 永遠不會真正看到上面的程式碼; 編譯器中的程式碼生成步驟應該使用內在代替。但是,不同的著色語言具有不同的內建函式,因此每個函式都被定義以允許正確性測試。類似地,WHLSL 包括一個 CPU 端直譯器,它在執行 WHLSL 程式時使用這些函式的 WHLSL 實現。對於包括紋理取樣函式在內的每個 WHLSL 函式都是如此。
並非 WHLSL 中存在 HLSL 標準庫中的每個功能。例如,HLSL 支援 printf()。但是,在 Metal Shading Language 或 SPIR-V 中實現這樣的功能將非常困難。我們在 HLSL 標準庫中包含儘可能多的函式,這在 Web 環境中是合理的。
可變壽命(Variable Lifetime)
但如果語言中有指標,我們應該如何處理自由使用後的問題? 例如,請考慮以下程式碼段:
thread int* foo() { int a; return &a; } … int b = *foo();複製程式碼
在像 C 這樣的語言中,此程式碼具有未定義的行為。因此,一種解決方案是 WHLSL 只是禁止這種結構,並在看到類似這樣的東西時丟擲編譯錯誤。但是,這需要跟蹤每個指標可能指向的值,這在存在迴圈和函式呼叫時是一個困難的分析。相反,WHLSL 使每個變數的行為就像它具有全域性生命週期一樣。
這意味著此 WHLSL 程式碼段完全有效並且定義明確,原因有兩個:
宣告沒有初始值設定項將對其進行零填充。因此,a 的值是明確定義的。每次呼叫 foo() 時都會發生這種零填充。所有變數都具有全域性生命週期(類似於 C 的靜態關鍵字)。因此,永遠不會超出範圍。
這種全域性生命週期是唯一可能的,因為不允許遞迴(這對於著色語言來說很常見),這意味著不存在任何重入問題。類似地,著色器無法分配或釋放記憶體,因此編譯器在編譯時知道著色器可能訪問的每個記憶體塊。
所以,例如:
thread int* foo() { int a; return &a; } … thread int* x = foo(); *x = 7; thread int* y = foo(); // *x equals 0, because the variable got zero-filled again *y = 8; // *x equals 8, because x and y point to the same variable複製程式碼
大多數變數不需要真正全域性化,因此對效能沒有太大影響。 如果編譯器可以證明特定變數是否實際具有全域性生存期是不可觀察的,則編譯器可以自由地將變數保持為本地變數。 因為在其他語言中不鼓勵返回指向本地的指標的模式(事實上,許多其他著色語言甚至沒有指標),像這樣的例子將是相對罕見的。
編譯階段
WHLSL 不像其他語言那樣使用前處理器。在其他語言中,前處理器的主要目的是將多個原始檔包含在一起。但是,在 Web 上,沒有直接檔案訪問許可權,通常整個著色器顯示在一個已下載的資源中。在許多著色語言中,前處理器用於在大型 ubershader 中有條件地啟用渲染功能,但 WHLSL 通過使用特化常量來允許此用例。此外,前處理器的許多變體以微妙的方式不相容,因此對 WHLSL 來說,一個前處理器的好處不會超過為它建立規範的複雜性。
WHLSL 專為兩階段編譯而設計。在我們的研究中,我們發現許多 3D 引擎想要編譯大型著色器,每個編譯包括在不同編譯之間重複的大型函式庫。不是多次編譯這些支援函式,更好的解決方案是一次編譯整個庫,然後允許第二階段選擇應該一起使用庫中的哪些入口點。
這個兩階段編譯意味著儘可能多地在第一遍中完成編譯,因此對於著色器系列不會多次執行。這就是 WHLSL 中的入口點被標記為頂點,片段或計算的原因。讓編譯的第一階段知道哪些函式是哪種型別的入口點讓更多的編譯發生在第一階段而不是第二階段。
第二個編譯階段還提供了指定特化常量的便利位置。回想一下,WHLSL 沒有前處理器,這是在 HLSL 中啟用和禁用功能的傳統方式。引擎通常通過啟用渲染效果或通過翻轉開關切換 BRDF 來為特定情況定製單個著色器。將每個渲染選項包含在單個著色器中的技術,以及基於啟用哪種效果來專門設定單個著色器的技術是如此常見,它有一個名稱:ubershaders。 WHLSL 程式設計師可以使用特殊化常量而不是前處理器巨集,它們的工作方式與 SPIR-V 的特化常量相同。從語言的角度來看,它們只是標量常量。但是,在第二個編譯階段提供了這些常量的值,這使得在執行時配置程式變得非常容易。
由於單個 WHLSL 程式可以包含多個著色器,因此著色器的輸入和輸出不會像其他著色語言那樣由全域性變量表示。相反,特定著色器的輸入和輸出與該著色器本身相關聯。輸入表示為著色器入口點的引數,輸出表示為入口點的返回值。
以下顯示瞭如何描述計算著色器入口點:
compute void ComputeKernel(device uint[] b : register(u0)) { … }複製程式碼
安全性
WHLSL 是一種安全的語言。這意味著無法訪問網站原點以外的資訊。 WHLSL 實現此目的的方法之一是消除未定義的行為,如上文關於均勻性所述。
WHLSL 實現安全性的另一種方式是執行陣列/指標訪問的邊界檢查。這些邊界檢查可能有三種方式:
1. Trapping。當程式中出現陷阱時,著色器階段會立即退出,為所有著色器階段的輸出填充 0。繪製呼叫繼續,圖形管道的下一個階段將執行。
因為陷印引入了新的控制流程,所以它對程式的一致性有影響。Trap 在邊界檢查內發出,這意味著它們必然存在於非均勻控制流中。對於某些不使用均勻性的程式可能沒問題,但一般來說這會使 trap 難以使用。
2. Clamping。陣列索引操作可以將索引限制為陣列的大小。這不涉及新的控制流程,因此它對均勻性沒有任何影響。甚至可以通過忽略寫入併為讀取返回 0 來 “clap” 指標訪問或零長度陣列訪問。這是可能的,因為你可以用 WHLSL 中的指標做的事情是有限的,所以我們可以簡單地讓每個操作用一個 “clamped” 指標做一些明確定義的事情。硬體和驅動程式支援。某些硬體和驅動程式已經包含一種不會發生越界訪問的模式。使用此方法,硬體禁止越界訪問的機制是實現定義的。一個例子是 ARB_robustness OpenGL 擴充套件。不幸的是,WHLSL 應該可以在幾乎所有現代硬體上執行,而且沒有足夠的 API / 裝置支援這些模式。
無論編譯器使用哪種方法,都不應影響著色器的均勻性; 換句話說,它不能將其他有效的程式變成無效的程式。
為了確定邊界檢查的最佳行為,我們進行了一些效能實驗。我們採用了 Metal Performance Shaders 框架中使用的一些核心,並建立了兩個新版本:一個使用 clamp,另一個使用 trap。我們選擇的核心是那些進行大量陣列訪問的核心:例如,乘以大型矩陣。我們在不同資料大小的各種裝置上執行此基準測試。我們確保沒有任何 trap 實際被擊中,並且沒有任何 clamp 實際上有任何影響,因此我們可以確定我們正在測量正確編寫的程式的常見情況。
我們期望 trap 更快,因為下游編譯器可以消除冗餘 trap。但是,我們發現沒有一個明顯的贏家。在某些器件上,trap 明顯快於 clamp,而在其他器件上,clamp 明顯快於 trap。這些結果表明編譯器應該能夠選擇哪種方法最適合它執行的特定裝置,而不是被迫總是選擇一種方法。
Shader 標識 WHLSL 支援 HLSL 的語言特性,稱為 “語義”。它們用於標識著色器階段和 WebGPU API 之間的變數。語義有四種類型:
-
內建變數,例如 uint vertexID:SV_VertexID
-
專精常數,例如 uint numlights:專門的
-
階段輸入 / 輸出語義,例如 float2 座標:屬性(0)
-
資源語義,例如 device float [] 座標:暫存器(u0)
如上所述,WHLSL 程式以函式引數的形式接受其輸入和輸出,而不是全域性變數。
但是,著色器通常具有多個輸出。最常見的例子是頂點著色器將多個輸出值傳遞給插值器,以作為輸入提供給片段著色器。
為了適應這種情況,著色器的返回值可以是結構,並且各個欄位是獨立處理的。實際上,這是遞迴工作的 - 結構可以包含另一個結構,其成員也可以獨立處理。巢狀的結構被展平,並且所有非結構化的欄位都被收集並視為著色器輸出。
著色器引數的工作方式相同。單個引數可以是著色器輸入,也可以是具有著色器輸入集合的結構。結構也可以包含其他結構。這些結構中的變數是獨立處理的,就好像它們是著色器的附加引數一樣。
在將所有這些結構扁平化為一組輸入和一組輸出之後,集合中的每個專案都必須具有語義。每個內建變數必須具有特定型別,並且只能在特定著色器階段使用。專精常量必須只有簡單的標量型別。
階段輸入/輸出變數具有屬性語義而不是傳統的 HLSL 語義,因為許多著色器傳遞的資料與 HLSL 提供的預設語義不匹配。在 HLSL 中,通常會將通用資料打包到 COLOR 語義中,因為 COLOR 是 float4,資料適合 float4。相反,SPIR-V 和金屬著色語言(通過 [[user(n)]])的方法是為每個階段輸入 / 輸出變數分配一個識別符號,並使用賦值來匹配著色器階段之間的變數。
HLSL 程式設計師應該熟悉資源語義。 WHLSL 包括資源語義和地址空間,但這兩者具有不同的用途。變數的地址空間用於確定應在其中訪問哪個快取和記憶體層次結構。地址空間是必要的,因為它甚至通過指標操作仍然存在;裝置指標不能設定為指向執行緒變數。在 WHLSL 中,資源語義僅用於標識 WebGPU API 中的變數。但是,為了與 HLSL 保持一致,資源語義必須 “匹配” 它所放置的變數的地址空間。例如,你不能在 texture 上放置暫存器(s0)。你不能將暫存器(u0)放在常量資源上。 WHLSL 中的陣列沒有地址空間(因為它們是值型別,而不是引用型別),因此如果陣列顯示為著色器引數,則將其視為用於匹配語義的裝置資源。
就像 Direct3D 一樣,WebGPU 有一個兩級繫結模型。資源描述符聚合成集,並且可以在 WebGPU API 中切換集。 WHLSL 通過在資源語義內部通過可選空間引數對其進行建模來匹配 HLSL:register(u0,space1)。
“邏輯模式”限制 WHLSL 的設計要求可以與 Metal Shading Language,SPIR-V 和 HLSL(或 DXIL)相容。 SPIR-V 具有許多不同的操作模式,以不同的嵌入 API 為目標。具體來說,我們對 Vulkan 所針對的 SPIR-V 的味道感興趣。
這種 SPIR-V 的味道是 SPIR-V 的味道,稱為邏輯定址模式。在 SPIR-V 邏輯模式中,變數不能具有指標型別。類似地,指標不能用於 Phi 操作。結果是每個指標必須始終指向一件事;指標只是值的名稱。
因為 WHLSL 需要與 SPIR-V 相容,所以 WHLSL 必須比 SPIR-V 更具表現力。因此,WHLSL 在 SPIR-V 邏輯模式中有一些限制使其可以表達。這些限制並未作為 WHLSL 的可選模式浮出水面;相反,它們是語言本身的一部分。最終,我們希望在將來的語言版本中可以解除這些限制,但在此之前,語言受到限制。
這些限制是:
指標和陣列引用不得出現在裝置,常量或執行緒組記憶體中指標和陣列引用不得出現在陣列或陣列引用中指標和陣列引用不得在其初始化程式之外分配(在其宣告中)返回指標或陣列引用的函式只能有一個返回點三元表示式不能產生指標有了這些限制,編譯器就會確切地知道每個指標指向的內容。
但不是那麼快!回想一下,執行緒變數具有全域性生命週期,這意味著它們的行為就像它們是在入口點的開頭宣告的那樣。如果執行時將所有這些區域性變數收集在一起,按型別排序,並將具有相同型別的所有變數聚合到陣列中,該怎麼辦?然後,指標可以簡單地是適當陣列的偏移量。在 WHLSL 中,指標不能重新指向不同的型別,這意味著編譯器會靜態確定相應的陣列。因此,執行緒指標不需要遵守上述限制。但是,這種技術不適用於其他地址空間中的指標;它只適用於執行緒指標。
資源
WHLSL 支援緩衝區的 texture,取樣器和陣列引用。就像在 HLSL 中一樣,WHLSL 中的紋理型別看起來像 Texture2D <float4>。這些尖括號的存在並不意味著模板或泛型;該語言沒有那些設施(為簡單起見)。允許使用它們的唯一型別是一組有限的內建型別。這種設計是允許這些型別(介於 HLSL 中)之間的中間地帶,但也允許以社群組可以使用尖括號字元的方式進一步開發語言。
深度 textures 與非深度 textures 不同,因為它們是 Metal Shading Language 中的不同型別,因此編譯器需要知道在發出金屬著色語言時要發出哪一個。因為 WHLSL 不支援成員函式,所以 textures 取樣不像 texture.Sample(...) ;相反,它是使用像 Sample(texture,...) 這樣的自由函式完成的。
取樣器不專業;所有用例都有一個取樣器型別。你可以將此取樣器用於深度 textures 和非深度 textures。深度 textures 支援取樣器中的比較操作等內容。如果取樣器配置為包含深度比較並且它與非深度 textures 一起使用,則忽略深度操作。
WebGPU API 將在特定位置自動發出一些資源障礙,這意味著 API 需要知道著色器中使用了哪些資源。因此,不能使用 “無約束” 的資源模型。這意味著所有資源都被列為著色器的顯式輸入。類似地,API 想知道哪些資源用於讀取以及哪些資源用於寫入;編譯器通過檢查程式來靜態地知道這一點。 “const” 沒有語言級支援,或者 StructuredBuffer 和 RWStructuredBuffer 之間沒有區別,因為該資訊已經存在於程式中。
當前進展
WebGPU 社群小組正在研究用 OTT 編寫的正式語言規範,該規範描述了 WHLSL 與其他 Web 語言採用的嚴格程度。 我們還在研究可以生成金屬著色語言,SPIR-V 和 HLSL 的編譯器。 此外,編譯器還包括一個 CPU 端直譯器,以顯示實現的正確性。 請試一試!
未來方向
WHLSL 還處於初級階段,在語言設計完成之前還有很長的路要走。我們很樂意聽取您的意見,疑慮和用例!請隨時在我們的 GitHub 儲存庫中提出有關您的想法和想法的問題!
對於第一個提案,我們希望滿足本文開頭概述的約束,同時為擴充套件語言提供充分的機會。語言的一種自然演變可以為型別的抽象新增設施,例如協議或介面。 WHLSL 包含沒有訪問控制或繼承的簡單結構。其他著色語言如 Slang 模型型別抽象作為必須存在於結構內的一組方法。但是,Slang 遇到了一個問題,即無法使現有型別遵循新介面。定義結構後,就無法向其中新增新方法;花括號永遠關閉了結構。這個問題通過擴充套件來解決,類似於 Objective-C 或 Swift,它可以在定義結構後追溯地將方法新增到結構中。 Java 通過鼓勵作者新增新類(稱為介面卡)來解決這個問題,這些類只存在於實現介面,並將每個呼叫連線到實現型別。
WHLSL 方法簡單得多;通過使用自由函式而不是結構方法,我們可以使用像 Haskell 型別類這樣的系統。這裡,型別類定義了一組必須存在的任意函式,型別通過實現它們來遵守型別類。這樣的解決方案可能會在未來新增到該語言中。
總結
這描述了 W3C 的 WebGPU 社群組擁有的名為 WHLSL 的新著色語言。它熟悉的基於 HLSL 的語法,安全保證和簡單,可擴充套件的設計滿足了該語言的目標。因此,它代表了編寫在 WebGPU API 中使用的著色器的最佳支援方式。但是,WebGPU 社群組不確定是否應直接向 WebGPU API 提供 WHLSL 程式,或者是否應在交付給 API 之前將它們編譯為中間形式。無論哪種方式,WebGPU 程式設計師都應該使用 WHLSL 編寫,因為它最適合 API。
請加入!我們正在 WebGPU GitHub 專案上做這項工作。我們一直在研究語言的正式規範,發出金屬著色語言和 SPIR-V 的參考編譯器,以及用於驗證正確性的 CPU 端直譯器。我們歡迎大家嘗試一下,讓我們知道它是怎麼回事!
欲瞭解更多資訊,您可以通過 [email protected] 或 @Litherum 與我聯絡,或者您可以聯絡我們的佈道者 Jonathan Davis。
英文原文:https://webkit.org/blog/8482/web-high-level-shading-language/
“UC國際技術” 致力於與你共享高質量的技術文章
歡迎微信搜尋 UC國際技術 關注我們的公眾號,或者將文章分享給你的好友