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).
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:
npx vgpu doctor # JSON verdict: healthy | unhealthy — each problem with its exact fixdoctor 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:
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 exitsKeep 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; useframe(gpu, (f) => ...)to batch multi-pass work andframeLoop(gpu, ...)for animation. - Use
set()for every binding declared in WGSL; missing bindings fail withVGPU-R1-BINDING-NEVER-SET. - Keep plain JS values plain from their first
set(); if you need user-owned lifetime, pass a resource from the firstset(). - Request optional device capabilities at startup with
init({ requiredFeatures: [...] })— for example"timestamp-query"fortimer(gpu); a name the adapter lacks fails init withVGPU-FEATURE-UNSUPPORTED.
Where to go next
Read the concept guides in order — each builds on the previous one:
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 cheapShipping 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:
vgpu docs cat nextjs.md # Next.js (Turbopack or webpack), Vite, .wgsl types, canvas componentFor performance work and testing:
vgpu docs cat /guides/performance-model.docs.md
vgpu docs cat /guides/performance-patterns.docs.md
vgpu docs cat browser-testing