Skip to content

(buffer …)

Creates a GPU buffer for storing vertex data, uniforms, storage data, or indices.

(buffer :name name
:size 1024
:usage [vertex storage]
:pool 2)

A buffer whose initial contents come from a (data …) entry writes :data instead of :size: the data sizes it.

Key Type Required Default Description
:name symbol Yes - Unique buffer name
:usage array Yes - Usage flags, at least one
:size number/expression/(define …) name No* - Size in bytes
:data reference No* - A (data …) entry whose bytes fill the buffer at creation, and size it
:index-of reference No* - Size + bytes + index-format from an indexed shape
:file string No* - WASM file whose exports supply size + initial bytes
:index-format symbol No - uint16 / uint32 when bound as an index buffer without :index-of
:pool number No 1 Ping-pong buffer pool size (1 to 255)

*A buffer has to name its size somehow: :size, or one of :data, :index-of and :file, which size it from the bytes they carry. Naming none of the four is a compile error that points at the form.

Type: number | expression | (define …) name

Buffer size in bytes: a non-negative integer. Can be:

  • Number: :size 1024
  • Expression: :size (* 4 256) (evaluated at compile time)
  • (define …) name: :size VERTEX_BYTES, on its own or inside an expression

A (data …) name is not one of them. Bytes and the size they imply are said once, with :data.

(define :name VERTEX_SIZE :value 40)
(define :name VERTEX_COUNT :value 100)
(define :name VERTEX_BYTES :value 4000)
(buffer :name vertices
:size (* VERTEX_SIZE VERTEX_COUNT)
:usage [vertex])
(buffer :name morePositions
:size VERTEX_BYTES
:usage [vertex])

A uniform buffer of fewer than 16 bytes is refused: that is WebGPU’s floor for a uniform binding.

Type: array of symbols

GPU buffer usage flags. At least one required.

Flag Description
vertex Vertex buffer
index Index buffer
uniform Uniform buffer (var<uniform>)
storage Storage buffer (var<storage>)
copy-src Source of copy operations
copy-dst Destination of copies and write-buffer ops
indirect Holds indirect draw/dispatch arguments
query-resolve Receives resolve-query-set results
map-read Mappable for CPU readback; combines with copy-dst and nothing else
map-write Mappable for CPU upload; combines with copy-src and nothing else

A mappable buffer is host memory the GPU may only copy in or out of, so the two map flags do not combine with vertex, storage, uniform and the rest. The compiler reports the combination rather than leaving it to the device.

(buffer :name storageBuffer
:size 4096
:usage [storage copy-dst])

Type: reference

Reference to a (data …) form holding the buffer’s initial contents. The buffer is created with those bytes already mapped, and takes its size from them, so :size is not written beside it. copy-dst is added to the usage flags automatically.

(data :name cubeVertices (cube :format [position4 color4 uv2]))
(buffer :name vertexBuffer
:usage [vertex]
:data cubeVertices)

Writing a numeric :size beside :data is legal and means what it says: the buffer is that many bytes and the data fills the front of it. A :size smaller than the data is a compile error.

Type: reference

Sources the buffer’s size, initial bytes, and index-format from an indexed shape’s index data (teapot, dragon). This references the shape (data …) directly; no synthetic _indices name is needed.

(data :name teapotMesh (teapot :format [position3 normal3]))
(buffer :name indexBuffer :index-of teapotMesh :usage [index])

Type: symbol (uint16 | uint32)

The byte width of index entries. With :index-of, the format propagates from the shape’s index data and this key is unnecessary. Set it explicitly when a buffer is bound as a pass’s :index-buffer without an :index-of source, for example when a compute pass generates the indices:

(buffer :name genIndices :size 4096 :usage [index storage] :index-format uint32)

Type: string (file path relative to the source)

A WASM (WebAssembly) module whose l / s / gen exports supply both the buffer’s size and its initial bytes, so :size is not authored at all. This is the same module convention the (pass … :data …) sugar uses, and the sugar synthesizes such buffers for you.

(buffer :name palette :usage [storage] :file "colors.wasm")

Type: number (1 to 255)

Creates multiple buffer instances for ping-pong patterns (alternating each frame between the instance being read and the instance being written). Used in compute simulations where you read from one buffer and write to another.

(buffer :name particles
:size 32768
:usage [vertex storage]
:pool 2)

With :pool 2, the runtime creates two buffer instances. Selection uses:

actual_id = base_id + (frame_counter + pool_offset) % pool_size

See Ping-Pong Pattern below.

(buffer :name positions :size 48 :usage [vertex])
(buffer :name uniforms :size 16 :usage [uniform copy-dst])

Use compile-time shape generators for mesh data:

(data :name cubeVertices (cube :format [position4 color4 uv2]))
(buffer :name vertexBuffer
:usage [vertex]
:data cubeVertices)

:data sizes the buffer from the generated shape data, so there is no byte count to write down or keep in step.

An indexed shape produces both vertex data and an index companion. Size the vertex buffer from the shape and the index buffer with :index-of:

(data :name teapotMesh (teapot :format [position3 normal3]))
(buffer :name vertexBuffer
:usage [vertex]
:data teapotMesh)
(buffer :name indexBuffer :index-of teapotMesh :usage [index])
(data :name triangleVerts :float32 [
0.0 0.5 1 0 0
-0.5 -0.5 0 1 0
0.5 -0.5 0 0 1
])
(buffer :name vertexBuffer
:usage [vertex]
:data triangleVerts)
(define :name NUM_PARTICLES :value 2048)
(define :name PARTICLE_SIZE :value 16)
(buffer :name particles
:size (* NUM_PARTICLES PARTICLE_SIZE)
:usage [vertex storage]
:pool 2)

For compute simulations requiring double-buffering:

(buffer :name particles
:size 32768
:usage [vertex storage]
:pool 2)
(bind-group :name simBindGroup :layout simPipeline :group 0 :pool 2
(entry :binding 0 :buffer particles :ping-pong 0)
(entry :binding 1 :buffer particles :ping-pong 1))
(compute-pass :name simulate
:pipeline simPipeline
:bind-groups [simBindGroup]
:bind-groups-pool-offsets [0]
(dispatch :workgroups [32]))

Each frame alternates which buffer is read vs written.

Rule Error
A name is declared once, across every form kind duplicate_cross_ref_target, or duplicate name 'x': already declared as a (buffer …) at line N
A builtin spelling (canvas, context-current-texture, the runtime input sources) cannot be declared 'canvas' is a builtin symbol and cannot name a (buffer …)
:usage required missing_required_key
:usage needs at least one flag vector_too_short
:data / :index-of must reference a (data …) not_cross_ref
:size must be a non-negative integer, or a declared (define …) name number_below_min, not_cross_ref
:pool must be in [1,255] number_below_min / number_above_max
The buffer has to name its size buffer 'b' names no size: give it :size …, or :data …, or :index-of …, or :file …
:size may not be smaller than :data buffer 'b' size (N bytes) is smaller than data 'd' (M bytes)
A uniform buffer is at least 16 bytes uniform buffer 'b' size (N bytes) is below WebGPU's 16-byte uniform floor
map-read combines only with copy-dst, map-write only with copy-src buffer 'b' is :usage [map-read … storage …]
A copy endpoint carries the usage the copy needs buffer 'b' is the :source of a copy but its :usage has no copy-src

The rows with a code are schema checks; the rows with a message are compiler checks that read a value rather than a shape, and they report the line the form is written on.

Maps to GPUBuffer:

device.createBuffer({
size: size,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE,
mappedAtCreation: hasMappedData
});