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), the double-buffering that compute simulations rely on.

(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)

A simulation step that reads its neighbours cannot safely write over the generation it is still reading, so it keeps two buffers and alternates:

(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
});

Every key and value on this page traced to the WebGPU name it stands for, with a link to the definition. How the tracing is made, and what keeps it from rotting, is the subject of Where the Words Come From.

(buffer …) mirrors GPUBufferDescriptor (MDN).

Key WebGPU Note
:name GPUObjectDescriptorBase.label the label of GPUObjectDescriptorBase, and the name every cross-reference resolves
:size GPUBufferDescriptor.size
:usage GPUBufferDescriptor.usage
:pool PNGine’s own ping-pong: N instances, one selected per frame
:data GPUBufferDescriptor.mappedAtCreation present means created mapped, and names the bytes too, which WebGPU writes in a separate call
:index-of PNGine’s own size, bytes and index format from an indexed shape
:index-format GPURenderCommandsMixin.setIndexBuffer() the indexFormat argument of the bind, carried on the buffer so the pass need not repeat it
:file PNGine’s own a WASM module whose exports supply the bytes

buffer-usage (:usage on (buffer …)) spells the GPUBufferUsage flags (MDN).

Value WebGPU Note
vertex GPUBufferUsage.VERTEX
index GPUBufferUsage.INDEX
uniform GPUBufferUsage.UNIFORM
storage GPUBufferUsage.STORAGE
copy-dst GPUBufferUsage.COPY_DST
copy-src GPUBufferUsage.COPY_SRC
indirect GPUBufferUsage.INDIRECT
query-resolve GPUBufferUsage.QUERY_RESOLVE
map-read GPUBufferUsage.MAP_READ
map-write GPUBufferUsage.MAP_WRITE

index-format (:index-format on (buffer …)) spells the GPUIndexFormat enum.

Value WebGPU Note
uint16 "uint16"
uint32 "uint32"

Checked against the WebGPU specification at revision b8c0fa9; the links go to the current draft.