WGSL basics

Here in this step, we will learn the basics of WebGPU Shading Language (WGSL). WGSL is designed to be a simple and efficient language that can be easily compiled to SPIR-V1 or other intermediate representations. The syntax of WGSL is similar to Rust2, C, Typescript or GLSL, making it easy to learn for developers who are already familiar with those languages. However, there are some key differences between WGSL and other shading languages that you should be aware of. The syntax of WGSL was the most controversial part of the WebGPU API, and it was changed multiple times during the development of the API 3. The final syntax is a compromise between the different stakeholders, and it is designed to be easy to read and write, while still being efficient to compile and execute. Debugging and profiling tools are still in development for browsers and they are expected to be available in the near future. So, here we don't have any interactive editor to run each code snippet, but you can try them in your local environment by setting up the WebGPU API and the necessary pipeline to run the shader code.

Table of contents:

  1. Plain Types
  2. Variable Declaration
    1. Type Conversion
  3. Vectors
    1. Vector Accessors
    2. Swizzling
  4. Matrices
    1. Matrix Accessors
  5. Arrays
    1. Runtime Arrays
  6. Entry Points
  7. Shader Attributes
    1. Inter Stage Communication
  8. Flow Control
  9. Built-in Functions

1. Plain Types

  • i32 : 32 bit signed integer
  • u32 : 32 bit unsigned integer
  • f32 : 32 bit floating point number
  • bool : boolean value
  • f16 : 16 bit floating point number (f16 extention should be enabled 4 )

2. Variable Declaration

var b: bool = true; // mutable variable declaration
let n: i32 = 5; // immutable variable declaration
const PI: f32 = 3.14159; // compile time constant.
fn s1(r: f32) -> f32 {
return PI * r * r; // correct usage of constant
}
fn s2(r: f32) -> f32 {
const sc = PI * r * r; // ERROR! const can only be used with compile time expressions
return sc;
}

Note that in a function declaration, the return type is specified after the -> symbol.

2.1. Type Conversion

let a = 1; // a is an i32
let b = 2.0; // b is a f32
let c = a + b; // ERROR can't add an i32 to an f32
let c = f32(a) + b; // OK, a is converted to f32

3. Vectors

WGSL supports 3 types of vectors: vec2, vec3, and vec4. These are used to represent 2, 3, and 4 component vectors respectively.

let v2: vec2<f32> = vec2<f32>(1.0, 2.0);
let v3: vec3<f32> = vec3<f32>(1.0, 2.0, 3.0);
let v4: vec4<f32> = vec4<f32>(1.0, 2.0, 3.0, 4.0);

3.1. Vector Accessors

You can access the components of a vector using the following syntax:

let v: vec4<f32> = vec4<f32>(1.0, 2.0, 3.0, 4.0);
let v1: f32 = v.y; // using x, y, z, w accessors
let v2: f32 = v.g; // using r, g, b, a accessors
let v3: f32 = v[1]; // using array index accessors

Note that v1, v2, and v3 will all have the value 2.0 as they are accessing the second component of the vector.

3.2. Swizzling

Swizzling is a feature that allows you to access the components of a vector in a specific order. For example, if you have a vec4 and you want to access the x and y components, you can do so using the following syntax:

let v: vec4<f32> = vec4<f32>(1.0, 2.0, 3.0, 4.0);
let v1: vec2<f32> = v.xy;
let v2: vec3<f32> = v.zzy;
var v3: vec3<f32> = v.xxy;
v3.rgb = v.zzy; // ERROR! Swizzles can not appear on the left!

Note: there is a proposal to allow swizzling on the left side of an assignment, but it is not yet supported in WGSL. 5

4. Matrices

WGSL supports many types of matrices, such as mat2x2, mat3x4, mat4x4, etc. These are used to represent 2x2, 3x4, 4x4 matrices, respectively.

let m2: mat2x2<f32> = mat2x2<f32>(
vec2<f32>(1.0, 2.0),
vec2<f32>(3.0, 4.0)
);
let m4: mat4x4f = ...;

4.1. Matrix Accessors

You can access the components of a matrix using the following syntax:

let m: mat2x2<f32> = mat2x2<f32>(
vec2<f32>(1.0, 2.0),
vec2<f32>(3.0, 4.0)
);
let m1: f32 = m[1][0]; // accessing the second row, first column

5. Arrays

WGSL supports arrays of any type, including other arrays. The difference between an array and a vector is that an array has a fixed size, while a vector has a fixed number of components.

let a = array<i32, 3>(1, 2, 3);
let b = array<vec4f, 2>(
vec4f(1.0, 2.0, 3.0, 4.0),
vec4f(5.0, 6.0, 7.0, 8.0)
);

Note: As of version 1.0, WGSL cannot retrieve the length of a fixed-size array.

5.1. Runtime Arrays

WGSL also supports runtime arrays, which are arrays whose size is determined during program execution. These arrays can only be declared at the root scope of the shader with storage buffer resources and are the only arrays that can be specified without a predefined size. The size of a runtime array can be determined using the arrayLength function.

struct S1 {
color: vec4f,
size: f32,
verts: array<vec3f>,
}
@group(0) @binding(1) var<storage> foo: S1;
fn main() {
let numVerts = arrayLength(&foo.verts);
}

6. Entry Points

An entry point is a function that is called by the host application to start the execution of the shader. It is either a @vertex, @fragment, or @compute function.

@vertex
fn vertex_main() {
// vertex shader code
}
@fragment
fn fragment_main() {
// fragment shader code
}
@compute
fn compute_main() {
// compute shader code
}

7. Shader Attributes

Shader attributes are used to specify the input and output of a shader. They start with the @ symbol.

@location(number) // input/output of shaders

7.1. Inter Stage Communication

Inter-stage communication is done using the @location attribute. It is used to specify the location of an input or output variable in the shader.

struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
};
struct FragmentInput {
@location(1) uv: vec2<f32>,
@location(0) color: vec4<f32>,
};
@vertex fn vertex_main() -> VertexOutput {
var output: VertexOutput;
output.position = ...;
output.color = ...;
return output;
}
@fragment fn fragment_main(input: FragmentInput) ...

In the example above, the @location attribute is used to specify the location of the color attribute in the VertexOutput struct and the uv attribute in the FragmentInput struct.

The @builtin attribute is used to specify built-in attributes that are provided by the WebGPU API 6.

NameStageDirectionType
vertex_indexvertexinputu32
instance_indexvertexinputu32
clip_distancesvertexoutputarray<f32, N> (N ≤ 8)
positionvertexoutputvec4
fragmentinputvec4
front_facingfragmentinputbool
frag_depthfragmentoutputf32
sample_indexfragmentinputu32
sample_maskfragmentinputu32
fragmentoutputu32
local_invocation_idcomputeinputvec3
local_invocation_indexcomputeinputu32
global_invocation_idcomputeinputvec3
workgroup_idcomputeinputvec3
num_workgroupscomputeinputvec3

8. Flow Control

WGSL supports the some of the common flow control statements. the syntax is similar to Rust or C.

  • if statement

    if (condition) {
    // code block
    } else {
    // code block
    }
  • for loop

    for (var i = 0; i < 10; i++) {
    // code block
    }
  • while loop

    while (condition) {
    // code block
    }
  • loop statement

    var k = 0;
    loop {
    k = k + 1;
    if (k == 10) {
    break;
    }
    }
  • switch statement

    switch expression {
    default: { // default doesn't have to be the last case
    // code block
    }
    case 1: {
    // code block
    }
    case 2: {
    // code block
    }
    }
  • continue statement

    for (var i = 0; i < 10; i++) {
    if (i == 5) {
    continue;
    }
    // code block
    }
  • continuing block

    for (var i = 0; i < 10; ++i) {
    if (i == 5) {
    continue;
    }
    // code block
    continuing {
    // continue goes here
    }
    }
  • discard statement

    if (condition) {
    discard; // early return from the shader (fragment shader only)
    }

9. Built-in Functions

There are many built-in functions in WGSL that can be used to perform common operations 7. Some of the most commonly used functions are:

  • abs(x): Returns the absolute value of x.
  • ceil(x): Returns the smallest integer greater than or equal to x.
  • clamp(x, min, max): Clamps x to the range [min, max].
  • cos(x): Returns the cosine of x.
  • sin(x): Returns the sine of x.
  • cross(a, b): Returns the cross product of a and b.
  • degrees(x): Converts x from radians to degrees.
  • length(x): Returns the length of x from the origin.
  • smoothstep(edge0, edge1, x): Returns a smooth interpolation between edge0 and edge1 based on x.

We will Learn more about these functions in the next steps.

Further reading

Footnotes

  1. https://www.khronos.org/registry/SPIR-V/specs/unified1/SPIRV.html

  2. https://github.com/gpuweb/gpuweb/issues/593

  3. A brief explaination about the WGSL syntax by Corentin Wallez, Tech lead of WebGPU at Google and co-chair of the respective w3c working group https://youtu.be/RR4FZ9L4AF4?si=p0SeIxFEDVNCkdNp&t=2806 (46:47)

  4. https://www.w3.org/TR/WGSL/#extension-f16

  5. https://github.com/gpuweb/gpuweb/discussions/3478#discussioncomment-3738911

  6. https://www.w3.org/TR/WGSL/#builtin-inputs-outputs

  7. https://www.w3.org/TR/WGSL/#builtin-functions