Skip to content

Commit ef8f1d8

Browse files
authored
impr: More capable and performant write API (#2279)
1 parent 2f23ee8 commit ef8f1d8

20 files changed

Lines changed: 1639 additions & 241 deletions

File tree

apps/typegpu-docs/src/content/docs/fundamentals/buffers.mdx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,137 @@ If you pass an unmapped buffer, the data will be written to the buffer using `GP
216216
If you passed your own buffer to the `root.createBuffer` function, you need to ensure it has the `GPUBufferUsage.COPY_DST` usage flag if you want to write to it using the `write` method.
217217
:::
218218

219+
### Permissive write inputs
220+
221+
`.write()` accepts several equivalent forms — you don't need to construct typed instances:
222+
223+
| Input form | Example for `vec3f` | Example for `mat2x2f` | Notes |
224+
|---|---|---|---|
225+
| Typed instance | `d.vec3f(1, 2, 3)` | `d.mat2x2f(1, 2, 3, 4)` | Allocates a TypeGPU wrapper object |
226+
| Plain JS array | `[1, 2, 3]` | `[1, 2, 3, 4]` | No TypeGPU wrapper allocated |
227+
| TypedArray | `new Float32Array([1, 2, 3])` | `new Float32Array([1, 2, 3, 4])` | Bytes copied as-is — must match GPU layout |
228+
| ArrayBuffer | `rawBytes` | `rawBytes` | Bytes copied as-is — must match GPU layout |
229+
230+
When data already lives in tuples or typed arrays, skipping typed instance construction avoids allocating TypeGPU wrapper objects, which can reduce garbage-collector pressure in hot paths such as per-frame updates or simulation ticks.
231+
232+
```ts twoslash
233+
import tgpu, { d } from 'typegpu';
234+
const root = await tgpu.init();
235+
// ---cut---
236+
const vecBuffer = root.createBuffer(d.vec3f);
237+
238+
vecBuffer.write(d.vec3f(1, 2, 3)); // typed instance
239+
vecBuffer.write([1, 2, 3]); // plain tuple
240+
vecBuffer.write(new Float32Array([1, 2, 3])); // TypedArray
241+
242+
const Particle = d.struct({ position: d.vec3f, velocity: d.vec3f, health: d.f32 });
243+
const particleBuffer = root.createBuffer(d.arrayOf(Particle, 100));
244+
245+
// Bytes already laid out exactly as the GPU expects
246+
declare const rawBytes: ArrayBuffer;
247+
particleBuffer.write(rawBytes); // ArrayBuffer
248+
```
249+
250+
:::tip[WGSL matrices are column-major]
251+
WGSL matrices are stored by columns, not rows.
252+
253+
For `mat3x3f`, use packed `number[]` (9 floats) or padded `Float32Array` (12 floats, one padding float per column).
254+
255+
```ts twoslash
256+
import tgpu, { d } from 'typegpu';
257+
const root = await tgpu.init();
258+
// ---cut---
259+
const mat3Buffer = root.createBuffer(d.mat3x3f);
260+
261+
// Column-major packed input (3 columns, 3 rows each)
262+
mat3Buffer.write([
263+
1, 2, 3, // column 0
264+
4, 5, 6, // column 1
265+
7, 8, 9, // column 2
266+
]);
267+
268+
// Equivalent padded WGSL layout
269+
mat3Buffer.write(new Float32Array([
270+
1, 2, 3, 0,
271+
4, 5, 6, 0,
272+
7, 8, 9, 0,
273+
]));
274+
```
275+
:::
276+
277+
:::note[TypedArray and ArrayBuffer inputs are copied as-is]
278+
Wherever a `TypedArray` or `ArrayBuffer` is passed, the bytes are copied directly without any interpretation. The data must already match the GPU memory layout, including any padding. For example, each element in `d.arrayOf(d.vec3f, N)` occupies **16 bytes** (12 bytes of data + 4 bytes of padding), so the input must include those padding bytes.
279+
280+
```ts twoslash
281+
import tgpu, { d } from 'typegpu';
282+
const root = await tgpu.init();
283+
// ---cut---
284+
const arrBuffer = root.createBuffer(d.arrayOf(d.vec3f, 2));
285+
286+
// TypedArray: must match the padded GPU layout (4 floats per element, 4th is padding)
287+
arrBuffer.write(new Float32Array([1, 2, 3, 0, 4, 5, 6, 0]));
288+
289+
// vec4f has no padding, so the layout is straightforward:
290+
const vec4Buffer = root.createBuffer(d.arrayOf(d.vec4f, 2));
291+
vec4Buffer.write(new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]));
292+
```
293+
294+
Plain JS arrays, tuples, and objects always go through the normal path, which handles layout automatically.
295+
:::
296+
297+
### Writing a slice
298+
299+
You can write a contiguous slice of data into a buffer using the optional second argument of `.write()`.
300+
Pass the values to write along with `startOffset` - the byte position at which writing begins.
301+
302+
:::tip
303+
Use `d.memoryLayoutOf` to obtain the correct byte offset for a given schema element without having to manually calculate it.
304+
:::
305+
306+
```ts twoslash
307+
import tgpu, { d } from 'typegpu';
308+
const root = await tgpu.init();
309+
// ---cut---
310+
const schema = d.arrayOf(d.u32, 6);
311+
const buffer = root.createBuffer(schema, [0, 1, 2, 0, 0, 0]);
312+
313+
// Get the byte offset of element [3]
314+
const layout = d.memoryLayoutOf(schema, (a) => a[3]);
315+
316+
// Write [4, 5, 6] starting at element [3], leaving [0, 1, 2] untouched
317+
buffer.write([4, 5, 6], { startOffset: layout.offset });
318+
const data = await buffer.read(); // will be [0, 1, 2, 4, 5, 6]
319+
```
320+
321+
An optional `endOffset` specifies the byte offset at which writing stops entirely.
322+
Combined with `startOffset` and `d.memoryLayoutOf`, this lets you write to a precise region of the buffer. If ommitted, writing will continue until the end of the provided data or the end of the buffer, whichever comes first.
323+
324+
:::note
325+
Both offsets are **byte-based**. Any component whose byte position falls at or beyond `endOffset` is not written, which means offsets that do not align to schema element boundaries can result in partial elements being written. Use `d.memoryLayoutOf` to target whole elements safely.
326+
:::
327+
328+
```ts twoslash
329+
import tgpu, { d } from 'typegpu';
330+
const root = await tgpu.init();
331+
// ---cut---
332+
const schema = d.arrayOf(d.vec3u, 4);
333+
const buffer = root.createBuffer(schema);
334+
335+
// Get the byte offsets of element [1] (start) and element [2] (stop)
336+
const startLayout = d.memoryLayoutOf(schema, (a) => a[1]);
337+
const endLayout = d.memoryLayoutOf(schema, (a) => a[2]);
338+
339+
// Write one vec3u at element [1], stopping before element [2]
340+
buffer.write([d.vec3u(4, 5, 6)], {
341+
startOffset: startLayout.offset,
342+
endOffset: endLayout.offset,
343+
});
344+
```
345+
346+
:::note
347+
In this particular case the `writePartial` method described in the next section would be a more convenient option, but the `startOffset` and `endOffset` options are useful for writing bigger slices of data.
348+
:::
349+
219350
### Partial writes
220351

221352
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>Press buttons to run benchmarks. Check the console for results.</div>

0 commit comments

Comments
 (0)