/blinkjs

Easy GPGPU in the browser, powered by WebGL 2.0.

Primary LanguageHTMLMIT LicenseMIT

blink.js

Easy GPGPU on the web, powered by WebGL 2.0.

Latest NPM release License Dependencies Dev Dependencies

blink.js (not to be confused with Blink, the Chromium render engine) is a small, easy to use GPGPU library for the web, exploiting the power of WebGL 2.0.

Please note: blink.js uses its own WebGL 2.0 context. Which means it's not pluggable with other WebGL frameworks. Though, theoretically, you could use blink.js' context as your main WebGL context.

Table of contents

Installation

Download the blink.min.js file from the dist folder. Then reference it in the HTML using the <script> tag.

<script src="blink.min.js"></script>

Or use the unpkg CDN:

<script src="https://unpkg.com/blink.js/dist/blink.min.js"></script>

Quickstart

blink.js works with two types of objects: Buffers and Kernels. A Buffer is an (large) array of values that can be read from and/or written to. A Kernel contains the shader code that will be executed on the device.

Counting up

In the following example we will initialize a Buffer, allocating space for 1,048,576 integers (4 MB). The Kernel will set all values to their corresponding location in the buffer.

let buffer = new blink.Buffer({
    alloc: 1024 ** 2,
    type: blink.UINT32,
    vector: 1
}) // 4 MB

let kernel = new blink.Kernel({
    output: { buffer }
}, `void main() {
        buffer = bl_Id();
    }`)

kernel.exec()

for (let a = 0; a < 10; a++) {
    console.log(`Value at ${a} is ${buffer.data[a]}.`)
}

Greyscale image

In this example we will use blink.js to convert the image data of a canvas context to black and white.

const ctx = canvas.getContext('2d')
/* Perform any drawing in the context here. */

let imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
let buffer = new blink.Buffer({
    data: imageData.data,
    vector: 4
})

let kernel = new blink.Kernel({
    input: { in_rgba: buffer },
    output: { out_rgba: buffer }
}, `void main() {
        lowp uvec4 color = texture(in_rgba, bl_UV);
        mediump uint grey = (color.r + color.g + color.b) / 3u;
        out_rgba = uvec4(uvec3(grey), color.a);
    }`)

kernel.exec()

ctx.putImageData(imageData, 0, 0)

Usage

Classes

Buffer

let buffer = new blink.Buffer({
    alloc: 1024 ** 2,
    type: blink.UINT8,
    vector: 1,
    wrap: blink.CLAMP
}) 
buffer.data[0] = 1

The Buffer class represents an array of values that can be read from and written to on the device. A Buffer's data is copied to the device the moment it's required. After a Kernel is done executing all its steps, the data on the device is copied back to the host, the data on the device is destroyed immediately.

  • new Buffer({ alloc|data, type, vector, wrap })

    Initialize a new buffer using the given Object containing the following parameters:

    • alloc: Initialize an (0 filled) ArrayBuffer with this size. Note: The given number represents the number of elements of type. Not the size in bytes.

    • data: Opposed to having blink.js initialize the data, you can parse a TypedArray. The Buffer will hold a reference to this TypedArray. Note: If both alloc and data are present in the Object, alloc is chosen.

    • type: The type of primitives of the Buffer. See Types. Default is FLOAT.

    • vector: Number of elements in the vector. Can be 1, 2 or 4. Default is 1.

    • wrap: Texture wrap mode. Can either be an array for S and T or a single constant. See Wrap modes. Default is CLAMP.

  • Buffer.prototype.data

    Reference to the TypedArray.

  • Buffer.prototype.copy()

    Returns a copy of the Buffer. The new instance will also hold a copy of the data allocated on the host.

DeviceBuffer

const size = 512 ** 2 * 4
let d_buffer = new blink.DeviceBuffer({
    alloc: size * 4,
    type: blink.UINT32,
})

d_buffer.toDevice(new Uint32Array(size).fill(1))
const array = d_buffer.toHost()

d_buffer.delete()

Unlike a Buffer, a DeviceBuffer keeps its memory allocated only on the device. This greatly increases performance when memory is not required to be copied back to the host after a Kernel is done executing.

Memory is allocated (or copied) the moment the DeviceBuffer is initialized. Memory is retained on the device until the DeviceBuffer's delete method is called. (Or until the browser garbage collects the DeviceBuffer. But it is strongly advised to manually maintain the memory on the device.)

Data can be downloaded to the host and uploaded to the device using the toHost and toDevice methods respectively.

  • new DeviceBuffer({ alloc|data, type, vector })

    See new Buffer(). Only major difference is that no data is allocated nor referenced on the host.

  • DeviceBuffer.prototype.copy()

    Returns a copy of the DeviceBuffer. The data on the device is also copied.

  • DeviceBuffer.prototype.delete()

    Delete the data on the device, and, essentially, turn the DeviceBuffer's instance unusable.

  • DeviceBuffer.prototype.toDevice(data)

    • data: A TypedArray (of the same type and size the DeviceBuffer was initialized with) whose data will be uploaded to the device.
  • DeviceBuffer.prototype.toHost([data])

    Download the data on the device back to the host.

    • data: (Optional) If given, it should be of the same type and size the DeviceBuffer was initialized with. If not given, blink.js will initialize and return the correct TypedArray.
  • DeviceBuffer.prototype.toHostAsync([data])

    Download the data on the device back to the host, asynchronously. Unlike toHost(), this method returns a Promise.

    • data: (Optional) See DeviceBuffer.prototype.toHost. If not given, blink.js will initialize the correct TypedArray, and pass it through the returned Promise's thenable.

NOTE: Only available if the WEBGL_get_buffer_sub_data_async extension is supported.

Kernel

let buffer = new blink.Buffer(/* ... */)

let input = { in_buffer: buffer }
let output = { out_buffer: buffer }
let kernel = new blink.Kernel({ input, output }, `
    uniform float multiplier;

    void main() {
        float val = texture(in_buffer, bl_UV).r;
        out_buffer = val * multiplier;
    }`)

kernel.exec({ multiplier: 2 })
kernel.delete()
  • new Kernel({ input, output }, shaderSource)

    Initialize a new Kernel with the given inputs, outputs and (fragment) shader source.

    • input: (Optional) A key-value Object, where keys are the names of the inputs and the values a reference to either a Buffer or DeviceBuffer. The input names become available in the shader to read from their respective buffer.

    • output: Same as input, except for writing to buffers.

    • shaderSource: Source of the shader as a String.

  • Kernel.prototype.exec([uniforms])

    Execute the Kernel.

    • uniforms: (Optional) If given, it should be a key-value Object with the keys being the uniforms' names and values their value.
  • Kernel.prototype.delete()

    Delete all associated shaders and programs on the device. Essentially rendering the Kernel's instance as unusable.

Types

  • blink.FLOAT (Float32Array)
  • blink.UINT8 (Uint8Array) (Uint8ClampedArray will be casted to Uint8Array)
  • blink.UINT16 (Uint16Array)
  • blink.UINT32 (Uint32Array)
  • blink.INT8 (Int8Array)
  • blink.INT16 (Int16Array)
  • blink.INT32 (Int32Array)

Wrap modes

  • blink.CLAMP
  • blink.REPEAT
  • blink.MIRROR

Device

blink.device contains information gathered from the WebGL context.

  • glslVersion: version of GLSL.
  • maxColorAttachments: Maximum number of outputs a Kernel can render to in a single step.
  • maxTextureSize: Maximum dimension of an input/output buffer.
  • maxTextureUnits: Maximum number of input buffers.
  • renderer: Renderer name.
  • vendor: Vendor name.
  • unmaskedRenderer: Unmasked renderer name.[1]
  • unmaskedVendor: Unmasked vendor name.[1]

[1] Only available if the WEBGL_debug_renderer_info extension is supported.

GLSL

WebGL 2.0 supports GLSL ES 3.00, which includes (but not limited to) the following significant new features compared to GLSL ES 1.30 in WebGL 1.0:

  • Unsigned integer types.
  • Bitwise operators.
  • for and while loops with variable lengths.
  • Non-square matrix types.
  • A butt-load of matrix functions.

Built-in variables

highp vec2 bl_UV;    // Normalized coordinate of the current fragment.
highp ivec2 bl_Size; // Dimensions of the output buffer(s).
highp uint bl_Id();  // Returns the id of the current fragment.

Type compatibility

Different combinations of operating systems, browsers and hardware may not support the entire list of type and vector size combinations. See COMPATIBILITY.md for a personally tested list of compatibility. Or open test/report.html in your browser to test yourself.

Built for today

Both Firefox 59 and Chrome 65 (on desktop) support WebGL 2.0 now. As of writing this readme, the status of WebGL 2.0 for Safari is declared as Supported in preview (Safari Technology Preview). However, shaders are unable to successfully compile, thus rendering WebGL 2.0 support as pretty much unusable. There is no concrete answer whether Safari will fully support WebGL 2.0 in the future.

blink.js uses ES6 syntax/features that are not transpiled or polyfilled for ES5. Babel is only used to minify the final code, using the babili preset.

See also

headless-gl - create WebGL contexts in Node.js, powered by ANGLE.

turbo.js - a similar GPGPU library for the browser, using WebGL 1.0.

WebGP.js - another similar GPGPU library for the browser.

gpu.js - transpile and run Javascript code on the GPU.

NOOOCL - OpenCL bindings for Node.js.

The Book of Shaders - Great introduction to GLSL by Patricio Gonzalez Vivo.

WebGL 2.0 Reference card - PDF containing all GLSL ES 3.00 functions and variables.