Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions apps/typegpu-docs/src/content/docs/fundamentals/buffers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,107 @@ particleBuffer.write({
});
```

### Permissive vector inputs

For vector fields and buffers, `.write()` accepts three equivalent forms - you don't need to construct a typed vec instance:

| Input form | Example for `vec3f` | Notes |
|---|---|---|
| Typed vec instance | `d.vec3f(1, 2, 3)` | Allocates a TypeGPU wrapper object |
| Plain JS tuple | `[1, 2, 3]` | No TypeGPU wrapper allocated |
| TypedArray | `new Float32Array([1, 2, 3])` | No TypeGPU wrapper allocated |

When the data already lives in tuples or typed arrays, skipping vec construction avoids allocating TypeGPU wrapper objects, which can reduce garbage-collector pressure in hot paths such as per-frame updates or simulation ticks.

```ts twoslash
import tgpu, { d } from 'typegpu';
const root = await tgpu.init();
// ---cut---
const vecBuffer = root.createBuffer(d.vec3f);

vecBuffer.write(d.vec3f(1, 2, 3)); // typed instance
vecBuffer.write([1, 2, 3]); // plain tuple
vecBuffer.write(new Float32Array([1, 2, 3])); // TypedArray
```

For **arrays of vectors**, a single flat TypedArray is also accepted. TypeGPU automatically handles the stride difference between the packed input and the padded GPU layout (e.g. `vec3f` occupies 16 bytes in a buffer, but only 3 floats in the TypedArray). When your data already lives in a flat TypedArray, this avoids both per-element wrapper allocation and array construction overhead:

```ts twoslash
import tgpu, { d } from 'typegpu';
const root = await tgpu.init();
// ---cut---
const arrBuffer = root.createBuffer(d.arrayOf(d.vec3f, 2));

// Array of instances, tuples, or per-element TypedArrays - all equivalent:
arrBuffer.write([d.vec3f(1, 2, 3), d.vec3f(4, 5, 6)]);
arrBuffer.write([[1, 2, 3], [4, 5, 6]]);

// Flat Float32Array - no per-element wrapper construction:
arrBuffer.write(new Float32Array([1, 2, 3, 4, 5, 6]));
```

The same rules apply to scalar arrays - a `Float32Array` (or `Int32Array` / `Uint32Array`) can be passed directly to a buffer of `arrayOf(d.f32, n)` (or `i32` / `u32`).

:::caution[WebGPU Interoperability]
If you create a buffer from an existing WebGPU buffer that happens to be mapped, the data will be written directly to the buffer (the buffer will not be unmapped).
If you pass an unmapped buffer, the data will be written to the buffer using `GPUQueue.writeBuffer`.

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.
:::

### Writing a slice

You can write a contiguous slice of data into a buffer using the optional second argument of `.write()`.
Pass the values to write along with `startOffset` - the byte position at which writing begins.

:::tip
Use `d.memoryLayoutOf` to obtain the correct byte offset for a given schema element without having to manually calculate it.
:::

```ts twoslash
import tgpu, { d } from 'typegpu';
const root = await tgpu.init();
// ---cut---
const schema = d.arrayOf(d.u32, 6);
const buffer = root.createBuffer(schema, [0, 1, 2, 0, 0, 0]);

// Get the byte offset of element [3]
const layout = d.memoryLayoutOf(schema, (a) => a[3]);

// Write [4, 5, 6] starting at element [3], leaving [0, 1, 2] untouched
buffer.write([4, 5, 6], { startOffset: layout.offset });
const data = await buffer.read(); // will be [0, 1, 2, 4, 5, 6]
```

An optional `endOffset` specifies the byte offset at which writing stops entirely.
Combined with `startOffset` and `d.memoryLayoutOf`, this lets you write to a precise region of the buffer.

:::note
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.
:::

```ts twoslash
import tgpu, { d } from 'typegpu';
const root = await tgpu.init();
// ---cut---
const schema = d.arrayOf(d.vec3u, 4);
const buffer = root.createBuffer(schema);

// Get the byte offsets of element [1] (start) and element [2] (stop)
const startLayout = d.memoryLayoutOf(schema, (a) => a[1]);
const endLayout = d.memoryLayoutOf(schema, (a) => a[2]);

// Write one vec3u at element [1], stopping before element [2]
buffer.write([d.vec3u(4, 5, 6)], {
startOffset: startLayout.offset,
endOffset: endLayout.offset,
});
```

:::note
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.
:::

### 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.
Expand Down
68 changes: 55 additions & 13 deletions packages/typegpu/src/core/buffer/buffer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { BufferReader, BufferWriter, getSystemEndianness } from 'typed-binary';
import { getCompiledWriterForSchema } from '../../data/compiledIO.ts';
import { readData, writeData } from '../../data/dataIO.ts';
import type { AnyData } from '../../data/dataTypes.ts';
import { isDisarray, type AnyData } from '../../data/dataTypes.ts';
import { getWriteInstructions } from '../../data/partialIO.ts';
import { sizeOf } from '../../data/sizeOf.ts';
import type { BaseData } from '../../data/wgslTypes.ts';
import { isWgslData } from '../../data/wgslTypes.ts';
import { isVec, isWgslArray, isWgslData } from '../../data/wgslTypes.ts';
import type { StorageFlag } from '../../extension.ts';
import type { TgpuNamable } from '../../shared/meta.ts';
import { getName, setName } from '../../shared/meta.ts';
import type {
Infer,
InferInput,
InferPartial,
IsValidIndexSchema,
IsValidStorageSchema,
Expand Down Expand Up @@ -103,6 +104,11 @@ type InnerValidUsagesFor<T> = {

export type ValidUsagesFor<T> = InnerValidUsagesFor<T>['usage'];

export type BufferWriteOptions = {
startOffset?: number;
endOffset?: number;
};

export interface TgpuBuffer<TData extends BaseData> extends TgpuNamable {
readonly [$internal]: true;
readonly resourceType: 'buffer';
Expand Down Expand Up @@ -131,7 +137,7 @@ export interface TgpuBuffer<TData extends BaseData> extends TgpuNamable {
as<T extends ViewUsages<this>>(usage: T): UsageTypeToBufferUsage<TData>[T];

compileWriter(): void;
write(data: Infer<TData>): void;
write(data: InferInput<TData>, options?: BufferWriteOptions): void;
writePartial(data: InferPartial<TData>): void;
clear(): void;
copyFrom(srcBuffer: TgpuBuffer<MemIdentity<TData>>): void;
Expand Down Expand Up @@ -222,7 +228,7 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
});

if (this.initial) {
this._writeToTarget(this._buffer.getMappedRange(), this.initial);
this._writeToTarget(this._buffer.getMappedRange(), this.initial as InferInput<TData>);
this._buffer.unmap();
}
}
Expand Down Expand Up @@ -287,12 +293,21 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
getCompiledWriterForSchema(this.dataType);
}

private _writeToTarget(target: ArrayBuffer, data: Infer<TData>): void {
private _writeToTarget(
target: ArrayBuffer,
data: InferInput<TData>,
options?: BufferWriteOptions,
): void {
const dataView = new DataView(target);
const isLittleEndian = endianness === 'little';
const startOffset = options?.startOffset ?? 0;
const endOffset = options?.endOffset ?? target.byteLength;

const compiledWriter = getCompiledWriterForSchema(this.dataType);

if (compiledWriter) {
try {
compiledWriter(new DataView(target), 0, data, endianness === 'little');
compiledWriter(dataView, startOffset, data, isLittleEndian, endOffset);
return;
} catch (error) {
console.error(
Expand All @@ -304,25 +319,52 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
}
}

writeData(new BufferWriter(target), this.dataType, data);
if (
ArrayBuffer.isView(data) &&
!(data instanceof DataView) &&
(isWgslArray(this.dataType) || isDisarray(this.dataType)) &&
isVec((this.dataType as { elementType?: unknown }).elementType)
) {
throw new Error(
'Flat TypedArray input for arrays of vectors requires the compiled writer. ' +
'This environment does not allow eval - pass an array of vec instances or plain tuples instead.',
);
}

const writer = new BufferWriter(target);
writer.seekTo(startOffset);
writeData(writer, this.dataType, data as Infer<TData>);
}

write(data: Infer<TData>): void {
write(data: InferInput<TData>, options?: BufferWriteOptions): void {
const gpuBuffer = this.buffer;
const bufferSize = sizeOf(this.dataType);
const startOffset = options?.startOffset ?? 0;
const endOffset = options?.endOffset ?? bufferSize;
const size = endOffset - startOffset;

if (startOffset < 0 || !Number.isInteger(startOffset)) {
throw new Error(`startOffset must be a non-negative integer, got ${startOffset}`);
}
if (endOffset < startOffset) {
throw new Error(`endOffset (${endOffset}) must be >= startOffset (${startOffset})`);
}
if (endOffset > bufferSize) {
throw new Error(`endOffset (${endOffset}) exceeds buffer size (${bufferSize})`);
}

if (gpuBuffer.mapState === 'mapped') {
const mapped = gpuBuffer.getMappedRange();
this._writeToTarget(mapped, data);
this._writeToTarget(mapped, data, options);
return;
}

const size = sizeOf(this.dataType);
if (!this._hostBuffer) {
this._hostBuffer = new ArrayBuffer(size);
this._hostBuffer = new ArrayBuffer(sizeOf(this.dataType));
}

this._writeToTarget(this._hostBuffer, data);
this.#device.queue.writeBuffer(gpuBuffer, 0, this._hostBuffer, 0, size);
this._writeToTarget(this._hostBuffer, data, options);
this.#device.queue.writeBuffer(gpuBuffer, startOffset, this._hostBuffer, startOffset, size);
}

public writePartial(data: InferPartial<TData>): void {
Expand Down
10 changes: 5 additions & 5 deletions packages/typegpu/src/core/buffer/bufferShorthand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { ResolvedSnippet } from '../../data/snippet.ts';
import type { BaseData } from '../../data/wgslTypes.ts';
import type { StorageFlag } from '../../extension.ts';
import { getName, setName, type TgpuNamable } from '../../shared/meta.ts';
import type { Infer, InferGPU, InferPartial } from '../../shared/repr.ts';
import type { Infer, InferGPU, InferInput, InferPartial } from '../../shared/repr.ts';
import { $getNameForward, $gpuValueOf, $internal, $resolve } from '../../shared/symbols.ts';
import type { ResolutionCtx, SelfResolvable } from '../../types.ts';
import type { TgpuBuffer, UniformFlag } from './buffer.ts';
import type { BufferWriteOptions, TgpuBuffer, UniformFlag } from './buffer.ts';
import type { TgpuBufferUsage } from './bufferUsage.ts';

// ----------
Expand All @@ -16,7 +16,7 @@ interface TgpuBufferShorthandBase<TData extends BaseData> extends TgpuNamable {
readonly [$internal]: true;

// Accessible on the CPU
write(data: Infer<TData>): void;
write(data: InferInput<TData>, options?: BufferWriteOptions): void;
writePartial(data: InferPartial<TData>): void;
read(): Promise<Infer<TData>>;
// ---
Expand Down Expand Up @@ -103,8 +103,8 @@ export class TgpuBufferShorthandImpl<
return this;
}

write(data: Infer<TData>): void {
this.buffer.write(data);
write(data: InferInput<TData>, options?: BufferWriteOptions): void {
this.buffer.write(data, options);
}

writePartial(data: InferPartial<TData>): void {
Expand Down
Loading
Loading