Writing a GPU program usually involves sharing data between the host (CPU) and the device (GPU), in this case between JavaScript and WGSL.
Any misalignments or misinterpretations of data can lead to bugs that are hard to debug (No console.log on the GPU, I am afraid).
While data is strongly typed in WGSL shaders, we give up that type safety completely when writing and reading data in JavaScript/TypeScript.
This is precisely what TypeGPU data types help with.
Examples
Let’s look at some examples of defining custom data types using the typegpu/data module. If you’re familiar with Zod, then this style of schema definitions may already seem familiar.
import*as
import d
dfrom'typegpu/data';
const
const Circle: d.WgslStruct<{
centerPos:d.Vec3i;
radius:d.F32;
}>
Circle =
import d
d.
struct<{
centerPos:d.Vec3i;
radius:d.F32;
}>(props: {
centerPos: d.Vec3i;
radius: d.F32;
}): d.WgslStruct<{
centerPos:d.Vec3i;
radius:d.F32;
}>
export struct
Creates a struct schema that can be used to construct GPU buffers.
Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema).
The order of members matches the passed in properties object.
@exampleconst buffer = root.createBuffer(d.vec3i, d.vec3i(0, 1, 2)); // buffer holding a d.vec3i value, with an initial value of vec3i(0, 1, 2);
vec3i(2, 4, 0),
radius: number
radius: 0.2,
};
By defining the Circle struct in TypeScript via TypeGPU, in a similar way to how we would in WGSL, we gain access to its TypeScript type
definition, which we can use to validate our data values. When reading from or writing data to the GPU, the type of the JavaScript value
is inferred automatically, and it’s enforced by TypeScript. Thanks to that, whenever we mistakenly set or assume a wrong value for an object,
we get a type error, avoiding unnecessary debugging afterwards. That’s a big improvement to the development process.
@exampleconst buffer = root.createBuffer(d.vec3i, d.vec3i(0, 1, 2)); // buffer holding a d.vec3i value, with an initial value of vec3i(0, 1, 2);
vec3i(2, 4, 0),
};
const
const diam:number
diam =
const redCircle1:InferRecord<{
centerPos:d.Vec3i;
radius:d.F32;
}>
redCircle1.rad * 2;
Error ts(2339) ― Property 'rad' does not exist on type 'InferRecord<{ centerPos: Vec3i; radius: F32; }>'.
Defined data structures automatically measure and hold information about their memory layout parameters, which is useful for writing to and reading data from the GPU.
functionsizeOf(schema: d.AnyData):number
Returns the size (in bytes) of data represented by the schema.
sizeOf(
const Circle: d.WgslStruct<{
centerPos:d.Vec3i;
radius:d.F32;
}>
Circle) // 16
functionalignmentOf(schema: d.AnyData):number
Returns the alignment (in bytes) of data represented by the schema.
alignmentOf(
const Circle: d.WgslStruct<{
centerPos:d.Vec3i;
radius:d.F32;
}>
Circle) // 16
TypeGPU data types are essential for the library’s automated data marshalling capabilities.
Scalars, Vectors & Matrices
There are a few ways to categorize numeric data-types in TypeGPU.
Characteristic (floating-point f, signed int i, unsigned int u).
Size in bits (8, 16, 32).
Number of components (1, 2, 3, 4, …).
d.f32, d.i32 and d.u32 all represent single-component 32-bit numeric values. When reading/writing values
in JS, they are all seen as just number.
Vectors (d.v2f, d.v3f, …) are interpreted in JS as special objects, which can be created by “calling” the
corresponding schema with numeric components, e.g.:
@exampleconst buffer = root.createBuffer(d.mat3x3f, d.mat3x3f()); // buffer holding a d.mat3x3f value, with an initial value of mat3x3f filled with zeros
@exampleconst buffer = root.createBuffer(d.mat2x2f, d.mat2x2f(0, 1, 2, 3)); // buffer holding a d.mat2x2f value, with an initial value of ((0, 1), (2, 3))
Values of different types can be grouped into structs.
import*as
import d
dfrom'typegpu/data';
const
const Boid: d.WgslStruct<{
position:d.Vec3u;
velocity:d.Vec3f;
color:d.Vec4f;
isActive:d.Bool;
}>
Boid =
import d
d.
struct<{
position:d.Vec3u;
velocity:d.Vec3f;
color:d.Vec4f;
isActive:d.Bool;
}>(props: {
position: d.Vec3u;
velocity: d.Vec3f;
color: d.Vec4f;
isActive: d.Bool;
}): d.WgslStruct<{
...;
}>
export struct
Creates a struct schema that can be used to construct GPU buffers.
Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema).
The order of members matches the passed in properties object.
@exampleconst 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(1.0, 0.2, 0.3, 1.0),
isAct
isAct: any
isActive
});
Struct schemas adjust the padding and alignment automatically, so that they comply with WebGPU’s memory alignment rules.
It is also possible to override default byte alignment and size for particular fields via the align and size functions.
const
const Boid: d.WgslStruct<{
position:d.Decorated<d.Vec3u, [d.Align<32>]>;
velocity:d.Vec3f;
color:d.Vec4f;
isActive:d.Decorated<d.Bool, [d.Size<8>]>;
}>
Boid =
import d
d.
struct<{
position:d.Decorated<d.Vec3u, [d.Align<32>]>;
velocity:d.Vec3f;
color:d.Vec4f;
isActive:d.Decorated<d.Bool, [d.Size<8>]>;
}>(props: {
position: d.Decorated<d.Vec3u, [d.Align<32>]>;
velocity: d.Vec3f;
color: d.Vec4f;
isActive: d.Decorated<d.Bool, [d.Size<8>]>;
}): d.WgslStruct<...>
export struct
Creates a struct schema that can be used to construct GPU buffers.
Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema).
The order of members matches the passed in properties object.
Gives the wrapped data-type a custom byte alignment. Useful in order to
fulfill uniform alignment requirements.
@exampleconst Data = d.struct({
a: u32, // takes up 4 bytes
// 12 bytes of padding, because b is custom aligned to multiples of 16 bytes
b: d.align(16, u32),
});
@param ― alignment The multiple of bytes this data should align itself to.
@param ― data The data-type to align.
align(32,
import d
d.
const vec3u: d.Vec3u
export vec3u
Schema representing vec3u - a vector with 3 elements of type u32.
Also a constructor function for this vector value.
Adds padding bytes after the wrapped data-type, until the whole value takes up size bytes.
@exampleconst Data = d.struct({
a: d.size(16, u32), // takes up 16 bytes, instead of 4
b: u32, // starts at byte 16, because a has a custom size
});
@param ― size The amount of bytes that should be reserved for this data-type.
@param ― data The data-type to wrap.
size(8,
import d
d.
const bool: d.Bool
export bool
A schema that represents a boolean value. (equivalent to bool in WGSL)
bool), // Has a minimum size of 8 bytes
});
Arrays
To define arrays of known constant length, use the d.arrayOf function. It accepts as arguments the array’s elements data type constructor and the length of the array.