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 的着色語言以保證瀏覽器之間的互操做性。 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) 操做。 咱們但願將括號放在類型後面而不是變量名後面,緣由有兩個:
  1. 將全部類型信息放在一個地方使得解析器更簡單(避免順時針 / 螺旋規則)
  2. 在單個語句中聲明多個變量時避免歧義(例如 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 和 Metal Shading Language(經過 [[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 中的不一樣類型,所以編譯器須要知道在發出 Metal Shading Language時要發出哪個。由於 WHLSL 不支持成員函數,因此 textures 採樣不像 texture.Sample(...) ;相反,它是使用像 Sample(texture,...) 這樣的自由函數完成的。

採樣器不專業;全部用例都有一個採樣器類型。你能夠將此採樣器用於深度 textures 和非深度 textures。深度 textures 支持採樣器中的比較操做等內容。若是採樣器配置爲包含深度比較而且它與非深度 textures 一塊兒使用,則忽略深度操做。

WebGPU API 將在特定位置自動發出一些資源障礙,這意味着 API 須要知道着色器中使用了哪些資源。所以,不能使用 「無約束」 的資源模型。這意味着全部資源都被列爲着色器的顯式輸入。相似地,API 想知道哪些資源用於讀取以及哪些資源用於寫入;編譯器經過檢查程序來靜態地知道這一點。 「const」 沒有語言級支持,或者 StructuredBuffer 和 RWStructuredBuffer 之間沒有區別,由於該信息已經存在於程序中。


當前進展

WebGPU 社區小組正在研究用 OTT 編寫的正式語言規範,該規範描述了 WHLSL 與其餘 Web 語言採用的嚴格程度。 咱們還在研究能夠生成 Metal Shading Language,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 項目上作這項工做。咱們一直在研究語言的正式規範,發出 Metal Shading Language和 SPIR-V 的參考編譯器,以及用於驗證正確性的 CPU 端解釋器。咱們歡迎你們嘗試一下,讓咱們知道它是怎麼回事!

欲瞭解更多信息,您能夠經過 mmaxfield@apple.com 或 @Litherum 與我聯繫,或者您能夠聯繫咱們的佈道者 Jonathan Davis。

英文原文:https://webkit.org/blog/8482/web-high-level-shading-language/

「UC國際技術」致力於與你共享高質量的技術文章
歡迎微信搜索 UC國際技術 關注咱們的公衆號,或者將文章分享給你的好友
相關文章
相關標籤/搜索