---
title: Frames
description: frame(gpu, cb) encodes your passes and submits once; frameLoop(gpu, cb) drives animation.
---

# Frames



A frame is one unit of GPU work. Inside it you open passes, each drawing into a target you choose, and draw the effects you created earlier. When the callback returns, vgpu encodes everything into one command encoder and submits it once.

## Render a single frame

[`frame(gpu)`](/docs/reference/vgpu/frame#framerunner) runs synchronously and renders immediately — every pass inside is encoded into one command encoder and submitted once. That single submit is what the frame is for:

```ts
import { init, effect, frame, sampler, surface, target } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });
const postEffect = effect(gpu, `
  @group(0) @binding(0) var src: texture_2d<f32>;
  @group(0) @binding(1) var samp: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let base = textureSampleLevel(src, samp, uv, 0.0);
    return vec4f(1.0 - base.rgb, 1.0);
  }
`);

// ---cut---
const sceneTarget = target(gpu, { size: [canvasTarget.size[0], canvasTarget.size[1]] });
postEffect.set({
  src: sceneTarget,
  samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
});

frame(gpu, (currentFrame) => {
  currentFrame.pass(sceneTarget, pulseEffect);
  currentFrame.pass(canvasTarget, postEffect);
}); // two passes, one encoder, one submit
```

One-shot draws like `pulseEffect.draw(canvasTarget)` are the simple default for a single pass. Multi-pass hot paths should use `frame(gpu)` to batch passes into one command encoder and one submit. One-shot draws never join a surrounding frame; inside `frame(gpu)`, always go through `frame.pass()`.

<Callout type="warn">
  Warning: one-shot `draw()` calls do not join a surrounding frame — inside a frame callback they submit on their own immediately. Inside `frame(gpu)`, always draw through `frame.pass()`.
</Callout>

<Callout type="warn">
  Warning: Do not call `frame(gpu)` from inside another frame callback or from a surface resize callback. vgpu throws `VGPU-FRAME-REENTRANT` so command encoders stay ordered and predictable.
</Callout>

## Render loops

For animation, use [`frameLoop(gpu)`](/docs/reference/vgpu/frame#framerunner) — it runs your frame every tick:

```ts
import { clock, init, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// ---cut---
const time = clock(gpu);
const handle = frameLoop(gpu, (frame) => {
  pulseEffect.set({ params: { time: time.time } }); // update uniforms every tick
  frame.pass(canvasTarget, pulseEffect);
}, { fps: 30 });

handle.stop(); // call it when your component unmounts
```

The loop advances the frame clock — `clock(gpu).time`, `deltaTime` and `frameCount` — and runs surface auto-resize before each tick. The optional `fps` throttles it.

This is what the same loop looks like by hand with `requestAnimationFrame`:

```ts
import { init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// ---cut---
function tick() {
  pulseEffect.set({ params: { time: performance.now() / 1000 } }); // you own the clock now
  pulseEffect.draw(canvasTarget);
  requestAnimationFrame(tick); // and the scheduling
}
requestAnimationFrame(tick);
```

Both work. `frameLoop(gpu)` is the same loop with the clock, throttling, and resize handling done for you.

See it live: the [fluid example](/examples/fluid) runs a compute-driven simulation with exactly this frame loop shape.


---

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)