- Published on
WebGPU and Rust/WASM: a small playground
- Authors

- Name
- Garfield Zhu
- @_AlohaYo_
@Author: Garfield Zhu
WebGPU and Rust/WASM: a small playground
I keep two sets of notes. The GPU notes lead to WebGPU. The WASM Quick Tour leads to Rust running in the browser.
Putting the two together sounds very serious. It is mostly one triangle and a color. The triangle is not impressed by my architecture diagram.
The split I want
Rust/WASM should do the small, deterministic calculation. JavaScript should own the browser APIs. WebGPU should own the canvas and shader. This is close to the split in the WASM Game of Life note: Rust owns the state, JavaScript reads the boundary and paints it.
Rust Game of Life example — hover for a previewA real Rust crate can expose a tiny function like this:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn shade(input: i32) -> i32 {
(input * 7).rem_euclid(255)
}
wasm-pack build --target web creates the browser package. JavaScript loads it with init() and calls shade(input). The wasm-bindgen deployment guide explains the no-bundler web target.
WebGPU starts in a different place: check navigator.gpu, request an adapter and a device, configure a canvas, then submit a render pass. MDN's WebGPU guide is the useful reference here. The shader is WGSL, not Rust. Every language gets its own little kingdom.
Try the playground
Start with the default code and press Run WASM. The comments in the editor are the map:
local.get 0reads the input control.i32.const 7is the multiplier. Change it to3, run again, andshade(12)changes from84to36.- The number is also the hue, so the triangle changes from green-ish to orange-ish.
Edit the WASM, then run it
One exported i32 → i32 function. No imports, no DOM, no surprise.
Change i32.const 7 to i32.const 3, then press Run WASM. With input 12, the result goes from 84 to 36, so the triangle changes color. ⌘/Ctrl + Enter works too.
Preparing the browser runtime…
Preparing the preview…
The code can still run while WebGPU is checked.
The WASM return value becomes a color and is sent to a WebGPU uniform buffer. The triangle is intentionally humble.
The editor is WAT-shaped and deliberately small. It compiles a real WebAssembly binary in the browser, then uses WebAssembly.instantiate() to call the export. No imports, memory, DOM access, or arbitrary JavaScript. A tiny fence is better than turning my blog into a tiny compiler-hosting company.
The real Rust path is still the one above. Editing arbitrary Rust in a static article would mean shipping a Rust compiler or a remote build service. That is possible, but it is a different project with a much larger security and download bill. The WAT editor keeps the interesting part visible: edit instructions, compile bytes, call an export, pass a value to the GPU.
How the playground is wired into MDX
The component is registered in components/MDXComponents.tsx, so the article can use <WebGpuWasmPlayground /> without importing React in the post.
The data flow is small:
- The textarea holds one safe WAT module with an
i32 -> i32export calledshade. - A tiny assembler turns the supported instructions into a WebAssembly binary.
WebAssembly.instantiate()creates the instance and runsshade(input).- The result is converted to a color and written to a WebGPU uniform buffer.
- A WGSL vertex/fragment pair draws the triangle.
The WebGPU branch is progressive enhancement. If navigator.gpu or the adapter is missing, the WASM result still appears in a visible fallback card. WebGPU is a secure-context API and browser/device support varies, so “works on my Mac” is not a compatibility plan.
Build it from scratch
For the Rust side, the old Rust and WebAssembly book and the wasm-pack quickstart are still the shortest route:
cargo new --lib shade
wasm-pack build --target web
Give the crate a cdylib target, export functions with #[wasm_bindgen], and keep the JavaScript/WASM boundary typed and boring. The Game of Life project is a better next exercise than inventing a framework on day one.
For the WebGPU side, the first pass is intentionally plain JavaScript:
if (!navigator.gpu) throw new Error('WebGPU is not available')
const adapter = await navigator.gpu.requestAdapter()
const device = await adapter.requestDevice()
const context = canvas.getContext('webgpu')
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() })
After that, create a shader module, a pipeline, a uniform buffer, and a command encoder. No framework is required. This is good news and also slightly rude, because now the mistakes are mine.
Next experiments
- Move the WASM call into a worker and measure the message/transfer cost.
- Replace the tiny assembler with a full WAT parser only if the article needs it.
- Build the Rust crate in CI and load its
--target weboutput as a versioned asset. - Use a compute shader for a small particle or reaction-diffusion step, then compare it with the same step in Rust/WASM.
- Keep a CPU fallback and show the capability result instead of hiding a blank canvas.
The point is not “WASM plus WebGPU everywhere.” It is a clean boundary I can inspect. Rust computes, WebGPU paints, and MDX explains the hand-off without swallowing the whole blog.