Webkit 推出新的著色語言 whlsl
本文將介紹一門叫作 Web High Level Shading Language(WHLSL,發音為“whistle”)的新 Web 圖形著色語言。它對 HLSL 進行了擴充套件,變得更安全、更可靠。
背景
在過去的幾十年中,3D 圖形已經發生了重大變化,程式員用來編寫 3D 應用程式的 API 也發生了相應的變化。
五年前,最先進的圖形應用程式使用 OpenGL 來執行渲染。然而,在過去幾年中,3D 圖形行業正朝著更新、更低級別的圖形框架轉變,這些框架與真實硬體的行為更加貼合。2014 年,Apple 推出了 Metal 框架,讓 iOS 和 macOS 應用程式可以充分利用 GPU。2015 年,微軟推出了 Direct3D 12,這是 Direct3D 的一個重大更新,帶來了控制檯級的渲染和計算效率。2016 年,Khronos Group 釋出了 Vulkan API,主要用於 Android,也具備了類似的優勢。
去年,Apple 在 W3C 內部成立了 WebGPU 社群組,致力於標準化新的 3D 圖形 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。
WHLSL
WHLSL 是一門適用於 Web 平臺的新著色語言。它由 W3C 的 WebGPU 社群組開發,這個開發組正忙於制定規範、開發編譯器和 CPU 端直譯器。
WHLSL 以 HLSL 為基礎,並對其進行了簡化和擴充套件。WHLSL 是一門功能強大且富有表現力的著色語言,帶來了安全性和其他好處。
語言基礎
與 HLSL 中一樣,WHLSL 的原始資料型別包括 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 語言風格的隱式轉換。 我們發現隱式轉換是著色器中常見的錯誤來源。此外,避免隱式轉換使規範和編譯器變得更簡單。
與 HLSL 中一樣,WHLSL 也有向量型別和矩陣型別,例如 float4 和 int3x4。我們儘量保持標準庫簡單,所以沒有新增一堆“x1”單元素向量和矩陣,因為單元素向量已經可以表示為標量,單元素矩陣已經可以表示為向量。這符合我們消除隱式轉換的願望,在 float1 和 float 之間進行顯式轉換是件麻煩且不必要的事情。
以下是有效的著色器片段:
複製程式碼
int a = 7 ; |
a += 3 ; |
float 3 b = float 3( float (a) * 5 , 6 , 7 ); |
float 3 c = b.xxy; |
float 3 d = b * c; |
我之前提到過,WHLSL 不支援隱式轉換,但你可能已經注意到,在上面的程式碼片段中,5 並未寫為 5.0。這是因為字面量表示為可與其他數字型別統一的特殊型別。當編譯器看到上面的程式碼時,它知道乘法運算子要求引數型別相同,第一個引數顯然是浮點數。所以,當編譯器看到 float(a) * 5 時,它說“好吧,我知道第一個引數是一個浮點數,我必須使用 (float, float) 過載,所以讓我們把第二個引數也變為浮點數”。即使兩個引數都是字面量也是一樣,因為字面量有一個首選型別。因此,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,但你可以進行型別覆蓋,例如 enum Weekday : uint。類似地,列舉的值可以具有基礎值,例如 Tuesday = 72。因為列舉已經定義了型別和值,因此它們可以被用在緩衝區中,並且可以在基礎型別和列舉型別之間進行轉換。當你想在程式碼中引用一個值時,可以像 Weekday.PizzaDay 這樣。這意味著列舉值不會汙染全域性名稱空間,列舉的值也不會發生衝突。
結構體
WHLSL 中的結構與 HLSL 和 C 語言類似。
複製程式碼
struct Foo { |
int x; |
float y; |
} |
結構體設計簡單,避免了繼承、虛擬方法和訪問控制。結構體沒有“私有”成員。因為結構體沒有訪問控制,所以不需要成員函式。
陣列
與其他著色語言一樣,陣列是可以傳給函式或從函式中返回的值型別。你可以使用以下語法建立一個數組:
複製程式碼
int [ 3 ] x;
與任何變數宣告一樣,陣列內容將使用零進行填充。我們將括號放在型別後面而不是變數名後面,有兩個原因:
- 將所有型別資訊放在一個地方可以讓解析更簡單(避免順時針 / 螺旋規則);
- 在單個語句中宣告多個變數時可以避免歧義(例如 int[10] x,y;)。
陣列是值型別,而 WHLSL 使用另外兩種型別實現引用語義:安全指標和陣列引用。
安全指標
某種形式的引用語義,幾乎被用在每一種 CPU 端程式語言中。在 WHLSL 中包含指標將使開發人員更容易將現有的 CPU 端程式碼遷移到 GPU,從而可以輕鬆移植機器學習、計算機視覺和訊號處理應用程式之類的東西。
為了滿足安全要求,WHLSL 使用了安全指標,保證指向有效的東西,或者為 null。與 C 語言一樣,你可以使用 & 運算子建立指向 lvalue 的指標,並可以使用 * 運算子取消引用。與 C 語言不同的是,你不能像陣列那樣對指標進行索引。你不能將其與標量之間進行轉換,也不能使用特定的位模式表示。因此,它不能出現在緩衝區中或作為著色器輸入 / 輸出。
WHLSL 有 4 種不同的堆:device、constant、threadgroup 和 thread。所有的引用型別都必須使用它們指向的地址空間進行標記。
device 地址空間對應於裝置上的大部分記憶體。記憶體是可讀寫的,對應於 Direct3D 中的無序訪問檢視以及 Metal Shading Language 中的 device 記憶體。constant 地址空間對應於記憶體的只讀區域,通常針對廣播到每個執行緒的資料進行優化。最後,threadgroup 地址空間對應於可讀寫的記憶體區域,該區域被執行緒組的每個執行緒共享。它只能用於計算著色器。
預設情況下,值存在於 thread 地址空間中:
複製程式碼
int i = 4 ; |
thread int * j = &i; |
*j = 7 ; |
// i is now 7 |
因為所有變數都使用零值初始化,所以指標是 null 初始化的。因此,以下的宣告是有效的:
複製程式碼
thread int* i ;
陣列引用
陣列引用類似於指標,但它們可以與下標運算子一起使用,以訪問陣列引用中的多個元素。雖然陣列的 length 在編譯時是已知的,並且必須在型別宣告中指明,但陣列引用的 length 要在執行時才能知道。與指標一樣,它們必須與地址空間相關聯,並且可能會是 nullptr。與陣列一樣,它們使用 uint 進行索引,以進行單比較邊界檢查,並且不能是稀疏的。
你可以使用 @運算子為 lvalue 建立陣列引用:
複製程式碼
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 ofollow,noindex"> 3] i = int [ 3 ; |
thread int [] j = @i; |
j[ 1 ] = 7 ; |
// i[1] is 7 |
// j.length is 3 |
函式
WHLSL 的函式與 C 語言中的函式非常相似。例如,這是標準庫中的一個函式:
複製程式碼
float 4 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; |
float 4 result; |
result.x = ambient; |
result.y = diffuse; |
result.z = specular; |
result.w = 1 ; |
return result; |
} |
運算子和運算子過載
當編譯器看 到 n_dot_h * m
時,它並不知道如何執行這個乘法。編譯器會將其轉換為對 operator*()
的呼叫。然後,通過標準函式過載決策演算法選擇特定的 operator*()
。這意味著你可以編寫自己的 operator*()
函式,告訴 WHLSL 如何執行自定義型別的乘法。
這同樣適用於像 ++ 這樣的操作。以下是標準庫中的一個示例:
複製程式碼
int operator ++( int value ) { |
return value + 1 ; |
} |
生成屬性
但 WHLSL 並不僅僅停留在運算子過載上。最開始的例子中有個 b.xxy,其中 b 是 float3。這是一個表示式,意思是“建立一個包含 3 個元素的向量,其中前兩個元素具有與 b.x 相同的值,第三個元素具有與 b.y 相同的值”。這有點像是向量的成員,只是沒有與任何儲存相關聯。相反,它是在訪問期間計算生成的。這些“混合運算子”存在於每種實時著色語言中,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 使用起來非常自然:
複製程式碼
float 4 a = float 4( 1 , 2 , 3 , 4 ); |
a.xyz = float 3( 7 , 8 , 9 ); |
Setter 使用新資料建立物件的副本。當編譯器遇到對生成屬性進行賦值時,它會呼叫 Setter,並將結果賦給原始變數。
Anders
Ander 是 Getter 和 Setter 的泛化,可以與指標一起使用。它是對效能的一種優化,這樣 Setter 就不必建立物件的副本。這是一個例子:
複製程式碼
thread float * operator .r(thread Foo* value ) { |
return & value ->x; |
} |
Anders 比 Getter 或 Setter 更強大,因為編譯器可以使用 Ander 來實現讀取或賦值。當通過 Ander 讀取生成屬性時,編譯器呼叫 Ander,然後取消對結果的引用。在寫入時,編譯器也呼叫 Ander,取消對結果的引用,並將結果分配給它。任何使用者定義的型別都可以包含 Getter、Setter、Ander 和 Indexer 的任意組合。如果相同型別具有 Ander 以及 Getter 或 Setter,編譯器將首選 Ander。
Indexers
在大多數實時著色語言中,不會使用與其列或行對應的成員來訪問矩陣。相反,它們使用陣列語法來訪問,例如 myMatrix 的 3 1 。向量型別通常也有這種語法:
複製程式碼
float operator{ |
switch (index) { |
case 0 : |
return v.x; |
case 1 : |
return v.y; |
default : |
/* trap or clamp, more on this below */ |
} |
} |
複製程式碼
float 2 operator[]=( float 2 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; |
} |
可見,索引也使用了運算子,因此可以被過載。向量也有“Indexer”,因此 myVector.x 和 myVector[0] 是互為同義詞。
標準庫
我們基於描述 HLSL 標準庫的 Microsoft Docs 設計了 WHLSL 標準庫。WHLSL 標準庫主要包括數學運算,既可以處理標量值,也可以處理向量和矩陣的元素。標準款定義了你期望的所有標準運算子,包括邏輯運算和按位運算,如 operator*() 和 operator<<()。
WHLSL 的設計原則之一是保持語言本身的小型化,所以儘可能多地在標準庫中定義其他內容。當然,並非標準庫中的所有函式都可以用 WHLSL 表示(如 bool operator*(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 中。但不同的著色語言具有不同的內建函式,因此每個函式定義都允許進行正確性測試。WHLSL 包含了一個 CPU 端直譯器,在執行 WHLSL 程式時將使用這些函式的 WHLSL 實現。
當然,並非出現在 HLSL 標準庫中的每個函式在 WHLSL 中也都會有。例如,HLSL 支援 printf(),但要在 Metal Shading Language 或 SPIR-V 中實現這樣的函式會非常困難。
安全性
WHLSL 是一門安全的語言,這意味著訪問網站以外的資訊是不可能的。WHLSL 通過消除未定義的行為來達到這個目的。
WHLSL 實現安全性的另一種方式是進行陣列 / 指標訪問邊界檢查。邊界檢查有三種方式:
-
Trapping。當程式中出現 trap 時,著色器階段會立即退出,將所有著色器階段的輸出填充為 0。繪製呼叫會繼續,並執行圖形管道的下一個階段。因為 Trapping 引入了新的控制流程,所以對程式的一致性有一定影響。trap 是在邊界檢查內發出的,這意味著它們必然存在於非一致的控制流程中。對於某些不使用一致性的程式可能沒問題,但一般來說這會導致 trap 難以使用。
-
Clamping。陣列索引操作可以將索引限制為陣列大小。這不涉及新的控制流程,因此它對一致性沒有任何影響。甚至可以通過忽略寫入併為讀取返回 0 來“clamp”指標訪問或零長度陣列訪問。這是可能的,因為你可以用 WHLSL 中的指標做的事情是有限的,所以我們可以簡單地讓每個操作用一個“clamp”指標做一些明確定義的事情。
-
硬體和驅動程式支援。某些硬體和驅動程式已經包含一種不會發生越界訪問的模式。ARB_robustness OpenGL 擴充套件就是一個很好的例子。可惜的是,WHLSL 要在幾乎所有現代硬體上執行,所以沒有足夠的 API/ 裝置支援這些模式。
無論編譯器使用哪種方法,都不應影響著色器的一致性。換句話說,它不可能能將有效的程式變成無效的程式。
為了確定邊界檢查的最佳行為,我們進行了一些效能實驗。我們採用了 Metal Performance Shaders 框架中的一些核心,並建立了兩個新版本:一個使用 clamping,另一個使用 traping。我們選擇的核心是那些進行大量陣列訪問的核心:例如,大型矩陣相乘。我們在各種裝置上執行這個基準測試。
我們希望 trapping 能夠更快,因為下游編譯器可以消除冗餘的 trap。但我們發現,在某些裝置上,trapping 明顯快於 clamping,而在其他裝置上,卻是反過來的。這些結果表明,編譯器應該能夠為特定裝置選擇更合適的方法,而不是被迫選擇一種給定的方法。
目前的工作
WebGPU 社群小組正在使用 OTT 編寫正式語言規範。我們還在開發一個可以生成 Metal Shading Language、SPIR-V 和 HLSL 的編譯器。此外,編譯器還包括了一個 CPU 端直譯器,可用於驗證實現的正確性。
未來的發展方向
WHLSL 還處於初級階段,在語言設計完成之前還有很長的路要走。請隨時在我們的 GitHub 儲存庫( https://github.com/gpuweb/WHLSL )中提出你的想法和問題!
英文原文:
https://webkit.org/blog/8482/web-high-level-shading-language/
更多內容,可關注前端之巔公眾號(ID:frontshow)。