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 typeconst 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: d.vec3f(Math.random(), 2, Math.random()), velocity: d.vec3f(0, 9.8, 0), health: 100, })), );// ^? TgpuBuffer<d.WgslArray<d.WgslStruct<{// position: d.Vec3f,// velocity: d.Vec3f,// health: d.F32,// }>>>
// -// --// --- Using in shader...// --// -
// Reading from the bufferconst value = await buffer.read();// ^? { position: d.vec3f, velocity: d.vec3f, health: number }[]
// Using the valueconsole.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.WgslArray<d.F32>>
const uniformsBuffer = root .createBuffer(d.struct({ a: d.f32, b: d.f32 }));// ^? TgpuBuffer<d.WgslStruct<{ 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');
Additional flags
It is also possible to add any of the GPUBufferUsage
flags to a typed buffer object, using the .$addFlags
method.
Though it shouldn’t be necessary in most scenarios as majority of the flags are handled automatically by the library
or indirectly through the .$usage
method.
buffer.$addFlags(GPUBufferUsage.QUERY_RESOLVE);
Flags can only be added this way if the typed buffer was not created with an existing GPU buffer. If it was, then all flags need to be provided to the existing buffer when constructing it.
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: d.vec2f, health: number })particleBuffer.write({ position: vec2f(1.0, 2.0), health: 100,});
Partial writes
When you want to update only a subset of a buffer’s fields, you can use the .writePartial(data)
method. This method updates only the fields provided in the data
object and leaves the rest unchanged.
The format of the data
value depends on your schema type:
-
For
d.struct
schemas: Provide an object with keys corresponding to the subset of the schema’s fields you wish to update. -
For
d.array
schemas: Provide an array of objects. Each object should specify:idx
: the index of the element to update.value
: the new value for that element.
const Planet = d.struct({ radius: d.f32, mass: d.f32, position: d.vec3f, colors: d.arrayOf(d.vec3f, 5),});
const planetBuffer = root.createBuffer(Planet);
planetBuffer.writePartial({ mass: 123.1, colors: [ { idx: 2, value: d.vec3f(1, 0, 0) }, { idx: 4, value: d.vec3f(0, 0, 1) }, ],});
Copying
There’s also an option to copy value from another typed buffer using the .copyFrom(buffer)
method,
as long as both buffers have a matching data schema.
const backupParticleBuffer = root.createBuffer(Particle);backupParticleBuffer.copyFrom(particleBuffer);
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[]