gpu.js
Version:
GPU Accelerated JavaScript
605 lines (555 loc) • 18.5 kB
JavaScript
const { gpuMock } = require('gpu-mock.js');
const { utils } = require('./utils');
const { Kernel } = require('./backend/kernel');
const { CPUKernel } = require('./backend/cpu/kernel');
const { HeadlessGLKernel } = require('./backend/headless-gl/kernel');
const { WebGL2Kernel } = require('./backend/web-gl2/kernel');
const { WebGLKernel } = require('./backend/web-gl/kernel');
const { kernelRunShortcut } = require('./kernel-run-shortcut');
/**
*
* @type {Array.<Kernel>}
*/
const kernelOrder = [HeadlessGLKernel, WebGL2Kernel, WebGLKernel];
/**
*
* @type {string[]}
*/
const kernelTypes = ['gpu', 'cpu'];
const internalKernels = {
'headlessgl': HeadlessGLKernel,
'webgl2': WebGL2Kernel,
'webgl': WebGLKernel,
};
let validate = true;
/**
* The GPU.js library class which manages the GPU context for the creating kernels
* @class
* @return {GPU}
*/
class GPU {
static disableValidation() {
validate = false;
}
static enableValidation() {
validate = true;
}
static get isGPUSupported() {
return kernelOrder.some(Kernel => Kernel.isSupported);
}
/**
*
* @returns {boolean}
*/
static get isKernelMapSupported() {
return kernelOrder.some(Kernel => Kernel.isSupported && Kernel.features.kernelMap);
}
/**
* @desc TRUE is platform supports OffscreenCanvas
*/
static get isOffscreenCanvasSupported() {
return (typeof Worker !== 'undefined' && typeof OffscreenCanvas !== 'undefined') || typeof importScripts !== 'undefined';
}
/**
* @desc TRUE if platform supports WebGL
*/
static get isWebGLSupported() {
return WebGLKernel.isSupported;
}
/**
* @desc TRUE if platform supports WebGL2
*/
static get isWebGL2Supported() {
return WebGL2Kernel.isSupported;
}
/**
* @desc TRUE if platform supports HeadlessGL
*/
static get isHeadlessGLSupported() {
return HeadlessGLKernel.isSupported;
}
/**
*
* @desc TRUE if platform supports Canvas
*/
static get isCanvasSupported() {
return typeof HTMLCanvasElement !== 'undefined';
}
/**
* @desc TRUE if platform supports HTMLImageArray}
*/
static get isGPUHTMLImageArraySupported() {
return WebGL2Kernel.isSupported;
}
/**
* @desc TRUE if platform supports single precision}
* @returns {boolean}
*/
static get isSinglePrecisionSupported() {
return kernelOrder.some(Kernel => Kernel.isSupported && Kernel.features.isFloatRead && Kernel.features.isTextureFloat);
}
/**
* Creates an instance of GPU.
* @param {IGPUSettings} [settings] - Settings to set mode, and other properties
* @constructor
*/
constructor(settings) {
settings = settings || {};
this.canvas = settings.canvas || null;
this.context = settings.context || null;
this.mode = settings.mode;
this.Kernel = null;
this.kernels = [];
this.functions = [];
this.nativeFunctions = [];
this.injectedNative = null;
if (this.mode === 'dev') return;
this.chooseKernel();
// add functions from settings
if (settings.functions) {
for (let i = 0; i < settings.functions.length; i++) {
this.addFunction(settings.functions[i]);
}
}
// add native functions from settings
if (settings.nativeFunctions) {
for (const p in settings.nativeFunctions) {
if (!settings.nativeFunctions.hasOwnProperty(p)) continue;
const s = settings.nativeFunctions[p];
const { name, source } = s;
this.addNativeFunction(name, source, s);
}
}
}
/**
* Choose kernel type and save on .Kernel property of GPU
*/
chooseKernel() {
if (this.Kernel) return;
/**
*
* @type {WebGLKernel|WebGL2Kernel|HeadlessGLKernel|CPUKernel}
*/
let Kernel = null;
if (this.context) {
for (let i = 0; i < kernelOrder.length; i++) {
const ExternalKernel = kernelOrder[i];
if (ExternalKernel.isContextMatch(this.context)) {
if (!ExternalKernel.isSupported) {
throw new Error(`Kernel type ${ExternalKernel.name} not supported`);
}
Kernel = ExternalKernel;
break;
}
}
if (Kernel === null) {
throw new Error('unknown Context');
}
} else if (this.mode) {
if (this.mode in internalKernels) {
if (!validate || internalKernels[this.mode].isSupported) {
Kernel = internalKernels[this.mode];
}
} else if (this.mode === 'gpu') {
for (let i = 0; i < kernelOrder.length; i++) {
if (kernelOrder[i].isSupported) {
Kernel = kernelOrder[i];
break;
}
}
} else if (this.mode === 'cpu') {
Kernel = CPUKernel;
}
if (!Kernel) {
throw new Error(`A requested mode of "${this.mode}" and is not supported`);
}
} else {
for (let i = 0; i < kernelOrder.length; i++) {
if (kernelOrder[i].isSupported) {
Kernel = kernelOrder[i];
break;
}
}
if (!Kernel) {
Kernel = CPUKernel;
}
}
if (!this.mode) {
this.mode = Kernel.mode;
}
this.Kernel = Kernel;
}
/**
* @desc This creates a callable function object to call the kernel function with the argument parameter set
* @param {Function|String|object} source - The calling to perform the conversion
* @param {IGPUKernelSettings} [settings] - The parameter configuration object
* @return {IKernelRunShortcut} callable function to run
*/
createKernel(source, settings) {
if (typeof source === 'undefined') {
throw new Error('Missing source parameter');
}
if (typeof source !== 'object' && !utils.isFunction(source) && typeof source !== 'string') {
throw new Error('source parameter not a function');
}
const kernels = this.kernels;
if (this.mode === 'dev') {
const devKernel = gpuMock(source, upgradeDeprecatedCreateKernelSettings(settings));
kernels.push(devKernel);
return devKernel;
}
source = typeof source === 'function' ? source.toString() : source;
const switchableKernels = {};
const settingsCopy = upgradeDeprecatedCreateKernelSettings(settings) || {};
// handle conversion of argumentTypes
if (settings && typeof settings.argumentTypes === 'object') {
settingsCopy.argumentTypes = Object.keys(settings.argumentTypes).map(argumentName => settings.argumentTypes[argumentName]);
}
function onRequestFallback(args) {
console.warn('Falling back to CPU');
const fallbackKernel = new CPUKernel(source, {
argumentTypes: kernelRun.argumentTypes,
constantTypes: kernelRun.constantTypes,
graphical: kernelRun.graphical,
loopMaxIterations: kernelRun.loopMaxIterations,
constants: kernelRun.constants,
dynamicOutput: kernelRun.dynamicOutput,
dynamicArgument: kernelRun.dynamicArguments,
output: kernelRun.output,
precision: kernelRun.precision,
pipeline: kernelRun.pipeline,
immutable: kernelRun.immutable,
optimizeFloatMemory: kernelRun.optimizeFloatMemory,
fixIntegerDivisionAccuracy: kernelRun.fixIntegerDivisionAccuracy,
functions: kernelRun.functions,
nativeFunctions: kernelRun.nativeFunctions,
injectedNative: kernelRun.injectedNative,
subKernels: kernelRun.subKernels,
strictIntegers: kernelRun.strictIntegers,
debug: kernelRun.debug,
});
fallbackKernel.build.apply(fallbackKernel, args);
const result = fallbackKernel.run.apply(fallbackKernel, args);
kernelRun.replaceKernel(fallbackKernel);
return result;
}
/**
*
* @param {IReason[]} reasons
* @param {IArguments} args
* @param {Kernel} _kernel
* @returns {*}
*/
function onRequestSwitchKernel(reasons, args, _kernel) {
if (_kernel.debug) {
console.warn('Switching kernels');
}
let newOutput = null;
if (_kernel.signature && !switchableKernels[_kernel.signature]) {
switchableKernels[_kernel.signature] = _kernel;
}
if (_kernel.dynamicOutput) {
for (let i = reasons.length - 1; i >= 0; i--) {
const reason = reasons[i];
if (reason.type === 'outputPrecisionMismatch') {
newOutput = reason.needed;
}
}
}
const Constructor = _kernel.constructor;
const argumentTypes = Constructor.getArgumentTypes(_kernel, args);
const signature = Constructor.getSignature(_kernel, argumentTypes);
const existingKernel = switchableKernels[signature];
if (existingKernel) {
existingKernel.onActivate(_kernel);
return existingKernel;
}
const newKernel = switchableKernels[signature] = new Constructor(source, {
argumentTypes,
constantTypes: _kernel.constantTypes,
graphical: _kernel.graphical,
loopMaxIterations: _kernel.loopMaxIterations,
constants: _kernel.constants,
dynamicOutput: _kernel.dynamicOutput,
dynamicArgument: _kernel.dynamicArguments,
context: _kernel.context,
canvas: _kernel.canvas,
output: newOutput || _kernel.output,
precision: _kernel.precision,
pipeline: _kernel.pipeline,
immutable: _kernel.immutable,
optimizeFloatMemory: _kernel.optimizeFloatMemory,
fixIntegerDivisionAccuracy: _kernel.fixIntegerDivisionAccuracy,
functions: _kernel.functions,
nativeFunctions: _kernel.nativeFunctions,
injectedNative: _kernel.injectedNative,
subKernels: _kernel.subKernels,
strictIntegers: _kernel.strictIntegers,
debug: _kernel.debug,
gpu: _kernel.gpu,
validate,
returnType: _kernel.returnType,
tactic: _kernel.tactic,
onRequestFallback,
onRequestSwitchKernel,
texture: _kernel.texture,
mappedTextures: _kernel.mappedTextures,
drawBuffersMap: _kernel.drawBuffersMap,
});
newKernel.build.apply(newKernel, args);
kernelRun.replaceKernel(newKernel);
kernels.push(newKernel);
return newKernel;
}
const mergedSettings = Object.assign({
context: this.context,
canvas: this.canvas,
functions: this.functions,
nativeFunctions: this.nativeFunctions,
injectedNative: this.injectedNative,
gpu: this,
validate,
onRequestFallback,
onRequestSwitchKernel
}, settingsCopy);
const kernel = new this.Kernel(source, mergedSettings);
const kernelRun = kernelRunShortcut(kernel);
//if canvas didn't come from this, propagate from kernel
if (!this.canvas) {
this.canvas = kernel.canvas;
}
//if context didn't come from this, propagate from kernel
if (!this.context) {
this.context = kernel.context;
}
kernels.push(kernel);
return kernelRun;
}
/**
*
* Create a super kernel which executes sub kernels
* and saves their output to be used with the next sub kernel.
* This can be useful if we want to save the output on one kernel,
* and then use it as an input to another kernel. *Machine Learning*
*
* @param {Object|Array} subKernels - Sub kernels for this kernel
* @param {Function} rootKernel - Root kernel
*
* @returns {Function} callable kernel function
*
* @example
* const megaKernel = gpu.createKernelMap({
* addResult: function add(a, b) {
* return a[this.thread.x] + b[this.thread.x];
* },
* multiplyResult: function multiply(a, b) {
* return a[this.thread.x] * b[this.thread.x];
* },
* }, function(a, b, c) {
* return multiply(add(a, b), c);
* });
*
* megaKernel(a, b, c);
*
* Note: You can also define subKernels as an array of functions.
* > [add, multiply]
*
*/
createKernelMap() {
let fn;
let settings;
const argument2Type = typeof arguments[arguments.length - 2];
if (argument2Type === 'function' || argument2Type === 'string') {
fn = arguments[arguments.length - 2];
settings = arguments[arguments.length - 1];
} else {
fn = arguments[arguments.length - 1];
}
if (this.mode !== 'dev') {
if (!this.Kernel.isSupported || !this.Kernel.features.kernelMap) {
if (this.mode && kernelTypes.indexOf(this.mode) < 0) {
throw new Error(`kernelMap not supported on ${this.Kernel.name}`);
}
}
}
const settingsCopy = upgradeDeprecatedCreateKernelSettings(settings);
// handle conversion of argumentTypes
if (settings && typeof settings.argumentTypes === 'object') {
settingsCopy.argumentTypes = Object.keys(settings.argumentTypes).map(argumentName => settings.argumentTypes[argumentName]);
}
if (Array.isArray(arguments[0])) {
settingsCopy.subKernels = [];
const functions = arguments[0];
for (let i = 0; i < functions.length; i++) {
const source = functions[i].toString();
const name = utils.getFunctionNameFromString(source);
settingsCopy.subKernels.push({
name,
source,
property: i,
});
}
} else {
settingsCopy.subKernels = [];
const functions = arguments[0];
for (let p in functions) {
if (!functions.hasOwnProperty(p)) continue;
const source = functions[p].toString();
const name = utils.getFunctionNameFromString(source);
settingsCopy.subKernels.push({
name: name || p,
source,
property: p,
});
}
}
return this.createKernel(fn, settingsCopy);
}
/**
*
* Combine different kernels into one super Kernel,
* useful to perform multiple operations inside one
* kernel without the penalty of data transfer between
* cpu and gpu.
*
* The number of kernel functions sent to this method can be variable.
* You can send in one, two, etc.
*
* @param {Function} subKernels - Kernel function(s) to combine.
* @param {Function} rootKernel - Root kernel to combine kernels into
*
* @example
* combineKernels(add, multiply, function(a,b,c){
* return add(multiply(a,b), c)
* })
*
* @returns {Function} Callable kernel function
*
*/
combineKernels() {
const firstKernel = arguments[0];
const combinedKernel = arguments[arguments.length - 1];
if (firstKernel.kernel.constructor.mode === 'cpu') return combinedKernel;
const canvas = arguments[0].canvas;
const context = arguments[0].context;
const max = arguments.length - 1;
for (let i = 0; i < max; i++) {
arguments[i]
.setCanvas(canvas)
.setContext(context)
.setPipeline(true);
}
return function() {
const texture = combinedKernel.apply(this, arguments);
if (texture.toArray) {
return texture.toArray();
}
return texture;
};
}
setFunctions(functions) {
this.functions = functions;
return this;
}
setNativeFunctions(nativeFunctions) {
this.nativeFunctions = nativeFunctions;
return this;
}
/**
* @desc Adds additional functions, that the kernel may call.
* @param {Function|String} source - Javascript function to convert
* @param {IFunctionSettings} [settings]
* @returns {GPU} returns itself
*/
addFunction(source, settings) {
this.functions.push({ source, settings });
return this;
}
/**
* @desc Adds additional native functions, that the kernel may call.
* @param {String} name - native function name, used for reverse lookup
* @param {String} source - the native function implementation, as it would be defined in it's entirety
* @param {object} [settings]
* @returns {GPU} returns itself
*/
addNativeFunction(name, source, settings) {
if (this.kernels.length > 0) {
throw new Error('Cannot call "addNativeFunction" after "createKernels" has been called.');
}
this.nativeFunctions.push(Object.assign({ name, source }, settings));
return this;
}
/**
* Inject a string just before translated kernel functions
* @param {String} source
* @return {GPU}
*/
injectNative(source) {
this.injectedNative = source;
return this;
}
/**
* @desc Destroys all memory associated with gpu.js & the webGl if we created it
* @return {Promise}
* @resolve {void}
* @reject {Error}
*/
destroy() {
return new Promise((resolve, reject) => {
if (!this.kernels) {
resolve();
}
// perform on next run loop - for some reason we dont get lose context events
// if webGl is created and destroyed in the same run loop.
setTimeout(() => {
try {
for (let i = 0; i < this.kernels.length; i++) {
this.kernels[i].destroy(true); // remove canvas if exists
}
// all kernels are associated with one context, go ahead and take care of it here
let firstKernel = this.kernels[0];
if (firstKernel) {
// if it is shortcut
if (firstKernel.kernel) {
firstKernel = firstKernel.kernel;
}
if (firstKernel.constructor.destroyContext) {
firstKernel.constructor.destroyContext(this.context);
}
}
} catch (e) {
reject(e);
}
resolve();
}, 0);
});
}
}
function upgradeDeprecatedCreateKernelSettings(settings) {
if (!settings) {
return {};
}
const upgradedSettings = Object.assign({}, settings);
if (settings.hasOwnProperty('floatOutput')) {
utils.warnDeprecated('setting', 'floatOutput', 'precision');
upgradedSettings.precision = settings.floatOutput ? 'single' : 'unsigned';
}
if (settings.hasOwnProperty('outputToTexture')) {
utils.warnDeprecated('setting', 'outputToTexture', 'pipeline');
upgradedSettings.pipeline = Boolean(settings.outputToTexture);
}
if (settings.hasOwnProperty('outputImmutable')) {
utils.warnDeprecated('setting', 'outputImmutable', 'immutable');
upgradedSettings.immutable = Boolean(settings.outputImmutable);
}
if (settings.hasOwnProperty('floatTextures')) {
utils.warnDeprecated('setting', 'floatTextures', 'optimizeFloatMemory');
upgradedSettings.optimizeFloatMemory = Boolean(settings.floatTextures);
}
return upgradedSettings;
}
module.exports = {
GPU,
kernelOrder,
kernelTypes
};