本文將介紹一門叫作Web High Level Shading Language(WHLSL啤挎,發(fā)音為“whistle”)的新Web圖形著色語言堪遂,它對HLSL進(jìn)行了擴(kuò)展,變得更安全检诗、更可靠甩鳄。
背景
在過去的幾十年中,3D圖形已經(jīng)發(fā)生了重大變化,程序員用來編寫3D應(yīng)用程序的 API也發(fā)生了相應(yīng)的變化渴逻。
五年前疾党,最先進(jìn)的圖形應(yīng)用程序使用OpenGL來執(zhí)行渲染。然而惨奕,在過去幾年中雪位,3D 圖形行業(yè)正朝著更新、更低級別的圖形框架轉(zhuǎn)變梨撞,這些框架與真實(shí)硬件的行為更加貼合雹洗。2014年,Apple推出了Metal框架卧波,讓iOS和macOS應(yīng)用程序可以充分利用GPU时肿。2015年,微軟推出了Direct3D 12港粱,這是Direct3D的一個重大更新螃成,帶來了控制臺級的渲染和計算效率。2016 年查坪,Khronos Group發(fā)布了Vulkan API寸宏,主要用于Android,也具備了類似的優(yōu)勢偿曙。
去年氮凝,Apple在W3C內(nèi)部成立了WebGPU社區(qū)組,致力于標(biāo)準(zhǔn)化新的3D圖形API遥昧,既要提供原生API的優(yōu)勢覆醇,同時也適用于Web環(huán)境。這個新的Web API可以在 Metal炭臭、Direct3D和Vulkan上實(shí)現(xiàn)永脓。所有主要的瀏覽器廠商都參與了標(biāo)準(zhǔn)化工作。
這些現(xiàn)代3D圖形API中的每一個都使用了著色器鞋仍,WebGPU也不例外常摧。著色器是一種利用GPU專門架構(gòu)的程序。在并行數(shù)值處理方面威创,GPU優(yōu)于CPU落午。為了利用這兩種架構(gòu),現(xiàn)代3D應(yīng)用程序使用了混合設(shè)計肚豺,使用CPU和GPU來完成不同的任務(wù)溃斋。通過利用每種架構(gòu)的最佳特性,現(xiàn)代圖形API為開發(fā)人員提供了一個強(qiáng)大的框架吸申,可以創(chuàng)建復(fù)雜梗劫、豐富享甸、快速的3D應(yīng)用程序。專為Metal設(shè)計的應(yīng)用程序使用的是 Metal Shading Language梳侨,為Direct3D 12設(shè)計的應(yīng)用程序使用的是 HLSL蛉威,為Vulkan設(shè)計的應(yīng)用程序使用的是SPIR-V或GLSL。
WHLSL
WHLSL是一門適用于Web平臺的新著色語言走哺。它由W3C的WebGPU社區(qū)組開發(fā)蚯嫌,這個開發(fā)組正忙于制定規(guī)范、開發(fā)編譯器和CPU端解釋器丙躏。
WHLSL以HLSL為基礎(chǔ)择示,并對其進(jìn)行了簡化和擴(kuò)展。WHLSL是一門功能強(qiáng)大且富有表現(xiàn)力的著色語言彼哼,帶來了安全性和其他好處对妄。
語言基礎(chǔ)
與HLSL中一樣,WHLSL的原始數(shù)據(jù)類型包括bool敢朱、int剪菱、uint、float和 half拴签。不支持Double孝常,因?yàn)樗贛etal中也不存在,并且會導(dǎo)致軟件模擬變慢蚓哩。Bool 沒有特定的位表示构灸,因此不能出現(xiàn)在著色器輸入/輸出或資源中。
SPIR-V中也存在同樣的限制岸梨,我們希望能夠在生成的SPIR-V代碼中使用 OpTypeBool喜颁。WHLSL還提供了較小的整數(shù)類型char、uchar曹阔、short和ushort半开,可以直接在Metal Shading Language中使用,在SPIR-V中可以將 OpTypeFloat指定為16赃份,并且可以在HLSL中進(jìn)行模擬寂拆。模擬這些類型比模擬 Double更快,因?yàn)檫@些類型更小并且它們的位表示不那么復(fù)雜抓韩。
WHLSL不提供C語言風(fēng)格的隱式轉(zhuǎn)換纠永。 我們發(fā)現(xiàn)隱式轉(zhuǎn)換是著色器中常見的錯誤來源。此外谒拴,避免隱式轉(zhuǎn)換使規(guī)范和編譯器變得更簡單尝江。
與HLSL中一樣,WHLSL也有矢量類型和矩陣類型英上,例如float4和int3x4茂装。我們盡量保持標(biāo)準(zhǔn)庫簡單怠蹂,所以沒有添加一堆“x1”單元素向量和矩陣,因?yàn)閱卧叵蛄恳呀?jīng)可以表示為標(biāo)量少态,單元素矩陣已經(jīng)可以表示為向量。這符合我們消除隱式轉(zhuǎn)換的愿望易遣,在float1和float之間進(jìn)行顯式轉(zhuǎn)換是件麻煩且不必要的事情彼妻。
以下是有效的著色器片段:
int a = 7;
a += 3;
float3 b = float3(float(a) * 5, 6, 7);
float3 c = b.xxy;
float3 d = b * c;
之前提到過,WHLSL不支持隱式轉(zhuǎn)換豆茫,但你可能已經(jīng)注意到侨歉,在上面的代碼片段中,5并未寫為5.0揩魂。這是因?yàn)樽置媪勘硎緸榭膳c其他數(shù)字類型統(tǒng)一的特殊類型幽邓。當(dāng)編譯器看到上面的代碼時,它知道乘法運(yùn)算符要求參數(shù)類型相同火脉,第一個參數(shù)顯然是浮點(diǎn)數(shù)牵舵。所以,當(dāng)編譯器看到float(a) * 5時倦挂,它說“好吧畸颅,我知道第一個參數(shù)是一個浮點(diǎn)數(shù),我必須使用(float, float)重載方援,所以讓我們把第二個參數(shù)也變?yōu)楦↑c(diǎn)數(shù)”没炒。即使兩個參數(shù)都是字面量也是一樣,因?yàn)樽置媪坑幸粋€首選類型犯戏。因此送火,5 * 5將對應(yīng) (int,int) 重載,5u *5u將對應(yīng)(uint,uint) 重載先匪,5.0 * 5.0將對應(yīng)(float,float) 重載种吸。
WHLSL和C語言之間的一個區(qū)別是WHLSL在聲明部分對所有未初始化的變量進(jìn)行零初始化。這可以避免跨操作系統(tǒng)和驅(qū)動程序的不可移植行為或者在著色器開始執(zhí)行之前讀取到任意值胚鸯。這也意味著WHLSL中的所有可構(gòu)造類型都具有零值骨稿。
枚舉
因?yàn)槊杜e不會產(chǎn)生任何運(yùn)行時成本并且非常有用,所以WHLSL原生支持枚舉姜钳。
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
PizzaDay
}
枚舉的基礎(chǔ)類型默認(rèn)為int坦冠,但你可以進(jìn)行類型覆蓋,例如enum Weekday : uint哥桥。類似地辙浑,枚舉的值可以具有基礎(chǔ)值,例如Tuesday = 72拟糕。因?yàn)槊杜e已經(jīng)定義了類型和值判呕,因此它們可以被用在緩沖區(qū)中倦踢,并且可以在基礎(chǔ)類型和枚舉類型之間進(jìn)行轉(zhuǎn)換。當(dāng)你想在代碼中引用一個值時侠草,可以像 Weekday.PizzaDay 這樣辱挥。這意味著枚舉值不會污染全局命名空間,枚舉的值也不會發(fā)生沖突边涕。
結(jié)構(gòu)體
WHLSL中的結(jié)構(gòu)與HLSL和C語言類似晤碘。
struct Foo {
int x;
float y;
}
結(jié)構(gòu)體設(shè)計簡單,避免了繼承功蜓、虛擬方法和訪問控制园爷。結(jié)構(gòu)體沒有“私有”成員。因?yàn)榻Y(jié)構(gòu)體沒有訪問控制式撼,所以不需要成員函數(shù)童社。
數(shù)組
與其他著色語言一樣,數(shù)組是可以傳給函數(shù)或從函數(shù)中返回的值類型著隆。你可以使用以下語法創(chuàng)建一個數(shù)組:
int[3] x;
與任何變量聲明一樣扰楼,數(shù)組內(nèi)容將使用零進(jìn)行填充。我們將括號放在類型后面而不是變量名后面旅东,有兩個原因:
- 將所有類型信息放在一個地方可以讓解析更簡單(避免順時針 / 螺旋規(guī)則);
- 在單個語句中聲明多個變量時可以避免歧義(例如int[10] x灭抑,y;)。
數(shù)組是值類型抵代,而WHLSL使用另外兩種類型實(shí)現(xiàn)引用語義:安全指針和數(shù)組引用腾节。
安全指針
某種形式的引用語義,幾乎被用在每一種CPU端編程語言中荤牍。在WHLSL中包含指針將使開發(fā)人員更容易將現(xiàn)有的CPU端代碼遷移到GPU案腺,從而可以輕松移植機(jī)器學(xué)習(xí)、計算機(jī)視覺和信號處理應(yīng)用程序之類的東西康吵。
為了滿足安全要求劈榨,WHLSL使用了安全指針,保證指向有效的東西晦嵌,或者為 null同辣。與C語言一樣,你可以使用&運(yùn)算符創(chuàng)建指向lvalue的指針惭载,并可以使用* 運(yùn)算符取消引用旱函。與C語言不同的是,你不能像數(shù)組那樣對指針進(jìn)行索引描滔。你不能將其與標(biāo)量之間進(jìn)行轉(zhuǎn)換棒妨,也不能使用特定的位模式表示。因此含长,它不能出現(xiàn)在緩沖區(qū)中或作為著色器輸入/輸出券腔。
WHLSL有4種不同的堆:device伏穆、constant、threadgroup和thread纷纫。所有的引用類型都必須使用它們指向的地址空間進(jìn)行標(biāo)記枕扫。
device地址空間對應(yīng)于設(shè)備上的大部分內(nèi)存。內(nèi)存是可讀寫的涛酗,對應(yīng)于Direct3D中的無序訪問視圖以及Metal Shading Language中的device內(nèi)存铡原。constant地址空間對應(yīng)于內(nèi)存的只讀區(qū)域,通常針對廣播到每個線程的數(shù)據(jù)進(jìn)行優(yōu)化。最后,threadgroup 地址空間對應(yīng)于可讀寫的內(nèi)存區(qū)域窟却,該區(qū)域被線程組的每個線程共享软驰。它只能用于計算著色器。
默認(rèn)情況下请唱,值存在于thread地址空間中:
int i = 4;
thread int* j = &i;
*j = 7;
// i is now 7
因?yàn)樗凶兞慷际褂昧阒党跏蓟诌洌灾羔樖莕ull初始化的。因此十绑,以下的聲明是有效的:
thread int* i;
數(shù)組引用
數(shù)組引用類似于指針聚至,但它們可以與下標(biāo)運(yùn)算符一起使用,以訪問數(shù)組引用中的多個元素本橙。雖然數(shù)組的length在編譯時是已知的扳躬,并且必須在類型聲明中指明,但數(shù)組引用的length要在運(yùn)行時才能知道甚亭。與指針一樣贷币,它們必須與地址空間相關(guān)聯(lián),并且可能會是nullptr亏狰。與數(shù)組一樣役纹,它們使用uint進(jìn)行索引,以進(jìn)行單比較邊界檢查暇唾,并且不能是稀疏的促脉。
你可以使用@運(yùn)算符為lvalue創(chuàng)建數(shù)組引用:
int i = 4;
thread int[] j = @i;
j[0] = 7;
// i is 7
// j.length is 1
在指針j上使用@會創(chuàng)建一個指向與j相同的數(shù)組引用:
int i = 4;
thread int* j = &i;
thread int[] k = @j;
k[0] = 7;
// i is 7
// k.length is 1
在數(shù)組上使用 @使數(shù)組引用指向該數(shù)組:
int[3] i = int[3](4, 5, 6);
thread int[] j = @i;
j[1] = 7;
// i[1] is 7
// j.length is 3
函數(shù)
WHLSL的函數(shù)與C語言中的函數(shù)非常相似。例如策州,這是標(biāo)準(zhǔn)庫中的一個函數(shù):
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;
}
運(yùn)算符和運(yùn)算符重載
當(dāng)編譯器看到n_dot_h * m時瘸味,它并不知道如何執(zhí)行這個乘法。編譯器會將其轉(zhuǎn)換為對operator * ()的調(diào)用抽活。然后硫戈,通過標(biāo)準(zhǔn)函數(shù)重載決策算法選擇特定的operator * ()。這意味著你可以編寫自己的operator *()函數(shù)下硕,告訴 WHLSL 如何執(zhí)行自定義類型的乘法丁逝。
這同樣適用于像 ++ 這樣的操作汁胆。以下是標(biāo)準(zhǔn)庫中的一個示例:
int operator++(int value) {
return value + 1;
}
生成屬性
但WHLSL并不僅僅停留在運(yùn)算符重載上。最開始的例子中有個b.xxy霜幼,其中b是 float3嫩码。這是一個表達(dá)式,意思是“創(chuàng)建一個包含3個元素的向量罪既,其中前兩個元素具有與b.x相同的值铸题,第三個元素具有與b.y相同的值”。這有點(diǎn)像是向量的成員琢感,只是沒有與任何存儲相關(guān)聯(lián)丢间。相反,它是在訪問期間計算生成的驹针。這些“混合運(yùn)算符”存在于每種實(shí)時著色語言中烘挫,WHLSL也不例外。這是通過生成屬性來實(shí)現(xiàn)的柬甥,就像在Swift中一樣饮六。
Getters
標(biāo)準(zhǔn)庫包含了很多以下形式的函數(shù):
float3 operator.xxy(float3 v) {
float3 result;
result.x = v.x;
result.y = v.x;
result.z = v.y;
return result;
}
當(dāng)編譯器遇到訪問不存在的成員的屬性時,它可以調(diào)用這個運(yùn)算符苛蒲,并將對象作為第一個參數(shù)傳遞進(jìn)去卤橄。通俗地說,我們稱之為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使用新數(shù)據(jù)創(chuàng)建對象的副本窟扑。當(dāng)編譯器遇到對生成屬性進(jìn)行賦值時,它會調(diào)用Setter寄月,并將結(jié)果賦給原始變量辜膝。
Anders
Ander是Getter和Setter的泛化,可以與指針一起使用漾肮。它是對性能的一種優(yōu)化厂抖,這樣Setter就不必創(chuàng)建對象的副本。這是一個例子:
thread float* operator.r(thread Foo* value) {
return &value->x;
}
Anders比Getter或Setter更強(qiáng)大克懊,因?yàn)榫幾g器可以使用Ander來實(shí)現(xiàn)讀取或賦值忱辅。當(dāng)通過Ander讀取生成屬性時,編譯器調(diào)用Ander谭溉,然后取消對結(jié)果的引用墙懂。在寫入時,編譯器也調(diào)用Ander扮念,取消對結(jié)果的引用损搬,并將結(jié)果分配給它。任何用戶定義的類型都可以包含Getter、Setter巧勤、Ander和Indexer的任意組合嵌灰。如果相同類型具有Ander以及Getter或Setter,編譯器將首選Ander颅悉。
Indexers
在大多數(shù)實(shí)時著色語言中沽瞭,不會使用與其列或行對應(yīng)的成員來訪問矩陣。相反剩瓶,它們使用數(shù)組語法來訪問驹溃,例如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;
}
可見延曙,索引也使用了運(yùn)算符豌鹤,因此可以被重載。向量也有“Indexer”枝缔,因此 myVector.x和myVector[0]是互為同義詞傍药。
標(biāo)準(zhǔn)庫
我們基于描述HLSL標(biāo)準(zhǔn)庫的Microsoft Docs設(shè)計了WHLSL標(biāo)準(zhǔn)庫。WHLSL標(biāo)準(zhǔn)庫主要包括數(shù)學(xué)運(yùn)算魂仍,既可以處理標(biāo)量值,也可以處理矢量和矩陣的元素拣挪。標(biāo)準(zhǔn)款定義了你期望的所有標(biāo)準(zhǔn)運(yùn)算符擦酌,包括邏輯運(yùn)算和按位運(yùn)算,如operator*()和 operator<<()菠劝。
WHLSL的設(shè)計原則之一是保持語言本身的小型化赊舶,所以盡可能多地在標(biāo)準(zhǔn)庫中定義其他內(nèi)容。當(dāng)然赶诊,并非標(biāo)準(zhǔn)庫中的所有函數(shù)都可以用WHLSL表示(如 bool operator*(float,float))笼平,但幾乎所有函數(shù)都可以使用WHLSL實(shí)現(xiàn)。例如舔痪,這個函數(shù)就是標(biāo)準(zhǔn)庫的一部分:
float smoothstep(float edge0, float edge1, float x) {
float t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
return t * t * (3 - 2 * t);
}
由于標(biāo)準(zhǔn)庫旨在盡可能與HLSL相匹配寓调,因此其中的大多數(shù)函數(shù)已經(jīng)存在于HLSL 中。但不同的著色語言具有不同的內(nèi)置函數(shù)锄码,因此每個函數(shù)定義都允許進(jìn)行正確性測試夺英。WHLSL包含了一個CPU端解釋器,在執(zhí)行WHLSL程序時將使用這些函數(shù)的 WHLSL實(shí)現(xiàn)滋捶。
當(dāng)然痛悯,并非出現(xiàn)在HLSL標(biāo)準(zhǔn)庫中的每個函數(shù)在WHLSL中也都會有。例如重窟,HLSL支持printf()载萌,但要在Metal Shading Language或SPIR-V中實(shí)現(xiàn)這樣的函數(shù)會非常困難。
安全性
WHLSL是一門安全的語言,這意味著訪問網(wǎng)站以外的信息是不可能的扭仁。WHLSL通過消除未定義的行為來達(dá)到這個目的垮衷。
WHLSL實(shí)現(xiàn)安全性的另一種方式是進(jìn)行數(shù)組 / 指針訪問邊界檢查。邊界檢查有三種方式:
Trapping斋枢。當(dāng)程序中出現(xiàn)trap時帘靡,著色器階段會立即退出,將所有著色器階段的輸出填充為0瓤帚。繪制調(diào)用會繼續(xù)描姚,并運(yùn)行圖形管道的下一個階段。因?yàn)?Trapping引入了新的控制流程戈次,所以對程序的一致性有一定影響轩勘。trap是在邊界檢查內(nèi)發(fā)出的,這意味著它們必然存在于非一致的控制流程中怯邪。對于某些不使用一致性的程序可能沒問題绊寻,但一般來說這會導(dǎo)致trap難以使用。
Clamping悬秉。數(shù)組索引操作可以將索引限制為數(shù)組大小澄步。這不涉及新的控制流程,因此它對一致性沒有任何影響和泌。甚至可以通過忽略寫入并為讀取返回0 來“clamp”指針訪問或零長度陣列訪問村缸。這是可能的,因?yàn)槟憧梢杂肳HLSL中的指針做的事情是有限的武氓,所以我們可以簡單地讓每個操作用一個“clamp”指針做一些明確定義的事情梯皿。
硬件和驅(qū)動程序支持。某些硬件和驅(qū)動程序已經(jīng)包含一種不會發(fā)生越界訪問的模式县恕。ARB_robustness OpenGL擴(kuò)展就是一個很好的例子东羹。可惜的是忠烛,WHLSL要在幾乎所有現(xiàn)代硬件上運(yùn)行属提,所以沒有足夠的API/設(shè)備支持這些模式。
無論編譯器使用哪種方法况木,都不應(yīng)影響著色器的一致性垒拢。換句話說,它不可能能將有效的程序變成無效的程序火惊。
為了確定邊界檢查的最佳行為求类,我們進(jìn)行了一些性能實(shí)驗(yàn)。我們采用了Metal Performance Shaders框架中的一些內(nèi)核屹耐,并創(chuàng)建了兩個新版本:一個使用 clamping尸疆,另一個使用traping椿猎。我們選擇的內(nèi)核是那些進(jìn)行大量數(shù)組訪問的內(nèi)核:例如,大型矩陣相乘寿弱。我們在各種設(shè)備上運(yùn)行這個基準(zhǔn)測試犯眠。
我們希望trapping能夠更快,因?yàn)橄掠尉幾g器可以消除冗余的trap症革。但我們發(fā)現(xiàn)筐咧,在某些設(shè)備上,trapping明顯快于clamping噪矛,而在其他設(shè)備上量蕊,卻是反過來的。這些結(jié)果表明艇挨,編譯器應(yīng)該能夠?yàn)樘囟ㄔO(shè)備選擇更合適的方法残炮,而不是被迫選擇一種給定的方法。
目前的工作
WebGPU社區(qū)小組正在使用OTT編寫正式語言規(guī)范缩滨。我們還在開發(fā)一個可以生成 Metal Shading Language势就、SPIR-V和HLSL的編譯器。此外脉漏,編譯器還包括了一個CPU端解釋器苞冯,可用于驗(yàn)證實(shí)現(xiàn)的正確性。
未來的發(fā)展方向
WHLSL還處于初級階段侧巨,在語言設(shè)計完成之前還有很長的路要走抱完。請隨時在GitHub(https://github.com/gpuweb/WHLSL)中提出你的想法和問題!
更多內(nèi)容刃泡,請參看英文原文。
英文原文:
https://webkit.org/blog/8482/web-high-level-shading-language/