](http://gpu.rocks/)
# GPU.js
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](https://github.com/gpujs/gpu.js/wiki/Quick-Concepts) on the wiki.
[](https://gitter.im/gpujs/gpu.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://slack.bri.im)
# What is this sorcery?
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](http://gpu.rocks). 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:
## Browser
```html
```
## CDN
```
https://unpkg.com/gpu.js@latest/dist/gpu-browser.min.js
https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js
```
## Node
```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);
```
## Typescript
```typescript
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](/examples) for more typescript examples.
# Table of Contents
Notice documentation is off? We do try our hardest, but if you find something,
[please bring it to our attention](https://github.com/gpujs/gpu.js/issues), or _[become a contributor](#contributors)_!
* [Demos](#demos)
* [Installation](#installation)
* [`GPU` Settings](#gpu-settings)
* [`gpu.createKernel` Settings](#gpucreatekernel-settings)
* [Declaring variables/functions within kernels](#declaring-variablesfunctions-within-kernels)
* [Creating and Running Functions](#creating-and-running-functions)
* [Debugging](#debugging)
* [Accepting Input](#accepting-input)
* [Graphical Output](#graphical-output)
* [Combining Kernels](#combining-kernels)
* [Create Kernel Map](#create-kernel-map)
* [Adding Custom Functions](#adding-custom-functions)
* [Adding Custom Functions Directly to Kernel](#adding-custom-functions-directly-to-kernel)
* [Types](#types)
* [Loops](#loops)
* [Pipelining](#pipelining)
* [Cloning Textures](#cloning-textures-new-in-v2)
* [Cleanup pipeline texture memory](#cleanup-pipeline-texture-memory-new-in-v24)
* [Offscreen Canvas](#offscreen-canvas)
* [Cleanup](#cleanup)
* [Flattened typed array support](#flattened-typed-array-support)
* [Precompiled and Lighter Weight Kernels](#precompiled-and-lighter-weight-kernels)
* [using JSON](#using-json)
* [Exporting kernel](#exporting-kernel)
* [Supported Math functions](#supported-math-functions)
* [How to check what is supported](#how-to-check-what-is-supported)
* [Typescript Typings](#typescript-typings)
* [Destructured Assignments](#destructured-assignments-new-in-v2)
* [Dealing With Transpilation](#dealing-with-transpilation)
* [Full API reference](#full-api-reference)
* [How possible in node](#how-possible-in-node)
* [Testing](#testing)
* [Building](#building)
* [Contributors](#contributors)
* [Contributing](#contributing)
* [Terms Explained](#terms-explained)
* [License](#license)
## Demos
GPU.js in the wild, all around the net. Add yours here!
* [Temperature interpolation using GPU.js](https://observablehq.com/@rveciana/temperature-interpolation-using-gpu-js)
* [Julia Set Fractal using GPU.js](https://observablehq.com/@ukabuer/julia-set-fractal-using-gpu-js)
* [Hello, gpu.js v2](https://observablehq.com/@fil/hello-gpu-js-v2)
* [Basic gpu.js canvas example](https://observablehq.com/@rveciana/basic-gpu-js-canvas-example)
* [Raster projection with GPU.js](https://observablehq.com/@fil/raster-projection-with-gpu-js)
* [GPU.js Example: Slow Fade](https://observablehq.com/@robertleeplummerjr/gpu-js-example-slow-fade)
* [GPU.JS CA Proof of Concept](https://observablehq.com/@alexlamb/gpu-js-ca-proof-of-concept)
* [Image Convolution using GPU.js](https://observablehq.com/@ukabuer/image-convolution-using-gpu-js)
* [Leaflet + gpu.js canvas](https://observablehq.com/@rveciana/leaflet-gpu-js-canvas)
* [Image to GPU.js](https://observablehq.com/@fil/image-to-gpu)
* [GPU Accelerated Heatmap using gpu.js](https://observablehq.com/@tracyhenry/gpu-accelerated-heatmap-using-gpu-js)
* [Dijkstra’s algorithm in gpu.js](https://observablehq.com/@fil/dijkstras-algorithm-in-gpu-js)
* [Voronoi with gpu.js](https://observablehq.com/@fil/voronoi-with-gpu-js)
* [The gpu.js loop](https://observablehq.com/@fil/the-gpu-js-loop)
* [GPU.js Example: Mandelbrot Set](https://observablehq.com/@robertleeplummerjr/gpu-js-example-mandelbrot-set)
* [GPU.js Example: Mandelbulb](https://observablehq.com/@robertleeplummerjr/gpu-js-example-mandelbulb)
* [Inverse of the distance with gpu.js](https://observablehq.com/@rveciana/inverse-of-the-distance-with-gpu-js)
* [gpu.js laser detection v2](https://observablehq.com/@robertleeplummerjr/gpu-js-laser-detection-v2)
* [GPU.js Canvas](https://observablehq.com/@hubgit/gpu-js-canvas)
* [Video Convolution using GPU.js](https://observablehq.com/@robertleeplummerjr/video-convolution-using-gpu-js)
* [GPU Rock Paper Scissors](https://observablehq.com/@alexlamb/gpu-rock-paper-scissors)
* [Shaded relief with gpujs and d3js](https://observablehq.com/@rveciana/shaded-relief-with-gpujs-and-d3js/2)
* [Caesar Cipher GPU.js Example](https://observablehq.com/@robertleeplummerjr/caesar-cipher-gpu-js-example)
* [Matrix Multiplication GPU.js + Angular Example](https://ng-gpu.surge.sh/)
* [Conway's game of life](https://observablehq.com/@brakdag/conway-game-of-life-gpu-js)
## Installation
On Linux, ensure you have the correct header files installed: `sudo apt install mesa-common-dev libxi-dev` (adjust for your distribution)
### npm
```bash
npm install gpu.js --save
```
### yarn
```bash
yarn add gpu.js
```
[npm package](https://www.npmjs.com/package/gpu.js)
### Node
```js
const { GPU } = require('gpu.js');
const gpu = new GPU();
```
### Node Typescript **New in V2!**
```js
import { GPU } from 'gpu.js';
const gpu = new GPU();
```
### Browser
Download the latest version of GPU.js and include the files in your HTML page using the following tags:
```html
```
## `GPU` Settings
Settings are an object used to create an instance of `GPU`. Example: `new GPU(settings)`
* `canvas`: `HTMLCanvasElement`. Optional. For sharing canvas. Example: use THREE.js and GPU.js on same canvas.
* `context`: `WebGL2RenderingContext` or `WebGLRenderingContext`. For sharing rendering context. Example: use THREE.js and GPU.js on same rendering context.
* `mode`: Defaults to 'gpu', other values generally for debugging:
* 'dev' **New in V2!**: VERY IMPORTANT! Use this so you can breakpoint and debug your kernel! This wraps your javascript in loops but DOES NOT transpile your code, so debugging is much easier.
* 'webgl': Use the `WebGLKernel` for transpiling a kernel
* 'webgl2': Use the `WebGL2Kernel` for transpiling a kernel
* 'headlessgl' **New in V2!**: Use the `HeadlessGLKernel` for transpiling a kernel
* 'cpu': Use the `CPUKernel` for transpiling a kernel
* `onIstanbulCoverageVariable`: Removed in v2.11.0, use v8 coverage
* `removeIstanbulCoverage`: Removed in v2.11.0, use v8 coverage
## `gpu.createKernel` Settings
Settings are an object used to create a `kernel` or `kernelMap`. Example: `gpu.createKernel(settings)`
* `output` or `kernel.setOutput(output)`: `array` or `object` that describes the output of kernel. When using `kernel.setOutput()` you _can_ call it after the kernel has compiled if `kernel.dynamicOutput` is `true`, to resize your output. Example:
* as array: `[width]`, `[width, height]`, or `[width, height, depth]`
* as object: `{ x: width, y: height, z: depth }`
* `pipeline` or `kernel.setPipeline(true)` **New in V2!**: boolean, default = `false`
* Causes `kernel()` calls to output a `Texture`. To get array's from a `Texture`, use:
```js
const result = kernel();
result.toArray();
```
* Can be passed _directly_ into kernels, and is preferred:
```js
kernel(texture);
```
* `graphical` or `kernel.setGraphical(boolean)`: boolean, default = `false`
* `loopMaxIterations` or `kernel.setLoopMaxIterations(number)`: number, default = 1000
* `constants` or `kernel.setConstants(object)`: object, default = null
* `dynamicOutput` or `kernel.setDynamicOutput(boolean)`: boolean, default = false - turns dynamic output on or off
* `dynamicArguments` or `kernel.setDynamicArguments(boolean)`: boolean, default = false - turns dynamic arguments (use different size arrays and textures) on or off
* `optimizeFloatMemory` or `kernel.setOptimizeFloatMemory(boolean)` **New in V2!**: boolean - causes a float32 texture to use all 4 channels rather than 1, using less memory, but consuming more GPU.
* `precision` or `kernel.setPrecision('unsigned' | 'single')` **New in V2!**: 'single' or 'unsigned' - if 'single' output texture uses float32 for each colour channel rather than 8
* `fixIntegerDivisionAccuracy` or `kernel.setFixIntegerDivisionAccuracy(boolean)` : boolean - some cards have accuracy issues dividing by factors of three and some other primes (most apple kit?). Default on for affected cards, disable if accuracy not required.
* `functions` or `kernel.setFunctions(array)`: array, array of functions to be used inside kernel. If undefined, inherits from `GPU` instance. Can also be an array of `{ source: function, argumentTypes: object, returnType: string }`.
* `nativeFunctions` or `kernel.setNativeFunctions(array)`: object, defined as: `{ name: string, source: string, settings: object }`. This is generally set via using GPU.addNativeFunction()
* VERY IMPORTANT! - Use this to add special native functions to your environment when you need specific functionality is needed.
* `injectedNative` or `kernel.setInjectedNative(string)` **New in V2!**: string, defined as: `{ functionName: functionSource }`. This is for injecting native code before translated kernel functions.
* `subKernels` or `kernel.setSubKernels(array)`: array, generally inherited from `GPU` instance.
* `immutable` or `kernel.setImmutable(boolean)`: boolean, default = `false`
* VERY IMPORTANT! - This was removed in v2.4.0 - v2.7.0, and brought back in v2.8.0 [by popular demand](https://github.com/gpujs/gpu.js/issues/572), please upgrade to get the feature
* `strictIntegers` or `kernel.setStrictIntegers(boolean)`: boolean, default = `false` - allows undefined argumentTypes and function return values to use strict integer declarations.
* `useLegacyEncoder` or `kernel.setUseLegacyEncoder(boolean)`: boolean, default `false` - more info [here](https://github.com/gpujs/gpu.js/wiki/Encoder-details).
* `tactic` or `kernel.setTactic('speed' | 'balanced' | 'precision')` **New in V2!**: Set the kernel's tactic for compilation. Allows for compilation to better fit how GPU.js is being used (internally uses `lowp` for 'speed', `mediump` for 'balanced', and `highp` for 'precision'). Default is lowest resolution supported for output.
## Creating and Running Functions
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]`
```js
const settings = {
output: [100]
};
```
or
```js
// 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.
```js
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.
```js
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.
```js
const kernel = gpu.createKernel(function() {
return this.thread.x;
}).setOutput([100]);
kernel();
// Result: Float32Array[0, 1, 2, 3, ... 99]
```
### Declaring variables/functions within kernels
GPU.js makes variable declaration inside kernel functions easy. Variable types supported are:
* `Number` (Integer or Number), example: `let value = 1` or `let value = 1.1`
* `Boolean`, example: `let value = true`
* `Array(2)`, example: `let value = [1, 1]`
* `Array(3)`, example: `let value = [1, 1, 1]`
* `Array(4)`, example: `let value = [1, 1, 1, 1]`
* `private Function`, example: `function myFunction(value) { return value + 1; }`
`Number` kernel example:
```js
const kernel = gpu.createKernel(function() {
const i = 1;
const j = 0.89;
return i + j;
}).setOutput([100]);
```
`Boolean` kernel example:
```js
const kernel = gpu.createKernel(function() {
const i = true;
if (i) return 1;
return 0;
}).setOutput([100]);
```
`Array(2)` kernel examples:
Using declaration
```js
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2];
return array2;
}).setOutput([100]);
```
Directly returned
```js
const kernel = gpu.createKernel(function() {
return [0.08, 2];
}).setOutput([100]);
```
`Array(3)` kernel example:
Using declaration
```js
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2, 0.1];
return array2;
}).setOutput([100]);
```
Directly returned
```js
const kernel = gpu.createKernel(function() {
return [0.08, 2, 0.1];
}).setOutput([100]);
```
`Array(4)` kernel example:
Using declaration
```js
const kernel = gpu.createKernel(function() {
const array2 = [0.08, 2, 0.1, 3];
return array2;
}).setOutput([100]);
```
Directly returned
```js
const kernel = gpu.createKernel(function() {
return [0.08, 2, 0.1, 3];
}).setOutput([100]);
```
`private Function` kernel example:
```js
const kernel = gpu.createKernel(function() {
function myPrivateFunction() {
return [0.08, 2, 0.1, 3];
}
return myPrivateFunction(); // <-- type inherited here
}).setOutput([100]);
```
## Debugging
Debugging can be done in a variety of ways, and there are different levels of debugging.
* Debugging kernels with breakpoints can be done with `new GPU({ mode: 'dev' })`
* This puts `GPU.js` into development mode. Here you can insert breakpoints, and be somewhat liberal in how your kernel is developed.
* This mode _does not_ actually "compile" (parse, and eval) a kernel, it simply iterates on your code.
* You can break a lot of rules here, because your kernel's function still has context of the state it came from.
* PLEASE NOTE: Mapped kernels are not supported in this mode. They simply cannot work because of context.
* Example:
```js
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] });
```
* Debugging actual kernels on CPU with `debugger`:
* This will cause "breakpoint" like behaviour, but in an actual CPU kernel. You'll peer into the compiled kernel here, for a CPU.
* Example:
```js
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] });
```
* Debugging an actual GPU kernel:
* There are no breakpoints available on the GPU, period. By providing the same level of abstraction and logic, the above methods should give you enough insight to debug, but sometimes we just need to see what is on the GPU.
* Be VERY specific and deliberate, and use the kernel to your advantage, rather than just getting frustrated or giving up.
* Example:
```js
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] });
```
In this example, we return early the value of x, to see exactly what it is. The rest of the logic is ignored, but now you can see the value that is calculated from `x`, and debug it.
This is an overly simplified problem.
* Sometimes you need to solve graphical problems, that can be done similarly.
* Example:
```js
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 });
```
Here we are making the canvas red or green depending on the value of `x`.
## Accepting Input
### Supported Input Types
* Numbers
* 1d,2d, or 3d Array of numbers
* Arrays of `Array`, `Float32Array`, `Int16Array`, `Int8Array`, `Uint16Array`, `uInt8Array`
* Pre-flattened 2d or 3d Arrays using 'Input', for faster upload of arrays
* Example:
```js
const { input } = require('gpu.js');
const value = input(flattenedArray, [width, height, depth]);
```
* HTML Image
* Array of HTML Images
* Video Element **New in V2!**
To define an argument, simply add it to the kernel function like regular JavaScript.
### Input Examples
```js
const kernel = gpu.createKernel(function(x) {
return x;
}).setOutput([100]);
kernel(42);
// Result: Float32Array[42, 42, 42, 42, ... 42]
```
Similarly, with array inputs:
```js
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:
```js
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:
```js
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!**
```js
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 frame
```
## Graphical Output
Sometimes, 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.
```js
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](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
### .getPixels() **New in V2!**
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:
```js
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...
```
### Alpha
Currently, if you need alpha do something like enabling `premultipliedAlpha` with your own gl context:
```js
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);
```
## Combining kernels
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.
```js
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.
## Create Kernel Map
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.
### object outputs
```js
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 }
```
### array outputs
```js
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.
## Adding custom functions
### To `GPU` instance
use `gpu.addFunction(function() {}, settings)` for adding custom functions to all kernels. Needs to be called BEFORE `gpu.createKernel`. Example:
```js
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]);
```
### To `Kernel` instance
use `kernel.addFunction(function() {}, settings)` for adding custom functions to all kernels. Example:
```js
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]);
```
### Adding strongly typed functions
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:
* `returnType`: optional, defaults to inference from `FunctionBuilder`, the value you'd like to return from the function.
* `argumentTypes`: optional, defaults to inference from `FunctionBuilder` for each param, a hash of param names with values of the return types.
Example on `GPU` instance:
```js
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:
```js
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.
## Adding custom functions directly to kernel
```js
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]);
```
## Types
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:
### Argument Types
* 'Array'
* 'Array(2)' **New in V2!**
* 'Array(3)' **New in V2!**
* 'Array(4)' **New in V2!**
* 'Array1D(2)' **New in V2!**
* 'Array1D(3)' **New in V2!**
* 'Array1D(4)' **New in V2!**
* 'Array2D(2)' **New in V2!**
* 'Array2D(3)' **New in V2!**
* 'Array2D(4)' **New in V2!**
* 'Array3D(2)' **New in V2!**
* 'Array3D(3)' **New in V2!**
* 'Array3D(4)' **New in V2!**
* 'HTMLCanvas' **New in V2.6**
* 'OffscreenCanvas' **New in V2.13**
* 'HTMLImage'
* 'ImageBitmap' **New in V2.14**
* 'ImageData' **New in V2.15**
* 'HTMLImageArray'
* 'HTMLVideo' **New in V2!**
* 'Number'
* 'Float'
* 'Integer'
* 'Boolean' **New in V2!**
### Return Types
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`
* 'Array(2)'
* 'Array(3)'
* 'Array(4)'
* 'Number'
* 'Float'
* 'Integer'
### Internal Types
Types generally used in the `Texture` class, for #pipelining or for advanced usage.
* 'ArrayTexture(1)' **New in V2!**
* 'ArrayTexture(2)' **New in V2!**
* 'ArrayTexture(3)' **New in V2!**
* 'ArrayTexture(4)' **New in V2!**
* 'NumberTexture'
* 'MemoryOptimizedNumberTexture' **New in V2!**
## Loops
* Any loops defined inside the kernel must have a maximum iteration count defined by the loopMaxIterations setting.
* Other than defining the iterations by a constant or fixed value as shown [Dynamic sized via constants](dynamic-sized-via-constants), you can also simply pass the number of iterations as a variable to the kernel
### Dynamic sized via constants
```js
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],
});
```
### Fixed sized
```js
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]);
```
## Pipelining
[Pipeline](https://en.wikipedia.org/wiki/Pipeline_(computing)) 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:
* In gpu mode using `texture.delete()` when appropriate.
* In cpu mode allowing values to go out of context
### Cloning Textures **New in V2!**
When using pipeline mode the outputs from kernels can be cloned using `texture.clone()`.
```js
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]
```
### Cleanup pipeline texture memory **New in V2.4!**
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.
## Offscreen Canvas
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`
```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`
```js
var worker = new Worker('gpu-worker.js');
worker.onmessage = function(e) {
var result = e.data;
console.log(result);
};
```
## Cleanup
* for instances of `GPU` use the `destroy` method. Example: `gpu.destroy()`
* for instances of `Kernel` use the `destroy` method. Example: `kernel.destroy()`
* for instances of `Texture` use the `delete` method. Example: `texture.delete()`
* for instances of `Texture` that you might want to reuse/reset to zeros, use the `clear` method. Example: `texture.clear()`
## Flattened typed array support
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:
```js
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)`
## Precompiled and Lighter Weight Kernels
### using JSON
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:
```js
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:
* [dist/gpu-browser-core.js](dist/gpu-browser-core.js)
* [dist/gpu-browser-core.min.js](dist/gpu-browser-core.min.js)
### Exporting kernel
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*:
* `Array(2)`
* `Array(3)`
* `Array(4)`
* `Integer`
* `Number`
* `Float`
* `Boolean`
Here is an example used to/from file:
```js
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:
```js
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]));
```
#### using constants with `kernel.toString(...args)`
You can assign _some_ new constants when using the function output from `.toString()`,
## Supported Math functions
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:
* `Math.abs()`
* `Math.acos()`
* `Math.acosh()`
* `Math.asin()`
* `Math.asinh()`
* `Math.atan()`
* `Math.atanh()`
* `Math.atan2()`
* `Math.cbrt()`
* `Math.ceil()`
* `Math.cos()`
* `Math.cosh()`
* `Math.exp()`
* `Math.expm1()`
* `Math.floor()`
* `Math.fround()`
* `Math.imul()`
* `Math.log()`
* `Math.log10()`
* `Math.log1p()`
* `Math.log2()`
* `Math.max()`
* `Math.min()`
* `Math.pow()`
* `Math.random()`
* A note on random. We use [a plugin](src/plugins/math-random-uniformly-distributed.js) to generate random.
Random seeded _and_ generated, _both from the GPU_, is not as good as random from the CPU as there are more things that the CPU can seed random from.
However, we seed random on the GPU, _from a random value in the CPU_.
We then seed the subsequent randoms from the previous random value.
So we seed from CPU, and generate from GPU.
Which is still not as good as CPU, but closer.
While this isn't perfect, it should suffice in most scenarios.
In any case, we must give thanks to [RandomPower](https://www.randompower.eu/), and this [issue](https://github.com/gpujs/gpu.js/issues/498), for assisting in improving our implementation of random.
* `Math.round()`
* `Math.sign()`
* `Math.sin()`
* `Math.sinh()`
* `Math.sqrt()`
* `Math.tan()`
* `Math.tanh()`
* `Math.trunc()`
This is a list and reasons of unsupported ones:
* `Math.clz32` - bits directly are hard
* `Math.hypot` - dynamically sized
## How to check what is supported
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:
* `GPU.disableValidation()` - turn off all kernel validation
* `GPU.enableValidation()` - turn on all kernel validation
* `GPU.isGPUSupported`: `boolean` - checks if GPU is in-fact supported
* `GPU.isKernelMapSupported`: `boolean` - checks if kernel maps are supported
* `GPU.isOffscreenCanvasSupported`: `boolean` - checks if offscreen canvas is supported
* `GPU.isWebGLSupported`: `boolean` - checks if WebGL v1 is supported
* `GPU.isWebGL2Supported`: `boolean` - checks if WebGL v2 is supported
* `GPU.isHeadlessGLSupported`: `boolean` - checks if headlessgl is supported
* `GPU.isCanvasSupported`: `boolean` - checks if canvas is supported
* `GPU.isGPUHTMLImageArraySupported`: `boolean` - checks if the platform supports HTMLImageArray's
* `GPU.isSinglePrecisionSupported`: `boolean` - checks if the system supports single precision float 32 values
## Typescript Typings
Typescript is supported! Typings can be found [here](src/index.d.ts)!
For strongly typed kernels:
```typescript
import { GPU, IKernelFunctionThis } from 'gpu.js';
const gpu = new GPU();
function kernelFunction(this: IKernelFunctionThis): number {
return 1 + this.thread.x;
}
const kernelMap = gpu.createKernelA parallel raytracer built with TypeScript and GPU.js (WebGL/GLSL).
GitHub: http://github.com/jin/raytracer
Press W, A, S, D to move the camera around.
The canvas is 640px by 640px. Each canvas object is controlled by a single GPU.js kernel and a single thread is spawned for each pixel to compute the color of the pixel.
Increase the dimensions of the grid to break the canvas up into tiles, so that there are multiple kernels controlling multiple tiles. With this approach, the kernels will run sequentially, computing one canvas after another.
From https://staceytay.com/raytracer/. A simple ray tracer with Lambertian and specular reflection, built with GPU.js. Read more about ray tracing and GPU.js in my blog post. Code available on GitHub.
This handles all the raw state, converted state, etc. Of a single function.
*/ class CPUFunctionNode extends FunctionNode { /** * @desc Parses the abstract syntax tree for to its *named function* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astFunction(ast, retArr) { // Setup function return type and name if (!this.isRootKernel) { retArr.push('function'); retArr.push(' '); retArr.push(this.name); retArr.push('('); // Arguments handling for (let i = 0; i < this.argumentNames.length; ++i) { const argumentName = this.argumentNames[i]; if (i > 0) { retArr.push(', '); } retArr.push('user_'); retArr.push(argumentName); } // Function opening retArr.push(') {\n'); } // Body statement iteration for (let i = 0; i < ast.body.body.length; ++i) { this.astGeneric(ast.body.body[i], retArr); retArr.push('\n'); } if (!this.isRootKernel) { // Function closing retArr.push('}\n'); } return retArr; } /** * @desc Parses the abstract syntax tree for to *return* statement * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astReturnStatement(ast, retArr) { const type = this.returnType || this.getType(ast.argument); if (!this.returnType) { this.returnType = type; } if (this.isRootKernel) { retArr.push(this.leadingReturnStatement); this.astGeneric(ast.argument, retArr); retArr.push(';\n'); retArr.push(this.followingReturnStatement); retArr.push('continue;\n'); } else if (this.isSubKernel) { retArr.push(`subKernelResult_${ this.name } = `); this.astGeneric(ast.argument, retArr); retArr.push(';'); retArr.push(`return subKernelResult_${ this.name };`); } else { retArr.push('return '); this.astGeneric(ast.argument, retArr); retArr.push(';'); } return retArr; } /** * @desc Parses the abstract syntax tree for *literal value* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astLiteral(ast, retArr) { // Reject non numeric literals if (isNaN(ast.value)) { throw this.astErrorOutput( 'Non-numeric literal not supported : ' + ast.value, ast ); } retArr.push(ast.value); return retArr; } /** * @desc Parses the abstract syntax tree for *binary* expression * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBinaryExpression(ast, retArr) { retArr.push('('); this.astGeneric(ast.left, retArr); retArr.push(ast.operator); this.astGeneric(ast.right, retArr); retArr.push(')'); return retArr; } /** * @desc Parses the abstract syntax tree for *identifier* expression * @param {Object} idtNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astIdentifierExpression(idtNode, retArr) { if (idtNode.type !== 'Identifier') { throw this.astErrorOutput( 'IdentifierExpression - not an Identifier', idtNode ); } switch (idtNode.name) { case 'Infinity': retArr.push('Infinity'); break; default: if (this.constants && this.constants.hasOwnProperty(idtNode.name)) { retArr.push('constants_' + idtNode.name); } else { retArr.push('user_' + idtNode.name); } } return retArr; } /** * @desc Parses the abstract syntax tree for *for-loop* expression * @param {Object} forNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the parsed webgl string */ astForStatement(forNode, retArr) { if (forNode.type !== 'ForStatement') { throw this.astErrorOutput('Invalid for statement', forNode); } const initArr = []; const testArr = []; const updateArr = []; const bodyArr = []; let isSafe = null; if (forNode.init) { this.pushState('in-for-loop-init'); this.astGeneric(forNode.init, initArr); for (let i = 0; i < initArr.length; i++) { if (initArr[i].includes && initArr[i].includes(',')) { isSafe = false; } } this.popState('in-for-loop-init'); } else { isSafe = false; } if (forNode.test) { this.astGeneric(forNode.test, testArr); } else { isSafe = false; } if (forNode.update) { this.astGeneric(forNode.update, updateArr); } else { isSafe = false; } if (forNode.body) { this.pushState('loop-body'); this.astGeneric(forNode.body, bodyArr); this.popState('loop-body'); } // have all parts, now make them safe if (isSafe === null) { isSafe = this.isSafe(forNode.init) && this.isSafe(forNode.test); } if (isSafe) { retArr.push(`for (${initArr.join('')};${testArr.join('')};${updateArr.join('')}){\n`); retArr.push(bodyArr.join('')); retArr.push('}\n'); } else { const iVariableName = this.getInternalVariableName('safeI'); if (initArr.length > 0) { retArr.push(initArr.join(''), ';\n'); } retArr.push(`for (let ${iVariableName}=0;${iVariableName}Instantiates properties to the CPU Kernel.
*/ class CPUKernel extends Kernel { static getFeatures() { return this.features; } static get features() { return Object.freeze({ kernelMap: true, isIntegerDivisionAccurate: true }); } static get isSupported() { return true; } static isContextMatch(context) { return false; } /** * @desc The current mode in which gpu.js is executing. */ static get mode() { return 'cpu'; } static nativeFunctionArguments() { return null; } static nativeFunctionReturnType() { throw new Error(`Looking up native function return type not supported on ${this.name}`); } static combineKernels(combinedKernel) { return combinedKernel; } static getSignature(kernel, argumentTypes) { return 'cpu' + (argumentTypes.length > 0 ? ':' + argumentTypes.join(',') : ''); } constructor(source, settings) { super(source, settings); this.mergeSettings(source.settings || settings); this._imageData = null; this._colorData = null; this._kernelString = null; this._prependedString = []; this.thread = { x: 0, y: 0, z: 0 }; this.translatedSources = null; } initCanvas() { if (typeof document !== 'undefined') { return document.createElement('canvas'); } else if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(0, 0); } } initContext() { if (!this.canvas) return null; return this.canvas.getContext('2d'); } initPlugins(settings) { return []; } /** * @desc Validate settings related to Kernel, such as dimensions size, and auto output support. * @param {IArguments} args */ validateSettings(args) { if (!this.output || this.output.length === 0) { if (args.length !== 1) { throw new Error('Auto output only supported for kernels with only one input'); } const argType = utils.getVariableType(args[0], this.strictIntegers); if (argType === 'Array') { this.output = utils.getDimensions(argType); } else if (argType === 'NumberTexture' || argType === 'ArrayTexture(4)') { this.output = args[0].output; } else { throw new Error('Auto output not supported for input type: ' + argType); } } if (this.graphical) { if (this.output.length !== 2) { throw new Error('Output must have 2 dimensions on graphical mode'); } } this.checkOutput(); } translateSource() { this.leadingReturnStatement = this.output.length > 1 ? 'resultX[x] = ' : 'result[x] = '; if (this.subKernels) { const followingReturnStatement = []; for (let i = 0; i < this.subKernels.length; i++) { const { name } = this.subKernels[i]; followingReturnStatement.push(this.output.length > 1 ? `resultX_${ name }[x] = subKernelResult_${ name };\n` : `result_${ name }[x] = subKernelResult_${ name };\n`); } this.followingReturnStatement = followingReturnStatement.join(''); } const functionBuilder = FunctionBuilder.fromKernel(this, CPUFunctionNode); this.translatedSources = functionBuilder.getPrototypes('kernel'); if (!this.graphical && !this.returnType) { this.returnType = functionBuilder.getKernelResultType(); } } /** * @desc Builds the Kernel, by generating the kernel * string using thread dimensions, and arguments * supplied to the kernel. * *If the graphical flag is enabled, canvas is used.
*/ build() { if (this.built) return; this.setupConstants(); this.setupArguments(arguments); this.validateSettings(arguments); this.translateSource(); if (this.graphical) { const { canvas, output } = this; if (!canvas) { throw new Error('no canvas available for using graphical output'); } const width = output[0]; const height = output[1] || 1; canvas.width = width; canvas.height = height; this._imageData = this.context.createImageData(width, height); this._colorData = new Uint8ClampedArray(width * height * 4); } const kernelString = this.getKernelString(); this.kernelString = kernelString; if (this.debug) { console.log('Function output:'); console.log(kernelString); } try { this.run = new Function([], kernelString).bind(this)(); } catch (e) { console.error('An error occurred compiling the javascript: ', e); } this.buildSignature(arguments); this.built = true; } color(r, g, b, a) { if (typeof a === 'undefined') { a = 1; } r = Math.floor(r * 255); g = Math.floor(g * 255); b = Math.floor(b * 255); a = Math.floor(a * 255); const width = this.output[0]; const height = this.output[1]; const x = this.thread.x; const y = height - this.thread.y - 1; const index = x + y * width; this._colorData[index * 4 + 0] = r; this._colorData[index * 4 + 1] = g; this._colorData[index * 4 + 2] = b; this._colorData[index * 4 + 3] = a; } /** * @desc Generates kernel string for this kernel program. * *If sub-kernels are supplied, they are also factored in. * This string can be saved by calling the `toString` method * and then can be reused later.
* * @returns {String} result * */ getKernelString() { if (this._kernelString !== null) return this._kernelString; let kernelThreadString = null; let { translatedSources } = this; if (translatedSources.length > 1) { translatedSources = translatedSources.filter(fn => { if (/^function/.test(fn)) return fn; kernelThreadString = fn; return false; }); } else { kernelThreadString = translatedSources.shift(); } return this._kernelString = ` const LOOP_MAX = ${ this._getLoopMaxString() }; ${ this.injectedNative || '' } const _this = this; ${ this._resultKernelHeader() } ${ this._processConstants() } return (${ this.argumentNames.map(argumentName => 'user_' + argumentName).join(', ') }) => { ${ this._prependedString.join('') } ${ this._earlyThrows() } ${ this._processArguments() } ${ this.graphical ? this._graphicalKernelBody(kernelThreadString) : this._resultKernelBody(kernelThreadString) } ${ translatedSources.length > 0 ? translatedSources.join('\n') : '' } };`; } /** * @desc Returns the *pre-compiled* Kernel as a JS Object String, that can be reused. */ toString() { return cpuKernelString(this); } /** * @desc Get the maximum loop size String. * @returns {String} result */ _getLoopMaxString() { return ( this.loopMaxIterations ? ` ${ parseInt(this.loopMaxIterations) };` : ' 1000;' ); } _processConstants() { if (!this.constants) return ''; const result = []; for (let p in this.constants) { const type = this.constantTypes[p]; switch (type) { case 'HTMLCanvas': case 'OffscreenCanvas': case 'HTMLImage': case 'ImageBitmap': case 'ImageData': case 'HTMLVideo': result.push(` const constants_${p} = this._mediaTo2DArray(this.constants.${p});\n`); break; case 'HTMLImageArray': result.push(` const constants_${p} = this._imageTo3DArray(this.constants.${p});\n`); break; case 'Input': result.push(` const constants_${p} = this.constants.${p}.value;\n`); break; default: result.push(` const constants_${p} = this.constants.${p};\n`); } } return result.join(''); } _earlyThrows() { if (this.graphical) return ''; if (this.immutable) return ''; if (!this.pipeline) return ''; const arrayArguments = []; for (let i = 0; i < this.argumentTypes.length; i++) { if (this.argumentTypes[i] === 'Array') { arrayArguments.push(this.argumentNames[i]); } } if (arrayArguments.length === 0) return ''; const checks = []; for (let i = 0; i < arrayArguments.length; i++) { const argumentName = arrayArguments[i]; const checkSubKernels = this._mapSubKernels(subKernel => `user_${argumentName} === result_${subKernel.name}`).join(' || '); checks.push(`user_${argumentName} === result${checkSubKernels ? ` || ${checkSubKernels}` : ''}`); } return `if (${checks.join(' || ')}) throw new Error('Source and destination arrays are the same. Use immutable = true');`; } _processArguments() { const result = []; for (let i = 0; i < this.argumentTypes.length; i++) { const variableName = `user_${this.argumentNames[i]}`; switch (this.argumentTypes[i]) { case 'HTMLCanvas': case 'OffscreenCanvas': case 'HTMLImage': case 'ImageBitmap': case 'ImageData': case 'HTMLVideo': result.push(` ${variableName} = this._mediaTo2DArray(${variableName});\n`); break; case 'HTMLImageArray': result.push(` ${variableName} = this._imageTo3DArray(${variableName});\n`); break; case 'Input': result.push(` ${variableName} = ${variableName}.value;\n`); break; case 'ArrayTexture(1)': case 'ArrayTexture(2)': case 'ArrayTexture(3)': case 'ArrayTexture(4)': case 'NumberTexture': case 'MemoryOptimizedNumberTexture': result.push(` if (${variableName}.toArray) { if (!_this.textureCache) { _this.textureCache = []; _this.arrayCache = []; } const textureIndex = _this.textureCache.indexOf(${variableName}); if (textureIndex !== -1) { ${variableName} = _this.arrayCache[textureIndex]; } else { _this.textureCache.push(${variableName}); ${variableName} = ${variableName}.toArray(); _this.arrayCache.push(${variableName}); } }`); break; } } return result.join(''); } _mediaTo2DArray(media) { const canvas = this.canvas; const width = media.width > 0 ? media.width : media.videoWidth; const height = media.height > 0 ? media.height : media.videoHeight; if (canvas.width < width) { canvas.width = width; } if (canvas.height < height) { canvas.height = height; } const ctx = this.context; let pixelsData; if (media.constructor === ImageData) { pixelsData = media.data; } else { ctx.drawImage(media, 0, 0, width, height); pixelsData = ctx.getImageData(0, 0, width, height).data; } const imageArray = new Array(height); let index = 0; for (let y = height - 1; y >= 0; y--) { const row = imageArray[y] = new Array(width); for (let x = 0; x < width; x++) { const pixel = new Float32Array(4); pixel[0] = pixelsData[index++] / 255; // r pixel[1] = pixelsData[index++] / 255; // g pixel[2] = pixelsData[index++] / 255; // b pixel[3] = pixelsData[index++] / 255; // a row[x] = pixel; } } return imageArray; } /** * * @param flip * @return {Uint8ClampedArray} */ getPixels(flip) { const [width, height] = this.output; // cpu is not flipped by default return flip ? utils.flipPixels(this._imageData.data, width, height) : this._imageData.data.slice(0); } _imageTo3DArray(images) { const imagesArray = new Array(images.length); for (let i = 0; i < images.length; i++) { imagesArray[i] = this._mediaTo2DArray(images[i]); } return imagesArray; } _resultKernelHeader() { if (this.graphical) return ''; if (this.immutable) return ''; if (!this.pipeline) return ''; switch (this.output.length) { case 1: return this._mutableKernel1DResults(); case 2: return this._mutableKernel2DResults(); case 3: return this._mutableKernel3DResults(); } } _resultKernelBody(kernelString) { switch (this.output.length) { case 1: return (!this.immutable && this.pipeline ? this._resultMutableKernel1DLoop(kernelString) : this._resultImmutableKernel1DLoop(kernelString)) + this._kernelOutput(); case 2: return (!this.immutable && this.pipeline ? this._resultMutableKernel2DLoop(kernelString) : this._resultImmutableKernel2DLoop(kernelString)) + this._kernelOutput(); case 3: return (!this.immutable && this.pipeline ? this._resultMutableKernel3DLoop(kernelString) : this._resultImmutableKernel3DLoop(kernelString)) + this._kernelOutput(); default: throw new Error('unsupported size kernel'); } } _graphicalKernelBody(kernelThreadString) { switch (this.output.length) { case 2: return this._graphicalKernel2DLoop(kernelThreadString) + this._graphicalOutput(); default: throw new Error('unsupported size kernel'); } } _graphicalOutput() { return ` this._imageData.data.set(this._colorData); this.context.putImageData(this._imageData, 0, 0); return;` } _getKernelResultTypeConstructorString() { switch (this.returnType) { case 'LiteralInteger': case 'Number': case 'Integer': case 'Float': return 'Float32Array'; case 'Array(2)': case 'Array(3)': case 'Array(4)': return 'Array'; default: if (this.graphical) { return 'Float32Array'; } throw new Error(`unhandled returnType ${ this.returnType }`); } } _resultImmutableKernel1DLoop(kernelString) { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const result = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new ${constructorString}(outputX);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') } for (let x = 0; x < outputX; x++) { this.thread.x = x; this.thread.y = 0; this.thread.z = 0; ${ kernelString } }`; } _mutableKernel1DResults() { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const result = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new ${constructorString}(outputX);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') }`; } _resultMutableKernel1DLoop(kernelString) { return ` const outputX = _this.output[0]; for (let x = 0; x < outputX; x++) { this.thread.x = x; this.thread.y = 0; this.thread.z = 0; ${ kernelString } }`; } _resultImmutableKernel2DLoop(kernelString) { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const outputY = _this.output[1]; const result = new Array(outputY); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new Array(outputY);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') } for (let y = 0; y < outputY; y++) { this.thread.z = 0; this.thread.y = y; const resultX = result[y] = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const resultX_${ subKernel.name } = result_${subKernel.name}[y] = new ${constructorString}(outputX);\n`).join('') } for (let x = 0; x < outputX; x++) { this.thread.x = x; ${ kernelString } } }`; } _mutableKernel2DResults() { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const outputY = _this.output[1]; const result = new Array(outputY); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new Array(outputY);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') } for (let y = 0; y < outputY; y++) { const resultX = result[y] = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const resultX_${ subKernel.name } = result_${subKernel.name}[y] = new ${constructorString}(outputX);\n`).join('') } }`; } _resultMutableKernel2DLoop(kernelString) { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const outputY = _this.output[1]; for (let y = 0; y < outputY; y++) { this.thread.z = 0; this.thread.y = y; const resultX = result[y]; ${ this._mapSubKernels(subKernel => `const resultX_${ subKernel.name } = result_${subKernel.name}[y] = new ${constructorString}(outputX);\n`).join('') } for (let x = 0; x < outputX; x++) { this.thread.x = x; ${ kernelString } } }`; } _graphicalKernel2DLoop(kernelString) { return ` const outputX = _this.output[0]; const outputY = _this.output[1]; for (let y = 0; y < outputY; y++) { this.thread.z = 0; this.thread.y = y; for (let x = 0; x < outputX; x++) { this.thread.x = x; ${ kernelString } } }`; } _resultImmutableKernel3DLoop(kernelString) { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const outputY = _this.output[1]; const outputZ = _this.output[2]; const result = new Array(outputZ); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new Array(outputZ);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') } for (let z = 0; z < outputZ; z++) { this.thread.z = z; const resultY = result[z] = new Array(outputY); ${ this._mapSubKernels(subKernel => `const resultY_${ subKernel.name } = result_${subKernel.name}[z] = new Array(outputY);\n`).join(' ') } for (let y = 0; y < outputY; y++) { this.thread.y = y; const resultX = resultY[y] = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const resultX_${ subKernel.name } = resultY_${subKernel.name}[y] = new ${constructorString}(outputX);\n`).join(' ') } for (let x = 0; x < outputX; x++) { this.thread.x = x; ${ kernelString } } } }`; } _mutableKernel3DResults() { const constructorString = this._getKernelResultTypeConstructorString(); return ` const outputX = _this.output[0]; const outputY = _this.output[1]; const outputZ = _this.output[2]; const result = new Array(outputZ); ${ this._mapSubKernels(subKernel => `const result_${ subKernel.name } = new Array(outputZ);\n`).join(' ') } ${ this._mapSubKernels(subKernel => `let subKernelResult_${ subKernel.name };\n`).join(' ') } for (let z = 0; z < outputZ; z++) { const resultY = result[z] = new Array(outputY); ${ this._mapSubKernels(subKernel => `const resultY_${ subKernel.name } = result_${subKernel.name}[z] = new Array(outputY);\n`).join(' ') } for (let y = 0; y < outputY; y++) { const resultX = resultY[y] = new ${constructorString}(outputX); ${ this._mapSubKernels(subKernel => `const resultX_${ subKernel.name } = resultY_${subKernel.name}[y] = new ${constructorString}(outputX);\n`).join(' ') } } }`; } _resultMutableKernel3DLoop(kernelString) { return ` const outputX = _this.output[0]; const outputY = _this.output[1]; const outputZ = _this.output[2]; for (let z = 0; z < outputZ; z++) { this.thread.z = z; const resultY = result[z]; for (let y = 0; y < outputY; y++) { this.thread.y = y; const resultX = resultY[y]; for (let x = 0; x < outputX; x++) { this.thread.x = x; ${ kernelString } } } }`; } _kernelOutput() { if (!this.subKernels) { return '\n return result;'; } return `\n return { result: result, ${ this.subKernels.map(subKernel => `${ subKernel.property }: result_${ subKernel.name }`).join(',\n ') } };`; } _mapSubKernels(fn) { return this.subKernels === null ? [''] : this.subKernels.map(fn); } destroy(removeCanvasReference) { if (removeCanvasReference) { delete this.canvas; } } static destroyContext(context) {} toJSON() { const json = super.toJSON(); json.functionNodes = FunctionBuilder.fromKernel(this, CPUFunctionNode).toJSON(); return json; } setOutput(output) { super.setOutput(output); const [width, height] = this.output; if (this.graphical) { this._imageData = this.context.createImageData(width, height); this._colorData = new Uint8ClampedArray(width * height * 4); } } prependString(value) { if (this._kernelString) throw new Error('Kernel already built'); this._prependedString.push(value); } hasPrependString(value) { return this._prependedString.indexOf(value) > -1; } } module.exports = { CPUKernel }; ================================================ FILE: src/backend/function-builder.js ================================================ /** * @desc This handles all the raw state, converted state, etc. of a single function. * [INTERNAL] A collection of functionNodes. * @class */ class FunctionBuilder { /** * * @param {Kernel} kernel * @param {FunctionNode} FunctionNode * @param {object} [extraNodeOptions] * @returns {FunctionBuilder} * @static */ static fromKernel(kernel, FunctionNode, extraNodeOptions) { const { kernelArguments, kernelConstants, argumentNames, argumentSizes, argumentBitRatios, constants, constantBitRatios, debug, loopMaxIterations, nativeFunctions, output, optimizeFloatMemory, precision, plugins, source, subKernels, functions, leadingReturnStatement, followingReturnStatement, dynamicArguments, dynamicOutput, } = kernel; const argumentTypes = new Array(kernelArguments.length); const constantTypes = {}; for (let i = 0; i < kernelArguments.length; i++) { argumentTypes[i] = kernelArguments[i].type; } for (let i = 0; i < kernelConstants.length; i++) { const kernelConstant = kernelConstants[i]; constantTypes[kernelConstant.name] = kernelConstant.type; } const needsArgumentType = (functionName, index) => { return functionBuilder.needsArgumentType(functionName, index); }; const assignArgumentType = (functionName, index, type) => { functionBuilder.assignArgumentType(functionName, index, type); }; const lookupReturnType = (functionName, ast, requestingNode) => { return functionBuilder.lookupReturnType(functionName, ast, requestingNode); }; const lookupFunctionArgumentTypes = (functionName) => { return functionBuilder.lookupFunctionArgumentTypes(functionName); }; const lookupFunctionArgumentName = (functionName, argumentIndex) => { return functionBuilder.lookupFunctionArgumentName(functionName, argumentIndex); }; const lookupFunctionArgumentBitRatio = (functionName, argumentName) => { return functionBuilder.lookupFunctionArgumentBitRatio(functionName, argumentName); }; const triggerImplyArgumentType = (functionName, i, argumentType, requestingNode) => { functionBuilder.assignArgumentType(functionName, i, argumentType, requestingNode); }; const triggerImplyArgumentBitRatio = (functionName, argumentName, calleeFunctionName, argumentIndex) => { functionBuilder.assignArgumentBitRatio(functionName, argumentName, calleeFunctionName, argumentIndex); }; const onFunctionCall = (functionName, calleeFunctionName, args) => { functionBuilder.trackFunctionCall(functionName, calleeFunctionName, args); }; const onNestedFunction = (ast, source) => { const argumentNames = []; for (let i = 0; i < ast.params.length; i++) { argumentNames.push(ast.params[i].name); } const nestedFunction = new FunctionNode(source, Object.assign({}, nodeOptions, { returnType: null, ast, name: ast.id.name, argumentNames, lookupReturnType, lookupFunctionArgumentTypes, lookupFunctionArgumentName, lookupFunctionArgumentBitRatio, needsArgumentType, assignArgumentType, triggerImplyArgumentType, triggerImplyArgumentBitRatio, onFunctionCall, })); nestedFunction.traceFunctionAST(ast); functionBuilder.addFunctionNode(nestedFunction); }; const nodeOptions = Object.assign({ isRootKernel: false, onNestedFunction, lookupReturnType, lookupFunctionArgumentTypes, lookupFunctionArgumentName, lookupFunctionArgumentBitRatio, needsArgumentType, assignArgumentType, triggerImplyArgumentType, triggerImplyArgumentBitRatio, onFunctionCall, optimizeFloatMemory, precision, constants, constantTypes, constantBitRatios, debug, loopMaxIterations, output, plugins, dynamicArguments, dynamicOutput, }, extraNodeOptions || {}); const rootNodeOptions = Object.assign({}, nodeOptions, { isRootKernel: true, name: 'kernel', argumentNames, argumentTypes, argumentSizes, argumentBitRatios, leadingReturnStatement, followingReturnStatement, }); if (typeof source === 'object' && source.functionNodes) { return new FunctionBuilder().fromJSON(source.functionNodes, FunctionNode); } const rootNode = new FunctionNode(source, rootNodeOptions); let functionNodes = null; if (functions) { functionNodes = functions.map((fn) => new FunctionNode(fn.source, { returnType: fn.returnType, argumentTypes: fn.argumentTypes, output, plugins, constants, constantTypes, constantBitRatios, optimizeFloatMemory, precision, lookupReturnType, lookupFunctionArgumentTypes, lookupFunctionArgumentName, lookupFunctionArgumentBitRatio, needsArgumentType, assignArgumentType, triggerImplyArgumentType, triggerImplyArgumentBitRatio, onFunctionCall, onNestedFunction, })); } let subKernelNodes = null; if (subKernels) { subKernelNodes = subKernels.map((subKernel) => { const { name, source } = subKernel; return new FunctionNode(source, Object.assign({}, nodeOptions, { name, isSubKernel: true, isRootKernel: false, })); }); } const functionBuilder = new FunctionBuilder({ kernel, rootNode, functionNodes, nativeFunctions, subKernelNodes }); return functionBuilder; } /** * * @param {IFunctionBuilderSettings} [settings] */ constructor(settings) { settings = settings || {}; this.kernel = settings.kernel; this.rootNode = settings.rootNode; this.functionNodes = settings.functionNodes || []; this.subKernelNodes = settings.subKernelNodes || []; this.nativeFunctions = settings.nativeFunctions || []; this.functionMap = {}; this.nativeFunctionNames = []; this.lookupChain = []; this.functionNodeDependencies = {}; this.functionCalls = {}; if (this.rootNode) { this.functionMap['kernel'] = this.rootNode; } if (this.functionNodes) { for (let i = 0; i < this.functionNodes.length; i++) { this.functionMap[this.functionNodes[i].name] = this.functionNodes[i]; } } if (this.subKernelNodes) { for (let i = 0; i < this.subKernelNodes.length; i++) { this.functionMap[this.subKernelNodes[i].name] = this.subKernelNodes[i]; } } if (this.nativeFunctions) { for (let i = 0; i < this.nativeFunctions.length; i++) { const nativeFunction = this.nativeFunctions[i]; this.nativeFunctionNames.push(nativeFunction.name); } } } /** * @desc Add the function node directly * * @param {FunctionNode} functionNode - functionNode to add * */ addFunctionNode(functionNode) { if (!functionNode.name) throw new Error('functionNode.name needs set'); this.functionMap[functionNode.name] = functionNode; if (functionNode.isRootKernel) { this.rootNode = functionNode; } } /** * @desc Trace all the depending functions being called, from a single function * * This allow for 'unneeded' functions to be automatically optimized out. * Note that the 0-index, is the starting function trace. * * @param {String} functionName - Function name to trace from, default to 'kernel' * @param {String[]} [retList] - Returning list of function names that is traced. Including itself. * * @returns {String[]} Returning list of function names that is traced. Including itself. */ traceFunctionCalls(functionName, retList) { functionName = functionName || 'kernel'; retList = retList || []; if (this.nativeFunctionNames.indexOf(functionName) > -1) { const nativeFunctionIndex = retList.indexOf(functionName); if (nativeFunctionIndex === -1) { retList.push(functionName); } else { /** * https://github.com/gpujs/gpu.js/issues/207 * if dependent function is already in the list, because a function depends on it, and because it has * already been traced, we know that we must move the dependent function to the end of the the retList. * */ const dependantNativeFunctionName = retList.splice(nativeFunctionIndex, 1)[0]; retList.push(dependantNativeFunctionName); } return retList; } const functionNode = this.functionMap[functionName]; if (functionNode) { // Check if function already exists const functionIndex = retList.indexOf(functionName); if (functionIndex === -1) { retList.push(functionName); functionNode.toString(); //ensure JS trace is done for (let i = 0; i < functionNode.calledFunctions.length; ++i) { this.traceFunctionCalls(functionNode.calledFunctions[i], retList); } } else { /** * https://github.com/gpujs/gpu.js/issues/207 * if dependent function is already in the list, because a function depends on it, and because it has * already been traced, we know that we must move the dependent function to the end of the the retList. * */ const dependantFunctionName = retList.splice(functionIndex, 1)[0]; retList.push(dependantFunctionName); } } return retList; } /** * @desc Return the string for a function * @param {String} functionName - Function name to trace from. If null, it returns the WHOLE builder stack * @returns {String} The full string, of all the various functions. Trace optimized if functionName given */ getPrototypeString(functionName) { return this.getPrototypes(functionName).join('\n'); } /** * @desc Return the string for a function * @param {String} [functionName] - Function name to trace from. If null, it returns the WHOLE builder stack * @returns {Array} The full string, of all the various functions. Trace optimized if functionName given */ getPrototypes(functionName) { if (this.rootNode) { this.rootNode.toString(); } if (functionName) { return this.getPrototypesFromFunctionNames(this.traceFunctionCalls(functionName, []).reverse()); } return this.getPrototypesFromFunctionNames(Object.keys(this.functionMap)); } /** * @desc Get string from function names * @param {String[]} functionList - List of function to build string * @returns {String} The string, of all the various functions. Trace optimized if functionName given */ getStringFromFunctionNames(functionList) { const ret = []; for (let i = 0; i < functionList.length; ++i) { const node = this.functionMap[functionList[i]]; if (node) { ret.push(this.functionMap[functionList[i]].toString()); } } return ret.join('\n'); } /** * @desc Return string of all functions converted * @param {String[]} functionList - List of function names to build the string. * @returns {Array} Prototypes of all functions converted */ getPrototypesFromFunctionNames(functionList) { const ret = []; for (let i = 0; i < functionList.length; ++i) { const functionName = functionList[i]; const functionIndex = this.nativeFunctionNames.indexOf(functionName); if (functionIndex > -1) { ret.push(this.nativeFunctions[functionIndex].source); continue; } const node = this.functionMap[functionName]; if (node) { ret.push(node.toString()); } } return ret; } toJSON() { return this.traceFunctionCalls(this.rootNode.name).reverse().map(name => { const nativeIndex = this.nativeFunctions.indexOf(name); if (nativeIndex > -1) { return { name, source: this.nativeFunctions[nativeIndex].source }; } else if (this.functionMap[name]) { return this.functionMap[name].toJSON(); } else { throw new Error(`function ${ name } not found`); } }); } fromJSON(jsonFunctionNodes, FunctionNode) { this.functionMap = {}; for (let i = 0; i < jsonFunctionNodes.length; i++) { const jsonFunctionNode = jsonFunctionNodes[i]; this.functionMap[jsonFunctionNode.settings.name] = new FunctionNode(jsonFunctionNode.ast, jsonFunctionNode.settings); } return this; } /** * @desc Get string for a particular function name * @param {String} functionName - Function name to trace from. If null, it returns the WHOLE builder stack * @returns {String} settings - The string, of all the various functions. Trace optimized if functionName given */ getString(functionName) { if (functionName) { return this.getStringFromFunctionNames(this.traceFunctionCalls(functionName).reverse()); } return this.getStringFromFunctionNames(Object.keys(this.functionMap)); } lookupReturnType(functionName, ast, requestingNode) { if (ast.type !== 'CallExpression') { throw new Error(`expected ast type of "CallExpression", but is ${ ast.type }`); } if (this._isNativeFunction(functionName)) { return this._lookupNativeFunctionReturnType(functionName); } else if (this._isFunction(functionName)) { const node = this._getFunction(functionName); if (node.returnType) { return node.returnType; } else { for (let i = 0; i < this.lookupChain.length; i++) { // detect circlical logic if (this.lookupChain[i].ast === ast) { // detect if arguments have not resolved, preventing a return type // if so, go ahead and resolve them, so we can resolve the return type if (node.argumentTypes.length === 0 && ast.arguments.length > 0) { const args = ast.arguments; for (let j = 0; j < args.length; j++) { this.lookupChain.push({ name: requestingNode.name, ast: args[i], requestingNode }); node.argumentTypes[j] = requestingNode.getType(args[j]); this.lookupChain.pop(); } return node.returnType = node.getType(node.getJsAST()); } throw new Error('circlical logic detected!'); } } // get ready for a ride! this.lookupChain.push({ name: requestingNode.name, ast, requestingNode }); const type = node.getType(node.getJsAST()); this.lookupChain.pop(); return node.returnType = type; } } return null; } /** * * @param {String} functionName * @return {FunctionNode} * @private */ _getFunction(functionName) { if (!this._isFunction(functionName)) { new Error(`Function ${functionName} not found`); } return this.functionMap[functionName]; } _isFunction(functionName) { return Boolean(this.functionMap[functionName]); } _getNativeFunction(functionName) { for (let i = 0; i < this.nativeFunctions.length; i++) { if (this.nativeFunctions[i].name === functionName) return this.nativeFunctions[i]; } return null; } _isNativeFunction(functionName) { return Boolean(this._getNativeFunction(functionName)); } _lookupNativeFunctionReturnType(functionName) { let nativeFunction = this._getNativeFunction(functionName); if (nativeFunction) { return nativeFunction.returnType; } throw new Error(`Native function ${ functionName } not found`); } lookupFunctionArgumentTypes(functionName) { if (this._isNativeFunction(functionName)) { return this._getNativeFunction(functionName).argumentTypes; } else if (this._isFunction(functionName)) { return this._getFunction(functionName).argumentTypes; } return null; } lookupFunctionArgumentName(functionName, argumentIndex) { return this._getFunction(functionName).argumentNames[argumentIndex]; } /** * * @param {string} functionName * @param {string} argumentName * @return {number} */ lookupFunctionArgumentBitRatio(functionName, argumentName) { if (!this._isFunction(functionName)) { throw new Error('function not found'); } if (this.rootNode.name === functionName) { const i = this.rootNode.argumentNames.indexOf(argumentName); if (i !== -1) { return this.rootNode.argumentBitRatios[i]; } } const node = this._getFunction(functionName); const i = node.argumentNames.indexOf(argumentName); if (i === -1) { throw new Error('argument not found'); } const bitRatio = node.argumentBitRatios[i]; if (typeof bitRatio !== 'number') { throw new Error('argument bit ratio not found'); } return bitRatio; } needsArgumentType(functionName, i) { if (!this._isFunction(functionName)) return false; const fnNode = this._getFunction(functionName); return !fnNode.argumentTypes[i]; } assignArgumentType(functionName, i, argumentType, requestingNode) { if (!this._isFunction(functionName)) return; const fnNode = this._getFunction(functionName); if (!fnNode.argumentTypes[i]) { fnNode.argumentTypes[i] = argumentType; } } /** * @param {string} functionName * @param {string} argumentName * @param {string} calleeFunctionName * @param {number} argumentIndex * @return {number|null} */ assignArgumentBitRatio(functionName, argumentName, calleeFunctionName, argumentIndex) { const node = this._getFunction(functionName); if (this._isNativeFunction(calleeFunctionName)) return null; const calleeNode = this._getFunction(calleeFunctionName); const i = node.argumentNames.indexOf(argumentName); if (i === -1) { throw new Error(`Argument ${argumentName} not found in arguments from function ${functionName}`); } const bitRatio = node.argumentBitRatios[i]; if (typeof bitRatio !== 'number') { throw new Error(`Bit ratio for argument ${argumentName} not found in function ${functionName}`); } if (!calleeNode.argumentBitRatios) { calleeNode.argumentBitRatios = new Array(calleeNode.argumentNames.length); } const calleeBitRatio = calleeNode.argumentBitRatios[i]; if (typeof calleeBitRatio === 'number') { if (calleeBitRatio !== bitRatio) { throw new Error(`Incompatible bit ratio found at function ${functionName} at argument ${argumentName}`); } return calleeBitRatio; } calleeNode.argumentBitRatios[i] = bitRatio; return bitRatio; } trackFunctionCall(functionName, calleeFunctionName, args) { if (!this.functionNodeDependencies[functionName]) { this.functionNodeDependencies[functionName] = new Set(); this.functionCalls[functionName] = []; } this.functionNodeDependencies[functionName].add(calleeFunctionName); this.functionCalls[functionName].push(args); } getKernelResultType() { return this.rootNode.returnType || this.rootNode.getType(this.rootNode.ast); } getSubKernelResultType(index) { const subKernelNode = this.subKernelNodes[index]; let called = false; for (let functionCallIndex = 0; functionCallIndex < this.rootNode.functionCalls.length; functionCallIndex++) { const functionCall = this.rootNode.functionCalls[functionCallIndex]; if (functionCall.ast.callee.name === subKernelNode.name) { called = true; } } if (!called) { throw new Error(`SubKernel ${ subKernelNode.name } never called by kernel`); } return subKernelNode.returnType || subKernelNode.getType(subKernelNode.getJsAST()); } getReturnTypes() { const result = { [this.rootNode.name]: this.rootNode.getType(this.rootNode.ast), }; const list = this.traceFunctionCalls(this.rootNode.name); for (let i = 0; i < list.length; i++) { const functionName = list[i]; const functionNode = this.functionMap[functionName]; result[functionName] = functionNode.getType(functionNode.ast); } return result; } } module.exports = { FunctionBuilder }; ================================================ FILE: src/backend/function-node.js ================================================ const acorn = require('acorn'); const { utils } = require('../utils'); const { FunctionTracer } = require('./function-tracer'); /** * * @desc Represents a single function, inside JS, webGL, or openGL. *This handles all the raw state, converted state, etc. Of a single function.
*/ class FunctionNode { /** * * @param {string|object} source * @param {IFunctionSettings} [settings] */ constructor(source, settings) { if (!source && !settings.ast) { throw new Error('source parameter is missing'); } settings = settings || {}; this.source = source; this.ast = null; this.name = typeof source === 'string' ? settings.isRootKernel ? 'kernel' : (settings.name || utils.getFunctionNameFromString(source)) : null; this.calledFunctions = []; this.constants = {}; this.constantTypes = {}; this.constantBitRatios = {}; this.isRootKernel = false; this.isSubKernel = false; this.debug = null; this.functions = null; this.identifiers = null; this.contexts = null; this.functionCalls = null; this.states = []; this.needsArgumentType = null; this.assignArgumentType = null; this.lookupReturnType = null; this.lookupFunctionArgumentTypes = null; this.lookupFunctionArgumentBitRatio = null; this.triggerImplyArgumentType = null; this.triggerImplyArgumentBitRatio = null; this.onNestedFunction = null; this.onFunctionCall = null; this.optimizeFloatMemory = null; this.precision = null; this.loopMaxIterations = null; this.argumentNames = (typeof this.source === 'string' ? utils.getArgumentNamesFromString(this.source) : null); this.argumentTypes = []; this.argumentSizes = []; this.argumentBitRatios = null; this.returnType = null; this.output = []; this.plugins = null; this.leadingReturnStatement = null; this.followingReturnStatement = null; this.dynamicOutput = null; this.dynamicArguments = null; this.strictTypingChecking = false; this.fixIntegerDivisionAccuracy = null; if (settings) { for (const p in settings) { if (!settings.hasOwnProperty(p)) continue; if (!this.hasOwnProperty(p)) continue; this[p] = settings[p]; } } this.literalTypes = {}; this.validate(); this._string = null; this._internalVariableNames = {}; } validate() { if (typeof this.source !== 'string' && !this.ast) { throw new Error('this.source not a string'); } if (!this.ast && !utils.isFunctionString(this.source)) { throw new Error('this.source not a function string'); } if (!this.name) { throw new Error('this.name could not be set'); } if (this.argumentTypes.length > 0 && this.argumentTypes.length !== this.argumentNames.length) { throw new Error(`argumentTypes count of ${ this.argumentTypes.length } exceeds ${ this.argumentNames.length }`); } if (this.output.length < 1) { throw new Error('this.output is not big enough'); } } /** * @param {String} name * @returns {boolean} */ isIdentifierConstant(name) { if (!this.constants) return false; return this.constants.hasOwnProperty(name); } isInput(argumentName) { return this.argumentTypes[this.argumentNames.indexOf(argumentName)] === 'Input'; } pushState(state) { this.states.push(state); } popState(state) { if (this.state !== state) { throw new Error(`Cannot popState ${ state } when in ${ this.state }`); } this.states.pop(); } isState(state) { return this.state === state; } get state() { return this.states[this.states.length - 1]; } /** * @function * @name astMemberExpressionUnroll * @desc Parses the abstract syntax tree for binary expression. * *Utility function for astCallExpression.
* * @param {Object} ast - the AST object to parse * * @returns {String} the function namespace call, unrolled */ astMemberExpressionUnroll(ast) { if (ast.type === 'Identifier') { return ast.name; } else if (ast.type === 'ThisExpression') { return 'this'; } if (ast.type === 'MemberExpression') { if (ast.object && ast.property) { //babel sniffing if (ast.object.hasOwnProperty('name') && ast.object.name !== 'Math') { return this.astMemberExpressionUnroll(ast.property); } return ( this.astMemberExpressionUnroll(ast.object) + '.' + this.astMemberExpressionUnroll(ast.property) ); } } //babel sniffing if (ast.hasOwnProperty('expressions')) { const firstExpression = ast.expressions[0]; if (firstExpression.type === 'Literal' && firstExpression.value === 0 && ast.expressions.length === 2) { return this.astMemberExpressionUnroll(ast.expressions[1]); } } // Failure, unknown expression throw this.astErrorOutput('Unknown astMemberExpressionUnroll', ast); } /** * @desc Parses the class function JS, and returns its Abstract Syntax Tree object. * This is used internally to convert to shader code * * @param {Object} [inParser] - Parser to use, assumes in scope 'parser' if null or undefined * * @returns {Object} The function AST Object, note that result is cached under this.ast; */ getJsAST(inParser) { if (this.ast) { return this.ast; } if (typeof this.source === 'object') { this.traceFunctionAST(this.source); return this.ast = this.source; } inParser = inParser || acorn; if (inParser === null) { throw new Error('Missing JS to AST parser'); } const ast = Object.freeze(inParser.parse(`const parser_${ this.name } = ${ this.source };`, { locations: true })); // take out the function object, outside the var declarations const functionAST = ast.body[0].declarations[0].init; this.traceFunctionAST(functionAST); if (!ast) { throw new Error('Failed to parse JS code'); } return this.ast = functionAST; } traceFunctionAST(ast) { const { contexts, declarations, functions, identifiers, functionCalls } = new FunctionTracer(ast); this.contexts = contexts; this.identifiers = identifiers; this.functionCalls = functionCalls; this.functions = functions; for (let i = 0; i < declarations.length; i++) { const declaration = declarations[i]; const { ast, inForLoopInit, inForLoopTest } = declaration; const { init } = ast; const dependencies = this.getDependencies(init); let valueType = null; if (inForLoopInit && inForLoopTest) { valueType = 'Integer'; } else { if (init) { const realType = this.getType(init); switch (realType) { case 'Integer': case 'Float': case 'Number': if (init.type === 'MemberExpression') { valueType = realType; } else { valueType = 'Number'; } break; case 'LiteralInteger': valueType = 'Number'; break; default: valueType = realType; } } } declaration.valueType = valueType; declaration.dependencies = dependencies; declaration.isSafe = this.isSafeDependencies(dependencies); } for (let i = 0; i < functions.length; i++) { this.onNestedFunction(functions[i], this.source); } } getDeclaration(ast) { for (let i = 0; i < this.identifiers.length; i++) { const identifier = this.identifiers[i]; if (ast === identifier.ast) { return identifier.declaration; } } return null; } /** * @desc Return the type of parameter sent to subKernel/Kernel. * @param {Object} ast - Identifier * @returns {String} Type of the parameter */ getVariableType(ast) { if (ast.type !== 'Identifier') { throw new Error(`ast of ${ast.type} not "Identifier"`); } let type = null; const argumentIndex = this.argumentNames.indexOf(ast.name); if (argumentIndex === -1) { const declaration = this.getDeclaration(ast); if (declaration) { return declaration.valueType; } } else { const argumentType = this.argumentTypes[argumentIndex]; if (argumentType) { type = argumentType; } } if (!type && this.strictTypingChecking) { throw new Error(`Declaration of ${name} not found`); } return type; } /** * Generally used to lookup the value type returned from a member expressions * @param {String} type * @return {String} */ getLookupType(type) { if (!typeLookupMap.hasOwnProperty(type)) { throw new Error(`unknown typeLookupMap ${ type }`); } return typeLookupMap[type]; } getConstantType(constantName) { if (this.constantTypes[constantName]) { const type = this.constantTypes[constantName]; if (type === 'Float') { return 'Number'; } else { return type; } } throw new Error(`Type for constant "${ constantName }" not declared`); } toString() { if (this._string) return this._string; return this._string = this.astGeneric(this.getJsAST(), []).join('').trim(); } toJSON() { const settings = { source: this.source, name: this.name, constants: this.constants, constantTypes: this.constantTypes, isRootKernel: this.isRootKernel, isSubKernel: this.isSubKernel, debug: this.debug, output: this.output, loopMaxIterations: this.loopMaxIterations, argumentNames: this.argumentNames, argumentTypes: this.argumentTypes, argumentSizes: this.argumentSizes, returnType: this.returnType, leadingReturnStatement: this.leadingReturnStatement, followingReturnStatement: this.followingReturnStatement, }; return { ast: this.ast, settings }; } /** * Recursively looks up type for ast expression until it's found * @param ast * @returns {String|null} */ getType(ast) { if (Array.isArray(ast)) { return this.getType(ast[ast.length - 1]); } switch (ast.type) { case 'BlockStatement': return this.getType(ast.body); case 'ArrayExpression': const childType = this.getType(ast.elements[0]); switch (childType) { case 'Array(2)': case 'Array(3)': case 'Array(4)': return `Matrix(${ast.elements.length})`; } return `Array(${ ast.elements.length })`; case 'Literal': const literalKey = this.astKey(ast); if (this.literalTypes[literalKey]) { return this.literalTypes[literalKey]; } if (Number.isInteger(ast.value)) { return 'LiteralInteger'; } else if (ast.value === true || ast.value === false) { return 'Boolean'; } else { return 'Number'; } case 'AssignmentExpression': return this.getType(ast.left); case 'CallExpression': if (this.isAstMathFunction(ast)) { return 'Number'; } if (!ast.callee || !ast.callee.name) { if (ast.callee.type === 'SequenceExpression' && ast.callee.expressions[ast.callee.expressions.length - 1].property.name) { const functionName = ast.callee.expressions[ast.callee.expressions.length - 1].property.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } if (this.getVariableSignature(ast.callee, true) === 'this.color') { return null; } if (ast.callee.type === 'MemberExpression' && ast.callee.object && ast.callee.property && ast.callee.property.name && ast.arguments) { const functionName = ast.callee.property.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } throw this.astErrorOutput('Unknown call expression', ast); } if (ast.callee && ast.callee.name) { const functionName = ast.callee.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast); case 'LogicalExpression': return 'Boolean'; case 'BinaryExpression': // modulos is Number switch (ast.operator) { case '%': case '/': if (this.fixIntegerDivisionAccuracy) { return 'Number'; } else { break; } case '>': case '<': return 'Boolean'; case '&': case '|': case '^': case '<<': case '>>': case '>>>': return 'Integer'; } const type = this.getType(ast.left); if (this.isState('skip-literal-correction')) return type; if (type === 'LiteralInteger') { const rightType = this.getType(ast.right); if (rightType === 'LiteralInteger') { if (ast.left.value % 1 === 0) { return 'Integer'; } else { return 'Float'; } } return rightType; } return typeLookupMap[type] || type; case 'UpdateExpression': return this.getType(ast.argument); case 'UnaryExpression': if (ast.operator === '~') { return 'Integer'; } return this.getType(ast.argument); case 'VariableDeclaration': { const declarations = ast.declarations; let lastType; for (let i = 0; i < declarations.length; i++) { const declaration = declarations[i]; lastType = this.getType(declaration); } if (!lastType) { throw this.astErrorOutput(`Unable to find type for declaration`, ast); } return lastType; } case 'VariableDeclarator': const declaration = this.getDeclaration(ast.id); if (!declaration) { throw this.astErrorOutput(`Unable to find declarator`, ast); } if (!declaration.valueType) { throw this.astErrorOutput(`Unable to find declarator valueType`, ast); } return declaration.valueType; case 'Identifier': if (ast.name === 'Infinity') { return 'Number'; } if (this.isAstVariable(ast)) { const signature = this.getVariableSignature(ast); if (signature === 'value') { return this.getCheckVariableType(ast); } } const origin = this.findIdentifierOrigin(ast); if (origin && origin.init) { return this.getType(origin.init); } return null; case 'ReturnStatement': return this.getType(ast.argument); case 'MemberExpression': if (this.isAstMathFunction(ast)) { switch (ast.property.name) { case 'ceil': return 'Integer'; case 'floor': return 'Integer'; case 'round': return 'Integer'; } return 'Number'; } if (this.isAstVariable(ast)) { const variableSignature = this.getVariableSignature(ast); switch (variableSignature) { case 'value[]': return this.getLookupType(this.getCheckVariableType(ast.object)); case 'value[][]': return this.getLookupType(this.getCheckVariableType(ast.object.object)); case 'value[][][]': return this.getLookupType(this.getCheckVariableType(ast.object.object.object)); case 'value[][][][]': return this.getLookupType(this.getCheckVariableType(ast.object.object.object.object)); case 'value.thread.value': case 'this.thread.value': return 'Integer'; case 'this.output.value': return this.dynamicOutput ? 'Integer' : 'LiteralInteger'; case 'this.constants.value': return this.getConstantType(ast.property.name); case 'this.constants.value[]': return this.getLookupType(this.getConstantType(ast.object.property.name)); case 'this.constants.value[][]': return this.getLookupType(this.getConstantType(ast.object.object.property.name)); case 'this.constants.value[][][]': return this.getLookupType(this.getConstantType(ast.object.object.object.property.name)); case 'this.constants.value[][][][]': return this.getLookupType(this.getConstantType(ast.object.object.object.object.property.name)); case 'fn()[]': case 'fn()[][]': case 'fn()[][][]': return this.getLookupType(this.getType(ast.object)); case 'value.value': if (this.isAstMathVariable(ast)) { return 'Number'; } switch (ast.property.name) { case 'r': case 'g': case 'b': case 'a': return this.getLookupType(this.getCheckVariableType(ast.object)); } case '[][]': return 'Number'; } throw this.astErrorOutput('Unhandled getType MemberExpression', ast); } throw this.astErrorOutput('Unhandled getType MemberExpression', ast); case 'ConditionalExpression': return this.getType(ast.consequent); case 'FunctionDeclaration': case 'FunctionExpression': const lastReturn = this.findLastReturn(ast.body); if (lastReturn) { return this.getType(lastReturn); } return null; case 'IfStatement': return this.getType(ast.consequent); case 'SequenceExpression': return this.getType(ast.expressions[ast.expressions.length - 1]); default: throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast); } } getCheckVariableType(ast) { const type = this.getVariableType(ast); if (!type) { throw this.astErrorOutput(`${ast.type} is not defined`, ast); } return type; } inferArgumentTypesIfNeeded(functionName, args) { // ensure arguments are filled in, so when we lookup return type, we already can infer it for (let i = 0; i < args.length; i++) { if (!this.needsArgumentType(functionName, i)) continue; const type = this.getType(args[i]); if (!type) { throw this.astErrorOutput(`Unable to infer argument ${i}`, args[i]); } this.assignArgumentType(functionName, i, type); } } isAstMathVariable(ast) { const mathProperties = [ 'E', 'PI', 'SQRT2', 'SQRT1_2', 'LN2', 'LN10', 'LOG2E', 'LOG10E', ]; return ast.type === 'MemberExpression' && ast.object && ast.object.type === 'Identifier' && ast.object.name === 'Math' && ast.property && ast.property.type === 'Identifier' && mathProperties.indexOf(ast.property.name) > -1; } isAstMathFunction(ast) { const mathFunctions = [ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'clz32', 'cos', 'cosh', 'expm1', 'exp', 'floor', 'fround', 'imul', 'log', 'log2', 'log10', 'log1p', 'max', 'min', 'pow', 'random', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', ]; return ast.type === 'CallExpression' && ast.callee && ast.callee.type === 'MemberExpression' && ast.callee.object && ast.callee.object.type === 'Identifier' && ast.callee.object.name === 'Math' && ast.callee.property && ast.callee.property.type === 'Identifier' && mathFunctions.indexOf(ast.callee.property.name) > -1; } isAstVariable(ast) { return ast.type === 'Identifier' || ast.type === 'MemberExpression'; } isSafe(ast) { return this.isSafeDependencies(this.getDependencies(ast)); } isSafeDependencies(dependencies) { return dependencies && dependencies.every ? dependencies.every(dependency => dependency.isSafe) : true; } /** * * @param ast * @param dependencies * @param isNotSafe * @return {Array} */ getDependencies(ast, dependencies, isNotSafe) { if (!dependencies) { dependencies = []; } if (!ast) return null; if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { this.getDependencies(ast[i], dependencies, isNotSafe); } return dependencies; } switch (ast.type) { case 'AssignmentExpression': this.getDependencies(ast.left, dependencies, isNotSafe); this.getDependencies(ast.right, dependencies, isNotSafe); return dependencies; case 'ConditionalExpression': this.getDependencies(ast.test, dependencies, isNotSafe); this.getDependencies(ast.alternate, dependencies, isNotSafe); this.getDependencies(ast.consequent, dependencies, isNotSafe); return dependencies; case 'Literal': dependencies.push({ origin: 'literal', value: ast.value, isSafe: isNotSafe === true ? false : ast.value > -Infinity && ast.value < Infinity && !isNaN(ast.value) }); break; case 'VariableDeclarator': return this.getDependencies(ast.init, dependencies, isNotSafe); case 'Identifier': const declaration = this.getDeclaration(ast); if (declaration) { dependencies.push({ name: ast.name, origin: 'declaration', isSafe: isNotSafe ? false : this.isSafeDependencies(declaration.dependencies), }); } else if (this.argumentNames.indexOf(ast.name) > -1) { dependencies.push({ name: ast.name, origin: 'argument', isSafe: false, }); } else if (this.strictTypingChecking) { throw new Error(`Cannot find identifier origin "${ast.name}"`); } break; case 'FunctionDeclaration': return this.getDependencies(ast.body.body[ast.body.body.length - 1], dependencies, isNotSafe); case 'ReturnStatement': return this.getDependencies(ast.argument, dependencies); case 'BinaryExpression': case 'LogicalExpression': isNotSafe = (ast.operator === '/' || ast.operator === '*'); this.getDependencies(ast.left, dependencies, isNotSafe); this.getDependencies(ast.right, dependencies, isNotSafe); return dependencies; case 'UnaryExpression': case 'UpdateExpression': return this.getDependencies(ast.argument, dependencies, isNotSafe); case 'VariableDeclaration': return this.getDependencies(ast.declarations, dependencies, isNotSafe); case 'ArrayExpression': dependencies.push({ origin: 'declaration', isSafe: true, }); return dependencies; case 'CallExpression': dependencies.push({ origin: 'function', isSafe: true, }); return dependencies; case 'MemberExpression': const details = this.getMemberExpressionDetails(ast); switch (details.signature) { case 'value[]': this.getDependencies(ast.object, dependencies, isNotSafe); break; case 'value[][]': this.getDependencies(ast.object.object, dependencies, isNotSafe); break; case 'value[][][]': this.getDependencies(ast.object.object.object, dependencies, isNotSafe); break; case 'this.output.value': if (this.dynamicOutput) { dependencies.push({ name: details.name, origin: 'output', isSafe: false, }); } break; } if (details) { if (details.property) { this.getDependencies(details.property, dependencies, isNotSafe); } if (details.xProperty) { this.getDependencies(details.xProperty, dependencies, isNotSafe); } if (details.yProperty) { this.getDependencies(details.yProperty, dependencies, isNotSafe); } if (details.zProperty) { this.getDependencies(details.zProperty, dependencies, isNotSafe); } return dependencies; } case 'SequenceExpression': return this.getDependencies(ast.expressions, dependencies, isNotSafe); default: throw this.astErrorOutput(`Unhandled type ${ ast.type } in getDependencies`, ast); } return dependencies; } getVariableSignature(ast, returnRawValue) { if (!this.isAstVariable(ast)) { throw new Error(`ast of type "${ ast.type }" is not a variable signature`); } if (ast.type === 'Identifier') { return 'value'; } const signature = []; while (true) { if (!ast) break; if (ast.computed) { signature.push('[]'); } else if (ast.type === 'ThisExpression') { signature.unshift('this'); } else if (ast.property && ast.property.name) { if ( ast.property.name === 'x' || ast.property.name === 'y' || ast.property.name === 'z' ) { signature.unshift(returnRawValue ? '.' + ast.property.name : '.value'); } else if ( ast.property.name === 'constants' || ast.property.name === 'thread' || ast.property.name === 'output' ) { signature.unshift('.' + ast.property.name); } else { signature.unshift(returnRawValue ? '.' + ast.property.name : '.value'); } } else if (ast.name) { signature.unshift(returnRawValue ? ast.name : 'value'); } else if (ast.callee && ast.callee.name) { signature.unshift(returnRawValue ? ast.callee.name + '()' : 'fn()'); } else if (ast.elements) { signature.unshift('[]'); } else { signature.unshift('unknown'); } ast = ast.object; } const signatureString = signature.join(''); if (returnRawValue) { return signatureString; } const allowedExpressions = [ 'value', 'value[]', 'value[][]', 'value[][][]', 'value[][][][]', 'value.value', 'value.thread.value', 'this.thread.value', 'this.output.value', 'this.constants.value', 'this.constants.value[]', 'this.constants.value[][]', 'this.constants.value[][][]', 'this.constants.value[][][][]', 'fn()[]', 'fn()[][]', 'fn()[][][]', '[][]', ]; if (allowedExpressions.indexOf(signatureString) > -1) { return signatureString; } return null; } build() { return this.toString().length > 0; } /** * @desc Parses the abstract syntax tree for generically to its respective function * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the parsed string array */ astGeneric(ast, retArr) { if (ast === null) { throw this.astErrorOutput('NULL ast', ast); } else { if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { this.astGeneric(ast[i], retArr); } return retArr; } switch (ast.type) { case 'FunctionDeclaration': return this.astFunctionDeclaration(ast, retArr); case 'FunctionExpression': return this.astFunctionExpression(ast, retArr); case 'ReturnStatement': return this.astReturnStatement(ast, retArr); case 'Literal': return this.astLiteral(ast, retArr); case 'BinaryExpression': return this.astBinaryExpression(ast, retArr); case 'Identifier': return this.astIdentifierExpression(ast, retArr); case 'AssignmentExpression': return this.astAssignmentExpression(ast, retArr); case 'ExpressionStatement': return this.astExpressionStatement(ast, retArr); case 'EmptyStatement': return this.astEmptyStatement(ast, retArr); case 'BlockStatement': return this.astBlockStatement(ast, retArr); case 'IfStatement': return this.astIfStatement(ast, retArr); case 'SwitchStatement': return this.astSwitchStatement(ast, retArr); case 'BreakStatement': return this.astBreakStatement(ast, retArr); case 'ContinueStatement': return this.astContinueStatement(ast, retArr); case 'ForStatement': return this.astForStatement(ast, retArr); case 'WhileStatement': return this.astWhileStatement(ast, retArr); case 'DoWhileStatement': return this.astDoWhileStatement(ast, retArr); case 'VariableDeclaration': return this.astVariableDeclaration(ast, retArr); case 'VariableDeclarator': return this.astVariableDeclarator(ast, retArr); case 'ThisExpression': return this.astThisExpression(ast, retArr); case 'SequenceExpression': return this.astSequenceExpression(ast, retArr); case 'UnaryExpression': return this.astUnaryExpression(ast, retArr); case 'UpdateExpression': return this.astUpdateExpression(ast, retArr); case 'LogicalExpression': return this.astLogicalExpression(ast, retArr); case 'MemberExpression': return this.astMemberExpression(ast, retArr); case 'CallExpression': return this.astCallExpression(ast, retArr); case 'ArrayExpression': return this.astArrayExpression(ast, retArr); case 'DebuggerStatement': return this.astDebuggerStatement(ast, retArr); case 'ConditionalExpression': return this.astConditionalExpression(ast, retArr); } throw this.astErrorOutput('Unknown ast type : ' + ast.type, ast); } } /** * @desc To throw the AST error, with its location. * @param {string} error - the error message output * @param {Object} ast - the AST object where the error is */ astErrorOutput(error, ast) { if (typeof this.source !== 'string') { return new Error(error); } const debugString = utils.getAstString(this.source, ast); const leadingSource = this.source.substr(ast.start); const splitLines = leadingSource.split(/\n/); const lineBefore = splitLines.length > 0 ? splitLines[splitLines.length - 1] : 0; return new Error(`${error} on line ${ splitLines.length }, position ${ lineBefore.length }:\n ${ debugString }`); } astDebuggerStatement(arrNode, retArr) { return retArr; } astConditionalExpression(ast, retArr) { if (ast.type !== 'ConditionalExpression') { throw this.astErrorOutput('Not a conditional expression', ast); } retArr.push('('); this.astGeneric(ast.test, retArr); retArr.push('?'); this.astGeneric(ast.consequent, retArr); retArr.push(':'); this.astGeneric(ast.alternate, retArr); retArr.push(')'); return retArr; } /** * @abstract * @param {Object} ast * @param {String[]} retArr * @returns {String[]} */ astFunction(ast, retArr) { throw new Error(`"astFunction" not defined on ${ this.constructor.name }`); } /** * @desc Parses the abstract syntax tree for to its *named function declaration* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astFunctionDeclaration(ast, retArr) { if (this.isChildFunction(ast)) { return retArr; } return this.astFunction(ast, retArr); } astFunctionExpression(ast, retArr) { if (this.isChildFunction(ast)) { return retArr; } return this.astFunction(ast, retArr); } isChildFunction(ast) { for (let i = 0; i < this.functions.length; i++) { if (this.functions[i] === ast) { return true; } } return false; } astReturnStatement(ast, retArr) { return retArr; } astLiteral(ast, retArr) { this.literalTypes[this.astKey(ast)] = 'Number'; return retArr; } astBinaryExpression(ast, retArr) { return retArr; } astIdentifierExpression(ast, retArr) { return retArr; } astAssignmentExpression(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *generic expression* statement * @param {Object} esNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astExpressionStatement(esNode, retArr) { this.astGeneric(esNode.expression, retArr); retArr.push(';'); return retArr; } /** * @desc Parses the abstract syntax tree for an *Empty* Statement * @param {Object} eNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astEmptyStatement(eNode, retArr) { return retArr; } astBlockStatement(ast, retArr) { return retArr; } astIfStatement(ast, retArr) { return retArr; } astSwitchStatement(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *Break* Statement * @param {Object} brNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBreakStatement(brNode, retArr) { retArr.push('break;'); return retArr; } /** * @desc Parses the abstract syntax tree for *Continue* Statement * @param {Object} crNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astContinueStatement(crNode, retArr) { retArr.push('continue;\n'); return retArr; } astForStatement(ast, retArr) { return retArr; } astWhileStatement(ast, retArr) { return retArr; } astDoWhileStatement(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *Variable Declarator* * @param {Object} iVarDecNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astVariableDeclarator(iVarDecNode, retArr) { this.astGeneric(iVarDecNode.id, retArr); if (iVarDecNode.init !== null) { retArr.push('='); this.astGeneric(iVarDecNode.init, retArr); } return retArr; } astThisExpression(ast, retArr) { return retArr; } astSequenceExpression(sNode, retArr) { const { expressions } = sNode; const sequenceResult = []; for (let i = 0; i < expressions.length; i++) { const expression = expressions[i]; const expressionResult = []; this.astGeneric(expression, expressionResult); sequenceResult.push(expressionResult.join('')); } if (sequenceResult.length > 1) { retArr.push('(', sequenceResult.join(','), ')'); } else { retArr.push(sequenceResult[0]); } return retArr; } /** * @desc Parses the abstract syntax tree for *Unary* Expression * @param {Object} uNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astUnaryExpression(uNode, retArr) { const unaryResult = this.checkAndUpconvertBitwiseUnary(uNode, retArr); if (unaryResult) { return retArr; } if (uNode.prefix) { retArr.push(uNode.operator); this.astGeneric(uNode.argument, retArr); } else { this.astGeneric(uNode.argument, retArr); retArr.push(uNode.operator); } return retArr; } checkAndUpconvertBitwiseUnary(uNode, retArr) {} /** * @desc Parses the abstract syntax tree for *Update* Expression * @param {Object} uNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astUpdateExpression(uNode, retArr) { if (uNode.prefix) { retArr.push(uNode.operator); this.astGeneric(uNode.argument, retArr); } else { this.astGeneric(uNode.argument, retArr); retArr.push(uNode.operator); } return retArr; } /** * @desc Parses the abstract syntax tree for *Logical* Expression * @param {Object} logNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astLogicalExpression(logNode, retArr) { retArr.push('('); this.astGeneric(logNode.left, retArr); retArr.push(logNode.operator); this.astGeneric(logNode.right, retArr); retArr.push(')'); return retArr; } astMemberExpression(ast, retArr) { return retArr; } astCallExpression(ast, retArr) { return retArr; } astArrayExpression(ast, retArr) { return retArr; } /** * * @param ast * @return {IFunctionNodeMemberExpressionDetails} */ getMemberExpressionDetails(ast) { if (ast.type !== 'MemberExpression') { throw this.astErrorOutput(`Expression ${ ast.type } not a MemberExpression`, ast); } let name = null; let type = null; const variableSignature = this.getVariableSignature(ast); switch (variableSignature) { case 'value': return null; case 'value.thread.value': case 'this.thread.value': case 'this.output.value': return { signature: variableSignature, type: 'Integer', name: ast.property.name }; case 'value[]': if (typeof ast.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object), xProperty: ast.property }; case 'value[][]': if (typeof ast.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object), yProperty: ast.object.property, xProperty: ast.property, }; case 'value[][][]': if (typeof ast.object.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object.object), zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; case 'value[][][][]': if (typeof ast.object.object.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object.object.object), zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; case 'value.value': if (typeof ast.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } if (this.isAstMathVariable(ast)) { name = ast.property.name; return { name, origin: 'Math', type: 'Number', signature: variableSignature, }; } switch (ast.property.name) { case 'r': case 'g': case 'b': case 'a': name = ast.object.name; return { name, property: ast.property.name, origin: 'user', signature: variableSignature, type: 'Number' }; default: throw this.astErrorOutput('Unexpected expression', ast); } case 'this.constants.value': if (typeof ast.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, }; case 'this.constants.value[]': if (typeof ast.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, xProperty: ast.property, }; case 'this.constants.value[][]': { if (typeof ast.object.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, yProperty: ast.object.property, xProperty: ast.property, }; } case 'this.constants.value[][][]': { if (typeof ast.object.object.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; } case 'fn()[]': case 'fn()[][]': case '[][]': return { signature: variableSignature, property: ast.property, }; default: throw this.astErrorOutput('Unexpected expression', ast); } } findIdentifierOrigin(astToFind) { const stack = [this.ast]; while (stack.length > 0) { const atNode = stack[0]; if (atNode.type === 'VariableDeclarator' && atNode.id && atNode.id.name && atNode.id.name === astToFind.name) { return atNode; } stack.shift(); if (atNode.argument) { stack.push(atNode.argument); } else if (atNode.body) { stack.push(atNode.body); } else if (atNode.declarations) { stack.push(atNode.declarations); } else if (Array.isArray(atNode)) { for (let i = 0; i < atNode.length; i++) { stack.push(atNode[i]); } } } return null; } findLastReturn(ast) { const stack = [ast || this.ast]; while (stack.length > 0) { const atNode = stack.pop(); if (atNode.type === 'ReturnStatement') { return atNode; } if (atNode.type === 'FunctionDeclaration') { continue; } if (atNode.argument) { stack.push(atNode.argument); } else if (atNode.body) { stack.push(atNode.body); } else if (atNode.declarations) { stack.push(atNode.declarations); } else if (Array.isArray(atNode)) { for (let i = 0; i < atNode.length; i++) { stack.push(atNode[i]); } } else if (atNode.consequent) { stack.push(atNode.consequent); } else if (atNode.cases) { stack.push(atNode.cases); } } return null; } getInternalVariableName(name) { if (!this._internalVariableNames.hasOwnProperty(name)) { this._internalVariableNames[name] = 0; } this._internalVariableNames[name]++; if (this._internalVariableNames[name] === 1) { return name; } return name + this._internalVariableNames[name]; } astKey(ast, separator = ',') { if (!ast.start || !ast.end) throw new Error('AST start and end needed'); return `${ast.start}${separator}${ast.end}`; } } const typeLookupMap = { 'Number': 'Number', 'Float': 'Float', 'Integer': 'Integer', 'Array': 'Number', 'Array(2)': 'Number', 'Array(3)': 'Number', 'Array(4)': 'Number', 'Matrix(2)': 'Number', 'Matrix(3)': 'Number', 'Matrix(4)': 'Number', 'Array2D': 'Number', 'Array3D': 'Number', 'Input': 'Number', 'HTMLCanvas': 'Array(4)', 'OffscreenCanvas': 'Array(4)', 'HTMLImage': 'Array(4)', 'ImageBitmap': 'Array(4)', 'ImageData': 'Array(4)', 'HTMLVideo': 'Array(4)', 'HTMLImageArray': 'Array(4)', 'NumberTexture': 'Number', 'MemoryOptimizedNumberTexture': 'Number', 'Array1D(2)': 'Array(2)', 'Array1D(3)': 'Array(3)', 'Array1D(4)': 'Array(4)', 'Array2D(2)': 'Array(2)', 'Array2D(3)': 'Array(3)', 'Array2D(4)': 'Array(4)', 'Array3D(2)': 'Array(2)', 'Array3D(3)': 'Array(3)', 'Array3D(4)': 'Array(4)', 'ArrayTexture(1)': 'Number', 'ArrayTexture(2)': 'Array(2)', 'ArrayTexture(3)': 'Array(3)', 'ArrayTexture(4)': 'Array(4)', }; module.exports = { FunctionNode }; ================================================ FILE: src/backend/function-tracer.js ================================================ const { utils } = require('../utils'); function last(array) { return array.length > 0 ? array[array.length - 1] : null; } const states = { trackIdentifiers: 'trackIdentifiers', memberExpression: 'memberExpression', inForLoopInit: 'inForLoopInit' }; class FunctionTracer { constructor(ast) { this.runningContexts = []; this.functionContexts = []; this.contexts = []; this.functionCalls = []; /** * * @type {IDeclaration[]} */ this.declarations = []; this.identifiers = []; this.functions = []; this.returnStatements = []; this.trackedIdentifiers = null; this.states = []; this.newFunctionContext(); this.scan(ast); } isState(state) { return this.states[this.states.length - 1] === state; } hasState(state) { return this.states.indexOf(state) > -1; } pushState(state) { this.states.push(state); } popState(state) { if (this.isState(state)) { this.states.pop(); } else { throw new Error(`Cannot pop the non-active state "${state}"`); } } get currentFunctionContext() { return last(this.functionContexts); } get currentContext() { return last(this.runningContexts); } newFunctionContext() { const newContext = { '@contextType': 'function' }; this.contexts.push(newContext); this.functionContexts.push(newContext); } newContext(run) { const newContext = Object.assign({ '@contextType': 'const/let' }, this.currentContext); this.contexts.push(newContext); this.runningContexts.push(newContext); run(); const { currentFunctionContext } = this; for (const p in currentFunctionContext) { if (!currentFunctionContext.hasOwnProperty(p) || newContext.hasOwnProperty(p)) continue; newContext[p] = currentFunctionContext[p]; } this.runningContexts.pop(); return newContext; } useFunctionContext(run) { const functionContext = last(this.functionContexts); this.runningContexts.push(functionContext); run(); this.runningContexts.pop(); } getIdentifiers(run) { const trackedIdentifiers = this.trackedIdentifiers = []; this.pushState(states.trackIdentifiers); run(); this.trackedIdentifiers = null; this.popState(states.trackIdentifiers); return trackedIdentifiers; } /** * @param {string} name * @returns {IDeclaration} */ getDeclaration(name) { const { currentContext, currentFunctionContext, runningContexts } = this; const declaration = currentContext[name] || currentFunctionContext[name] || null; if ( !declaration && currentContext === currentFunctionContext && runningContexts.length > 0 ) { const previousRunningContext = runningContexts[runningContexts.length - 2]; if (previousRunningContext[name]) { return previousRunningContext[name]; } } return declaration; } /** * Recursively scans AST for declarations and functions, and add them to their respective context * @param ast */ scan(ast) { if (!ast) return; if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { this.scan(ast[i]); } return; } switch (ast.type) { case 'Program': this.useFunctionContext(() => { this.scan(ast.body); }); break; case 'BlockStatement': this.newContext(() => { this.scan(ast.body); }); break; case 'AssignmentExpression': case 'LogicalExpression': this.scan(ast.left); this.scan(ast.right); break; case 'BinaryExpression': this.scan(ast.left); this.scan(ast.right); break; case 'UpdateExpression': if (ast.operator === '++') { const declaration = this.getDeclaration(ast.argument.name); if (declaration) { declaration.suggestedType = 'Integer'; } } this.scan(ast.argument); break; case 'UnaryExpression': this.scan(ast.argument); break; case 'VariableDeclaration': if (ast.kind === 'var') { this.useFunctionContext(() => { ast.declarations = utils.normalizeDeclarations(ast); this.scan(ast.declarations); }); } else { ast.declarations = utils.normalizeDeclarations(ast); this.scan(ast.declarations); } break; case 'VariableDeclarator': { const { currentContext } = this; const inForLoopInit = this.hasState(states.inForLoopInit); const declaration = { ast: ast, context: currentContext, name: ast.id.name, origin: 'declaration', inForLoopInit, inForLoopTest: null, assignable: currentContext === this.currentFunctionContext || (!inForLoopInit && !currentContext.hasOwnProperty(ast.id.name)), suggestedType: null, valueType: null, dependencies: null, isSafe: null, }; if (!currentContext[ast.id.name]) { currentContext[ast.id.name] = declaration; } this.declarations.push(declaration); this.scan(ast.id); this.scan(ast.init); break; } case 'FunctionExpression': case 'FunctionDeclaration': if (this.runningContexts.length === 0) { this.scan(ast.body); } else { this.functions.push(ast); } break; case 'IfStatement': this.scan(ast.test); this.scan(ast.consequent); if (ast.alternate) this.scan(ast.alternate); break; case 'ForStatement': { let testIdentifiers; const context = this.newContext(() => { this.pushState(states.inForLoopInit); this.scan(ast.init); this.popState(states.inForLoopInit); testIdentifiers = this.getIdentifiers(() => { this.scan(ast.test); }); this.scan(ast.update); this.newContext(() => { this.scan(ast.body); }); }); if (testIdentifiers) { for (const p in context) { if (p === '@contextType') continue; if (testIdentifiers.indexOf(p) > -1) { context[p].inForLoopTest = true; } } } break; } case 'DoWhileStatement': case 'WhileStatement': this.newContext(() => { this.scan(ast.body); this.scan(ast.test); }); break; case 'Identifier': { if (this.isState(states.trackIdentifiers)) { this.trackedIdentifiers.push(ast.name); } this.identifiers.push({ context: this.currentContext, declaration: this.getDeclaration(ast.name), ast, }); break; } case 'ReturnStatement': this.returnStatements.push(ast); this.scan(ast.argument); break; case 'MemberExpression': this.pushState(states.memberExpression); this.scan(ast.object); this.scan(ast.property); this.popState(states.memberExpression); break; case 'ExpressionStatement': this.scan(ast.expression); break; case 'SequenceExpression': this.scan(ast.expressions); break; case 'CallExpression': this.functionCalls.push({ context: this.currentContext, ast, }); this.scan(ast.arguments); break; case 'ArrayExpression': this.scan(ast.elements); break; case 'ConditionalExpression': this.scan(ast.test); this.scan(ast.alternate); this.scan(ast.consequent); break; case 'SwitchStatement': this.scan(ast.discriminant); this.scan(ast.cases); break; case 'SwitchCase': this.scan(ast.test); this.scan(ast.consequent); break; case 'ThisExpression': case 'Literal': case 'DebuggerStatement': case 'EmptyStatement': case 'BreakStatement': case 'ContinueStatement': break; default: throw new Error(`unhandled type "${ast.type}"`); } } } module.exports = { FunctionTracer, }; ================================================ FILE: src/backend/gl/kernel-string.js ================================================ const { glWiretap } = require('gl-wiretap'); const { utils } = require('../../utils'); function toStringWithoutUtils(fn) { return fn.toString() .replace('=>', '') .replace(/^function /, '') .replace(/utils[.]/g, '/*utils.*/'); } /** * * @param {GLKernel} Kernel * @param {KernelVariable[]} args * @param {Kernel} originKernel * @param {string} [setupContextString] * @param {string} [destroyContextString] * @returns {string} */ function glKernelString(Kernel, args, originKernel, setupContextString, destroyContextString) { if (!originKernel.built) { originKernel.build.apply(originKernel, args); } args = args ? Array.from(args).map(arg => { switch (typeof arg) { case 'boolean': return new Boolean(arg); case 'number': return new Number(arg); default: return arg; } }) : null; const uploadedValues = []; const postResult = []; const context = glWiretap(originKernel.context, { useTrackablePrimitives: true, onReadPixels: (targetName) => { if (kernel.subKernels) { if (!subKernelsResultVariableSetup) { postResult.push(` const result = { result: ${getRenderString(targetName, kernel)} };`); subKernelsResultVariableSetup = true; } else { const property = kernel.subKernels[subKernelsResultIndex++].property; postResult.push(` result${isNaN(property) ? '.' + property : `[${property}]`} = ${getRenderString(targetName, kernel)};`); } if (subKernelsResultIndex === kernel.subKernels.length) { postResult.push(' return result;'); } return; } if (targetName) { postResult.push(` return ${getRenderString(targetName, kernel)};`); } else { postResult.push(` return null;`); } }, onUnrecognizedArgumentLookup: (argument) => { const argumentName = findKernelValue(argument, kernel.kernelArguments, [], context, uploadedValues); if (argumentName) { return argumentName; } const constantName = findKernelValue(argument, kernel.kernelConstants, constants ? Object.keys(constants).map(key => constants[key]) : [], context, uploadedValues); if (constantName) { return constantName; } return null; } }); let subKernelsResultVariableSetup = false; let subKernelsResultIndex = 0; const { source, canvas, output, pipeline, graphical, loopMaxIterations, constants, optimizeFloatMemory, precision, fixIntegerDivisionAccuracy, functions, nativeFunctions, subKernels, immutable, argumentTypes, constantTypes, kernelArguments, kernelConstants, tactic, } = originKernel; const kernel = new Kernel(source, { canvas, context, checkContext: false, output, pipeline, graphical, loopMaxIterations, constants, optimizeFloatMemory, precision, fixIntegerDivisionAccuracy, functions, nativeFunctions, subKernels, immutable, argumentTypes, constantTypes, tactic, }); let result = []; context.setIndent(2); kernel.build.apply(kernel, args); result.push(context.toString()); context.reset(); kernel.kernelArguments.forEach((kernelArgument, i) => { switch (kernelArgument.type) { // primitives case 'Integer': case 'Boolean': case 'Number': case 'Float': // non-primitives case 'Array': case 'Array(2)': case 'Array(3)': case 'Array(4)': case 'HTMLCanvas': case 'HTMLImage': case 'HTMLVideo': context.insertVariable(`uploadValue_${kernelArgument.name}`, kernelArgument.uploadValue); break; case 'HTMLImageArray': for (let imageIndex = 0; imageIndex < args[i].length; imageIndex++) { const arg = args[i]; context.insertVariable(`uploadValue_${kernelArgument.name}[${imageIndex}]`, arg[imageIndex]); } break; case 'Input': context.insertVariable(`uploadValue_${kernelArgument.name}`, kernelArgument.uploadValue); break; case 'MemoryOptimizedNumberTexture': case 'NumberTexture': case 'Array1D(2)': case 'Array1D(3)': case 'Array1D(4)': case 'Array2D(2)': case 'Array2D(3)': case 'Array2D(4)': case 'Array3D(2)': case 'Array3D(3)': case 'Array3D(4)': case 'ArrayTexture(1)': case 'ArrayTexture(2)': case 'ArrayTexture(3)': case 'ArrayTexture(4)': context.insertVariable(`uploadValue_${kernelArgument.name}`, args[i].texture); break; default: throw new Error(`unhandled kernelArgumentType insertion for glWiretap of type ${kernelArgument.type}`); } }); result.push('/** start of injected functions **/'); result.push(`function ${toStringWithoutUtils(utils.flattenTo)}`); result.push(`function ${toStringWithoutUtils(utils.flatten2dArrayTo)}`); result.push(`function ${toStringWithoutUtils(utils.flatten3dArrayTo)}`); result.push(`function ${toStringWithoutUtils(utils.flatten4dArrayTo)}`); result.push(`function ${toStringWithoutUtils(utils.isArray)}`); if (kernel.renderOutput !== kernel.renderTexture && kernel.formatValues) { result.push( ` const renderOutput = function ${toStringWithoutUtils(kernel.formatValues)};` ); } result.push('/** end of injected functions **/'); result.push(` const innerKernel = function (${kernel.kernelArguments.map(kernelArgument => kernelArgument.varName).join(', ')}) {`); context.setIndent(4); kernel.run.apply(kernel, args); if (kernel.renderKernels) { kernel.renderKernels(); } else if (kernel.renderOutput) { kernel.renderOutput(); } result.push(' /** start setup uploads for kernel values **/'); kernel.kernelArguments.forEach(kernelArgument => { result.push(' ' + kernelArgument.getStringValueHandler().split('\n').join('\n ')); }); result.push(' /** end setup uploads for kernel values **/'); result.push(context.toString()); if (kernel.renderOutput === kernel.renderTexture) { context.reset(); const framebufferName = context.getContextVariableName(kernel.framebuffer); if (kernel.renderKernels) { const results = kernel.renderKernels(); const textureName = context.getContextVariableName(kernel.texture.texture); result.push(` return { result: { texture: ${ textureName }, type: '${ results.result.type }', toArray: ${ getToArrayString(results.result, textureName, framebufferName) } },`); const { subKernels, mappedTextures } = kernel; for (let i = 0; i < subKernels.length; i++) { const texture = mappedTextures[i]; const subKernel = subKernels[i]; const subKernelResult = results[subKernel.property]; const subKernelTextureName = context.getContextVariableName(texture.texture); result.push(` ${subKernel.property}: { texture: ${ subKernelTextureName }, type: '${ subKernelResult.type }', toArray: ${ getToArrayString(subKernelResult, subKernelTextureName, framebufferName) } },`); } result.push(` };`); } else { const rendered = kernel.renderOutput(); const textureName = context.getContextVariableName(kernel.texture.texture); result.push(` return { texture: ${ textureName }, type: '${ rendered.type }', toArray: ${ getToArrayString(rendered, textureName, framebufferName) } };`); } } result.push(` ${destroyContextString ? '\n' + destroyContextString + ' ': ''}`); result.push(postResult.join('\n')); result.push(' };'); if (kernel.graphical) { result.push(getGetPixelsString(kernel)); result.push(` innerKernel.getPixels = getPixels;`); } result.push(' return innerKernel;'); let constantsUpload = []; kernelConstants.forEach((kernelConstant) => { constantsUpload.push(`${kernelConstant.getStringValueHandler()}`); }); return `function kernel(settings) { const { context, constants } = settings; ${constantsUpload.join('')} ${setupContextString ? setupContextString : ''} ${result.join('\n')} }`; } function getRenderString(targetName, kernel) { const readBackValue = kernel.precision === 'single' ? targetName : `new Float32Array(${targetName}.buffer)`; if (kernel.output[2]) { return `renderOutput(${readBackValue}, ${kernel.output[0]}, ${kernel.output[1]}, ${kernel.output[2]})`; } if (kernel.output[1]) { return `renderOutput(${readBackValue}, ${kernel.output[0]}, ${kernel.output[1]})`; } return `renderOutput(${readBackValue}, ${kernel.output[0]})`; } function getGetPixelsString(kernel) { const getPixels = kernel.getPixels.toString(); const useFunctionKeyword = !/^function/.test(getPixels); return utils.flattenFunctionToString(`${useFunctionKeyword ? 'function ' : ''}${ getPixels }`, { findDependency: (object, name) => { if (object === 'utils') { return `const ${name} = ${utils[name].toString()};`; } return null; }, thisLookup: (property) => { if (property === 'context') { return null; } if (kernel.hasOwnProperty(property)) { return JSON.stringify(kernel[property]); } throw new Error(`unhandled thisLookup ${ property }`); } }); } function getToArrayString(kernelResult, textureName, framebufferName) { const toArray = kernelResult.toArray.toString(); const useFunctionKeyword = !/^function/.test(toArray); const flattenedFunctions = utils.flattenFunctionToString(`${useFunctionKeyword ? 'function ' : ''}${ toArray }`, { findDependency: (object, name) => { if (object === 'utils') { return `const ${name} = ${utils[name].toString()};`; } else if (object === 'this') { if (name === 'framebuffer') { return ''; } return `${useFunctionKeyword ? 'function ' : ''}${kernelResult[name].toString()}`; } else { throw new Error('unhandled fromObject'); } }, thisLookup: (property, isDeclaration) => { if (property === 'texture') { return textureName; } if (property === 'context') { if (isDeclaration) return null; return 'gl'; } if (kernelResult.hasOwnProperty(property)) { return JSON.stringify(kernelResult[property]); } throw new Error(`unhandled thisLookup ${ property }`); } }); return `() => { function framebuffer() { return ${framebufferName}; }; ${flattenedFunctions} return toArray(); }`; } /** * * @param {KernelVariable} argument * @param {KernelValue[]} kernelValues * @param {KernelVariable[]} values * @param context * @param {KernelVariable[]} uploadedValues * @return {string|null} */ function findKernelValue(argument, kernelValues, values, context, uploadedValues) { if (argument === null) return null; if (kernelValues === null) return null; switch (typeof argument) { case 'boolean': case 'number': return null; } if ( typeof HTMLImageElement !== 'undefined' && argument instanceof HTMLImageElement ) { for (let i = 0; i < kernelValues.length; i++) { const kernelValue = kernelValues[i]; if (kernelValue.type !== 'HTMLImageArray' && kernelValue) continue; if (kernelValue.uploadValue !== argument) continue; // TODO: if we send two of the same image, the parser could get confused, and short circuit to the first, handle that here const variableIndex = values[i].indexOf(argument); if (variableIndex === -1) continue; const variableName = `uploadValue_${kernelValue.name}[${variableIndex}]`; context.insertVariable(variableName, argument); return variableName; } } for (let i = 0; i < kernelValues.length; i++) { const kernelValue = kernelValues[i]; if (argument !== kernelValue.uploadValue) continue; const variable = `uploadValue_${kernelValue.name}`; context.insertVariable(variable, kernelValue); return variable; } return null; } module.exports = { glKernelString }; ================================================ FILE: src/backend/gl/kernel.js ================================================ const { Kernel } = require('../kernel'); const { utils } = require('../../utils'); const { GLTextureArray2Float } = require('./texture/array-2-float'); const { GLTextureArray2Float2D } = require('./texture/array-2-float-2d'); const { GLTextureArray2Float3D } = require('./texture/array-2-float-3d'); const { GLTextureArray3Float } = require('./texture/array-3-float'); const { GLTextureArray3Float2D } = require('./texture/array-3-float-2d'); const { GLTextureArray3Float3D } = require('./texture/array-3-float-3d'); const { GLTextureArray4Float } = require('./texture/array-4-float'); const { GLTextureArray4Float2D } = require('./texture/array-4-float-2d'); const { GLTextureArray4Float3D } = require('./texture/array-4-float-3d'); const { GLTextureFloat } = require('./texture/float'); const { GLTextureFloat2D } = require('./texture/float-2d'); const { GLTextureFloat3D } = require('./texture/float-3d'); const { GLTextureMemoryOptimized } = require('./texture/memory-optimized'); const { GLTextureMemoryOptimized2D } = require('./texture/memory-optimized-2d'); const { GLTextureMemoryOptimized3D } = require('./texture/memory-optimized-3d'); const { GLTextureUnsigned } = require('./texture/unsigned'); const { GLTextureUnsigned2D } = require('./texture/unsigned-2d'); const { GLTextureUnsigned3D } = require('./texture/unsigned-3d'); const { GLTextureGraphical } = require('./texture/graphical'); /** * @abstract * @extends Kernel */ class GLKernel extends Kernel { static get mode() { return 'gpu'; } static getIsFloatRead() { const kernelString = `function kernelFunction() { return 1; }`; const kernel = new this(kernelString, { context: this.testContext, canvas: this.testCanvas, validate: false, output: [1], precision: 'single', returnType: 'Number', tactic: 'speed', }); kernel.build(); kernel.run(); const result = kernel.renderOutput(); kernel.destroy(true); return result[0] === 1; } static getIsIntegerDivisionAccurate() { function kernelFunction(v1, v2) { return v1[this.thread.x] / v2[this.thread.x]; } const kernel = new this(kernelFunction.toString(), { context: this.testContext, canvas: this.testCanvas, validate: false, output: [2], returnType: 'Number', precision: 'unsigned', tactic: 'speed', }); const args = [ [6, 6030401], [3, 3991] ]; kernel.build.apply(kernel, args); kernel.run.apply(kernel, args); const result = kernel.renderOutput(); kernel.destroy(true); // have we not got whole numbers for 6/3 or 6030401/3991 // add more here if others see this problem return result[0] === 2 && result[1] === 1511; } static getIsSpeedTacticSupported() { function kernelFunction(value) { return value[this.thread.x]; } const kernel = new this(kernelFunction.toString(), { context: this.testContext, canvas: this.testCanvas, validate: false, output: [4], returnType: 'Number', precision: 'unsigned', tactic: 'speed', }); const args = [ [0, 1, 2, 3] ]; kernel.build.apply(kernel, args); kernel.run.apply(kernel, args); const result = kernel.renderOutput(); kernel.destroy(true); return Math.round(result[0]) === 0 && Math.round(result[1]) === 1 && Math.round(result[2]) === 2 && Math.round(result[3]) === 3; } /** * @abstract */ static get testCanvas() { throw new Error(`"testCanvas" not defined on ${ this.name }`); } /** * @abstract */ static get testContext() { throw new Error(`"testContext" not defined on ${ this.name }`); } static getFeatures() { const gl = this.testContext; const isDrawBuffers = this.getIsDrawBuffers(); return Object.freeze({ isFloatRead: this.getIsFloatRead(), isIntegerDivisionAccurate: this.getIsIntegerDivisionAccurate(), isSpeedTacticSupported: this.getIsSpeedTacticSupported(), isTextureFloat: this.getIsTextureFloat(), isDrawBuffers, kernelMap: isDrawBuffers, channelCount: this.getChannelCount(), maxTextureSize: this.getMaxTextureSize(), lowIntPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT), lowFloatPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT), mediumIntPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT), mediumFloatPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT), highIntPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT), highFloatPrecision: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT), }); } /** * @abstract */ static setupFeatureChecks() { throw new Error(`"setupFeatureChecks" not defined on ${ this.name }`); } static getSignature(kernel, argumentTypes) { return kernel.getVariablePrecisionString() + (argumentTypes.length > 0 ? ':' + argumentTypes.join(',') : ''); } /** * @desc Fix division by factor of 3 FP accuracy bug * @param {Boolean} fix - should fix */ setFixIntegerDivisionAccuracy(fix) { this.fixIntegerDivisionAccuracy = fix; return this; } /** * @desc Toggle output mode * @param {String} flag - 'single' or 'unsigned' */ setPrecision(flag) { this.precision = flag; return this; } /** * @desc Toggle texture output mode * @param {Boolean} flag - true to enable floatTextures * @deprecated */ setFloatTextures(flag) { utils.warnDeprecated('method', 'setFloatTextures', 'setOptimizeFloatMemory'); this.floatTextures = flag; return this; } /** * A highly readable very forgiving micro-parser for a glsl function that gets argument types * @param {String} source * @returns {{argumentTypes: String[], argumentNames: String[]}} */ static nativeFunctionArguments(source) { const argumentTypes = []; const argumentNames = []; const states = []; const isStartingVariableName = /^[a-zA-Z_]/; const isVariableChar = /[a-zA-Z_0-9]/; let i = 0; let argumentName = null; let argumentType = null; while (i < source.length) { const char = source[i]; const nextChar = source[i + 1]; const state = states.length > 0 ? states[states.length - 1] : null; // begin MULTI_LINE_COMMENT handling if (state === 'FUNCTION_ARGUMENTS' && char === '/' && nextChar === '*') { states.push('MULTI_LINE_COMMENT'); i += 2; continue; } else if (state === 'MULTI_LINE_COMMENT' && char === '*' && nextChar === '/') { states.pop(); i += 2; continue; } // end MULTI_LINE_COMMENT handling // begin COMMENT handling else if (state === 'FUNCTION_ARGUMENTS' && char === '/' && nextChar === '/') { states.push('COMMENT'); i += 2; continue; } else if (state === 'COMMENT' && char === '\n') { states.pop(); i++; continue; } // end COMMENT handling // being FUNCTION_ARGUMENTS handling else if (state === null && char === '(') { states.push('FUNCTION_ARGUMENTS'); i++; continue; } else if (state === 'FUNCTION_ARGUMENTS') { if (char === ')') { states.pop(); break; } if (char === 'f' && nextChar === 'l' && source[i + 2] === 'o' && source[i + 3] === 'a' && source[i + 4] === 't' && source[i + 5] === ' ') { states.push('DECLARE_VARIABLE'); argumentType = 'float'; argumentName = ''; i += 6; continue; } else if (char === 'i' && nextChar === 'n' && source[i + 2] === 't' && source[i + 3] === ' ') { states.push('DECLARE_VARIABLE'); argumentType = 'int'; argumentName = ''; i += 4; continue; } else if (char === 'v' && nextChar === 'e' && source[i + 2] === 'c' && source[i + 3] === '2' && source[i + 4] === ' ') { states.push('DECLARE_VARIABLE'); argumentType = 'vec2'; argumentName = ''; i += 5; continue; } else if (char === 'v' && nextChar === 'e' && source[i + 2] === 'c' && source[i + 3] === '3' && source[i + 4] === ' ') { states.push('DECLARE_VARIABLE'); argumentType = 'vec3'; argumentName = ''; i += 5; continue; } else if (char === 'v' && nextChar === 'e' && source[i + 2] === 'c' && source[i + 3] === '4' && source[i + 4] === ' ') { states.push('DECLARE_VARIABLE'); argumentType = 'vec4'; argumentName = ''; i += 5; continue; } } // end FUNCTION_ARGUMENTS handling // begin DECLARE_VARIABLE handling else if (state === 'DECLARE_VARIABLE') { if (argumentName === '') { if (char === ' ') { i++; continue; } if (!isStartingVariableName.test(char)) { throw new Error('variable name is not expected string'); } } argumentName += char; if (!isVariableChar.test(nextChar)) { states.pop(); argumentNames.push(argumentName); argumentTypes.push(typeMap[argumentType]); } } // end DECLARE_VARIABLE handling // Progress to next character i++; } if (states.length > 0) { throw new Error('GLSL function was not parsable'); } return { argumentNames, argumentTypes, }; } static nativeFunctionReturnType(source) { return typeMap[source.match(/int|float|vec[2-4]/)[0]]; } static combineKernels(combinedKernel, lastKernel) { combinedKernel.apply(null, arguments); const { texSize, context, threadDim } = lastKernel.texSize; let result; if (lastKernel.precision === 'single') { const w = texSize[0]; const h = Math.ceil(texSize[1] / 4); result = new Float32Array(w * h * 4 * 4); context.readPixels(0, 0, w, h * 4, context.RGBA, context.FLOAT, result); } else { const bytes = new Uint8Array(texSize[0] * texSize[1] * 4); context.readPixels(0, 0, texSize[0], texSize[1], context.RGBA, context.UNSIGNED_BYTE, bytes); result = new Float32Array(bytes.buffer); } result = result.subarray(0, threadDim[0] * threadDim[1] * threadDim[2]); if (lastKernel.output.length === 1) { return result; } else if (lastKernel.output.length === 2) { return utils.splitArray(result, lastKernel.output[0]); } else if (lastKernel.output.length === 3) { const cube = utils.splitArray(result, lastKernel.output[0] * lastKernel.output[1]); return cube.map(function(x) { return utils.splitArray(x, lastKernel.output[0]); }); } } constructor(source, settings) { super(source, settings); this.transferValues = null; this.formatValues = null; /** * * @type {Texture} */ this.TextureConstructor = null; this.renderOutput = null; this.renderRawOutput = null; this.texSize = null; this.translatedSource = null; this.compiledFragmentShader = null; this.compiledVertexShader = null; this.switchingKernels = null; this._textureSwitched = null; this._mappedTextureSwitched = null; } checkTextureSize() { const { features } = this.constructor; if (this.texSize[0] > features.maxTextureSize || this.texSize[1] > features.maxTextureSize) { throw new Error(`Texture size [${this.texSize[0]},${this.texSize[1]}] generated by kernel is larger than supported size [${features.maxTextureSize},${features.maxTextureSize}]`); } } translateSource() { throw new Error(`"translateSource" not defined on ${this.constructor.name}`); } /** * Picks a render strategy for the now finally parsed kernel * @param args * @return {null|KernelOutput} */ pickRenderStrategy(args) { if (this.graphical) { this.renderRawOutput = this.readPackedPixelsToUint8Array; this.transferValues = (pixels) => pixels; this.TextureConstructor = GLTextureGraphical; return null; } if (this.precision === 'unsigned') { this.renderRawOutput = this.readPackedPixelsToUint8Array; this.transferValues = this.readPackedPixelsToFloat32Array; if (this.pipeline) { this.renderOutput = this.renderTexture; if (this.subKernels !== null) { this.renderKernels = this.renderKernelsToTextures; } switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': if (this.output[2] > 0) { this.TextureConstructor = GLTextureUnsigned3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureUnsigned2D; return null; } else { this.TextureConstructor = GLTextureUnsigned; return null; } case 'Array(2)': case 'Array(3)': case 'Array(4)': return this.requestFallback(args); } } else { if (this.subKernels !== null) { this.renderKernels = this.renderKernelsToArrays; } switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': this.renderOutput = this.renderValues; if (this.output[2] > 0) { this.TextureConstructor = GLTextureUnsigned3D; this.formatValues = utils.erect3DPackedFloat; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureUnsigned2D; this.formatValues = utils.erect2DPackedFloat; return null; } else { this.TextureConstructor = GLTextureUnsigned; this.formatValues = utils.erectPackedFloat; return null; } case 'Array(2)': case 'Array(3)': case 'Array(4)': return this.requestFallback(args); } } } else if (this.precision === 'single') { this.renderRawOutput = this.readFloatPixelsToFloat32Array; this.transferValues = this.readFloatPixelsToFloat32Array; if (this.pipeline) { this.renderOutput = this.renderTexture; if (this.subKernels !== null) { this.renderKernels = this.renderKernelsToTextures; } switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': { if (this.optimizeFloatMemory) { if (this.output[2] > 0) { this.TextureConstructor = GLTextureMemoryOptimized3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureMemoryOptimized2D; return null; } else { this.TextureConstructor = GLTextureMemoryOptimized; return null; } } else { if (this.output[2] > 0) { this.TextureConstructor = GLTextureFloat3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureFloat2D; return null; } else { this.TextureConstructor = GLTextureFloat; return null; } } } case 'Array(2)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray2Float3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray2Float2D; return null; } else { this.TextureConstructor = GLTextureArray2Float; return null; } } case 'Array(3)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray3Float3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray3Float2D; return null; } else { this.TextureConstructor = GLTextureArray3Float; return null; } } case 'Array(4)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray4Float3D; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray4Float2D; return null; } else { this.TextureConstructor = GLTextureArray4Float; return null; } } } } this.renderOutput = this.renderValues; if (this.subKernels !== null) { this.renderKernels = this.renderKernelsToArrays; } if (this.optimizeFloatMemory) { switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureMemoryOptimized3D; this.formatValues = utils.erectMemoryOptimized3DFloat; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureMemoryOptimized2D; this.formatValues = utils.erectMemoryOptimized2DFloat; return null; } else { this.TextureConstructor = GLTextureMemoryOptimized; this.formatValues = utils.erectMemoryOptimizedFloat; return null; } } case 'Array(2)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray2Float3D; this.formatValues = utils.erect3DArray2; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray2Float2D; this.formatValues = utils.erect2DArray2; return null; } else { this.TextureConstructor = GLTextureArray2Float; this.formatValues = utils.erectArray2; return null; } } case 'Array(3)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray3Float3D; this.formatValues = utils.erect3DArray3; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray3Float2D; this.formatValues = utils.erect2DArray3; return null; } else { this.TextureConstructor = GLTextureArray3Float; this.formatValues = utils.erectArray3; return null; } } case 'Array(4)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray4Float3D; this.formatValues = utils.erect3DArray4; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray4Float2D; this.formatValues = utils.erect2DArray4; return null; } else { this.TextureConstructor = GLTextureArray4Float; this.formatValues = utils.erectArray4; return null; } } } } else { switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureFloat3D; this.formatValues = utils.erect3DFloat; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureFloat2D; this.formatValues = utils.erect2DFloat; return null; } else { this.TextureConstructor = GLTextureFloat; this.formatValues = utils.erectFloat; return null; } } case 'Array(2)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray2Float3D; this.formatValues = utils.erect3DArray2; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray2Float2D; this.formatValues = utils.erect2DArray2; return null; } else { this.TextureConstructor = GLTextureArray2Float; this.formatValues = utils.erectArray2; return null; } } case 'Array(3)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray3Float3D; this.formatValues = utils.erect3DArray3; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray3Float2D; this.formatValues = utils.erect2DArray3; return null; } else { this.TextureConstructor = GLTextureArray3Float; this.formatValues = utils.erectArray3; return null; } } case 'Array(4)': { if (this.output[2] > 0) { this.TextureConstructor = GLTextureArray4Float3D; this.formatValues = utils.erect3DArray4; return null; } else if (this.output[1] > 0) { this.TextureConstructor = GLTextureArray4Float2D; this.formatValues = utils.erect2DArray4; return null; } else { this.TextureConstructor = GLTextureArray4Float; this.formatValues = utils.erectArray4; return null; } } } } } else { throw new Error(`unhandled precision of "${this.precision}"`); } throw new Error(`unhandled return type "${this.returnType}"`); } /** * @abstract * @returns String */ getKernelString() { throw new Error(`abstract method call`); } getMainResultTexture() { switch (this.returnType) { case 'LiteralInteger': case 'Float': case 'Integer': case 'Number': return this.getMainResultNumberTexture(); case 'Array(2)': return this.getMainResultArray2Texture(); case 'Array(3)': return this.getMainResultArray3Texture(); case 'Array(4)': return this.getMainResultArray4Texture(); default: throw new Error(`unhandled returnType type ${ this.returnType }`); } } /** * @abstract * @returns String[] */ getMainResultKernelNumberTexture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultSubKernelNumberTexture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultKernelArray2Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultSubKernelArray2Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultKernelArray3Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultSubKernelArray3Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultKernelArray4Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultSubKernelArray4Texture() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultGraphical() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultMemoryOptimizedFloats() { throw new Error(`abstract method call`); } /** * @abstract * @returns String[] */ getMainResultPackedPixels() { throw new Error(`abstract method call`); } getMainResultString() { if (this.graphical) { return this.getMainResultGraphical(); } else if (this.precision === 'single') { if (this.optimizeFloatMemory) { return this.getMainResultMemoryOptimizedFloats(); } return this.getMainResultTexture(); } else { return this.getMainResultPackedPixels(); } } getMainResultNumberTexture() { return utils.linesToString(this.getMainResultKernelNumberTexture()) + utils.linesToString(this.getMainResultSubKernelNumberTexture()); } getMainResultArray2Texture() { return utils.linesToString(this.getMainResultKernelArray2Texture()) + utils.linesToString(this.getMainResultSubKernelArray2Texture()); } getMainResultArray3Texture() { return utils.linesToString(this.getMainResultKernelArray3Texture()) + utils.linesToString(this.getMainResultSubKernelArray3Texture()); } getMainResultArray4Texture() { return utils.linesToString(this.getMainResultKernelArray4Texture()) + utils.linesToString(this.getMainResultSubKernelArray4Texture()); } /** * * @return {string} */ getFloatTacticDeclaration() { const variablePrecision = this.getVariablePrecisionString(this.texSize, this.tactic); return `precision ${variablePrecision} float;\n`; } /** * * @return {string} */ getIntTacticDeclaration() { return `precision ${this.getVariablePrecisionString(this.texSize, this.tactic, true)} int;\n`; } /** * * @return {string} */ getSampler2DTacticDeclaration() { return `precision ${this.getVariablePrecisionString(this.texSize, this.tactic)} sampler2D;\n`; } getSampler2DArrayTacticDeclaration() { return `precision ${this.getVariablePrecisionString(this.texSize, this.tactic)} sampler2DArray;\n`; } renderTexture() { return this.immutable ? this.texture.clone() : this.texture; } readPackedPixelsToUint8Array() { if (this.precision !== 'unsigned') throw new Error('Requires this.precision to be "unsigned"'); const { texSize, context: gl } = this; const result = new Uint8Array(texSize[0] * texSize[1] * 4); gl.readPixels(0, 0, texSize[0], texSize[1], gl.RGBA, gl.UNSIGNED_BYTE, result); return result; } readPackedPixelsToFloat32Array() { return new Float32Array(this.readPackedPixelsToUint8Array().buffer); } readFloatPixelsToFloat32Array() { if (this.precision !== 'single') throw new Error('Requires this.precision to be "single"'); const { texSize, context: gl } = this; const w = texSize[0]; const h = texSize[1]; const result = new Float32Array(w * h * 4); gl.readPixels(0, 0, w, h, gl.RGBA, gl.FLOAT, result); return result; } /** * * @param {Boolean} [flip] * @return {Uint8ClampedArray} */ getPixels(flip) { const { context: gl, output } = this; const [width, height] = output; const pixels = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // flipped by default, so invert return new Uint8ClampedArray((flip ? pixels : utils.flipPixels(pixels, width, height)).buffer); } renderKernelsToArrays() { const result = { result: this.renderOutput(), }; for (let i = 0; i < this.subKernels.length; i++) { result[this.subKernels[i].property] = this.mappedTextures[i].toArray(); } return result; } renderKernelsToTextures() { const result = { result: this.renderOutput(), }; if (this.immutable) { for (let i = 0; i < this.subKernels.length; i++) { result[this.subKernels[i].property] = this.mappedTextures[i].clone(); } } else { for (let i = 0; i < this.subKernels.length; i++) { result[this.subKernels[i].property] = this.mappedTextures[i]; } } return result; } resetSwitchingKernels() { const existingValue = this.switchingKernels; this.switchingKernels = null; return existingValue; } setOutput(output) { const newOutput = this.toKernelOutput(output); if (this.program) { if (!this.dynamicOutput) { throw new Error('Resizing a kernel with dynamicOutput: false is not possible'); } const newThreadDim = [newOutput[0], newOutput[1] || 1, newOutput[2] || 1]; const newTexSize = utils.getKernelTextureSize({ optimizeFloatMemory: this.optimizeFloatMemory, precision: this.precision, }, newThreadDim); const oldTexSize = this.texSize; if (oldTexSize) { const oldPrecision = this.getVariablePrecisionString(oldTexSize, this.tactic); const newPrecision = this.getVariablePrecisionString(newTexSize, this.tactic); if (oldPrecision !== newPrecision) { if (this.debug) { console.warn('Precision requirement changed, asking GPU instance to recompile'); } this.switchKernels({ type: 'outputPrecisionMismatch', precision: newPrecision, needed: output }); return; } } this.output = newOutput; this.threadDim = newThreadDim; this.texSize = newTexSize; const { context: gl } = this; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); this.updateMaxTexSize(); this.framebuffer.width = this.texSize[0]; this.framebuffer.height = this.texSize[1]; gl.viewport(0, 0, this.maxTexSize[0], this.maxTexSize[1]); this.canvas.width = this.maxTexSize[0]; this.canvas.height = this.maxTexSize[1]; if (this.texture) { this.texture.delete(); } this.texture = null; this._setupOutputTexture(); if (this.mappedTextures && this.mappedTextures.length > 0) { for (let i = 0; i < this.mappedTextures.length; i++) { this.mappedTextures[i].delete(); } this.mappedTextures = null; this._setupSubOutputTextures(); } } else { this.output = newOutput; } return this; } renderValues() { return this.formatValues( this.transferValues(), this.output[0], this.output[1], this.output[2] ); } switchKernels(reason) { if (this.switchingKernels) { this.switchingKernels.push(reason); } else { this.switchingKernels = [reason]; } } getVariablePrecisionString(textureSize = this.texSize, tactic = this.tactic, isInt = false) { if (!tactic) { if (!this.constructor.features.isSpeedTacticSupported) return 'highp'; const low = this.constructor.features[isInt ? 'lowIntPrecision' : 'lowFloatPrecision']; const medium = this.constructor.features[isInt ? 'mediumIntPrecision' : 'mediumFloatPrecision']; const high = this.constructor.features[isInt ? 'highIntPrecision' : 'highFloatPrecision']; const requiredSize = Math.log2(textureSize[0] * textureSize[1]); if (requiredSize <= low.rangeMax) { return 'lowp'; } else if (requiredSize <= medium.rangeMax) { return 'mediump'; } else if (requiredSize <= high.rangeMax) { return 'highp'; } else { throw new Error(`The required size exceeds that of the ability of your system`); } } switch (tactic) { case 'speed': return 'lowp'; case 'balanced': return 'mediump'; case 'precision': return 'highp'; default: throw new Error(`Unknown tactic "${tactic}" use "speed", "balanced", "precision", or empty for auto`); } } /** * * @param {WebGLKernelValue} kernelValue * @param {GLTexture} arg */ updateTextureArgumentRefs(kernelValue, arg) { if (!this.immutable) return; if (this.texture.texture === arg.texture) { const { prevArg } = kernelValue; if (prevArg) { if (prevArg.texture._refs === 1) { this.texture.delete(); this.texture = prevArg.clone(); this._textureSwitched = true; } prevArg.delete(); } kernelValue.prevArg = arg.clone(); } else if (this.mappedTextures && this.mappedTextures.length > 0) { const { mappedTextures } = this; for (let i = 0; i < mappedTextures.length; i++) { const mappedTexture = mappedTextures[i]; if (mappedTexture.texture === arg.texture) { const { prevArg } = kernelValue; if (prevArg) { if (prevArg.texture._refs === 1) { mappedTexture.delete(); mappedTextures[i] = prevArg.clone(); this._mappedTextureSwitched[i] = true; } prevArg.delete(); } kernelValue.prevArg = arg.clone(); return; } } } } onActivate(previousKernel) { this._textureSwitched = true; this.texture = previousKernel.texture; if (this.mappedTextures) { for (let i = 0; i < this.mappedTextures.length; i++) { this._mappedTextureSwitched[i] = true; } this.mappedTextures = previousKernel.mappedTextures; } } initCanvas() {} } const typeMap = { int: 'Integer', float: 'Number', vec2: 'Array(2)', vec3: 'Array(3)', vec4: 'Array(4)', }; module.exports = { GLKernel }; ================================================ FILE: src/backend/gl/texture/array-2-float-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray2Float2D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(2)'; } toArray() { return utils.erect2DArray2(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureArray2Float2D }; ================================================ FILE: src/backend/gl/texture/array-2-float-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray2Float3D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(2)'; } toArray() { return utils.erect3DArray2(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureArray2Float3D }; ================================================ FILE: src/backend/gl/texture/array-2-float.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray2Float extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(2)'; } toArray() { return utils.erectArray2(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureArray2Float }; ================================================ FILE: src/backend/gl/texture/array-3-float-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray3Float2D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(3)'; } toArray() { return utils.erect2DArray3(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureArray3Float2D }; ================================================ FILE: src/backend/gl/texture/array-3-float-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray3Float3D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(3)'; } toArray() { return utils.erect3DArray3(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureArray3Float3D }; ================================================ FILE: src/backend/gl/texture/array-3-float.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray3Float extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(3)'; } toArray() { return utils.erectArray3(this.renderValues(), this.output[0]); } } module.exports = { GLTextureArray3Float }; ================================================ FILE: src/backend/gl/texture/array-4-float-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray4Float2D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(4)'; } toArray() { return utils.erect2DArray4(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureArray4Float2D }; ================================================ FILE: src/backend/gl/texture/array-4-float-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray4Float3D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(4)'; } toArray() { return utils.erect3DArray4(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureArray4Float3D }; ================================================ FILE: src/backend/gl/texture/array-4-float.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureArray4Float extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(4)'; } toArray() { return utils.erectArray4(this.renderValues(), this.output[0]); } } module.exports = { GLTextureArray4Float }; ================================================ FILE: src/backend/gl/texture/float-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureFloat2D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(1)'; } toArray() { return utils.erect2DFloat(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureFloat2D }; ================================================ FILE: src/backend/gl/texture/float-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureFloat3D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'ArrayTexture(1)'; } toArray() { return utils.erect3DFloat(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureFloat3D }; ================================================ FILE: src/backend/gl/texture/float.js ================================================ const { utils } = require('../../../utils'); const { GLTexture } = require('./index'); class GLTextureFloat extends GLTexture { get textureType() { return this.context.FLOAT; } constructor(settings) { super(settings); this.type = 'ArrayTexture(1)'; } renderRawOutput() { const gl = this.context; const size = this.size; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer()); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0 ); const result = new Float32Array(size[0] * size[1] * 4); gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.FLOAT, result); return result; } renderValues() { if (this._deleted) return null; return this.renderRawOutput(); } toArray() { return utils.erectFloat(this.renderValues(), this.output[0]); } } module.exports = { GLTextureFloat }; ================================================ FILE: src/backend/gl/texture/graphical.js ================================================ const { GLTextureUnsigned } = require('./unsigned'); class GLTextureGraphical extends GLTextureUnsigned { constructor(settings) { super(settings); this.type = 'ArrayTexture(4)'; } toArray() { return this.renderValues(); } } module.exports = { GLTextureGraphical }; ================================================ FILE: src/backend/gl/texture/index.js ================================================ const { Texture } = require('../../../texture'); /** * @class * @property framebuffer * @extends Texture */ class GLTexture extends Texture { /** * @returns {Number} * @abstract */ get textureType() { throw new Error(`"textureType" not implemented on ${ this.name }`); } clone() { return new this.constructor(this); } /** * @returns {Boolean} */ beforeMutate() { if (this.texture._refs > 1) { this.newTexture(); return true; } return false; } /** * @private */ cloneTexture() { this.texture._refs--; const { context: gl, size, texture, kernel } = this; if (kernel.debug) { console.warn('cloning internal texture'); } gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer()); selectTexture(gl, texture); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); const target = gl.createTexture(); selectTexture(gl, target); gl.texImage2D(gl.TEXTURE_2D, 0, this.internalFormat, size[0], size[1], 0, this.textureFormat, this.textureType, null); gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, size[0], size[1]); target._refs = 1; this.texture = target; } /** * @private */ newTexture() { this.texture._refs--; const gl = this.context; const size = this.size; const kernel = this.kernel; if (kernel.debug) { console.warn('new internal texture'); } const target = gl.createTexture(); selectTexture(gl, target); gl.texImage2D(gl.TEXTURE_2D, 0, this.internalFormat, size[0], size[1], 0, this.textureFormat, this.textureType, null); target._refs = 1; this.texture = target; } clear() { if (this.texture._refs) { this.texture._refs--; const gl = this.context; const target = this.texture = gl.createTexture(); selectTexture(gl, target); const size = this.size; target._refs = 1; gl.texImage2D(gl.TEXTURE_2D, 0, this.internalFormat, size[0], size[1], 0, this.textureFormat, this.textureType, null); } const { context: gl, texture } = this; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer()); gl.bindTexture(gl.TEXTURE_2D, texture); selectTexture(gl, texture); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } delete() { if (this._deleted) return; this._deleted = true; if (this.texture._refs) { this.texture._refs--; if (this.texture._refs) return; } this.context.deleteTexture(this.texture); // TODO: Remove me // if (this.texture._refs === 0 && this._framebuffer) { // this.context.deleteFramebuffer(this._framebuffer); // this._framebuffer = null; // } } framebuffer() { if (!this._framebuffer) { this._framebuffer = this.kernel.getRawValueFramebuffer(this.size[0], this.size[1]); } return this._framebuffer; } } function selectTexture(gl, texture) { /* Maximum a texture can be, so that collision is highly unlikely * basically gl.TEXTURE15 + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); * Was gl.TEXTURE31, but safari didn't like it * */ gl.activeTexture(gl.TEXTURE15); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); } module.exports = { GLTexture }; ================================================ FILE: src/backend/gl/texture/memory-optimized-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureMemoryOptimized2D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'MemoryOptimizedNumberTexture'; } toArray() { return utils.erectMemoryOptimized2DFloat(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureMemoryOptimized2D }; ================================================ FILE: src/backend/gl/texture/memory-optimized-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureMemoryOptimized3D extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'MemoryOptimizedNumberTexture'; } toArray() { return utils.erectMemoryOptimized3DFloat(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureMemoryOptimized3D }; ================================================ FILE: src/backend/gl/texture/memory-optimized.js ================================================ const { utils } = require('../../../utils'); const { GLTextureFloat } = require('./float'); class GLTextureMemoryOptimized extends GLTextureFloat { constructor(settings) { super(settings); this.type = 'MemoryOptimizedNumberTexture'; } toArray() { return utils.erectMemoryOptimizedFloat(this.renderValues(), this.output[0]); } } module.exports = { GLTextureMemoryOptimized }; ================================================ FILE: src/backend/gl/texture/unsigned-2d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureUnsigned } = require('./unsigned'); class GLTextureUnsigned2D extends GLTextureUnsigned { constructor(settings) { super(settings); this.type = 'NumberTexture'; } toArray() { return utils.erect2DPackedFloat(this.renderValues(), this.output[0], this.output[1]); } } module.exports = { GLTextureUnsigned2D }; ================================================ FILE: src/backend/gl/texture/unsigned-3d.js ================================================ const { utils } = require('../../../utils'); const { GLTextureUnsigned } = require('./unsigned'); class GLTextureUnsigned3D extends GLTextureUnsigned { constructor(settings) { super(settings); this.type = 'NumberTexture'; } toArray() { return utils.erect3DPackedFloat(this.renderValues(), this.output[0], this.output[1], this.output[2]); } } module.exports = { GLTextureUnsigned3D }; ================================================ FILE: src/backend/gl/texture/unsigned.js ================================================ const { utils } = require('../../../utils'); const { GLTexture } = require('./index'); class GLTextureUnsigned extends GLTexture { get textureType() { return this.context.UNSIGNED_BYTE; } constructor(settings) { super(settings); this.type = 'NumberTexture'; } renderRawOutput() { const { context: gl } = this; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer()); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0 ); const result = new Uint8Array(this.size[0] * this.size[1] * 4); gl.readPixels(0, 0, this.size[0], this.size[1], gl.RGBA, gl.UNSIGNED_BYTE, result); return result; } renderValues() { if (this._deleted) return null; return new Float32Array(this.renderRawOutput().buffer); } toArray() { return utils.erectPackedFloat(this.renderValues(), this.output[0]); } } module.exports = { GLTextureUnsigned }; ================================================ FILE: src/backend/headless-gl/kernel.js ================================================ const getContext = require('gl'); const { WebGLKernel } = require('../web-gl/kernel'); const { glKernelString } = require('../gl/kernel-string'); let isSupported = null; let testCanvas = null; let testContext = null; let testExtensions = null; let features = null; class HeadlessGLKernel extends WebGLKernel { static get isSupported() { if (isSupported !== null) return isSupported; this.setupFeatureChecks(); isSupported = testContext !== null; return isSupported; } static setupFeatureChecks() { testCanvas = null; testExtensions = null; if (typeof getContext !== 'function') return; try { // just in case, edge cases testContext = getContext(2, 2, { preserveDrawingBuffer: true }); if (!testContext || !testContext.getExtension) return; testExtensions = { STACKGL_resize_drawingbuffer: testContext.getExtension('STACKGL_resize_drawingbuffer'), STACKGL_destroy_context: testContext.getExtension('STACKGL_destroy_context'), OES_texture_float: testContext.getExtension('OES_texture_float'), OES_texture_float_linear: testContext.getExtension('OES_texture_float_linear'), OES_element_index_uint: testContext.getExtension('OES_element_index_uint'), WEBGL_draw_buffers: testContext.getExtension('WEBGL_draw_buffers'), WEBGL_color_buffer_float: testContext.getExtension('WEBGL_color_buffer_float'), }; features = this.getFeatures(); } catch (e) { console.warn(e); } } static isContextMatch(context) { try { return context.getParameter(context.RENDERER) === 'ANGLE'; } catch (e) { return false; } } static getIsTextureFloat() { return Boolean(testExtensions.OES_texture_float); } static getIsDrawBuffers() { return Boolean(testExtensions.WEBGL_draw_buffers); } static getChannelCount() { return testExtensions.WEBGL_draw_buffers ? testContext.getParameter(testExtensions.WEBGL_draw_buffers.MAX_DRAW_BUFFERS_WEBGL) : 1; } static getMaxTextureSize() { return testContext.getParameter(testContext.MAX_TEXTURE_SIZE); } static get testCanvas() { return testCanvas; } static get testContext() { return testContext; } static get features() { return features; } initCanvas() { return {}; } initContext() { return getContext(2, 2, { preserveDrawingBuffer: true }); } initExtensions() { this.extensions = { STACKGL_resize_drawingbuffer: this.context.getExtension('STACKGL_resize_drawingbuffer'), STACKGL_destroy_context: this.context.getExtension('STACKGL_destroy_context'), OES_texture_float: this.context.getExtension('OES_texture_float'), OES_texture_float_linear: this.context.getExtension('OES_texture_float_linear'), OES_element_index_uint: this.context.getExtension('OES_element_index_uint'), WEBGL_draw_buffers: this.context.getExtension('WEBGL_draw_buffers'), }; } build() { super.build.apply(this, arguments); if (!this.fallbackRequested) { this.extensions.STACKGL_resize_drawingbuffer.resize(this.maxTexSize[0], this.maxTexSize[1]); } } destroyExtensions() { this.extensions.STACKGL_resize_drawingbuffer = null; this.extensions.STACKGL_destroy_context = null; this.extensions.OES_texture_float = null; this.extensions.OES_texture_float_linear = null; this.extensions.OES_element_index_uint = null; this.extensions.WEBGL_draw_buffers = null; } static destroyContext(context) { const extension = context.getExtension('STACKGL_destroy_context'); if (extension && extension.destroy) { extension.destroy(); } } /** * @desc Returns the *pre-compiled* Kernel as a JS Object String, that can be reused. */ toString() { const setupContextString = `const gl = context || require('gl')(1, 1);\n`; const destroyContextString = ` if (!context) { gl.getExtension('STACKGL_destroy_context').destroy(); }\n`; return glKernelString(this.constructor, arguments, this, setupContextString, destroyContextString); } setOutput(output) { super.setOutput(output); if (this.graphical && this.extensions.STACKGL_resize_drawingbuffer) { this.extensions.STACKGL_resize_drawingbuffer.resize(this.maxTexSize[0], this.maxTexSize[1]); } return this; } } module.exports = { HeadlessGLKernel }; ================================================ FILE: src/backend/kernel-value.js ================================================ /** * @class KernelValue */ class KernelValue { /** * @param {KernelVariable} value * @param {IKernelValueSettings} settings */ constructor(value, settings) { const { name, kernel, context, checkContext, onRequestContextHandle, onUpdateValueMismatch, origin, strictIntegers, type, tactic, } = settings; if (!name) { throw new Error('name not set'); } if (!type) { throw new Error('type not set'); } if (!origin) { throw new Error('origin not set'); } if (origin !== 'user' && origin !== 'constants') { throw new Error(`origin must be "user" or "constants" value is "${ origin }"`); } if (!onRequestContextHandle) { throw new Error('onRequestContextHandle is not set'); } this.name = name; this.origin = origin; this.tactic = tactic; this.varName = origin === 'constants' ? `constants.${name}` : name; this.kernel = kernel; this.strictIntegers = strictIntegers; // handle textures this.type = value.type || type; this.size = value.size || null; this.index = null; this.context = context; this.checkContext = checkContext !== null && checkContext !== undefined ? checkContext : true; this.contextHandle = null; this.onRequestContextHandle = onRequestContextHandle; this.onUpdateValueMismatch = onUpdateValueMismatch; this.forceUploadEachRun = null; } get id() { return `${this.origin}_${name}`; } getSource() { throw new Error(`"getSource" not defined on ${ this.constructor.name }`); } updateValue(value) { throw new Error(`"updateValue" not defined on ${ this.constructor.name }`); } } module.exports = { KernelValue }; ================================================ FILE: src/backend/kernel.js ================================================ const { utils } = require('../utils'); const { Input } = require('../input'); class Kernel { /** * @type {Boolean} */ static get isSupported() { throw new Error(`"isSupported" not implemented on ${ this.name }`); } /** * @abstract * @returns {Boolean} */ static isContextMatch(context) { throw new Error(`"isContextMatch" not implemented on ${ this.name }`); } /** * @type {IKernelFeatures} * Used internally to populate the kernel.feature, which is a getter for the output of this value */ static getFeatures() { throw new Error(`"getFeatures" not implemented on ${ this.name }`); } static destroyContext(context) { throw new Error(`"destroyContext" called on ${ this.name }`); } static nativeFunctionArguments() { throw new Error(`"nativeFunctionArguments" called on ${ this.name }`); } static nativeFunctionReturnType() { throw new Error(`"nativeFunctionReturnType" called on ${ this.name }`); } static combineKernels() { throw new Error(`"combineKernels" called on ${ this.name }`); } /** * * @param {string|IKernelJSON} source * @param [settings] */ constructor(source, settings) { if (typeof source !== 'object') { if (typeof source !== 'string') { throw new Error('source not a string'); } if (!utils.isFunctionString(source)) { throw new Error('source not a function string'); } } this.useLegacyEncoder = false; this.fallbackRequested = false; this.onRequestFallback = null; /** * Name of the arguments found from parsing source argument * @type {String[]} */ this.argumentNames = typeof source === 'string' ? utils.getArgumentNamesFromString(source) : null; this.argumentTypes = null; this.argumentSizes = null; this.argumentBitRatios = null; this.kernelArguments = null; this.kernelConstants = null; this.forceUploadKernelConstants = null; /** * The function source * @type {String|IKernelJSON} */ this.source = source; /** * The size of the kernel's output * @type {Number[]} */ this.output = null; /** * Debug mode * @type {Boolean} */ this.debug = false; /** * Graphical mode * @type {Boolean} */ this.graphical = false; /** * Maximum loops when using argument values to prevent infinity * @type {Number} */ this.loopMaxIterations = 0; /** * Constants used in kernel via `this.constants` * @type {Object} */ this.constants = null; /** * * @type {Object.This method calls a helper method *renderOutput* to return the result.
* @returns {Float32Array|Float32Array[]|Float32Array[][]|void} Result The final output of the program, as float, and as Textures for reuse. * @abstract */ run() { throw new Error(`"run" not defined on ${ this.constructor.name }`) } /** * @abstract * @return {Object} */ initCanvas() { throw new Error(`"initCanvas" not defined on ${ this.constructor.name }`); } /** * @abstract * @return {Object} */ initContext() { throw new Error(`"initContext" not defined on ${ this.constructor.name }`); } /** * @param {IDirectKernelSettings} settings * @return {string[]}; * @abstract */ initPlugins(settings) { throw new Error(`"initPlugins" not defined on ${ this.constructor.name }`); } /** * * @param {KernelFunction|string|IGPUFunction} source * @param {IFunctionSettings} [settings] * @return {Kernel} */ addFunction(source, settings = {}) { if (source.name && source.source && source.argumentTypes && 'returnType' in source) { this.functions.push(source); } else if ('settings' in source && 'source' in source) { this.functions.push(this.functionToIGPUFunction(source.source, source.settings)); } else if (typeof source === 'string' || typeof source === 'function') { this.functions.push(this.functionToIGPUFunction(source, settings)); } else { throw new Error(`function not properly defined`); } return this; } /** * * @param {string} name * @param {string} source * @param {IGPUFunctionSettings} [settings] */ addNativeFunction(name, source, settings = {}) { const { argumentTypes, argumentNames } = settings.argumentTypes ? splitArgumentTypes(settings.argumentTypes) : this.constructor.nativeFunctionArguments(source) || {}; this.nativeFunctions.push({ name, source, settings, argumentTypes, argumentNames, returnType: settings.returnType || this.constructor.nativeFunctionReturnType(source) }); return this; } /** * @desc Setup the parameter types for the parameters * supplied to the Kernel function * * @param {IArguments} args - The actual parameters sent to the Kernel */ setupArguments(args) { this.kernelArguments = []; if (!this.argumentTypes) { if (!this.argumentTypes) { this.argumentTypes = []; for (let i = 0; i < args.length; i++) { const argType = utils.getVariableType(args[i], this.strictIntegers); const type = argType === 'Integer' ? 'Number' : argType; this.argumentTypes.push(type); this.kernelArguments.push({ type }); } } } else { for (let i = 0; i < this.argumentTypes.length; i++) { this.kernelArguments.push({ type: this.argumentTypes[i] }); } } // setup sizes this.argumentSizes = new Array(args.length); this.argumentBitRatios = new Int32Array(args.length); for (let i = 0; i < args.length; i++) { const arg = args[i]; this.argumentSizes[i] = arg.constructor === Input ? arg.size : null; this.argumentBitRatios[i] = this.getBitRatio(arg); } if (this.argumentNames.length !== args.length) { throw new Error(`arguments are miss-aligned`); } } /** * Setup constants */ setupConstants() { this.kernelConstants = []; let needsConstantTypes = this.constantTypes === null; if (needsConstantTypes) { this.constantTypes = {}; } this.constantBitRatios = {}; if (this.constants) { for (let name in this.constants) { if (needsConstantTypes) { const type = utils.getVariableType(this.constants[name], this.strictIntegers); this.constantTypes[name] = type; this.kernelConstants.push({ name, type }); } else { this.kernelConstants.push({ name, type: this.constantTypes[name] }); } this.constantBitRatios[name] = this.getBitRatio(this.constants[name]); } } } /** * * @param flag * @return {this} */ setOptimizeFloatMemory(flag) { this.optimizeFloatMemory = flag; return this; } /** * * @param {Array|Object} output * @return {number[]} */ toKernelOutput(output) { if (output.hasOwnProperty('x')) { if (output.hasOwnProperty('y')) { if (output.hasOwnProperty('z')) { return [output.x, output.y, output.z]; } else { return [output.x, output.y]; } } else { return [output.x]; } } else { return output; } } /** * @desc Set output dimensions of the kernel function * @param {Array|Object} output - The output array to set the kernel output size to * @return {this} */ setOutput(output) { this.output = this.toKernelOutput(output); return this; } /** * @desc Toggle debug mode * @param {Boolean} flag - true to enable debug * @return {this} */ setDebug(flag) { this.debug = flag; return this; } /** * @desc Toggle graphical output mode * @param {Boolean} flag - true to enable graphical output * @return {this} */ setGraphical(flag) { this.graphical = flag; this.precision = 'unsigned'; return this; } /** * @desc Set the maximum number of loop iterations * @param {number} max - iterations count * @return {this} */ setLoopMaxIterations(max) { this.loopMaxIterations = max; return this; } /** * @desc Set Constants * @return {this} */ setConstants(constants) { this.constants = constants; return this; } /** * * @param {IKernelValueTypes} constantTypes * @return {this} */ setConstantTypes(constantTypes) { this.constantTypes = constantTypes; return this; } /** * * @param {IFunction[]|KernelFunction[]} functions * @return {this} */ setFunctions(functions) { for (let i = 0; i < functions.length; i++) { this.addFunction(functions[i]); } return this; } /** * * @param {IGPUNativeFunction[]} nativeFunctions * @return {this} */ setNativeFunctions(nativeFunctions) { for (let i = 0; i < nativeFunctions.length; i++) { const settings = nativeFunctions[i]; const { name, source } = settings; this.addNativeFunction(name, source, settings); } return this; } /** * * @param {String} injectedNative * @return {this} */ setInjectedNative(injectedNative) { this.injectedNative = injectedNative; return this; } /** * Set writing to texture on/off * @param flag * @return {this} */ setPipeline(flag) { this.pipeline = flag; return this; } /** * Set precision to 'unsigned' or 'single' * @param {String} flag 'unsigned' or 'single' * @return {this} */ setPrecision(flag) { this.precision = flag; return this; } /** * @param flag * @return {Kernel} * @deprecated */ setDimensions(flag) { utils.warnDeprecated('method', 'setDimensions', 'setOutput'); this.output = flag; return this; } /** * @param flag * @return {this} * @deprecated */ setOutputToTexture(flag) { utils.warnDeprecated('method', 'setOutputToTexture', 'setPipeline'); this.pipeline = flag; return this; } /** * Set to immutable * @param flag * @return {this} */ setImmutable(flag) { this.immutable = flag; return this; } /** * @desc Bind the canvas to kernel * @param {Object} canvas * @return {this} */ setCanvas(canvas) { this.canvas = canvas; return this; } /** * @param {Boolean} flag * @return {this} */ setStrictIntegers(flag) { this.strictIntegers = flag; return this; } /** * * @param flag * @return {this} */ setDynamicOutput(flag) { this.dynamicOutput = flag; return this; } /** * @deprecated * @param flag * @return {this} */ setHardcodeConstants(flag) { utils.warnDeprecated('method', 'setHardcodeConstants'); this.setDynamicOutput(flag); this.setDynamicArguments(flag); return this; } /** * * @param flag * @return {this} */ setDynamicArguments(flag) { this.dynamicArguments = flag; return this; } /** * @param {Boolean} flag * @return {this} */ setUseLegacyEncoder(flag) { this.useLegacyEncoder = flag; return this; } /** * * @param {Boolean} flag * @return {this} */ setWarnVarUsage(flag) { utils.warnDeprecated('method', 'setWarnVarUsage'); return this; } /** * @deprecated * @returns {Object} */ getCanvas() { utils.warnDeprecated('method', 'getCanvas'); return this.canvas; } /** * @deprecated * @returns {Object} */ getWebGl() { utils.warnDeprecated('method', 'getWebGl'); return this.context; } /** * @desc Bind the webGL instance to kernel * @param {WebGLRenderingContext} context - webGl instance to bind */ setContext(context) { this.context = context; return this; } /** * * @param {IKernelValueTypes|GPUVariableType[]} argumentTypes * @return {this} */ setArgumentTypes(argumentTypes) { if (Array.isArray(argumentTypes)) { this.argumentTypes = argumentTypes; } else { this.argumentTypes = []; for (const p in argumentTypes) { if (!argumentTypes.hasOwnProperty(p)) continue; const argumentIndex = this.argumentNames.indexOf(p); if (argumentIndex === -1) throw new Error(`unable to find argument ${ p }`); this.argumentTypes[argumentIndex] = argumentTypes[p]; } } return this; } /** * * @param {Tactic} tactic * @return {this} */ setTactic(tactic) { this.tactic = tactic; return this; } requestFallback(args) { if (!this.onRequestFallback) { throw new Error(`"onRequestFallback" not defined on ${ this.constructor.name }`); } this.fallbackRequested = true; return this.onRequestFallback(args); } /** * @desc Validate settings * @abstract */ validateSettings() { throw new Error(`"validateSettings" not defined on ${ this.constructor.name }`); } /** * @desc Add a sub kernel to the root kernel instance. * This is what `createKernelMap` uses. * * @param {ISubKernel} subKernel - function (as a String) of the subKernel to add */ addSubKernel(subKernel) { if (this.subKernels === null) { this.subKernels = []; } if (!subKernel.source) throw new Error('subKernel missing "source" property'); if (!subKernel.property && isNaN(subKernel.property)) throw new Error('subKernel missing "property" property'); if (!subKernel.name) throw new Error('subKernel missing "name" property'); this.subKernels.push(subKernel); return this; } /** * @desc Destroys all memory associated with this kernel * @param {Boolean} [removeCanvasReferences] remove any associated canvas references */ destroy(removeCanvasReferences) { throw new Error(`"destroy" called on ${ this.constructor.name }`); } /** * bit storage ratio of source to target 'buffer', i.e. if 8bit array -> 32bit tex = 4 * @param value * @returns {number} */ getBitRatio(value) { if (this.precision === 'single') { // 8 and 16 are up-converted to float32 return 4; } else if (Array.isArray(value[0])) { return this.getBitRatio(value[0]); } else if (value.constructor === Input) { return this.getBitRatio(value.value); } switch (value.constructor) { case Uint8ClampedArray: case Uint8Array: case Int8Array: return 1; case Uint16Array: case Int16Array: return 2; case Float32Array: case Int32Array: default: return 4; } } /** * @param {Boolean} [flip] * @returns {Uint8ClampedArray} */ getPixels(flip) { throw new Error(`"getPixels" called on ${ this.constructor.name }`); } checkOutput() { if (!this.output || !utils.isArray(this.output)) throw new Error('kernel.output not an array'); if (this.output.length < 1) throw new Error('kernel.output is empty, needs at least 1 value'); for (let i = 0; i < this.output.length; i++) { if (isNaN(this.output[i]) || this.output[i] < 1) { throw new Error(`${ this.constructor.name }.output[${ i }] incorrectly defined as \`${ this.output[i] }\`, needs to be numeric, and greater than 0`); } } } /** * * @param {String} value */ prependString(value) { throw new Error(`"prependString" called on ${ this.constructor.name }`); } /** * * @param {String} value * @return Boolean */ hasPrependString(value) { throw new Error(`"hasPrependString" called on ${ this.constructor.name }`); } /** * @return {IKernelJSON} */ toJSON() { return { settings: { output: this.output, pipeline: this.pipeline, argumentNames: this.argumentNames, argumentsTypes: this.argumentTypes, constants: this.constants, pluginNames: this.plugins ? this.plugins.map(plugin => plugin.name) : null, returnType: this.returnType, } }; } /** * @param {IArguments} args */ buildSignature(args) { const Constructor = this.constructor; this.signature = Constructor.getSignature(this, Constructor.getArgumentTypes(this, args)); } /** * @param {Kernel} kernel * @param {IArguments} args * @returns GPUVariableType[] */ static getArgumentTypes(kernel, args) { const argumentTypes = new Array(args.length); for (let i = 0; i < args.length; i++) { const arg = args[i]; const type = kernel.argumentTypes[i]; if (arg.type) { argumentTypes[i] = arg.type; } else { switch (type) { case 'Number': case 'Integer': case 'Float': case 'ArrayTexture(1)': argumentTypes[i] = utils.getVariableType(arg); break; default: argumentTypes[i] = type; } } } return argumentTypes; } /** * * @param {Kernel} kernel * @param {GPUVariableType[]} argumentTypes * @abstract */ static getSignature(kernel, argumentTypes) { throw new Error(`"getSignature" not implemented on ${ this.name }`); } /** * * @param {String|Function} source * @param {IFunctionSettings} [settings] * @returns {IGPUFunction} */ functionToIGPUFunction(source, settings = {}) { if (typeof source !== 'string' && typeof source !== 'function') throw new Error('source not a string or function'); const sourceString = typeof source === 'string' ? source : source.toString(); let argumentTypes = []; if (Array.isArray(settings.argumentTypes)) { argumentTypes = settings.argumentTypes; } else if (typeof settings.argumentTypes === 'object') { argumentTypes = utils.getArgumentNamesFromString(sourceString) .map(name => settings.argumentTypes[name]) || []; } else { argumentTypes = settings.argumentTypes || []; } return { name: utils.getFunctionNameFromString(sourceString) || null, source: sourceString, argumentTypes, returnType: settings.returnType || null, }; } /** * * @param {Kernel} previousKernel * @abstract */ onActivate(previousKernel) {} } function splitArgumentTypes(argumentTypesObject) { const argumentNames = Object.keys(argumentTypesObject); const argumentTypes = []; for (let i = 0; i < argumentNames.length; i++) { const argumentName = argumentNames[i]; argumentTypes.push(argumentTypesObject[argumentName]); } return { argumentTypes, argumentNames }; } module.exports = { Kernel }; ================================================ FILE: src/backend/web-gl/fragment-shader.js ================================================ // language=GLSL const fragmentShader = `__HEADER__; __FLOAT_TACTIC_DECLARATION__; __INT_TACTIC_DECLARATION__; __SAMPLER_2D_TACTIC_DECLARATION__; const int LOOP_MAX = __LOOP_MAX__; __PLUGINS__; __CONSTANTS__; varying vec2 vTexCoord; float acosh(float x) { return log(x + sqrt(x * x - 1.0)); } float sinh(float x) { return (pow(${Math.E}, x) - pow(${Math.E}, -x)) / 2.0; } float asinh(float x) { return log(x + sqrt(x * x + 1.0)); } float atan2(float v1, float v2) { if (v1 == 0.0 || v2 == 0.0) return 0.0; return atan(v1 / v2); } float atanh(float x) { x = (x + 1.0) / (x - 1.0); if (x < 0.0) { return 0.5 * log(-x); } return 0.5 * log(x); } float cbrt(float x) { if (x >= 0.0) { return pow(x, 1.0 / 3.0); } else { return -pow(x, 1.0 / 3.0); } } float cosh(float x) { return (pow(${Math.E}, x) + pow(${Math.E}, -x)) / 2.0; } float expm1(float x) { return pow(${Math.E}, x) - 1.0; } float fround(highp float x) { return x; } float imul(float v1, float v2) { return float(int(v1) * int(v2)); } float log10(float x) { return log2(x) * (1.0 / log2(10.0)); } float log1p(float x) { return log(1.0 + x); } float _pow(float v1, float v2) { if (v2 == 0.0) return 1.0; return pow(v1, v2); } float tanh(float x) { float e = exp(2.0 * x); return (e - 1.0) / (e + 1.0); } float trunc(float x) { if (x >= 0.0) { return floor(x); } else { return ceil(x); } } vec4 _round(vec4 x) { return floor(x + 0.5); } float _round(float x) { return floor(x + 0.5); } const int BIT_COUNT = 32; int modi(int x, int y) { return x - y * (x / y); } int bitwiseOr(int a, int b) { int result = 0; int n = 1; for (int i = 0; i < BIT_COUNT; i++) { if ((modi(a, 2) == 1) || (modi(b, 2) == 1)) { result += n; } a = a / 2; b = b / 2; n = n * 2; if(!(a > 0 || b > 0)) { break; } } return result; } int bitwiseXOR(int a, int b) { int result = 0; int n = 1; for (int i = 0; i < BIT_COUNT; i++) { if ((modi(a, 2) == 1) != (modi(b, 2) == 1)) { result += n; } a = a / 2; b = b / 2; n = n * 2; if(!(a > 0 || b > 0)) { break; } } return result; } int bitwiseAnd(int a, int b) { int result = 0; int n = 1; for (int i = 0; i < BIT_COUNT; i++) { if ((modi(a, 2) == 1) && (modi(b, 2) == 1)) { result += n; } a = a / 2; b = b / 2; n = n * 2; if(!(a > 0 && b > 0)) { break; } } return result; } int bitwiseNot(int a) { int result = 0; int n = 1; for (int i = 0; i < BIT_COUNT; i++) { if (modi(a, 2) == 0) { result += n; } a = a / 2; n = n * 2; } return result; } int bitwiseZeroFillLeftShift(int n, int shift) { int maxBytes = BIT_COUNT; for (int i = 0; i < BIT_COUNT; i++) { if (maxBytes >= n) { break; } maxBytes *= 2; } for (int i = 0; i < BIT_COUNT; i++) { if (i >= shift) { break; } n *= 2; } int result = 0; int byteVal = 1; for (int i = 0; i < BIT_COUNT; i++) { if (i >= maxBytes) break; if (modi(n, 2) > 0) { result += byteVal; } n = int(n / 2); byteVal *= 2; } return result; } int bitwiseSignedRightShift(int num, int shifts) { return int(floor(float(num) / pow(2.0, float(shifts)))); } int bitwiseZeroFillRightShift(int n, int shift) { int maxBytes = BIT_COUNT; for (int i = 0; i < BIT_COUNT; i++) { if (maxBytes >= n) { break; } maxBytes *= 2; } for (int i = 0; i < BIT_COUNT; i++) { if (i >= shift) { break; } n /= 2; } int result = 0; int byteVal = 1; for (int i = 0; i < BIT_COUNT; i++) { if (i >= maxBytes) break; if (modi(n, 2) > 0) { result += byteVal; } n = int(n / 2); byteVal *= 2; } return result; } vec2 integerMod(vec2 x, float y) { vec2 res = floor(mod(x, y)); return res * step(1.0 - floor(y), -res); } vec3 integerMod(vec3 x, float y) { vec3 res = floor(mod(x, y)); return res * step(1.0 - floor(y), -res); } vec4 integerMod(vec4 x, vec4 y) { vec4 res = floor(mod(x, y)); return res * step(1.0 - floor(y), -res); } float integerMod(float x, float y) { float res = floor(mod(x, y)); return res * (res > floor(y) - 1.0 ? 0.0 : 1.0); } int integerMod(int x, int y) { return x - (y * int(x / y)); } __DIVIDE_WITH_INTEGER_CHECK__; // Here be dragons! // DO NOT OPTIMIZE THIS CODE // YOU WILL BREAK SOMETHING ON SOMEBODY\'S MACHINE // LEAVE IT AS IT IS, LEST YOU WASTE YOUR OWN TIME const vec2 MAGIC_VEC = vec2(1.0, -256.0); const vec4 SCALE_FACTOR = vec4(1.0, 256.0, 65536.0, 0.0); const vec4 SCALE_FACTOR_INV = vec4(1.0, 0.00390625, 0.0000152587890625, 0.0); // 1, 1/256, 1/65536 float decode32(vec4 texel) { __DECODE32_ENDIANNESS__; texel *= 255.0; vec2 gte128; gte128.x = texel.b >= 128.0 ? 1.0 : 0.0; gte128.y = texel.a >= 128.0 ? 1.0 : 0.0; float exponent = 2.0 * texel.a - 127.0 + dot(gte128, MAGIC_VEC); float res = exp2(_round(exponent)); texel.b = texel.b - 128.0 * gte128.x; res = dot(texel, SCALE_FACTOR) * exp2(_round(exponent-23.0)) + res; res *= gte128.y * -2.0 + 1.0; return res; } float decode16(vec4 texel, int index) { int channel = integerMod(index, 2); if (channel == 0) return texel.r * 255.0 + texel.g * 65280.0; if (channel == 1) return texel.b * 255.0 + texel.a * 65280.0; return 0.0; } float decode8(vec4 texel, int index) { int channel = integerMod(index, 4); if (channel == 0) return texel.r * 255.0; if (channel == 1) return texel.g * 255.0; if (channel == 2) return texel.b * 255.0; if (channel == 3) return texel.a * 255.0; return 0.0; } vec4 legacyEncode32(float f) { float F = abs(f); float sign = f < 0.0 ? 1.0 : 0.0; float exponent = floor(log2(F)); float mantissa = (exp2(-exponent) * F); // exponent += floor(log2(mantissa)); vec4 texel = vec4(F * exp2(23.0-exponent)) * SCALE_FACTOR_INV; texel.rg = integerMod(texel.rg, 256.0); texel.b = integerMod(texel.b, 128.0); texel.a = exponent*0.5 + 63.5; texel.ba += vec2(integerMod(exponent+127.0, 2.0), sign) * 128.0; texel = floor(texel); texel *= 0.003921569; // 1/255 __ENCODE32_ENDIANNESS__; return texel; } // https://github.com/gpujs/gpu.js/wiki/Encoder-details vec4 encode32(float value) { if (value == 0.0) return vec4(0, 0, 0, 0); float exponent; float mantissa; vec4 result; float sgn; sgn = step(0.0, -value); value = abs(value); exponent = floor(log2(value)); mantissa = value*pow(2.0, -exponent)-1.0; exponent = exponent+127.0; result = vec4(0,0,0,0); result.a = floor(exponent/2.0); exponent = exponent - result.a*2.0; result.a = result.a + 128.0*sgn; result.b = floor(mantissa * 128.0); mantissa = mantissa - result.b / 128.0; result.b = result.b + exponent*128.0; result.g = floor(mantissa*32768.0); mantissa = mantissa - result.g/32768.0; result.r = floor(mantissa*8388608.0); return result/255.0; } // Dragons end here int index; ivec3 threadId; ivec3 indexTo3D(int idx, ivec3 texDim) { int z = int(idx / (texDim.x * texDim.y)); idx -= z * int(texDim.x * texDim.y); int y = int(idx / texDim.x); int x = int(integerMod(idx, texDim.x)); return ivec3(x, y, z); } float get32(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int w = texSize.x; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize)); return decode32(texel); } float get16(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int w = texSize.x * 2; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize.x * 2, texSize.y)); return decode16(texel, index); } float get8(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int w = texSize.x * 4; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize.x * 4, texSize.y)); return decode8(texel, index); } float getMemoryOptimized32(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int channel = integerMod(index, 4); index = index / 4; int w = texSize.x; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize)); if (channel == 0) return texel.r; if (channel == 1) return texel.g; if (channel == 2) return texel.b; if (channel == 3) return texel.a; return 0.0; } vec4 getImage2D(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int w = texSize.x; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; return texture2D(tex, st / vec2(texSize)); } float getFloatFromSampler2D(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { vec4 result = getImage2D(tex, texSize, texDim, z, y, x); return result[0]; } vec2 getVec2FromSampler2D(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { vec4 result = getImage2D(tex, texSize, texDim, z, y, x); return vec2(result[0], result[1]); } vec2 getMemoryOptimizedVec2(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + (texDim.x * (y + (texDim.y * z))); int channel = integerMod(index, 2); index = index / 2; int w = texSize.x; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize)); if (channel == 0) return vec2(texel.r, texel.g); if (channel == 1) return vec2(texel.b, texel.a); return vec2(0.0, 0.0); } vec3 getVec3FromSampler2D(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { vec4 result = getImage2D(tex, texSize, texDim, z, y, x); return vec3(result[0], result[1], result[2]); } vec3 getMemoryOptimizedVec3(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int fieldIndex = 3 * (x + texDim.x * (y + texDim.y * z)); int vectorIndex = fieldIndex / 4; int vectorOffset = fieldIndex - vectorIndex * 4; int readY = vectorIndex / texSize.x; int readX = vectorIndex - readY * texSize.x; vec4 tex1 = texture2D(tex, (vec2(readX, readY) + 0.5) / vec2(texSize)); if (vectorOffset == 0) { return tex1.xyz; } else if (vectorOffset == 1) { return tex1.yzw; } else { readX++; if (readX >= texSize.x) { readX = 0; readY++; } vec4 tex2 = texture2D(tex, vec2(readX, readY) / vec2(texSize)); if (vectorOffset == 2) { return vec3(tex1.z, tex1.w, tex2.x); } else { return vec3(tex1.w, tex2.x, tex2.y); } } } vec4 getVec4FromSampler2D(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { return getImage2D(tex, texSize, texDim, z, y, x); } vec4 getMemoryOptimizedVec4(sampler2D tex, ivec2 texSize, ivec3 texDim, int z, int y, int x) { int index = x + texDim.x * (y + texDim.y * z); int channel = integerMod(index, 2); int w = texSize.x; vec2 st = vec2(float(integerMod(index, w)), float(index / w)) + 0.5; vec4 texel = texture2D(tex, st / vec2(texSize)); return vec4(texel.r, texel.g, texel.b, texel.a); } vec4 actualColor; void color(float r, float g, float b, float a) { actualColor = vec4(r,g,b,a); } void color(float r, float g, float b) { color(r,g,b,1.0); } void color(sampler2D image) { actualColor = texture2D(image, vTexCoord); } float modulo(float number, float divisor) { if (number < 0.0) { number = abs(number); if (divisor < 0.0) { divisor = abs(divisor); } return -mod(number, divisor); } if (divisor < 0.0) { divisor = abs(divisor); } return mod(number, divisor); } __INJECTED_NATIVE__; __MAIN_CONSTANTS__; __MAIN_ARGUMENTS__; __KERNEL__; void main(void) { index = int(vTexCoord.s * float(uTexSize.x)) + int(vTexCoord.t * float(uTexSize.y)) * uTexSize.x; __MAIN_RESULT__; }`; module.exports = { fragmentShader }; ================================================ FILE: src/backend/web-gl/function-node.js ================================================ const { utils } = require('../../utils'); const { FunctionNode } = require('../function-node'); /** * @desc [INTERNAL] Takes in a function node, and does all the AST voodoo required to toString its respective WebGL code */ class WebGLFunctionNode extends FunctionNode { constructor(source, settings) { super(source, settings); if (settings && settings.hasOwnProperty('fixIntegerDivisionAccuracy')) { this.fixIntegerDivisionAccuracy = settings.fixIntegerDivisionAccuracy; } } astConditionalExpression(ast, retArr) { if (ast.type !== 'ConditionalExpression') { throw this.astErrorOutput('Not a conditional expression', ast); } const consequentType = this.getType(ast.consequent); const alternateType = this.getType(ast.alternate); // minification handling if void if (consequentType === null && alternateType === null) { retArr.push('if ('); this.astGeneric(ast.test, retArr); retArr.push(') {'); this.astGeneric(ast.consequent, retArr); retArr.push(';'); retArr.push('} else {'); this.astGeneric(ast.alternate, retArr); retArr.push(';'); retArr.push('}'); return retArr; } retArr.push('('); this.astGeneric(ast.test, retArr); retArr.push('?'); this.astGeneric(ast.consequent, retArr); retArr.push(':'); this.astGeneric(ast.alternate, retArr); retArr.push(')'); return retArr; } /** * @desc Parses the abstract syntax tree for to its *named function* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astFunction(ast, retArr) { // Setup function return type and name if (this.isRootKernel) { retArr.push('void'); } else { // looking up return type, this is a little expensive, and can be avoided if returnType is set if (!this.returnType) { const lastReturn = this.findLastReturn(); if (lastReturn) { this.returnType = this.getType(ast.body); if (this.returnType === 'LiteralInteger') { this.returnType = 'Number'; } } } const { returnType } = this; if (!returnType) { retArr.push('void'); } else { const type = typeMap[returnType]; if (!type) { throw new Error(`unknown type ${returnType}`); } retArr.push(type); } } retArr.push(' '); retArr.push(this.name); retArr.push('('); if (!this.isRootKernel) { // Arguments handling for (let i = 0; i < this.argumentNames.length; ++i) { const argumentName = this.argumentNames[i]; if (i > 0) { retArr.push(', '); } let argumentType = this.argumentTypes[this.argumentNames.indexOf(argumentName)]; // The type is too loose ended, here we decide to solidify a type, lets go with float if (!argumentType) { throw this.astErrorOutput(`Unknown argument ${argumentName} type`, ast); } if (argumentType === 'LiteralInteger') { this.argumentTypes[i] = argumentType = 'Number'; } const type = typeMap[argumentType]; if (!type) { throw this.astErrorOutput('Unexpected expression', ast); } const name = utils.sanitizeName(argumentName); if (type === 'sampler2D' || type === 'sampler2DArray') { // mash needed arguments together, since now we have end to end inference retArr.push(`${type} user_${name},ivec2 user_${name}Size,ivec3 user_${name}Dim`); } else { retArr.push(`${type} user_${name}`); } } } // Function opening retArr.push(') {\n'); // Body statement iteration for (let i = 0; i < ast.body.body.length; ++i) { this.astGeneric(ast.body.body[i], retArr); retArr.push('\n'); } // Function closing retArr.push('}\n'); return retArr; } /** * @desc Parses the abstract syntax tree for to *return* statement * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astReturnStatement(ast, retArr) { if (!ast.argument) throw this.astErrorOutput('Unexpected return statement', ast); this.pushState('skip-literal-correction'); const type = this.getType(ast.argument); this.popState('skip-literal-correction'); const result = []; if (!this.returnType) { if (type === 'LiteralInteger' || type === 'Integer') { this.returnType = 'Number'; } else { this.returnType = type; } } switch (this.returnType) { case 'LiteralInteger': case 'Number': case 'Float': switch (type) { case 'Integer': result.push('float('); this.astGeneric(ast.argument, result); result.push(')'); break; case 'LiteralInteger': this.castLiteralToFloat(ast.argument, result); // Running astGeneric forces the LiteralInteger to pick a type, and here, if we are returning a float, yet // the LiteralInteger has picked to be an integer because of constraints on it we cast it to float. if (this.getType(ast) === 'Integer') { result.unshift('float('); result.push(')'); } break; default: this.astGeneric(ast.argument, result); } break; case 'Integer': switch (type) { case 'Float': case 'Number': this.castValueToInteger(ast.argument, result); break; case 'LiteralInteger': this.castLiteralToInteger(ast.argument, result); break; default: this.astGeneric(ast.argument, result); } break; case 'Array(4)': case 'Array(3)': case 'Array(2)': case 'Matrix(2)': case 'Matrix(3)': case 'Matrix(4)': case 'Input': this.astGeneric(ast.argument, result); break; default: throw this.astErrorOutput(`unhandled return type ${this.returnType}`, ast); } if (this.isRootKernel) { retArr.push(`kernelResult = ${ result.join('') };`); retArr.push('return;'); } else if (this.isSubKernel) { retArr.push(`subKernelResult_${ this.name } = ${ result.join('') };`); retArr.push(`return subKernelResult_${ this.name };`); } else { retArr.push(`return ${ result.join('') };`); } return retArr; } /** * @desc Parses the abstract syntax tree for *literal value* * * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * * @returns {Array} the append retArr */ astLiteral(ast, retArr) { // Reject non numeric literals if (isNaN(ast.value)) { throw this.astErrorOutput( 'Non-numeric literal not supported : ' + ast.value, ast ); } const key = this.astKey(ast); if (Number.isInteger(ast.value)) { if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.literalTypes[key] = 'Integer'; retArr.push(`${ast.value}`); } else if (this.isState('casting-to-float') || this.isState('building-float')) { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}.0`); } else { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}.0`); } } else if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.literalTypes[key] = 'Integer'; retArr.push(Math.round(ast.value)); } else { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}`); } return retArr; } /** * @desc Parses the abstract syntax tree for *binary* expression * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBinaryExpression(ast, retArr) { if (this.checkAndUpconvertOperator(ast, retArr)) { return retArr; } if (this.fixIntegerDivisionAccuracy && ast.operator === '/') { retArr.push('divWithIntCheck('); this.pushState('building-float'); switch (this.getType(ast.left)) { case 'Integer': this.castValueToFloat(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(', '); switch (this.getType(ast.right)) { case 'Integer': this.castValueToFloat(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } this.popState('building-float'); retArr.push(')'); return retArr; } retArr.push('('); const leftType = this.getType(ast.left) || 'Number'; const rightType = this.getType(ast.right) || 'Number'; if (!leftType || !rightType) { throw this.astErrorOutput(`Unhandled binary expression`, ast); } const key = leftType + ' & ' + rightType; switch (key) { case 'Integer & Integer': this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); break; case 'Number & Float': case 'Float & Number': case 'Float & Float': case 'Number & Number': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-float'); break; case 'LiteralInteger & LiteralInteger': if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); } else { this.pushState('building-float'); this.castLiteralToFloat(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToFloat(ast.right, retArr); this.popState('building-float'); } break; case 'Integer & Float': case 'Integer & Number': if (ast.operator === '>' || ast.operator === '<' && ast.right.type === 'Literal') { // if right value is actually a float, don't loose that information, cast left to right rather than the usual right to left if (!Number.isInteger(ast.right.value)) { this.pushState('building-float'); this.castValueToFloat(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-float'); break; } } this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.pushState('casting-to-integer'); if (ast.right.type === 'Literal') { const literalResult = []; this.astGeneric(ast.right, literalResult); const literalType = this.getType(ast.right); if (literalType === 'Integer') { retArr.push(literalResult.join('')); } else { throw this.astErrorOutput(`Unhandled binary expression with literal`, ast); } } else { retArr.push('int('); this.astGeneric(ast.right, retArr); retArr.push(')'); } this.popState('casting-to-integer'); this.popState('building-integer'); break; case 'Integer & LiteralInteger': this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToInteger(ast.right, retArr); this.popState('building-integer'); break; case 'Number & Integer': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToFloat(ast.right, retArr); this.popState('building-float'); break; case 'Float & LiteralInteger': case 'Number & LiteralInteger': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToFloat(ast.right, retArr); this.popState('building-float'); break; case 'LiteralInteger & Float': case 'LiteralInteger & Number': if (this.isState('casting-to-integer')) { this.pushState('building-integer'); this.castLiteralToInteger(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToInteger(ast.right, retArr); this.popState('building-integer'); } else { this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.pushState('casting-to-float'); this.astGeneric(ast.right, retArr); this.popState('casting-to-float'); this.popState('building-float'); } break; case 'LiteralInteger & Integer': this.pushState('building-integer'); this.castLiteralToInteger(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); break; case 'Boolean & Boolean': this.pushState('building-boolean'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-boolean'); break; case 'Float & Integer': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToFloat(ast.right, retArr); this.popState('building-float'); break; default: throw this.astErrorOutput(`Unhandled binary expression between ${key}`, ast); } retArr.push(')'); return retArr; } checkAndUpconvertOperator(ast, retArr) { const bitwiseResult = this.checkAndUpconvertBitwiseOperators(ast, retArr); if (bitwiseResult) { return bitwiseResult; } const upconvertableOperators = { '%': this.fixIntegerDivisionAccuracy ? 'integerCorrectionModulo' : 'modulo', '**': 'pow', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); switch (this.getType(ast.left)) { case 'Integer': this.castValueToFloat(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(','); switch (this.getType(ast.right)) { case 'Integer': this.castValueToFloat(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } retArr.push(')'); return retArr; } checkAndUpconvertBitwiseOperators(ast, retArr) { const upconvertableOperators = { '&': 'bitwiseAnd', '|': 'bitwiseOr', '^': 'bitwiseXOR', '<<': 'bitwiseZeroFillLeftShift', '>>': 'bitwiseSignedRightShift', '>>>': 'bitwiseZeroFillRightShift', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); const leftType = this.getType(ast.left); switch (leftType) { case 'Number': case 'Float': this.castValueToInteger(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(','); const rightType = this.getType(ast.right); switch (rightType) { case 'Number': case 'Float': this.castValueToInteger(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } retArr.push(')'); return retArr; } checkAndUpconvertBitwiseUnary(ast, retArr) { const upconvertableOperators = { '~': 'bitwiseNot', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); switch (this.getType(ast.argument)) { case 'Number': case 'Float': this.castValueToInteger(ast.argument, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.argument, retArr); break; default: this.astGeneric(ast.argument, retArr); } retArr.push(')'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castLiteralToInteger(ast, retArr) { this.pushState('casting-to-integer'); this.astGeneric(ast, retArr); this.popState('casting-to-integer'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castLiteralToFloat(ast, retArr) { this.pushState('casting-to-float'); this.astGeneric(ast, retArr); this.popState('casting-to-float'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castValueToInteger(ast, retArr) { this.pushState('casting-to-integer'); retArr.push('int('); this.astGeneric(ast, retArr); retArr.push(')'); this.popState('casting-to-integer'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castValueToFloat(ast, retArr) { this.pushState('casting-to-float'); retArr.push('float('); this.astGeneric(ast, retArr); retArr.push(')'); this.popState('casting-to-float'); return retArr; } /** * @desc Parses the abstract syntax tree for *identifier* expression * @param {Object} idtNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astIdentifierExpression(idtNode, retArr) { if (idtNode.type !== 'Identifier') { throw this.astErrorOutput('IdentifierExpression - not an Identifier', idtNode); } const type = this.getType(idtNode); const name = utils.sanitizeName(idtNode.name); if (idtNode.name === 'Infinity') { // https://stackoverflow.com/a/47543127/1324039 retArr.push('3.402823466e+38'); } else if (type === 'Boolean') { if (this.argumentNames.indexOf(name) > -1) { retArr.push(`bool(user_${name})`); } else { retArr.push(`user_${name}`); } } else { retArr.push(`user_${name}`); } return retArr; } /** * @desc Parses the abstract syntax tree for *for-loop* expression * @param {Object} forNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the parsed webgl string */ astForStatement(forNode, retArr) { if (forNode.type !== 'ForStatement') { throw this.astErrorOutput('Invalid for statement', forNode); } const initArr = []; const testArr = []; const updateArr = []; const bodyArr = []; let isSafe = null; if (forNode.init) { const { declarations } = forNode.init; if (declarations.length > 1) { isSafe = false; } this.astGeneric(forNode.init, initArr); for (let i = 0; i < declarations.length; i++) { if (declarations[i].init && declarations[i].init.type !== 'Literal') { isSafe = false; } } } else { isSafe = false; } if (forNode.test) { this.astGeneric(forNode.test, testArr); } else { isSafe = false; } if (forNode.update) { this.astGeneric(forNode.update, updateArr); } else { isSafe = false; } if (forNode.body) { this.pushState('loop-body'); this.astGeneric(forNode.body, bodyArr); this.popState('loop-body'); } // have all parts, now make them safe if (isSafe === null) { isSafe = this.isSafe(forNode.init) && this.isSafe(forNode.test); } if (isSafe) { const initString = initArr.join(''); const initNeedsSemiColon = initString[initString.length - 1] !== ';'; retArr.push(`for (${initString}${initNeedsSemiColon ? ';' : ''}${testArr.join('')};${updateArr.join('')}){\n`); retArr.push(bodyArr.join('')); retArr.push('}\n'); } else { const iVariableName = this.getInternalVariableName('safeI'); if (initArr.length > 0) { retArr.push(initArr.join(''), '\n'); } retArr.push(`for (int ${iVariableName}=0;${iVariableName}This builds the shaders and runs them on the GPU, * the outputs the result back as float(enabled by default) and Texture.
* * @property {WebGLTexture[]} textureCache - webGl Texture cache * @property {Object.