Skip to content

TGSL

TGSL (TypeGPU Shading Language) is a subset of JavaScript used to define functions that run on the GPU via TypeGPU. It works by transpiling JavaScript into a compact AST format, called tinyest, which is then used to generate equivalent WGSL.

You can check the current state of supported JavaScript syntax in the tinyest-for-wgsl repository.

For the TGSL functions to work, you need to use the dedicated build plugin — unplugin-typegpu.

Instead of using a WGSL code string, you can pass TGSL to the tgpu function shell as an argument instead. Functions from the WGSL standard library (distance, arrayLength, workgroupBarrier, etc.) are accessible through the typegpu/std endpoint. The package also includes functions for vector and matrix operators (add, eq, lt…).

import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import * as std from 'typegpu/std';
const MAX_OBSTACLES = 4;
const obstacles = root
.createReadonly(d.arrayOf(d.struct({
center: d.vec2i,
size: d.vec2i,
enabled: d.u32,
}), MAX_OBSTACLES));
const isInsideObstacle = tgpu.fn([d.i32, d.i32], d.bool)((x, y) => {
for (let obsIdx = 0; obsIdx < MAX_OBSTACLES; obsIdx++) {
const obs = obstacles.$[obsIdx];
if (obs.enabled === 0) {
continue;
}
const minX = std.max(0, obs.center.x - d.i32(obs.size.x / 2));
const maxX = std.min(gridSize, obs.center.x + d.i32(obs.size.x / 2));
const minY = std.max(0, obs.center.y - d.i32(obs.size.y / 2));
const maxY = std.min(gridSize, obs.center.y + d.i32(obs.size.y / 2));
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
return true;
}
}
return false;
});
import {
oklabGamutClip,
oklabGamutClipAlphaAccess,
oklabGamutClipSlot,
oklabToLinearRgb,
oklabToRgb,
} from '@typegpu/color';
import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import { any, cos, floor, gt, lt, mul, select, sin } from 'typegpu/std';
const mainFragment = tgpu['~unstable'].fragmentFn({
in: { uv: d.vec2f },
out: d.vec4f,
})((input) => {
const hue = layout.$.uniforms.hue;
const pos = scaleView(input.uv);
const lab = d.vec3f(
pos.y,
mul(pos.x, d.vec2f(cos(hue), sin(hue))),
);
const rgb = oklabToLinearRgb(lab);
const outOfGamut = any(lt(rgb, d.vec3f(0))) || any(gt(rgb, d.vec3f(1)));
const clipLab = oklabGamutClipSlot.value(lab);
const color = oklabToRgb(lab);
const patternScaled = patternSlot.value(input.uv, clipLab) * 0.1 + 0.9;
return d.vec4f(select(color, mul(patternScaled, color), outOfGamut), 1);
});

Sometimes, we are unable to recognize functions that are supposed to be TGSL. For that case, we have a “kernel” directive.

const patternFn = tgpu.fn([d.vec2f], d.f32);
const patternCheckers = patternFn((uv) => {
'kernel';
const suv = floor(mul(20, uv));
return suv.x + suv.y - 2 * floor((suv.x + suv.y) * 0.5);
});
const patternSolid = patternFn(() => {
'kernel';
return 1;
});

TGSL-implemented functions can also be invoked on the CPU, as along as they do not use any GPU-exclusive functionalities, like buffers or textures (regardless of whether they are marked as “kernel” or not).

  • TGSL limitations — For a function to be valid TGSL, it must consist only of supported JS syntax (again, see tinyest-for-wgsl repository), possibly including references to bound buffers, constant variables defined outside of the function, other TGSL functions etc. This means that, for example, console.log() calls will not work on the GPU.

  • Differences between JS on the CPU and GPU — TGSL is developed to work on the GPU the same as on the CPU as much as possible, however because of the fundamental differences between the JavaScript and WGSL languages, it is not guaranteed to always be the case.

    Currently the biggest known difference is that vectors, matrices and structs are treated as reference types in JavaScript and value types in WGSL. That is, on the WGSL side, the assignment operator copies the value instead of the reference, and two different vectors can be equal to each other if only they store the same values, unlike in JS, where they need to point to the same reference. To somehow alleviate this issue, when passing arguments to tgpu functions on JS side, we perform a deep copy of them (note that in WGSL arguments are immutable by default).

    When using TGSL on the GPU, the behavior is that of WGSL, not JS, as one would expect. Therefore some WGSL knowledge is still required, even when opting out for TGSL.

  • .value — Objects that have different types on the CPU and on the GPU (like buffers, layouts, slots etc.) need to be accessed via the value property in TGSL functions (or the $ property alias). This is different from how they appear in WGSL-implemented ones.

import
const tgpu: {
fn: {
<Args extends d.AnyData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
<Args extends d.AnyData[] | [], Return extends d.AnyData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
};
... 7 more ...;
'~unstable': {
...;
};
}
tgpu
from 'typegpu';
import * as
import d
d
from 'typegpu/data';
const
const root: TgpuRoot
root
= await
const tgpu: {
fn: {
<Args extends d.AnyData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
<Args extends d.AnyData[] | [], Return extends d.AnyData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
};
... 7 more ...;
'~unstable': {
...;
};
}
tgpu
.
init: (options?: InitOptions) => Promise<TgpuRoot>

Requests a new GPU device and creates a root around it. If a specific device should be used instead, use

@seeinitFromDevice. *

@example

When given no options, the function will ask the browser for a suitable GPU device.

const root = await tgpu.init();

@example

If there are specific options that should be used when requesting a device, you can pass those in.

const adapterOptions: GPURequestAdapterOptions = ...;
const deviceDescriptor: GPUDeviceDescriptor = ...;
const root = await tgpu.init({ adapter: adapterOptions, device: deviceDescriptor });

init
();
const
const bgColor: TgpuUniform<d.Vec4f>
bgColor
=
const root: TgpuRoot
root
.
TgpuRoot.createUniform<d.Vec4f>(typeSchema: d.Vec4f, initial?: d.v4f | undefined): TgpuUniform<d.Vec4f> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@paramtypeSchema The type of data that this buffer will hold.

@paraminitial The initial value of the buffer. (optional)

createUniform
(
import d
d
.
const vec4f: d.Vec4f
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

vec4f
,
import d
d
.
function vec4f(x: number, y: number, z: number, w: number): d.v4f (+9 overloads)
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

vec4f
(0.114, 0.447, 0.941, 1));
const
const fragmentTgsl: TgpuFragmentFn<{}, d.Vec4f>
fragmentTgsl
=
const tgpu: {
fn: {
<Args extends d.AnyData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
<Args extends d.AnyData[] | [], Return extends d.AnyData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
};
... 7 more ...;
'~unstable': {
...;
};
}
tgpu
['~unstable'].
fragmentFn: <d.Vec4f>(options: {
out: d.Vec4f;
}) => TgpuFragmentFnShell<{}, d.Vec4f> (+1 overload)
fragmentFn
({
out: d.Vec4f
out
:
import d
d
.
const vec4f: d.Vec4f
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

vec4f
})(() => {
return
const bgColor: TgpuUniform<d.Vec4f>
bgColor
.$;
TgpuUniform<Vec4f>.$: d.v4f
});
const
const fragmentWgsl: TgpuFragmentFn<{}, d.Vec4f>
fragmentWgsl
=
const tgpu: {
fn: {
<Args extends d.AnyData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, d.Void>;
<Args extends d.AnyData[] | [], Return extends d.AnyData>(argTypes: Args, returnType: Return): TgpuFnShell<Args, Return>;
};
... 7 more ...;
'~unstable': {
...;
};
}
tgpu
['~unstable'].
fragmentFn: <d.Vec4f>(options: {
out: d.Vec4f;
}) => TgpuFragmentFnShell<{}, d.Vec4f> (+1 overload)
fragmentFn
({
out: d.Vec4f
out
:
import d
d
.
const vec4f: d.Vec4f
export vec4f

Schema representing vec4f - a vector with 4 elements of type f32. Also a constructor function for this vector value.

@example const vector = d.vec4f(); // (0.0, 0.0, 0.0, 0.0) const vector = d.vec4f(1); // (1.0, 1.0, 1.0, 1.0) const vector = d.vec4f(1, 2, 3, 4.5); // (1.0, 2.0, 3.0, 4.5)

@example const buffer = root.createBuffer(d.vec4f, d.vec4f(0, 1, 2, 3)); // buffer holding a d.vec4f value, with an initial value of vec4f(0, 1, 2, 3);

vec4f
})`{
return bgColor;
}
`.
TgpuFragmentFn<{}, Vec4f>.$uses(dependencyMap: Record<string, unknown>): TgpuFragmentFn<{}, d.Vec4f>
$uses
({
bgColor: TgpuUniform<d.Vec4f>
bgColor
});
  • Operators — JavaScript does not support operator overloading. This means that, while you can still use operators for numbers, you have to use supplementary functions from typegpu/std (add, mul, eq, lt, ge…) for operations involving vectors and matrices.

  • When to use TGSL instead of WGSL — Writing the code using TGSL has a few significant advantages. It allows defining utils only once and using them both as a kernel and host functions, as well as enables complete syntax highlighting and autocomplete in TypeGPU function definitions, leading to a better developer UX. However, it sometimes might be better to choose WGSL for certain functions. Since JavaScript doesn’t support operator overloading, functions including complex matrix operations can be more readable in WGSL. Writing WGSL becomes a necessity whenever TGSL does not support some feature or standard library function quite yet. Luckily, you don’t have to choose one or the other for the entire project. It is possible to mix and match WGSL and TGSL at every step of the way.