putImageData vs drawImage (+ more for gfx engine) (v2)

Revision 2 of this benchmark created by Unkn0wn on


Description

Compare speed of calling drawImage with fully opaque, fully transparent, fully translucent image (with 50% transparency) and putImageData.

<b>Background</b>: Anyone wishing to create a graphics engine will need to test the speed of various operations involving transparency. Full transparency (as opposed to translucency) is a common feature for rendering layers, so this benchmark tests against the different cases for that reason.

An additional benchmark of a custom transparency test prior to putImageData is included to benchmark the speed of a custom rendering engine.

<b>Results</b> show that drawImage with translucent and transparent pixels is a great deal faster than performing a custom blit with a transparency check. Also the test shows that manipulating packed RGBA values is much more efficient than accessing an interleaved array where each array index holds a single colour component.

Since Array's are faster than Typed-Arrays in Chrome, and of the typed arrays Int32 arrays are faster than Int8 arrays (* http://goo.gl/Yf2Gv ), it stands to reason that custom manipulation is best handled with packed RGBA arrays.

Preparation HTML

<canvas id="output" width="160" height="144">
</canvas>

Setup

const RED_INDEX = 0;
    const GREEN_INDEX = 1;
    const BLUE_INDEX = 2;
    const ALPHA_INDEX = 3;
    
    var outputCanvas = document.getElementById('output');
    var outputContext = outputCanvas.getContext('2d');
    var width = outputCanvas.width;
    var height = outputCanvas.height;
    var outputImageData = outputContext.createImageData(width, height);
    
    var bufferOpaque = CreateBuffer(width, height);
    var bufferTransparency = CreateBuffer(width, height);
    var bufferTranslucency = CreateBuffer(width, height);
    
    var packedTransparencyData = new Array(width * height);
    
    // Create random white noise (with 0xaa luminosity).
    {
      for (var i = 0; i < outputImageData.data.length; i += 4) {
        var value = Math.round(Math.random() * 0xaa);
        for (var j = 0; j < 3; ++j) {
          outputImageData.data[i + j] = value;
        }
        outputImageData.data[i + 3] = 255;
      }
    
      outputContext.putImageData(outputImageData, 0, 0);
    }
    
    // Create fully opaque GREEN bufferContext and bufferData
    {
      var context = bufferOpaque.context;
      var data = bufferOpaque.imageData.data;
      for (var i = 0; i < data.length; i += 4) {
        for (var j = 0; j < 3; ++j) {
          data[i + j] = 0;
        }
        data[i + GREEN_INDEX] = 255;
        data[i + ALPHA_INDEX] = 255;
      }
      context.putImageData(bufferOpaque.imageData, 0, 0);
    }
    
    // Cover with fully transparent RED (random pixels are fully transparent).
    {
      var context = bufferTransparency.context;
      var data = bufferTransparency.imageData.data;
      for (var i = 0; i < data.length; i += 4) {
        for (var j = 0; j < 3; ++j) {
          data[i + j] = 0;
        }
        data[i + RED_INDEX] = 255;
        var alpha = 0;
        if ((i % 8) == 0) {
          alpha = 255;
        }
        data[i + ALPHA_INDEX] = alpha;
    
        // Store packed data.
        packedTransparencyData[i / 4] = 0;
        for (var j = 0; j < 4; ++j) {
          packedTransparencyData[i / 4] = (packedTransparencyData[i / 4] << 8) | data[i + j];
        }
      }
      context.putImageData(bufferTransparency.imageData, 0, 0);
    }
    
    // Cover with translucency.
    {
      var context = bufferTranslucency.context;
      var data = bufferTranslucency.imageData.data;
      for (var i = 0; i < data.length; i += 4) {
        for (var j = 0; j < 3; ++j) {
          data[i + j] = 0;
        }
        data[i + BLUE_INDEX] = 255;
        var alpha = 0;
        if ((i % 8) == 0) {
          alpha = 128;
        }
        data[i + ALPHA_INDEX] = alpha;
      }
      context.putImageData(bufferTranslucency.imageData, 0, 0);
    }
    
    function CreateBuffer(width, height) {
      var buffer = new Object();
      buffer.canvas = document.createElement("canvas");
      buffer.canvas.width = width;
      buffer.canvas.height = height;
      buffer.context = buffer.canvas.getContext("2d");
      buffer.imageData = buffer.context.createImageData(width, height);
    
      return buffer;
    }
    
    // Will copy inputData to outputData
    
    
    function ManualBlit(inputImageData, outputImageData, transparencyOn) {
      var inputData = inputImageData.data;
      var outputData = outputImageData.data;
      var length = inputData.length / 4;
    
      var indexPtr = 0;
    
      if (transparencyOn) {
        for (var i = 0; i < length; ++i) {
          if (inputData[indexPtr + ALPHA_INDEX] >= 128) {
            outputData[indexPtr] = inputData[indexPtr++];
            outputData[indexPtr] = inputData[indexPtr++];
            outputData[indexPtr] = inputData[indexPtr++];
            outputData[indexPtr++] = 255; // ALPHA_INDEX
          } else {
            indexPtr += 4;
          }
        }
      } else {
        // No alpha check
        for (var i = 0; i < length; ++i) {
          outputData[indexPtr] = inputData[indexPtr++];
          outputData[indexPtr] = inputData[indexPtr++];
          outputData[indexPtr] = inputData[indexPtr++];
          outputData[indexPtr] = 255; // ALPHA_INDEX
        }
      }
    }
    
    function PackedBlit(packedImageData, outputImageData) {
      var packedPtr = 0;
      var outputPtr = 0;
      var data = outputImageData.data;
      for (var i = 0; i < packedImageData.length; ++i) {
        if ((packedValue & 0xff) > 128) {
          var packedValue = packedImageData[packedPtr];
          data[outputPtr++] = packedValue >> 24;
          data[outputPtr++] = (packedValue >> 16) & 0xff;
          data[outputPtr++] = (packedValue >> 8) & 0xff;
          data[outputPtr++] = 0xff;
        } else {
          outputPtr += 4;
        }
    
        ++packedPtr;
      }
    }

Test runner

Ready to run.

Testing in
TestOps/sec
Fully opaque, drawImage
outputContext.drawImage(bufferOpaque.canvas, 0, 0);
ready
Full opaque, putImageData
outputContext.putImageData(bufferOpaque.imageData, 0, 0);
ready
Full transparency, drawImage
outputContext.drawImage(bufferTransparency.canvas, 0, 0);
ready
Full transparency, custom operation, putImageData
ManualBlit(bufferTransparency.imageData, outputImageData, true);
outputContext.putImageData(outputImageData, 0, 0);
ready
Full transparency, custom operation, packed pixels, putPixel
PackedBlit(packedTransparencyData, outputImageData);
outputContext.putImageData(outputImageData, 0, 0);
ready
Translucency, drawImage
outputContext.drawImage(bufferTranslucency.canvas, 0, 0);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Unkn0wn on
  • Revision 2: published by Unkn0wn on