Skip to content
Draft
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
5 changes: 5 additions & 0 deletions apps/typegpu-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ export default defineConfig({
label: 'Buffers',
slug: 'apis/buffers',
},
{
label: 'Buffer Views',
slug: 'apis/buffer-views',
badge: { text: 'new' },
},
{
label: 'Textures',
slug: 'apis/textures',
Expand Down
193 changes: 193 additions & 0 deletions apps/typegpu-docs/src/content/docs/apis/buffer-views.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: Buffer Views
description: A guide on how to store raw bytes in buffers and view them as typed data in shaders.
---

:::caution[Language extension]
Buffer views require the `buffer_view` WGSL language extension, which is not yet supported everywhere.
Check `root.enabledWgslLanguageFeatures.has('buffer_view')` and fall back to typed schemas, such as `d.struct` and `d.arrayOf`, when it is missing.
:::

A `d.bytes(n)` schema describes `n` bytes with no type of their own, the WGSL `buffer<n>` type.
Shaders read and write those bytes as any host-shareable type with `std.bufferView`, at offsets they compute at runtime.
One buffer can then hold differently typed data, arrays of varying length, or several views of the same memory.

## Creating buffers

`d.bytes(n)` works wherever a schema does, in storage and uniform buffers, workgroup variables and bind group layouts.
Bind group layouts also take `d.bytes` itself, for storage buffers of any size.

```ts twoslash
import { tgpu, d } from 'typegpu';

const root = await tgpu.init();
// ---cut---
const packet = root.createMutable(d.bytes(1024));
const lights = root.createUniform(d.bytes(512));
const scratch = tgpu.workgroupVar(d.bytes(4096));

const layout = tgpu.bindGroupLayout({
mesh: { storage: d.bytes },
});
```

On the CPU, the buffer takes an `ArrayBuffer` or a typed array, and reads back an `ArrayBuffer`.

```ts twoslash
import { tgpu, d } from 'typegpu';

const root = await tgpu.init();
// ---cut---
const packet = root.createBuffer(d.bytes(16), new Uint32Array([1, 2, 3, 4])).$usage('storage');

packet.write(new Float32Array([0.5]), { startOffset: 8 });

const bytes = await packet.read();
const values = new Uint32Array(bytes);
```

## Viewing bytes

`std.bufferView(schema, buffer, byteOffset)` views the bytes at `byteOffset` as `schema` and returns a reference.
Access its members and elements directly, and read or write the whole value through `.$`.
The view can be written if the buffer can.

```ts twoslash
import { tgpu, d, std } from 'typegpu';

const root = await tgpu.init();
const packet = root.createMutable(d.bytes(1024));
// ---cut---
const Header = d.struct({ count: d.u32, scale: d.f32 });

const main = tgpu.computeFn({ workgroupSize: [1] })(() => {
const header = std.bufferView(Header, packet.$, 0);
header.count += 1;
std.bufferView(d.vec4f, packet.$, 16).$ = d.vec4f(header.scale);
});
```

A runtime-sized schema, such as `d.arrayOf(d.f32)`, extends to the end of the buffer.
`std.bufferArrayView(schema, buffer, byteOffset, byteSize)` limits it to `byteSize` bytes, and `std.bufferLength(buffer)` returns the size of the whole buffer.

Each view must start at a multiple of its schema's alignment and fit in the buffer.
TypeGPU checks both when the offset is known at resolution time.
Views cannot hold atomics or booleans, and buffers cannot be struct members or array elements.

## Packing differently typed data

A mesh can store a header, its indices and its vertices in one buffer, with offsets that depend on the header.

```ts twoslash
import { tgpu, d, std } from 'typegpu';

const root = await tgpu.init();
// ---cut---
const Header = d.struct({ indexCount: d.u32, vertexCount: d.u32 });

const layout = tgpu.bindGroupLayout({
mesh: { storage: d.bytes },
extent: { storage: d.vec2f, access: 'mutable' },
});

const measure = tgpu.computeFn({ workgroupSize: [1] })(() => {
const header = std.bufferView(Header, layout.$.mesh, 0);
const indicesSize = header.indexCount * 4;
const indices = std.bufferArrayView(d.arrayOf(d.u32), layout.$.mesh, 8, indicesSize);
const vertices = std.bufferView(d.arrayOf(d.vec2f), layout.$.mesh, 8 + indicesSize);

for (const index of indices) {
layout.$.extent = std.max(layout.$.extent, std.abs(vertices[index]!));
}
});
```

## Arrays of varying length

Records of different lengths can follow each other, each a count and its values.
`std.bufferArrayView` gives each record an array of the right length.

```ts twoslash
import { tgpu, d, std } from 'typegpu';

const root = await tgpu.init();
// ---cut---
const records = root.createReadonly(d.bytes(4096));
const totals = root.createMutable(d.arrayOf(d.f32, 64));

const sum = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
let offset = d.u32(0);
for (let i = d.u32(0); i < gid.x; i++) {
offset += 4 + std.bufferView(d.u32, records.$, offset).$ * 4;
}

const count = std.bufferView(d.u32, records.$, offset).$;
const values = std.bufferArrayView(d.arrayOf(d.f32), records.$, offset + 4, count * 4);

let total = d.f32(0);
for (const value of values) {
total += value;
}
totals.$[gid.x] = total;
});
```

## Reinterpreting bytes

Two views of the same bytes read them as different types.

```ts twoslash
import { tgpu, d, std } from 'typegpu';

const root = await tgpu.init();
const packet = root.createMutable(d.bytes(1024));
// ---cut---
const flipSign = tgpu.fn([d.u32])((offset) => {
const bits = std.bufferView(d.u32, packet.$, offset);
bits.$ ^= 0x80000000;
});
```

## Reusing workgroup memory

A workgroup buffer can hold different data in each phase of a computation.

```ts twoslash
import { tgpu, d, std } from 'typegpu';
// ---cut---
const scratch = tgpu.workgroupVar(d.bytes(1024));

const main = tgpu.computeFn({
workgroupSize: [128],
in: { lid: d.builtin.localInvocationIndex },
})(({ lid }) => {
const floats = std.bufferView(d.arrayOf(d.f32, 256), scratch.$, 0);
floats[lid] = d.f32(lid);
std.workgroupBarrier();

const pairs = std.bufferView(d.arrayOf(d.vec2u, 128), scratch.$, 0);
pairs[lid] = d.vec2u(lid);
});
```

## Passing buffers to functions

Functions take buffers through pointers, such as `d.ptrStorage(d.bytes(), 'read-write')`.

```ts twoslash
import { tgpu, d, std } from 'typegpu';

const root = await tgpu.init();
const packet = root.createMutable(d.bytes(1024));
// ---cut---
const bump = tgpu.fn([d.ptrStorage(d.bytes(), 'read-write'), d.u32])((buffer, index) => {
std.bufferView(d.u32, buffer, index * 4).$ += 1;
});

const main = tgpu.computeFn({ workgroupSize: [1] })(() => {
bump(d.ref(packet.$), 0);
});
```
17 changes: 17 additions & 0 deletions apps/typegpu-docs/src/examples/simple/shape-stream/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<style>
#unsupported:not([hidden]) {
position: absolute;
inset: 0;
display: grid;
place-items: center;
margin: 0;
padding: 1rem;
text-align: center;
}
</style>

<canvas></canvas>
<p id="unsupported" hidden>
This example needs the <code>buffer_view</code> WGSL language extension, which your browser does
not support yet.
</p>
Loading
Loading