| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
GPU.js is a JavaScript Acceleration library for GPGPU (General purpose computing on GPUs) in JavaScript for Web and Node. GPU.js automatically transpiles simple JavaScript functions into shader language and compiles them so they run on your GPU. In case a GPU is not available, the functions will still run in regular JavaScript. For some more quick concepts, see Quick Concepts on the wiki.
New to GPU programming? Learn GPGPU in your browser — a free, hands-on course that teaches the subject itself, not just this library. See Learn GPGPU below.
Creates a GPU accelerated kernel transpiled from a javascript function that computes a single element in the 512 x 512 matrix (2D array). The kernel functions are ran in tandem on the GPU often resulting in very fast computations! You can run a benchmark of this here. Typically, it will run 1-15x faster depending on your hardware. Matrix multiplication (perform matrix multiplication on 2 matrices of size 512 x 512) written in GPU.js:
<script src="dist/gpu-browser.min.js"></script>
<script>
// GPU is a constructor and namespace for browser
const gpu = new GPU();
const multiplyMatrix = gpu.createKernel(function(a, b) {
let sum = 0;
for (let i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);
const c = multiplyMatrix(a, b);
</script>https://unpkg.com/gpu.js@latest/dist/gpu-browser.min.js https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js
const { GPU } = require('gpu.js');
const gpu = new GPU();
const multiplyMatrix = gpu.createKernel(function(a, b) {
let sum = 0;
for (let i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);
const c = multiplyMatrix(a, b);import { GPU } from 'gpu.js';
const gpu = new GPU();
const multiplyMatrix = gpu.createKernel(function(a: number[][], b: number[][]) {
let sum = 0;
for (let i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);
const c = multiplyMatrix(a, b) as number[][];Click here for more typescript examples.
Warning
The next major version of GPU.js will make every kernel call return a Promise. This is a breaking API change: synchronous kernel calls as you write them today will not survive the v3 upgrade unchanged. Code written against mode: 'async' (new in 2.20.0) already conforms and will run on v3 unchanged — the migration guide below is six steps.
This breaks the API you are using today, so it warrants both notice and an apology. We owe you the apology because the original synchronous design was not forward-thinking, and we should have started async in the first place. A GPU is an asynchronous device: you hand it work, and the results are ready later. WebGL let this library pretend otherwise — readPixels silently freezes the page until the GPU catches up, and we built our API on that pretense because it made the first example look like an ordinary function call. The cost has been paid by every user since: every kernel readback blocks the main thread for its full duration (measurably ~96% of a readback-heavy loop frozen, in one stall as long as the whole loop), and WebGPU — which has no synchronous readback at all, correctly — cannot be offered under the synchronous contract except as a walled-off special mode. An async-first API would have cost one await in the examples and none of this debt.
v3 corrects the mistake: async everywhere, one contract, every backend. The WebGL backends keep a synchronous escape hatch (setAsyncMode(false)) through the migration; WebGPU can never offer one.
Async-by-default is not an aesthetic preference — it is the price of making WebGPU a first-class backend instead of a walled-off special mode, and WebGPU earns that price twice over.
Performance. Measured on the same kernels, same machine (Apple M1 Max), against our own WebGL2 backend at its best:
Accuracy. This one matters more than the speed. Every GPU backend before WebGPU computes by pretending a fragment shader is a compute unit, and this library carries years of scar tissue from that pretense — workarounds you may be relying on without knowing it:
The synchronous API is the only thing standing between users and those improvements being the default. That is why it goes.
The v3 contract is available today — opt in with mode: 'async' (or asyncMode: true per kernel) and your code is already v3-shaped:
// v2 (sync)
const gpu = new GPU();
const kernel = gpu.createKernel(fn).setOutput([512, 512]);
const result = kernel(a, b);
// v3 (async) — works today with { mode: 'async' }
const gpu = new GPU({ mode: 'async' });
const kernel = gpu.createKernel(fn).setOutput([512, 512]);
const result = await kernel(a, b);Notice documentation is off? We do try our hardest, but if you find something, please bring it to our attention, or become a contributor!
Learn GPGPU in your browser — a free, hands-on course built on GPU.js. Fifteen lessons across three modules, roughly ten hours, with no toolchain to install: you write real kernels in the page and run them on your own GPU, with the results in front of you.
The point worth making is that it teaches GPGPU, not just this library. GPU.js is the vehicle, chosen because JavaScript in a browser is the shortest path from "no setup" to "code running on your GPU" — but what you take away is the subject itself, and it transfers:
| module | lessons |
|---|---|
| 1 — Fundamentals | Hello, Kernel · Data In, Data Out · Thinking in Parallel · Pipelines & Textures · Measuring Speed Honestly |
| 2 — Real algorithms | Matrix Multiply · Reductions · Convolution & Filters · Monte Carlo Methods · N-Body Gravity |
| 3 — Graphics | Pixels from Scratch · Escape-Time Fractals · Cellular Automata · Reaction–Diffusion · Ray-Marched Metaballs |
Start at Hello, Kernel — if you can write a JavaScript for loop, you have the prerequisites.
Representative performance factor: 1024×1024 matrix multiplication including readback, versus the CPU backend on the same machine (Apple M1 Max, Chromium; your hardware will vary — run node scripts/benchmark-webgpu.mjs for yours).
| Backend | Environment | Technology | Perf factor | Notes |
|---|---|---|---|---|
| webgpu New in 2.20.0! | Browser | WGSL compute shaders | ~370× | Async API; opt-in via mode: 'webgpu' or automatic via mode: 'async' |
| webgl2 | Browser | GLSL ES 3.00 fragment shaders | ~127× | The default browser backend. 2.20.0 renders scalar single-precision kernels to R32F and reads back one float per value where the driver allows |
| webgl | Browser | GLSL ES 1.00 fragment shaders | ~87× | Fallback for older browsers |
| headlessgl | Node | GLSL ES 1.00 via ANGLE | ~123× | The default Node backend |
| webasm New! | Anywhere | WebAssembly + f32x4 SIMD + threads | Auto-selected only where no GL backend works; explicit via mode: 'webasm'. Threads engage under the async contract | |
| cpu | Anywhere | Plain JavaScript | 1× | Guaranteed fallback; also the reference for correctness |
GPU.js in the wild, all around the net. Add yours here!
More examples with screenshots: gpu.rocks examples gallery
Libraries and tools built on GPU.js:
A note on CodePen: its JavaScript "loop protection" rewrites loops inside kernel functions (injecting window.CP.shouldStopExecution(...)), which breaks kernel transpilation. Disable loop protection in the pen's JS settings, or use Observable/JSFiddle instead. ||||||| 6d7dde3
On Linux, ensure you have the correct header files installed: sudo apt install mesa-common-dev libxi-dev (adjust for your distribution)
npm install gpu.js --saveyarn add gpu.jsconst { GPU } = require('gpu.js');
const gpu = new GPU();import { GPU } from 'gpu.js';
const gpu = new GPU();Download the latest version of GPU.js and include the files in your HTML page using the following tags:
<script src="dist/gpu-browser.min.js"></script>
<script>
const gpu = new GPU();
</script>Settings are an object used to create an instance of GPU. Example: new GPU(settings)
const gpu = new GPU({ mode: 'async' });
const kernel = gpu.createKernel(function(a) {
return a[this.thread.x] * 2;
}).setOutput([64]);
const result = await kernel(myArray); // webgpu, webgl2 or cpu underneathSettings are an object used to create a kernel or kernelMap. Example: gpu.createKernel(settings)
const result = kernel();
result.toArray();kernel(texture);Depending on your output type, specify the intended size of your output. You cannot have an accelerated function that does not specify any output size.
| Output size | How to specify output size | How to reference in kernel |
|---|---|---|
| 1D | [length] | value[this.thread.x] |
| 2D | [width, height] | value[this.thread.y][this.thread.x] |
| 3D | [width, height, depth] | value[this.thread.z][this.thread.y][this.thread.x] |
const settings = {
output: [100]
};or
// You can also use x, y, and z
const settings = {
output: { x: 100 }
};Create the function you want to run on the GPU. The first input parameter to createKernel is a kernel function which will compute a single number in the output. The thread identifiers, this.thread.x, this.thread.y or this.thread.z will allow you to specify the appropriate behavior of the kernel function at specific positions of the output.
const kernel = gpu.createKernel(function() {
return this.thread.x;
}, settings);The created function is a regular JavaScript function, and you can use it like one.
kernel();
// Result: Float32Array[0, 1, 2, 3, ... 99]Note: Instead of creating an object, you can use the chainable shortcut methods as a neater way of specifying settings.
const kernel = gpu.createKernel(function() {
return this.thread.x;
}).setOutput([100]);
kernel();
// Result: Float32Array[0, 1, 2, 3, ... 99]GPU.js makes variable declaration inside kernel functions easy. Variable types supported are:
Number kernel example:
const kernel = gpu.createKernel(function() {
const i = 1;
const j = 0.89;
return i + j;
}).setOutput([100]);Boolean kernel example:
const kernel = gpu.createKernel(function() {
const i = true;
if (i) return 1;
return 0;
}).setOutput([100]);Array(2) kernel examples: Using declaration
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2];
return array2;
}).setOutput([100]);Directly returned
const kernel = gpu.createKernel(function() {
return [0.08, 2];
}).setOutput([100]);Array(3) kernel example: Using declaration
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2, 0.1];
return array2;
}).setOutput([100]);Directly returned
const kernel = gpu.createKernel(function() {
return [0.08, 2, 0.1];
}).setOutput([100]);Array(4) kernel example: Using declaration
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2, 0.1, 3];
return array2;
}).setOutput([100]);Directly returned
const kernel = gpu.createKernel(function() {
return [0.08, 2, 0.1, 3];
}).setOutput([100]);private Function kernel example:
const kernel = gpu.createKernel(function() {
function myPrivateFunction() {
return [0.08, 2, 0.1, 3];
}
return myPrivateFunction(); // <-- type inherited here
}).setOutput([100]);Debugging can be done in a variety of ways, and there are different levels of debugging.
const gpu = new GPU({ mode: 'dev' });
const kernel = gpu.createKernel(function(arg1, time) {
// put a breakpoint on the next line, and watch it get hit
const v = arg1[this.thread.y][this.thread.x * time];
return v;
}, { output: [100, 100] });const gpu = new GPU({ mode: 'cpu' });
const kernel = gpu.createKernel(function(arg1, time) {
debugger; // <--NOTICE THIS, IMPORTANT!
const v = arg1[this.thread.y][this.thread.x * time];
return v;
}, { output: [100, 100] });const gpu = new GPU({ mode: 'cpu' });
const kernel = gpu.createKernel(function(arg1, time) {
const x = this.thread.x * time;
return x; // <--NOTICE THIS, IMPORTANT!
const v = arg1[this.thread.y][x];
return v;
}, { output: [100, 100] });const gpu = new GPU({ mode: 'cpu' });
const kernel = gpu.createKernel(function(arg1, time) {
const x = this.thread.x * time;
if (x < 4 || x > 2) {
// RED
this.color(1, 0, 0); // <--NOTICE THIS, IMPORTANT!
return;
}
if (x > 6 && x < 12) {
// GREEN
this.color(0, 1, 0); // <--NOTICE THIS, IMPORTANT!
return;
}
const v = arg1[this.thread.y][x];
return v;
}, { output: [100, 100], graphical: true });const { input } = require('gpu.js');
const value = input(flattenedArray, [width, height, depth]);input(new Float32Array([1,2, 3,4, 5,6, 7,8]), [2, 2, 2])
// same as: [ [[1,2],[3,4]], [[5,6],[7,8]] ]const kernel = gpu.createKernel(function(x) {
return x;
}).setOutput([100]);
kernel(42);
// Result: Float32Array[42, 42, 42, 42, ... 42]Similarly, with array inputs:
const kernel = gpu.createKernel(function(x) {
return x[this.thread.x % 3];
}).setOutput([100]);
kernel([1, 2, 3]);
// Result: Float32Array[1, 2, 3, 1, ... 1 ]An HTML Image:
const kernel = gpu.createKernel(function(image) {
const pixel = image[this.thread.y][this.thread.x];
this.color(pixel[0], pixel[1], pixel[2], pixel[3]);
})
.setGraphical(true)
.setOutput([100, 100]);
const image = document.createElement('img');
image.src = 'my/image/source.png';
image.onload = () => {
kernel(image);
// Result: colorful image
document.getElementsByTagName('body')[0].appendChild(kernel.canvas);
};An Array of HTML Images:
const kernel = gpu.createKernel(function(image) {
const pixel = image[this.thread.z][this.thread.y][this.thread.x];
this.color(pixel[0], pixel[1], pixel[2], pixel[3]);
})
.setGraphical(true)
.setOutput([100, 100]);
const image1 = document.createElement('img');
image1.src = 'my/image/source1.png';
image1.onload = onload;
const image2 = document.createElement('img');
image2.src = 'my/image/source2.png';
image2.onload = onload;
const image3 = document.createElement('img');
image3.src = 'my/image/source3.png';
image3.onload = onload;
const totalImages = 3;
let loadedImages = 0;
function onload() {
loadedImages++;
if (loadedImages === totalImages) {
kernel([image1, image2, image3]);
// Result: colorful image composed of many images
document.getElementsByTagName('body')[0].appendChild(kernel.canvas);
}
};An HTML Video: New in V2!
const kernel = gpu.createKernel(function(videoFrame) {
const pixel = videoFrame[this.thread.y][this.thread.x];
this.color(pixel[0], pixel[1], pixel[2], pixel[3]);
})
.setGraphical(true)
.setOutput([100, 100]);
const video = new document.createElement('video');
video.src = 'my/video/source.webm';
kernel(image); //note, try and use requestAnimationFrame, and the video should be ready or playing
// Result: video frameSometimes, you want to produce a canvas image instead of doing numeric computations. To achieve this, set the graphical flag to true and the output dimensions to [width, height]. The thread identifiers will now refer to the x and y coordinate of the pixel you are producing. Inside your kernel function, use this.color(r,g,b) or this.color(r,g,b,a) to specify the color of the pixel.
For performance reasons, the return value of your function will no longer be anything useful. Instead, to display the image, retrieve the canvas DOM node and insert it into your page.
const render = gpu.createKernel(function() {
this.color(0, 0, 0, 1);
})
.setOutput([20, 20])
.setGraphical(true);
render();
const canvas = render.canvas;
document.getElementsByTagName('body')[0].appendChild(canvas);Note: To animate the rendering, use requestAnimationFrame instead of setTimeout for optimal performance. For more information, see this.
To make it easier to get pixels from a context, use kernel.getPixels(), which returns a flat array similar to what you get from WebGL's readPixels method. A note on why: webgl's readPixels returns an array ordered differently from javascript's getImageData. This makes them behave similarly. While the values may be somewhat different, because of graphical precision available in the kernel, and alpha, this allows us to easily get pixel data in unified way.
Example:
const render = gpu.createKernel(function() {
this.color(0, 0, 0, 1);
})
.setOutput([20, 20])
.setGraphical(true);
render();
const pixels = render.getPixels();
// [r,g,b,a, r,g,b,a...Currently, if you need alpha do something like enabling premultipliedAlpha with your own gl context:
const canvas = DOM.canvas(500, 500);
const gl = canvas.getContext('webgl2', { premultipliedAlpha: false });
const gpu = new GPU({
canvas,
context: gl
});
const krender = gpu.createKernel(function(x) {
this.color(this.thread.x / 500, this.thread.y / 500, x[0], x[1]);
})
.setOutput([500, 500])
.setGraphical(true);Sometimes you want to do multiple math operations on the gpu without the round trip penalty of data transfer from cpu to gpu to cpu to gpu, etc. To aid this there is the combineKernels method. Note: Kernels can have different output sizes.
const add = gpu.createKernel(function(a, b) {
return a[this.thread.x] + b[this.thread.x];
}).setOutput([20]);
const multiply = gpu.createKernel(function(a, b) {
return a[this.thread.x] * b[this.thread.x];
}).setOutput([20]);
const superKernel = gpu.combineKernels(add, multiply, function(a, b, c) {
return multiply(add(a, b), c);
});
superKernel(a, b, c);This gives you the flexibility of using multiple transformations but without the performance penalty, resulting in a much much MUCH faster operation.
Sometimes you want to do multiple math operations in one kernel, and save the output of each of those operations. An example is Machine Learning where the previous output is required for back propagation. To aid this there is the createKernelMap method.
const megaKernel = gpu.createKernelMap({
addResult: function add(a, b) {
return a + b;
},
multiplyResult: function multiply(a, b) {
return a * b;
},
}, function(a, b, c) {
return multiply(add(a[this.thread.x], b[this.thread.x]), c[this.thread.x]);
}, { output: [10] });
megaKernel(a, b, c);
// Result: { addResult: Float32Array, multiplyResult: Float32Array, result: Float32Array }const megaKernel = gpu.createKernelMap([
function add(a, b) {
return a + b;
},
function multiply(a, b) {
return a * b;
}
], function(a, b, c) {
return multiply(add(a[this.thread.x], b[this.thread.x]), c[this.thread.x]);
}, { output: [10] });
megaKernel(a, b, c);
// Result: { 0: Float32Array, 1: Float32Array, result: Float32Array }This gives you the flexibility of using parts of a single transformation without the performance penalty, resulting in much much MUCH faster operation.
use gpu.addFunction(function() {}, settings) for adding custom functions to all kernels. Needs to be called BEFORE gpu.createKernel. Example:
gpu.addFunction(function mySuperFunction(a, b) {
return a - b;
});
function anotherFunction(value) {
return value + 1;
}
gpu.addFunction(anotherFunction);
const kernel = gpu.createKernel(function(a, b) {
return anotherFunction(mySuperFunction(a[this.thread.x], b[this.thread.x]));
}).setOutput([20]);use kernel.addFunction(function() {}, settings) for adding custom functions to all kernels. Example:
kernel.addFunction(function mySuperFunction(a, b) {
return a - b;
});
function anotherFunction(value) {
return value + 1;
}
kernel.addFunction(anotherFunction);
const kernel = gpu.createKernel(function(a, b) {
return anotherFunction(mySuperFunction(a[this.thread.x], b[this.thread.x]));
}).setOutput([20]);To manually strongly type a function you may use settings. By setting this value, it makes the build step of the kernel less resource intensive. Settings take an optional hash values:
Example on GPU instance:
gpu.addFunction(function mySuperFunction(a, b) {
return [a - b[1], b[0] - a];
}, { argumentTypes: { a: 'Number', b: 'Array(2)'}, returnType: 'Array(2)' });Example on Kernel instance:
kernel.addFunction(function mySuperFunction(a, b) {
return [a - b[1], b[0] - a];
}, { argumentTypes: { a: 'Number', b: 'Array(2)'}, returnType: 'Array(2)' });NOTE: GPU.js infers types if they are not defined and is generally able to detect the types you need, however 'Array(2)', 'Array(3)', and 'Array(4)' are exceptions, at least on the kernel level. Also, it is nice to have power over the automatic type inference system.
function mySuperFunction(a, b) {
return a - b;
}
const kernel = gpu.createKernel(function(a, b) {
return mySuperFunction(a[this.thread.x], b[this.thread.x]);
})
.setOutput([20])
.setFunctions([mySuperFunction]);GPU.js does type inference when types are not defined, so even if you code weak type, you are typing strongly typed. This is needed because c++, which glsl is a subset of, is, of course, strongly typed. Types that can be used with GPU.js are as follows:
NOTE: These refer the the return type of the kernel function, the actual result will always be a collection in the size of the defined output
Types generally used in the Texture class, for #pipelining or for advanced usage.
const matMult = gpu.createKernel(function(a, b) {
var sum = 0;
for (var i = 0; i < this.constants.size; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}, {
constants: { size: 512 },
output: [512, 512],
});const matMult = gpu.createKernel(function(a, b) {
var sum = 0;
for (var i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);Pipeline is a feature where values are sent directly from kernel to kernel via a texture. This results in extremely fast computing. This is achieved with the kernel setting pipeline: boolean or by calling kernel.setPipeline(true) In an effort to make the CPU and GPU work similarly, pipeline on CPU and GPU modes causes the kernel result to be reused when immutable: false (which is default). If you'd like to keep kernel results around, use immutable: true and ensure you cleanup memory:
When using pipeline mode the outputs from kernels can be cloned using texture.clone().
const kernel1 = gpu.createKernel(function(v) {
return v[this.thread.x];
})
.setPipeline(true)
.setOutput([100]);
const kernel2 = gpu.createKernel(function(v) {
return v[this.thread.x];
})
.setOutput([100]);
const result1 = kernel1(array);
// Result: Texture
console.log(result1.toArray());
// Result: Float32Array[0, 1, 2, 3, ... 99]
const result2 = kernel2(result1);
// Result: Float32Array[0, 1, 2, 3, ... 99]When using kernel.immutable = true recycling GPU memory is handled internally, but a good practice is to clean up memory you no longer need it. Cleanup kernel outputs by using texture.delete() to keep GPU memory as small as possible.
NOTE: Internally textures will only release from memory if there are no references to them. When using pipeline mode on a kernel K the output for each call will be a newly allocated texture T. If, after getting texture T as an output, T.delete() is called, the next call to K will reuse T as its output texture.
Alternatively, if you'd like to clear out a texture and yet keep it in memory, you may use texture.clear(), which will cause the texture to persist in memory, but its internal values to become all zeros.
GPU.js supports offscreen canvas where available. Here is an example of how to use it with two files, gpu-worker.js, and index.js:
file: gpu-worker.js
importScripts('path/to/gpu.js');
onmessage = function() {
// define gpu instance
const gpu = new GPU();
// input values
const a = [1,2,3];
const b = [3,2,1];
// setup kernel
const kernel = gpu.createKernel(function(a, b) {
return a[this.thread.x] - b[this.thread.x];
})
.setOutput([3]);
// output some results!
postMessage(kernel(a, b));
};file: index.js
var worker = new Worker('gpu-worker.js');
worker.onmessage = function(e) {
var result = e.data;
console.log(result);
};To use the useful x, y, z thread lookup api inside of GPU.js, and yet use flattened arrays, there is the Input type. This is generally much faster for when sending values to the gpu, especially with larger data sets. Usage example:
const { GPU, input, Input } = require('gpu.js');
const gpu = new GPU();
const kernel = gpu.createKernel(function(a, b) {
return a[this.thread.y][this.thread.x] + b[this.thread.y][this.thread.x];
}).setOutput([3,3]);
kernel(
input(
new Float32Array([1,2,3,4,5,6,7,8,9]),
[3, 3]
),
input(
new Float32Array([1,2,3,4,5,6,7,8,9]),
[3, 3]
)
);Note: input(value, size) is a simple pointer for new Input(value, size)
GPU.js packs a lot of functionality into a single file, such as a complete javascript parse, which may not be needed in some cases. To aid in keeping your kernels lightweight, the kernel.toJSON() method was added. This allows you to reuse a previously built kernel, without the need to re-parse the javascript. Here is an example:
const gpu = new GPU();
const kernel = gpu.createKernel(function() {
return [1,2,3,4];
}, { output: [1] });
console.log(kernel()); // [Float32Array([1,2,3,4])];
const json = kernel.toJSON();
const newKernelFromJson = gpu.createKernel(json);
console.log(newKernelFromJSON()); // [Float32Array([1,2,3,4])];NOTE: There is lighter weight, pre-built, version of GPU.js to assist with serializing from to and from json in the dist folder of the project, which include:
GPU.js supports seeing exactly how it is interacting with the graphics processor by means of the kernel.toString(...) method. This method, when called, creates a kernel that executes exactly the instruction set given to the GPU (or CPU) as a very tiny reusable function that instantiates a kernel.
NOTE: When exporting a kernel and using constants the following constants are not changeable:
Here is an example used to/from file:
import { GPU } from 'gpu.js';
import * as fs from 'fs';
const gpu = new GPU();
const kernel = gpu.createKernel(function(v) {
return this.thread.x + v + this.constants.v1;
}, { output: [10], constants: { v1: 100 } });
const result = kernel(1);
const kernelString = kernel.toString(1);
fs.writeFileSync('./my-exported-kernel.js', 'module.exports = ' + kernelString);
import * as MyExportedKernel from './my-exported-kernel';
import gl from 'gl';
const myExportedKernel = MyExportedKernel({ context: gl(1,1), constants: { v1: 100 } });Here is an example for just-in-time function creation:
const gpu = new GPU();
const kernel = gpu.createKernel(function(a) {
let sum = 0;
for (let i = 0; i < 6; i++) {
sum += a[this.thread.x][i];
}
return sum;
}, { output: [6] });
kernel(input(a, [6, 6]));
const kernelString = kernel.toString(input(a, [6, 6]));
const newKernel = new Function('return ' + kernelString)()({ context });
newKernel(input(a, [6, 6]));You can assign some new constants when using the function output from .toString(),
Since the code running in the kernel is actually compiled to GLSL code, not all functions from the JavaScript Math module are supported.
This is a list of the supported ones:
const kernel = gpu.createKernel(function() {
return Math.random();
}, { output: [64], randomSeed: 42 });This is a list and reasons of unsupported ones:
To assist with mostly unit tests, but perhaps in scenarios outside of GPU.js, there are the following logical checks to determine what support level the system executing a GPU.js kernel may have:
Typescript is supported! Typings can be found here! For strongly typed kernels:
import { GPU, IKernelFunctionThis } from 'gpu.js';
const gpu = new GPU();
function kernelFunction(this: IKernelFunctionThis): number {
return 1 + this.thread.x;
}
const kernelMap = gpu.createKernel<typeof kernelFunction>(kernelFunction)
.setOutput([3,3,3]);
const result = kernelMap();
console.log(result as number[][][]);For strongly typed mapped kernels:
import { GPU, Texture, IKernelFunctionThis } from 'gpu.js';
const gpu = new GPU();
function kernelFunction(this: IKernelFunctionThis): [number, number] {
return [1, 1];
}
function subKernel(): [number, number] {
return [1, 1];
}
const kernelMap = gpu.createKernelMap<typeof kernelFunction>({
test: subKernel,
}, kernelFunction)
.setOutput([1])
.setPipeline(true);
const result = kernelMap();
console.log((result.test as Texture).toArray() as [number, number][]);For extending constants:
import { GPU, IKernelFunctionThis } from 'gpu.js';
const gpu = new GPU();
interface IConstants {
screen: [number, number];
}
type This = {
constants: IConstants
} & IKernelFunctionThis;
function kernelFunction(this: This): number {
const { screen } = this.constants;
return 1 + screen[0];
}
const kernelMap = gpu.createKernel<typeof kernelFunction>(kernelFunction)
.setOutput([3,3,3])
.setConstants<IConstants>({
screen: [1, 1]
});
const result = kernelMap();
console.log(result as number[][][]);Click here for more typescript examples.
Destructured Objects and Arrays work in GPU.js.
const gpu = new GPU();
const kernel = gpu.createKernel(function() {
const { thread: {x, y} } = this;
return x + y;
}, { output: [2] });
console.log(kernel());const gpu = new GPU();
const kernel = gpu.createKernel(function(array) {
const [first, second] = array;
return first + second;
}, {
output: [2],
argumentTypes: { array: 'Array(2)' }
});
console.log(kernel([1, 2]));Transpilation doesn't do the best job of keeping code beautiful. To aid in this endeavor GPU.js can handle some scenarios to still aid you harnessing the GPU in less than ideal circumstances. Here is a list of a few things that GPU.js does to fix transpilation:
New in 2.20.0!
WebGPU is what this library always wanted underneath: real compute shaders over real buffers. Every other GPU backend here works by drawing a full-screen quad and abusing a fragment shader as a compute unit — values packed into texture pixels on the way in, unpacked on the way out. The WebGPU backend compiles your kernel to a WGSL compute shader reading and writing f32 storage buffers directly, and it shows: on an Apple M1 Max, a 1024×1024 matrix multiplication including readback runs about 3× faster than the WebGL2 backend and 370× faster than the CPU.
Because WebGPU has no synchronous readback (correctly — see v3 Will Be Async by Default), kernel calls in this mode return a Promise of the usual result:
const gpu = new GPU({ mode: 'webgpu' });
const kernel = gpu.createKernel(function(a, b) {
let sum = 0;
for (let i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);
const c = await kernel(a, b); // same result shapes as every other backendFeature detection is two-tier, because navigator.gpu can exist on a machine with no usable adapter:
GPU.isWebGPUSupported; // sync: the API surface exists
await GPU.isWebGPUAvailable(); // async: an adapter actually answeredpipeline: true resolves to a GPU-resident buffer handle that passes straight into downstream kernels with no readback, and await handle.toArray() reads it back when you want the values. Large 1D outputs dispatch past the 65,535-workgroup limit automatically.
The mode is explicit opt-in and is never auto-selected — a synchronous caller handed a Promise would fail in silent, confusing ways. If you want automatic selection, that is exactly what mode: 'async' is for. Graphical mode works: the kernel writes this.color(...) into a storage buffer and a fixed render pass presents it to the kernel's canvas — with one API difference, getPixels() returns a Promise (WebGPU readback is asynchronous). Since presentation needs no readback, an un-awaited kernel() per animation frame works. Math.random() works, and differently than on the GL backends: it is a PCG generator in integer WGSL, so with randomSeed the stream is bit-exact across runs and drivers — the GL backends' float-hash generator cannot promise that. Not yet supported (each throws a clear error): kernel maps, toString(), precision: 'unsigned'.
New!
The webasm backend compiles your kernel to a WebAssembly module and runs it on the CPU — but not the way the cpu backend does. Three things separate it from transpiled JavaScript:
Math.random() is the same PCG generator as the webgpu backend, in native i32 arithmetic: with randomSeed the stream is bit-exact across runs, platforms, and thread counts.
Honesty about where it sits: any working GL backend outranks it. In auto-selection (mode: 'gpu', default, or 'async') it is chosen only where no GL context exists — a Node build without headless-gl, a browser with WebGL disabled — one step above the cpu fallback. Opt in explicitly to benchmark it:
const gpu = new GPU({ mode: 'webasm' });
const kernel = gpu.createKernel(function(a, b) {
let sum = 0;
for (let i = 0; i < 512; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
}).setOutput([512, 512]);
const c = kernel(a, b); // synchronous, SIMDA kernel is priced by the work it describes. A scatter algorithm rewritten gather-style so every thread computes its own cell — compaction as a binary search per output slot, a histogram as a per-bin scan — does log-factor or bin-count times the reads of the plain loop it replaces; a GL backend hides that multiplier under thousands of parallel threads, while cpu and webasm execute it serially and pay it in full. Measured against hand-written JavaScript of the same transposed algorithm, the cpu backend is within 2% and webasm within ±1.5× (its SIMD gather is often faster) — the cost is the transposition, not the transpilation. When cpu or webasm is a likely destination, prefer the direct algorithm over the GPU-shaped rewrite.
GPU.isWebAssemblySupported reports the platform answer. pipeline: true is accepted the way the cpu backend accepts it: there is no device memory to pipeline into, so the result is a plain typed array (a fresh copy per call) that passes straight into downstream kernels. Not yet supported: graphical mode, kernel maps, and texture/image arguments all degrade to the cpu backend — in auto modes and under explicit mode: 'webasm' alike — the console warning names the reason and kernel.kernel.fallbackReason carries it queryably; a graphical fallback renders into the kernel's own canvas; toString() throws. Threaded runs accept a poolSize setting to cap the worker pool (defaults to hardwareConcurrency, or 4 when it cannot be read). precision: 'unsigned' is accepted and computed as single precision — wasm has no packed storage to be lossy in.
New!
gpu.createPipeline compiles a whole multi-kernel computation — loops included — into one callable plan:
const sweep = gpu.createKernel(function(u, q) {
const x = this.thread.x, y = this.thread.y;
if (x === 0 || y === 0 || x === this.constants.hi || y === this.constants.hi) return u[y][x];
return 0.25 * (u[y][x - 1] + u[y][x + 1] + u[y - 1][x] + u[y + 1][x] + q[y][x]);
}, { constants: { hi: 1023 }, output: [1024, 1024] });
const solve = gpu.createPipeline(function(u, q) {
for (let s = 0; s < this.constants.sweeps; s++) {
u = sweep(u, q);
}
return u;
}, { constants: { sweeps: 512 } });
const result = await solve(u0, q); // one launch, fences inside, one readbackThe orchestration function runs once, at build time (the first call), with opaque handles standing in for its arguments. The kernel calls it makes are recorded — nothing executes — and plain JS control flow simply unrolls: the loop above records 512 steps over ONE kernel and two alternating buffers (a step that would overwrite data a later step still reads gets double-buffering automatically; liveness is static because the unrolled plan is a DAG). Every later call executes the compiled plan without re-entering your code, intermediates never leave device memory, and you pay one readback at the end. Return a handle, an Array of handles, or a plain object of handles — the call resolves to the same shape holding plain results. Not to be confused with Pipelining: pipeline: true keeps one kernel's output resident and leaves the orchestration to you per call; createPipeline compiles the orchestration itself. Inner kernels do not need pipeline: true — intermediate residency is the pipeline's business, and kernels stay shared between pipelines and direct use.
Because orchestration is tracing, not running, these are the rules — each violation throws at build, naming itself:
Calling a pipeline always returns a Promise — the async contract — and concurrent calls to one pipeline serialize in call order, like threaded kernels. pipeline.destroy() releases the plan's buffers and instances, and gpu.destroy() reaches pipelines the way it reaches kernels.
Every backend runs pipelines. The reference path (executorKind: 'generic') walks the plan through the normal kernel machinery — private per-pipeline kernel instances with pipeline: true forced on, your kernel's settings never observably touched — so on GL it is textures end-to-end. On webasm the plan fuses: every step compiles over one shared WebAssembly.Memory laid out [pipeline args | plan buffers], passes run back-to-back with intermediates never copied out between steps ('fused-sync'), and where wasm threads are available the worker pool executes the whole plan per worker with Atomics-based barriers between steps — one dispatch per pipeline call, no main-thread round trip per pass ('fused-threaded'). Anything the webasm backend cannot take degrades to the generic executor under its usual contract: the reason is queryable at pipeline.fallbackReason, and pipeline.executorKind tells you which executor actually ran.
Introspection is supported API, not plan internals — it exists precisely so a correctness harness can assert the backend it asked for is the backend that ran (the guard that caught seventeen silent CPU degradations in #868):
What the fusion buys, measured on the gauntlet's jacobi and heat benches rewritten via createPipeline (checksums identical to the per-pass versions): 5.7× on heat threaded, 5.2× on jacobi (heat 890 ms vs 5073 ms per-pass, jacobi 387 ms vs 1997 ms — and 2.8×/3.2× over plain JavaScript on rows the webasm backend previously lost), against the same kernels called per pass on webasm. The per-pass costs it deletes are exactly the ones that dominate short passes — a task round-trip through the worker pool per call, argument re-upload, and a readback per step — leaving the arithmetic, which was already SIMD.
On webgpu the same benches run 1.55–1.62× over per-pass chaining (jacobi 28 ms vs 44, heat 37 vs 60) — a smaller multiplier because webgpu's per-pass baseline already pipelines on the GPU queue; the encoder fusion removes the per-call JS, bind, and submit overhead that remains, and long chains feel it most (a 12,289-pass wavefront ran 10× faster migrated). On the GL backends the generic executor runs at parity with a hand-rolled two-kernel ping-pong — the pattern it generates for you — so the ergonomic win is the whole win there: one kernel and a plain loop replace duplicate kernels, upload kernels, and manual texture juggling, with identical results and no leaked per-step textures.
Not in v1, stated plainly:
On webgpu, pipelines compile to the fused-encoder executor: every step is recorded as a compute pass into ONE command encoder over persistent storage buffers (ping-pong steps alternate between two static bind groups), one queue.submit runs the whole plan, and the results come back through a single mapAsync readback. Anything the encoder cannot take statically — GPU-resident handles as pipeline arguments, vector-returning intermediates — degrades to the generic executor with the reason in fallbackReason.
New in 2.20.0!
Async is a property any kernel can have, on any backend. asyncMode: true (or kernel.setAsyncMode(true)) makes every call return a Promise of the usual result:
const kernel = gpu.createKernel(fn, { output: [64], asyncMode: true });
const result = await kernel(myArray);What that buys depends on the backend, but the contract never changes:
mode: 'async' puts the whole GPU instance under this contract and picks the backend for you — the best synchronously-provable one immediately (headlessgl → webgl2 → webgl → webasm → cpu), upgraded to WebGPU on a kernel's first call if an adapter actually answers. On a GL-less platform that means webasm, where the async contract also unlocks its worker-pool threading — see the WebAssembly section for the SharedArrayBuffer caveat. Graphical kernels bind at creation instead: a canvas is permanently committed to its first context type, so the backend is decided before kernel.canvas is ever exposed — await GPU.isWebGPUAvailable() before createKernel to guarantee the probe has settled; a kernel created before it settles stays on the proven backend, and either way the canvas never changes identity. The Promise contract is exactly what buys the room for that probe. A kernel the WebGPU backend cannot take yet (a kernel map, say) simply stays on the proven backend:
const gpu = new GPU({ mode: 'async' });
const kernel = gpu.createKernel(function(a) {
return a[this.thread.x] * 2;
}).setOutput([64]);
const result = await kernel(myArray); // webgpu, webgl2 or cpu underneath — same codeYou can find a complete API reference here.
The reference is generated from the source with npm run docs and hosted from the gpu.rocks repository (public/api/).
GPU.js uses HeadlessGL in node for GPU acceleration. GPU.js is written in such a way, you can introduce your own backend. Have a suggestion? We'd love to hear it!
Because gpu.js ultimately depends on whatever the GPU driver behind a WebGL context does, it is also tested on real browsers and real mobile devices on BrowserStack.
This project is tested with BrowserStack.
To run it yourself you need a BrowserStack Automate account:
npm run make # the devices test dist/, so build it first
export BROWSERSTACK_USERNAME=...
export BROWSERSTACK_ACCESS_KEY=...
npm run test:browserstack # smoke suite on real iOS/Android devicesThe runner serves this checkout over a BrowserStack Local tunnel, so the devices exercise your working copy rather than a published build. Options:
| Command | What it does |
|---|---|
| npm run test:browserstack | Smoke suite on the real-device set |
| npm run test:browserstack:desktop | Smoke suite on desktop Chrome/Firefox/Edge/Safari |
| node test/browserstack/run.js --browsers=all | Both sets |
| node test/browserstack/run.js --only=iPhone | Only targets whose name matches |
| node test/browserstack/run.js --suite=visual | Visual regression: compares rendered output against each device's own CPU render |
| node test/browserstack/run.js --suite=qunit | The full test/all.html suite instead of the smoke suite |
The smoke suite (test/browserstack/smoke.html) covers kernel compilation and execution in cpu, webgl and webgl2 modes: 1D/2D/3D output, loops and branching, Math built-ins, constants and custom functions, typed-array and input() arguments, dynamic output, texture pipelines, graphical output, kernel maps, and both precision modes.
The visual suite (test/browserstack/visual.html) renders a flat fill, a gradient and a Mandelbrot in each GPU mode and compares them against the same device's CPU render, which is bit-identical across hardware. Pixel-exact comparison between devices does not work — GPUs disagree by a least significant bit on ordinary rounding — so it asserts against measured tolerances instead.
Targets live in test/browserstack/browsers.js. Results are written to browserstack-results.json.
Building isn't required on node, but is for browser. To build the browser's files, run: yarn make
Contributors are welcome! Create a merge request to the develop branch and we will gladly review it. If you wish to get write access to the repository, please email us and we will review your application and grant you access to the develop branch.
We promise never to pass off your code as ours.
If you have an issue, either a bug or a feature you think would benefit your project let us know and we will do our best.
Create issues here and follow the template.
This project exists thanks to all the people who contribute. [Contribute].
Thank you to all our backers! 🙏 [Become a backer]
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]
Sponsored NodeJS GPU environment from LeaderGPU - These guys rock!
Sponsored Browser GPU environment's from BrowserStack - Second to none!
| Back | FazBrowse Home | New Git URL |