Getting started

Start with the public vgpu package. A program has one Gpu context, explicit WGSL bindings, and explicit frames. There are no global uniforms: time comes from the frame clock (clock(gpu).time, .deltaTime, .frameCount) and resolution comes from targets (target.size, target.texelSize).

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { clock, init, effect, frameLoop, surface } from "vgpu";
 
const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const gradient = effect(gpu, `
struct Params { time: f32, texel: vec2f }
@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, texel: canvasSurface.texelSize } } });
 
canvasSurface.onResize(() => {
  gradient.set({ params: { texel: canvasSurface.texelSize } });
});
 
const time = clock(gpu);
frameLoop(gpu, (frame) => {
  gradient.set({ params: { time: time.time } });
  frame.pass(canvasSurface, gradient);
});

Two habits keep this correct as it grows: bindings are set by their WGSL names — params is a struct, so its members nest inside it — and set() writes immediately, so the render loop only writes what actually changes (time); size-class values like texel belong in the resize handler.

Validate with static renders

You do not need a browser — or eyes — to prove a shader right. First, verify the machine can render at all:

Bash
1
npx vgpu doctor   # JSON verdict: healthy | unhealthy — each problem with its exact fix

doctor acquires a real adapter and renders a frame; when something is missing it prescribes the exact package install or environment variable to set. Once healthy, render headless and read the pixels back — objective evidence instead of guesswork:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { writeFileSync } from "node:fs";
import { PNG } from "pngjs";
import { init, effect, target } from "vgpu/node";
 
const SHADER = `
  @fragment fn main() -> @location(0) vec4f {
    return vec4f(0.25, 0.5, 0.75, 1.0);
  }
`;
const width = 160;
const height = 90;
const gpu = await init();
const colorTarget = target(gpu, { size: [width, height] }); // small targets stay fast, even on CPU
effect(gpu, SHADER).draw(colorTarget);
const pixels = await colorTarget.read();                   // RGBA bytes — assert on them
const png = new PNG({ width, height });               // ...and write a PNG you can open
png.data.set(pixels);
writeFileSync("frame.png", PNG.sync.write(png));
gpu.dispose();                                        // stops Dawn's polling so the process exits

Keep the loop tight: render → read → adjust → render. Every visual claim you make should be backed by pixels you actually read — assert on pixels when you know the expected value, and open frame.png when you need to judge composition. PNG encoding is project-owned: pngjs is one option, any encoder works.

The full step-by-step playbook — from vgpu docs to browser validation — is The default workflow for developing shaders with vgpu. When a shader is multi-pass or a user reports a visual bug, do not iterate by eye: extract the shader's internal values as pixels and diff them against a CPU reference, following Debugging shaders by extracting internal values.

Default choices

  • Use effect(gpu) for fullscreen fragment work.
  • Use draw(gpu) for vertex shaders, meshes, storage-driven vertices, instancing, MRT, and depth.
  • Use effect.draw(target) for simple single-pass draws; use frame(gpu, (f) => ...) to batch multi-pass work and frameLoop(gpu, ...) for animation.
  • Use set() for every binding declared in WGSL; missing bindings fail with VGPU-R1-BINDING-NEVER-SET.
  • Keep plain JS values plain from their first set(); if you need user-owned lifetime, pass a resource from the first set().
  • Request optional device capabilities at startup with init({ requiredFeatures: [...] }) — for example "timestamp-query" for timer(gpu); a name the adapter lacks fails init with VGPU-FEATURE-UNSUPPORTED.

Where to go next

Read the concept guides in order — each builds on the previous one:

Bash
1
2
3
4
5
6
7
vgpu docs cat concepts-context.md         # the Gpu context, surfaces, targets
vgpu docs cat concepts-draws.md           # draw(), meshes, instancing
vgpu docs cat concepts-compilation.md     # compile() and pipeline warmup
vgpu docs cat concepts-effects.md         # fragment effects and set()
vgpu docs cat concepts-passes.md          # frame.pass and multi-pass work
vgpu docs cat concepts-frames.md          # frame batching and animation loops
vgpu docs cat concepts-render-bundles.md  # record draws once, replay cheap

Shipping this inside an app? Shaders in their own .wgsl files need a bundler loader plus one ambient TypeScript declaration, and the canvas has to live in a client component:

Bash
1
vgpu docs cat nextjs.md   # Next.js (Turbopack or webpack), Vite, .wgsl types, canvas component

For performance work and testing:

Bash
1
2
3
vgpu docs cat /guides/performance-model.docs.md
vgpu docs cat /guides/performance-patterns.docs.md
vgpu docs cat browser-testing