UNPKG

gpu.js

Version:

GPU Accelerated JavaScript

1,607 lines (1,475 loc) 50 kB
const { GLKernel } = require('../gl/kernel'); const { FunctionBuilder } = require('../function-builder'); const { WebGLFunctionNode } = require('./function-node'); const { utils } = require('../../utils'); const mrud = require('../../plugins/math-random-uniformly-distributed'); const { fragmentShader } = require('./fragment-shader'); const { vertexShader } = require('./vertex-shader'); const { glKernelString } = require('../gl/kernel-string'); const { lookupKernelValueType } = require('./kernel-value-maps'); let isSupported = null; /** * * @type {HTMLCanvasElement|OffscreenCanvas|null} */ let testCanvas = null; /** * * @type {WebGLRenderingContext|null} */ let testContext = null; let testExtensions = null; let features = null; const plugins = [mrud]; const canvases = []; const maxTexSizes = {}; /** * @desc Kernel Implementation for WebGL. * <p>This builds the shaders and runs them on the GPU, * the outputs the result back as float(enabled by default) and Texture.</p> * * @property {WebGLTexture[]} textureCache - webGl Texture cache * @property {Object.<string, WebGLUniformLocation>} programUniformLocationCache - Location of program variables in memory * @property {WebGLFramebuffer} framebuffer - Webgl frameBuffer * @property {WebGLBuffer} buffer - WebGL buffer * @property {WebGLProgram} program - The webGl Program * @property {FunctionBuilder} functionBuilder - Function Builder instance bound to this Kernel * @property {Boolean} pipeline - Set output type to FAST mode (GPU to GPU via Textures), instead of float * @property {string} endianness - Endian information like Little-endian, Big-endian. * @property {string[]} argumentTypes - Types of parameters sent to the Kernel * @property {string|null} compiledFragmentShader - Compiled fragment shader string * @property {string|null} compiledVertexShader - Compiled Vertical shader string * @extends GLKernel */ class WebGLKernel extends GLKernel { static get isSupported() { if (isSupported !== null) { return isSupported; } this.setupFeatureChecks(); isSupported = this.isContextMatch(testContext); return isSupported; } static setupFeatureChecks() { if (typeof document !== 'undefined') { testCanvas = document.createElement('canvas'); } else if (typeof OffscreenCanvas !== 'undefined') { testCanvas = new OffscreenCanvas(0, 0); } if (!testCanvas) return; testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); if (!testContext || !testContext.getExtension) return; testExtensions = { 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'), }; features = this.getFeatures(); } static isContextMatch(context) { if (typeof WebGLRenderingContext !== 'undefined') { return context instanceof WebGLRenderingContext; } 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); } /** * * @param type * @param dynamic * @param precision * @param value * @returns {KernelValue} */ static lookupKernelValueType(type, dynamic, precision, value) { return lookupKernelValueType(type, dynamic, precision, value); } static get testCanvas() { return testCanvas; } static get testContext() { return testContext; } static get features() { return features; } static get fragmentShader() { return fragmentShader; } static get vertexShader() { return vertexShader; } /** * * @param {String|IKernelJSON} source * @param {IDirectKernelSettings} settings */ constructor(source, settings) { super(source, settings); this.program = null; this.pipeline = settings.pipeline; this.endianness = utils.systemEndianness(); this.extensions = {}; this.argumentTextureCount = 0; this.constantTextureCount = 0; this.fragShader = null; this.vertShader = null; this.drawBuffersMap = null; /** * * @type {Int32Array|null} */ this.maxTexSize = null; this.onRequestSwitchKernel = null; this.texture = null; this.mappedTextures = null; this.mergeSettings(source.settings || settings); /** * The thread dimensions, x, y and z * @type {Array|null} */ this.threadDim = null; this.framebuffer = null; this.buffer = null; this.textureCache = []; this.programUniformLocationCache = {}; this.uniform1fCache = {}; this.uniform1iCache = {}; this.uniform2fCache = {}; this.uniform2fvCache = {}; this.uniform2ivCache = {}; this.uniform3fvCache = {}; this.uniform3ivCache = {}; this.uniform4fvCache = {}; this.uniform4ivCache = {}; } initCanvas() { if (typeof document !== 'undefined') { const canvas = document.createElement('canvas'); // Default width and height, to fix webgl issue in safari canvas.width = 2; canvas.height = 2; return canvas; } else if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(0, 0); } } /** * * @return {WebGLRenderingContext} */ initContext() { const settings = { alpha: false, depth: false, antialias: false }; return this.canvas.getContext('webgl', settings) || this.canvas.getContext('experimental-webgl', settings); } /** * * @param {IDirectKernelSettings} settings * @return {string[]} */ initPlugins(settings) { // default plugins const pluginsToUse = []; const { source } = this; if (typeof source === 'string') { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (source.match(plugin.functionMatch)) { pluginsToUse.push(plugin); } } } else if (typeof source === 'object') { // `source` is from object, json if (settings.pluginNames) { //TODO: in context of JSON support, pluginNames may not exist here for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; const usePlugin = settings.pluginNames.some(pluginName => pluginName === plugin.name); if (usePlugin) { pluginsToUse.push(plugin); } } } } return pluginsToUse; } initExtensions() { this.extensions = { 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'), WEBGL_color_buffer_float: this.context.getExtension('WEBGL_color_buffer_float'), }; } /** * @desc Validate settings related to Kernel, such as dimensions size, and auto output support. * @param {IArguments} args */ validateSettings(args) { if (!this.validate) { this.texSize = utils.getKernelTextureSize({ optimizeFloatMemory: this.optimizeFloatMemory, precision: this.precision, }, this.output); return; } const { features } = this.constructor; if (this.optimizeFloatMemory === true && !features.isTextureFloat) { throw new Error('Float textures are not supported'); } else if (this.precision === 'single' && !features.isFloatRead) { throw new Error('Single precision not supported'); } else if (!this.graphical && this.precision === null && features.isTextureFloat) { this.precision = features.isFloatRead ? 'single' : 'unsigned'; } if (this.subKernels && this.subKernels.length > 0 && !this.extensions.WEBGL_draw_buffers) { throw new Error('could not instantiate draw buffers extension'); } if (this.fixIntegerDivisionAccuracy === null) { this.fixIntegerDivisionAccuracy = !features.isIntegerDivisionAccurate; } else if (this.fixIntegerDivisionAccuracy && features.isIntegerDivisionAccurate) { this.fixIntegerDivisionAccuracy = false; } this.checkOutput(); 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); switch (argType) { case 'Array': this.output = utils.getDimensions(argType); break; case 'NumberTexture': case 'MemoryOptimizedNumberTexture': case 'ArrayTexture(1)': case 'ArrayTexture(2)': case 'ArrayTexture(3)': case 'ArrayTexture(4)': this.output = args[0].output; break; default: 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'); } if (this.precision === 'precision') { this.precision = 'unsigned'; console.warn('Cannot use graphical mode and single precision at the same time'); } this.texSize = utils.clone(this.output); return; } else if (this.precision === null && features.isTextureFloat) { this.precision = 'single'; } this.texSize = utils.getKernelTextureSize({ optimizeFloatMemory: this.optimizeFloatMemory, precision: this.precision, }, this.output); this.checkTextureSize(); } updateMaxTexSize() { const { texSize, canvas } = this; if (this.maxTexSize === null) { let canvasIndex = canvases.indexOf(canvas); if (canvasIndex === -1) { canvasIndex = canvases.length; canvases.push(canvas); maxTexSizes[canvasIndex] = [texSize[0], texSize[1]]; } this.maxTexSize = maxTexSizes[canvasIndex]; } if (this.maxTexSize[0] < texSize[0]) { this.maxTexSize[0] = texSize[0]; } if (this.maxTexSize[1] < texSize[1]) { this.maxTexSize[1] = texSize[1]; } } setupArguments(args) { this.kernelArguments = []; this.argumentTextureCount = 0; const needsArgumentTypes = this.argumentTypes === null; // TODO: remove if (needsArgumentTypes) { this.argumentTypes = []; } this.argumentSizes = []; this.argumentBitRatios = []; // TODO: end remove if (args.length < this.argumentNames.length) { throw new Error('not enough arguments for kernel'); } else if (args.length > this.argumentNames.length) { throw new Error('too many arguments for kernel'); } const { context: gl } = this; let textureIndexes = 0; const onRequestTexture = () => { return this.createTexture(); }; const onRequestIndex = () => { return this.constantTextureCount + textureIndexes++; }; const onUpdateValueMismatch = (constructor) => { this.switchKernels({ type: 'argumentMismatch', needed: constructor }); }; const onRequestContextHandle = () => { return gl.TEXTURE0 + this.constantTextureCount + this.argumentTextureCount++; }; for (let index = 0; index < args.length; index++) { const value = args[index]; const name = this.argumentNames[index]; let type; if (needsArgumentTypes) { type = utils.getVariableType(value, this.strictIntegers); this.argumentTypes.push(type); } else { type = this.argumentTypes[index]; } const KernelValue = this.constructor.lookupKernelValueType(type, this.dynamicArguments ? 'dynamic' : 'static', this.precision, args[index]); if (KernelValue === null) { return this.requestFallback(args); } const kernelArgument = new KernelValue(value, { name, type, tactic: this.tactic, origin: 'user', context: gl, checkContext: this.checkContext, kernel: this, strictIntegers: this.strictIntegers, onRequestTexture, onRequestIndex, onUpdateValueMismatch, onRequestContextHandle, }); this.kernelArguments.push(kernelArgument); kernelArgument.setup(); this.argumentSizes.push(kernelArgument.textureSize); this.argumentBitRatios[index] = kernelArgument.bitRatio; } } createTexture() { const texture = this.context.createTexture(); this.textureCache.push(texture); return texture; } setupConstants(args) { const { context: gl } = this; this.kernelConstants = []; this.forceUploadKernelConstants = []; let needsConstantTypes = this.constantTypes === null; if (needsConstantTypes) { this.constantTypes = {}; } this.constantBitRatios = {}; let textureIndexes = 0; for (const name in this.constants) { const value = this.constants[name]; let type; if (needsConstantTypes) { type = utils.getVariableType(value, this.strictIntegers); this.constantTypes[name] = type; } else { type = this.constantTypes[name]; } const KernelValue = this.constructor.lookupKernelValueType(type, 'static', this.precision, value); if (KernelValue === null) { return this.requestFallback(args); } const kernelValue = new KernelValue(value, { name, type, tactic: this.tactic, origin: 'constants', context: this.context, checkContext: this.checkContext, kernel: this, strictIntegers: this.strictIntegers, onRequestTexture: () => { return this.createTexture(); }, onRequestIndex: () => { return textureIndexes++; }, onRequestContextHandle: () => { return gl.TEXTURE0 + this.constantTextureCount++; } }); this.constantBitRatios[name] = kernelValue.bitRatio; this.kernelConstants.push(kernelValue); kernelValue.setup(); if (kernelValue.forceUploadEachRun) { this.forceUploadKernelConstants.push(kernelValue); } } } build() { if (this.built) return; this.initExtensions(); this.validateSettings(arguments); this.setupConstants(arguments); if (this.fallbackRequested) return; this.setupArguments(arguments); if (this.fallbackRequested) return; this.updateMaxTexSize(); this.translateSource(); const failureResult = this.pickRenderStrategy(arguments); if (failureResult) { return failureResult; } const { texSize, context: gl, canvas } = this; gl.enable(gl.SCISSOR_TEST); if (this.pipeline && this.precision === 'single') { gl.viewport(0, 0, this.maxTexSize[0], this.maxTexSize[1]); canvas.width = this.maxTexSize[0]; canvas.height = this.maxTexSize[1]; } else { gl.viewport(0, 0, this.maxTexSize[0], this.maxTexSize[1]); canvas.width = this.maxTexSize[0]; canvas.height = this.maxTexSize[1]; } const threadDim = this.threadDim = Array.from(this.output); while (threadDim.length < 3) { threadDim.push(1); } const compiledVertexShader = this.getVertexShader(arguments); const vertShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertShader, compiledVertexShader); gl.compileShader(vertShader); this.vertShader = vertShader; const compiledFragmentShader = this.getFragmentShader(arguments); const fragShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragShader, compiledFragmentShader); gl.compileShader(fragShader); this.fragShader = fragShader; if (this.debug) { console.log('GLSL Shader Output:'); console.log(compiledFragmentShader); } if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { throw new Error('Error compiling vertex shader: ' + gl.getShaderInfoLog(vertShader)); } if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { throw new Error('Error compiling fragment shader: ' + gl.getShaderInfoLog(fragShader)); } const program = this.program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.linkProgram(program); this.framebuffer = gl.createFramebuffer(); this.framebuffer.width = texSize[0]; this.framebuffer.height = texSize[1]; this.rawValueFramebuffers = {}; const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1 ]); const texCoords = new Float32Array([ 0, 0, 1, 0, 0, 1, 1, 1 ]); const texCoordOffset = vertices.byteLength; let buffer = this.buffer; if (!buffer) { buffer = this.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, vertices.byteLength + texCoords.byteLength, gl.STATIC_DRAW); } else { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); } gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertices); gl.bufferSubData(gl.ARRAY_BUFFER, texCoordOffset, texCoords); const aPosLoc = gl.getAttribLocation(this.program, 'aPos'); gl.enableVertexAttribArray(aPosLoc); gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0); const aTexCoordLoc = gl.getAttribLocation(this.program, 'aTexCoord'); gl.enableVertexAttribArray(aTexCoordLoc); gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); let i = 0; gl.useProgram(this.program); for (let p in this.constants) { this.kernelConstants[i++].updateValue(this.constants[p]); } this._setupOutputTexture(); if ( this.subKernels !== null && this.subKernels.length > 0 ) { this._mappedTextureSwitched = {}; this._setupSubOutputTextures(); } this.buildSignature(arguments); this.built = true; } translateSource() { const functionBuilder = FunctionBuilder.fromKernel(this, WebGLFunctionNode, { fixIntegerDivisionAccuracy: this.fixIntegerDivisionAccuracy }); this.translatedSource = functionBuilder.getPrototypeString('kernel'); this.setupReturnTypes(functionBuilder); } setupReturnTypes(functionBuilder) { if (!this.graphical && !this.returnType) { this.returnType = functionBuilder.getKernelResultType(); } if (this.subKernels && this.subKernels.length > 0) { for (let i = 0; i < this.subKernels.length; i++) { const subKernel = this.subKernels[i]; if (!subKernel.returnType) { subKernel.returnType = functionBuilder.getSubKernelResultType(i); } } } } run() { const { kernelArguments, texSize, forceUploadKernelConstants, context: gl } = this; gl.useProgram(this.program); gl.scissor(0, 0, texSize[0], texSize[1]); if (this.dynamicOutput) { this.setUniform3iv('uOutputDim', new Int32Array(this.threadDim)); this.setUniform2iv('uTexSize', texSize); } this.setUniform2f('ratio', texSize[0] / this.maxTexSize[0], texSize[1] / this.maxTexSize[1]); for (let i = 0; i < forceUploadKernelConstants.length; i++) { const constant = forceUploadKernelConstants[i]; constant.updateValue(this.constants[constant.name]); if (this.switchingKernels) return; } for (let i = 0; i < kernelArguments.length; i++) { kernelArguments[i].updateValue(arguments[i]); if (this.switchingKernels) return; } if (this.plugins) { for (let i = 0; i < this.plugins.length; i++) { const plugin = this.plugins[i]; if (plugin.onBeforeRun) { plugin.onBeforeRun(this); } } } if (this.graphical) { if (this.pipeline) { gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); if (this.immutable) { this._replaceOutputTexture(); } gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); return this.immutable ? this.texture.clone() : this.texture; } gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); return; } gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); if (this.immutable) { this._replaceOutputTexture(); } if (this.subKernels !== null) { if (this.immutable) { this._replaceSubOutputTextures(); } this.drawBuffers(); } gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } drawBuffers() { this.extensions.WEBGL_draw_buffers.drawBuffersWEBGL(this.drawBuffersMap); } getInternalFormat() { return this.context.RGBA; } getTextureFormat() { const { context: gl } = this; switch (this.getInternalFormat()) { case gl.RGBA: return gl.RGBA; default: throw new Error('Unknown internal format'); } } /** * * @desc replace output textures where arguments my be the same values */ _replaceOutputTexture() { if (this.texture.beforeMutate() || this._textureSwitched) { const gl = this.context; gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture.texture, 0); this._textureSwitched = false; } } /** * @desc Setup output texture */ _setupOutputTexture() { const gl = this.context; const texSize = this.texSize; if (this.texture) { // here we inherit from an already existing kernel, so go ahead and just bind textures to the framebuffer gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture.texture, 0); return; } const texture = this.createTexture(); gl.activeTexture(gl.TEXTURE0 + this.constantTextureCount + this.argumentTextureCount); 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); const format = this.getInternalFormat(); if (this.precision === 'single') { gl.texImage2D(gl.TEXTURE_2D, 0, format, texSize[0], texSize[1], 0, gl.RGBA, gl.FLOAT, null); } else { gl.texImage2D(gl.TEXTURE_2D, 0, format, texSize[0], texSize[1], 0, format, gl.UNSIGNED_BYTE, null); } gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); this.texture = new this.TextureConstructor({ texture, size: texSize, dimensions: this.threadDim, output: this.output, context: this.context, internalFormat: this.getInternalFormat(), textureFormat: this.getTextureFormat(), kernel: this, }); } /** * * @desc replace sub-output textures where arguments my be the same values */ _replaceSubOutputTextures() { const gl = this.context; for (let i = 0; i < this.mappedTextures.length; i++) { const mappedTexture = this.mappedTextures[i]; if (mappedTexture.beforeMutate() || this._mappedTextureSwitched[i]) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i + 1, gl.TEXTURE_2D, mappedTexture.texture, 0); this._mappedTextureSwitched[i] = false; } } } /** * @desc Setup on inherit sub-output textures */ _setupSubOutputTextures() { const gl = this.context; if (this.mappedTextures) { // here we inherit from an already existing kernel, so go ahead and just bind textures to the framebuffer for (let i = 0; i < this.subKernels.length; i++) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i + 1, gl.TEXTURE_2D, this.mappedTextures[i].texture, 0); } return; } const texSize = this.texSize; this.drawBuffersMap = [gl.COLOR_ATTACHMENT0]; this.mappedTextures = []; for (let i = 0; i < this.subKernels.length; i++) { const texture = this.createTexture(); this.drawBuffersMap.push(gl.COLOR_ATTACHMENT0 + i + 1); gl.activeTexture(gl.TEXTURE0 + this.constantTextureCount + this.argumentTextureCount + i); 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); if (this.precision === 'single') { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texSize[0], texSize[1], 0, gl.RGBA, gl.FLOAT, null); } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texSize[0], texSize[1], 0, gl.RGBA, gl.UNSIGNED_BYTE, null); } gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i + 1, gl.TEXTURE_2D, texture, 0); this.mappedTextures.push(new this.TextureConstructor({ texture, size: texSize, dimensions: this.threadDim, output: this.output, context: this.context, internalFormat: this.getInternalFormat(), textureFormat: this.getTextureFormat(), kernel: this, })); } } setUniform1f(name, value) { if (this.uniform1fCache.hasOwnProperty(name)) { const cache = this.uniform1fCache[name]; if (value === cache) { return; } } this.uniform1fCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform1f(loc, value); } setUniform1i(name, value) { if (this.uniform1iCache.hasOwnProperty(name)) { const cache = this.uniform1iCache[name]; if (value === cache) { return; } } this.uniform1iCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform1i(loc, value); } setUniform2f(name, value1, value2) { if (this.uniform2fCache.hasOwnProperty(name)) { const cache = this.uniform2fCache[name]; if ( value1 === cache[0] && value2 === cache[1] ) { return; } } this.uniform2fCache[name] = [value1, value2]; const loc = this.getUniformLocation(name); this.context.uniform2f(loc, value1, value2); } setUniform2fv(name, value) { if (this.uniform2fvCache.hasOwnProperty(name)) { const cache = this.uniform2fvCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] ) { return; } } this.uniform2fvCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform2fv(loc, value); } setUniform2iv(name, value) { if (this.uniform2ivCache.hasOwnProperty(name)) { const cache = this.uniform2ivCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] ) { return; } } this.uniform2ivCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform2iv(loc, value); } setUniform3fv(name, value) { if (this.uniform3fvCache.hasOwnProperty(name)) { const cache = this.uniform3fvCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] && value[2] === cache[2] ) { return; } } this.uniform3fvCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform3fv(loc, value); } setUniform3iv(name, value) { if (this.uniform3ivCache.hasOwnProperty(name)) { const cache = this.uniform3ivCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] && value[2] === cache[2] ) { return; } } this.uniform3ivCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform3iv(loc, value); } setUniform4fv(name, value) { if (this.uniform4fvCache.hasOwnProperty(name)) { const cache = this.uniform4fvCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] && value[2] === cache[2] && value[3] === cache[3] ) { return; } } this.uniform4fvCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform4fv(loc, value); } setUniform4iv(name, value) { if (this.uniform4ivCache.hasOwnProperty(name)) { const cache = this.uniform4ivCache[name]; if ( value[0] === cache[0] && value[1] === cache[1] && value[2] === cache[2] && value[3] === cache[3] ) { return; } } this.uniform4ivCache[name] = value; const loc = this.getUniformLocation(name); this.context.uniform4iv(loc, value); } /** * @desc Return WebGlUniformLocation for various variables * related to webGl program, such as user-defined variables, * as well as, dimension sizes, etc. */ getUniformLocation(name) { if (this.programUniformLocationCache.hasOwnProperty(name)) { return this.programUniformLocationCache[name]; } return this.programUniformLocationCache[name] = this.context.getUniformLocation(this.program, name); } /** * @desc Generate Shader artifacts for the kernel program. * The final object contains HEADER, KERNEL, MAIN_RESULT, and others. * * @param {Array} args - The actual parameters sent to the Kernel * @returns {Object} An object containing the Shader Artifacts(CONSTANTS, HEADER, KERNEL, etc.) */ _getFragShaderArtifactMap(args) { return { HEADER: this._getHeaderString(), LOOP_MAX: this._getLoopMaxString(), PLUGINS: this._getPluginsString(), CONSTANTS: this._getConstantsString(), DECODE32_ENDIANNESS: this._getDecode32EndiannessString(), ENCODE32_ENDIANNESS: this._getEncode32EndiannessString(), DIVIDE_WITH_INTEGER_CHECK: this._getDivideWithIntegerCheckString(), INJECTED_NATIVE: this._getInjectedNative(), MAIN_CONSTANTS: this._getMainConstantsString(), MAIN_ARGUMENTS: this._getMainArgumentsString(args), KERNEL: this.getKernelString(), MAIN_RESULT: this.getMainResultString(), FLOAT_TACTIC_DECLARATION: this.getFloatTacticDeclaration(), INT_TACTIC_DECLARATION: this.getIntTacticDeclaration(), SAMPLER_2D_TACTIC_DECLARATION: this.getSampler2DTacticDeclaration(), SAMPLER_2D_ARRAY_TACTIC_DECLARATION: this.getSampler2DArrayTacticDeclaration(), }; } /** * @desc Generate Shader artifacts for the kernel program. * The final object contains HEADER, KERNEL, MAIN_RESULT, and others. * * @param {Array} args - The actual parameters sent to the Kernel * @returns {Object} An object containing the Shader Artifacts(CONSTANTS, HEADER, KERNEL, etc.) */ _getVertShaderArtifactMap(args) { return { FLOAT_TACTIC_DECLARATION: this.getFloatTacticDeclaration(), INT_TACTIC_DECLARATION: this.getIntTacticDeclaration(), SAMPLER_2D_TACTIC_DECLARATION: this.getSampler2DTacticDeclaration(), SAMPLER_2D_ARRAY_TACTIC_DECLARATION: this.getSampler2DArrayTacticDeclaration(), }; } /** * @desc Get the header string for the program. * This returns an empty string if no sub-kernels are defined. * * @returns {String} result */ _getHeaderString() { return ( this.subKernels !== null ? '#extension GL_EXT_draw_buffers : require\n' : '' ); } /** * @desc Get the maximum loop size String. * @returns {String} result */ _getLoopMaxString() { return ( this.loopMaxIterations ? ` ${parseInt(this.loopMaxIterations)};\n` : ' 1000;\n' ); } _getPluginsString() { if (!this.plugins) return '\n'; return this.plugins.map(plugin => plugin.source && this.source.match(plugin.functionMatch) ? plugin.source : '').join('\n'); } /** * @desc Generate transpiled glsl Strings for constant parameters sent to a kernel * @returns {String} result */ _getConstantsString() { const result = []; const { threadDim, texSize } = this; if (this.dynamicOutput) { result.push( 'uniform ivec3 uOutputDim', 'uniform ivec2 uTexSize' ); } else { result.push( `ivec3 uOutputDim = ivec3(${threadDim[0]}, ${threadDim[1]}, ${threadDim[2]})`, `ivec2 uTexSize = ivec2(${texSize[0]}, ${texSize[1]})` ); } return utils.linesToString(result); } /** * @desc Get texture coordinate string for the program * @returns {String} result */ _getTextureCoordinate() { const subKernels = this.subKernels; if (subKernels === null || subKernels.length < 1) { return 'varying vec2 vTexCoord;\n'; } else { return 'out vec2 vTexCoord;\n'; } } /** * @desc Get Decode32 endianness string for little-endian and big-endian * @returns {String} result */ _getDecode32EndiannessString() { return ( this.endianness === 'LE' ? '' : ' texel.rgba = texel.abgr;\n' ); } /** * @desc Get Encode32 endianness string for little-endian and big-endian * @returns {String} result */ _getEncode32EndiannessString() { return ( this.endianness === 'LE' ? '' : ' texel.rgba = texel.abgr;\n' ); } /** * @desc if fixIntegerDivisionAccuracy provide method to replace / * @returns {String} result */ _getDivideWithIntegerCheckString() { return this.fixIntegerDivisionAccuracy ? `float divWithIntCheck(float x, float y) { if (floor(x) == x && floor(y) == y && integerMod(x, y) == 0.0) { return float(int(x) / int(y)); } return x / y; } float integerCorrectionModulo(float number, float divisor) { if (number < 0.0) { number = abs(number); if (divisor < 0.0) { divisor = abs(divisor); } return -(number - (divisor * floor(divWithIntCheck(number, divisor)))); } if (divisor < 0.0) { divisor = abs(divisor); } return number - (divisor * floor(divWithIntCheck(number, divisor))); }` : ''; } /** * @desc Generate transpiled glsl Strings for user-defined parameters sent to a kernel * @param {Array} args - The actual parameters sent to the Kernel * @returns {String} result */ _getMainArgumentsString(args) { const results = []; const { argumentNames } = this; for (let i = 0; i < argumentNames.length; i++) { results.push(this.kernelArguments[i].getSource(args[i])); } return results.join(''); } _getInjectedNative() { return this.injectedNative || ''; } _getMainConstantsString() { const result = []; const { constants } = this; if (constants) { let i = 0; for (const name in constants) { if (!this.constants.hasOwnProperty(name)) continue; result.push(this.kernelConstants[i++].getSource(this.constants[name])); } } return result.join(''); } getRawValueFramebuffer(width, height) { if (!this.rawValueFramebuffers[width]) { this.rawValueFramebuffers[width] = {}; } if (!this.rawValueFramebuffers[width][height]) { const framebuffer = this.context.createFramebuffer(); framebuffer.width = width; framebuffer.height = height; this.rawValueFramebuffers[width][height] = framebuffer; } return this.rawValueFramebuffers[width][height]; } getKernelResultDeclaration() { switch (this.returnType) { case 'Array(2)': return 'vec2 kernelResult'; case 'Array(3)': return 'vec3 kernelResult'; case 'Array(4)': return 'vec4 kernelResult'; case 'LiteralInteger': case 'Float': case 'Number': case 'Integer': return 'float kernelResult'; default: if (this.graphical) { return 'float kernelResult'; } else { throw new Error(`unrecognized output type "${ this.returnType }"`); } } } /** * @desc Get Kernel program string (in *glsl*) for a kernel. * @returns {String} result */ getKernelString() { const result = [this.getKernelResultDeclaration()]; const { subKernels } = this; if (subKernels !== null) { switch (this.returnType) { case 'Number': case 'Float': case 'Integer': for (let i = 0; i < subKernels.length; i++) { const subKernel = subKernels[i]; result.push( subKernel.returnType === 'Integer' ? `int subKernelResult_${ subKernel.name } = 0` : `float subKernelResult_${ subKernel.name } = 0.0` ); } break; case 'Array(2)': for (let i = 0; i < subKernels.length; i++) { result.push( `vec2 subKernelResult_${ subKernels[i].name }` ); } break; case 'Array(3)': for (let i = 0; i < subKernels.length; i++) { result.push( `vec3 subKernelResult_${ subKernels[i].name }` ); } break; case 'Array(4)': for (let i = 0; i < subKernels.length; i++) { result.push( `vec4 subKernelResult_${ subKernels[i].name }` ); } break; } } return utils.linesToString(result) + this.translatedSource; } getMainResultGraphical() { return utils.linesToString([ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ' gl_FragColor = actualColor', ]); } getMainResultPackedPixels() { switch (this.returnType) { case 'LiteralInteger': case 'Number': case 'Integer': case 'Float': return this.getMainResultKernelPackedPixels() + this.getMainResultSubKernelPackedPixels(); default: throw new Error(`packed output only usable with Numbers, "${this.returnType}" specified`); } } /** * @return {String} */ getMainResultKernelPackedPixels() { return utils.linesToString([ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ` gl_FragData[0] = ${this.useLegacyEncoder ? 'legacyEncode32' : 'encode32'}(kernelResult)` ]); } /** * @return {String} */ getMainResultSubKernelPackedPixels() { const result = []; if (!this.subKernels) return ''; for (let i = 0; i < this.subKernels.length; i++) { const subKernel = this.subKernels[i]; if (subKernel.returnType === 'Integer') { result.push( ` gl_FragData[${i + 1}] = ${this.useLegacyEncoder ? 'legacyEncode32' : 'encode32'}(float(subKernelResult_${this.subKernels[i].name}))` ); } else { result.push( ` gl_FragData[${i + 1}] = ${this.useLegacyEncoder ? 'legacyEncode32' : 'encode32'}(subKernelResult_${this.subKernels[i].name})` ); } } return utils.linesToString(result); } getMainResultMemoryOptimizedFloats() { const result = [ ' index *= 4', ]; switch (this.returnType) { case 'Number': case 'Integer': case 'Float': const channels = ['r', 'g', 'b', 'a']; for (let i = 0; i < channels.length; i++) { const channel = channels[i]; this.getMainResultKernelMemoryOptimizedFloats(result, channel); this.getMainResultSubKernelMemoryOptimizedFloats(result, channel); if (i + 1 < channels.length) { result.push(' index += 1'); } } break; default: throw new Error(`optimized output only usable with Numbers, ${this.returnType} specified`); } return utils.linesToString(result); } getMainResultKernelMemoryOptimizedFloats(result, channel) { result.push( ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ` gl_FragData[0].${channel} = kernelResult` ); } getMainResultSubKernelMemoryOptimizedFloats(result, channel) { if (!this.subKernels) return result; for (let i = 0; i < this.subKernels.length; i++) { const subKernel = this.subKernels[i]; if (subKernel.returnType === 'Integer') { result.push( ` gl_FragData[${i + 1}].${channel} = float(subKernelResult_${this.subKernels[i].name})` ); } else { result.push( ` gl_FragData[${i + 1}].${channel} = subKernelResult_${this.subKernels[i].name}` ); } } } getMainResultKernelNumberTexture() { return [ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ' gl_FragData[0][0] = kernelResult', ]; } getMainResultSubKernelNumberTexture() { const result = []; if (!this.subKernels) return result; for (let i = 0; i < this.subKernels.length; ++i) { const subKernel = this.subKernels[i]; if (subKernel.returnType === 'Integer') { result.push( ` gl_FragData[${i + 1}][0] = float(subKernelResult_${subKernel.name})` ); } else { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${subKernel.name}` ); } } return result; } getMainResultKernelArray2Texture() { return [ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ' gl_FragData[0][0] = kernelResult[0]', ' gl_FragData[0][1] = kernelResult[1]', ]; } getMainResultSubKernelArray2Texture() { const result = []; if (!this.subKernels) return result; for (let i = 0; i < this.subKernels.length; ++i) { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${this.subKernels[i].name}[0]`, ` gl_FragData[${i + 1}][1] = subKernelResult_${this.subKernels[i].name}[1]` ); } return result; } getMainResultKernelArray3Texture() { return [ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ' gl_FragData[0][0] = kernelResult[0]', ' gl_FragData[0][1] = kernelResult[1]', ' gl_FragData[0][2] = kernelResult[2]', ]; } getMainResultSubKernelArray3Texture() { const result = []; if (!this.subKernels) return result; for (let i = 0; i < this.subKernels.length; ++i) { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${this.subKernels[i].name}[0]`, ` gl_FragData[${i + 1}][1] = subKernelResult_${this.subKernels[i].name}[1]`, ` gl_FragData[${i + 1}][2] = subKernelResult_${this.subKernels[i].name}[2]` ); } return result; } getMainResultKernelArray4Texture() { return [ ' threadId = indexTo3D(index, uOutputDim)', ' kernel()', ' gl_FragData[0] = kernelResult', ]; } getMainResultSubKernelArray4Texture() { const result = []; if (!this.subKernels) return result; switch (this.returnType) { case 'Number': case 'Float': case 'Integer': for (let i = 0; i < this.subKernels.length; ++i) { const subKernel = this.subKernels[i]; if (subKernel.returnType === 'Integer') { result.push( ` gl_FragData[${i + 1}] = float(subKernelResult_${this.subKernels[i].name})` ); } else { result.push( ` gl_FragData[${i + 1}] = subKernelResult_${this.subKernels[i].name}` ); } } break; case 'Array(2)': for (let i = 0; i < this.subKernels.length; ++i) { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${this.subKernels[i].name}[0]`, ` gl_FragData[${i + 1}][1] = subKernelResult_${this.subKernels[i].name}[1]` ); } break; case 'Array(3)': for (let i = 0; i < this.subKernels.length; ++i) { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${this.subKernels[i].name}[0]`, ` gl_FragData[${i + 1}][1] = subKernelResult_${this.subKernels[i].name}[1]`, ` gl_FragData[${i + 1}][2] = subKernelResult_${this.subKernels[i].name}[2]` ); } break; case 'Array(4)': for (let i = 0; i < this.subKernels.length; ++i) { result.push( ` gl_FragData[${i + 1}][0] = subKernelResult_${this.subKernels[i].name}[0]`, ` gl_FragData[${i + 1}][1] = subKernelResult_${this.subKernels[i].name}[1]`, ` gl_FragData[${i + 1}][2] = subKernelResult_${this.subKernels[i].name}[2]`, ` gl_FragData[${i + 1}][3] = subKernelResult_${this.subKernels[i].name}[3]` ); } break; } return result; } /** * @param {String} src - Shader string * @param {Object} map - Variables/Constants associated with shader */ replaceArtifacts(src, map) { return src.replace(/[ ]*__([A-Z]+[0-9]*([_]?[A-Z]*[0-9]?)*)__;\n/g, (match, artifact) => { if (map.hasOwnProperty(artifact)) { return map[artifact]; } throw `unhandled artifact ${artifact}`; }); } /** * @desc Get the fragment shader String. * If the String hasn't been compiled yet, * then this method compiles it as well * * @param {Array} args - The actual parameters sent to the Kernel * @returns {string} Fragment Shader string */ getFragmentShader(args) { if (this.compiledFragmentShader !== null) { return this.compiledFragmentShader; } return this.compiledFragmentShader = this.replaceArtifacts(this.constructor.fragmentShader, this._getFragShaderArtifactMap(args)); } /** * @desc Get the vertical shader String * @param {Array|IArguments} args - The actual parameters sent to the Kernel * @returns {string} Vertical Shader string */ getVertexShader(args) { if (this.compiledVertexShader !== null) { return this.compiledVertexShader; } return this.compiledVertexShader = this.replaceArtifacts(this.constructor.vertexShader, this._getVertShaderArtifactMap(args)); } /** * @desc Returns the *pre-compiled* Kernel as a JS Object String, that can be reused. */ toString() { const setupContextString = utils.linesToString([ `const gl = context`, ]); return glKernelString(this.constructor, arguments, this, setupContextString); } destroy(removeCanvasReferences) { if (!this.context) return; if (this.buffer) { this.context.deleteBuffer(this.buffer); } if (this.framebuffer) { this.context.deleteFramebuffer(this.framebuffer); } for (const width in this.rawValueFramebuffers) { for (const height in this.rawValueFramebuffers[width]) { this.context.deleteFramebuffer(this.rawValueFramebuffers[width][height]); delete this.rawValueFramebuffers[width][height]; } delete this.rawValueFramebuffers[width]; } if (this.vertShader) { this.context.deleteShader(this.vertShader); } if (this.fragShader) { this.context.deleteShader(this.fragShader); } if (this.program) { this.context.deleteProgram(this.program); } if (this.texture) { this.texture.delete(); const textureCacheIndex = this.textureCache.indexOf(this.texture.texture); if (textureCacheIndex > -1) { this.textureCache.splice(textureCacheIndex, 1); } this.texture = null; } if (this.mappedTextures && this.mappedTextures.length) { for (let i = 0; i < this.mappedTextures.length; i++) { const mappedTexture = this.mappedTextures[i]; mappedTexture.delete(); const textureCacheIndex = this.textureCache.indexOf(mappedTexture.texture); if (textureCacheIndex > -1) { this.textureCache.splice(textureCacheIndex, 1); } } this.mappedTextures = null; } if (this.kernelArguments) { for (let i = 0; i < this.kernelArguments.length; i++) { this.kernelArguments[i].destroy(); } } if (this.kernelConstants) { for (let i = 0; i < this.kernelConstants.length; i++) { this.kernelConstants[i].destroy(); } } while (this.textureCache.length > 0) { const texture = this.textureCache.pop(); this.context.deleteTexture(texture); } if (removeCanvasReferences) { const idx = canvases.indexOf(this.canvas); if (idx >= 0) { canvases[idx] = null; maxTexSizes[idx] = null; } } this.destroyExtensions(); delete this.context; delete this.canvas; if (!this.gpu) return; const i = this.gpu.kernels.indexOf(this); if (i === -1) return; this.gpu.kernels.splice(i, 1); } destroyExtensions() { 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('WEBGL_lose_context'); if (extension) { extension.loseContext(); } } /** * @return {IKernelJSON} */ toJSON() { const json = super.toJSON(); json.functionNodes = FunctionBuilder.fromKernel(this, WebGLFunctionNode).toJSON(); json.settings.threadDim = this.threadDim; return json; } } module.exports = { WebGLKernel };