A standalone Vite + React + TypeScript playground for experimenting with WebGPU. A single-page app that hosts multiple self-contained WebGPU demos behind a switcher.
Design principle: each demo's GPU code is a plain, framework-agnostic TS
module under src/demos/*/. React is a thin shell (switcher, control panels,
file upload) and never appears inside the GPU modules — so the useful demos (the
point cloud and the crossfilter engine especially) can be lifted into another
app with no rewrite.
npm install
npm run dev # http://localhost:5173
npm run build # tsc -b && vite build
npm run preview # serve the production build (with COOP/COEP)
npm run lint # oxlintOpen in a browser with WebGPU: recent Chrome / Edge (or Safari 18+), with hardware acceleration enabled. Browsers without WebGPU get a graceful "not supported" message instead of a blank screen.
src/gpu/types.ts— the core contracts:Demo,DemoInstance,DemoContext. A demo exports a pureinit(ctx); its optional ReactControlscomponent lives in a sibling.tsxand is wired up only in the registry.src/gpu/device.ts— singleton adapter/device init. Clears its cache on device loss so re-init is possible.src/gpu/registry.ts— the array of all demos (switcher order).src/host/CanvasHost.tsx— owns the canvas ref, the singlerequestAnimationFrameloop, aResizeObserverapplyingdevicePixelRatio, and device-loss / unsupported UI. Swapping demos =dispose()old,init()new (driven by a Reactkey).src/host/Sidebar.tsx— navigation list reading the registry.src/demos/*/— each demo. GPU code is React-free; WGSL is imported with Vite's?rawsuffix (import shader from './shader.wgsl?raw') so editing a shader hot-reloads.src/lib/— framework-agnostic helpers reused across demos:chunk.ts+vector-store.ts(retrieval),scales.ts(CPU-side linear scales/ticks for axes and hit-testing),gpu-image.ts(ImagePipeline— ping-pong rgba16float compute filter chain — andImageBlitter, shared by still-image and live-camera sources).
- Create
src/demos/<id>/index.tsexporting aDemowhoseinitreturns aDemoInstance(frame/ optionalresize/dispose). - (Optional) add a sibling
Controls.tsxand setControlson theDemo. - Register it in
src/gpu/registry.ts.
The inference libraries (transformers.js, WebLLM) need SharedArrayBuffer,
which requires the page to be cross-origin isolated.
The dev server and vite preview already send these headers (see
vite.config.ts):
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
credentialless (rather than require-corp) lets the cross-origin model files
from the Hugging Face CDN load without a CORP header on their responses, while
still keeping the page cross-origin isolated. Chromium and Firefox support it;
if you self-host models that send Cross-Origin-Resource-Policy: cross-origin,
require-corp works too (and is required for Safari, which doesn't yet support
credentialless).
Your production host must send the same two headers on the document response, or model loading fails in confusing ways. Examples:
Netlify — netlify.toml or public/_headers:
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
Vercel — vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
]
}
]
}nginx:
add_header Cross-Origin-Opener-Policy same-origin always;
add_header Cross-Origin-Embedder-Policy credentialless always;
- No SSR.
navigator.gpuonly exists in the browser (Vite SPA, no SSR). - GPU modules stay React-free. Anything under
src/demos/*/that touches the device is importable without React. - Inference runs in Web Workers (semantic-search, rag-llm) so the render loop never stutters.
- COOP/COEP headers required (above).
- Unhappy paths handled: no-WebGPU browsers,
GPUDeviceloss, and canvas resize withdevicePixelRatio.
Each is a self-contained module under src/demos/, chosen from the sidebar
(listed here in sidebar order).
- Volumetric Hologram (
volumetric-hologram) — an SDF density field raymarched front-to-back in a single fullscreen fragment pass and shaded with fresnel edges, scanlines, and chromatic drift. Drag to orbit; sliders for step count, density, hue, fresnel, scanlines, chroma, and flicker. - Fluid FX (
fluid-fx) — a GPU effects playground with three toggleable modes sharing one Inigo Quilez cosine palette (recolorable, with a hue shift):- Fluid — an interactive stable-fluids (Navier–Stokes) solver on a fixed
320×180 grid. Each frame runs splat → advect → divergence → Jacobi pressure
solve → gradient-subtract → dye-advect as compute passes over ping-pong
rgba16floatfields. Drag to inject swirling dye; auto-swirls when idle. - Flow — ~90k GPU particles advected through a curl-noise flow field, drawn as instanced soft points and accumulated into an HDR trail texture (a per-frame fade pass gives the trails). Drag to push the field; Speed/Trail sliders.
- Ambient — a passive two-level domain-warped fbm mapped through the palette; the pointer bends the field. Speed/Warp sliders.
- Fluid — an interactive stable-fluids (Navier–Stokes) solver on a fixed
320×180 grid. Each frame runs splat → advect → divergence → Jacobi pressure
solve → gradient-subtract → dye-advect as compute passes over ping-pong
- 3D Point Cloud (
point-cloud) — instanced rendering of a procedural galaxy with an orbit camera and depth buffer, and a 1k–1M point-count slider. - Media Lab (
image-lab) — upload an image (or use the synthetic sample) or process a livegetUserMediacamera feed with the same GPU compute filters: exposure, contrast, saturation, temperature, separable gaussian blur, unsharp, Sobel edges, vignette, and grayscale. Each filter is a compute pass over ping-pongrgba16floattextures (one pass per step, so reads see the previous pass's full output). Still images only re-run when a value changes; live video runs in real time with optional selfie mirroring. A before/after split wipes the result against the original, and the feed never leaves the browser. - Crossfilter Dashboard (
crossfilter) — five linked panels (three histograms, a time series, a latency×cost density heatmap) over 1M–10M rows of synthetic LLM telemetry. Drag a range on any histogram and every other panel refilters instantly; brushes compound across dimensions. The dataset is pre-binned and packed one u32 per row (data.ts), uploaded once as a columnar buffer, and an atomic-scatter compute pass (aggregate.wgsl) rebuilds every panel's histogram on each brush change — the CPU never rescans the rows.engine.ts(React-free) owns the buffers and brush state; panels render into viewports of one canvas (render.wgsl). Interactive at 10M rows. - XPBD Physics (
xpbd) — a GPU particle-constraint physics engine (Extended Position-Based Dynamics), shown as cloth. Each frame runs a substepped solve: integrate → graph-colored distance constraints → spatial-hash self-collision (plus floor/sphere) → finalize, all as compute passes over storage buffers, then recomputes normals and renders the mesh. Sliders for substeps, gravity, wind, stiffness, and particle radius; drag to orbit. - Semantic Search (
semantic-search) — paste a document; transformers.js embeds it in a Web Worker (WebGPU backend), and queries retrieve the most relevant chunks by cosine similarity, fully local. - RAG Chat (
rag-llm) — reuses the semantic-search retrieval and adds a WebLLM generation worker. A question retrieves the top passages as grounded context for a small instruct model (default Llama-3.2-1B, swappable); tokens stream back into the chat. - LLM Observability (
observability) — a toggle-driven DX/debugging demo. It captures live browser signals (console, network, errors, performance, and a React error boundary), runs them through a deterministic reduction pipeline (redact → sample → dedupe/cluster → normalize → stack-frame extraction) of pure, independently toggleable functions, then uses an in-browser WebLLM model to narrate the reduced digest — clustering, severity, category, and advisory-only remediations. The LLM is the narration layer, not the brain: all detection and classification-by-rule is deterministic and nothing the model emits triggers an action. Redaction is on by default (leaking is the opt-in demo); WebLLM runs in a Web Worker by default, with a toggle to move it to the main thread and demonstrate jank; structured output is XGrammar/JSON-schema-constrained. Degrades gracefully with no WebGPU — the LLM disables while capture and reduction keep running.
Three shapes of demo:
- Canvas demos (volumetric-hologram, fluid-fx, point-cloud, image-lab,
crossfilter, xpbd) provide a React-free
init(ctx)and run underCanvasHostwith an optionalControlsside panel. - DOM/inference demos (semantic-search, rag-llm) provide a
Panelthat takes over the main area — WebGPU is the compute/inference backend inside Web Workers, so there's no canvas or render loop. - DOM/observability demo (observability) also provides a
Panel, but WebGPU backs an in-browser WebLLM model that narrates deterministically captured and reduced browser telemetry.
@webgpu/typesprovides the WebGPU TS types (added totsconfig.app.jsontypes).- Backing-store DPR is capped at 2 in
CanvasHostto avoid huge render targets on HiDPI displays. - The inference libs are marked
optimizeDeps.excludeinvite.config.tsso Vite serves their workers/wasm as-is.