vgpu is a WebGPU library from Vercel Labs that describes itself as designed for agents: the docs are served as markdown over content negotiation, the examples ship with an OpenAPI description and a hosted MCP server, and the whole example gallery is pullable from a CLI with revision-pinned files and integrity hashes.
I gave it the obvious test: pull four examples, adapt them into standalone sites, and put them live. This is what that took, and what one of them cost afterwards.
Pulling four demos
The CLI is the entry point. npx vgpu examples search "black hole" finds the gallery entries, and npx vgpu examples pull black-hole --out ./black-hole writes the source into the workspace:
{"revision":"8ca322aa1cf0bc25aff3d38389d48090bf065c6baf14bea858fd9b8e4bceea96","id":"black-hole","out":"/Users/dan/code/black-hole","files":7,"bytes":18446,"aggregateSha256":"7b2bbffc32ed…"}
Every file set is pinned to a revision with an aggregate hash, so what you pull is what the gallery indexed. I pulled four: a raymarched black hole, a glass cube with screen-space refraction and chromatic dispersion, a volumetric logo flare, and a Navier-Stokes fluid solver driven by compute shaders.
Each example is a React component wrapping a framework-agnostic renderer: createRenderer({ canvas }) plus WGSL shaders. The React part is a dozen lines, so the adaptation to a standalone page is small. I dropped the wrapper, added a vanilla entry that creates the renderer on a fullscreen canvas, and bundled with esbuild using a .wgsl text loader.
All four are live now, each on its own subdomain behind the same nginx-plus-Dokploy setup the rest of my demos use: black-hole.danieljohnmorris.com, transmission.danieljohnmorris.com, nextjs-flare.danieljohnmorris.com, and fluid.danieljohnmorris.com. The bundling worked for three of them. The fourth needed a detour.
The WGSL import dialect
The fluid example’s shaders import each other:
import { Grid, index_of } from "./fluid-common.wgsl";
That is not standard WGSL: it is a module dialect vgpu resolves at build time, and a raw shader string with imports in it is rejected at runtime with VGPU-WGSL-RUNTIME-IMPORT. vgpu ships a Vite plugin for the dialect, @vgpu/wgsl/loader-vite. My standalone demos bundle with esbuild, so I inlined the imports with a twenty-line plugin. When I ported the sim into this site, which is Astro and therefore Vite, I swapped in the official loader.
The fluid was jerky
The fluid demo shipped and the first feedback arrived within the hour: it’s jerky when you drag. Five bugs were hiding behind that word.
The input handler overwrote its pointer segment on every event, but the sim consumes one segment per 60 Hz step and a mouse reports at 125 to 250 Hz, so fast drags painted a dashed trail. Keeping the segment start at the last consumed position fixes it. Three more were display-rate bugs: Chrome’s DevTools protocol screencasts every presented frame, and diffing adjacent frames showed 45 of 90 pixel-identical on a simulated 120 Hz display, because the sim renders a hard 60 steps per second. Blending the pre-step and post-step dye fields took that to zero. At 60 Hz the same blending turned rAF jitter into speed oscillation, so the renderer now blends only when the display clearly outruns the sim, a choice gated with hysteresis so it cannot flap while the rate hunts.
The last one: once the display ramps down to 60 Hz, Chrome’s rAF averages slightly over 16.67 ms, and the accumulator creeps until one frame fires two physics steps. A hitch every few seconds. Capping each frame at one step ends it.
Every measurement came from a faked clock in a headless browser, so the live demo takes a ?debug parameter that overlays fps, steps per second, and frame interval. The next report can arrive with numbers attached.
The same sim, behind this text
The fluid solver now runs as an option in this site’s background selector, with window-level pointer input so the dye follows your cursor behind the content. It carries all five fixes, resolved through the official Vite loader. Visitors without WebGPU never see it: the picker excludes it from rotation and the selector disables the option.
That is the loop vgpu seems designed for. The five bugs that came afterwards were all in pointer input and display timing.