gpu.js
Version:
GPU Accelerated JavaScript
673 lines (612 loc) • 21.7 kB
JavaScript
const { Kernel } = require('../kernel');
const { FunctionBuilder } = require('../function-builder');
const { CPUFunctionNode } = require('./function-node');
const { utils } = require('../../utils');
const { cpuKernelString } = require('./kernel-string');
/**
* @desc Kernel Implementation for CPU.
* <p>Instantiates properties to the CPU Kernel.</p>
*/
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.
*
* <p>If the graphical flag is enabled, canvas is used.</p>
*/
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.
*
* <p>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.</p>
*
* @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
};