Skip to content

Data Schemas

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 d from 'typegpu/data';
const Circle = d.struct({
centerPos: d.vec3i,
radius: d.f32,
});
// type Circle = {
// centerPos: d.vec3i;
// radius: number;
// }
type Circle = d.Parsed<typeof Circle>;
const redCircle: Circle = {
centerPos: d.vec3i(2, 4, 0),
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.

const redCircle: Circle = {
// Error: Property 'z' is missing in type 'vec2i' but required in type 'vec3i'.
centerPos: d.vec2i(2, 4),
radius: 0.2,
};
const redCircle: Circle = {
centerPos: d.vec3i(2, 4, 0),
// Error: Type 'string' is not assignable to type 'number'.
radius: "0.2",
};
// Error: Property 'radius' is missing in type '{ centerPos: vec3i; }'
// but required in type '{ centerPos: vec3i; radius: number; }'.
const redCircle: Circle = {
centerPos: d.vec3i(2, 4, 0),
};
// Error: Property 'rad' does not exist on type '{ centerPos: vec3i; radius: number; }'.
const diam = redCircle.rad * 2;

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.

Circle.size // 16
Circle.byteAlignment // 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.vec2f, d.vec3f, …) are interpreted in JS as special objects, which can be created by “calling” the schema with numeric components, e.g.:

const v0 = d.vec3f(1.1, 2.5, 3.3); // => vec3f
const v1 = d.vec4u(3, 4, 5, 2); // => vec4u
// ...

Matrices work in a similar way.

const mat0 = d.mat3x3f(
1.1, 2.5, 3.3,
1.2, 2.6, 3.4,
1.3, 2.7, 3.5,
); // => mat3x3f
const mat1 = d.mat2x2f(3, 4, 5, 2); // => mat2x2f
// ...

For a comprehensive list of all available schemas, see the Data Schema Cheatsheet.

Structs

Values of different types can be grouped into structs.

import * as d from 'typegpu/data';
const Boid = d.struct({
position: d.vec3u,
velocity: d.vec3f,
color: d.vec4f,
isActive: d.bool,
});
const boid: d.Parsed<typeof Boid> = {
position: d.vec3u(0, 0, 0),
velocity: d.vec3f(1, 0.5, 0.5),
color: d.vec4f(1.0, 0.2, 0.3, 1.0),
isActive: true,
};

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 Boid = d.struct({
position: d.align(32, d.vec3u), // Aligned to multiples of 32 bytes
velocity: d.vec3f,
color: d.vec4f,
isActive: d.size(8, d.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.

const RecentResults = d.arrayOf(d.f32, 4);
type RecentResults = Parsed<typeof RecentResults>; // => number[]
const recentResults: RecentResults = [
1, 0, 0.5, 20
];