weblas
Version:
GPU accelerated BLAS for node and the browser
530 lines (397 loc) • 17 kB
JavaScript
var glslify = require('glslify');
/*
Copyright (c) 2015 Waylon Flinn
webgl.js
multiply matrices up to 4096 x 4096 on GPUs that support OES_texture_float
extension. input is encoded into the red and green channels of an input texture and
calculations are done using a custom fragment shader.
*/
/*
A WebGL context associated with a specific canvas element.
* creates a canvas
* sets up webgl context
* translates numbers into textures
* compiles shader programs for executing math (when supplied with an
operation specific fragment shader)
*/
function WebGL(options) {
var glOptions,
ext;
options = options || {};
// canvas
if(typeof options.canvas === 'undefined')
this.canvas = document.createElement('canvas');
else
this.canvas = options.canvas;
// context
glOptions = { premultipliedAlpha: false, preserveDrawingBuffer: false };
this.context = this.canvas.getContext("experimental-webgl", glOptions);
if (this.context == null)
throw new Error("No support for Webgl.");
// float texture extension
try {
ext = this.context.getExtension('OES_texture_float');
} catch(e) {
}
if (ext == null) {
console.log("No support for OES_texture_float extension!");
this.hasFloat = false;
} else {
this.hasFloat = true;
}
var highp = this.context.getShaderPrecisionFormat(this.context.FRAGMENT_SHADER, this.context.HIGH_FLOAT);
this.hasHighPrecision = highp.precision != 0;
if(this.hasHighPrecision) this.highp = highp;
// create pass through vertex shader
var passThrough = glslify('./glsl/pass_through.glsl');
this.vertexShader = this.context.createShader(this.context.VERTEX_SHADER);
this.context.shaderSource(this.vertexShader, passThrough);
this.context.compileShader(this.vertexShader);
var encode = glslify('./glsl/encode.glsl'),
transpose = glslify('./glsl/transpose.glsl'),
reshape = glslify('./glsl/reshape.glsl'),
reshape_simple = glslify('./glsl/reshape_simple.glsl'),
submatrix = glslify('./glsl/submatrix.glsl'),
combine = glslify('./glsl/combine.glsl');
this.encode_program = this.createProgram(encode);
this.transpose_program = this.createProgram(transpose);
this.reshape_program = this.createProgram(reshape);
this.reshape_simple_program = this.createProgram(reshape_simple);
this.submatrix_program = this.createProgram(submatrix);
this.combine_program = this.createProgram(combine);
};
module.exports = WebGL;
// RGBA is the standard input/ouput texture
WebGL.COMPONENTS_PER_TEXEL = 4;
WebGL.POSITION_UNIFORM_NAME = "pos";
WebGL.TEXTURE_UNIFORM_NAME = "tex";
WebGL.prototype.encode = function(M, N, texture0, out){
this.program = this.encode_program;
this.selectProgram(this.program);
var pad = this.getPad(N);
var N_gl = this.context.getUniformLocation(this.program, "N"),
pad_gl = this.context.getUniformLocation(this.program, "pad");
this.context.uniform1i(N_gl, N);
this.context.uniform1i(pad_gl, pad);
this.bindInputTexture(texture0, this.context.TEXTURE0, "A");
this.bindOutputTexture(M, N, out);
this.context.drawElements(this.context.TRIANGLES, /*num items*/6, this.context.UNSIGNED_SHORT, 0);
this.unbindInputTexture(this.context.TEXTURE0);
}
/* tranpose a texture where input has M rows and N columns
*/
WebGL.prototype.transpose = function(M, N, texture0, out){
this.program = this.transpose_program;
this.selectProgram(this.program);
var npad = this.getPad(N),
mpad = this.getPad(M);
// in the shader M and N describe rows and columns in the *output*, respectively
var N_gl = this.context.getUniformLocation(this.program, "N"),
npad_gl = this.context.getUniformLocation(this.program, "npad"),
M_gl = this.context.getUniformLocation(this.program, "M"),
mpad_gl = this.context.getUniformLocation(this.program, "mpad");
this.context.uniform1i(N_gl, M);
this.context.uniform1i(npad_gl, mpad);
this.context.uniform1i(M_gl, N);
this.context.uniform1i(mpad_gl, npad);
this.bindInputTexture(texture0, this.context.TEXTURE0, "A");
this.bindOutputTexture(N, (M + mpad)/4, out);
this.context.drawElements(this.context.TRIANGLES, /*num items*/6, this.context.UNSIGNED_SHORT, 0);
this.unbindInputTexture(this.context.TEXTURE0);
};
/* tranpose a texture where input has M rows and N columns
*/
WebGL.prototype.reshape = function(M, N, M_out, N_out, texture0, out){
var pad = this.getPad(N),
pad_out = this.getPad(N_out);
if(pad == 0 && pad_out == 0){
this.program = this.reshape_simple_program;
} else {
this.program = this.reshape_program;
console.log("# WARNING: using slow reshape shader.");
}
this.selectProgram(this.program);
// in the shader M and N describe rows and columns in the *output*, respectively
var M_gl = this.context.getUniformLocation(this.program, "M"),
N_gl = this.context.getUniformLocation(this.program, "N"),
pad_gl = this.context.getUniformLocation(this.program, "pad"),
M_in_gl = this.context.getUniformLocation(this.program, "M_in"),
N_in_gl = this.context.getUniformLocation(this.program, "N_in"),
pad_in_gl = this.context.getUniformLocation(this.program, "pad_in");
this.context.uniform1f(M_gl, M_out);
this.context.uniform1f(N_gl, N_out);
this.context.uniform1f(pad_gl, pad_out);
this.context.uniform1f(M_in_gl, M);
this.context.uniform1f(N_in_gl, N);
this.context.uniform1f(pad_in_gl, pad);
this.bindInputTexture(texture0, this.context.TEXTURE0, "A");
this.bindOutputTexture(M_out, (N_out + pad_out)/4, out);
this.context.drawElements(this.context.TRIANGLES, /*num items*/6, this.context.UNSIGNED_SHORT, 0);
this.unbindInputTexture(this.context.TEXTURE0);
};
/* extract a portion of a texture into another texture
*/
WebGL.prototype.submatrix = function(N, M_out, N_out, stride, offset, texture0, out){
this.program = this.submatrix_program;
this.selectProgram(this.program);
var pad = this.getPad(N),
pad_out = this.getPad(N_out);
// in the shader M and N describe rows and columns in the *output*, respectively
var N_gl = this.context.getUniformLocation(this.program, "N"),
pad_gl = this.context.getUniformLocation(this.program, "pad"),
N_in_gl = this.context.getUniformLocation(this.program, "N_in"),
pad_in_gl = this.context.getUniformLocation(this.program, "pad_in"),
offset_gl = this.context.getUniformLocation(this.program, "offset");
stride_gl = this.context.getUniformLocation(this.program, "stride");
this.context.uniform1f(N_gl, N_out);
this.context.uniform1f(pad_gl, pad_out);
this.context.uniform1f(N_in_gl, N);
this.context.uniform1f(pad_in_gl, pad);
this.context.uniform1f(stride_gl, stride);
this.context.uniform1f(offset_gl, offset);
this.bindInputTexture(texture0, this.context.TEXTURE0, "X");
this.bindOutputTexture(M_out, (N_out + pad_out)/4, out);
this.context.drawElements(this.context.TRIANGLES, /*num items*/6, this.context.UNSIGNED_SHORT, 0);
this.unbindInputTexture(this.context.TEXTURE0);
};
/* combine two smaller textures into a larger texture
M - input rows
N - input columns
*/
WebGL.prototype.combine = function(M, N, stride, texture0, texture1, out){
this.program = this.combine_program;
this.selectProgram(this.program);
var N_out = N * 2,
pad = this.getPad(N),
pad_out = this.getPad(N_out); // = (pad * 2) % 4
// in the shader M and N describe rows and columns in the *output*, respectively
var N_in_gl = this.context.getUniformLocation(this.program, "N_in"),
pad_in_gl = this.context.getUniformLocation(this.program, "pad_in"),
stride_gl = this.context.getUniformLocation(this.program, "stride");
this.context.uniform1f(N_in_gl, N);
this.context.uniform1f(pad_in_gl, pad);
this.context.uniform1f(stride_gl, stride);
this.bindInputTexture(texture0, this.context.TEXTURE0, "A");
this.bindInputTexture(texture1, this.context.TEXTURE1, "B");
this.bindOutputTexture(M, (N_out + pad_out)/4, out);
this.context.drawElements(this.context.TRIANGLES, /*num items*/6, this.context.UNSIGNED_SHORT, 0);
this.unbindInputTexture(this.context.TEXTURE0);
};
WebGL.prototype.bindInputTexture = function(texture, textureUnit, name){
var gl = this.context,
program = this.program;
gl.activeTexture(textureUnit); // gl.TEXTURE0, gl.TEXTURE1, etc
gl.bindTexture( gl.TEXTURE_2D, texture);
var sampler = gl.getUniformLocation(program, name);
gl.uniform1i(sampler, textureUnit - gl.TEXTURE0);
};
/* Create a shader program based on a pass through vertex shader and
the supplied operation specific fragment shader.
fragmentShaderSource - string containing the fragment shader source code.
shader will recieve `vec2 outTex` with texture coordinates from the pass
through vertex shader.
*/
WebGL.prototype.createProgram = function(fragmentShaderSource){
var gl = this.context,
fragmentShader;
// compile the provided fragment/texture shader
fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
// did it compile correctly?
if (gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) == 0)
throw new Error(gl.getShaderInfoLog(fragmentShader));
// link the program specific fragment shader and the generic pass through
// shader into a program
var program = gl.createProgram();
gl.attachShader(program, this.vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
return program;
};
WebGL.prototype.selectProgram = function(program){
var gl = this.context;
// set calculator program to current shader program
gl.useProgram(program);
this.bindVertices(program);
};
/* setup required to draw a square to our vertex shader and have
fragment shader called for each pixel
*/
WebGL.prototype.bindVertices = function(program) {
var gl = this.context,
renderer = program;
// bind vertices
var position = gl.getAttribLocation(renderer, WebGL.POSITION_UNIFORM_NAME);
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// define a square that covers the screen
var vertices = [-1.0, -1.0, 0.0, // bottom left
1.0, -1.0, 0.0, // bottom right
1.0, 1.0, 0.0, // top right
-1.0, 1.0, 0.0]; // top left
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(position, /*item size*/3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(position);
// bind texture cords
var texture = gl.getAttribLocation(renderer, WebGL.TEXTURE_UNIFORM_NAME);
var texCoords = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoords);
var textureCoords = [0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
gl.vertexAttribPointer(texture, /*item size*/2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texture);
// index to vertices
var indices = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices);
// tesselate square into triangles
// indeces into vertex array creating triangles, with counter-clockwise winding
var vertexIndices = [0, 1, 2, // bottom right triangle
0, 2, 3]; // top left triangle
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndices), gl.STATIC_DRAW);
};
/* create RGBA texture of width w/4 from given texels
padding the width of each row to a multiple of 4, where necessary.
if texels is null, an empty texture is created.
alternative to textures?
http://stackoverflow.com/questions/17203508/webgl-hardware-skinning-with-a-bone-texture
*/
WebGL.prototype.createDataTexture = function(h, w, texels){
var gl = this.context;
var PAD_TEMPLATE = [0.0, 0.0, 0.0, 0.0]; // value to pad remainder with
var rem = (w % WebGL.COMPONENTS_PER_TEXEL),
pad = rem == 0 ? 0 : WebGL.COMPONENTS_PER_TEXEL - rem;
// create the texture from our floats
var texture = gl.createTexture();
gl.bindTexture( gl.TEXTURE_2D, texture);
/*
// https://www.opengl.org/wiki/GLAPI/glPixelStore
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, w/4);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
see also: https://www.opengl.org/wiki/Common_Mistakes#Creating_a_complete_texture
*/
if(pad == 0 || texels == null || typeof texels === 'undefined'){
// no padding required, write directly from input array
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, (w + pad) / WebGL.COMPONENTS_PER_TEXEL, h, 0,
gl.RGBA, gl.FLOAT, texels);
} else {
// must pad each row
// create empty texture
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, (w + pad) / WebGL.COMPONENTS_PER_TEXEL, h, 0,
gl.RGBA, gl.FLOAT, null);
var full_texel_row_len = w - rem,
full_row_texture_width = full_texel_row_len / WebGL.COMPONENTS_PER_TEXEL;
var row_start = 0;
var last_texel = new Float32Array(PAD_TEMPLATE);
var row, remainder;
// set texture data, one row at a time, padding each row to a multiple
// of the texel length
for(var i = 0; i < h; i++){
row_start = i * w;
full_texel_row_end = row_start + full_texel_row_len;
row = new Float32Array(texels.buffer, row_start * texels.BYTES_PER_ELEMENT, full_texel_row_len);
if(full_texel_row_len > 0){
// https://www.khronos.org/registry/webgl/specs/latest/1.0/index.html#TEXSUBIMAGE2D
gl.texSubImage2D(gl.TEXTURE_2D,
0, // mip-map level
0, // x-offset
i, // y-offset
full_row_texture_width, // width
1, // height
gl.RGBA, // format
gl.FLOAT, // type
row // data
);
}
remainder = new Float32Array(texels.buffer, full_texel_row_end * texels.BYTES_PER_ELEMENT, rem);
last_texel.set(remainder); // copy remaining data
gl.texSubImage2D(gl.TEXTURE_2D,
0, // mip-map level
full_row_texture_width, // x-offset
i, // y-offset
1, // width
1, // height
gl.RGBA, // format
gl.FLOAT, // type
last_texel // data
);
}
}
// clamp to edge to support non-power of two textures
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);
// don't interpolate when getting data from texture
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// we're done with setup, so unbind current texture
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
};
/* Create a (padded) texture suitable for reading into an array with readPixels.
UNSIGNED_BYTE
Can be passed to bindDestinationTexture.
Returns an unsigned byte RGBA texture (other formats are not yet supported
on most platforms, see WEBGL_color_buffer_float extension)
*/
WebGL.prototype.createOutputTexture = function(h, w) {
var gl = this.context;
var pad = this.getPad(w);
// create and bind texture to render to
var destTexture = gl.createTexture();
//gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, destTexture);
gl.texImage2D(gl.TEXTURE_2D,/*level*/0, gl.RGBA, w + pad, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// clamp to edge to support non-power of two textures
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);
// don't interpolate when getting data from texture
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// we're done with setup, so unbind current texture
gl.bindTexture(gl.TEXTURE_2D, null);
return destTexture;
};
/* Set up output
M - number of rows in output
N - number of columns in output
dstTex - texture for holding the output
*/
WebGL.prototype.bindOutputTexture = function(M, N, texture) {
var gl = this.context;
// set canvas and viewport size
this.canvas.height = M;
this.canvas.width = N;
gl.viewport(0, 0, N, M);
// create and bind framebuffer
this.framebuffer = this.framebuffer || gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, /*level*/0);
if( gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
throw new Error("Bound framebuffer is not complete.");
return this.framebuffer;
};
WebGL.prototype.unbindInputTexture = function(textureUnit){
var gl = this.context;
gl.activeTexture(textureUnit);
gl.bindTexture(gl.TEXTURE_2D, null);
};
/* Read data out as unsigned bytes */
WebGL.prototype.readData = function(M, N){
var gl = this.context;
// create destination buffer
rawbuffer = new ArrayBuffer(M*N*Float32Array.BYTES_PER_ELEMENT);
// read the result into our buffer, as bytes
prod = new Uint8Array(rawbuffer);
gl.readPixels(0, 0, N, M, gl.RGBA, gl.UNSIGNED_BYTE, prod);
// return raw result bytes
return rawbuffer; // M x N
};
// how many extra elements do we need to fill up a pixel?
WebGL.prototype.getPad = function(N){
var rem = (N % WebGL.COMPONENTS_PER_TEXEL),
pad = rem == 0 ? 0 : WebGL.COMPONENTS_PER_TEXEL - rem;
return pad;
};