---
title: UniformPool
description: Dynamic-offset ring allocator for many per-draw uniforms. Allocate a typed `UniformSlot` once, push values each frame, call `endFrame()`, and pass returned offsets through `p.draw(draw, { offsets })`.
---

# UniformPool



## Import

```ts
import { UniformPool } from "vgpu/core";
import type { UniformPoolOptions, UniformLayout, UniformSlot } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

interface UniformPoolOptions {
  readonly capacityBytes?: number;
}

interface UniformLayout<T> {
  readonly size: number;
  readonly bindings?: readonly GPUBindGroupLayoutEntry[];
  readonly bindGroupLayout?: GPUBindGroupLayout;
  encode(value: T, dst: ArrayBuffer, byteOffset: number): void;
}

interface UniformSlot<T> {
  readonly pool: UniformPool;
  readonly layout: UniformLayout<T>;
  readonly bindGroup: GPUBindGroup;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly gpu: GPUBuffer;
  readonly stride: number;
  push(value: T): number;
  pushBytes(bytes: ArrayBufferView<ArrayBuffer>): number;
}

declare class UniformPool {
  readonly device: Device;
  readonly minOffsetAlignment: number;
  readonly capacityBytes: number;
  readonly maxUniformBindingSize: number;
  readonly cpuMirror: ArrayBuffer;
  readonly gpu: GPUBuffer;
  constructor(device: Device, opts?: UniformPoolOptions);
  get usedBytes(): number;
  get disposed(): boolean;
  alloc<T>(layout: UniformLayout<T>): UniformSlot<T>;
  push<T>(slot: UniformSlot<T>, value: T): number;
  pushBytes(slot: UniformSlot<unknown>, bytes: ArrayBufferView<ArrayBuffer>): number;
  beginFrame(frameIndex: number): void;
  endFrame(): void;
  assertReadyForSubmit(where: string): void;
  dispose(): void;
}
```

## Parameters

| Param                      | Type                                 | Required | Default                                                         | Notes                                                                                                             |
| -------------------------- | ------------------------------------ | -------: | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| device                     | `Device`                             |        ✔ | —                                                               | Core device. Device limits determine alignment and max binding size.                                              |
| opts                       | `UniformPoolOptions`                 |        ✖ | `{}`                                                            | Pool capacity options.                                                                                            |
| opts.capacityBytes         | `number`                             |        ✖ | `4 * 1024 * 1024`                                               | CPU mirror and GPU buffer size.                                                                                   |
| layout.size                | `number`                             |        ✔ | —                                                               | Logical uniform byte size before stride alignment. Must fit both pool capacity and `maxUniformBufferBindingSize`. |
| layout.bindings            | `readonly GPUBindGroupLayoutEntry[]` |        ✖ | Binding `0` uniform, dynamic offset, visibility vertex+fragment | Used only when `layout.bindGroupLayout` is omitted.                                                               |
| layout.bindGroupLayout     | `GPUBindGroupLayout`                 |        ✖ | New layout from `layout.bindings` or default binding `0`        | Usually `draw.layout(group, { dynamicOffsets: true })` so the draw pipeline and slot agree.                       |
| layout.encode              | `(value, dst, byteOffset) => void`   |        ✔ | —                                                               | Writes one value into the pool CPU mirror at `byteOffset`.                                                        |
| alloc.layout               | `UniformLayout<T>`                   |        ✔ | —                                                               | Layout used for one reusable slot. Slot stride is `roundUp(layout.size, minUniformBufferOffsetAlignment)`.        |
| push.slot                  | `UniformSlot<T>`                     |        ✔ | —                                                               | Must be allocated by the same pool.                                                                               |
| push.value                 | `T`                                  |        ✔ | —                                                               | Encoded into the CPU mirror; returned offset is passed as dynamic offset.                                         |
| pushBytes.bytes            | `ArrayBufferView<ArrayBuffer>`       |        ✔ | —                                                               | Must have byte length exactly equal to `slot.layout.size`.                                                        |
| beginFrame.frameIndex      | `number`                             |        ✔ | —                                                               | Currently unused marker; call before pushes to reset the ring head to `0`.                                        |
| assertReadyForSubmit.where | `string`                             |        ✔ | —                                                               | Error context if pushes are unflushed.                                                                            |

**Returns:** Constructor returns `UniformPool`; `alloc()` returns `UniformSlot<T>`; `push()` / `pushBytes()` return the dynamic byte offset; `usedBytes` returns current ring head; lifecycle methods return `void`.

**Throws:** `VGPU-UNIFORM-POOL-OVERFLOW` when a push would exceed `capacityBytes`; `VGPU-UNIFORM-LAYOUT-OVERSIZED` when `layout.size` exceeds pool capacity or device `maxUniformBufferBindingSize`; `VGPU-CORE-INVALID-USAGE` when using a disposed pool, pushing a slot from another pool, `pushBytes` length mismatches, or submitting before `endFrame()` flushes unflushed pushes.

## Examples

```ts
import { init, clock, draw, frame, target } from "vgpu/mock";
import { UniformPool, type UniformLayout } from "vgpu/core";

const gpu = await init();
const colorTarget = target(gpu, { size: [32, 32] });
const drawable = draw(gpu, { shader: `
  struct Object { model: mat4x4f }
  @group(0) @binding(0) var<uniform> object: Object;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return object.model * vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
` });

type ObjectUniforms = { model: Float32Array };
const objectLayout: UniformLayout<ObjectUniforms> = {
  size: 64,
  bindGroupLayout: drawable.layout(0, { dynamicOffsets: true }),
  encode(value, dst, byteOffset) {
    new Float32Array(dst, byteOffset, 16).set(value.model);
  },
};
const pool = new UniformPool(gpu.device, { capacityBytes: 1 << 20 });
const slot = pool.alloc(objectLayout);
drawable.group(0, slot.bindGroup);

pool.beginFrame(clock(gpu).frameCount);
const offset = slot.push({ model: new Float32Array(16) });
pool.endFrame();
frame(gpu, (currentFrame) => currentFrame.pass({ target: colorTarget }, (pass) => pass.draw(drawable, { offsets: { 0: [offset] } })));
```

```ts
import { init } from "vgpu/mock";
import { UniformPool, type UniformLayout } from "vgpu/core";

const gpu = await init();
const pool = new UniformPool(gpu.device, { capacityBytes: 1024 });
const layout: UniformLayout<Float32Array> = {
  size: 16,
  encode(value, dst, byteOffset) { new Float32Array(dst, byteOffset, 4).set(value); },
};
const slot = pool.alloc(layout);
pool.beginFrame(0);
const offset = slot.pushBytes(new Float32Array([1, 0, 0, 0]));
pool.endFrame();
void offset;
```

## Notes

* Call `beginFrame()` before pushes and `endFrame()` before submitting commands that use the returned offsets.
* A dynamic-offset bind group stores `offset: 0`; the per-draw offset comes from `DrawCallOptions.offsets`.
* The pool is a ring for transient per-frame data, not long-lived storage. Persistent globals usually fit `Uniform` or `SharedUniforms` better.
* **See also:** `Draw.layout`, `DrawCallOptions.offsets`, `FramePass.draw`, `Uniform`, `StructuredUniform`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)