寫在最前:歡迎你來到「UC國際技術」公衆號,咱們將爲你們提供與客戶端、服務端、算法、測試、數據、前端等相關的高質量技術文章,不限於原創與翻譯。
2016 年,Khronos Group 發佈了 Vulkan API,主要用於 Android,具備相似的優點。 前端
這些現代 3D 圖形 API 中的每個都使用着色器,WebGPU 也不例外。着色器是利用 GPU 專用架構的程序。特別是,在重型並行數值處理中,GPU 要優於 CPU。爲了利用這兩種架構,現代 3D 應用使用混合設計,使用 CPU 和 GPU 來完成不一樣的任務。經過利用每一個架構的最佳特性,現代圖形 API 爲開發人員提供了一個強大的框架,能夠建立複雜,豐富,快速的 3D 應用程序。專爲 Metal 設計的應用使用 Metal Shading Language,爲 Direct3D 12 設計的應用使用 HLSL,爲 Vulkan 設計的應用使用 SPIR-V 或 GLSL。 程序員
它須要明確指定語言規範。語言規範必須明確是否每一個可能的字符串都是有效的程序。與全部其餘 Web 格式同樣,必須精確指定 Web 的着色語言以保證瀏覽器之間的互操做性。 web
它須要翻譯成 Metal Shading Language,HLSL(或 DXIL)和 SPIR-V。這是由於 WebGPU 被設計爲能同時在 Metal,Direct3D 12 和 Vulkan 之上工做,所以着色器須要可以以以上每一個 API 均可以接受的形式表示。 算法
它須要使用 WebGPU API 進行演變。 WebGPU 功能(如綁定模型和曲面細分模型)與着色語言深度交互。儘管使用獨立於 API 開發的語言是可行的,但在同一論壇中使用 WebGPU API 和着色語言可確保共享目標,並使開發更加簡化。 編程
第二部分是語言應該是人類可讀的。 Web 的文化是任何人均可以用文本編輯器和瀏覽器開始編寫網頁。內容的民主化是 Web 最大的優點之一。這種文化創造了一個豐富的工具和審查員生態系統,修補者能夠經過 View-Source 調查任何網頁的工做方式。使用單一規範的人類可讀語言將極大地幫助社區採用 WebGPU API。 數組
相似地,使用諸如 WebAssembly 之類的字節碼格式並不能避免瀏覽器對源代碼進行優化的須要。每一個主要瀏覽器在執行以前都會在字節碼上運行優化。不幸的是,追求更簡單的編譯器的願望從未結束。 瀏覽器
Metal Shading Language 與 C++ 很是類似,這意味着它具備位轉換和原始指針的全部功能。它很是強大; 甚至能夠爲 CPU 和 GPU 編譯相同的源代碼。將現有的 CPU 端代碼移植到 Metal Shading Language 很是容易。不幸的是,全部這些能力都有一些缺點。例如,在 Metal Shading Language 中,你能夠編寫一個着色器,將指針轉換爲整數,添加 17,將其強制轉換回指針,而後取消引用它。這是一個安全問題,由於它意味着着色器能夠訪問剛好位於應用程序地址空間中的任何資源,這與 Web 的安全模型相反。從理論上講,能夠指定一個沒有原始指針的 Metal Shading Language,但指針對於 C 和 C++ 語言來講是如此基礎,結果將徹底陌生。 C++ 也嚴重依賴於未定義的行爲,所以任何徹底指定 C++ 衆多功能的努力都不太可能成功。 緩存
GLSL 是 WebGL 使用的語言,並被 WebGL 用於 Web 平臺。可是,因爲 GLSL 編譯器不兼容,達到跨瀏覽器的互操做性極其困難。因爲仍然存在長期的安全性和可移植性錯誤,GLSL 仍處於調研中。此外,GLSL 到年紀了。它的侷限性在於它沒有相似指針的對象,或者具備可變長度數組的能力。它的輸入和輸出是具備硬編碼名稱的全局變量。 安全
其次,SPIR-V 包含 50 多個可選功能,它們的實現是選擇性支持的,所以使用 SPIR-V 的着色器做者不知道它們的着色器是否能夠在 WebGPU 實現上工做。這與 Web 的一次寫入運行特性相反。 性能優化
WebAssembly 是另外一種熟悉的可能性,但它也不能很好地映射到 GPU 的體系結構。例如,WebAssembly 假設一個動態大小的堆,但 GPU 程序能夠訪問多個動態大小的緩衝區。沒有從新編譯,沒有一種高性能的方法能夠在兩個模型之間進行映射。
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;複製代碼
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 類型的仿真更快,由於類型更小而且它們的位表示不那麼複雜。
就像在 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;複製代碼
WHLSL 和 C 之間的一個區別是 WHLSL 在其聲明站點對全部未初始化的變量進行零初始化。這能夠防止跨操做系統和驅動程序的不可移植行爲——甚至更糟糕的是,在着色器開始執行以前讀取頁面的任何值。這也意味着 WHLSL 中的全部可構造類型都具備零值。
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
PizzaDay
}複製代碼
struct Foo {
int x;
float y;
}複製代碼
與其餘着色語言同樣,數組是經過值傳遞和返回函數的值類型(也稱爲 「copy-in copy-out」,相似於常規標量)。 使用如下語法能夠建立一個:
int[3] x;複製代碼
咱們確保語言安全的一個關鍵方法是對每一個陣列訪問執行邊界檢查。 咱們經過多種方式使這種潛在的昂貴操做變得高效。 數組索引是 uint,它將檢查減小到單個比較。 數組沒有稀疏實現,而且包含一個在編譯時可用的長度成員,使訪問成本接近於零。
爲了知足安全要求,WHLSL 使用安全指針,保證指向有效或無效的指針。與 C 同樣,你可使用&運算符建立指向左值的指針,並可使用 * 運算符取消引用。與 C 不一樣,你不能經過指針索引 - 若是它是一個數組。您不能將其轉換爲標量值,也不能使用特定的位模式表示。所以,它不能存在於緩衝區中或做爲着色器輸入/輸出。
設備地址空間對應於設備上的大部份內存。該存儲器是可讀寫的,對應於 Direct3D 中的無序訪問視圖和 Metal Shading Language 中的設備存儲器。常量地址空間對應於存儲器的只讀區域,一般針對廣播到每一個線程的數據進行優化。所以,寫入存在於常量地址空間中的左值是編譯錯誤。最後,線程組地址空間對應於可讀寫的內存區域,該區域在線程組中的每一個線程之間共享。它只能用於計算着色器。
int i = 4;
thread int* j = &i;
*j = 7;
// i is now 7複製代碼
thread int* i;複製代碼
它們對應於 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複製代碼
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複製代碼
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;
}複製代碼
操做符和操做符重載 可是,這裏也有其餘事情發生。 當編譯器看到 n_dot_h * m 時,它本質上不知道如何執行該乘法。 相反,編譯器會將其轉換爲對 operator() 的調用。 而後,經過標準函數重載決策算法選擇特定運算符執行。 這很重要,由於這意味着你能夠編寫本身的 operator*() 函數,並教 WHLSL 如何將你本身的類型相乘。
int operator++(int value) {
return value + 1;
}複製代碼
整個語言都使用了操做符重載。 這就是實現向量和矩陣乘法的方式。 這是數組索引的方式。 這是混合運算符的工做方式。 運算符重載提供了功能和簡單性; 核心語言沒必要直接瞭解每一個操做,由於它們是由重載的運算符實現的。
float3 operator.xxy(float3 v) {
float3 result;
result.x = v.x;
result.y = v.x;
result.z = v.y;
return result;
}複製代碼
float4 operator.xyz=(float4 v, float3 c) {
float4 result = v;
result.x = c.x;
result.y = c.y;
result.z = c.z;
return result;
}複製代碼
float4 a = float4(1, 2, 3, 4);
a.xyz = float3(7, 8, 9);複製代碼
thread float* operator.r(thread Foo* value) {
return &value->x;
}複製代碼
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;
}複製代碼
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);
}複製代碼
並不是 WHLSL 中存在 HLSL 標準庫中的每一個功能。例如,HLSL 支持 printf()。可是,在 Metal Shading Language 或 SPIR-V 中實現這樣的功能將很是困難。咱們在 HLSL 標準庫中包含儘量多的函數,這在 Web 環境中是合理的。
thread int* foo() {
int a;
return &a;
}
…
int b = *foo();複製代碼
這意味着此 WHLSL 代碼段徹底有效而且定義明確,緣由有兩個:
這種全局生命週期是惟一可能的,由於不容許遞歸(這對於着色語言來講很常見),這意味着不存在任何重入問題。相似地,着色器沒法分配或釋放內存,所以編譯器在編譯時知道着色器可能訪問的每一個內存塊。
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 專爲兩階段編譯而設計。在咱們的研究中,咱們發現許多 3D 引擎想要編譯大型着色器,每一個編譯包括在不一樣編譯之間重複的大型函數庫。不是屢次編譯這些支持函數,更好的解決方案是一次編譯整個庫,而後容許第二階段選擇應該一塊兒使用庫中的哪些入口點。
第二個編譯階段還提供了指定特化常量的便利位置。回想一下,WHLSL 沒有預處理器,這是在 HLSL 中啓用和禁用功能的傳統方式。引擎一般經過啓用渲染效果或經過翻轉開關切換 BRDF 來爲特定狀況定製單個着色器。將每一個渲染選項包含在單個着色器中的技術,以及基於啓用哪一種效果來專門設置單個着色器的技術是如此常見,它有一個名稱:ubershaders。 WHLSL 程序員可使用特殊化常量而不是預處理器宏,它們的工做方式與 SPIR-V 的特化常量相同。從語言的角度來看,它們只是標量常量。可是,在第二個編譯階段提供了這些常量的值,這使得在運行時配置程序變得很是容易。
compute void ComputeKernel(device uint[] b : register(u0)) {
…
}複製代碼
WHLSL 實現安全性的另外一種方式是執行數組/指針訪問的邊界檢查。這些邊界檢查可能有三種方式:
2. Clamping。數組索引操做能夠將索引限制爲數組的大小。這不涉及新的控制流程,所以它對均勻性沒有任何影響。甚至能夠經過忽略寫入併爲讀取返回 0 來 「clap」 指針訪問或零長度陣列訪問。這是可能的,由於你能夠用 WHLSL 中的指針作的事情是有限的,因此咱們能夠簡單地讓每一個操做用一個 「clamped」 指針作一些明肯定義的事情。硬件和驅動程序支持。某些硬件和驅動程序已經包含一種不會發生越界訪問的模式。使用此方法,硬件禁止越界訪問的機制是實現定義的。一個例子是 ARB_robustness OpenGL 擴展。不幸的是,WHLSL 應該能夠在幾乎全部現代硬件上運行,並且沒有足夠的 API / 設備支持這些模式。
爲了肯定邊界檢查的最佳行爲,咱們進行了一些性能實驗。咱們採用了 Metal Performance Shaders 框架中使用的一些內核,並建立了兩個新版本:一個使用 clamp,另外一個使用 trap。咱們選擇的內核是那些進行大量數組訪問的內核:例如,乘以大型矩陣。咱們在不一樣數據大小的各類設備上運行此基準測試。咱們確保沒有任何 trap 實際被擊中,而且沒有任何 clamp 實際上有任何影響,所以咱們能夠肯定咱們正在測量正確編寫的程序的常見狀況。
爲了適應這種狀況,着色器的返回值能夠是結構,而且各個字段是獨立處理的。實際上,這是遞歸工做的 - 結構能夠包含另外一個結構,其成員也能夠獨立處理。嵌套的結構被展平,而且全部非結構化的字段都被收集並視爲着色器輸出。
在將全部這些結構扁平化爲一組輸入和一組輸出以後,集合中的每一個項目都必須具備語義。每一個內置變量必須具備特定類型,而且只能在特定着色器階段使用。專精常量必須只有簡單的標量類型。
HLSL 程序員應該熟悉資源語義。 WHLSL 包括資源語義和地址空間,但這二者具備不一樣的用途。變量的地址空間用於肯定應在其中訪問哪一個緩存和內存層次結構。地址空間是必要的,由於它甚至經過指針操做仍然存在;設備指針不能設置爲指向線程變量。在 WHLSL 中,資源語義僅用於標識 WebGPU API 中的變量。可是,爲了與 HLSL 保持一致,資源語義必須 「匹配」 它所放置的變量的地址空間。例如,你不能在 texture 上放置寄存器(s0)。你不能將寄存器(u0)放在常量資源上。 WHLSL 中的數組沒有地址空間(由於它們是值類型,而不是引用類型),所以若是數組顯示爲着色器參數,則將其視爲用於匹配語義的設備資源。
「邏輯模式」限制 WHLSL 的設計要求能夠與 Metal Shading Language,SPIR-V 和 HLSL(或 DXIL)兼容。 SPIR-V 具備許多不一樣的操做模式,以不一樣的嵌入 API 爲目標。具體來講,咱們對 Vulkan 所針對的 SPIR-V 的味道感興趣。
由於 WHLSL 須要與 SPIR-V 兼容,因此 WHLSL 必須比 SPIR-V 更具表現力。所以,WHLSL 在 SPIR-V 邏輯模式中有一些限制使其能夠表達。這些限制並未做爲 WHLSL 的可選模式浮出水面;相反,它們是語言自己的一部分。最終,咱們但願在未來的語言版本中能夠解除這些限制,但在此以前,語言受到限制。
但不是那麼快!回想一下,線程變量具備全局生命週期,這意味着它們的行爲就像它們是在入口點的開頭聲明的那樣。若是運行時將全部這些局部變量收集在一塊兒,按類型排序,並將具備相同類型的全部變量聚合到數組中,該怎麼辦?而後,指針能夠簡單地是適當數組的偏移量。在 WHLSL 中,指針不能從新指向不一樣的類型,這意味着編譯器會靜態肯定相應的數組。所以,線程指針不須要遵照上述限制。可是,這種技術不適用於其餘地址空間中的指針;它只適用於線程指針。
深度 textures 與非深度 textures 不一樣,由於它們是 Metal Shading Language 中的不一樣類型,所以編譯器須要知道在發出 Metal Shading Language時要發出哪個。由於 WHLSL 不支持成員函數,因此 textures 採樣不像 texture.Sample(...) ;相反,它是使用像 Sample(texture,...) 這樣的自由函數完成的。
WebGPU API 將在特定位置自動發出一些資源障礙,這意味着 API 須要知道着色器中使用了哪些資源。所以,不能使用 「無約束」 的資源模型。這意味着全部資源都被列爲着色器的顯式輸入。相似地,API 想知道哪些資源用於讀取以及哪些資源用於寫入;編譯器經過檢查程序來靜態地知道這一點。 「const」 沒有語言級支持,或者 StructuredBuffer 和 RWStructuredBuffer 之間沒有區別,由於該信息已經存在於程序中。
對於第一個提案,咱們但願知足本文開頭概述的約束,同時爲擴展語言提供充分的機會。語言的一種天然演變能夠爲類型的抽象添加設施,例如協議或接口。 WHLSL 包含沒有訪問控制或繼承的簡單結構。其餘着色語言如 Slang 模型類型抽象做爲必須存在於結構內的一組方法。可是,Slang 遇到了一個問題,即沒法使現有類型遵循新接口。定義結構後,就沒法向其中添加新方法;花括號永遠關閉告終構。這個問題經過擴展來解決,相似於 Objective-C 或 Swift,它能夠在定義結構後追溯地將方法添加到結構中。 Java 經過鼓勵做者添加新類(稱爲適配器)來解決這個問題,這些類只存在於實現接口,並將每一個調用鏈接到實現類型。
請加入!咱們正在 WebGPU GitHub 項目上作這項工做。咱們一直在研究語言的正式規範,發出 Metal Shading Language和 SPIR-V 的參考編譯器,以及用於驗證正確性的 CPU 端解釋器。咱們歡迎你們嘗試一下,讓咱們知道它是怎麼回事!
英文原文:https://webkit.org/blog/8482/web-high-level-shading-language/