blob: d3b66567826a51dd6c754d4e9b24d7578972bb3d [file] [log] [blame] [view]
Keith Millerd8cbaab2024-10-30 16:45:461# WebGPU Shading Language
2
3WebGPU 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
7WSL 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
14The 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
18WSL 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
32On 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
41In 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
45The 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
54WSL 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
61WSL types differ from C types. For example, this is an array of 42 integers in WSL:
62
63 int[42] array;
64
65The type never surrounds the variable, like it would in C (`int array[42]`).
66
67## Type-safe pointers
68
69WSL 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
82Pointers 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
84WSL 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
106It'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
110WSL 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
112An array reference can be created using the `@` operator:
113
114 int[42] array;
115 thread int[] arrayRef = @array;
116
117Both 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
122Both arrays and array references know their length:
123
124 uint arrayLength = array.length;
125 uint arrayRefLength = arrayRef.length;
126
127Given 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
132A 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
138Similarly, 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
145It's not legal to use `@` on an array reference:
146
147 thread int[] ref;
148 thread int[][] referef = @ref; // Error!
149
150## Generics
151
152WSL 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
157Semantic errors inside generic functions show up once regardless of the number of times the generic function is instantiated.
158
159This is a simple generic function:
160
161 T identity<T>(T value)
162 {
163 T tmp = value;
164 return tmp;
165 }
166
167WSL 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
180Type 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
188Constant expressions passed as type arguments must obey a very narrow definition of constantness. Only literals and references to other constant parameters qualify.
189
190WSL 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
198The `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
200WSL'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
218Protocols 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
228The 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
245Protocols 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
260Protocols 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
276WSL 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
287WSL 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
297Generic 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
301Many 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
309WSL'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
315Some 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
321Cast 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
337WSL 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
348It'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
363Providing 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
369It'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
379Providing 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
381The 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
392Alternatively, it's possible to overload getters and setters (`operator[]` and `operator[]=`).
393
394# Mapping of API concepts
395
396WSL is designed to be useful as both a graphics shading language and as a computation language. However, these two environments have
397slightly different semantics.
398
399When 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
403WebGPU's API passes data to a WSL vertex shader in four ways:
404
405- Attributes
406- Buffered data
407- Texture data
408- Samplers
409
410Each 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
412Variables 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
414Some 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
416The 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
420Fragment 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
422For 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
453WebGPU's API passes data to a compute shader in three ways:
454
455- Buffered data
456- Texture data
457- Samplers
458
459Compute 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
463Errors 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
465Buffer 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
469WSL 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
473The 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