Skip to content

Buffers

Memory on the GPU can be allocated and managed through buffers. That way, WGSL shaders can be provided with an additional context, or retrieve the results of parallel computation back to JS. When creating a buffer, a schema for the contained values has to be provided, which allows for:

  • Calculating the required size of the buffer,
  • Automatic conversion to-and-from a binary representation,
  • Type-safe APIs for writing and reading.

As an example, let’s create a buffer for storing particles.

import tgpu from 'typegpu';
import * as d from 'typegpu/data';
// Defining a struct type
const Particle = d.struct({
position: d.vec3f,
velocity: d.vec3f,
health: d.f32,
});
const root = await tgpu.init();
// Creating and initializing a buffer.
const buffer = root
.createBuffer(
// Can hold an array of 100 particles
d.arrayOf(Particle, 100),
// Initial value
Array.from({ length: 100 }).map(() => ({
position: vec3f(Math.random(), 2, Math.random()),
velocity: vec3f(0, 9.8, 0),
health: 100,
})),
);
// ^? TgpuBuffer<TgpuArray<TgpuStruct<{
// position: Vec3f,
// velocity: Vec3f,
// health: F32,
// }>>>
// -
// --
// --- Using in shader...
// --
// -
// Reading from the buffer
const value = await buffer.read();
// ^? { position: vec3f, velocity: vec3f, health: number }[]
// Using the value
console.log(value);

This buffer can then be used and/or updated by a WGSL shader.

Creating a buffer

To create a buffer, you will need to define its schema by composing data types imported from typegpu/data. Every WGSL data-type can be represented as JS schemas, including structs and arrays. They will be explored in more detail in a following chapter.

const countBuffer = root.createBuffer(d.u32);
// ^? TgpuBuffer<d.U32>
const listBuffer = root
.createBuffer(d.arrayOf(d.f32));
// ^? TgpuBuffer<d.TgpuArray<d.F32>>
const uniformsBuffer = root
.createBuffer(d.struct({ a: d.f32, b: d.f32 }));
// ^? TgpuBuffer<d.TgpuStruct<{ a: d.F32, b: d.F32 }>>

Usage flags

To be able to use these buffers in WGSL shaders, we have to declare their usage upfront with .$usage(...).

const buffer = root.createBuffer(d.u32)
.$usage('uniform')
.$usage('storage');

You can also add all flags in a single $usage().

const buffer = root.createBuffer(d.u32)
.$usage('uniform', 'storage');

Initial value

You can also pass an initial value to the root.createBuffer function. When the buffer is created, it will be mapped at creation, and the initial value will be written to the buffer.

// Will be initialized to `100`
const buffer = root.createBuffer(d.u32, 100);
// Will be initialized to an array of two vec3fs with the specified values.
const buffer = root
.createBuffer(
d.arrayOf(d.vec3f, 2),
[d.vec3f(0, 1, 2), d.vec3f(3, 4, 5)],
);

Using an existing buffer

You can also create a buffer using an existing WebGPU buffer. This is useful when you have existing logic but want to introduce type-safe data operations.

const existingBuffer = device.createBuffer(...);
const buffer = root.createBuffer(d.u32, existingBuffer);
buffer.write(12); // Writing to `existingBuffer` through a type-safe API

Writing to a buffer

To write data to a buffer, you can use the .write(value) method. The typed schema enables auto-complete as well as static validation of this method’s arguments.

const Particle = d.struct({
position: d.vec2f,
health: d.u32,
});
const particleBuffer = root.createBuffer(Particle);
// .write(data: { position: vec2f, health: number })
particleBuffer.write({
position: vec2f(1.0, 2.0),
health: 100,
});

Reading from a buffer

To read data from a buffer, you can use the .read() method. It returns a promise that resolves to the data read from the buffer.

const buffer = root.createBuffer(d.arrayOf(d.u32, 10));
const data = await buffer.read(); // => number[]