From a06ea03451f379fddc4ae9667f3a0b9130c89bd4 Mon Sep 17 00:00:00 2001 From: Scott Seaward Date: Wed, 12 Jul 2017 21:28:40 +0100 Subject: [PATCH] Improve performance of copyGLTo2D in Firefox & potentially other browsers (#4086) Why: * When filtering with webgl copying data to 2d canvas from GL context dominates the filtering time * Chrome is happy to copy data from GL with ctx.drawImage in sub-millisecond time * Firefox performs drawImage much more slowly but can do putImageData fast instead * Using putImageData results in ~ 2-3x speedup on Firefox 54 in the fabricjs.com filtering demo on my MacbookPro This change addresses the need by: * Adding a runtime check for which copy func is better performing * Swapping drawImage for putImageData if that func is detected as being faster Also submitted for review: * Log minifier error output to console if build.js minifyCmd fails (helps identify syntax errors while using build:watch) --- build.js | 1 + src/filters/webgl_backend.class.js | 113 ++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/build.js b/build.js index 53b11cc5..6e6022ae 100644 --- a/build.js +++ b/build.js @@ -270,6 +270,7 @@ else { exec(mininfierCmd, function (error, output) { if (error) { console.error('Minification failed using', minifier, 'with', mininfierCmd); + console.error('Minifier error output:\n' + error); process.exit(1); } console.log('Minified using', minifier, 'to ' + distributionPath + 'fabric.min.js'); diff --git a/src/filters/webgl_backend.class.js b/src/filters/webgl_backend.class.js index 89dfd935..ac462d56 100644 --- a/src/filters/webgl_backend.class.js +++ b/src/filters/webgl_backend.class.js @@ -60,6 +60,55 @@ this.createWebGLCanvas(width, height); // eslint-disable-next-line this.squareVertices = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.chooseFastestCopyGLTo2DMethod(width, height); + }, + + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod: function(width, height) { + var canMeasurePerf = typeof window.performance !== 'undefined'; + var canUseImageData; + try { + new ImageData(1, 1); + canUseImageData = true; + } + catch (e) { + canUseImageData = false; + } + // eslint-disable-next-line no-undef + var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; + // eslint-disable-next-line no-undef + var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + + if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { + return; + } + + var targetCanvas = fabric.util.createCanvasElement(); + // eslint-disable-next-line no-undef + var imageBuffer = new ArrayBuffer(width * height * 4); + var testContext = { imageBuffer: imageBuffer }; + var startTime, drawImageTime, putImageDataTime; + targetCanvas.width = width; + targetCanvas.height = height; + + startTime = window.performance.now(); + copyGLTo2DDrawImage.call(testContext, this.gl, targetCanvas); + drawImageTime = window.performance.now() - startTime; + + startTime = window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, targetCanvas); + putImageDataTime = window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + else { + this.copyGLTo2D = copyGLTo2DDrawImage; + } }, /** @@ -122,7 +171,7 @@ var tempFbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); - this.copyGLTo2D(gl.canvas, targetCanvas); + this.copyGLTo2D(gl, targetCanvas); gl.bindTexture(gl.TEXTURE_2D, null); gl.deleteTexture(pipelineState.sourceTexture); gl.deleteTexture(pipelineState.targetTexture); @@ -243,25 +292,6 @@ } }, - /** - * Copy an input WebGL canvas on to an output 2D canvas. - * - * The WebGL canvas is assumed to be upside down, with the top-left pixel of the - * desired output image appearing in the bottom-left corner of the WebGL canvas. - * - * @param {HTMLCanvasElement} sourceCanvas The WebGL source canvas to copy from. - * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. - */ - copyGLTo2D: function(sourceCanvas, targetCanvas) { - var ctx = targetCanvas.getContext('2d'); - ctx.translate(0, targetCanvas.height); // move it down again - ctx.scale(1, -1); // vertical flip - // where is my image on the big glcanvas? - var sourceY = sourceCanvas.height - targetCanvas.height; - ctx.drawImage(sourceCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, - targetCanvas.width, targetCanvas.height); - }, - /** * Clear out cached resources related to a source image that has been * filtered previously. @@ -275,6 +305,8 @@ } }, + copyGLTo2D: copyGLTo2DDrawImage, + /** * Attempt to extract GPU information strings from a WebGL context. * @@ -304,3 +336,44 @@ }, }; })(); + +/** + * Copy an input WebGL canvas on to an output 2D canvas. + * + * The WebGL canvas is assumed to be upside down, with the top-left pixel of the + * desired output image appearing in the bottom-left corner of the WebGL canvas. + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + */ +function copyGLTo2DDrawImage(gl, targetCanvas) { + var sourceCanvas = gl.canvas; + var ctx = targetCanvas.getContext('2d'); + ctx.translate(0, targetCanvas.height); // move it down again + ctx.scale(1, -1); // vertical flip + // where is my image on the big glcanvas? + var sourceY = sourceCanvas.height - targetCanvas.height; + ctx.drawImage(sourceCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + targetCanvas.width, targetCanvas.height); +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData + * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + */ +function copyGLTo2DPutImageData(gl, targetCanvas) { + var ctx = targetCanvas.getContext('2d'); + var width = targetCanvas.width; + var height = targetCanvas.height; + var numBytes = width * height * 4; + // eslint-disable-next-line no-undef + var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); + // eslint-disable-next-line no-undef + var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, width); + ctx.putImageData(imgData, 0, 0); +}