Table of Contents

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU WGSL

For an in-depth overview of WGSL see Tour of WGSL. There’s also the actual WGSL spec though it can be hard to process since it’s written for language lawyers 😂

This article assumes you already know how to program so that that just by looking at examples of WGSL you should be likely be able to kind of grok what you see. It’s probably way too terse but hopefully it can give you some help in understanding and writing WGSL shader programs.

WGSL is strictly typed

Unlike JavaScript, WGSL requires knowing the types of every variable, struct field, function parameter and function return type. If you’ve used typescript, rust, C++, C#, Java, Swift, Kotlin, etc then you’re used to this.

plain types

The plain types in WGSL are:

  • i32 a 32 bit signed integer
  • u32 a 32 bit unsigned integer
  • f32 a 32 bit floating point number
  • bool a boolean value
  • f16 a 16 bit floating point number (this is an optional feature you need to check for and request)

variable declaration

In JavaScript you can declare variables and functions like this.

var a = 1;
let c = 3;
function d(e) { return e * 2; }

In WGSL the full form of those would be

var a: f32 = 1;
let c: f32 = 3;
fn d(e: f32) -> f32 { return e * 2; }

The important thing to note from above is having to add : <type> like : f32 for the variable declarations and -> <type> to function declarations.

auto types

WGSL has a shortcut for variables. Similar to typescript, if you don’t declare the type of the variable then it automatically becomes the type of the expression on the right.

fn foo() -> bool { return false; }

var a = 1;     // a is an i32
let b = 2.0;   // b is an f32
var c = 3u;    // c is a u32
var d = foo(); // d is bool

type conversion

Further, being strictly type means you often have to convert types.

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

The fix is to convert one to the other.

let a = 1;     // a is an i32
let b = 2.0;   // b is a f32
let c = f32(a) + b; // ok

but!, WGSL has what are called “AbstractInt” and “AbstractFloat”. You can think of them as numbers that have not yet decided their type. These are compile time only features.

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 d = 1 + 2.0;      // d is a f32

numeric suffixes

2i   // i32
3u   // u32
4f   // f32
4.5f // f32
5h   // f16
5.6h // f16
6    // AbstractInt
7.0  // AbstractFloat

let var and const mean different things in WGSL vs JavaScript

In JavaScript var is a variable with function scope. let is a variable with block scope. const is a constant variable (can’t be changed) [1] with block scope.

In WGSL all variables have block scope. var is a variable that has storage and so is mutable. let is a constant value.

fn foo() {
  let a = 1;
*  a = a + 1;  // ERROR: a is a constant expression
  var b = 2;
  b = b + 1;  // ok
}

const is not a variable, it’s a compile time constant. You can not use const for something that happens at runtime.

const one = 1;              // ok
const two = one * 2;        // ok
const PI = radians(180.0);  // ok

fn add(a: f32, b: f32) -> f32 {
*  const result = a + b;   // ERROR! const can only be used with compile time expressions
  return result;
}

vector types

WGSL has 3 vector types vec2, vec3, and vec4. Their basic style is vec?<type> so vec2<i32> (a vector of two i32s), vec3<f32> (a vector of 3 f32s), vec4<u32>(a vector of 4 u32s), vec3<bool> a vector of 3 boolean values.

Examples:

let a = vec2<i32>(1, -2);
let b = vec3<f32>(3.4, 5.6, 7.8);
let c = vec4<u32>(9, 10, 11, 12);

accessors

You can access the values inside a vector with various accessors.

let a = vec4<f32>(1, 2, 3, 4);
let b = a.z;   // via x,y,z,w
let c = a.b;   // via r,g,b,a
let d = a[2];  // via array element accessors

Above, b, c, and d are all the same. They are all accessing the 3rd element of a. They are all ‘3’.

swizzles

You can also access more than 1 element.

let a = vec4<f32>(1, 2, 3, 4);
let b = a.zx;   // via x,y,z,w
let c = a.br;   // via r,g,b,a
let d = vec2<f32>(a[2], a[0]);

Above, b, c, and d are all the same. They are all a vec2<f32>(3, 1).

You can also repeat elements.

let a = vec4<f32>(1, 2, 3, 4);
let b = vec3<f32>(a.z, a.z, a.y);
let c = a.zzy;

Above b and c are the same. They both vec3<f32> who contents is 3, 3, 2.

vector shortcuts

There are shortcuts for the base types. Change the <i32> => i, <f32> => f, <u32> to u and <f16> to h so

let a = vec4<f32>(1, 2, 3, 4);
let b = vec4f(1, 2, 3, 4);

a and b are the same type.

vector construction

vectors can be constructed with smaller types.

let a = vec4f(1, 2, 3, 4);
let b = vec2f(2, 3);
let c = vec4f(1, b, 4);
let d = vec4f(1, a.yz, 4);
let e = vec4f(a.xyz, 4);
let f = vec4f(1, a.yzw);

a, c, d, e and f are the same.

vector math

You can do math on vectors.

let a = vec4f(1, 2, 3, 4);
let b = vec4f(5, 6, 7, 8);
let c = a + b;  // c is vec4f(6, 8, 10, 12)
let d = a * b;  // d is vec4f(5, 12, 21, 32)
let e = a - b;  // e is vec4f(-4, -4, -4, -4)

Many functions also work on vectors.

let a = vec4f(1, 2, 3, 4);
let b = vec4f(5, 6, 7, 8);
let c = mix(a, b, 0.5);                   // c is vec4f(3, 4, 5, 6)
let d = mix(a, b, vec4f(0, 0.5, 0.5, 1)); // d is vec4f(1, 4, 5, 8)

matrices

WGSL has a bunch of matrix types. Matrices are arrays of vectors. The format is mat<numVectors>x<vectorSize><<type>> so for example mat3x4<f32> is an array of 3 vec4<f32>s. Like vectors, matrices have the same shortcuts

let a: mat4x4<f32> = ...
let b: mat4x4f = ...

a and b are the same type.

matrix vector access

You can reference a vector of a matrix with array syntax.

let a = mat4x4f(...);
let b = a[2];   // b is a vec4f of the 3rd vector of a

The most common matrix type for 3D computation is mat4x4f and can be multiplied directly with a vec4f to produce another vec4f.

let a = mat4x4f(....);
let b = vec4f(1, 2, 3, 4);
let c = a * b;  // c is a vec4f and the result of a * b

arrays

Arrays in WGSL are declared with the array<type, numElements> syntax.

let a = array<f32, 5>;   // an array of five f32s
let b = array<vec4f, 6>; // an array of six vec4fs

But there’s also the array constructor. It takes any number of arguments and returns an array. The arguments must all be of the same type.

let arrOf3Vec3fsA = array(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9));
let arrOf3Vec3fsB = array<vec3f, 3>(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9));

Above arrOf3Vec3fsA is the same as arrOf3Vec3fsB.

Unfortunately, as of version 1 of WGSL there is no way to get the size of fixed size array.

runtime sized arrays

Arrays that are at the root scope storage declarations or as the last field in a root scope struct are the only arrays that can be specified with no size.

struct Stuff {
  color: vec4f,
  size: f32,
  verts: array<vec3f>,
};
@group(0) @binding(0) var<storage> foo: array<mat4x4f>;
@group(0) @binding(1) var<storage> bar: Stuff;

The number of elements in foo and in bar.verts is defined by the settings of the bind group used at runtime. You can query this size in your WGSL with arrayLength.

@group(0) @binding(0) var<storage> foo: array<mat4x4f>;
@group(0) @binding(1) var<storage> bar: Stuff;

...
  let numMatrices = arrayLength(&foo);
  let numVerts = arrayLength(&bar.verts);

functions

Functions in WGSL follow the pattern fn name(parameters) -> returnType { ..body... }.

fn add(a: f32, b: f32) -> f32 {
  return a + b;
}

entry points

WGSL programs need an entry point. An entry point is designated by either @vertex, @fragment or @compute.

@vertex fn myFunc(a: f32, b: f32) -> @builtin(position): vec4f {
  return vec4f(0, 0, 0, 0);
}

shaders only use what their entry point accesses

@group(0) @binding(0) var<uniforms> uni: vec4f;

vec4f fn foo() {
  return uni;
}

@vertex fn vs1(): @builtin(position) vec4f {
  return vec4f(0);
}

@vertex fn vs2(): @builtin(position) vec4f {
  return foo();
}

Above uni is not accessed by vs1 and so will not show up as a required binding if you use vs1 in a pipeline. vs2 does reference uni indirectly through calling foo so it will show up as a required binding when using vs2 in a pipeline.

attributes

The word attributes has 2 meanings in WebGPU. One is vertex attributes which is covered in the article on vertex buffers. The other is in WGSL where an attribute starts with @.

@location(number)

@location(number) is used to defined inputs and outputs of shaders.

vertex shader inputs

For a vertex shader, inputs are defined by the @location attributes of the entry point function of the vertex shader.

@vertex vs1(@location(0) foo: f32, @location(1) bar: vec4f) ...

struct Stuff {
  @location(0) foo: f32,
  @location(1) bar: vec4f,
};
@vertex vs2(s: Stuff) ...

Both vs1 and vs2 declare inputs to the vertex shader on locations 0 and 1 which need to be supplied by vertex buffers.

inter stage variables

For inter stage variables, @location attributes define the location where the variables are passed between shaders.

struct VSOut {
  @builtin(position) pos: vec4f,
  @location(0) color: vec4f,
  @location(1) texcoords: vec2f,
};

struct FSIn {
  @location(1) uv: vec2f,
  @location(0) diffuse: vec4f,
};

@vertex fn foo(...) -> VSOut { ... }
@fragment fn bar(moo: FSIn) ... 

Above, the vertex shader foo passes color as vec4f on location(0) and texcoords as a vec2f on location(1). The fragment shader bar receives them as uv and diffuse because their locations match.

fragment shader outputs

For fragment shaders, @location specifies which GPURenderPassDescriptor.colorAttachment to store the result in.

struct FSOut {
  @location(0) albedo: vec4f;
  @location(1) normal: vec4f;
}
@fragment fn bar(...) -> FSOut { ... }

@builtin(name)

The @builtin attribute is used to specify that a particular variable’s value comes from a built-in feature of WebGPU.

@vertex fn vs1(@builtin(vertex_index) foo: u32, @builtin(instance_index) bar: u32) ... {
  ...
}

Above foo gets its value from the builtin vertex_index and bar gets its value from the builtin instance_index.

struct Foo {
  @builtin(vertex_index) vNdx: u32,
  @builtin(instance_index) iNdx: u32,
}
@vertex fn vs1(blap: Foo) ... {
  ...
}

Above blap.vNdx gets its value from the builtin vertex_index and blap.iNdx gets its value from the builtin instance_index.

Builtin Name Stage IO Type Description
vertex_index vertex input u32 Index of the current vertex within the current API-level draw command, independent of draw instancing.

For a non-indexed draw, the first vertex has an index equal to the firstVertex argument of the draw, whether provided directly or indirectly. The index is incremented by one for each additional vertex in the draw instance.

For an indexed draw, the index is equal to the index buffer entry for the vertex, plus the baseVertex argument of the draw, whether provided directly or indirectly.

instance_index vertex input u32 Instance index of the current vertex within the current API-level draw command.

The first instance has an index equal to the firstInstance argument of the draw, whether provided directly or indirectly. The index is incremented by one for each additional instance in the draw.

position vertex output vec4<f32> Output position of the current vertex, using homogeneous coordinates. After homogeneous normalization (where each of the x, y, and z components are divided by the w component), the position is in the WebGPU normalized device coordinate space. See WebGPU § 3.3 Coordinate Systems.
fragment input vec4<f32> Framebuffer position of the current fragment in framebuffer space. (The x, y, and z components have already been scaled such that w is now 1.) See WebGPU § 3.3 Coordinate Systems.
front_facing fragment input bool True when the current fragment is on a front-facing primitive. False otherwise.
frag_depth fragment output f32 Updated depth of the fragment, in the viewport depth range. See WebGPU § 3.3 Coordinate Systems.
local_invocation_id compute input vec3<u32> The current invocation’s local invocation ID, i.e. its position in the workgroup grid.
local_invocation_index compute input u32 The current invocation’s local invocation index, a linearized index of the invocation’s position within the workgroup grid.
global_invocation_id compute input vec3<u32> The current invocation’s global invocation ID, i.e. its position in the compute shader grid.
workgroup_id compute input vec3<u32> The current invocation’s workgroup ID, i.e. the position of the workgroup in the workgroup grid.
num_workgroups compute input vec3<u32> The dispatch size, vec<u32>(group_count_x, group_count_y, group_count_z), of the compute shader dispatched by the API.
sample_index fragment input u32 Sample index for the current fragment. The value is least 0 and at most sampleCount-1, where sampleCount is the MSAA sample count specified for the GPU render pipeline.
See WebGPU § 10.3 GPURenderPipeline.
sample_mask fragment input u32 Sample coverage mask for the current fragment. It contains a bitmask indicating which samples in this fragment are covered by the primitive being rendered.
See WebGPU § 23.3.11 Sample Masking.
fragment output u32 Sample coverage mask control for the current fragment. The last value written to this variable becomes the shader-output mask. Zero bits in the written value will cause corresponding samples in the color attachments to be discarded.
See WebGPU § 23.3.11 Sample Masking.

flow control

Like most computer languages, WGSL has flow control statements.

for

  for (var i = 0; i < 10; i++) { ... }

if

    if (i < 5) {
      ...
    } else if (i > 7) {
      ..
    } else {
      ...
    }

while

  var j = 0;
  while (j < 5) {
    ...
    j++;
  }

loop

  var k = 0;
  loop {
    k++;
    if (k >= 5) {
      break;
    }
  }

break

  var k = 0;
  loop {
    k++;
    if (k >= 5) {
      break;
    }
  }

break if

  var k = 0;
  loop {
    k++;
    break if (k >= 5);
  }

continue

  for (var i = 0; i < 10; ++i) {
    if (i % 2 == 1) {
      continue;
    }
    ...
  }

continuing

  for (var i = 0; i < 10; ++i) {
    if (i % 2 == 1) {
      continue;
    }
    ...

    continuing {
      // continue goes here
      ...
    }
  }

discard

   if (v < 0.5) {
     discard;
   }

discard exits the shader. It can only be used in a fragment shader.

switch

var a : i32;
let x : i32 = generateValue();
switch x {
  case 0: {      // The colon is optional
    a = 1;
  }
  default {      // The default need not appear last
    a = 2;
  }
  case 1, 2, {   // Multiple selector values can be used
    a = 3;
  }
  case 3, {      // The trailing comma is optional
    a = 4;
  }
  case 4 {
    a = 5;
  }
}

switch only works with u32 or i32 and cases must be constants.

Operators

Name Operators Associativity Binding
Parenthesized (...)
Primary a(), a[], a.b Left-to-right
Unary -a, !a, ~a, *a, &a Right-to-left All above
Multiplicative a * b, a / b, a % b Left-to-right All above
Additive a + b, a - b Left-to-right All above
Shift a << b, a >> b Requires parentheses Unary
Relational a < b, a > b, a <= b, a >= b, a == b, a != b Requires parentheses All above
Binary AND a & b Left-to-right Unary
Binary XOR a ^ b Left-to-right Unary
Binary OR a | b Left-to-right Unary
Short-circuit AND a && b Left-to-right Relational
Short-circuit OR a || b Left-to-right Relational

Builtin functions

See the WGSL Function reference.

Differences with other languages

if, while, switch, break-if expressions don’t need parenthesizes.

if a < 5 {
  doTheThing();
}

no ternary operator

Many languages have a ternary operator condition ? trueExpression : falseExpression WGSL does not. WGSL does have select.

  let a = select(falseExpression, trueExpression, condition);

++ and -- are statements, not expressions.

Many languages have pre-increment and post-increment operators.

// JavaScript
let a = 5;
let b = a++;  // b = 5, a = 6  (post-increment)
let c = ++a;  // c = 7, a = 7  (pre-increment)

WGSL has neither. It just has the increment and decrement statements.

// WGSL
var a = 5;
a++;          // is now 6
*++a;          // ERROR: no such thing as pre-increment
*let b = a++;  // ERROR: a++ is not an expression, it's a statement

+=, -= are not expressions, they’re assignment statements

// JavaScript
let a = 5;
a += 2;          // a = 7
let b = a += 2;  // a = 9, b = 9
// WGSL
let a = 5;
a += 2;           // a is 7
*let b = a += 2;  // ERROR: a += 2 is not an expression

Swizzles can not appear on the left

In some languages but not WGSL.

var color = vec4f(0.25, 0.5, 0.75, 1);
*color.rgb = color.bgr; // ERROR
color = vec4(color.bgr, color.a);  // Ok

Note: there is a proposal to add this feature.

Phony assignment to _

_ is a special variable you can assign to to make something appear used but not actually use it.

@group(0) @binding(0) var<uniforms> uni1: vec4f;
@group(0) @binding(0) var<uniforms> uni2: mat4x4f;

@vertex fn vs1(): @builtin(position) vec4f {
  return vec4f(0);
}

@vertex fn vs2(): @builtin(position) vec4f {
  _ = uni1;
  _ = uni2;
  return vec4f(0);
}

Above neither uni1 nor uni2 are accessed by vs1 and so they will not show up as a required bindings if you use vs1 in a pipeline. vs2 does reference both uni1 and uni2 so they will both show up as a required bindings when using vs2 in a pipeline.


  1. Variables in JavaScript hold base types of undefined, null, boolean, number, string, reference-to-object. It can be confusing for people new to programming that const o = {name: 'foo'}; o.name = 'bar'; works because o was declared as const. The thing is, o is const. It is a constant reference to the object. You can not change which object o references. You can change object itself. ↩︎

Questions? Ask on stackoverflow.
Suggestion? Request? Issue? Bug?
comments powered by Disqus