Make JetStream 2
[WebKit-https.git] / PerformanceTests / JetStream2 / WSL / WSL.md
1 # WebGPU Shading Language
2
3 WebGPU Shading Language, or WSL for short, is a type-safe low-overhead programming language for GPUs (graphics processing units). This document explains how WSL works.
4
5 # Goals
6
7 WSL is designed to achieve the following goals:
8
9 - WSL should feel *familiar* to C++ programmers.
10 - WSL should be have a *sound* and *decidable* type system.
11 - WSL should not permit *out-of-bounds* memory accesses.
12 - WSL should be have *low overhead*.
13
14 The combination of a sound type system and bounds checking makes WSL a secure shader language: the language itself is responsible for isolating the shader from the rest of the GPU.
15
16 # Familiar Syntax
17
18 WSL is based on C syntax, but excludes features that are either unnecessary, insecure, or replaced by other WSL features:
19
20 - No strings.
21 - No `register`, `volatile`, `const`, `restrict`, or `extern` keywords.
22 - No unions. `union` is not a keyword.
23 - No goto or labels. `goto` is not a keyword.
24 - No `*` pointers.
25 - Effectless expressions are not statements (`a + b;` is a parse error).
26 - No undefined values (`int x;` initializes x to 0).
27 - No automatic type conversions (`int x; uint y = x;` is a type error).
28 - No recursion.
29 - No dynamic memory allocation.
30 - No modularity. The whole program is one file.
31
32 On top of this bare C-like foundation, WSL adds secure versions of familiar C++ features:
33
34 - Type-safe pointers (`^`) and array references (`[]`).
35 - Generics to replace templates.
36 - Operator overloading. Built-in operations like `int operator+(int, int)` are just native functions.
37 - Cast overloading. Built-in casts like `operator int(double)` are just native functions.
38 - Getters and setters. Property accesses like `vec.x` resolve to overloads like `float operator.field(float4)`.
39 - Array access overloading. Array accesses like `vec[0]` resolve to verloads like `float operator[](float4, uint)`.
40
41 In the following sections, WSL is shown by example starting with its C-like foundation and then building up to include more sophisticated features like generics.
42
43 ## Common subset of C and WSL
44
45 The following is a valid WSL function definition:
46
47     int foo(int x, int y, bool p)
48     {
49         if (p)
50             return x - y;
51         return x + y;
52     }
53
54 WSL source files behave similarly to C source files:
55
56 - Top-level statements must be type or function definitions.
57 - WSL uses structured C control flow constructs, like `if`, `while`, `for`, `do`, `break`, and `continue`.
58 - WSL uses C-like `switch` statements, but does not allow them to overlap other control flow (i.e. no [Duff's device](https://en.wikipedia.org/wiki/Duff%27s_device)).
59 - WSL allows variable declarations anywhere C++ would.
60
61 WSL types differ from C types. For example, this is an array of 42 integers in WSL:
62
63     int[42] array;
64
65 The type never surrounds the variable, like it would in C (`int array[42]`).
66
67 ## Type-safe pointers
68
69 WSL includes a secure pointer type. To emphasize that it is not like the C pointer, WSL uses `^` for the pointer type and for dereference. Like in C, `&` is used to take the address of a value.  Because GPUs have different kinds of memories, pointers must be annotated with an address space (one of `thread`, `threadgroup`, `device`, or `constant`). For example:
70
71     void bar(thread int^ p)
72     {
73         ^p += 42;
74     }
75     int foo()
76     {
77         int x = 24;
78         bar(&x);
79         return x; // Returns 66.
80     }
81
82 Pointers can be `null`. Each pointer access is null-checked, though most pointer accesses (like this one) will not have a null check. WSL places enough constraints on the programmer that programs are easy for the compiler to analyze. The compiler will always know that `^p += 42` adds `42` to `x` in this case.
83
84 WSL pointers do not support casting or pointer arithmetic. All memory accessible to a shader outlives the shader. This is even true of local variables. This is possible because WSL does not support recursion. Therefore, local variables simply get global storage. Local variables are initialized at the point of their declaration. Hence, the following is a valid program, which will exhibit the same behavior on every WSL implementation:
85
86     thread int^ foo()
87     {
88         int x = 42;
89         return &x;
90     }
91     int bar()
92     {
93         thread int^ p = foo();
94         thread int^ q = foo();
95         ^p = 53;
96         return ^q; // Returns 53.
97     }
98     int baz()
99     {
100         thread int^ p = foo();
101         ^p = 53;
102         foo();
103         return ^p; // Returns 42.
104     }
105
106 It's possible to point to any kind of data type. For example, `thread double[42]^` is a pointer to an array of 42 doubles.
107
108 ## Type-safe array references
109
110 WSL supports array references that carry a pointer to the base of the array and the array's length. This allows accesses to the array to be bounds-checked.
111
112 An array reference can be created using the `@` operator:
113
114     int[42] array;
115     thread int[] arrayRef = @array;
116
117 Both arrays and array references can be loaded from and stored to using `operator[]`:
118
119     int x = array[i];
120     int y = arrayRef[i];
121
122 Both arrays and array references know their length:
123
124     uint arrayLength = array.length;
125     uint arrayRefLength = arrayRef.length;
126
127 Given an array or array reference, it's possible to get a pointer to one of its elements:
128
129     thread int^ ptr1 = &array[i];
130     thread int^ ptr2 = &arrayRef[i];
131
132 A pointer is like an array reference with one element. It's possible to perform this conversion:
133
134     thread int[] ref = @ptr1;
135     ref[0] // Equivalent to ^ptr1.
136     ref.length // 0 if ptr1 was null, 1 if ptr1 was not null.
137
138 Similarly, using `@` on a non-pointer value results in a reference of length 1:
139
140     int x;
141     thread int[] ref = @x;
142     ref[0] // Aliases x.
143     ref.length // Returns 1.
144
145 It's not legal to use `@` on an array reference:
146
147     thread int[] ref;
148     thread int[][] referef = @ref; // Error!
149
150 ## Generics
151
152 WSL supports generic types using a simple syntax. WSL's generic are designed to integrate cleanly into the compiler pipeline:
153
154 - Generics have unambiguous syntax.
155 - Generic functions can be type checked before they are instantiated.
156
157 Semantic errors inside generic functions show up once regardless of the number of times the generic function is instantiated.
158
159 This is a simple generic function:
160
161     T identity<T>(T value)
162     {
163         T tmp = value;
164         return tmp;
165     }
166
167 WSL also supports structs, which are also allowed to be generic:
168
169     // Not generic.
170     struct Foo {
171         int x;
172         double y;
173     }
174     // Generic.
175     struct Bar<T, U> {
176         T x;
177         U y;
178     }
179
180 Type parameters can also be constant expressions. For example:
181
182     void initializeArray<T, uint length>(thread T[length]^ array, T value)
183     {
184         for (uint i = length; i--;)
185             (^array)[i] = value;
186     }
187
188 Constant expressions passed as type arguments must obey a very narrow definition of constantness. Only literals and references to other constant parameters qualify.
189
190 WSL is guaranteed to compile generics by instantiation. This is observable, since functions can return pointers to their locals. Here is an example of this phenomenon:
191
192     thread int^ allocate<uint>()
193     {
194         int x;
195         return &x;
196     }
197
198 The `allocate` function will return a different pointer for each unsigned integer constant passed as a type parameter. This allocation is completely static, since the `uint` parameter must be given a compile-time constant.
199
200 WSL's `typedef` uses a slightly different syntax than C. For example:
201
202     struct Complex<T> {
203         T real;
204         T imag;
205     }
206     typedef FComplex = Complex<float>;
207
208 `typedef` can be used to create generic types:
209
210     struct Foo<T, U> {
211         T x;
212         U y;
213     }
214     typedef Bar<T> = Foo<T, T>;
215
216 ## Protocols
217
218 Protocols enable generic functions to work with data of generic type. Because a function must be type-checkable before instantiation, the following would not be legal:
219
220     int bar(int) { ... }
221     double bar(double) { ... }
222     
223     T foo<T>(T value)
224     {
225         return bar(value); // Error!
226     }
227
228 The call to `bar` doesn't type check because the compiler cannot know that `foo<T>` will be instantiated with `T = int` or `T = double`. Protocols enable the programmer to tell the compiler what to expect of a type variable:
229
230     int bar(int) { ... }
231     double bar(double) { ... }
232     
233     protocol SupportsBar {
234         SupportsBar bar(SupportsBar);
235     }
236     
237     T foo<T:SupportsBar>(T value)
238     {
239         return bar(value);
240     }
241     
242     int x = foo(42);
243     double y = foo(4.2);
244
245 Protocols have automatic relationships to one another based on what functions they contain:
246
247     protocol Foo {
248         void foo(Foo);
249     }
250     protocol Bar {
251         void foo(Bar);
252         void bar(Bar);
253     }
254     void fuzz<T:Foo>(T x) { ... }
255     void buzz<T:Bar>(T x)
256     {
257         fuzz(x); // OK, because Bar is more specific than Foo.    
258     }
259
260 Protocols can also mix other protocols in explicitly. Like in the example above, the example below relies on the fact that `Bar` is a more specific protocol than `Foo`. However, this example declares this explicitly (`protocol Bar : Foo`) instead of relying on the language to infer it:
261
262     protocol Foo {
263         void foo(Foo);
264     }
265     protocol Bar : Foo {
266         void bar(Bar);
267     }
268     void fuzz<T:Foo>(T x) { ... }
269     void buzz<T:Bar>(T x)
270     {
271         fuzz(x); // OK, because Bar is more specific than Foo.    
272     }
273
274 ## Overloading
275
276 WSL supports overloading very similarly to how C++ does it. For example:
277
278     void foo(int);    // 1
279     void foo(double); // 2
280     
281     int x;
282     foo(x); // calls 1
283     
284     double y;
285     foo(y); // calls 2
286
287 WSL automatically selects the most specific overload if given multiple choices. For example:
288
289     void foo(int);    // 1
290     void foo(double); // 2
291     void foo<T>(T);   // 3
292
293     foo(1); // calls 1
294     foo(1.); // calls 2
295     foo(1u); // calls 3
296
297 Generic functions like `foo<T>` can be called with or without all of their type arguments supplied. If they are not supplied, the function participates in overload resolution like any other function would. Functions are ranked by how specific they are; function A is more specific than function B if A's parameter types can be used as argument types for a call to B but not vice-versa. If one functions more specific than all others then this function is selected, otherwise a type error is issued.
298
299 ## Operator Overloading
300
301 Many WSL operations desugar to calls to functions. Those functions are called *operator overloads* and are declared using syntax that involves the keyword `operator`. The following operations result in calls to operator overloads:
302
303 - Numerical operators (`+`, `-`, `*`, `/`, etc.).
304 - Increment and decrement (`++`, `--`).
305 - Casting (`type(value)`).
306 - Accessing values in arrays (`[]`, `[]=`, `&[]`).
307 - Accessing fields (`.field`, `.field=`, `&.field`).
308
309 WSL's operator overloading is designed to synthesize many operators for you:
310
311 - Read-modify-write operators like `+=` are desugared to a load of the load value, the underlying operator (like `+`), and a store of the new value. It's not possible to override `+=`, `-=`, etc.
312 - `x++` and `++x` both call the same operator overload. `operator++` takes the old value and returns a new one; for example the built-in `++` for `int` could be written as: `int operator++(int value) { return value + 1; }`.
313 - `operator==` can be overloaded, but `!=` is automatically synthesized and cannot be overloaded.
314
315 Some operators and overloads are restricted:
316
317 - `!`, `&&`, and `||` are built-in operations on the `bool` type.
318 - Self-casts (`T(T)`) are always the identity function.
319 - Casts with no arguments (`T()`) always return the default value for that type. Every type has a default value (`0`, `null`, or the equivalent for each field).
320
321 Cast overloading allows for supporting conversions between types and for creating constructors for custom types. Here is an example of cast overloading being used to create a constructor:
322
323     struct Complex<T> {
324         T real;
325         T imag;
326     }
327     operator<T> Complex<T>(T real, T imag)
328     {
329         Complex<T> result;
330         result.real = real;
331         result.imag = imag;
332         return result;
333     }
334     
335     Complex<float> i = Complex<float>(0, 1);
336
337 WSL supports accessor overloading as part of the operator overloading syntax. This gives the programmer broad powers. For example:
338
339     struct Foo {
340         int x;
341         int y;
342     }
343     int operator.sum(Foo foo)
344     {
345         return foo.x + foo.y;
346     }
347
348 It's possible to say `foo.sum` to call the `operator.sum` function. Both getters and setters can be provided:
349
350     struct Foo {
351         int value;
352     }
353     double operator.doubleValue(Foo value)
354     {
355         return double(value.value);
356     }
357     Foo operator.doubleValue=(Foo value, double doubleValue)
358     {
359         value.value = int(doubleValue);
360         return value;
361     }
362
363 Providing both getter and setter overloads makes `doubleValue` behave almost as if it was a field of `Foo`. For example, it's possible to say:
364
365     Foo foo;
366     foo.value = 42;
367     foo.doubleValue *= 2; // Now foo.value is 84
368
369 It's also possible to provide an address-getting overload called an *ander*:
370
371     struct Foo {
372         int value;
373     }
374     thread int^ operator&.valueAlias(thread Foo^ foo)
375     {
376         return &foo->value;
377     }
378
379 Providing just this overload for a pointer type in every address space gives the same power as overloading getters and setters. Additionally, it makes it possible to `&foo.valueAlias`.
380
381 The same overloading power is provided for array accesses. For example:
382
383     struct Vector<T> {
384         T x;
385         T y;
386     }
387     thread T^ operator&[](thread T^ ptr, uint index)
388     {
389         return index ? &ptr->y : &ptr->x;
390     }
391
392 Alternatively, it's possible to overload getters and setters (`operator[]` and `operator[]=`).
393
394 # Mapping of API concepts
395
396 WSL is designed to be useful as both a graphics shading language and as a computation language. However, these two environments have
397 slightly different semantics.
398
399 When using WSL as a graphics shading language, there is a distinction between *entry-points* and *non-entry-points*. Entry points are top-level functions which have either the `vertex` or `fragment` keyword in front of their declaration. Entry points may not be forward declared. An entry point annotated with the `vertex` keyword may not be used as a fragment shader, and an entry point annotated with the `fragment` keyword may not be used as a vertex shader. No argument nor return value of an entry point may be a pointer. Entry points must not accept type arguments (also known as "generics").
400
401 ## Vertex entry points
402
403 WebGPU's API passes data to a WSL vertex shader in four ways:
404
405 - Attributes
406 - Buffered data
407 - Texture data
408 - Samplers
409
410 Each of these API objects is referred to by name from the API. Variables in WSL are not annotated with extra API-visible names (like they are in some other graphics APIs).
411
412 Variables are passed to vertex shaders as arguments to a vertex entry point. Each buffer is represented as an argument with an array reference type (using the `[]` syntax). Textures and samplers are represented by arguments with the `texture` and `sampler` types, respectively. All other non-builtin arguments to a vertex entry point are implied to be attributes.
413
414 Some arguments are recognized by the compiler from their name and type. These arguments provide built-in functionality inherent in the graphics pipeline. For example, an argument of the form `int wsl_vertexID` refers to the ID of the current vertex, and is not recognized as an attribute. All non-builtin arguments to a vertex entry point must be associated with an API object whenever any draw call using the vertex entry point is invoked. Otherwise, the draw call will fail.
415
416 The only way to pass data between successive shader stages within a single draw call is by return value. An entry point must indicate that it returns a collection of values contained within a structure. Every variable inside this structure, recursively, is passed to the next stage in the graphics pipeline. Members of this struct may also be output built-in variables. For example, a vertex entry point may return a struct which contains a member `float4 wsl_Position`, and this variable will represent the rasterized position of the vertex. Buffers (as described by WSL array references), textures, and samplers must not be present in this returned struct. Built-in variables must never appear twice inside the returned structure.
417
418 ## Fragment entry points
419
420 Fragment entry points may accept one argument with the type that the previous shader stage returned. The argument name for this argument must be `stageIn`. In addition to this argument, fragment entry points may accept buffers, textures, and samplers as arguments in the same way that vertex entry points accept them. Fragment entry points also must return a struct, and all members of this struct must be built-in variables. The set of recognized built-in variables which may be accepted or returned from an entry point is different between all types of entry points.
421
422 For example, this would be a valid graphics program:
423
424     struct VertexInput {
425         float2 position;
426         float3 color;
427     }
428     
429     struct VertexOutput {
430         float4 wsl_Position;
431         float3 color;
432     }
433     
434     struct FragmentOutput {
435         float4 wsl_Color;
436     }
437     
438     vertex VertexOutput vertexShader(VertexInput vertexInput) {
439         VertexOutput result;
440         result.wsl_Position = float4(vertexInput.position, 0., 1.);
441         result.color = vertexInput.color;
442         return result;
443     }
444     
445     fragment FragmentOutput fragmentShader(VertexOutput stageIn) {
446         FragmentOutput result;
447         result.wsl_Color = float4(stageIn.color, 1.);
448         return result;
449     }
450
451 ## Compute entry points
452
453 WebGPU's API passes data to a compute shader in three ways:
454
455 - Buffered data
456 - Texture data
457 - Samplers
458
459 Compute entry points start with the keyword `compute`. The return type for a compute entry point must be `void`. Each buffer is represented as an argument with an array reference type (using the `[]` syntax). Textures and samplers are represented by arguments with the `texture` and `sampler` types, respectively. Compute entry points may also accept built-in variables as arguments. Arguments of any other type are disallowed. Arguments may not use the `threadgroup` memory space.
460
461 # Error handling
462
463 Errors may occur during shader processing. For example, the shader may attempt to dereference a `null` array reference. If this occurs, the shader stage immediately completes successfully. The entry point immediately returns a struct with all fields set to 0. After this event, subsequent shader stages will proceed as if there was no problem.
464
465 Buffer and texture reads and writes before the error all complete, and have the same semantics as if no error had occurred. Buffer and texture reads and writes after the error do not occur.
466
467 # Summary
468
469 WSL is a type-safe language based on C syntax. It eliminates some C features, like unions and pointer casts, but adds other modern features in their place, like generics and overloading.
470
471 # Additional Limitations
472
473 The following additional limitations may be placed on a WSL program:
474
475 - `device`, `constant`, and `threadgroup` pointers cannot point to data that may have pointers in it. This safety check is not done as part of the normal type system checks. It's performed only after instantiation.
476 - Pointers and array references (collectively, *references*) may be restricted to support compiling to SPIR-V *logical mode*. In this mode, arrays must not transitively hold references. References must be initialized upon declaration and never reassigned. Functions that return references must have one return point. Ternary expressions may not return references.
477 - Graphics entry points must transitively never refer to the `threadgroup` memory space.
478
479