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;
float3 b = float3(float(a) * 5, 6, 7);
float3 c = b.xxy;
float3 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[3] i = int[3](4, 5, 6);
thread int[] j = @i;
j[1] = 7;
// i[1] is 7
// j.length is 3

函数

WHLSL的函数与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;
}

运算符和运算符重载

当编译器看到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使用起来非常自然:

float4 a = float4(1, 2, 3, 4);
a.xyz = float3(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。矢量类型通常也有这种语法:

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;
}

可见,索引也使用了运算符,因此可以被重载。向量也有“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,而在其他设备上,却是反过来的。这些结果表明,编译器应该能够为特定设备选择更合适的方法,而不是被迫选择一种给定的方法。

图1

目前的工作

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/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容