Jacket is a graph-based visualizer for the execution of WebGPU code.
It is a static, offline-capable web app which watches your local code folder, runs your WebGPU code and shows you in realtime what it's doing.
Jacket makes use of newish (for mid-2025) Web APIs. As such it will only work on recent browsers such as Chrome stable and soon Firefox stable.
- Show you WebGPU objects such as buffers, pipelines, encoders. Basically any of the classes named
GPUxxxin Javascript - Show object relationships, principally 'produced-by' such as
GPUDevice -> GPUCommandEncoder, and 'used-by' e.g.GPUBuffer -> GPUBindGroup - Give the class-specific status/details of objects like whether a
GPUBufferis mapped or unmapped - Do it all in realtime
- Auto-reload your local code
- Be used entirely offline. It won't send your code anywhere and uses a service worker to cache itself after first load
Visit the hosted version, click the 'path' button to load up a folder. The folder should minimally contain a main.js module which exports program(params) {...}.
The params object contains:
- proxied versions of the
navigatorandHTMLCanvasElement - a
filesrecord with name and text of any other files found in the folder. So for example if you havemain.jsand next to it afragment.wgsl, your code would access that viaparams.files['fragment.wgsl']
In this function you can write your setup code, and then return a Promise<() => void> containing your per-frame code.
If you prefer to use a bundler and external dependencies that's fine, you can point Jacket at your build folder and it should be happy!
See
examples/for one way to set that up.
Just clone this repo and run npm i then npm run dev or npm run build; npm run preview
Values produced by your code are represented as nodes. To the right of the node will be the values of relevant properties/object state:
A method call is represented by a 🔹 blue edge running from the parent object to the returned value:
Arguments to method calls are connected with a 🩶 grey 'dependency' edge to the value which required them:
Finally any kind of edge can be either solid or dashed (animated). A dashed edge indicates that the target was created as part of per-frame rendering:
Jacket can be boiled down to a pipeline with the following components:
- A folder watcher which uses the File System API to gain continuous access to a local folder. The handle is saved in IndexedDB and reused on page reload.
- An executor which takes the code, turns it into a JS module and calls
module.programwith the parameters. Thenavigatorand canvas are wrapped in a Proxy which is able to track method calls, produced values, etc. The tracked values are given IDs and have their relationships encoded using those IDs. Everything gets stored in a flat array for further processing. - The visualizer, which turns the flat array into a graph, does some pruning to get rid of nulls and primitives and displays it all in a React Flow component.
As the user's code runs, it produces values of arbitrary kinds, passes them around and consumes them. Jacket's job is to observe this process and visualize it, hopefully without requiring too many changes by the user. There are many possible options for programmatically observing code without prior knowledge but a popular one makes use of a Proxy which wraps the underlying values. Proxies are great because they can be made transparent to the user's code while allowing us to hook into property access. They're also tricky to use and can lead to many subtle bugs. If you use vanilla Proxies be assured you will meet all the seediest 'features' of Javascript.
An option not explored here is static analysis, i.e. parsing the source without running it and trying to infer something about its semantics. Jacket doesn't do this because it's hard and we can avoid it for our use-case.
Anyway, after the initial bug squashing, our proxies look like this:
- We wrap a pre-existing
HTMLCanvasElementand the built-innavigatorin our proxy - The proxies are passed to the user's code
- Wherever a proxy's properties are accessed, we track the event, then take the underlying values and wrap those in a proxy too. The same happens with the results of method calls and promises. Examples:
navigator.gpu- logs a property access
- evaluates to
Proxy(GPU)
gpu.requestAdapter(...)- logs a property access, a method call and a promise resolution
- evaluates to a promise which resolves to
Proxy(GPUAdapter)
- The proxies spread out tree-like, wrapping buffers, pipelines, encoders, etc
- Expose more information
- Improve performance & stability
mini for her awesome library proxify (should see a proper release soonTM)




