UNPKG

gpu.js

Version:

GPU Accelerated JavaScript

949 lines (854 loc) 22.3 kB
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.<string, string>} */ this.constantTypes = null; /** * * @type {Object.<string, number>} */ this.constantBitRatios = null; /** * * @type {boolean} */ this.dynamicArguments = false; /** * * @type {boolean} */ this.dynamicOutput = false; /** * * @type {Object} */ this.canvas = null; /** * * @type {Object} */ this.context = null; /** * * @type {Boolean} */ this.checkContext = null; /** * * @type {GPU} */ this.gpu = null; /** * * @type {IGPUFunction[]} */ this.functions = null; /** * * @type {IGPUNativeFunction[]} */ this.nativeFunctions = null; /** * * @type {String} */ this.injectedNative = null; /** * * @type {ISubKernel[]} */ this.subKernels = null; /** * * @type {Boolean} */ this.validate = true; /** * Enforces kernel to write to a new array or texture on run * @type {Boolean} */ this.immutable = false; /** * Enforces kernel to write to a texture on run * @type {Boolean} */ this.pipeline = false; /** * Make GPU use single precision or unsigned. Acceptable values: 'single' or 'unsigned' * @type {String|null} * @enum 'single' | 'unsigned' */ this.precision = null; /** * * @type {String|null} * @enum 'speed' | 'balanced' | 'precision' */ this.tactic = null; this.plugins = null; this.returnType = null; this.leadingReturnStatement = null; this.followingReturnStatement = null; this.optimizeFloatMemory = null; this.strictIntegers = false; this.fixIntegerDivisionAccuracy = null; this.built = false; this.signature = null; } /** * * @param {IDirectKernelSettings|IJSONSettings} settings */ mergeSettings(settings) { for (let p in settings) { if (!settings.hasOwnProperty(p) || !this.hasOwnProperty(p)) continue; switch (p) { case 'output': if (!Array.isArray(settings.output)) { this.setOutput(settings.output); // Flatten output object continue; } break; case 'functions': this.functions = []; for (let i = 0; i < settings.functions.length; i++) { this.addFunction(settings.functions[i]); } continue; case 'graphical': if (settings[p] && !settings.hasOwnProperty('precision')) { this.precision = 'unsigned'; } this[p] = settings[p]; continue; case 'nativeFunctions': if (!settings.nativeFunctions) continue; this.nativeFunctions = []; for (let i = 0; i < settings.nativeFunctions.length; i++) { const s = settings.nativeFunctions[i]; const { name, source } = s; this.addNativeFunction(name, source, s); } continue; } this[p] = settings[p]; } if (!this.canvas) this.canvas = this.initCanvas(); if (!this.context) this.context = this.initContext(); if (!this.plugins) this.plugins = this.initPlugins(settings); } /** * @desc Builds the Kernel, by compiling Fragment and Vertical Shaders, * and instantiates the program. * @abstract */ build() { throw new Error(`"build" not defined on ${ this.constructor.name }`); } /** * @desc Run the kernel program, and send the output to renderOutput * <p> This method calls a helper method *renderOutput* to return the result. </p> * @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 };